[med-svn] [apollo] 05/08: New upstream version 2.0.8

Andreas Tille tille at debian.org
Fri Dec 1 15:43:13 UTC 2017


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository apollo.

commit 2c784263aaa35e2c4739c900473481a6ecfcde25
Author: Andreas Tille <tille at debian.org>
Date:   Fri Dec 1 16:32:27 2017 +0100

    New upstream version 2.0.8
---
 .coveralls.yml                                     |     2 +
 .github/CONTRIBUTING.md                            |   309 +
 .github/ISSUE_TEMPLATE.md                          |     8 +
 .github/PULL_REQUEST_TEMPLATE.md                   |     1 +
 .github/images/githubForkButton.png                |   Bin 0 -> 17869 bytes
 .github/images/githubForkTarget.png                |   Bin 0 -> 45377 bytes
 .github/images/githubPullRequest.png               |   Bin 0 -> 10778 bytes
 .github/images/githubTestProgress.png              |   Bin 0 -> 86938 bytes
 .github/images/githubTestStatus.png                |   Bin 0 -> 38085 bytes
 .gitignore                                         |    39 +
 .jshintrc                                          |    29 +
 .travis.yml                                        |    57 +
 ChangeLog.md                                       |   648 +
 LICENSE.md                                         |    13 +
 README.md                                          |    74 +
 apollo                                             |   244 +
 application.properties                             |     6 +
 build.gradle                                       |   440 +
 build.xml                                          |   256 +
 client/apollo/css/add_sequence_alteration.css      |    34 +
 client/apollo/css/annotation_info_editor.css       |   145 +
 client/apollo/css/confirm_dialog.css               |    19 +
 client/apollo/css/custom_track_styles.css          |    73 +
 client/apollo/css/edit_comments.css                |    30 +
 client/apollo/css/edit_dbxrefs.css                 |    34 +
 client/apollo/css/export.css                       |    22 +
 client/apollo/css/flat.css                         |    29 +
 client/apollo/css/genome.css                       |    67 +
 client/apollo/css/get_history.css                  |    68 +
 client/apollo/css/get_sequence.css                 |    35 +
 client/apollo/css/information_editor_panel.css     |    57 +
 client/apollo/css/main.css                         |    20 +
 client/apollo/css/maker.css                        |   292 +
 client/apollo/css/maker_darkbackground.css         |   357 +
 client/apollo/css/maker_styles.css                 |   448 +
 client/apollo/css/search_sequence.css              |    76 +
 client/apollo/css/webapollo_track_styles.css       |   882 +
 client/apollo/img/ApolloLogo_100x36.png            |   Bin 0 -> 3071 bytes
 client/apollo/img/exclamation_circle_orange.png    |   Bin 0 -> 818 bytes
 client/apollo/img/loading.gif                      |   Bin 0 -> 673 bytes
 client/apollo/img/marker_rounded_red.png           |   Bin 0 -> 1612 bytes
 client/apollo/img/marker_squared_red.png           |   Bin 0 -> 1556 bytes
 client/apollo/img/minus-arrowhead.png              |   Bin 0 -> 2863 bytes
 client/apollo/img/plus-arrowhead.png               |   Bin 0 -> 2875 bytes
 client/apollo/img/small-minus-arrowhead.png        |   Bin 0 -> 228 bytes
 client/apollo/img/small-plus-arrowhead.png         |   Bin 0 -> 212 bytes
 client/apollo/img/user_icon_16px.png               |   Bin 0 -> 470 bytes
 client/apollo/img/warning_exclamation.png          |   Bin 0 -> 1469 bytes
 client/apollo/img/warning_exclamation_small.png    |   Bin 0 -> 1368 bytes
 client/apollo/img/webapollo_favicon.ico            |   Bin 0 -> 1150 bytes
 client/apollo/js/BioFeatureUtils.js                |    59 +
 client/apollo/js/EUtils.js                         |    81 +
 client/apollo/js/FeatureEdgeMatchManager.js        |   189 +
 client/apollo/js/FeatureSelectionManager.js        |   184 +
 client/apollo/js/FormatUtils.js                    |    25 +
 client/apollo/js/InformationEditor.js              |    72 +
 client/apollo/js/JSONUtils.js                      |   422 +
 client/apollo/js/Permission.js                     |    11 +
 client/apollo/js/SequenceFeatureDialog.js          |   170 +
 client/apollo/js/SequenceOntologyUtils.js          |   415 +
 client/apollo/js/SequenceSearch.js                 |   243 +
 client/apollo/js/Store/SeqFeature/ApolloGFF3.js    |   132 +
 client/apollo/js/Store/SeqFeature/PseudoNCList.js  |    22 +
 client/apollo/js/Store/SeqFeature/ScratchPad.js    |    75 +
 client/apollo/js/TrackConfigTransformer.js         |    57 +
 client/apollo/js/View/Dialog/Help.js               |    60 +
 client/apollo/js/View/Track/AnnotSequenceTrack.js  |    18 +
 client/apollo/js/View/Track/AnnotTrack.js          |  5934 +++++++
 client/apollo/js/View/Track/DraggableAlignments.js |   127 +
 .../apollo/js/View/Track/DraggableBLASTFeatures.js |    30 +
 .../apollo/js/View/Track/DraggableHTMLFeatures.js  |  1433 ++
 .../js/View/Track/DraggableResultFeatures.js       |    49 +
 client/apollo/js/View/Track/SequenceTrack.js       |  1239 ++
 .../apollo/js/View/Track/WebApolloAlignments2.js   |   123 +
 .../js/View/Track/WebApolloCanvasFeatures.js       |   132 +
 client/apollo/js/View/TrackList/Faceted.js         |    22 +
 client/apollo/js/View/TrackList/Hierarchical.js    |    21 +
 client/apollo/js/WebApollo.profile.js              |    64 +
 client/apollo/js/main.js                           |   754 +
 client/apollo/jslib/bbop/bbop.js                   | 16724 +++++++++++++++++++
 client/apollo/jslib/bbop/golr.js                   |  3782 +++++
 client/apollo/jslib/bbop/jquery.js                 |   331 +
 client/apollo/jslib/bbop/search_box.js             |   178 +
 client/apollo/jslib/jquery/jquery.js               |  9266 ++++++++++
 client/apollo/jslib/jqueryui/accordion.js          |   614 +
 client/apollo/jslib/jqueryui/autocomplete.js       |   627 +
 client/apollo/jslib/jqueryui/button.js             |   417 +
 client/apollo/jslib/jqueryui/core.js               |   322 +
 client/apollo/jslib/jqueryui/datepicker-af.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ar-DZ.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-ar.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-az.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-bg.js      |    27 +
 client/apollo/jslib/jqueryui/datepicker-bs.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-ca.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-cs.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-cy-GB.js   |    25 +
 client/apollo/jslib/jqueryui/datepicker-da.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-de.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-el.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-en-AU.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-en-GB.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-en-NZ.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-eo.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-es.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-et.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-eu.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-fa.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-fi.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-fo.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-fr-CH.js   |    25 +
 client/apollo/jslib/jqueryui/datepicker-fr.js      |    28 +
 client/apollo/jslib/jqueryui/datepicker-gl.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-he.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-hi.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-hr.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-hu.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-hy.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-id.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-is.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-it.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ja.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-kk.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-km.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ko.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-lb.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-lt.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-lv.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-mk.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ml.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ms.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-nl-BE.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-nl.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-no.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-pl.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-pt-BR.js   |    25 +
 client/apollo/jslib/jqueryui/datepicker-pt.js      |    24 +
 client/apollo/jslib/jqueryui/datepicker-rm.js      |    24 +
 client/apollo/jslib/jqueryui/datepicker-ro.js      |    29 +
 client/apollo/jslib/jqueryui/datepicker-ru.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-sk.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-sl.js      |    27 +
 client/apollo/jslib/jqueryui/datepicker-sq.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-sr-SR.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-sr.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-sv.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-ta.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-th.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-tj.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-tr.js      |    25 +
 client/apollo/jslib/jqueryui/datepicker-uk.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-vi.js      |    26 +
 client/apollo/jslib/jqueryui/datepicker-zh-CN.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-zh-HK.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker-zh-TW.js   |    26 +
 client/apollo/jslib/jqueryui/datepicker.js         |  1829 ++
 client/apollo/jslib/jqueryui/dialog.js             |   881 +
 client/apollo/jslib/jqueryui/draggable.js          |   828 +
 client/apollo/jslib/jqueryui/droppable.js          |   299 +
 client/apollo/jslib/jqueryui/effects/blind.js      |    52 +
 client/apollo/jslib/jqueryui/effects/bounce.js     |    81 +
 client/apollo/jslib/jqueryui/effects/clip.js       |    57 +
 client/apollo/jslib/jqueryui/effects/core.js       |   766 +
 client/apollo/jslib/jqueryui/effects/drop.js       |    53 +
 client/apollo/jslib/jqueryui/effects/explode.js    |    82 +
 client/apollo/jslib/jqueryui/effects/fade.js       |    35 +
 client/apollo/jslib/jqueryui/effects/fold.js       |    59 +
 client/apollo/jslib/jqueryui/effects/highlight.js  |    53 +
 client/apollo/jslib/jqueryui/effects/pulsate.js    |    54 +
 client/apollo/jslib/jqueryui/effects/scale.js      |   181 +
 client/apollo/jslib/jqueryui/effects/shake.js      |    60 +
 client/apollo/jslib/jqueryui/effects/slide.js      |    53 +
 client/apollo/jslib/jqueryui/effects/transfer.js   |    48 +
 client/apollo/jslib/jqueryui/mouse.js              |   165 +
 client/apollo/jslib/jqueryui/position.js           |   301 +
 client/apollo/jslib/jqueryui/progressbar.js        |   112 +
 client/apollo/jslib/jqueryui/resizable.js          |   811 +
 client/apollo/jslib/jqueryui/selectable.js         |   270 +
 client/apollo/jslib/jqueryui/slider.js             |   665 +
 client/apollo/jslib/jqueryui/sortable.js           |  1084 ++
 client/apollo/jslib/jqueryui/tabs.js               |   761 +
 .../base/images/ui-bg_flat_0_aaaaaa_40x100.png     |   Bin 0 -> 180 bytes
 .../base/images/ui-bg_flat_75_ffffff_40x100.png    |   Bin 0 -> 178 bytes
 .../base/images/ui-bg_glass_55_fbf9ee_1x400.png    |   Bin 0 -> 120 bytes
 .../base/images/ui-bg_glass_65_ffffff_1x400.png    |   Bin 0 -> 105 bytes
 .../base/images/ui-bg_glass_75_dadada_1x400.png    |   Bin 0 -> 111 bytes
 .../base/images/ui-bg_glass_75_e6e6e6_1x400.png    |   Bin 0 -> 110 bytes
 .../base/images/ui-bg_glass_95_fef1ec_1x400.png    |   Bin 0 -> 119 bytes
 .../ui-bg_highlight-soft_75_cccccc_1x100.png       |   Bin 0 -> 101 bytes
 .../themes/base/images/ui-icons_222222_256x240.png |   Bin 0 -> 4369 bytes
 .../themes/base/images/ui-icons_2e83ff_256x240.png |   Bin 0 -> 4369 bytes
 .../themes/base/images/ui-icons_454545_256x240.png |   Bin 0 -> 4369 bytes
 .../themes/base/images/ui-icons_888888_256x240.png |   Bin 0 -> 4369 bytes
 .../themes/base/images/ui-icons_cd0a0a_256x240.png |   Bin 0 -> 4369 bytes
 .../jqueryui/themes/base/jquery.ui.accordion.css   |    19 +
 .../jslib/jqueryui/themes/base/jquery.ui.all.css   |    11 +
 .../themes/base/jquery.ui.autocomplete.css         |    53 +
 .../jslib/jqueryui/themes/base/jquery.ui.base.css  |    21 +
 .../jqueryui/themes/base/jquery.ui.button.css      |    38 +
 .../jslib/jqueryui/themes/base/jquery.ui.core.css  |    38 +
 .../jqueryui/themes/base/jquery.ui.datepicker.css  |    68 +
 .../jqueryui/themes/base/jquery.ui.dialog.css      |    21 +
 .../jqueryui/themes/base/jquery.ui.progressbar.css |    11 +
 .../jqueryui/themes/base/jquery.ui.resizable.css   |    20 +
 .../jqueryui/themes/base/jquery.ui.selectable.css  |    10 +
 .../jqueryui/themes/base/jquery.ui.slider.css      |    24 +
 .../jslib/jqueryui/themes/base/jquery.ui.tabs.css  |    18 +
 .../jslib/jqueryui/themes/base/jquery.ui.theme.css |   247 +
 client/apollo/jslib/jqueryui/widget.js             |   275 +
 client/apollo/jslib/websocket/spring-websocket.js  |  2884 ++++
 client/apollo/json/annot.json                      |    81 +
 debian/changelog                                   |     5 -
 debian/compat                                      |     1 -
 debian/control                                     |    27 -
 debian/copyright                                   |    11 -
 debian/rules                                       |     6 -
 debian/upstream/metadata                           |    12 -
 debian/watch                                       |     4 -
 docs/Apollo2Build.md                               |   158 +
 docs/Architecture.md                               |   288 +
 docs/CannedElements.md                             |    72 +
 docs/ChadoExport.md                                |    73 +
 docs/Command_line.md                               |    84 +
 docs/Configure.md                                  |   558 +
 docs/Contributing.md                               |   309 +
 docs/Data_loading.md                               |   264 +
 docs/Data_loading_via_web_services.md              |   159 +
 docs/ExampleBuild.md                               |    75 +
 docs/Migration.md                                  |    99 +
 docs/NCBI_checks/Additional_Checks.txt             |    26 +
 ...mmon_Eukaryotic_Annotation_Errors_to_Avoid.docx |   Bin 0 -> 123360 bytes
 docs/Permissions.md                                |    59 +
 docs/PostgreSQLSetup.md                            |    37 +
 docs/Prerequisites.md                              |    57 +
 docs/Release.md                                    |    18 +
 docs/Setup.md                                      |   248 +
 docs/TestScript.md                                 |   260 +
 docs/Testing_notes.md                              |    74 +
 docs/Troubleshooting.md                            |   271 +
 docs/Web_services.md                               |   137 +
 docs/_config.yml                                   |     1 +
 docs/apollo-stress-tests/suite1/README.docx        |   Bin 0 -> 340584 bytes
 docs/apollo-stress-tests/suite1/README.pdf         |   Bin 0 -> 344824 bytes
 docs/apollo-stress-tests/suite1/apollo-stress.jmx  |   337 +
 .../suite1/testdata/cercap/operations.csv          |     3 +
 .../suite1/testdata/cercap/usernames.csv           |     1 +
 .../suite2/example-users-for-stress-test.csv       |    17 +
 .../suite2/jmeter-dev-blast-test-4.jmx             |  3208 ++++
 .../suite2/jmeter-dev-stress-test-1.jmx            |  1900 +++
 .../suite2/jmeter-dev-stress-test-2.jmx            |  2645 +++
 .../suite2/jmeter-dev-stress-test-3.jmx            |  3352 ++++
 .../suite2/jmeter-dev-stress-test-notes.md         |    40 +
 .../suite2/jmeter-dev-stress-test.jmx              |   324 +
 .../suite2/jmeter-stress-test-template.jmx         |   318 +
 .../suite2/jmeter-stress-test.pdf                  |   Bin 0 -> 1269326 bytes
 .../suite2/summary-rc2-2015-07-26.csv              |    49 +
 docs/architecture2.png                             |   Bin 0 -> 329693 bytes
 docs/conf.py                                       |   269 +
 docs/exts/markdowntransform.py                     |    10 +
 docs/exts/markdowntransform.pyc                    |   Bin 0 -> 737 bytes
 docs/images/1.png                                  |   Bin 0 -> 44395 bytes
 docs/images/2.png                                  |   Bin 0 -> 50348 bytes
 docs/images/3.png                                  |   Bin 0 -> 76103 bytes
 docs/images/download.png                           |   Bin 0 -> 73441 bytes
 docs/images/download_small.png                     |   Bin 0 -> 2728 bytes
 docs/index.rst                                     |    48 +
 docs/schemaupdates.graffle/data.plist              |   Bin 0 -> 15598 bytes
 docs/schemaupdates.graffle/image1.tiff             |   Bin 0 -> 20676 bytes
 docs/schemaupdates.graffle/image10.tiff            |   Bin 0 -> 288336 bytes
 docs/schemaupdates.graffle/image2.png              |   Bin 0 -> 16873 bytes
 docs/schemaupdates.graffle/image3.png              |   Bin 0 -> 52412 bytes
 docs/schemaupdates.graffle/image4.png              |   Bin 0 -> 9964 bytes
 docs/schemaupdates.graffle/image5.png              |   Bin 0 -> 20846 bytes
 docs/schemaupdates.graffle/image7.tiff             |   Bin 0 -> 7964 bytes
 docs/schemaupdates.graffle/image8.tiff             |   Bin 0 -> 86088 bytes
 docs/schemaupdates.graffle/image9.tiff             |   Bin 0 -> 7964 bytes
 docs/schemaupdates.pdf                             |   Bin 0 -> 16378 bytes
 docs/undoredotranscript.graffle                    |   Bin 0 -> 2982 bytes
 .../examples/groovy/Apollo1Operations.groovy       |    55 +
 .../examples/groovy/Apollo2Operations.groovy       |    94 +
 .../examples/groovy/SampleFeatures.groovy          |    30 +
 .../examples/groovy/add_organism.groovy            |    85 +
 docs/web_services/examples/groovy/add_users.groovy |   117 +
 .../examples/groovy/alter_group_permissions.groovy |   164 +
 .../alter_user_groups_and_permissions.groovy       |   228 +
 .../groovy/delete_annotations_from_organism.groovy |    78 +
 .../groovy/export_annotations_to_chado.groovy      |    79 +
 docs/web_services/examples/groovy/get_fasta.groovy |    99 +
 docs/web_services/examples/groovy/get_gff3.groovy  |    82 +
 .../examples/groovy/get_new_features.groovy        |    93 +
 .../examples/groovy/get_usersinwa1.groovy          |    44 +
 .../examples/groovy/migrate_annotations1to2.groovy |   104 +
 docs/web_services/examples/groovy/stress_suite.sh  |    22 +
 .../examples/groovy/stress_test.groovy             |   122 +
 .../examples/groovy/test_user_file.csv             |     4 +
 .../groovy/transfer_annotations2to2.groovy         |   222 +
 .../web_services/examples/perl/test_add_feature.pl |    76 +
 .../examples/perl/test_remove_features.pl          |    84 +
 docs/web_services/examples/shell/addAttribute.sh   |    24 +
 docs/web_services/examples/shell/addComment.sh     |    23 +
 docs/web_services/examples/shell/deleteFeature.sh  |    21 +
 docs/web_services/examples/shell/doLogin.sh        |    19 +
 docs/web_services/examples/shell/exportFeatures.sh |    22 +
 .../examples/shell/findAllOrganisms.sh             |    20 +
 docs/web_services/examples/shell/getFeatures.sh    |    26 +
 docs/web_services/examples/shell/listStatus.sh     |    24 +
 docs/web_services/examples/shell/loadGroups.sh     |    27 +
 docs/web_services/examples/shell/loadUsers.sh      |    25 +
 docs/web_services/examples/shell/sample_rest.sh    |     4 +
 docs/web_services/examples/shell/setStatus.sh      |    25 +
 gradle/wrapper/gradle-wrapper.jar                  |   Bin 0 -> 53638 bytes
 gradle/wrapper/gradle-wrapper.properties           |     6 +
 gradlew                                            |   160 +
 gradlew.bat                                        |    90 +
 .../assets/images/ApolloLogo-letter-2-small_0.png  |   Bin 0 -> 3673 bytes
 grails-app/assets/images/ApolloLogo_100x36.png     |   Bin 0 -> 3071 bytes
 .../assets/images/apple-touch-icon-retina.png      |   Bin 0 -> 14986 bytes
 grails-app/assets/images/apple-touch-icon.png      |   Bin 0 -> 5434 bytes
 grails-app/assets/images/favicon.ico               |   Bin 0 -> 10134 bytes
 grails-app/assets/images/grails_logo.png           |   Bin 0 -> 10172 bytes
 grails-app/assets/images/skin/database_add.png     |   Bin 0 -> 658 bytes
 grails-app/assets/images/skin/database_delete.png  |   Bin 0 -> 659 bytes
 grails-app/assets/images/skin/database_edit.png    |   Bin 0 -> 767 bytes
 grails-app/assets/images/skin/database_save.png    |   Bin 0 -> 755 bytes
 grails-app/assets/images/skin/database_table.png   |   Bin 0 -> 726 bytes
 grails-app/assets/images/skin/exclamation.png      |   Bin 0 -> 701 bytes
 grails-app/assets/images/skin/house.png            |   Bin 0 -> 806 bytes
 grails-app/assets/images/skin/information.png      |   Bin 0 -> 778 bytes
 grails-app/assets/images/skin/shadow.jpg           |   Bin 0 -> 300 bytes
 grails-app/assets/images/skin/sorted_asc.gif       |   Bin 0 -> 835 bytes
 grails-app/assets/images/skin/sorted_desc.gif      |   Bin 0 -> 834 bytes
 grails-app/assets/images/spinner.gif               |   Bin 0 -> 2037 bytes
 grails-app/assets/images/springsource.png          |   Bin 0 -> 9109 bytes
 grails-app/assets/images/webapollo_favicon.ico     |   Bin 0 -> 1150 bytes
 .../assets/javascripts/WebServicesController.js    |    15 +
 grails-app/assets/javascripts/annotator/app.js     |    35 +
 .../javascripts/annotator/controllers/Annotator.js |    17 +
 .../javascripts/annotator/directives/Directive.js  |     1 +
 .../javascripts/annotator/filters/filter1.js       |     1 +
 .../assets/javascripts/annotator/models/Feature.js |     1 +
 .../javascripts/annotator/partials/Form.html       |     0
 .../javascripts/annotator/services/Service.js      |     1 +
 grails-app/assets/javascripts/application.js       |    22 +
 grails-app/assets/javascripts/oldlook.js           |    21 +
 grails-app/assets/javascripts/selectTracks.js      |    48 +
 .../javascripts/vendor/angular-1.3.17.min.js       |   253 +
 .../assets/javascripts/vendor/angular-route.js     |   920 +
 .../assets/javascripts/vendor/angular-strap.min.js |    10 +
 .../javascripts/vendor/angular-strap.tpl.min.js    |     8 +
 .../assets/javascripts/vendor/angular.min.js       |   294 +
 .../assets/javascripts/vendor/angular.min.js.map   |     8 +
 .../assets/javascripts/vendor/jquery-1.11.1.min.js |     4 +
 .../external/jquery/jquery.js                      |  9789 +++++++++++
 .../ui-bg_diagonals-thick_18_b81900_40x40.png      |   Bin 0 -> 418 bytes
 .../ui-bg_diagonals-thick_20_666666_40x40.png      |   Bin 0 -> 312 bytes
 .../images/ui-bg_flat_10_000000_40x100.png         |   Bin 0 -> 205 bytes
 .../images/ui-bg_glass_100_f6f6f6_1x400.png        |   Bin 0 -> 262 bytes
 .../images/ui-bg_glass_100_fdf5ce_1x400.png        |   Bin 0 -> 348 bytes
 .../images/ui-bg_glass_65_ffffff_1x400.png         |   Bin 0 -> 207 bytes
 .../images/ui-bg_gloss-wave_35_f6a828_500x100.png  |   Bin 0 -> 5815 bytes
 .../ui-bg_highlight-soft_100_eeeeee_1x100.png      |   Bin 0 -> 278 bytes
 .../ui-bg_highlight-soft_75_ffe45c_1x100.png       |   Bin 0 -> 328 bytes
 .../images/ui-icons_222222_256x240.png             |   Bin 0 -> 6922 bytes
 .../images/ui-icons_228ef1_256x240.png             |   Bin 0 -> 4549 bytes
 .../images/ui-icons_ef8c08_256x240.png             |   Bin 0 -> 4549 bytes
 .../images/ui-icons_ffd27a_256x240.png             |   Bin 0 -> 4549 bytes
 .../images/ui-icons_ffffff_256x240.png             |   Bin 0 -> 6299 bytes
 .../vendor/jquery-ui-1.11.2.custom/index.html      |   513 +
 .../vendor/jquery-ui-1.11.2.custom/jquery-ui.css   |  1225 ++
 .../vendor/jquery-ui-1.11.2.custom/jquery-ui.js    | 16582 ++++++++++++++++++
 .../jquery-ui-1.11.2.custom/jquery-ui.min.css      |     7 +
 .../jquery-ui-1.11.2.custom/jquery-ui.min.js       |    13 +
 .../jquery-ui.structure.css                        |   833 +
 .../jquery-ui.structure.min.css                    |     5 +
 .../jquery-ui-1.11.2.custom/jquery-ui.theme.css    |   410 +
 .../jquery-ui.theme.min.css                        |     5 +
 .../images/ui-bg_flat_0_aaaaaa_40x100.png          |   Bin 0 -> 180 bytes
 .../images/ui-bg_flat_75_ffffff_40x100.png         |   Bin 0 -> 178 bytes
 .../images/ui-bg_glass_55_fbf9ee_1x400.png         |   Bin 0 -> 120 bytes
 .../images/ui-bg_glass_65_ffffff_1x400.png         |   Bin 0 -> 105 bytes
 .../images/ui-bg_glass_75_dadada_1x400.png         |   Bin 0 -> 111 bytes
 .../images/ui-bg_glass_75_e6e6e6_1x400.png         |   Bin 0 -> 110 bytes
 .../images/ui-bg_glass_95_fef1ec_1x400.png         |   Bin 0 -> 119 bytes
 .../ui-bg_highlight-soft_75_cccccc_1x100.png       |   Bin 0 -> 101 bytes
 .../images/ui-icons_222222_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_2e83ff_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_454545_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_888888_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_cd0a0a_256x240.png             |   Bin 0 -> 4369 bytes
 .../vendor/jquery-ui-menubar/jquery-1.8.2.js       |  9440 +++++++++++
 .../jquery-ui-menubar/jquery.ui.accordion.css      |    16 +
 .../vendor/jquery-ui-menubar/jquery.ui.all.css     |    12 +
 .../jquery-ui-menubar/jquery.ui.autocomplete.css   |    18 +
 .../vendor/jquery-ui-menubar/jquery.ui.base.css    |    26 +
 .../vendor/jquery-ui-menubar/jquery.ui.button.css  |    40 +
 .../vendor/jquery-ui-menubar/jquery.ui.button.js   |   415 +
 .../vendor/jquery-ui-menubar/jquery.ui.core.css    |    39 +
 .../vendor/jquery-ui-menubar/jquery.ui.core.js     |   343 +
 .../jquery-ui-menubar/jquery.ui.datepicker.css     |    67 +
 .../vendor/jquery-ui-menubar/jquery.ui.dialog.css  |    22 +
 .../vendor/jquery-ui-menubar/jquery.ui.dialog.js   |   847 +
 .../vendor/jquery-ui-menubar/jquery.ui.menu.css    |    30 +
 .../vendor/jquery-ui-menubar/jquery.ui.menu.js     |   628 +
 .../vendor/jquery-ui-menubar/jquery.ui.menubar.css |    15 +
 .../vendor/jquery-ui-menubar/jquery.ui.menubar.js  |   327 +
 .../vendor/jquery-ui-menubar/jquery.ui.position.js |   517 +
 .../jquery-ui-menubar/jquery.ui.progressbar.css    |    12 +
 .../jquery-ui-menubar/jquery.ui.resizable.css      |    21 +
 .../jquery-ui-menubar/jquery.ui.selectable.css     |    11 +
 .../vendor/jquery-ui-menubar/jquery.ui.slider.css  |    25 +
 .../vendor/jquery-ui-menubar/jquery.ui.spinner.css |    24 +
 .../vendor/jquery-ui-menubar/jquery.ui.tabs.css    |    18 +
 .../vendor/jquery-ui-menubar/jquery.ui.theme.css   |   247 +
 .../vendor/jquery-ui-menubar/jquery.ui.tooltip.css |    22 +
 .../vendor/jquery-ui-menubar/jquery.ui.widget.js   |   502 +
 grails-app/assets/javascripts/vendor/raf/index.js  |    31 +
 .../javascripts/vendor/ui-bootstrap-0.11.0.min.js  |     9 +
 .../vendor/ui-bootstrap-custom-0.13.4-csp.css      |     6 +
 .../vendor/ui-bootstrap-custom-0.13.4.js           |  5450 ++++++
 .../vendor/ui-bootstrap-custom-0.13.4.min.js       |    10 +
 .../vendor/ui-bootstrap-custom-tpls-0.13.4.js      |  5836 +++++++
 .../vendor/ui-bootstrap-custom-tpls-0.13.4.min.js  |    10 +
 grails-app/assets/javascripts/vendor/ui-layout.js  |   123 +
 .../assets/javascripts/vendor/ui-layout.min.js     |     7 +
 grails-app/assets/stylesheets/annotator.css        |   126 +
 grails-app/assets/stylesheets/application.css      |    55 +
 grails-app/assets/stylesheets/errors.css           |   109 +
 grails-app/assets/stylesheets/login.css            |    22 +
 grails-app/assets/stylesheets/main.css             |   649 +
 grails-app/assets/stylesheets/mobile.css           |    82 +
 grails-app/assets/stylesheets/oldlook.css          |   171 +
 grails-app/assets/stylesheets/search_sequence.css  |    78 +
 .../stylesheets/ui-bootstrap-custom-0.13.4-csp.css |     6 +
 grails-app/assets/stylesheets/ui-layout.css        |    49 +
 .../assets/stylesheets/web_api_stylesheet.css      |    39 +
 grails-app/conf/AppNavigation.groovy               |    26 +
 grails-app/conf/BootStrap.groovy                   |    60 +
 grails-app/conf/BuildConfig.groovy                 |   191 +
 grails-app/conf/Config.groovy                      |   435 +
 grails-app/conf/DataSource.groovy                  |   107 +
 grails-app/conf/UrlMappings.groovy                 |   132 +
 .../conf/org/bbop/apollo/SecurityFilters.groovy    |   100 +
 grails-app/conf/spring/resources.groovy            |     3 +
 .../bbop/apollo/AbstractApolloController.groovy    |    48 +
 .../bbop/apollo/AnnotationEditorController.groovy  |  1291 ++
 .../org/bbop/apollo/AnnotatorController.groovy     |   507 +
 .../org/bbop/apollo/AuthController.groovy          |    97 +
 .../bbop/apollo/AvailableStatusController.groovy   |   329 +
 .../org/bbop/apollo/CannedCommentController.groovy |   335 +
 .../org/bbop/apollo/CannedKeyController.groovy     |   337 +
 .../org/bbop/apollo/CannedValueController.groovy   |   334 +
 .../org/bbop/apollo/FeatureEventController.groovy  |   301 +
 .../org/bbop/apollo/FeatureTypeController.groovy   |   113 +
 .../org/bbop/apollo/GroupController.groovy         |   377 +
 .../org/bbop/apollo/HomeController.groovy          |   127 +
 .../org/bbop/apollo/IOServiceController.groovy     |   251 +
 .../org/bbop/apollo/JbrowseController.groovy       |   575 +
 .../org/bbop/apollo/LoginController.groovy         |   239 +
 .../bbop/apollo/NcbiProxyServiceController.groovy  |    46 +
 .../org/bbop/apollo/OrganismController.groovy      |  1087 ++
 .../org/bbop/apollo/ProxyController.groovy         |   152 +
 .../org/bbop/apollo/SequenceController.groovy      |   380 +
 .../org/bbop/apollo/TrackController.groovy         |   309 +
 .../org/bbop/apollo/UserController.groovy          |   578 +
 .../org/bbop/apollo/WebServicesController.groovy   |    15 +
 grails-app/domain/org/bbop/apollo/Analysis.groovy  |    65 +
 .../domain/org/bbop/apollo/AnalysisFeature.groovy  |    46 +
 .../domain/org/bbop/apollo/AnalysisProperty.groovy |    38 +
 .../domain/org/bbop/apollo/AvailableStatus.groovy  |    17 +
 .../apollo/AvailableStatusOrganismFilter.groovy    |    10 +
 .../domain/org/bbop/apollo/BiologicalRegion.groovy |    17 +
 grails-app/domain/org/bbop/apollo/CDS.groovy       |    15 +
 grails-app/domain/org/bbop/apollo/CV.groovy        |    13 +
 grails-app/domain/org/bbop/apollo/CVTerm.groovy    |    68 +
 .../domain/org/bbop/apollo/CVTermPath.groovy       |    45 +
 .../org/bbop/apollo/CVTermRelationship.groovy      |    39 +
 .../domain/org/bbop/apollo/CannedComment.groovy    |    16 +
 .../bbop/apollo/CannedCommentOrganismFilter.groovy |    10 +
 grails-app/domain/org/bbop/apollo/CannedKey.groovy |    16 +
 .../org/bbop/apollo/CannedKeyOrganismFilter.groovy |    10 +
 .../domain/org/bbop/apollo/CannedValue.groovy      |    16 +
 .../bbop/apollo/CannedValueOrganismFilter.groovy   |    10 +
 .../domain/org/bbop/apollo/Chromosome.groovy       |    14 +
 grails-app/domain/org/bbop/apollo/Comment.groovy   |    10 +
 grails-app/domain/org/bbop/apollo/Contig.groovy    |    15 +
 .../org/bbop/apollo/CustomDomainMapping.groovy     |    15 +
 .../domain/org/bbop/apollo/CustomFeature.groovy    |    66 +
 .../domain/org/bbop/apollo/CustomTranscript.groovy |    67 +
 grails-app/domain/org/bbop/apollo/DB.groovy        |    44 +
 grails-app/domain/org/bbop/apollo/DBXref.groovy    |    66 +
 .../domain/org/bbop/apollo/DBXrefProperty.groovy   |    40 +
 .../domain/org/bbop/apollo/DataAdapter.groovy      |    40 +
 grails-app/domain/org/bbop/apollo/Deletion.groovy  |    19 +
 .../domain/org/bbop/apollo/Environment.groovy      |    42 +
 .../org/bbop/apollo/EnvironmentCVTerm.groovy       |    35 +
 grails-app/domain/org/bbop/apollo/Exon.groovy      |    16 +
 grails-app/domain/org/bbop/apollo/Feature.groovy   |   167 +
 .../domain/org/bbop/apollo/FeatureAttribute.groovy |    14 +
 .../domain/org/bbop/apollo/FeatureCVTerm.groovy    |    50 +
 .../domain/org/bbop/apollo/FeatureEvent.groovy     |    63 +
 .../domain/org/bbop/apollo/FeatureGenotype.groovy  |    49 +
 .../org/bbop/apollo/FeatureLazyResidues.groovy     |    16 +
 .../domain/org/bbop/apollo/FeatureLocation.groovy  |    86 +
 .../domain/org/bbop/apollo/FeatureProperty.groovy  |    59 +
 .../org/bbop/apollo/FeatureRelationship.groovy     |    52 +
 .../domain/org/bbop/apollo/FeatureSynonym.groovy   |    45 +
 .../domain/org/bbop/apollo/FeatureType.groovy      |    12 +
 .../domain/org/bbop/apollo/Frameshift.groovy       |    33 +
 grails-app/domain/org/bbop/apollo/Gene.groovy      |    19 +
 .../domain/org/bbop/apollo/GeneMemberRegion.groovy |    14 +
 grails-app/domain/org/bbop/apollo/Genotype.groovy  |    51 +
 .../org/bbop/apollo/GroupOrganismPermission.groovy |    31 +
 .../domain/org/bbop/apollo/GroupPermission.groovy  |    13 +
 .../org/bbop/apollo/GroupTrackPermission.groovy    |    13 +
 grails-app/domain/org/bbop/apollo/Insertion.groovy |    17 +
 grails-app/domain/org/bbop/apollo/Intron.groovy    |    15 +
 grails-app/domain/org/bbop/apollo/MRNA.groovy      |    11 +
 grails-app/domain/org/bbop/apollo/Match.groovy     |    76 +
 grails-app/domain/org/bbop/apollo/MiRNA.groovy     |    11 +
 .../domain/org/bbop/apollo/Minus1Frameshift.groovy |    24 +
 .../domain/org/bbop/apollo/Minus2Frameshift.groovy |    24 +
 grails-app/domain/org/bbop/apollo/NcRNA.groovy     |    11 +
 .../apollo/NonCanonicalFivePrimeSpliceSite.groovy  |    15 +
 .../apollo/NonCanonicalThreePrimeSpliceSite.groovy |    14 +
 grails-app/domain/org/bbop/apollo/Operation.groovy |    36 +
 grails-app/domain/org/bbop/apollo/Organism.groovy  |    76 +
 .../domain/org/bbop/apollo/OrganismDBXref.groovy   |    35 +
 .../domain/org/bbop/apollo/OrganismFilter.groovy   |    10 +
 .../domain/org/bbop/apollo/OrganismProperty.groovy |    54 +
 grails-app/domain/org/bbop/apollo/PartOf.groovy    |    10 +
 .../domain/org/bbop/apollo/Permission.groovy       |    16 +
 grails-app/domain/org/bbop/apollo/Phenotype.groovy |    56 +
 .../org/bbop/apollo/PhenotypeDescription.groovy    |    44 +
 .../org/bbop/apollo/PhenotypeStatement.groovy      |    46 +
 .../domain/org/bbop/apollo/Plus1Frameshift.groovy  |    24 +
 .../domain/org/bbop/apollo/Plus2Frameshift.groovy  |    24 +
 .../domain/org/bbop/apollo/Preference.groovy       |    18 +
 grails-app/domain/org/bbop/apollo/Proxy.groovy     |    24 +
 .../domain/org/bbop/apollo/Pseudogene.groovy       |    16 +
 .../domain/org/bbop/apollo/Publication.groovy      |    90 +
 .../org/bbop/apollo/PublicationAuthor.groovy       |    47 +
 .../org/bbop/apollo/PublicationDBXref.groovy       |    40 +
 .../org/bbop/apollo/PublicationRelationship.groovy |    37 +
 grails-app/domain/org/bbop/apollo/RRNA.groovy      |    11 +
 grails-app/domain/org/bbop/apollo/Region.groovy    |    16 +
 .../domain/org/bbop/apollo/RepeatRegion.groovy     |    14 +
 grails-app/domain/org/bbop/apollo/Role.groovy      |    15 +
 .../domain/org/bbop/apollo/SearchTool.groovy       |    19 +
 grails-app/domain/org/bbop/apollo/Sequence.groovy  |    39 +
 .../org/bbop/apollo/SequenceAlteration.groovy      |    24 +
 .../org/bbop/apollo/SequenceAttribute.groovy       |    14 +
 .../domain/org/bbop/apollo/SequenceCache.groovy    |    32 +
 .../domain/org/bbop/apollo/SequenceChunk.groovy    |    16 +
 .../domain/org/bbop/apollo/SequenceFeature.groovy  |    14 +
 .../domain/org/bbop/apollo/ServerData.groovy       |    21 +
 grails-app/domain/org/bbop/apollo/SnRNA.groovy     |    12 +
 grails-app/domain/org/bbop/apollo/SnoRNA.groovy    |    11 +
 .../domain/org/bbop/apollo/SpliceSite.groovy       |    13 +
 grails-app/domain/org/bbop/apollo/Status.groovy    |    11 +
 .../org/bbop/apollo/StopCodonReadThrough.groovy    |    19 +
 .../domain/org/bbop/apollo/Substitution.groovy     |    14 +
 .../domain/org/bbop/apollo/SuperContig.groovy      |    13 +
 grails-app/domain/org/bbop/apollo/Synonym.groovy   |    39 +
 grails-app/domain/org/bbop/apollo/TRNA.groovy      |    12 +
 .../domain/org/bbop/apollo/TrackCache.groovy       |    36 +
 .../domain/org/bbop/apollo/Transcript.groovy       |    16 +
 .../org/bbop/apollo/TranscriptAttribute.groovy     |    14 +
 .../domain/org/bbop/apollo/TranscriptRegion.groovy |    13 +
 .../org/bbop/apollo/TransposableElement.groovy     |    14 +
 grails-app/domain/org/bbop/apollo/User.groovy      |    75 +
 grails-app/domain/org/bbop/apollo/UserGroup.groovy |    14 +
 .../org/bbop/apollo/UserOrganismPermission.groovy  |    32 +
 .../org/bbop/apollo/UserOrganismPreference.groovy  |    41 +
 .../domain/org/bbop/apollo/UserPermission.groovy   |    15 +
 .../domain/org/bbop/apollo/UserPreference.groovy   |    10 +
 .../org/bbop/apollo/UserTrackPermission.groovy     |    13 +
 grails-app/domain/org/gmod/chado/Chadoprop.groovy  |    24 +
 grails-app/domain/org/gmod/chado/Cv.groovy         |    22 +
 grails-app/domain/org/gmod/chado/Cvprop.groovy     |    25 +
 grails-app/domain/org/gmod/chado/Cvterm.groovy     |   157 +
 .../domain/org/gmod/chado/CvtermDbxref.groovy      |    16 +
 .../org/gmod/chado/CvtermRelationship.groovy       |    16 +
 grails-app/domain/org/gmod/chado/Cvtermpath.groovy |    23 +
 grails-app/domain/org/gmod/chado/Cvtermprop.groovy |    22 +
 .../domain/org/gmod/chado/Cvtermsynonym.groovy     |    21 +
 grails-app/domain/org/gmod/chado/Db.groovy         |    24 +
 grails-app/domain/org/gmod/chado/Dbxref.groovy     |    48 +
 grails-app/domain/org/gmod/chado/Dbxrefprop.groovy |    22 +
 grails-app/domain/org/gmod/chado/Eimage.groovy     |    21 +
 .../domain/org/gmod/chado/Environment.groovy       |    30 +
 .../domain/org/gmod/chado/EnvironmentCvterm.groovy |    15 +
 grails-app/domain/org/gmod/chado/Expression.groovy |    26 +
 .../domain/org/gmod/chado/ExpressionCvterm.groovy  |    18 +
 .../org/gmod/chado/ExpressionCvtermprop.groovy     |    23 +
 .../domain/org/gmod/chado/ExpressionImage.groovy   |    15 +
 .../domain/org/gmod/chado/ExpressionPub.groovy     |    15 +
 .../domain/org/gmod/chado/Expressionprop.groovy    |    23 +
 grails-app/domain/org/gmod/chado/Feature.groovy    |    81 +
 .../domain/org/gmod/chado/FeatureContains.groovy   |    38 +
 .../domain/org/gmod/chado/FeatureCvterm.groovy     |    26 +
 .../org/gmod/chado/FeatureCvtermDbxref.groovy      |    15 +
 .../domain/org/gmod/chado/FeatureCvtermPub.groovy  |    15 +
 .../domain/org/gmod/chado/FeatureCvtermprop.groovy |    23 +
 .../domain/org/gmod/chado/FeatureDbxref.groovy     |    16 +
 .../domain/org/gmod/chado/FeatureExpression.groovy |    17 +
 .../org/gmod/chado/FeatureExpressionprop.groovy    |    23 +
 .../domain/org/gmod/chado/FeatureGenotype.groovy   |    24 +
 .../org/gmod/chado/FeatureIntersection.groovy      |    58 +
 .../domain/org/gmod/chado/FeaturePhenotype.groovy  |    15 +
 grails-app/domain/org/gmod/chado/FeaturePub.groovy |    16 +
 .../domain/org/gmod/chado/FeaturePubprop.groovy    |    23 +
 .../org/gmod/chado/FeatureRelationship.groovy      |    26 +
 .../org/gmod/chado/FeatureRelationshipPub.groovy   |    15 +
 .../org/gmod/chado/FeatureRelationshipprop.groovy  |    24 +
 .../gmod/chado/FeatureRelationshippropPub.groovy   |    15 +
 .../domain/org/gmod/chado/FeatureSynonym.groovy    |    18 +
 grails-app/domain/org/gmod/chado/Featureloc.groovy |    35 +
 .../domain/org/gmod/chado/FeaturelocPub.groovy     |    15 +
 grails-app/domain/org/gmod/chado/Featuremap.groovy |    24 +
 .../domain/org/gmod/chado/FeaturemapPub.groovy     |    15 +
 grails-app/domain/org/gmod/chado/Featurepos.groovy |    21 +
 .../domain/org/gmod/chado/Featureprop.groovy       |    24 +
 .../domain/org/gmod/chado/FeaturepropPub.groovy    |    15 +
 .../domain/org/gmod/chado/Featurerange.groovy      |    26 +
 .../domain/org/gmod/chado/FeaturesetMeets.groovy   |    38 +
 grails-app/domain/org/gmod/chado/Genotype.groovy   |    35 +
 .../domain/org/gmod/chado/Genotypeprop.groovy      |    24 +
 grails-app/domain/org/gmod/chado/Organism.groovy   |    36 +
 .../domain/org/gmod/chado/OrganismDbxref.groovy    |    15 +
 .../org/gmod/chado/OrganismFeatureCount.groovy     |    58 +
 .../domain/org/gmod/chado/Organismprop.groovy      |    23 +
 grails-app/domain/org/gmod/chado/Phendesc.groovy   |    18 +
 grails-app/domain/org/gmod/chado/Phenotype.groovy  |    41 +
 .../org/gmod/chado/PhenotypeComparison.groovy      |    22 +
 .../gmod/chado/PhenotypeComparisonCvterm.groovy    |    17 +
 .../domain/org/gmod/chado/PhenotypeCvterm.groovy   |    21 +
 .../domain/org/gmod/chado/Phenstatement.groovy     |    18 +
 grails-app/domain/org/gmod/chado/Phylonode.groovy  |    42 +
 .../domain/org/gmod/chado/PhylonodeDbxref.groovy   |    15 +
 .../domain/org/gmod/chado/PhylonodeOrganism.groovy |    15 +
 .../domain/org/gmod/chado/PhylonodePub.groovy      |    15 +
 .../org/gmod/chado/PhylonodeRelationship.groovy    |    22 +
 .../domain/org/gmod/chado/Phylonodeprop.groovy     |    22 +
 grails-app/domain/org/gmod/chado/Phylotree.groovy  |    27 +
 .../domain/org/gmod/chado/PhylotreePub.groovy      |    15 +
 grails-app/domain/org/gmod/chado/Pub.groovy        |    89 +
 grails-app/domain/org/gmod/chado/PubDbxref.groovy  |    16 +
 .../domain/org/gmod/chado/PubRelationship.groovy   |    16 +
 grails-app/domain/org/gmod/chado/Pubauthor.groovy  |    28 +
 grails-app/domain/org/gmod/chado/Pubprop.groovy    |    22 +
 grails-app/domain/org/gmod/chado/Synonym.groovy    |    26 +
 grails-app/i18n/messages.properties                |    55 +
 grails-app/i18n/messages_cs_CZ.properties          |    55 +
 grails-app/i18n/messages_da.properties             |    56 +
 grails-app/i18n/messages_de.properties             |    55 +
 grails-app/i18n/messages_es.properties             |    55 +
 grails-app/i18n/messages_fr.properties             |    19 +
 grails-app/i18n/messages_it.properties             |    55 +
 grails-app/i18n/messages_ja.properties             |    55 +
 grails-app/i18n/messages_nb.properties             |    56 +
 grails-app/i18n/messages_nl.properties             |    55 +
 grails-app/i18n/messages_pl.properties             |    59 +
 grails-app/i18n/messages_pt_BR.properties          |    59 +
 grails-app/i18n/messages_pt_PT.properties          |    34 +
 grails-app/i18n/messages_ru.properties             |    31 +
 grails-app/i18n/messages_sv.properties             |    55 +
 grails-app/i18n/messages_th.properties             |    55 +
 grails-app/i18n/messages_zh_CN.properties          |    18 +
 grails-app/i18n/shiro.properties                   |     1 +
 .../org/bbop/apollo/CleanupPreferencesJob.groovy   |    18 +
 .../jobs/org/bbop/apollo/PhoneHomeJob.groovy       |    20 +
 grails-app/migrations/changelog-2_0_0.groovy       |  2192 +++
 grails-app/migrations/changelog-2_0_1.groovy       |   117 +
 grails-app/migrations/changelog-2_0_2.groovy       |    12 +
 grails-app/migrations/changelog-2_0_3.groovy       |    24 +
 grails-app/migrations/changelog-2_0_7.groovy       |     7 +
 grails-app/migrations/changelog-2_0_8.groovy       |     7 +
 grails-app/migrations/changelog.groovy             |     7 +
 grails-app/realms/org/bbop/apollo/DbRealm.groovy   |   132 +
 .../org/bbop/apollo/AnnotationEditorService.groovy |    20 +
 .../org/bbop/apollo/AnnotatorService.groovy        |    92 +
 .../services/org/bbop/apollo/CdsService.groovy     |   169 +
 .../org/bbop/apollo/ChadoHandlerService.groovy     |  1459 ++
 .../org/bbop/apollo/ConfigWrapperService.groovy    |   129 +
 .../bbop/apollo/ConfigurableFeatureService.groovy  |    61 +
 .../services/org/bbop/apollo/CvTermService.groovy  |    54 +
 .../org/bbop/apollo/DomainMarshallerService.groovy |    88 +
 .../services/org/bbop/apollo/ExonService.groovy    |   481 +
 .../org/bbop/apollo/FastaHandlerService.groovy     |   264 +
 .../org/bbop/apollo/FeatureEventService.groovy     |   910 +
 .../org/bbop/apollo/FeaturePropertyService.groovy  |   114 +
 .../bbop/apollo/FeatureRelationshipService.groovy  |   226 +
 .../services/org/bbop/apollo/FeatureService.groovy |  2977 ++++
 .../org/bbop/apollo/FeatureTypeService.groovy      |    34 +
 .../services/org/bbop/apollo/FileService.groovy    |   210 +
 .../org/bbop/apollo/Gff3HandlerService.groovy      |   439 +
 .../services/org/bbop/apollo/JbrowseService.groovy |    41 +
 .../services/org/bbop/apollo/NameService.groovy    |   156 +
 .../apollo/NonCanonicalSplitSiteService.groovy     |   281 +
 .../org/bbop/apollo/OrganismService.groovy         |    86 +
 .../org/bbop/apollo/OverlapperService.groovy       |   186 +
 .../org/bbop/apollo/PermissionService.groovy       |   669 +
 .../org/bbop/apollo/PhoneHomeService.groovy        |    71 +
 .../org/bbop/apollo/PreferenceService.groovy       |   756 +
 .../org/bbop/apollo/ProjectionService.groovy       |    35 +
 .../services/org/bbop/apollo/ProxyService.groovy   |    68 +
 .../services/org/bbop/apollo/ReportService.groovy  |   222 +
 .../org/bbop/apollo/RequestHandlingService.groovy  |  2360 +++
 .../org/bbop/apollo/SequenceSearchService.groovy   |    81 +
 .../org/bbop/apollo/SequenceService.groovy         |   595 +
 .../services/org/bbop/apollo/SvgService.groovy     |   177 +
 .../org/bbop/apollo/TrackMapperService.groovy      |    69 +
 .../services/org/bbop/apollo/TrackService.groovy   |   558 +
 .../org/bbop/apollo/TranscriptService.groovy       |   508 +
 .../services/org/bbop/apollo/UserService.groovy    |   115 +
 .../authenticator/AuthenticatorService.groovy      |    16 +
 .../RemoteUserAuthenticatorService.groovy          |   125 +
 .../UsernamePasswordAuthenticatorService.groovy    |    41 +
 .../taglib/org/bbop/apollo/PermissionTagLib.groovy |    29 +
 grails-app/views/annotator/adminPanel.gsp          |    18 +
 grails-app/views/annotator/componentDiv.gsp        |    53 +
 grails-app/views/annotator/demo.gsp                |    40 +
 grails-app/views/annotator/detail.gsp              |   156 +
 grails-app/views/annotator/index.gsp               |    54 +
 grails-app/views/annotator/notAuthorized.gsp       |    17 +
 grails-app/views/annotator/report.gsp              |   117 +
 grails-app/views/annotator/splitter.gsp            |    52 +
 grails-app/views/annotator/version.gsp             |    49 +
 grails-app/views/annotator/workingDiv.gsp          |    53 +
 grails-app/views/auth/login.gsp                    |    68 +
 grails-app/views/auth/unauthorized.gsp             |    24 +
 grails-app/views/availableStatus/_form.gsp         |    38 +
 grails-app/views/availableStatus/create.gsp        |    38 +
 grails-app/views/availableStatus/edit.gsp          |    41 +
 grails-app/views/availableStatus/index.gsp         |    58 +
 grails-app/views/availableStatus/show.gsp          |    79 +
 grails-app/views/cannedComment/_form.gsp           |    47 +
 grails-app/views/cannedComment/create.gsp          |    38 +
 grails-app/views/cannedComment/edit.gsp            |    41 +
 grails-app/views/cannedComment/index.gsp           |    64 +
 grails-app/views/cannedComment/show.gsp            |    91 +
 grails-app/views/cannedKey/_form.gsp               |    45 +
 grails-app/views/cannedKey/create.gsp              |    38 +
 grails-app/views/cannedKey/edit.gsp                |    41 +
 grails-app/views/cannedKey/index.gsp               |    63 +
 grails-app/views/cannedKey/show.gsp                |   102 +
 grails-app/views/cannedValue/_form.gsp             |    43 +
 grails-app/views/cannedValue/create.gsp            |    38 +
 grails-app/views/cannedValue/edit.gsp              |    41 +
 grails-app/views/cannedValue/index.gsp             |    70 +
 grails-app/views/cannedValue/show.gsp              |    88 +
 grails-app/views/error.gsp                         |    18 +
 grails-app/views/featureEvent/_form.gsp            |   112 +
 grails-app/views/featureEvent/create.gsp           |    38 +
 grails-app/views/featureEvent/edit.gsp             |    41 +
 grails-app/views/featureEvent/index.gsp            |    66 +
 grails-app/views/featureEvent/report.gsp           |   166 +
 grails-app/views/featureEvent/show.gsp             |   161 +
 grails-app/views/featureType/_form.gsp             |    40 +
 grails-app/views/featureType/create.gsp            |    38 +
 grails-app/views/featureType/edit.gsp              |    41 +
 grails-app/views/featureType/index.gsp             |    59 +
 grails-app/views/featureType/show.gsp              |    72 +
 grails-app/views/google_analytics.gsp              |    26 +
 grails-app/views/home/metrics.gsp                  |    66 +
 grails-app/views/home/systemInfo.gsp               |    90 +
 grails-app/views/index.gsp                         |   122 +
 .../views/jbrowse/chooseOrganismForJbrowse.gsp     |    46 +
 grails-app/views/layouts/_reportHeader.gsp         |    47 +
 grails-app/views/layouts/annotator.gsp             |    63 +
 grails-app/views/layouts/annotator2.gsp            |    67 +
 grails-app/views/layouts/embedded.gsp              |    71 +
 grails-app/views/layouts/login.gsp                 |    89 +
 grails-app/views/layouts/main.gsp                  |    71 +
 grails-app/views/layouts/oldlook.gsp               |    64 +
 grails-app/views/layouts/report.gsp                |    67 +
 grails-app/views/login/doLogin.gsp                 |   109 +
 grails-app/views/menu.gsp                          |    43 +
 grails-app/views/organism/_summaryEntry.gsp        |    49 +
 grails-app/views/organism/report.gsp               |    38 +
 grails-app/views/proxy/_form.gsp                   |    41 +
 grails-app/views/proxy/create.gsp                  |    38 +
 grails-app/views/proxy/edit.gsp                    |    41 +
 grails-app/views/proxy/index.gsp                   |    73 +
 grails-app/views/proxy/show.gsp                    |    82 +
 grails-app/views/sequence/index.gsp                |   194 +
 grails-app/views/sequence/permissions.gsp          |    50 +
 grails-app/views/sequence/report.gsp               |   109 +
 grails-app/views/sequence/singleUser.gsp           |    69 +
 grails-app/views/sequence/websocketTest.gsp        |    91 +
 grails-app/views/webServices/index.gsp             |   171 +
 grails-app/views/web_services.gsp                  |  2176 +++
 grailsw                                            |   363 +
 grailsw.bat                                        |   186 +
 gwt-sdk/gwt-dev.jar                                |   Bin 0 -> 38520843 bytes
 gwt-sdk/gwt-user.jar                               |   Bin 0 -> 32026261 bytes
 gwt-sdk/validation-api-1.0.0.GA-sources.jar        |   Bin 0 -> 65220 bytes
 gwt-sdk/validation-api-1.0.0.GA.jar                |   Bin 0 -> 47433 bytes
 index.html                                         |    10 +
 install_jbrowse.sh                                 |    36 +
 lib/gbol/resources/1.0/resources-1.0.jar           |   Bin 0 -> 900 bytes
 lib/gwt/gwt-servlet-deps.jar                       |   Bin 0 -> 276574 bytes
 lib/gwt/gwt-servlet.jar                            |   Bin 0 -> 9426252 bytes
 lib/gwt/gwtbootstrap3-0.9.4.jar                    |   Bin 0 -> 1635022 bytes
 lib/gwt/gwtbootstrap3-extras-0.9.4.jar             |   Bin 0 -> 1218547 bytes
 package.json                                       |    33 +
 sample-docker-apollo-config.groovy                 |     3 +
 sample-h2-apollo-config.groovy                     |   111 +
 sample-mysql-apollo-config.groovy                  |   110 +
 sample-postgres-apollo-config.groovy               |   158 +
 scripts/_Events.groovy                             |   202 +
 scripts/chado-schema-with-ontologies.sql.gz        |   Bin 0 -> 5358015 bytes
 scripts/clean_organism.sh                          |    24 +
 scripts/copy_client.sh                             |    10 +
 scripts/delete_all_features.sh                     |    20 +
 scripts/delete_only_features.sh                    |    19 +
 scripts/load_chado_schema.sh                       |   147 +
 scripts/print_all_features.sh                      |    34 +
 scripts/version-template.jsp                       |     1 +
 settings.gradle                                    |    19 +
 setup.py                                           |     0
 .../org/bbop/apollo/ConfigurableFeature.groovy     |    20 +
 src/groovy/org/bbop/apollo/CvTermStringEnum.groovy |    17 +
 .../org/bbop/apollo/DefaultPaddingStrategy.groovy  |    10 +
 .../bbop/apollo/FeaturePositionComparator.groovy   |    51 +
 src/groovy/org/bbop/apollo/FlankingRegion.groovy   |    17 +
 src/groovy/org/bbop/apollo/FormatUtil.groovy       |    78 +
 src/groovy/org/bbop/apollo/GFF3Entry.groovy        |   124 +
 .../org/bbop/apollo/LeftPaddingStrategy.groovy     |    15 +
 .../org/bbop/apollo/LetterPaddingStrategy.groovy   |    18 +
 src/groovy/org/bbop/apollo/Ontological.groovy      |    11 +
 src/groovy/org/bbop/apollo/PaddingStrategy.groovy  |     9 +
 src/groovy/org/bbop/apollo/PhoneHomeEnum.groovy    |    31 +
 src/groovy/org/bbop/apollo/TermTypeEnum.groovy     |    12 +
 .../alteration/SequenceAlterationInContext.groovy  |    16 +
 .../org/bbop/apollo/event/AnnotationEvent.groovy   |    33 +
 .../apollo/event/AnnotationEventListener.groovy    |     9 +
 .../bbop/apollo/event/AnnotationListener.groovy    |     9 +
 .../org/bbop/apollo/filter/Cds3Filter.groovy       |    28 +
 .../org/bbop/apollo/filter/FeatureFilter.groovy    |    13 +
 .../org/bbop/apollo/filter/StopCodonFilter.groovy  |    27 +
 .../bbop/apollo/history/FeatureEventView.groovy    |    16 +
 .../bbop/apollo/history/FeatureOperation.groovy    |    34 +
 .../org/bbop/apollo/operation/OperationEnum.groovy |    43 +
 .../org/bbop/apollo/preference/OrganismDTO.groovy  |     8 +
 .../org/bbop/apollo/preference/SequenceDTO.groovy  |    12 +
 .../org/bbop/apollo/preference/UserDTO.groovy      |     7 +
 .../preference/UserOrganismPreferenceDTO.groovy    |    43 +
 .../apollo/projection/AbstractProjection.groovy    |    21 +
 .../org/bbop/apollo/projection/Coordinate.groovy   |    35 +
 .../projection/DiscontinuousProjection.groovy      |   121 +
 .../DiscontinuousProjectionFactory.groovy          |    36 +
 .../projection/DuplicateTrackProjection.groovy     |    17 +
 .../org/bbop/apollo/projection/Projection.groovy   |    27 +
 .../bbop/apollo/projection/RefSeqProjector.groovy  |    15 +
 .../bbop/apollo/projection/ReferenceTrack.groovy   |    11 +
 .../apollo/projection/ReverseProjection.groovy     |    29 +
 src/groovy/org/bbop/apollo/projection/Track.groovy |    44 +
 .../bbop/apollo/projection/TrackProjector.groovy   |     9 +
 .../org/bbop/apollo/report/AnnotatorSummary.groovy |    15 +
 .../apollo/report/OrganismPermissionSummary.groovy |    12 +
 .../org/bbop/apollo/report/OrganismSummary.groovy  |    36 +
 .../bbop/apollo/report/PerformanceMetric.groovy    |    13 +
 .../org/bbop/apollo/report/SequenceSummary.groovy  |    15 +
 .../org/bbop/apollo/sequence/DownloadFile.groovy   |    10 +
 .../org/bbop/apollo/sequence/Overlapper.groovy     |    13 +
 src/groovy/org/bbop/apollo/sequence/Range.groovy   |    25 +
 .../org/bbop/apollo/sequence/SequenceDTO.groovy    |    11 +
 .../apollo/sequence/SequenceLocationDTO.groovy     |    27 +
 .../sequence/SequenceTranslationHandler.groovy     |   230 +
 .../sequence/StandardTranslationTable.groovy       |    86 +
 src/groovy/org/bbop/apollo/sequence/Strand.groovy  |    36 +
 .../bbop/apollo/sequence/TranslationTable.groovy   |    93 +
 .../org/bbop/apollo/sequence/search/Alignment.java |     9 +
 .../sequence/search/AlignmentParsingException.java |    11 +
 .../apollo/sequence/search/SequenceSearchTool.java |    22 +
 .../search/SequenceSearchToolException.java        |    15 +
 .../sequence/search/blast/BlastAlignment.java      |   135 +
 .../sequence/search/blast/BlastCommandLine.java    |   156 +
 .../search/blast/TabDelimittedAlignment.java       |    43 +
 .../sequence/search/blat/BlatCommandLine.java      |   153 +
 .../BlatCommandLineNucleotideToNucleotide.java     |     5 +
 .../blat/BlatCommandLineProteinToNucleotide.java   |     9 +
 .../org/bbop/apollo/track/RenderObject.groovy      |    15 +
 src/gwt/org/bbop/apollo/gwt/Annotator.gwt.xml      |    43 +
 .../gwt/client/AnnotationContainerWidget.java      |    43 +
 src/gwt/org/bbop/apollo/gwt/client/Annotator.java  |   170 +
 .../org/bbop/apollo/gwt/client/AnnotatorPanel.java |   836 +
 .../bbop/apollo/gwt/client/AnnotatorPanel.ui.xml   |   175 +
 .../org/bbop/apollo/gwt/client/ErrorDialog.java    |    57 +
 .../bbop/apollo/gwt/client/ExonDetailPanel.java    |   411 +
 .../bbop/apollo/gwt/client/ExonDetailPanel.ui.xml  |    89 +
 .../org/bbop/apollo/gwt/client/ExportPanel.java    |   298 +
 .../bbop/apollo/gwt/client/GeneDetailPanel.java    |   135 +
 .../bbop/apollo/gwt/client/GeneDetailPanel.ui.xml  |    72 +
 src/gwt/org/bbop/apollo/gwt/client/GroupPanel.java |   522 +
 .../org/bbop/apollo/gwt/client/GroupPanel.ui.xml   |   144 +
 src/gwt/org/bbop/apollo/gwt/client/LinkDialog.java |    31 +
 .../org/bbop/apollo/gwt/client/LoadingDialog.java  |    44 +
 .../org/bbop/apollo/gwt/client/LoginDialog.java    |   101 +
 .../org/bbop/apollo/gwt/client/LoginDialog.ui.xml  |    49 +
 src/gwt/org/bbop/apollo/gwt/client/MainPanel.java  |  1138 ++
 .../org/bbop/apollo/gwt/client/MainPanel.ui.xml    |   191 +
 .../org/bbop/apollo/gwt/client/MutableBoolean.java |    21 +
 .../org/bbop/apollo/gwt/client/OrganismPanel.java  |   486 +
 .../bbop/apollo/gwt/client/OrganismPanel.ui.xml    |   116 +
 .../bbop/apollo/gwt/client/PreferencePanel.java    |   103 +
 .../bbop/apollo/gwt/client/PreferencePanel.ui.xml  |    66 +
 .../apollo/gwt/client/ReferenceSequenceOracle.java |    74 +
 .../org/bbop/apollo/gwt/client/RegisterDialog.java |   144 +
 .../apollo/gwt/client/RepeatRegionDetailPanel.java |   117 +
 .../gwt/client/RepeatRegionDetailPanel.ui.xml      |    64 +
 .../org/bbop/apollo/gwt/client/SequencePanel.java  |   453 +
 .../bbop/apollo/gwt/client/SequencePanel.ui.xml    |   218 +
 src/gwt/org/bbop/apollo/gwt/client/TrackPanel.java |   501 +
 .../org/bbop/apollo/gwt/client/TrackPanel.ui.xml   |   120 +
 .../apollo/gwt/client/TranscriptDetailPanel.java   |   124 +
 .../apollo/gwt/client/TranscriptDetailPanel.ui.xml |    74 +
 .../org/bbop/apollo/gwt/client/UrlDialogBox.java   |    41 +
 src/gwt/org/bbop/apollo/gwt/client/UserPanel.java  |   718 +
 .../org/bbop/apollo/gwt/client/UserPanel.ui.xml    |   181 +
 .../apollo/gwt/client/WebApolloSimplePager.java    |    37 +
 .../gwt/client/comparators/AlphanumericSorter.java |    96 +
 .../bbop/apollo/gwt/client/dto/AnnotationInfo.java |   149 +
 .../gwt/client/dto/AnnotationInfoConverter.java    |    72 +
 .../apollo/gwt/client/dto/AppInfoConverter.java    |    33 +
 .../bbop/apollo/gwt/client/dto/AppStateInfo.java   |    95 +
 .../org/bbop/apollo/gwt/client/dto/ExportInfo.java |    37 +
 .../org/bbop/apollo/gwt/client/dto/GroupInfo.java  |   118 +
 .../client/dto/GroupOrganismPermissionInfo.java    |    39 +
 .../org/bbop/apollo/gwt/client/dto/HasJSON.java    |    13 +
 .../bbop/apollo/gwt/client/dto/OrganismInfo.java   |   184 +
 .../gwt/client/dto/OrganismInfoConverter.java      |   113 +
 .../gwt/client/dto/OrganismPermissionInfo.java     |    76 +
 .../bbop/apollo/gwt/client/dto/SequenceInfo.java   |   140 +
 .../gwt/client/dto/SequenceInfoConverter.java      |    46 +
 .../org/bbop/apollo/gwt/client/dto/TrackInfo.java  |   122 +
 .../org/bbop/apollo/gwt/client/dto/UserInfo.java   |   198 +
 .../apollo/gwt/client/dto/UserInfoConverter.java   |   112 +
 .../gwt/client/dto/UserOrganismPermissionInfo.java |    38 +
 .../client/event/AnnotationInfoChangeEvent.java    |    53 +
 .../event/AnnotationInfoChangeEventHandler.java    |    13 +
 .../bbop/apollo/gwt/client/event/ExportEvent.java  |    49 +
 .../gwt/client/event/ExportEventHandler.java       |    13 +
 .../apollo/gwt/client/event/GroupChangeEvent.java  |    69 +
 .../gwt/client/event/GroupChangeEventHandler.java  |    12 +
 .../gwt/client/event/OrganismChangeEvent.java      |    74 +
 .../client/event/OrganismChangeEventHandler.java   |    13 +
 .../apollo/gwt/client/event/UserChangeEvent.java   |    89 +
 .../gwt/client/event/UserChangeEventHandler.java   |    12 +
 .../org/bbop/apollo/gwt/client/resources/Table.css |   141 +
 .../gwt/client/resources/TableResources.java       |    18 +
 .../org/bbop/apollo/gwt/client/resources/Tree.css  |    83 +
 .../apollo/gwt/client/resources/TreeResources.java |     9 +
 .../gwt/client/rest/AnnotationRestService.java     |    30 +
 .../apollo/gwt/client/rest/GroupRestService.java   |   193 +
 .../gwt/client/rest/OrganismRestService.java       |   129 +
 .../bbop/apollo/gwt/client/rest/RestService.java   |    61 +
 .../gwt/client/rest/SequenceRestService.java       |   146 +
 .../apollo/gwt/client/rest/UserRestService.java    |   252 +
 src/gwt/org/bbop/apollo/gwt/public/theme.css       |    66 +
 .../apollo/gwt/shared/ClientTokenGenerator.java    |    30 +
 .../bbop/apollo/gwt/shared/FeatureStringEnum.java  |   166 +
 .../org/bbop/apollo/gwt/shared/PermissionEnum.java |    72 +
 .../apollo/gwt/shared/track/NclistColumnEnum.java  |    37 +
 .../bbop/apollo/gwt/shared/track/TrackIndex.java   |   194 +
 .../org/bbop/apollo/AnnotationException.groovy     |    10 +
 src/java/org/bbop/apollo/ImprovedH2Dialect.java    |    19 +
 .../org/bbop/apollo/ImprovedPostgresDialect.java   |    19 +
 src/java/org/bbop/apollo/Pair.java                 |    29 +
 .../org/bbop/apollo/PermissionException.groovy     |    10 +
 src/templates/war/web.xml                          |    92 +
 .../client/data/MAKER/Group1.33_Amel_4.5.maker.gff |    12 +
 .../Group1.33_Amel_4.5.maker.jbrowse.expected.json |    44 +
 ...Amel_4.5.maker.jbrowse.expected_PRETTIFIED.json |    43 +
 ...roup1.33_Amel_4.5.maker.webapollo.expected.json |    45 +
 test/client/data/heterozygous_snv.vcf              |     9 +
 test/client/js_tests/index.html                    |    55 +
 test/client/js_tests/lib/jasmine-1.2.0/MIT.LICENSE |    20 +
 .../js_tests/lib/jasmine-1.2.0/jasmine-html.js     |   616 +
 test/client/js_tests/lib/jasmine-1.2.0/jasmine.css |    81 +
 test/client/js_tests/lib/jasmine-1.2.0/jasmine.js  |  2529 +++
 test/client/js_tests/spec/GFF3toJbrowseJsonSpec.js |   135 +
 test/client/js_tests/spec/JSONUtilsSpec.js         |   130 +
 test/client/selenium_tests/lib/WebApolloTest.py    |    35 +
 test/client/selenium_tests/lib/__init__.py         |     2 +
 test/client/selenium_tests/pythium_test.py         |    29 +
 test/config/mysql.travis                           |    21 +
 test/config/postgres.travis                        |    18 +
 .../org/bbop/apollo/AbstractIntegrationSpec.groovy |    78 +
 .../bbop/apollo/CdsServiceIntegrationSpec.groovy   |    68 +
 .../ChadoHandlerServiceIntegrationSpec.groovy      |   358 +
 .../bbop/apollo/ExonServiceIntegrationSpec.groovy  |    97 +
 .../FastaHandlerServiceIntegrationSpec.groovy      |    48 +
 .../FeatureEventServiceIntegrationSpec.groovy      |  1454 ++
 ...eatureRelationshipServiceIntegrationSpec.groovy |    83 +
 .../apollo/FeatureServiceIntegrationSpec.groovy    |   318 +
 .../Gff3HandlerServiceIntegrationSpec.groovy       |    61 +
 .../apollo/OrganismServiceIntegrationSpec.groovy   |    55 +
 .../apollo/OverlapperServiceIntegrationSpec.groovy |   247 +
 .../apollo/PhoneHomeServiceIntegrationSpec.groovy  |    15 +
 .../apollo/PreferenceServiceIntegrationSpec.groovy |  1020 ++
 .../RequestHandlingServiceIntegrationSpec.groovy   |  3899 +++++
 .../apollo/SequenceServiceIntegrationSpec.groovy   |   352 +
 .../seq/f78/c6f/0c/Group1.10-0.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-1.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-10.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-11.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-12.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-13.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-14.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-15.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-16.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-17.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-18.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-19.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-2.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-20.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-21.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-22.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-23.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-24.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-25.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-26.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-27.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-28.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-29.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-3.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-30.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-31.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-32.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-33.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-34.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-35.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-36.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-37.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-38.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-39.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-4.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-40.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-41.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-42.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-43.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-44.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-45.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-46.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-47.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-48.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-49.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-5.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-50.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-51.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-52.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-53.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-54.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-55.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-56.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-57.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-58.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-59.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-6.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-60.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-61.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-62.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-63.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-64.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-65.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-66.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-67.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-68.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-69.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-7.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-70.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-8.txt                 |     1 +
 .../seq/f78/c6f/0c/Group1.10-9.txt                 |     1 +
 .../sequences/honeybee-Group1.10/seq/refSeqs.json  |     1 +
 .../sequences/honeybee-Group1.10/trackList.json    |     1 +
 .../sequences/honeybee-Group1.10/tracks.conf       |     0
 .../honeybee-tracks/seq/2ac/141/c6/Group11.4-0.txt |     1 +
 .../honeybee-tracks/seq/2ac/141/c6/Group11.4-1.txt |     1 +
 .../honeybee-tracks/seq/2ac/141/c6/Group11.4-2.txt |     1 +
 .../honeybee-tracks/seq/2ac/141/c6/Group11.4-3.txt |     1 +
 .../honeybee-tracks/seq/7cf/4ac/72/GroupUn87-0.txt |     1 +
 .../honeybee-tracks/seq/7cf/4ac/72/GroupUn87-1.txt |     1 +
 .../honeybee-tracks/seq/7cf/4ac/72/GroupUn87-2.txt |     1 +
 .../honeybee-tracks/seq/7cf/4ac/72/GroupUn87-3.txt |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-0.txt |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-1.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-10.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-11.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-12.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-13.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-14.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-15.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-16.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-17.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-18.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-19.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-2.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-20.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-21.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-22.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-23.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-24.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-25.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-26.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-27.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-28.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-29.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-3.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-30.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-31.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-32.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-33.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-34.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-35.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-36.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-37.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-38.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-39.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-4.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-40.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-41.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-42.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-43.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-44.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-45.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-46.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-47.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-48.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-49.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-5.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-50.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-51.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-52.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-53.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-54.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-55.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-56.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-57.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-58.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-59.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-6.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-60.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-61.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-62.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-63.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-64.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-65.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-66.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-67.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-68.txt                |     1 +
 .../seq/f78/c6f/0c/Group1.10-69.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-7.txt |     1 +
 .../seq/f78/c6f/0c/Group1.10-70.txt                |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-8.txt |     1 +
 .../honeybee-tracks/seq/f78/c6f/0c/Group1.10-9.txt |     1 +
 .../sequences/honeybee-tracks/trackList.json       |     1 +
 .../Group1.1/hist-100000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group1.1/names.txt      |    56 +
 .../Official Gene Set v3.2/Group1.1/trackData.json |     1 +
 .../Group1.10/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group1.10/lf-1.json     |     1 +
 .../Official Gene Set v3.2/Group1.10/lf-2.json     |     1 +
 .../Official Gene Set v3.2/Group1.10/lf-3.json     |     1 +
 .../Official Gene Set v3.2/Group1.10/names.txt     |   143 +
 .../Group1.10/trackData.json                       |     1 +
 .../Group11.4/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group11.4/names.txt     |     6 +
 .../Group11.4/trackData.json                       |     1 +
 .../Group11.6/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group11.6/lf-1.json     |     1 +
 .../Official Gene Set v3.2/Group11.6/lf-2.json     |     1 +
 .../Official Gene Set v3.2/Group11.6/names.txt     |   109 +
 .../Group11.6/trackData.json                       |     1 +
 .../Group2.19/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-1.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-2.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-3.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-4.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-5.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-6.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/lf-7.json     |     1 +
 .../Official Gene Set v3.2/Group2.19/names.txt     |   313 +
 .../Group2.19/trackData.json                       |     1 +
 .../Group4.1/hist-50000-0.json                     |     1 +
 .../Official Gene Set v3.2/Group4.1/names.txt      |    29 +
 .../Official Gene Set v3.2/Group4.1/trackData.json |     1 +
 .../Group6.10/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/Group6.10/names.txt     |    35 +
 .../Group6.10/trackData.json                       |     1 +
 .../GroupUn87/hist-50000-0.json                    |     1 +
 .../Official Gene Set v3.2/GroupUn87/names.txt     |     4 +
 .../GroupUn87/trackData.json                       |     1 +
 .../resources/sequences/yeast/.htaccess            |    10 +
 .../resources/sequences/yeast/names/0.json         |     1 +
 .../resources/sequences/yeast/names/1.json         |     1 +
 .../resources/sequences/yeast/names/2.json         |     1 +
 .../resources/sequences/yeast/names/3.json         |     1 +
 .../resources/sequences/yeast/names/4.json         |     1 +
 .../resources/sequences/yeast/names/5.json         |     1 +
 .../resources/sequences/yeast/names/6.json         |     1 +
 .../resources/sequences/yeast/names/7.json         |     1 +
 .../resources/sequences/yeast/names/8.json         |     1 +
 .../resources/sequences/yeast/names/9.json         |     1 +
 .../resources/sequences/yeast/names/a.json         |     1 +
 .../resources/sequences/yeast/names/b.json         |     1 +
 .../resources/sequences/yeast/names/c.json         |     1 +
 .../resources/sequences/yeast/names/d.json         |     1 +
 .../resources/sequences/yeast/names/e.json         |     1 +
 .../resources/sequences/yeast/names/f.json         |     1 +
 .../resources/sequences/yeast/names/meta.json      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-0.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-1.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-10.txt     |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-11.txt     |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-2.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-3.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-4.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-5.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-6.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-7.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-8.txt      |     1 +
 .../sequences/yeast/seq/5c1/aff/29/chrI-9.txt      |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-0.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-1.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-10.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-11.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-12.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-13.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-14.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-15.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-16.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-17.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-18.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-19.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-2.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-20.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-21.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-22.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-23.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-24.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-25.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-26.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-27.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-28.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-29.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-3.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-30.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-31.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-32.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-33.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-34.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-35.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-36.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-37.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-38.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-39.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-4.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-40.txt    |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-5.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-6.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-7.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-8.txt     |     1 +
 .../sequences/yeast/seq/9fe/c94/2a/chrII-9.txt     |     1 +
 .../resources/sequences/yeast/seq/refSeqs.json     |     1 +
 .../resources/sequences/yeast/trackList.json       |    71 +
 .../resources/sequences/yeast/tracks.conf          |     0
 .../yeast/tracks/Genes/chrI/hist-5000-0.json       |     1 +
 .../sequences/yeast/tracks/Genes/chrI/names.txt    |   117 +
 .../yeast/tracks/Genes/chrI/trackData.json         |     1 +
 .../yeast/tracks/Genes/chrII/hist-10000-0.json     |     1 +
 .../yeast/tracks/Genes/chrII/hist-5000-0.json      |     1 +
 .../sequences/yeast/tracks/Genes/chrII/lf-1.json   |     1 +
 .../sequences/yeast/tracks/Genes/chrII/lf-2.json   |     1 +
 .../sequences/yeast/tracks/Genes/chrII/lf-3.json   |     1 +
 .../sequences/yeast/tracks/Genes/chrII/lf-4.json   |     1 +
 .../sequences/yeast/tracks/Genes/chrII/names.txt   |   456 +
 .../yeast/tracks/Genes/chrII/trackData.json        |     1 +
 .../resources/track-data/inputNcList.json          |   357 +
 .../resources/track-data/mouseMsx2.json            |   156 +
 .../resources/track-data/trackClasses.json         |     1 +
 .../resources/track-data/wormGeneflp-1.json        |  1258 ++
 .../apollo/AbstractApolloControllerSpec.groovy     |    20 +
 .../bbop/apollo/AnnotationEditorServiceSpec.groovy |    30 +
 .../apollo/AvailableStatusControllerSpec.groovy    |   151 +
 .../bbop/apollo/CannedCommentControllerSpec.groovy |   151 +
 .../org/bbop/apollo/CannedKeyControllerSpec.groovy |   152 +
 .../bbop/apollo/CannedValueControllerSpec.groovy   |   152 +
 .../org/bbop/apollo/ChadoHandlerServiceSpec.groovy |    20 +
 .../bbop/apollo/FeatureEventControllerSpec.groovy  |   156 +
 .../org/bbop/apollo/FeatureEventServiceSpec.groovy |  1201 ++
 .../apollo/FeatureRelationshipServiceSpec.groovy   |    71 +
 .../unit/org/bbop/apollo/FeatureServiceSpec.groovy |    64 +
 test/unit/org/bbop/apollo/FeatureSpec.groovy       |   130 +
 .../bbop/apollo/FeatureTypeControllerSpec.groovy   |   155 +
 .../org/bbop/apollo/FeatureTypeServiceSpec.groovy  |    36 +
 .../org/bbop/apollo/Gff3HandlerServiceSpec.groovy  |    48 +
 .../unit/org/bbop/apollo/JbrowseServiceSpec.groovy |    44 +
 test/unit/org/bbop/apollo/NameServiceSpec.groovy   |    29 +
 .../org/bbop/apollo/PermissionServiceSpec.groovy   |   139 +
 test/unit/org/bbop/apollo/ProjectionSpec.groovy    |   187 +
 .../org/bbop/apollo/ProxyControllerSpec.groovy     |   154 +
 .../bbop/apollo/RequestHandlingServiceSpec.groovy  |    30 +
 test/unit/org/bbop/apollo/SvgServiceSpec.groovy    |    20 +
 test/unit/org/bbop/apollo/TrackServiceSpec.groovy  |   129 +
 .../org/bbop/apollo/TranslationTableSpec.groovy    |    73 +
 test/unit/org/bbop/apollo/UserSpec.groovy          |    63 +
 tools/cleanup/remove_temporary_files.sh            |    30 +
 .../data/add_features_from_gff3_to_annotations.pl  |   920 +
 .../add_transcripts_from_gff3_to_annotations.pl    |   749 +
 web-app/WEB-INF/applicationContext.xml             |    31 +
 web-app/WEB-INF/sitemesh.xml                       |    14 +
 web-app/WEB-INF/tld/c.tld                          |   572 +
 web-app/WEB-INF/tld/fmt.tld                        |   671 +
 web-app/WEB-INF/tld/grails.tld                     |   550 +
 web-app/WEB-INF/tld/spring-form.tld                |  2411 +++
 web-app/WEB-INF/tld/spring.tld                     |   457 +
 web-app/css/Annotator.css                          |    42 +
 web-app/css/chado.css                              |    20 +
 web-app/css/login.css                              |    22 +
 web-app/css/search_sequence.css                    |    78 +
 web-app/css/ui-layout.css                          |    49 +
 web-app/css/web_api_stylesheet.css                 |    31 +
 web-app/images/ApolloLogo_100x36.png               |   Bin 0 -> 3071 bytes
 web-app/images/loading.gif                         |   Bin 0 -> 673 bytes
 web-app/images/qrcode.26636394.png                 |   Bin 0 -> 377 bytes
 web-app/images/sign_in_green.png                   |   Bin 0 -> 1803 bytes
 web-app/images/user_icon_16px.png                  |   Bin 0 -> 470 bytes
 web-app/images/webapollo_favicon.ico               |   Bin 0 -> 1150 bytes
 web-app/js/DataTables-plugins/dataTablesPlugins.js |    36 +
 web-app/js/DataTables/css/demo_table.css           |   577 +
 web-app/js/DataTables/images/Sorting icons.psd     |   Bin 0 -> 27490 bytes
 web-app/js/DataTables/images/back_disabled.png     |   Bin 0 -> 1361 bytes
 web-app/js/DataTables/images/back_enabled.png      |   Bin 0 -> 1379 bytes
 .../js/DataTables/images/back_enabled_hover.png    |   Bin 0 -> 1375 bytes
 web-app/js/DataTables/images/favicon.ico           |   Bin 0 -> 894 bytes
 web-app/js/DataTables/images/forward_disabled.png  |   Bin 0 -> 1363 bytes
 web-app/js/DataTables/images/forward_enabled.png   |   Bin 0 -> 1380 bytes
 .../js/DataTables/images/forward_enabled_hover.png |   Bin 0 -> 1379 bytes
 web-app/js/DataTables/images/sort_asc.png          |   Bin 0 -> 1118 bytes
 web-app/js/DataTables/images/sort_asc_disabled.png |   Bin 0 -> 1050 bytes
 web-app/js/DataTables/images/sort_both.png         |   Bin 0 -> 1136 bytes
 web-app/js/DataTables/images/sort_desc.png         |   Bin 0 -> 1127 bytes
 .../js/DataTables/images/sort_desc_disabled.png    |   Bin 0 -> 1045 bytes
 web-app/js/DataTables/js/jquery.dataTables.js      | 12099 ++++++++++++++
 web-app/js/DataTables/js/jquery.dataTables.min.js  |   155 +
 web-app/js/DataTables/js/jquery.js                 |     2 +
 web-app/js/jQueryUtils.js                          |    11 +
 .../images/ui-bg_flat_0_aaaaaa_40x100.png          |   Bin 0 -> 180 bytes
 .../images/ui-bg_flat_75_ffffff_40x100.png         |   Bin 0 -> 178 bytes
 .../images/ui-bg_glass_55_fbf9ee_1x400.png         |   Bin 0 -> 120 bytes
 .../images/ui-bg_glass_65_ffffff_1x400.png         |   Bin 0 -> 105 bytes
 .../images/ui-bg_glass_75_dadada_1x400.png         |   Bin 0 -> 111 bytes
 .../images/ui-bg_glass_75_e6e6e6_1x400.png         |   Bin 0 -> 110 bytes
 .../images/ui-bg_glass_95_fef1ec_1x400.png         |   Bin 0 -> 119 bytes
 .../ui-bg_highlight-soft_75_cccccc_1x100.png       |   Bin 0 -> 101 bytes
 .../images/ui-icons_222222_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_2e83ff_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_454545_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_888888_256x240.png             |   Bin 0 -> 4369 bytes
 .../images/ui-icons_cd0a0a_256x240.png             |   Bin 0 -> 4369 bytes
 web-app/js/jquery-ui-menubar/jquery-1.8.2.js       |  9440 +++++++++++
 .../js/jquery-ui-menubar/jquery.ui.accordion.css   |    16 +
 web-app/js/jquery-ui-menubar/jquery.ui.all.css     |    12 +
 .../jquery-ui-menubar/jquery.ui.autocomplete.css   |    18 +
 web-app/js/jquery-ui-menubar/jquery.ui.base.css    |    26 +
 web-app/js/jquery-ui-menubar/jquery.ui.button.css  |    40 +
 web-app/js/jquery-ui-menubar/jquery.ui.button.js   |   415 +
 web-app/js/jquery-ui-menubar/jquery.ui.core.css    |    39 +
 web-app/js/jquery-ui-menubar/jquery.ui.core.js     |   343 +
 .../js/jquery-ui-menubar/jquery.ui.datepicker.css  |    67 +
 web-app/js/jquery-ui-menubar/jquery.ui.dialog.css  |    22 +
 web-app/js/jquery-ui-menubar/jquery.ui.dialog.js   |   847 +
 web-app/js/jquery-ui-menubar/jquery.ui.menu.css    |    30 +
 web-app/js/jquery-ui-menubar/jquery.ui.menu.js     |   628 +
 web-app/js/jquery-ui-menubar/jquery.ui.menubar.css |    15 +
 web-app/js/jquery-ui-menubar/jquery.ui.menubar.js  |   327 +
 web-app/js/jquery-ui-menubar/jquery.ui.position.js |   517 +
 .../js/jquery-ui-menubar/jquery.ui.progressbar.css |    12 +
 .../js/jquery-ui-menubar/jquery.ui.resizable.css   |    21 +
 .../js/jquery-ui-menubar/jquery.ui.selectable.css  |    11 +
 web-app/js/jquery-ui-menubar/jquery.ui.slider.css  |    25 +
 web-app/js/jquery-ui-menubar/jquery.ui.spinner.css |    24 +
 web-app/js/jquery-ui-menubar/jquery.ui.tabs.css    |    18 +
 web-app/js/jquery-ui-menubar/jquery.ui.theme.css   |   247 +
 web-app/js/jquery-ui-menubar/jquery.ui.tooltip.css |    22 +
 web-app/js/jquery-ui-menubar/jquery.ui.widget.js   |   502 +
 web-app/js/restapidoc/restapidoc.json              |  6886 ++++++++
 .../ncbi_10_translation_table.txt                  |     1 +
 .../ncbi_11_translation_table.txt                  |     6 +
 .../ncbi_12_translation_table.txt                  |     1 +
 .../ncbi_13_translation_table.txt                  |     6 +
 .../ncbi_14_translation_table.txt                  |     5 +
 .../ncbi_15_translation_table.txt                  |     1 +
 .../ncbi_16_translation_table.txt                  |     1 +
 .../ncbi_1_translation_table.txt                   |     0
 .../ncbi_21_translation_table.txt                  |     6 +
 .../ncbi_22_translation_table.txt                  |     2 +
 .../ncbi_23_translation_table.txt                  |     3 +
 .../ncbi_24_translation_table.txt                  |     6 +
 .../ncbi_25_translation_table.txt                  |     3 +
 .../ncbi_2_translation_table.txt                   |     4 +
 .../ncbi_3_translation_table.txt                   |     8 +
 .../ncbi_4_translation_table.txt                   |     1 +
 .../ncbi_5_translation_table.txt                   |     5 +
 .../ncbi_6_translation_table.txt                   |     2 +
 .../ncbi_9_translation_table.txt                   |     5 +
 wrapper/grails-wrapper-runtime-2.5.5.jar           |   Bin 0 -> 6110 bytes
 wrapper/grails-wrapper.properties                  |     1 +
 1410 files changed, 254513 insertions(+), 66 deletions(-)

diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..637536e
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1,2 @@
+service_name: travis-pro
+repo_token: eknS0v5b4YZyx543Ahl9gjN7LZWOIIZeY
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..1afad4f
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,309 @@
+# How to contribute code to Apollo
+
+##### Audience
+These guidelines are for developers of Apollo software, whether internal or in the broader community. 
+
+## Basic principles of the Apollo-flavored [GitHub Workflow](http://guides.github.com/overviews/flow/)
+
+##### Principle 1: Work from a personal fork 
+* Prior to adopting the workflow, a developer will perform a *one-time setup* to create a personal Fork of apollo and will subsequently perform their development and testing on a task-specific branch within their forked repo. This forked repo will be associated with that developer's GitHub account, and is distinct from the shared repo managed by GMOD.
+
+##### Principle 2: Commit to personal branches of that fork
+* Changes will never be committed directly to the master branch on the shared repo. Rather, they will be composed as branches within the developer's forked repo, where the developer can iterate and refine their code prior to submitting it for review.
+
+##### Principle 3: Propose changes via pull request of personal branches
+*  Each set of changes will be developed as a task-specific *branch* in the developer's forked repo, and then create a [pull request](https://github.com/GMOD/Apollo/compare) will be created to develop and propose changes to the shared repo. This mechanism provides a way for developers to discuss, revise and ultimately merge changes from the forked repo into the shared Apollo repo.
+
+##### Principle 4: Delete or ignore stale branches, but don't recycle merged ones
+*  Once a pull request has been merged, the task-specific branch is no longer needed and may be deleted or ignored. It is bad practice to reuse an existing branch once it has been merged. Instead, a subsequent branch and pull-request cycle should begin when a developer switches to a different coding task. 
+*  You may create a pull request in order to get feedback, but if you wish to continue working on the branch, so state with "DO NOT MERGE YET".
+
+## Table of contents
+
+<!-- MarkdownTOC -->
+
+- [One Time Setup - Forking a Shared Repo](#one-time-setup---forking-a-shared-repo)
+    - [Step 1 - Backup your existing repo (optional)](#step-1---backup-your-existing-repo-optional)
+    - [Step 2 - Fork `apollo` via the Web](#step-2---fork-apollo-via-the-web)
+    - [Step 3 - Clone the Fork Locally](#step-3---clone-the-fork-locally)
+    - [Step 4 - Configure the local forked repo](#step-4---configure-the-local-forked-repo)
+    - [Step 5 - Configure  `.bashrc` to show current branch (optional)](#step-5---configure--bashrc-to-show-current-branch-optional)
+- [Typical Development Cycle](#typical-development-cycle)
+    - [Refresh and clean up local environment](#refresh-and-clean-up-local-environment)
+        - [Step 1 - Fetch remotes](#step-1---fetch-remotes)
+        - [Step 2 - Ensure that 'master' is up to date](#step-2---ensure-that-master-is-up-to-date)
+    - [Create a new branch](#create-a-new-branch)
+    - [Changes, Commits and Pushes](#changes-commits-and-pushes)
+    - [Reconcile branch with upstream changes](#reconcile-branch-with-upstream-changes)
+        - [Fetching the upstream branch](#fetching-the-upstream-branch)
+        - [Rebasing to avoid Conflicts and Merge Commits](#rebasing-to-avoid-conflicts-and-merge-commits)
+        - [Dealing with merge conflicts during rebase](#dealing-with-merge-conflicts-during-rebase)
+        - [Advanced: Interactive rebase](#advanced-interactive-rebase)
+    - [Submitting a PR (pull request)](#submitting-a-pr-pull-request)
+    - [Reviewing a pull request](#reviewing-a-pull-request)
+    - [Respond to TravisCI tests](#respond-to-travisci-tests)
+    - [Respond to peer review](#respond-to-peer-review)
+    - [Repushing to a PR branch](#repushing-to-a-pr-branch)
+    - [Merge a pull request](#merge-a-pull-request)
+    - [Celebrate and get back to work](#celebrate-and-get-back-to-work)
+- [GitHub Tricks and Tips](#github-tricks-and-tips)
+- [References and Documentation](#references-and-documentation)
+
+<!-- /MarkdownTOC -->
+
+
+
+## One Time Setup - Forking a Shared Repo
+
+The official shared ``Apollo`` repository is intended to be modified solely via pull requests that are reviewed and merged by a set of responsible 'gatekeeper' developers within the Apollo development team. These pull requests are initially created as task-specific named branches within a developer's personal forked repo.
+
+Typically, a developer will fork a shared repo once, which creates a personal copy of the repo that is associated with the developer's GitHub account. Subsequent pull requests are developed as branches within this personal forked repo. The repo need never be forked again, although each pull request will be based upon a new named branch within this forked repo.
+
+### Step 1 - Backup your existing repo (optional)
+
+The Apollo team has recently adopted the workflow described in this document. Many developers will have an existing clone of the shared repo that they have been using for development. This cloned local directory must be *moved aside* so that a proper clone of the forked repo can be used instead.
+
+*If you do not have an existing local copy of the shared repo, then skip to [Step 2](#step-2---fork-apollo-via-the-web) below.*
+
+
+### Step 2 - Fork `apollo` via the Web
+
+The easiest way to fork the `apollo` repository is via the GitHub web interface:
+
+- Ensure you are logged into GitHub as your GitHub user.
+- Navigate to the apollo shared repo at [https://github.com/GMOD/apollo](https://github.com/GMOD/apollo).
+- Notice the 'Fork' button in the upper right corner. It has a number to the right of the button.
+![](images/githubForkButton.png)
+- Click the Fork button. The resulting behavior will depend upon whether your GitHub user is a member of a GitHub organization. If not a member of an organization, then the fork operation will be performed and the forked repo will be created in the user's account.
+- If your user is a member of an organization (e.g., GMOD or acme-incorporated), then GitHub will present a dialog for the user to choose where to place the forked repo. The user should click on the icon corresponding to their username.
+![](images/githubForkTarget.png)
+- *If you accidentally click the number, you will be on the Network Graphs page and should go back.*
+
+### Step 3 - Clone the Fork Locally
+
+At this point, you will have a fork of the shared repo (e.g., apollo) stored within GitHub, but it is not yet available on your local development machine. This is done as follows:
+
+    # Assumes that directory ~/MI/ will contain your Apollo repos.
+    # Assumes that your username is MarieCurie.
+    # Adapt these instructions to suit your environment
+    > cd ~/MI
+    > git clone git at github.com:MarieCurie/apollo.git
+    > cd apollo
+
+Notice that we are using the SSH transport to clone this repo, rather than the HTTPS transport. The telltale indicator of this is the `git at github.com:MarieCurie...` rather than the alternative `https://github.com/MarieCurie...`.
+
+*Note: If you encounter difficulties with the above `git clone`, you may need to associate your local public SSH key with your GitHub account. See [Which remote URL should I use?](https://help.github.com/articles/which-remote-url-should-i-use/) for information.*
+
+### Step 4 - Configure the local forked repo
+
+The `git clone` above copied the forked repo locally, and configured the symbolic name 'origin' to point back to the *remote* GitHub fork. We will need to create an additional *remote* name to point back to the shared version of the repo (the one that we forked in Step 2). The following should work:
+
+    # Assumes that you are already in the local apollo directory
+    > git remote add upstream https://github.com/GMOD/apollo.git
+
+Verify that remotes are configured correctly by using the command `git remote -v`. The output should resemble:
+
+
+    upstream    https://github.com/GMOD/apollo.git (fetch)
+    upstream    https://github.com/GMOD/apollo.git (push)
+    origin  git at github.com:MarieCurie/apollo.git (fetch)
+    origin  git at github.com:MarieCurie/apollo.git (push)
+
+
+### Step 5 - Configure  `.bashrc` to show current branch (optional)
+
+One of the important things when using Git is to know what branch your working directory is tracking. This can be easily done with the `git status` command, but checking your branch periodically can get tedious. It is easy to configure your `bash` environment so that your current git branch is always displayed in your bash prompt.
+
+If you want to try this out, add the following to your `~/.bashrc` file:
+
+    function parse_git_branch()
+    {
+      git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ \1/'
+    }
+    LIGHT_GRAYBG="\[\033[0;47m\]"
+    LIGHT_PURPLE="\[\033[0;35m\]"
+    NO_COLOR="\[\033[0m\]"
+    export PS1="$LIGHT_PURPLE\w$LIGHT_GRAYBG\$(parse_git_branch)$NO_COLOR \$ "
+
+You will need to open up a new Terminal window (or re-login to your existing terminal) to see the effect of the above `.bashrc` changes.
+
+If you cd to a git working directory, the branch will be displayed in the prompt. For example:
+
+    ~ $
+    ~ $ # This isn't a git directory, so no branch is shown
+    ~ $
+    ~ $ cd /tmp
+    /tmp $
+    /tmp $ # This isn't a git directory, so no branch is shown
+    /tmp $
+    /tmp $ cd ~/MI/apollo/
+    ~/MI/apollo fix-feedback-button $
+    ~/MI/apollo fix-feedback-button $ # The current branch is shown
+    ~/MI/apollo fix-feedback-button $
+    ~/MI/apollo fix-feedback-button $ git status
+    On branch fix-feedback-button
+    Changes not staged for commit:
+      (use "git add <file>..." to update what will be committed)
+      (use "git checkout -- <file>..." to discard changes in working directory)
+        ... remaining output of git status elided ...
+
+---
+
+## Typical Development Cycle
+
+Once you have completed the One-time Setup above, then it will be possible to create new branches and pull requests using the instructions below. The typical development cycle will have the following phases:
+
+- Refresh and clean up local environment
+- Create a new task-specific branch
+- Perform ordinary development work, periodically committing to the branch
+- Prepare and submit a Pull Request (PR) that refers to the branch
+- Participate in PR Review, possibly making changes and pushing new commits to the branch
+- Celebrate when your PR is finally Merged into the shared repo.
+- Move onto the next task and repeat this cycle
+
+
+### Refresh and clean up local environment
+
+Git will not automatically sync your Forked repo with the original shared repo, and will not automatically update your local copy of the Forked repo. These tasks are part of the developer's normal *cycle*, and should be the first thing done prior to beginning a new development effort and creating a new branch. In addition, this
+
+#### Step 1 - Fetch remotes
+
+In the (likely) event that the *upstream* repo (the apollo shared repo) has changed since the developer last began a task, it is important to update the local copy of the upstream repo so that its changes can be incorporated into subsequent development.
+
+    > git fetch upstream        # Updates the local copy of shared repo BUT does not affect the working directory, it simply makes the upstream code available locally for subsequent Git operations. See step 2.
+
+#### Step 2 - Ensure that 'master' is up to date
+
+Assuming that new development begins with branch 'master' (a good practice), then we want to make sure our local 'master' has all the recent changes from 'upstream'. This can be done as follows:
+
+    > git checkout master
+    > git reset --hard upstream/master
+
+The above command is potentially dangerous if you are not paying attention, as it will remove any local commits to master (which you should not have) as well as any changes to local files that are also in the upstream/master version (which you should not have). In other words, the above command ensures a proper clean slate where your local master branch is identical to the upstream master branch.
+
+Some people advocate the use of `git merge upstream/master` or `git rebase upstream/master` instead of the `git reset --hard`. One risk of these options is that unintended local changes accumulate in the branch and end up in an eventual pull request. Basically, it leaves open the possibility that a developer is not really branching from upstream/master, but is branching from some developer-specific branch point.
+
+
+### Create a new branch
+
+Once you have updated the local copy of the master branch of your forked repo, you can create a named branch from this copy and begin to work on your code and pull-request. This is done with:
+
+    > git checkout -b fix-feedback-button   # This is an example name
+
+This will create a local branch called 'fix-feedback-button' and will configure your working directory to track that branch instead of 'master'.
+
+You may now freely make modifications and improvements and these changes will be accumulated into the new branch when you commit.
+
+If you followed the instructions in [Step 5 - Configure  `.bashrc` to show current branch (optional)](#step-5---configure--bashrc-to-show-current-branch-optional), your shell prompt should look something like this:
+
+    ~/MI/apollo fix-feedback-button $
+
+### Changes, Commits and Pushes
+
+Once you are in your working directory on a named branch, you make changes as normal. When you make a commit, you will be committing to the named branch by default, and not to master.
+
+You may wish to periodically `git push` your code to GitHub. Note the use of an explicit branch name that matches the branch you are on (this may not be necessary; a git expert may know better):
+
+    > git push origin fix-feedback-button   # This is an example name
+
+Note that we are pushing to 'origin', which is our forked repo. We are definitely NOT pushing to the shared 'upstream' remote, for which we may not have permission to push.
+
+
+### Reconcile branch with upstream changes
+
+If you have followed the instructions above at [Refresh and clean up local environment](#refresh-and-clean-up-local-environment), then your working directory and task-specific branch will be based on a starting point from the latest-and-greatest version of the shared repo's master branch. Depending upon how long it takes you to develop your changes, and upon how much other developer activity there is, it is possible that changes to the upstream master will conflict with changes in your branch.
+
+So it is a good practice to periodically pull down these upstream changes and reconcile your task branch with the upstream master branch. At the least, this should be performed prior to submitting a PR.
+
+#### Fetching the upstream branch
+
+The first step is to fetch the update upstream master branch down to your local development machine. Note that this command will NOT affect your working directory, but will simply make the upstream master branch available in your local Git environment.
+
+    > git fetch upstream
+
+#### Rebasing to avoid Conflicts and Merge Commits
+
+Now that you've fetched the upstream changes to your local Git environment, you will use the `git rebase` command to adjust your branch
+
+
+    > # Make that your changes are committed to your branch
+    > # before doing any rebase operations
+    > git status
+        # ... Review the git status output to ensure your changes are committed
+        # ... Also a good chance to double-check that you are on your
+        # ... task branch and not accidentally on master
+    > git rebase upstream/master
+
+The rebase command will have the effect of adjusting your commit history so that your task branch changes appear to be based upon the most recently fetched master branch, rather than the older version of master you may have used when you began your task branch.
+
+By periodically rebasing in this way, you can ensure that your changes are in sync with the rest of Apollo development and you can avoid hassles with merge conflicts during the PR process.
+
+
+#### Dealing with merge conflicts during rebase
+
+Sometimes conflicts happen where another developer has made changes and committed them to the upstream master (ideally via a successful PR) and some of those changes overlap with the code you are working on in your branch. The `git rebase` command will detect these conflicts and will give you an opportunity to fix them before continuing the rebase operation. The Git instructions during rebase should be sufficient to understand what to do, but a very verbose explanation can be found at [R [...]
+
+#### Advanced: Interactive rebase
+
+As you gain more confidence in Git and this workflow, you may want to create PRs that are easier to review and best reflect the intent of your code changes. One technique that is helpful is to use the *interactive rebase* capability of Git to help you clean up your branch prior to submitting it as a PR. This is completely optional for novice Git users, but it does produce a nicer shared commit history.
+
+See [squashing commits with rebase](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) for a good explanation.
+
+
+### Submitting a PR (pull request)
+
+Once you have developed code and are confident it is ready for review and final integration into the upstream version, you will want to do a final `git push origin ...` (see Changes, Commits and Pushes above). Then you will use the GitHub website to perform the operation of creating a Pull Request based upon the newly pushed branch.
+
+See [submitting a pull request](https://help.github.com/articles/creating-a-pull-request).
+
+
+### Reviewing a pull request
+
+The set of open PRs for the apollo can be viewed by first visiting the shared apollo GitHub page at [https://github.com/GMOD/apollo](https://github.com/GMOD/apollo).
+
+Click on the 'Pull Requests' link on the right-side of the page:
+![](images/githubPullRequest.png)
+
+Note that the Pull Request you created from your forked repo shows up in the shared repo's Pull Request list. One way to avoid confusion is to think of the shared repo's PR list as a queue of changes to be applied, pending their review and approval.
+
+### Respond to TravisCI tests
+
+The GitHub Pull Request mechanism is designed to allow review and refinement of code prior to its final merge to the shared repo. After creating your Pull Request, the TravisCI tests for apollo will be executed automatically, ensuring that the code that 'worked fine' on your development machine also works in the production-like environment provided by TravisCI. The current status of the tests can be found near the bottom of the individual PR page, to the right of the Merge Request symbol:
+![](images/githubTestProgress.png)
+![](images/githubTestStatus.png)
+
+TBD - Something should be written about developers running tests PRIOR to TravisCI and the the PR. This may already be in the README.md, but should be cited.
+
+
+### Respond to peer review
+
+The GitHub Pull Request mechanism is designed to allow review and refinement of code prior to its final merge to the shared repo. After creating your Pull Request, the TravisCI tests for apollo will be executed automatically, ensuring that the code that 'worked fine' on your development machine also works in the production-like environment provided by TravisCI. The current status of the tests can be found
+
+### Repushing to a PR branch
+
+It's likely that after created a Pull Request, you will receive useful peer review or your TravisCI tests will have failed. In either case, you will make the required changes on your development machine, retest your changes, and you can then push your new changes back to your task branch and the PR will be automatically updated. This allows a PR to evolve in response to feedback from peers. Once everyone is satisfied, the PR may be merged. (see below).
+
+
+### Merge a pull request
+
+One of the goals behind the workflow described here is to enable a large group of developers to meaningfully contribute to the Apollo codebase. The Pull Request mechanism encourages review and refinement of the proposed code changes. As a matter of informal policy, Apollo expects that a PR will not be merged by its author and that a PR will not be merged without at least one reviewer approving it (via a comment such as +1 in the PR's Comment section).
+
+### Celebrate and get back to work
+
+You have successfully gotten your code improvements into the shared repository. Congratulations! The branch you created for this PR is no longer useful, and may be deleted from your forked repo or may be kept. But in no case should the branch be further developed or reused once it has been successfully merge. Subsequent development should be on a new branch. Prepare for your next work by returning to [Refresh and clean up local environment](#refresh-and-clean-up-local-environment).
+
+---
+
+## GitHub Tricks and Tips
+
+- Add `?w=1` to a GitHub file compare URL to ignore whitespace differences.
+
+
+## References and Documentation
+
+- The instructions presented here are derived from several sources. However, a very readable and complete article is [Using the Fork-and-Branch Git Workflow](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/). Note that the article doesn't make clear that certain steps like Forking are one-time setup steps, after which Branch-PullRequest-Merge steps are used; the instructions below will attempt to clarify this.
+
+- New to GitHub? The [GitHub Guides](http://guides.github.com) are a great place to start.
+
+- Advanced GitHub users might want to check out the [GitHub Cheat Sheet](https://github.com/tiimgreen/github-cheat-sheet/blob/master/README.md)
+
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..bba0ef8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,8 @@
+* Search [mailing list](http://gmod.827538.n3.nabble.com/Apollo-f815553.html) or email list ```apollo at lists.lbl.gov``` if a general setup question. 
+
+* Provide what you were doing and what you expected to see.  Screenshots, directory, config files are a plus if relevant.
+ 
+* Provide the javascript console log output generated from the action. 
+ 
+* Provide the server log output generated from the action (typically ```catalina.out```).
+ 
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1 @@
+
diff --git a/.github/images/githubForkButton.png b/.github/images/githubForkButton.png
new file mode 100644
index 0000000..19166bf
Binary files /dev/null and b/.github/images/githubForkButton.png differ
diff --git a/.github/images/githubForkTarget.png b/.github/images/githubForkTarget.png
new file mode 100644
index 0000000..29c26bc
Binary files /dev/null and b/.github/images/githubForkTarget.png differ
diff --git a/.github/images/githubPullRequest.png b/.github/images/githubPullRequest.png
new file mode 100644
index 0000000..c8dad32
Binary files /dev/null and b/.github/images/githubPullRequest.png differ
diff --git a/.github/images/githubTestProgress.png b/.github/images/githubTestProgress.png
new file mode 100644
index 0000000..83233de
Binary files /dev/null and b/.github/images/githubTestProgress.png differ
diff --git a/.github/images/githubTestStatus.png b/.github/images/githubTestStatus.png
new file mode 100644
index 0000000..cb416b2
Binary files /dev/null and b/.github/images/githubTestStatus.png differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3faeeac
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+.idea/**
+apollo.i*
+*.swp
+*.swo
+target/**
+config.properties
+config.xml
+blat_config.xml
+.project
+.classpath
+*.iml
+*.ipr
+*.iws
+out
+web-app/jbrowse
+jbrowse-github
+jbrowse-download
+log4j2.json
+log4j2-test.json
+tmp
+version.jsp
+update_client.sh
+*.log
+apollo-config.groovy
+gwt-unitCache/**
+web-app/WEB-INF/deploy/**
+web-app/annotator/**
+/extlib/
+setup.log
+/src/perl5/
+/bin/
+/site/
+docs/Makefile
+.gradle
+.slcache
+restapi.doc
+package-lock.json
+jb_run.js
+jb_setup.js
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..bdd20bc
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,29 @@
+{
+    "asi": true,      //allow missing semicolon
+    "-W041": false,
+    "-W009": false,
+    "-W010": false,
+    "-W018":false,    //incorrect use of !
+    "-W032":false,    //unnecessary semicolon allowed
+    "expr": true,     // allow returning assignment expressions
+    "boss": true,     // allow if statement assignment expressions
+    "browser": true,
+    "dojo": true,
+    "evil": true,     // allow eval
+    "laxbreak": true,
+    "laxcomma": true,
+    "loopfunc": true,
+    "funcscope": true,
+    "maxlen": 10000,
+    "indent": 4,
+    "shadow": true,
+    "maxerr": 250,
+    "sub": true,
+    "predef": [ "require", "define", "SockJS", "Stomp", "bbop", "amigo" ],
+    "maxcomplexity": 40,
+    "indent": 2,
+    "undef": true,
+    "trailing": true,
+    "devel": true
+}
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed76d8f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,57 @@
+sudo: required
+dist: trusty
+group: deprecated
+
+language: groovy
+node_js:
+  - "iojs"
+
+env:
+  - DB=postgres
+  - DB=mysql
+  - DB=h2
+
+addons:
+  postgresql: "9.3"
+  apt:
+    packages:
+    - mysql-server-5.6
+    - mysql-client-core-5.6
+    - mysql-client-5.6
+    - oracle-java8-installer
+
+services:
+- postgresql
+- mysql
+
+before_install:
+  - npm install -g jshint
+
+before_script:
+  - node -v
+  - npm -v
+  - "echo $JAVA_OPTS"
+  - "export JAVA_OPTS='-Xmx2048m'"
+  - "echo $JAVA_OPTS"
+  - if [ ${DB} == "mysql" ]; then cp test/config/mysql.travis apollo-config.groovy; mysql -u root -e 'SET GLOBAL interactive_timeout=28800;'; mysql -u root -e 'SET GLOBAL wait_timeout=28800;'; mysql -u root -e 'SET GLOBAL connect_timeout=28800;';  mysql -u root -e 'create database apollo'; fi;
+  - if [ ${DB} == "postgres" ]; then cp test/config/postgres.travis apollo-config.groovy; psql -c 'create database apollo;' -U postgres; psql -c 'create database apollo_chado' -U postgres; gunzip -c scripts/chado-schema-with-ontologies.sql.gz | psql -U postgres -h localhost -d apollo_chado; fi;
+
+before_cache:
+  - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+
+
+cache:
+  directories:
+    - extlib/
+    - $HOME/.grails/
+    - $HOME/.m2/
+    - $HOME/.npm/
+    - $HOME/.gradle/caches/
+    - $HOME/.gradle/wrapper/
+script:
+  - travis_wait ./gradlew handleJBrowse copy-resources gwtc && ./grailsw refresh-dependencies --stacktrace && ./grailsw test-app -coverage -xml --stacktrace
+  - node web-app/jbrowse/src/dojo/dojo.js load=build --require "web-app/jbrowse/src/JBrowse/init.js" --profile "web-app/jbrowse/plugins/WebApollo/js/WebApollo"
+  - jshint client/apollo/js
+after_success:
+  - ./grailsw coveralls
+
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
index 0000000..0e4f464
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,648 @@
+
+
+## 2.0.8
+
+Features
+
+- Added the ability to annotate from high performance [Alignments2](http://gmod.org/wiki/JBrowse_Configuration_Guide#Alignments2) BAM reads [#1789](https://github.com/GMOD/Apollo/pull/1789)
+- Added support for indexed FASTA to be used as reference sequence. [#1791](https://github.com/GMOD/Apollo/pull/1791)
+- Added sequence API [#1799](https://github.com/GMOD/Apollo/pull/1799)
+
+Bugfixes 
+
+- Fixed bug when flipping strand fails to flip the strand of the owning gene [#1769](https://github.com/GMOD/Apollo/issues/1769)
+- Fixes to track services to allow remote jbrowse tracks and jsonz [#1767](https://github.com/GMOD/Apollo/issues/1767)
+- Fixes to track services to return 404 when bad URL is given [#1768](https://github.com/GMOD/Apollo/issues/1768)
+- Fixed CORS issues [#1760](https://github.com/GMOD/Apollo/issues/1760)
+- Fixed bug where gene positions are sometimes wrong when a longer isoform is deleted from a gene [#1770](https://github.com/GMOD/Apollo/issues/1770)
+- Fixed permissions for REMOTE_USER when using web services [#1759](https://github.com/GMOD/Apollo/issues/1759)
+- Fixes build issues [#1756](https://github.com/GMOD/Apollo/issues/1756) [#1752](https://github.com/GMOD/Apollo/issues/1752) [#1773](https://github.com/GMOD/Apollo/issues/1773)
+- Fixes error in SQL query for listing alterations [#1754](https://github.com/GMOD/Apollo/issues/1754)
+- Minimum node version is version 6
+- Fix UserPanel for a large number of users [#1800](https://github.com/GMOD/Apollo/pull/1800)
+- Fixes recalculated gene positions for some delete exon operations [#1808](https://github.com/GMOD/Apollo/issues/1808)
+- Fix big when updating organism via web-service [#1804](https://github.com/GMOD/Apollo/issues/1804)
+
+
+## 2.0.7
+
+Features
+
+- Add the ability to upload organism sequence data and track data to a remote Apollo instance via Apollo Web Services [#1670](https://github.com/GMOD/Apollo/pull/1670).
+- Allow setting of alternate translation table per organism using the _Details_ panel under the _Organism_ tab in the Annotator panel. [#95](https://github.com/GMOD/Apollo/issues/95)
+- Draggable BAM tracks now support coloring by strand. Reads aligned to forward strand are colored blue, while those in the reverse strand are red.  [#412](https://github.com/GMOD/Apollo/issues/412)
+- The list of _Tracks_ in the Annotator panel now allows for the separation of data types into categories. [#536](https://github.com/GMOD/Apollo/issues/536)
+- Tracks in a category can be added or removed all at once. [#1733](https://github.com/GMOD/Apollo/pull/1733)
+- When applicable, warnings now alert users of insufficient permissions to perform certain functions. [#553](https://github.com/GMOD/Apollo/issues/553)
+- Restrictions are now in place to prevent users from modifying or deleting annotations that they did not create. [#1260](https://github.com/GMOD/Apollo/issues/1260)
+- Updated settings for the ability to filter by organism when applying metadata. For instance, admin may now apply canned comments, keys and values, only to a subset of organisms in their server. As well, statuses can be retrieved per type of genomic element, per organism, etc. [#1676](https://github.com/GMOD/Apollo/pull/1676)
+- Admins can now build public URLs to hyperlink directly to a specific genomic element. [#1482](https://github.com/GMOD/Apollo/pull/1482)
+- It is now possible to set _Statuses_ as well as adding or editing _Canned elements_ using our Web Service (REST) API. [#1538](https://github.com/GMOD/Apollo/pull/1538)
+- In the absence of ```Name``` attribute in GFF3 file, Apollo uses ```ID``` attribute to name the annotation in JSON. [#1639](https://github.com/GMOD/Apollo/pull/1639)
+- A number of other improvements to performance have been made, such as fetching preferences from session. [#1604](https://github.com/GMOD/Apollo/pull/1604) [#1725](https://github.com/GMOD/Apollo/pull/1725)
+- Added date created field to changes report. [#1728](https://github.com/GMOD/Apollo/pull/1728)
+- Removal of bower in favor of npm to install JBrowse. [#1691](https://github.com/GMOD/Apollo/pull/1691)
+- Added documentation for a Web Service wrapper for Python, PHP, etc. See [Web Services API documentation](http://genomearchitect.readthedocs.io/en/latest/Web_services.html).
+
+Bugfixes
+
+- Fixed bug in which ```add_transcripts_from_gff3_to_annotations.pl``` replaced valid mRNA name with gene name. [#1475](https://github.com/GMOD/Apollo/issues/1475)
+- Fixed bug in which ```REMOTE_USER``` was not cached everywhere and was being ignored on ping request. [#1492](https://github.com/GMOD/Apollo/pull/1492)
+- Added warning to Production pre-requisites: if using gradle and gradlew, admins should define ```JAVA_HOME``` to avoid build fails. See documentation [here](http://genomearchitect.readthedocs.io/en/latest/Setup.html#production-pre-requisites).
+- Fixed sorting bug on the dropdown list of organisms. [#1497](https://github.com/GMOD/Apollo/issues/1497)
+- Fixed a bug in which the absence of an organism created downstream issues such as errors listing groups of users in Annotator panel. (Feature for admins). [#1504](https://github.com/GMOD/Apollo/pull/1504)
+- Fixed a bug in which creating a user via Web Service API generated an error message. [#1510](https://github.com/GMOD/Apollo/pull/1510)
+- Fixed import script ```add_transcripts_from_gff3_to_annotations.pl``` to introduce correct handling of sequence alterations and read-through stop codons. [#1524](https://github.com/GMOD/Apollo/pull/1524)
+- Fixed bug that now allows leading start non-M codons in organisms with non-standard code to be translated as Methionine (M). [#1544](https://github.com/GMOD/Apollo/issues/1544)
+- Updated GWT code to fix a bug that prevented Apollo from generating URLs appropriately - pipes were not being encoded. [#1606](https://github.com/GMOD/Apollo/pull/1606)
+- Fixed bug in the calculation of open reading frames for the negative strand for the purpose of coloring each exon according to the CDS. Exported sequences had been - and remain - correctly generated. [#1629](https://github.com/GMOD/Apollo/issues/1629)
+- Fixed bug that delayed propagation of updates when boundaries for an annotation's parent element were changed. [#1631](https://github.com/GMOD/Apollo/issues/1631) 
+- Restored _'Pin to top'_ and _'Delete track'_ functionality for tracks with ```HTMLFeatures```. [#1632](https://github.com/GMOD/Apollo/issues/1632)
+- Fixed cascade bug when changing annotation type for an annotation that has a read-through stop codon. [#1717](https://github.com/GMOD/Apollo/pull/1717)
+- Apollo client being initialized twice in some instances. [#1742](https://github.com/GMOD/Apollo/issues/1742)
+
+
+
+## 2.0.6
+
+Features
+
++ Moved the native track panel button to the main window #1398
++ Add new 'default_group' param for remote_user auth #1445
++ Added icon to toggle view of native JBrowse tracks that is always visible #1452
+
+Bugfixes
+
++ Failure to load tracks when switching organisms with identical Sequence IDs #1391
++ Unable to add organism from script without a pre-existing organism #1388
++ When logged in, clicking on JBrowse would not load the Annotator Panel #1395
++ ```server_data``` may lock some times in dev mode #1419
++ Intron persists in tracks if a single exon if neat features enabled #1417
++ Authentication error with galaxy tools + remote_user #1423
++ When logged in as non-admin user, the show track panel button does not look or work properly #1429
++ When deleting an organism from the interface, should instantly update the organism list similar to add organism #1431
++ 404 errors on CSV metadata in some cases #1448
++ Problems loading list of Tracks when switching organisms on slower connection #1434
++ Setting exon details in details panel fails to set transcript boundaries properly #1428
++ Error opening on double-click for annotations listed in the second page of the annotation panel #1459
++ Transcripts of pseudogenes should NOT have the word 'transcript' or other type in the name #1451
++ Issue with gene names when performing undo and redo right after changing annotation type #1464
++ Interface freezes if right-clicking an unselected annotation #1465
++ Fixed issue where double-click on transcript navigates and then closes the transcript / gene #1467
++ Remote_user not authorized properly everywhere #1468
++ Fixes the small problem with the sticky tracks from loadLinks #1474
++ Bumped default JBrowse version
+
+
+## 2.0.5
+
+Features 
+
++ Increase UI and performance by calling setCurrentSequenceLocation less aggressively #1007
++ Various performance improvements #1272,#1276
++ Added ability for Apollo to call home to track server usage #1339
++ Allow multiple calls to google analytics to support users internally #1340
++ Numerous annotator tab user interface improvements #1343,#333
++ Updated 'changes' report page for more detail and better filtering #806
++ Added URL option to open and close the track panels #1332
++ Adds a findChanges web-services method #1316
++ Importing features should be able to optionally include metadata #52
++ Make organism, groups, users tabs more consistent #622
++ Convert javascript "alert" to something more appealing visually #630
++ Using Bootstrap in all panels #847
++ Improved report login window look and feel #1103
++ Upgraded to Java 8  #1327
++ Upgraded to Gwt 2.8.0 and Gwt-Bootstrap 0.9.4 #1075
++ Delete expired preferences #1368
++ Location URLs are now encapsulated in links #1361
++ Bumped default JBrowse version 
+
+
+Bugfixes
+
++ Web-service method 'getUserPermissionsForUser' #1230
++ UI glitch with more than ten groups #1242
++ Error going between full-screen and annotator panel when stored are loaded in the URL #1156,#1214,#1271,#1330,#1331, #1008, #1371
++ 'use_cds_for_new_transcripts' was not being picked up properly #1297
++ Client interprets 5'UTR and 3'UTR features as exons #1308
++ Flag when 3' end or 5' end are missing, still calculate longest ORF #1302
++ Limit Chado export to admins #1322
++ Improved security of non-public genomes #861
++ Adding transcripts via the load transcripts script added preferences #1277
++ Was not validating scaffold against organism on import #1173
++ Sequence display with annotation did not update automatically when moved to opposed strand #645
++ Needed to update the coding detail panel when changing transcript details #379
++ Warn user when making an intron is not possible #1331
++ Load script was returning the wrong error when the wrong password was given #1275
++ use_cds_for_new_transcripts is not being picked up properly #1297
++ Improve merging of functional annotations data during merge operation #646
++ Annotator panel calls the official set exon boundaries method #653
++ Pinstripes disappear in small scaffolds. #709
++ Spring to splice site functionality works once only #735
++ Genomic insertion coordinates: start is greater than end in gff3 output from Reference Sequence tab #747
++ Remove errant bootstrap calls #746
++ Remove Environment.TEST from code #655
++ No glyphs for sequence alterations #908
++ getUserPermissionsForUser is non-standard #1230
++ Fixed case in Webservices docs #1244
++ UI glitch on Groups tab when more than ten groups #1242
++ Fixed error with setting upstream donor failing #1379
++ NCBI Pubmed upgraded to use https form discontinued http #1377
++ Updating exon details in annotator panel fails #1375
+
+
+## 2.0.4
+
+Features
+
++ Upgraded to Grails 2.5.5
++ Allow REMOTE_USER authentication (apache / nginx) and added pluggable user authentication (#1042)
++ Ability to enter pre-specified (canned) values for Attributes in the 'Information Editor', similar to canned Comments. (#86)
++ Users may download the genomic sequence from highlighted regions using the export menu in the _User-created Annotations_ area export (#1163)
++ Information from parent features can be retained when loading transcripts onto the 'User-created Annotations' area using the add_transcript_from_gff3_to_annotations.pl loading script (#1171)
++ Added [documentation for using Apollo with Docker](https://github.com/GMOD/Apollo/blob/master/docs/Setup.md#configure-for-docker) (#1016)
+
+
+Bugfixes
+
++ Fixed multiple errors in the add_transcript_from_gff3_to_annotations.pl loading script (#1146)
++ Expired sessions or server disconnection triggers reconnection instead of a silent failure (#493)
++ Fixed bugs in certain web service scripts (#1155)
++ Fixed bug where garbage client token is created (#1172)
++ Fixed several deployment and installation issues (#1135, #1137, #1138, #1150)
++ Fixed adding comments to a sequence alteration fails to create the sequence alteration (#1179)
++ Parameters passed into loadLink are no longer dropped (#1140)
++ Fixed bug not allowing addition of FeatureType in admin menu (#1144)
++ Improved performance of loading the information editor for large genes (#1152)
++ Fixes error when deleting a DBXref or GO ID error (#1163)
+
+
+## 2.0.3
+
+__Warnings__
+
+* __Before doing the build with Apollo v2.0.3, make sure to install node.js (which includes npm) and install bower. This process will be automatic in v2.0.4 and fixes are detailed in [#1138](https://github.com/GMOD/Apollo/pull/1138).__
+* __Download node.js from https://nodejs.org/en/download/__
+* __To install bower:__
+    * <code>npm install -g bower</code>
+* __Then__:
+    * <code>./apollo clean-all</code>
+    * <code>./apollo deploy</code>
+
+Features
+
++ Added Ability to export Apollo annotations to a Chado database (#145).
++ Added ability to update an existing Chado database for Chado export (#967).
++ Updated application to use Grails 2.5.4 (#444).
++ Allow apollo-config.groovy configuration to create admin user on startup (#1040).
++ Added ability to change feature type in User created annotations track (#220).
++ Added ability to use multiple organisms at the same time (#441).
++ Added ability to search selected sequences (for export) and clear selection in Sequence Panel (#730).
++ Added ability to allow username to be a non-email based name (#939).
++ Sync with JBrowse 1.12.2-apollo for stability (#971).
+
+Bugfixes
+
++ Fixed a bug were set translation start, in an intron, produces an uncaught out of bounds exception (#532).
++ Remove alternate hover CSS on tables in Annotator Panel for better visibility of entries (#632).
++ Fixed clientToken not found error for operation `get_gff3` and `get_sequence` (#1027).
++ Fixed a bug where PubMed and Gene Ontology lookup, in Information Editor, fails (#1028).
++ Fixed issues with export sequence API (#1045).
++ Fixed a bug where changing the number of transcripts of a gene did not update the drop-down in Information Editor (#587).
++ Fixed a bug where the bookmark icon did not show up for the current feature in History window upon revert (#769).
++ Fixed a bug where CDS FASTA export was attempting to export sequences of ncRNAs. (#833).
++ Fixed a bug where using `?organism=organismName` does not work if logged in (#845).
++ Fixed a bug where if a mRNA, without a strand, is added to the annotations track then it cannot be assigned a strand (#873).
++ Fixed a bug where import generates unique 'gene' name based on the existing gene (#879).
++ Fixed a bug where Comments field was restricted to 256 characters (#963).
++ Fixed a bug where an undo operation on a pseudogene causes an error (#1001).
++ Fixed inconsistencies when converting a feature to a JSONObject and vice-versa (#1003).
++ Fixed clientToken error originating from `getCurrentOrganismForCurrentUser` (#1054).
++ Fixed UI problems with 'Full-screen view' mode (#1055).
++ Fixed a bug where `targetURL` was not preserved properly through login in loadLink and Reports page (#1058).
++ Fixed a bug where user/group selection drop-down goes the wrong direction (#1066).
++ Fixed a bug where creating a pseudogene, repeat region or transposable element from a Canvas feature track led to an error (#1077).
++ Fixed a bug when creating a non-coding Transcript owner was not set and the username was not being displayed when hovering (#1085). 
+
+## 2.0.2
+
+Features
+
++ Added ability to filter summary of annotations by user (#79).
++ Re-added translation table support (#757).
++ Improved speed of GFF3 export via optimization (#274).
++ Enhanced GFF3 export of sequence alterations by including the altered residues (#754).
++ Added warning for Tomcat memory settings when doing apollo deploy (#767).
++ Replaced the Split Panel interface of Annotator with Dock Panel (#768).
++ Added label to the 'Revert' button in History window (#769).
++ Added ability to sort annotations by 'Last Updated' on the Annotator Panel (#770).
++ Added ability to add a comment to sequence alterations (#781).
++ Added annotation count to the Sequences Panel (#803).
++ Optimized pagination in Annotator Panel when dealing with large number of annotations (#820).
++ Enhanced the web service API to enable the ability to edit name of features (#776).
++ Enhanced the web service API to enable the ability to add GO, Dbxref, attributes and publications to features (#829).
++ Improved the speed of FASTA export via optimization (#854).
++ Updated history window to indicate current position (#797).
+
+Bugfixes
+
++ Fixed a bug where sequence modifications weren't being included in the GFF3 export (#748).
++ Provide alternate translation table support on the client (#759).
++ Fixed a bug that causes a persistent screen over 'Links to this Location' popup (#778).
++ Fixed a bug that made it unable to retrieve GFF3 of pseudogenes (#784).
++ Fixed a bug where merging or splitting transcripts and then doing an undo causes error (#842).
++ Fixed a bug which causes links to transcript on the second page of the Annotator Panel to fail (#801).
++ Fixed a bug which causes filter and sort to be mutually exclusive in Changes report page (#824).
++ Fixed a bug where repeat regions, transposable elements and sequence alterations were not part of GFF3 export (#836).
++ Fixed a bug where merging of transcripts fail in certain cases (#850).
++ Fixed a bug that was as a result of using native javascript confirm boxes (#851).
++ Fixed a bug where removing a dbxref visually removes the feature from track (#764).
++ Fixed a bug which causes all genomic elements to disappear when 'Show minus strand' option was selected (#782).
++ Fixed a bug where modification of a gene model was taking a long time when the backend was MySQL (#743).
+
+## 2.0.1
+
+Features
+
++ Added more intuitive paging to sidebar panels (#700).
++ Added support for flatfile-to-json tracks loaded with the `--compress` option in the data loading pipeline (#517).
++ Added right-click option to CanvasFeatures tracks to "Create new annotation" (alternative to drag and drop) (#576).
++ Added an option for enabling/disabling the public JBrowse mode (#433).
++ Added an `-ignoressl` option to bypass certificate authority for groovy command line scripts (#557).
++ Made the administration of group membership more clear from the User/Group panels (#598).
++ Added scripts and web services for managing user and group permissions (#595).
++ Added additional Web Services API documentation generated from source code annotations (#582).
++ Added ability to toggle the JBrowse tracklist from the annotator panel (#597).
++ Added ability to collapse HTMLFeatures evidence tracks from track menu (#571).
++ Added case insensitive search to annotator panel search boxes (#575).
++ Added some bootstrap styling to annotator panel features (#489).
++ Added a feature to remember the width of the annotator panel (#591).
++ Added scripts and web services for deleting all features from a given organism (#539).
++ Added proxy support to support https servers and general user-configurable proxy support (#148).
++ Improved user experience for login, user, and group pages (#603, #601, #592).
++ Improved, expanded, and automated web services documentation (#546).
++ Added feature to let users change their own password (#620).
++ Updated the /featureEvent/changes page to show list of recently changed features (#642).
++ Enhanced the loadGroups and loadUsers API to retrieve the info of specific groups or users (#643).
++ Enhanced the findAllOrganisms API to retrieve the info of specific organisms (#666).
++ Added ability to reference the organism by name in the jbrowse URL for easier to remember URL formats (#653).
++ Added a get_fasta.groovy script to fetch FASTA for annotations via web services.
+
+Bugfixes
+
++ Fixed the permissions to only allow the global admin role to create and delete organisms (#542).
++ Fixed an issue with JBrowse compatibility for certain refSeqs.json files not containing length.
++ Fixed the calculation of isoform overlap (#558).
++ Fixed a bug that made certain annotation operations slow down over time (#555).
++ Fixed a bug that made changing the location of the organisms's data directory cause problems (#567).
++ Fixed a bug that occured when splitting and merging a transcript back together again (#588).
++ Fixed a bug that prevented multiple values for an attribute in the Information Editor (#579). 
++ Fixed a bug preventing features with long names (#580).
++ Fixed a bug where a closed track in the genome browser was not showing up as un-checked in the side-panel (#554). 
++ Fixed a bug where the group permissions where not being displayed correctly on the Group panel (#664).
+
+
+## 2.0.0
+
+Bugfixes
+
++ Organism panel not showing all organisms (#540).
++ Admins for specific organisms have issues with giving other users permissions (#542).
+
+## 2.0.0-RC6
+
+Bugfixes
+
++ Fixed multiple bugs having to do with sequence alterations (#534, #531, #458, #456).
++ Fixed logout for multiple windows on the same browser (#480).
++ JBrowse only mode not listening to websockets (#537).  
+
+
+## 2.0.0-RC5
+
+Features
+
++ Optimized transcript merging (#529,#515).
+
+Bugfixes
+
++ History operations fail when setting acceptor / donor (#530). 
+
+## 2.0.0-RC4
+Features
+
++ Optimized transcript merging (#529).
++ Added "add_comment", "add_attribute", "set_status" to the web services API (#492). 
++ Add an interim export panel (#78).
++ Added google analytics integration (#146).
+
+Bugfixes
+
++ User's last location isn't preserved on page on page refresh (#522).
++ Added security to report pages (#513).
++ Unble to add / view more than 4 organism permissions (#512).
++ Set current sequence dropdown not selecting sequence (#511).
++ Peptide sequence not exporting properly (#453).
++ Peptide sequence not exporting properly (#453).
+
+
+## 2.0.0-RC3
+
+Features
+
++ Added CSV downloads of reports, and created more extensible framework for creating customizable reports.
++ Updated application to use Grails 2.4.5.
++ Updated bulk loader to support loading for a specific organism with the --organism parameter (See #505).
++ Updated bulk loader to support looking up the name of a feature from a specific GFF3 tag with the --name_attributes. Thanks to @anaome for the idea and implementation (See #396).
++ Raise limit on number of tracks allowed in track panel (#502).
++ Added some database optimizations for retrieving sequence features (#504, #452).
++ Optimized annotation panel (#389).
++ UI improvements (#385).
++ Add compression to gzip / fasta (#252).
++ Add stress testing frameworks (#137).
+
+Bugfixes
+
++ Fixed bug that prevented deleting of certain isoforms after database optimizations were applied in RC2 (#497).
++ Moving to opposite strand was not recalculating the ORF (#468).
++ Secure GFF3 / FASTA export (#464, #467).
++ Unable to add organisms from some users (#463).
++ Fixed bug when not stopping (#448).
++ Readthrough stop codons are not being highlighted after undo/redo (#400).
+
+
+
+## 2.0.0-RC2
+
+Features
+
++ Added a PDF version of documentation to [readthedocs](http://genomearchitect.readthedocs.io/).
++ Added a button to generate a public link to the genome browser for a particular organism.
++ Added ability to manage statuses, custom comments, and feature types using the "Admin panel".
++ Added a feature to logout all instances of webapollo from different windows if one window is logged out (#409).
++ Added stress testing scripts using JMeter and tested application reliability.
++ Added new report pages for getting overview of annotations, organisms, sequences, users, and system performance statistics.
++ Added asynchronous data provider to the "Annotator panel" for faster "on-demand" download of data.
++ Added customizable data adapter configurations in the configuration guide.
++ Added gzip functionality to data downloads (#252).
++ Added command line exporter for GFF3.
+
+Bugfixes
+
++ Fixed small bug with permission checking on creating new organism permissions (#463).
++ Fixed bug with stop codons being retained in peptide sequence exports (#448).
++ Fixed bug with stop codon readthrough features not being restored after "Undo/Redo" (#400).
++ Fixed a bug with downloading data files (#464).
++ Fixed a bug that prevented running multiple instances of Apollo at the same time (#462).
++ Fixed a bug where tracks without a key would cause track panel to produce error (#461).
+
+## 2.0.0-RC1
+
+Features
+
++ Created a major rewrite of the backend using [Grails](https://grails.org/), which is a scalable, high-concurrency framework based on Spring and Hibernate
++ Added ability to support multiple organisms in a single application instance.
++ Added organism-level permissions for users and groups and created new admin panel for setting these permissions.
++ Added webservices and command-line scripts for creating new organisms (#360).
++ Added webservices and command line scripts for adding users and [support for migrating annotations from WA1 to WA2](https://github.com/GMOD/Apollo/blob/master/docs/Migration.md) (#255).
++ Created a new "Annotator panel", a side-bar for viewing annotations, reference sequences, export options, and admin features.
++ Added ability to load Apollo client-side plugin automatically, so there is no need to run add-webapollo-plugin.pl (#435).
++ Implemented new data models for Hibernate with support for MySQL, PostgreSQL, Oracle, and H2.
++ Implemented simplified configuration via apollo-config.groovy.
++ Added websocket implementation for annotation updates, with long-polling fallback (#14).
++ Optimized non-canonical splice site search (#454).
++ Updated undo/redo operations to work in WA2.0 and fixed several issues with undoing merged transcript operations (#356).
+
+
+Bugfixes
+
++ Fixed several bugs with sequence alternations (#442, #447, 428, #420).
++ Fixed bug with in-frame stop codons not being identified after manually setting translation start (#55).
++ Fixed bug with exon identified as having non-canonical splice site with non-existent boundaries (#16).
++ Fix merge of unlike annotation types, causes mixed subfeatures (#23).
++ Fixed bug with where CDS calculation was triggered on non-coding features (#30).
++ Extension of mRNA causes 500 error (#27).
+
+
+## 1.0.4 release
+
+Features
+
++ Update to JBrowse 1.11.6 (http://jbrowse.org/jbrowse-1-11-6/)
++ Added new Help page with Web Apollo specific content (#153).
++ Added Drupal authentication module to share authentication with an existing Drupal DB (#117).
++ Made "Show track labels" and "Color by CDS" more persistent (#120).
++ Added a "Collapse" option to the "User-created Annotations" track. The track labels are automatically removed when selecting to "Collapse" the track, but can be shown again. (#155).
++ Changed maxHeight on "User-created Annotations" track to prevent overflow (#124).
++ Allow single-level features to be dragged to the "User-created Annotations" track for editing (#193).
+
+Bugfixes
+
++ Updated URL to new server to access Gene Ontology terms (GOLR) (#190).
++ Fixed an issue where the API could be used to create random berkeley DBs (#152).
++ Fixed the sample log4j2 implementation and added extra notes on it to the documentation (#151).
++ Fixed an issue where the config files were readable by the outside world in previous 1.x versions.
++ Changed default user database to be encrypted. Unencrypted options are still available via command line scripts for people with older configurations (#147).
++ Fixed bug where Tomcat could report "Too many open files error" (#162).
++ Fixed bug where the dark theme made the reference sequence too dark (#119).
++ Added some basic help text for search parameters in the sequences view (#160).
++ Added MIME types for bigwig files (#166).
++ Added labels to the boxes in the Sequence Search page to inform user of acceptable query options (#158).
++ Fixed GFF3 export to update fileds with non-specified phase, score, and strand (#177).
++ Fixed "Previous" button on sequence page to update datagrid appropriately (#176).
++ Fixed "Show track labels" feature that was causing feature labels to go offscreen (#179).
++ Renamed "Edit Annotation" menu item to "Edit Information" and camel-case file-menu options. (#172)
++ Fixed mislabeled column in "Changes" page (#169). 
++ Fixed "Add sequence search track" function not matching amino acid queries (#168).
++ Fixed bad layout on "Changes" page (#180).
++ Fixed plus/minus strand filters making bigwig score go to zero (#181).
++ Fixed problem encountered with using iframe embedded mode options (#183).
++ Fixed problem with "Add user" popup using outdated server configuration (#182).
++ Fixed minor issue raised when attempting to retrieve (non-existent) peptides from untranslated regions (#157).
+
+## 1.0.3 release
+
+Features 
+
++ Added ability to view GFF3 for individual annotations
+
+Bugfixes
+
++ Speed up set\_track\_permissions.pl (#118)
++ Fix some cases where error reporting was broken on login pages (#111)
++ Fix Chado export case where DBXref or DB were not pre-existing (#103)
++ Fixed issue where HTTP header size could become large when exporting all tracks (#101)
++ Fixed issue when jbrowse "bin" directory not created properly during deployment (#97)
++ Added apollo "release" target to build a precompiled target (#96)
++ Provided support to visualize GFF3 files on a per-feature basis (#89)
++ Fixed URL encoding of multiple attributes with the same key in GFF3 export (#82)
++ Fixed GFF3 and FASTA export where no annotations existed (#62)
++ Fixed where genome insertion trigers recalculate CDS on non-coding features (#30)
+
+## 1.0.2 release 
+
+Features: 
+
++ Using JBrowse 1.11.5
++ Make subfeature unresizable after it becomes unselected
++ Added functionality for extending to downstream/upstream acceptor/donor splice sites
++ Added EncryptedLocalDbUserAuthentication option which allows use of encrypted database but does not require it
++ Added support for EncryptedLocalDbUserAuthentication in add_user.pl,change_user_password.pl
++ Added ability to encrypt an unencrypted  database via encrypt_passwords.pl
++ Added "revert" buttons on rows in history to allow one click change of state
++ Added "{" and "}" for navigating between top level features
++ Added feature coordinates for get_sequence output
++ Added "disableJBrowseMode" option to disable JBrowse mode
++ Added a forceRedirect option for logins that can be used for custom login classes
++ Added tool to split all isoforms into individual genes (RemoveIsoforms.java)
++ Added tool for fixing gene boundaries based on children transcripts (FixGeneBoundaries.java)
++ Using javascript minimization for JBrowse+Apollo to allow faster initial load time
++ Using Maven build system for deployment ([See new installation guide] (https://github.com/GMOD/Apollo/blob/master/docs/index.md))
++ Integrate with TravisCI: https://travis-ci.org/GMOD/Apollo
++ Added menus for changes / sequences to annotation screen, fixing memory issues from selectTrack.jsp and recentChanges.jsp pages.
++ Added ability to hide track labels under "view".
++ alt/Option click brings up Information Editor on annotated track.
++ Added webservices doc links to interface.
++ Added command line exporters for GFF3 files.
++ Added light / dark color schemes.
+
+Bugfixes:
+
++ Fixed phase in GFF3 output
++ Disabled scrollToPreviousEdge/scrollToNextEdge if the feature is fully visible at the current zoom level
++ Included Chado libraries that were missing in 2014-04-03 release
++ Removed 'null' tracks from changes.
++ Menus use proper CSS pointer.
+
+
+## 2014-04-03 release
+
+Features:
+
++ Using JBrowse 1.11.3
+- Removed "Information" from context menu
++ Renamed "Annotation Info Editor" to "Information Editor" and added date created and date last modified fields
++ Added tooltips when hovering over annotations in annotation track (displays type, owner, date of most recent modification)
++ Changed the behavior when dragging discontiguous features into annotation track from creating separate features to creating a single feature
++ Added functionality for setting end of translation
++ Added the ability to switch between isoforms when editing meta data in the annotation info editor
++ Enabled floating arrow heads appear to show orientation of feature when zoomed in too much to see the whole feature
++ Added <export_source_genomic_sequence> option for whether to export the underlying source genomic sequence data for GFF3 adapter
++ Search results from reference sequence selection screen how opens up JBrowse with the region highlighted (the same behavior as when searching from within JBrowse itself)
++ Search result window within JBrowse now doesn't automatically close when selecting a result (requires explicit closing from the user)
++ Deleting all subfeatures now pops up a warning about deleting the whole feature not being undoable, like when selecting the parent feature and deleting that
++ History page (recentChanges.jsp)
++ Added the ability to export metadata in in deflines for FASTA exporting
++ Can now continue dragging an exon boundary after initial drag (used to require reclicking on the exon)
++ FASTA output in GFF3 now contains at most 60 residues per line
++ Can now directly set an annotation to a specific state from the history display
++ Added / updated filters for changes and sequences to be more memory efficient and added added filters.
+
+Bugfixes:
+
++ Fixed bug of not applying timestamp to temporary BLAT searches (also now requires unique token to guarantee uniqueness)
++ Fixed building of feature_relationship pointers in hybrid data store
++ Long polling now tries to reconnect when connection returns a status of 0 (up to five times, at increasing timeouts)
++ Fixed coloring by CDS bug
++ Fixed showing DNA residues on annotation when zoomed in enough
++ Create DB entry on Chado if it doesn't already exist when writing data to Chado
++ Fixed issue with null time last modified meta data
++ Fixed issue with null owner meta data
++ Chado writer will now throw an exception if an expected entity isn't found
++ Fixed 1-off error in BLAT searches for the end of the match
++ Added fixes for properly setting name for non mRNA features
++ Fixed issue where single level features sometimes disappeared while scrolling
++ Fix error adding transposable_element annotation with only a match and no match_part
++ Fix off-by-one error in blat highlight
++ Fixed empty block that hides tracks on top when zooming in to base level then zooming out while not logged in
++ In sequences, data adapters will now not appear in the Export submenu if the current user does not have sufficient privileges to use the adapter (assumes same privileges through all genomic regions for the user)
+
+
+
+## 2013-11-22 release
+
+Features:
+
++ using JBrowse 1.10.9 release
++ new hybrid store (memory/disk) should use much less memory than the pure memory store (with a small degrade in performance) - useful for genomes with many annotations (you can configure which one to use as best fits your needs)
++ viewing of annotation info editor for users without write privilege (cannot modify the data)
++ FASTA data adapter
++ different annotation info editor configurations for different annotation types
++ database/history merger tool (command line)
++ data adapters now use iterators when exporting data (improves memory footprint)
++ add a configurable option for dumping owner and other meta-data to the GFF3 adapter
++ undoing an "add_feature/add_transcript" operation will now warn the user that proceed will delete the feature
++ data adapter grouping (see FASTA adapter)
++ adding/updating PubMed ID will now show the publication title for confirmation
++ improved add_transcripts_from_gff3_to_annotations.pl
++ adding of different annotation types (gene, pseudogene, tRNA, snRNA, snoRNA, ncRNA, rRNA, miRNA, repeat_region, transposable_element)
++ adding GO terms now supports searching/autocompletion
++ annotation info editor now requests data from the server in batch mode (should improve speed)
+
+Fixes:
+
++ Chado featureprop writeback for generic attributes
++ add_transcripts_from_gff3_to_annotations.pl now properly loads status attributes
++ attributes being lost after undo/redo
++ redo of a merge_transcript operation after deleting one of the transcripts *should* work now
++ properly handle dragged transcripts that contain UTR elements
++ compressed data sent to the client no longer causes the client to hang when there are too many annotations
++ rewrote much of the split_transcript operation to better handle gene splitting
+
+
+## 2013-09-02 release
+
+Features:
+
++ Updated core client base code to JBrowse 1.10.2
++ 'Share' tab has been removed from the toolbar on top. The complete URL can be obtained from the browser line, it keeps track of all displayed tracks, and highlighted regions, if any.
++ in the aligned sequence BAM reads display, green, yellow and red colors flag insertions, substitutions and deletions (respectively). When the user hovers over them, details about the substituting nucleotide, or the size of the insertion or deletion are displayed.
++ Pinning a track to the top is now possible. The "User-Created Annotations" track is always pinned to the top, and may not be removed.
++ Alternate codon translation tables are now available and configurable.
++ Alternate splice sites are now configurable.
++ Filtering the list of available tracks is now possible.
++ It is also possible to combine the information from different quantitative tracks into a 'Combination Track', under the 'File' menu. Data from tracks containing graphs may be compared and combined in an additive, subtractive, or divisive manner. The resulting track highlights the differences between the data. This feature is useful for visualization of differential expression analyses, to observe changes in expression depending on he conditions.
++ The 'View' tab in the toolbar at the top now contains the menus from the 'Options' tab in the last version. Also, it is possible to 'Highlight' a region by selecting the option, then rubber-band marking the region. The highlight option will automatically be turned 'On' when inspecting the results from a BLAT search.
++ From the 'right/apple-click' menu, there are a number of new options.
+    + It is now possible to set the 5', 3', or both boundaries from a track of evidence or alignment, as the boundaries of an exon in the 'User-Created Annotations' area.
+    + It is also possible to annotate 'Readthrough Stop' signals using the apple/right-click menu. The current 'Stop' exon will be highlighted in purple, and the next 'Stop' signal in frame will be used as the end of translation.
+    + It is also possible to use the 'Set longest ORF' option.
++ New metadata configuration in the 'Annotation Information Editor' now includes:
+    + Name, Symbol, and Description Lines
+    + Fields to input crossed references to other Data Bases (DBXRefs) such as WormBase, FlyBase, NCBI, etc.
+    + PubMed IDs
+    + GO Terms/IDs
+    + The field 'Attributes' captures all additional metadata not already included in Comments, PubMed IDs, GO, and DBXRefs in a 'Tag/Value' format.
+    + When ready to export, all metadata in the AIE goes to the 9th column of the GFF3.
++ GFF3 adapter now allows configuring the value to be put in the source column (column 2)
++ Rewrote 'add_transcripts_to_annotations_from_gff3.pl' script to write data to server in chunks (helps with efficiency and timeouts), no longer using BioPerl so it can properly handle CDS features
++ Numerous bug fixes
+
+
+
+## 2013-05-17 release:
+
+Features:
+
++ Fully revamped genomic sequence selection screen
++ sorting by name and length
++ uses customizable JavaScript function
++ filtering of genomic sequence names
++ Annotation info editor
++ allows editing of symbols
++ editing of comments
+    + editing of dbxrefs
+    + Script for bulk loading gene/transcript/exons to annotation track
++ Improved login system
+    + allows logging in from either genomic region selection screen or editor
++ Configuration for using computed CDS (if available) when first creating a transcript (rather that using the longest ORF)
++ Improved memory management
++ Improved handling of concurrent edits
++ HTTPS support
++ Deletion of transcripts now warn users
++ Accessing editor without logging in hides annotation track
++ Using "[" and "]" to navigate through subfeatures when a feature in the annotation track is selected
++ Improved interface for adding insertions and deletions
++ Option to hide plus and/or minus strand
++ Various bugfixes
+
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..d98ed24
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,13 @@
+-------
+Copyright (c) 2015, Regents of the University of California. 
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+Neither the name of the Lawrence Berkeley National Lab nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSI [...]
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b856a9a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+# Apollo
+[![DOI](https://zenodo.org/badge/13471446.svg)](https://zenodo.org/badge/latestdoi/13471446)
+![Build](https://travis-ci.org/GMOD/Apollo.svg?branch=master)
+[![Coverage](https://coveralls.io/repos/github/GMOD/Apollo/badge.svg?branch=master)](https://coveralls.io/github/GMOD/Apollo?branch=master)
+[![Documentation](https://readthedocs.org/projects/genomearchitect/badge/?version=latest)](https://genomearchitect.readthedocs.org/en/latest/)
+[![Chat at Gitter](https://badges.gitter.im/GMOD/Apollo.svg)](https://gitter.im/GMOD/Apollo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+
+### [![](https://github.com/GMOD/Apollo/blob/master/docs/images/download_small.png) Download the latest release](https://github.com/GMOD/Apollo/releases/latest)
+
+An instantaneous, collaborative, genome annotation editor.  The stack is a Java web application / database backend and a
+Javascript client that runs in a web browser as a JBrowse plugin.  
+
+For general information on Apollo, go to [http://genomearchitect.org/](http://genomearchitect.org/).
+
+Complete Apollo installation and configuration instructions are available from the [Apollo documentation pages](http://genomearchitect.readthedocs.io/en/latest/)
+
+The Apollo client is implemented as a plugin for [JBrowse](http://jbrowse.org).  Additional JBrowse plugins may be found in the [JBrowse registry](https://gmod.github.io/jbrowse-registry/) and configured in ```apollo-config.groovy```.
+
+
+## Setup guide
+
+[Setup guide](docs/Setup.md) for deploying on production and [custom configuration guide](docs/Configure.md).  
+Launchable public Amazon EC2 images may also be found in most regions under the name 'Apollo' as well as [instructions for docker](docs/Setup.md#configure-for-docker). 
+
+The [quick-start guide for developers](docs/Apollo2Build.md) shows how to easily get started with Apollo. 
+
+
+## Migrating data from older versions
+
+You can follow steps in our [migration guide](https://github.com/GMOD/Apollo/blob/master/docs/Migration.md) to move annotations and data from older versions.
+
+### Note about data directories
+
+Apollo 2.0 allows you to add multiple data directories to your webapp, and it expects the data directories to be stored
+outside of the tomcat webapps directory. Use the WA2.0 [quick-start guide](docs/Apollo2Build.md) to learn how to add new
+data directories for your organisms.
+
+
+**Important Note: All data from a webapps directory will disappear when doing tomcat "undeploy" operations, even if
+it is a symlink.**.
+
+
+### Launch Apollo in a temporary server
+
+To launch Apollo with temporary settings, use the `apollo run-local` command, which will initialize your server
+automatically with an H2 (zero-configuration) database.
+ 
+    apollo run-local 8080
+
+It will also use your custom settings if an apollo-config.groovy file has been setup.
+
+### Generate a war file
+
+Users can generate a war file (for example target/apollo-1.0.2.war) that will be copied into their tomcat webapps
+directory for production deployments:
+
+    apollo deploy 
+
+Note: make sure to create an apollo-config.groovy file following the sample data (e.g.
+sample-postgres-apollo-config.groovy) to make sure you use your preferred database settings.
+
+
+### Run locally for GWT development
+
+    apollo devmode 
+   
+
+### Thanks to
+[![IntelliJ](https://lh6.googleusercontent.com/--QIIJfKrjSk/UJJ6X-UohII/AAAAAAAAAVM/cOW7EjnH778/s800/banner_IDEA.png)](
+http://www.jetbrains.com/idea/index.html)
+
+[![YourKit](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/) 
+
+Thanks to YourKit for providing us the use of their YourKit Java Profiler.  YourKit supports Open Source.
diff --git a/apollo b/apollo
new file mode 100755
index 0000000..4392e79
--- /dev/null
+++ b/apollo
@@ -0,0 +1,244 @@
+#!/bin/bash
+
+function usage(){
+    echo "";
+    echo "Usage: apollo <command>";
+    echo "";
+    echo "Production Commands:";
+    echo "";
+    echo "deploy:           Builds Unoptimized war file (no processing of javascript) into the target directory.";
+    echo "release:          Builds in release mode (minimized javascript). (beta)";
+    echo "run-local <port>: Runs from current directory, but not in dev-mode. ";
+    echo "help:             This message.";
+    echo "";
+    echo "Development Commands:";
+    echo "devmode:          Runs from current directory debug mode (non-minimized javascript).";
+    echo "run-app   <port>: Runs from current directory, but does not compile annotator panel (you have to run run-local once first).";
+    echo "test:             Runs test-suite.";
+    echo "debug:            Runs from current directory in debug mode (non-minimized javascript).";
+    echo "watchman:         Creates watchman daemon to autocopy plugin files (non-minimized javascript).";
+    echo "compile:          Compiled the build.";
+    echo "clean:            Removes class files.";
+    echo "clean-all:        Removes class files and jbrowse files.";
+    echo "create-rest-doc:  Recreates REST documentation.";
+};
+
+
+grails_executable=""
+gradle_executable=""
+
+if [[ $# == 0 || $1 == help || $1 == --help ]]; then
+    usage
+    exit 1;
+fi
+
+function deploy_message(){
+   echo "***************************************"
+   echo "NOTE: Please set the memory for your servlet container (tomcat, jetty, etc.) or Apollo may not start correctly: http://genomearchitect.readthedocs.io/en/latest/Troubleshooting.html#suggested-tomcat-memory-settings"
+   echo "***************************************"
+}
+
+function check_rest_api(){
+	if [ ! -d target ]; then
+		$grails_executable doc
+	fi
+}
+
+function check_java(){
+    javac_version=`javac -version 2>&1 | grep ^javac`
+    echo "$javac_version found";
+    if [[ $javac_version == javac*  ]]; then
+        echo "javac installed";
+        minor_version=`echo $javac_version  | cut -d\. -f2 `
+        if [[ "$minor_version" != '8' ]] ; then
+            echo "You must install JDK 1.8"
+            exit 1 ;
+        else
+            echo "JDK 1.8 found: $javac_version"
+        fi
+    else
+       echo "javac not installed.  Please install JDK 1.8."
+       exit 1 ;
+    fi
+}
+
+function check_node(){
+    node_executable=$(which node)
+    if ! [ -x "$node_executable" ] ; then
+        nodejs_executable=$(which nodejs)
+        if ! [ -x "$nodejs_executable" ] ; then
+            echo "You must install 'Node JS' to do a release of Apollo."
+            exit 1 ;
+        else
+            echo "STOPPING BUILD: You need to create a symlink from 'node' to 'nodejs'"
+            echo "e.g., sudo ln -s $nodejs_executable /usr/bin/node"
+            exit 0
+        fi
+    fi
+    NODE_VERSION=`$node_executable -v | cut -d\. -f1`
+    if [[ $NODE_VERSION == v* ]]; then
+        NODE_VERSION=${NODE_VERSION:1}
+    fi
+    echo "Node Version: $NODE_VERSION"
+    if [ $NODE_VERSION -lt 6 ]; then
+        echo "node version 6 or better must be installed.  Please install an updated version of node.js by following the instructions appropriate for your system https://nodejs.org/en/download/package-manager/";
+        exit 1
+    fi
+
+
+    npm_executable=$(which npm)
+    NPM_VERSION=`$npm_executable -v | cut -d\. -f1`
+    echo "Npm Version: $NPM_VERSION"
+    if [ $NPM_VERSION -lt 3 ]; then
+        echo "npm version 3 or better must be installed.  Please install an updated version of node.js by following the instructions appropriate for your system https://nodejs.org/en/download/package-manager/";
+        exit 1
+    fi
+}
+
+
+function check_perldependencies(){
+    perl -e 'use Text::Markdown'
+    if [ $? != 0 ] ; then
+        echo "Perl package 'Text::Markdown' is required in order to do a release of Apollo."
+        exit 1 ;
+    fi
+    perl -e 'use DateTime'
+    if [ $? != 0 ] ; then
+        echo "Perl package 'DateTime' is required in order to do a release of Apollo."
+        exit 1 ;
+    fi
+}
+
+function check_configs_for_release(){
+    check_perldependencies
+}
+
+
+function check_configs(){
+    grails_executable=$(which grails)
+    gradle_executable=$(which gradle)
+    git_executable=$(which git)
+    if ! [ -x "$grails_executable" ] ; then
+        if [ -f 'grailsw' ]; then
+            echo "Grails not found using grailsw";
+           : "${JAVA_HOME?:Need to set JAVA_HOME}"
+            grails_executable="./grailsw"
+        else
+           echo "You must install 'grails' to install Apollo."
+           exit 1 ;
+        fi
+    fi
+    if ! [ -x "$gradle_executable" ] ; then
+       if [ -f 'gradlew' ]; then
+           echo "Gradle not found using gradlew";
+           : "${JAVA_HOME?:Need to set JAVA_HOME}"
+           gradle_executable="./gradlew"
+       else
+           echo "You must install 'gradle' to install Apollo."
+           exit 1 ;
+        fi
+    fi
+    if ! [ -x "$git_executable" ] ; then
+       echo "You must install 'git' to install Apollo."
+       exit 1 ;
+    fi
+
+    check_node
+    check_java
+}
+
+function copy_configs(){
+    rm -f src/java/apollo-config.groovy
+    cp apollo-config.groovy src/java/apollo-config.groovy
+}
+
+function clean_code(){
+    $grails_executable clean
+    $gradle_executable cleanAll
+}
+
+function clean_all(){
+    check_configs
+    rm -rf bin
+    rm -rf jbrowse-download
+    rm -rf JBrowse-dev
+    rm -rf web-app/jbrowse
+    rm -f *.zip
+    clean_code
+}
+
+
+if [[ $1 == "devmode" ]];then
+    check_configs
+    $gradle_executable handleJBrowse copyResourcesDev devmode &
+    $grails_executable -reloading run-app
+elif [[ $1 == "run-local" ]];then
+    check_configs
+    if [[ $# == 2 ]]; then
+        $gradle_executable handleJBrowse copy-resources copyResourcesDev gwtc && $grails_executable -Dserver.port=$2 run-app
+    else
+        $gradle_executable handleJBrowse copy-resources copyResourcesDev gwtc && $grails_executable run-app
+    fi
+elif [[ $1 == "watchman" ]]; then
+    watchman_executable=$(which watchman)
+    if ! [ -x "$watchman_executable" ] ; then
+        echo "Watchman not found. Install watchman to automatically update the client side plugins for apollo development modes"
+    fi
+    if [ -x "$watchman_executable" ]; then $watchman_executable -- trigger $(PWD) jsfilescopy 'client/apollo/**/*.js' -- scripts/copy_client.sh; fi;
+elif [[ $1 == "run-app" ]];then
+    check_configs
+    if [[ $# == 2 ]]; then
+        $gradle_executable handleJBrowse copyResourcesDev && $grails_executable -Dserver.port=$2 run-app
+    else
+        $gradle_executable handleJBrowse copyResourcesDev && $grails_executable run-app
+    fi
+elif [[ $1 == "debug" ]];then
+    # TODO: feel like there is a better way to do this
+    OLD_MAVEN_OPTS=$MAVEN_OPTS
+    check_configs
+    copy_configs
+    clean_all
+    export MAVEN_OPTS=-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n
+    $gradle_executable handleJBrowse devmode &
+    $grails_executable -reloading debug
+    export MAVEN_OPTS=$OLD_MAVEN_OPTS
+    unset OLD_MAVEN_OPTS
+elif [[ $1 == "test" ]];then
+    check_configs
+    copy_configs
+    $gradle_executable handleJBrowse
+    check_rest_api
+	$grails_executable test-app
+elif [[ $1 == "test-unit" ]];then
+    check_configs
+    copy_configs
+    $grails_executable handleJBrowse test-app :unit
+elif [[ $1 == "deploy" ]];then
+    check_configs
+    copy_configs
+    $gradle_executable handleJBrowse copy-resources gwtc &&  $grails_executable war
+    deploy_message
+elif [[ $1 == "release" ]];then
+    check_configs
+    check_configs_for_release
+    copy_configs
+    clean_all
+    $gradle_executable handleJBrowse release gwtc && $grails_executable war
+    deploy_message
+elif [[ $1 == "compile" ]];then
+    check_configs
+    $gradle_executable gwtc && $grails_executable compile
+elif [[ $1 == "create-rest-doc" ]];then
+    check_configs
+    $grails_executable compile && $grails_executable rest-api-doc && mv -f restapidoc.json web-app/js/restapidoc/
+elif [[ $1 == "clean-all" ]];then
+    clean_all
+elif [[ $1 == "clean" ]];then
+    check_configs
+    $gradle_executable clean &&  $grails_executable clean
+elif [[ $1 == "jbrowse" ]];then
+    check_configs
+    $gradle_executable handleJBrowse
+else
+    usage
+fi
diff --git a/application.properties b/application.properties
new file mode 100644
index 0000000..2002058
--- /dev/null
+++ b/application.properties
@@ -0,0 +1,6 @@
+#Grails Metadata file
+#Sat Apr 16 11:48:41 PDT 2016
+app.grails.version=2.5.5
+app.name=apollo
+app.servlet.version=3.0
+app.version=2.0.8
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..08d4314
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,440 @@
+import org.apache.tools.ant.taskdefs.condition.Os
+
+/*
+ * This build file was auto generated by running the Gradle 'init' task
+ * by 'nathandunn' at '3/14/16 12:08 PM' with Gradle 2.11
+ *
+ * This generated file contains a commented-out sample Java project to get you started.
+ * For more details take a look at the Java Quickstart chapter in the Gradle
+ * user guide available at https://docs.gradle.org/2.11/userguide/tutorial_java_projects.html
+ */
+
+buildscript {
+    repositories {
+        mavenCentral()
+    }
+    dependencies {
+    }
+}
+
+plugins {
+//    id "com.eriwen.gradle.js" version "1.12.1"
+//    id "com.moowork.node" version "0.12"
+}
+
+// In this section you declare where to find the dependencies of your project
+repositories {
+    // Use 'jcenter' for resolving your dependencies.
+    // You can declare any Maven/Ivy/file repository here.
+    jcenter()
+}
+
+
+ext {
+    npmCommand = Os.isFamily(Os.FAMILY_WINDOWS) ? 'npm.cmd' : 'npm'
+    githubURL = "https://github.com/"
+    nodeModulesDirectory = "node_modules"
+    jbrowseDirectory = "jbrowse-download"
+    pluginsDirectory = "jbrowse-download/plugins"
+    defaultGitMethod = "shell"
+}
+
+
+ant.importBuild 'build.xml'
+
+// Apply the java plugin to add support for Java
+apply plugin: 'java'
+apply plugin: 'groovy'
+apply plugin: 'eclipse'
+apply plugin: 'idea'
+//apply plugin: "de.qaware.seu.as.code.git"
+
+// In this section you declare the dependencies for your production and test code
+dependencies {
+    // The production code uses the SLF4J logging API at compile time
+//    compile 'org.slf4j:slf4j-api:1.7.14'
+
+    // Declare the dependency for your favourite test framework you want to use in your tests.
+    // TestNG is also supported by the Gradle Test task. Just change the
+    // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add
+    // 'test.useTestNG()' to your build script.
+//    testCompile 'junit:junit:4.12'
+}
+
+def jbrowseConfig
+def jbrowsePlugins
+
+task evaluateJBrowseConfigs {
+    // gradle read in Config.groovy
+    def config = new ConfigSlurper().parse(new File("grails-app/conf/Config.groovy").toURI().toURL())
+    jbrowseConfig = config.jbrowse
+    jbrowsePlugins = jbrowseConfig.plugins
+    // if apollo-config.groovy exists, read THAT in and apply it as well
+    File testFile = new File("./apollo-config.groovy")
+    def revisedConfig
+    if (testFile.exists()) {
+        revisedConfig = new ConfigSlurper().parse(new File("apollo-config.groovy").toURI().toURL())
+    } else {
+        try {
+            Class scriptClass = getClass().classLoader.loadClass('apollo-config.groovy')
+            revisedConfig = new ConfigSlurper().parse(scriptClass)
+        } catch (e) {
+            logger.error "No log file found in classpath."
+        }
+    }
+
+    if (revisedConfig) {
+        jbrowseConfig = revisedConfig.jbrowse ?: jbrowseConfig
+        jbrowsePlugins << revisedConfig.jbrowse.plugins
+    }
+
+    logger.quiet "Final JBrowse settings ${jbrowseConfig}"
+    logger.quiet "Final plugins ${jbrowsePlugins}"
+}
+
+task installJBrowse(dependsOn: evaluateJBrowseConfigs) << {
+    logger.quiet "Installing JBrowse ${jbrowseConfig}"
+
+    File jbrowseFile = new File("jbrowse-download")
+    if (jbrowseConfig.git) {
+        def git = jbrowseConfig.git
+        if (jbrowseFile.exists()) {
+            if ( (git.alwaysRecheck || git.alwaysPull)) {
+                tasks.refreshNpmRepo.execute()
+            }
+        } else {
+            logger.quiet "Cloning: ${git.url} into ${"jbrowse-download"}"
+            def branch = git.branch ?: (git.tag ? "tags/${git.tag}" : null)
+            // NOTE: this is possible, but user may have to explicitly force the checkout
+//            cloneRepo(git.url, "jbrowse-download", branch, 1)
+            cloneRepoNpm(git.url, "jbrowse-download", branch)
+        }
+    } else if (jbrowseConfig.url && !jbrowseFile.exists()) {
+
+        if (jbrowseConfig.url.type == "zip") {
+            // assume its a zip
+            def f = new File("jbrowse-directory"+ ".zip")
+            new URL(jbrowseConfig.url.url).withInputStream { i -> f.withOutputStream { it << i } }
+            File tempFile = File.createTempDir()
+            ant.unzip(src: f.absolutePath, dest: tempFile, overwrite: false)
+            org.gradle.util.GFileUtils.moveDirectory(new File(tempFile.absolutePath + "/" + jbrowseConfig.url.fileName), jbrowseFile)
+            org.gradle.util.GFileUtils.deleteDirectory(new File(jbrowseFile.absolutePath + "/sample_data"))
+            tempFile.deleteDir()
+            f.delete()
+        } else {
+//            new URL(jbrowseConfig.url.url).withInputStream { i -> f.withOutputStream { it << i } }
+            logger.error "Must specify JBrowse download type"
+            throw new GradleException("Unable to specify JBrowse download type")
+        }
+    }
+}
+
+
+task installJBrowseLocal(dependsOn: installJBrowse,type: Exec) {
+	workingDir = '.'
+    commandLine './install_jbrowse.sh'
+}
+
+task copyResourcesDev(dependsOn: installJBrowse) << {
+    File jbrowse = new File("web-app/jbrowse")
+    logger.quiet "Found JBrowse directory ${jbrowse.file}"
+    if(jbrowse.exists()){
+        tasks["copy-resources-dev"].execute()
+    }
+    else{
+        tasks["copy-resources"].execute()
+    }
+    installJBrowseLocal.execute()
+}
+
+task copyApolloPlugin(dependsOn:installJBrowse,type:Copy){
+    logger.quiet "Copying apollo plugin"
+    from("client/apollo")
+    into("jbrowse-download/plugins/WebApollo")
+}
+
+// wrapper for ant script
+task setupJBrowse(dependsOn: "setup-jbrowse") << {}
+
+task installJBrowsePlugins(dependsOn: copyApolloPlugin) << {
+    logger.quiet "Installing JBrowse plugins  ${jbrowsePlugins}"
+
+    for (plugin in jbrowsePlugins) {
+        def path = pluginsDirectory + "/" + plugin.key
+        logger.quiet "Evaluating plugin ${plugin}"
+        def pluginExists = confirmPlugin(path)
+        if (plugin.value.included==true) {
+                if (pluginExists) {
+                    logger.quiet "Plugin ${path} exists and appears valid."
+                } else {
+                    logger.error "Error: There is a problem with the plugin at ${path}!"
+                    throw new GradleException("Included plugin ${path} not found in build")
+                }
+        }
+        else if (plugin.value.included==false) {
+            if (pluginExists) {
+                logger.quiet "Plugin ${path} exists but is not included."
+            } else {
+                logger.quiet "Plugin ${path} does not exist but is also not included."
+            }
+        }
+        else if (plugin.value.git) {
+            logger.quiet "Plugin is supplied by git"
+            if (pluginExists) {
+                logger.quiet "Plugin ${path} exists and appears valid."
+                if (plugin.value.alwaysRecheck || plugin.value.alwaysPull) {
+                    logger.quiet "Checking out branch ${plugin.value.branch}"
+                    if (plugin.value.tag) {
+                        checkoutBranch(new File(path),"tags/${plugin.value.tag}")
+                    } else if (plugin.value.branch) {
+                        checkoutBranch(new File(path),plugin.value.branch)
+                    }
+
+                    if (plugin.value.alwaysPull) {
+                        logger.quiet "Pulling ${plugin.value.git}"
+                        gitPull(new File(path))
+                    }
+                }
+            } else {
+                logger.quiet "Cloning '${plugin.value.git}' into '${path}'"
+                // checking if gitMethod exists, otherwise setting the default to 'shell'
+//                def gitMethod = plugin.value.gitMethod ? plugin.value.gitMethod : defaultGitMethod
+                // Currently we only support the shell git method.
+                // At a future time (when we install plugins as npm modules in JBrowse), this method can be optional.
+                def gitMethod = defaultGitMethod
+                cloneRepo(plugin.value.git, path, plugin.value.branch, 0, gitMethod)
+                logger.quiet "Cloned from ${plugin.value.git} into ${path}"
+            }
+        } else if (plugin.value.url == true) {
+            // TODO: test
+            if (!pluginExists) {
+                logger.quiet "Plugin ${path} exists and appears valid. Downloading..."
+                def f = new File(path)
+                new URL(plugin.value.url).withInputStream { i -> f.withOutputStream { it << i } }
+            }
+        } else {
+            logger.error "Invalid plugin settings"
+            throw new GradleException("Invalid plugin settings for path [${path}] and setting [${plugin.value}]")
+        }
+    }
+}
+
+// wrapper for ant script
+task buildJBrowse(dependsOn: 'build.jbrowse'){}
+
+// TODO: should inherent handleJBrowse
+task handleJBrowseRelease(dependsOn: [copyApolloPlugin]) {
+    logger.quiet "Handling JBrowse release ${jbrowseConfig}"
+
+    doLast {
+        installJBrowsePlugins
+        setupJBrowse
+        copyResourcesDev
+		buildJBrowse
+		installJBrowseLocal
+    }
+
+}
+
+// root task
+task handleJBrowse(dependsOn: [installJBrowsePlugins]) {
+    logger.quiet "Handling JBrowse ${jbrowseConfig}"
+
+    doLast {
+        setupJBrowse
+        copyResourcesDev
+		installJBrowseLocal
+    }
+
+}
+
+def confirmPlugin(String path) {
+    File file = new File(path)
+    return file.exists() && file.isDirectory() && file.canRead()
+}
+
+def cloneRepo(String url, String directory, String branch, Integer depth=0, String method=defaultGitMethod){
+    logger.quiet "Cloning ${url} via ${method}"
+    switch (method){
+        case "npm":
+            cloneRepoNpm(url, directory, branch)
+            break
+        case "shell":
+            cloneRepoShell(url, directory, branch, depth)
+            break
+        default:
+            throw new GradleException("Error in cloneRepo: invalid method ${method}")
+	    break
+    }
+}
+
+def cloneRepoShell(String url, String directory, String branch, Integer depth) {
+    String depthString = depth != null && depth > 0 ? " --recursive --depth ${depth} " : ""
+    def commandToExecute = "git clone ${depthString} ${url} ${directory} "
+    logger.quiet "Command to execute [${commandToExecute}]"
+    def proc = commandToExecute.execute()
+    def outputStream = new StringBuffer();
+    def errorStream = new StringBuffer();
+    proc.waitForProcessOutput(outputStream, errorStream);
+    println outputStream
+    println errorStream
+
+    if(!branch){
+        branch = "master"
+    }
+    else
+    if (branch.startsWith("tags")) {
+        fetchTags(new File(directory))
+    }
+    checkoutBranch(new File(directory), branch)
+    tasks.installJBrowseScript.execute()
+}
+
+def cloneRepoNpm(String url, String directory, String branch){
+    logger.quiet "Cloning repo with ${url},${directory},${branch}"
+    String finalUrl = url + (branch ? "#${branch}" : "")
+    finalUrl = finalUrl.replaceAll("tags/","")
+    if(finalUrl.startsWith(githubURL)){
+        finalUrl = finalUrl.substring(githubURL.length())
+        logger.quiet "Final github URL: ${finalUrl}"
+    }
+    else{
+        logger.quiet "Final non-github URL: ${finalUrl}"
+    }
+    exec{
+        logger.quiet "Using npm to install: ${finalUrl}"
+        commandLine "npm","install",finalUrl,"--ignore-scripts"
+    }
+
+    File npmDirectory = new File('node_modules')
+    if(finalUrl.contains("#") && (finalUrl.endsWith("jbrowse") || finalUrl.split("#")[0].endsWith("jbrowse"))) {
+        logger.quiet "Moving JBrowse into position ${finalUrl}"
+        tasks.moveNodeModulesJBrowse.execute()
+        tasks.installJBrowseScript.execute()
+    }
+    else{
+        npmDirectory.eachDir {
+            File targetFile = new File(directory)
+            logger.quiet "Moving plugin into place: ${npmDirectory.absolutePath} -> ${targetFile}"
+            it.renameTo(targetFile)
+        }
+    }
+
+    // delete on exit
+    if(npmDirectory.exists() && npmDirectory.listFiles()?.length==0){
+        assert npmDirectory.delete()
+    }
+}
+
+task removeJBrowseDownload(type:Delete){
+    delete(jbrowseDirectory)
+}
+
+task moveNodeModulesJBrowse(dependsOn:removeJBrowseDownload) {
+    doLast {
+        file("jbrowse-download").deleteDir() // dependency handles this
+        file("node_modules/jbrowse").renameTo(file("jbrowse-download"))
+        file("node_modules/*").renameTo(file("jbrowse-download/src"))
+        file("node_modules").deleteDir()
+//        file("jbrowse-download/node_modules").deleteDir()
+    }
+}
+
+task installJBrowseScript(type:Exec) {
+    logger.quiet "Setting up JBrowse... "
+    workingDir jbrowseDirectory
+    commandLine './setup.sh'
+}
+
+task refreshNpmRepo(type: Exec){
+//    println "using npm to update: ${directory}"
+    workingDir jbrowseDirectory
+    commandLine "npm","update"
+}
+
+// Get the path for the locally installed binaries
+task npmBin << {
+    new ByteArrayOutputStream().withStream { os ->
+        exec {
+            executable = npmCommand
+            args = ['bin']
+            standardOutput = os
+        }
+        ext.binPath = os.toString().trim() + File.separator
+    }
+}
+
+// Install packages from package.json
+task npm(type: Exec) {
+    description = "Grab NodeJS dependencies (from package.json)"
+    commandLine = [npmCommand, "install"]
+    inputs.file "package.json"
+    outputs.dir "node_modules"
+    tasks.npmBin.execute()
+}
+
+task cleanAll(type:Delete){
+    delete 'web-app/WEB-INF/deploy', 'web-app/annotator','target'
+}
+
+def fetchTags(File file) {
+    logger.quiet "Fetching tags"
+    if(!isValidGitDirectory(file)) return
+
+    def processBuilder = new ProcessBuilder()
+    processBuilder.redirectErrorStream(true)
+    processBuilder.directory(file)
+    processBuilder.command("git", "fetch", "--tags")
+    def proc = processBuilder.start()
+    def outputStream = new StringBuffer();
+    def errorStream = new StringBuffer();
+    proc.waitForProcessOutput(outputStream, errorStream);
+    if(outputStream.length()>0) println outputStream
+    if(errorStream.length()>0) println "Error: " + errorStream
+}
+
+Boolean isValidGitDirectory(File file){
+    File gitFile = new File(file.absolutePath+"/.git")
+    if(!gitFile.exists() || !gitFile.canRead()){
+        logger.warn "Not a valid git directory so no git operations will run ${file.absolutePath}"
+        return false
+    }
+    return true
+}
+
+def checkoutBranch(File file, String branch) {
+    logger.quiet "Checkout out branch ${branch}"
+
+    if(!isValidGitDirectory(file)) return
+
+    def processBuilder = new ProcessBuilder()
+    processBuilder.redirectErrorStream(true)
+    processBuilder.directory(file)
+    if(branch.startsWith("tags/")){
+        branch = branch.substring("tags/".length())
+    }
+    processBuilder.command("git", "checkout", branch)
+    def proc = processBuilder.start()
+    def outputStream = new StringBuffer();
+    def errorStream = new StringBuffer();
+    proc.waitForProcessOutput(outputStream, errorStream);
+    if(outputStream.length()>0) println outputStream
+    if(errorStream.length()>0) println "Error: " + errorStream
+
+}
+
+def gitPull(File file) {
+    logger.quiet "git pull ${file.absolutePath}"
+    if(!isValidGitDirectory(file)) return
+
+    def processBuilder = new ProcessBuilder()
+    processBuilder.redirectErrorStream(true)
+    processBuilder.directory(file)
+    processBuilder.command("git", "pull")
+    def proc = processBuilder.start()
+    def outputStream = new StringBuffer();
+    def errorStream = new StringBuffer();
+    proc.waitForProcessOutput(outputStream, errorStream);
+    if(outputStream.length()>0) println outputStream
+    if(errorStream.length()>0) println "Error: " + errorStream
+}
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..2aac6fa
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<project name="Apollo" default="usage" basedir=".">
+    <!-- Arguments to gwtc and devmode targets -->
+    <property name="gwt.args" value=""/>
+
+    <!-- Configure path to GWT SDK -->
+    <property name="gwt.sdk" location="gwt-sdk"/>
+
+    <path id="project.class.path">
+        <pathelement location="target/classes/gwt"/>
+        <pathelement location="lib/gwt"/>
+        <pathelement location="${gwt.sdk}/gwt-user.jar"/>
+        <pathelement location="${gwt.sdk}/gwt-dev.jar"/>
+        <pathelement location="${gwt.sdk}/validation-api-1.0.0.GA.jar"/>
+        <pathelement location="${gwt.sdk}/validation-api-1.0.0.GA-sources.jar"/>
+        <fileset dir="lib/gwt" includes="**/*.jar"/>
+        <!-- Add any additional non-server libs (such as JUnit) here -->
+    </path>
+
+    <target name="javac" description="Compile java source to bytecode">
+        <mkdir dir="target/classes/gwt"/>
+        <javac srcdir="src/gwt" includes="**" encoding="utf-8"
+               destdir="target/classes/gwt"
+               includeantruntime="false"
+               source="1.8" target="1.8" nowarn="true"
+               debug="true" debuglevel="lines,vars,source">
+            <classpath refid="project.class.path"/>
+        </javac>
+        <copy todir="target/classes">
+            <fileset dir="src" excludes="**/*.java"/>
+        </copy>
+    </target>
+
+    <target name="gwtc" depends="javac" description="GWT compile to JavaScript (production mode)">
+        <java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler" maxmemory="256m">
+            <classpath>
+                <pathelement location="src/gwt"/>
+                <path refid="project.class.path"/>
+            </classpath>
+            <arg line="-war"/>
+            <arg value="web-app"/>
+            <!--            <arg line="-style"/>-->
+            <!--            <arg value="PRETTY"/>-->
+            <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
+            <arg line="${gwt.args}"/>
+            <arg value="org.bbop.apollo.gwt.Annotator"/>
+        </java>
+    </target>
+
+    <target name="devmode" depends="copy-resources,javac"
+            description="Run development mode (pass -Dgwt.args=-nosuperDevMode to fallback to classic DevMode)">
+        <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode" maxmemory="1g">
+            <classpath>
+                <pathelement location="src/gwt"/>
+                <path refid="project.class.path"/>
+                <pathelement location="${gwt.sdk}/gwt-codeserver.jar"/>
+            </classpath>
+            <arg value="-startupUrl"/>
+            <arg value="http://localhost:8080/apollo/annotator/"/>
+            <arg line="-war"/>
+            <arg value="web-app"/>
+            <!-- Additional arguments like -style PRETTY, -logLevel DEBUG or -nosuperDevMode -->
+            <arg line="${gwt.args}"/>
+            <arg value="org.bbop.apollo.gwt.Annotator"/>
+            <arg value="org.bbop.apollo.gwt.Annotator"/>
+        </java>
+    </target>
+
+
+    <!--<target name="build" depends="gwtc" description="Build this project"/>-->
+
+    <property name="grails-clean" value="false"/>
+
+
+    <property name="apollo.webapp.directory" location="web-app" relative="true" basedir="."/>
+    <property name="apollo.jbrowse.directory" location="${apollo.webapp.directory}/jbrowse"/>
+    <property name="apollo.client.directory" location="${basedir}/client/apollo" basedir="." relative="true"/>
+    <property name="apollo.plugin.directory" location="${apollo.jbrowse.directory}/plugins/WebApollo"/>
+    <property name="jbrowse.download.directory" location="jbrowse-download"/>
+    <property name="jbrowse.webapp.test.file" location="${apollo.jbrowse.directory}/src/dojo/dojo.js"/>
+    <property name="webapp.apollo.plugin.test.file" location="${apollo.plugin.directory}/js/main.js"/>
+    <property name="webapp.apollo.plugin.uncompressed.test.file"
+              location="${apollo.plugin.directory}/js/main.js.uncompressed.js"/>
+    <property name="scripts.directory" location="scripts"/>
+    <property name="jbrowse.git.test.file" location="${jbrowse.download.directory}/src/dojo/dojo.js"/>
+    <property name="jbrowse.github" value="https://github.com/gmod/jbrowse"/>
+    <property name="jbrowse.version" value="dev"/>
+    <property name="jbrowse.perl.dependency.test.file" value="bin/flatfile-to-json.pl"/>
+
+    <property name="jbrowse.release.version" value="1.11.6-release"/>
+
+    <available file=".git" type="dir" property="git.present"/>
+    <available file="${jbrowse.git.test.file}" type="file" property="jbrowse.git.present"/>
+    <available file="${jbrowse.webapp.test.file}" type="file" property="jbrowse.webapp.installed"/>
+    <available file="${webapp.apollo.plugin.test.file}" type="file" property="apollo.plugin.installed"/>
+    <available file="${jbrowse.perl.dependency.test.file}" type="file" property="jbrowse.perl.installed"/>
+    <available file="${webapp.apollo.plugin.uncompressed.test.file}" type="file" property="jbrowse.precompiled"/>
+
+    <target name="build.jbrowse" description="Build JBrowse using the build/Makefile">
+        <exec executable="sh" failifexecutionfails="false">
+            <arg value="-c"/>
+            <arg line="'ulimit -n 1000; cd ${jbrowse.download.directory} && make -f build/Makefile RELEASE_VERSION=dev release-notest'"/>
+        </exec>
+    </target>
+
+    <target name="unoptimized" description="unoptimized" depends="cloneJBrowse,copy.apollo.plugin.jbrowse">
+        <copy todir="${apollo.jbrowse.directory}">
+            <fileset dir="${jbrowse.download.directory}">
+                <exclude name="sample_data/**"/>
+                <exclude name="node_modules/**"/>
+                <exclude name="tests/**"/>
+            </fileset>
+        </copy>
+        <delete dir="${apollo.jbrowse.directory}/.git"/>
+        <echo>${jbrowse.download.directory}</echo>
+    </target>
+
+    <target name="release" description=""
+            depends="cloneJBrowse,copy.apollo.plugin.jbrowse,build.jbrowse,install.jbrowse.perl">
+        <copy file="${jbrowse.download.directory}/JBrowse-${jbrowse.version}.zip" todir="${basedir}"/>
+        <exec executable="unzip" failifexecutionfails="false" errorproperty="">
+            <arg value="JBrowse-${jbrowse.version}.zip"/>
+        </exec>
+        <move todir="${apollo.jbrowse.directory}">
+            <fileset dir="JBrowse-${jbrowse.version}">
+                <exclude name="sample_data/**"/>
+                <exclude name="node_modules/**"/>
+                <exclude name="tests/**"/>
+            </fileset>
+        </move>
+        <delete file="JBrowse-${jbrowse.version}.zip"/>
+        <delete dir="JBrowse-${jbrowse.version}"/>
+    </target>
+
+    <target name="debug" description=""
+            depends="cloneJBrowse,copy.apollo.plugin.jbrowse,build.jbrowse">
+        <copy file="${jbrowse.download.directory}/JBrowse-${jbrowse.version}-dev.zip" todir="${basedir}"/>
+        <exec executable="unzip" failifexecutionfails="false" errorproperty="">
+            <arg value="JBrowse-${jbrowse.version}-dev.zip"/>
+        </exec>
+        <move todir="${apollo.jbrowse.directory}">
+            <fileset dir="JBrowse-${jbrowse.version}-dev"/>
+        </move>
+        <delete file="JBrowse-${jbrowse.version}-dev.zip"/>
+        <delete dir="JBrowse-${jbrowse.version}-dev"/>
+    </target>
+
+
+    <target name="git.clean" description="Remove all unstaged files from ${basedir}">
+        <exec executable="git" failifexecutionfails="false" errorproperty="">
+            <arg value="clean"/>
+            <arg value="-fdx"/>
+        </exec>
+    </target>
+
+    <target name="cloneJBrowse" description="Clone git repository." unless="jbrowse.git.present">
+        <exec executable="git" failifexecutionfails="false" errorproperty="">
+            <arg value="clone"/>
+            <arg value="--recursive"/>
+            <arg value="${jbrowse.github}"/>
+            <arg value="${jbrowse.download.directory}"/>
+        </exec>
+        <exec executable="git" failifexecutionfails="false" errorproperty="" dir="${jbrowse.download.directory}">
+            <arg value="checkout"/>
+            <arg value="tags/${jbrowse.release.version}"/>
+        </exec>
+    </target>
+
+
+    <target name="usage">
+        <exec executable="./apollo" failifexecutionfails="false" errorproperty=""/>
+    </target>
+
+    <target name="install.jbrowse.perl" description="Install jbrowse." if="jbrowse.git.present"
+            unless="jbrowse.perl.installed">
+        <exec executable="./install_jbrowse.sh" failifexecutionfails="true" failonerror="true"
+              errorproperty="jbrowse-fail">
+            <arg value="${jbrowse.download.directory}"/>
+        </exec>
+    </target>
+
+    <target name="git.revision" description="Store git revision in ${repository.version}" if="git.present">
+        <exec executable="git" outputproperty="git.revision" failifexecutionfails="false" errorproperty="">
+            <arg value="describe"/>
+            <arg value="--tags"/>
+            <arg value="--always"/>
+            <arg value="HEAD"/>
+        </exec>
+        <condition property="repository.version" value="${git.revision}" else="unknown">
+            <and>
+                <isset property="git.revision"/>
+                <length string="${git.revision}" trim="yes" length="0" when="greater"/>
+            </and>
+        </condition>
+    </target>
+
+
+    <target name="setup-jbrowse" unless="jbrowse.webapp.installed">
+        <antcall target="cloneJBrowse"/>
+        <antcall target="copy.apollo.plugin.jbrowse"/>
+
+
+        <echo>Will be setting up jbrowse</echo>
+        <delete dir="${apollo.jbrowse.directory}"/>
+
+        <copy todir="${apollo.jbrowse.directory}">
+            <fileset dir="${jbrowse.download.directory}">
+                <exclude name="sample_data/**"/>
+                <exclude name="node_modules/**"/>
+                <exclude name="tests/**"/>
+            </fileset>
+        </copy>
+        <antcall target="install.jbrowse.perl"/>
+
+    </target>
+
+    <target name="copy.apollo.plugin.jbrowse">
+        <echo>copying plugin to jbrowse git ...</echo>
+        <copy todir="${jbrowse.download.directory}/plugins/WebApollo" failonerror="true">
+            <fileset dir="${apollo.client.directory}"/>
+        </copy>
+    </target>
+
+    <target name="copy.apollo.plugin.webapp" unless="jbrowse.precompiled" if="jbrowse.webapp.installed"
+            description="Copy client into webapp plugin directory.">
+        <echo>Copying WebApollo plugin to jbrowse webapp ...</echo>
+        <copy todir="${apollo.jbrowse.directory}/plugins/WebApollo" failonerror="true">
+            <fileset dir="${apollo.client.directory}"/>
+        </copy>
+    </target>
+
+    <target name="copy-jbrowse-into-webapp">
+        <copy todir="${apollo.jbrowse.directory}">
+            <fileset dir="${jbrowse.download.directory}">
+                <exclude name="sample_data/**"/>
+                <exclude name="node_modules/**"/>
+                <exclude name="tests/**"/>
+            </fileset>
+        </copy>
+    </target>
+
+
+    <target name="copy-resources-dev"
+            depends="copy.apollo.plugin.webapp,copy-jbrowse-into-webapp"
+            description="This is called externally by shell script.">
+
+    </target>
+
+
+    <target name="copy-resources"
+            depends="copy.apollo.plugin.webapp,setup-jbrowse"
+            description="This is called externally by shell script.">
+
+    </target>
+
+</project>
diff --git a/client/apollo/css/add_sequence_alteration.css b/client/apollo/css/add_sequence_alteration.css
new file mode 100644
index 0000000..a8a1098
--- /dev/null
+++ b/client/apollo/css/add_sequence_alteration.css
@@ -0,0 +1,34 @@
+ at CHARSET "UTF-8";
+
+.sequence_alteration_button_div {
+	margin-top: 10px;
+	margin-left: auto;
+	margin-right: auto;
+	width: 25%;
+}
+
+.sequence_alteration_input_label {
+	margin-left: 3px;
+	font-size: 1.0em;
+	font-family: courier,monospace;
+}
+
+.sequence_alteration_input_field {
+	margin-left: 10px;
+	font-family: courier,monospace;
+	text-transform: uppercase;
+}
+
+.sequence_alteration_comment_label {
+	margin-left: 3px;
+	font-size: 1.0em;
+	font-family: courier,monospace;
+}
+
+.sequence_alteration_comment_field {
+	margin-left: 10px;
+	font-family: courier,monospace;
+}
+
+.sequence_alteration_button {
+}
\ No newline at end of file
diff --git a/client/apollo/css/annotation_info_editor.css b/client/apollo/css/annotation_info_editor.css
new file mode 100644
index 0000000..201eec1
--- /dev/null
+++ b/client/apollo/css/annotation_info_editor.css
@@ -0,0 +1,145 @@
+ at CHARSET "UTF-8";
+
+.parent_annotation_info_editor {
+	font-size: 1.2em;
+	width: 22em !important;
+	border-min-width: 1px;
+	border-right: dotted;
+	padding-right: 4%;
+	float: left;
+}
+
+.annotation_info_editor {
+	font-size: 1.2em;
+	width: 22em !important;
+	padding-right: 1%;
+	float: right;
+}
+
+.annotation_info_editor_container {
+	padding-bottom: 15%;
+}
+
+.annotation_info_editor_header {
+	font-size: 1.3em;
+	text-align: center;
+	padding-bottom: 5px;
+}
+
+.dbxrefs {
+	min-width: 100%;
+	min-height: 7em;
+	border: solid;
+	border-width: 1px;
+	border-color: lightgray;
+}
+
+.attributes {
+	min-width: 100%;
+	min-height: 7em;
+	border: solid;
+	border-width: 1px;
+	border-color: lightgray;
+}
+
+.pubmed_ids {
+	min-width: 100%;
+	min-height: 7em;
+	border: solid;
+	border-width: 1px;
+	border-color: lightgray;
+}
+
+.go_ids {
+	min-width: 100%;
+	min-height: 7em;
+	border: solid;
+	border-width: 1px;
+	border-color: lightgray;
+}
+
+.comments {
+	min-width: 100%;
+	min-height: 10em;
+	border: solid;
+	border-width: 1px;
+	border-color: lightgray;
+}
+
+.annotation_info_editor_content {
+	font-size: 1.5em;
+}
+
+.annotation_info_editor_section_header {
+	text-align: center;
+}
+
+.annotation_info_editor_label {
+	text-align: right;
+	padding-right: 10px;
+	float: left;
+}
+
+.annotation_info_editor_section {
+	padding-top: 20px;
+}
+
+.annotation_info_editor_field_section {
+	padding-bottom: 1em;
+	padding-top: 0.5em;
+}
+
+.annotation_info_editor_button_group {
+	/*
+	width: 60%;
+	margin-left: auto;
+	margin-right: auto;
+	*/
+}
+
+.annotation_info_editor_button {
+	width: 5em;
+	margin-right: 2.5px;
+	margin-left: 2.5px;
+}
+
+.comments .dojoxGridHeader,
+.pubmed_ids .dojoxGridHeader,
+.go_ids .dojoxGridHeader {
+	display: none;
+}
+
+.annotation_editor_field {
+	float: right;
+}
+
+.status {
+	border: 1px;
+	border-style: solid;
+	border-color: lightgray;
+	padding: 0.5em;
+	overflow-y: auto;
+	max-height: 2em;
+}
+
+.annotation_info_editor_radio {
+/*	padding-right: 1em; */
+	display: inline-block;
+}
+
+.annotation_info_editor_radio_label {
+}
+
+.ui-autocomplete {
+	max-width: 75%;
+}
+
+.annotation_info_editor_selector {
+	margin-bottom: 1.5em;
+}
+
+.annotation_info_editor_selector_label {
+	text-align: right;
+	padding-right: 10px;
+	float: left;
+}
\ No newline at end of file
diff --git a/client/apollo/css/confirm_dialog.css b/client/apollo/css/confirm_dialog.css
new file mode 100644
index 0000000..abc1c9b
--- /dev/null
+++ b/client/apollo/css/confirm_dialog.css
@@ -0,0 +1,19 @@
+ at CHARSET "UTF-8";
+
+.confirm_message {
+	
+}
+
+.confirm_buttons {
+	margin-top: 0.5em;
+	width: 50%;
+	margin-left: auto;
+	margin-right: auto;	
+}
+
+.confirm_button {
+	width: 5em;
+	margin: 0.4em;	
+}
+
+
diff --git a/client/apollo/css/custom_track_styles.css b/client/apollo/css/custom_track_styles.css
new file mode 100644
index 0000000..42b8372
--- /dev/null
+++ b/client/apollo/css/custom_track_styles.css
@@ -0,0 +1,73 @@
+.whatever-80pct, 
+.whatever-blue-80pct, 
+.whatever-blue-80pct  {
+    background-color: blue;
+    height: 80%;
+    top: 10%;
+}
+
+.green-80pct, 
+.plus-green-80pct, 
+.minus-green-80pct  {
+    background-color: green; 
+    height: 80%;
+    top: 10%;
+}
+
+.blue-80pct, 
+.plus-blue-80pct, 
+.minus-blue-80pct  {
+    background-color: blue; 
+    height: 80%;
+    top: 10%;
+}
+
+.purple-80pct, 
+.plus-purple-80pct, 
+.minus-purple-80pct  {
+    background-color: purple; 
+    height: 80%;
+    top: 10%;
+}
+
+.springgreen-80pct, 
+.plus-springgreen-80pct, 
+.minus-springgreen-80pct  {
+    background-color: springgreen; 
+    height: 80%;
+    top: 10%;
+}
+
+.blueviolet-80pct, 
+.plus-blueviolet-80pct, 
+.minus-blueviolet-80pct  {
+    background-color: blueviolet; 
+    height: 80%;
+    top: 10%;
+}
+
+.mediumpurple-80pct, 
+.plus-mediumpurple-80pct, 
+.minus-mediumpurple-80pct  {
+    background-color: mediumpurple; 
+    height: 80%;
+    top: 10%;
+}
+
+.orange-80pct, 
+.plus-orange-80pct, 
+.minus-orange-80pct  {
+    background-color: rgb(255, 163, 0);
+    height: 80%;
+    top: 10%;
+    border: 1px solid gray;
+}
+
+.darkorange-60pct, 
+.plus-darkorange-60pct, 
+.minus-darkorange-60pct  {
+    background-color: rgb(214, 172, 121);
+    height: 60%;
+    top: 20%;
+    border: 1px solid gray;
+}
diff --git a/client/apollo/css/edit_comments.css b/client/apollo/css/edit_comments.css
new file mode 100644
index 0000000..ead16e6
--- /dev/null
+++ b/client/apollo/css/edit_comments.css
@@ -0,0 +1,30 @@
+ at CHARSET "UTF-8";
+
+.comments {
+	width: 58em;
+}
+
+.comment_header {
+	font-size: 1.5em;
+	margin-bottom: 5px;
+}
+
+.comment_area {
+	width: 40em;
+	height: 3.5em;
+}
+
+.comment_button {
+	height: 2.5em;
+}
+
+.comment_add_button_div {
+	margin-top: 5px;
+}
+
+.parent_comments_div {
+	margin-bottom: 10px;
+	border-bottom-style: dotted;
+	border-bottom-width: 2px;
+	padding-bottom: 10px;
+}
\ No newline at end of file
diff --git a/client/apollo/css/edit_dbxrefs.css b/client/apollo/css/edit_dbxrefs.css
new file mode 100644
index 0000000..a7aa889
--- /dev/null
+++ b/client/apollo/css/edit_dbxrefs.css
@@ -0,0 +1,34 @@
+ at CHARSET "UTF-8";
+
+.dbxrefs {
+	width: 39em;
+}
+
+.dbxref_header {
+	font-size: 1.5em;
+	margin-bottom: 5px;
+}
+
+.dbxref_table_header_field {
+	margin-right:5em;
+}
+
+.dbxref_field {
+	margin-right:3px;
+	margin-top: 0px;
+	margin-bottom: 0px;
+}
+
+.dbxref_button {
+}
+
+.dbxref_add_button_div {
+	margin-top: 5px;
+}
+
+.parent_dbxrefs_div {
+	margin-bottom: 10px;
+	border-bottom-style: dotted;
+	border-bottom-width: 2px;
+	padding-bottom: 10px;
+}
\ No newline at end of file
diff --git a/client/apollo/css/export.css b/client/apollo/css/export.css
new file mode 100644
index 0000000..e33354b
--- /dev/null
+++ b/client/apollo/css/export.css
@@ -0,0 +1,22 @@
+ at CHARSET "UTF-8";
+
+.export_response {
+	width: 15em;
+	text-align: center;
+}
+
+.waiting_image {
+	display: block;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+.export_response_iframe {
+	border: none;
+	text-align: center;
+	font-size: 12px;
+	max-width: 15em;
+	max-height: 6em;
+	overflow-x: auto;
+	overflow-y: hidden;
+}
\ No newline at end of file
diff --git a/client/apollo/css/flat.css b/client/apollo/css/flat.css
new file mode 100644
index 0000000..04c574a
--- /dev/null
+++ b/client/apollo/css/flat.css
@@ -0,0 +1,29 @@
+.Flat div.gridline_minor {
+	border: none
+}
+.Flat div.gridline_major {
+	border: none
+}
+
+.Flat div.overview {
+	background: #f9f9f9;
+}
+.Flat.Dark div.overview {
+	background: #333333;
+	box-shadow: 0 2px 10px 0px #2b2b2b;
+	border: 1px solid black;
+}
+
+.Flat.Dark div.overview-pos{
+	color: white;
+}
+
+.Flat div#static_track{
+	background: #f9f9f9;
+}
+.Flat.Dark div#static_track{
+	color: white;
+	background: #333333;
+	box-shadow: 0 2px 10px 0px black;
+	border: 1px solid black;
+}
diff --git a/client/apollo/css/genome.css b/client/apollo/css/genome.css
new file mode 100644
index 0000000..1d7c2ff
--- /dev/null
+++ b/client/apollo/css/genome.css
@@ -0,0 +1,67 @@
+/** TODO: need to remove all standard JBrowse styles, since they're now in either "main.css" or "feature_style.css", 
+     and keep added / modified styles here (or break out into additional "webapollo.css" ? */
+/* main application CSS styles */
+/* @import url("main.css"); */
+
+
+/* This is the window around the whole feature map (including the numbers at the top) */
+/* kept diff frrm JBrowse main.css, but commented out
+div.dragWindow {
+    cursor: url("img/openhand.cur");
+}
+*/
+
+/*  kept diff from JBrowse main.css, but commented out
+div.locationTrap {
+    border-bottom-color: #ccc;
+}
+*/
+
+/* This is the red rectangular outline that appears on the top scale bar */
+/*  kept diff from JBrowse main.css, but commented out
+div.locationThumb {
+    cursor: url("img/openhand.cur");
+}
+*/
+
+
+/* tweak the style of the powered_by link that has the WA logo in it */
+.menuBar .powered_by {
+    height: 27px;
+    background: white;
+    border-right: 1px solid rgb(156,156,156);
+    position: relative;
+    top: -1px;
+}
+.menuBar .powered_by > img {
+    position: relative;
+    top: 1px;
+}
+
+.menuBar .user .icon {
+    height: 16px;
+    width: 16px;
+    margin-right: 2px;
+    margin-top: -2px;
+    display: inline;
+    display: inline-block;
+    background: url("img/glyphs_white.png") no-repeat -168px -27px;
+}
+
+.usericon {
+    height: 16px;
+    width: 16px;
+    margin-right: 2px;
+    margin-top: -2px;
+    display: inline;
+    display: inline-block;
+   /* background: url("img/glyphs_white.png") no-repeat -168px -27px; */
+/*    background: pink; */
+    background: url("../img/user_icon_16px.png");
+}
+
+
+/* hiding standard JBrowse Help->About menuitem */
+#menubar_about {
+    display: none;
+}
diff --git a/client/apollo/css/get_history.css b/client/apollo/css/get_history.css
new file mode 100644
index 0000000..e2cc1a0
--- /dev/null
+++ b/client/apollo/css/get_history.css
@@ -0,0 +1,68 @@
+ at CHARSET "UTF-8";
+
+.history_div {
+	font-size: 9px;
+}
+
+.history_table {
+	font-size: 1.5em;
+/*	max-width: 47em; */
+}
+
+.history_header_column {
+	font-weight: bold;
+	text-decoration: underline;
+}
+.history_set{
+	text-align: left;
+	margin-right: auto;
+}
+
+.history_column_operation {
+	width: 17em !important;
+}
+
+.history_column_action{
+	width: 5em !important;
+}
+
+.history_column {
+	width: 15em;
+	display: inline-block;
+	background-color: inherit;
+}
+
+.history_row_selected  {
+	background-color: #4169e1;
+	color: white;
+}
+
+.history_row:hover  {
+	background-color: #000099;
+	color: white;
+}
+
+.history_row_current {
+	background-color: #787878;
+	color: white;
+}
+
+.history_rows {
+	max-height: 10em;
+	overflow-y: auto;
+}
+
+.history_column_date {
+	max-width: 10em;
+}
+
+.history_preview {
+	width: 45em;
+	height: 2em;
+	padding-top: 3em;
+}
+
+.revert_button.dijitButton .dijitButtonNode {
+	padding: 0px;
+	margin: 0px;
+}
diff --git a/client/apollo/css/get_sequence.css b/client/apollo/css/get_sequence.css
new file mode 100644
index 0000000..f4597ce
--- /dev/null
+++ b/client/apollo/css/get_sequence.css
@@ -0,0 +1,35 @@
+ at CHARSET "UTF-8";
+
+.get_sequence {
+	font-size:10px;
+}
+
+.gff3_area{
+	width: 50em;
+	height: 20em;
+	font-family: 'courier';
+	font-size: 1.0em;
+}
+
+.sequence_area {
+	width: 60em;
+	height: 20em;
+	font-family: 'courier';
+	font-size: 1.5em;
+}
+
+.first_button_div {
+	margin-top: 10px;
+}
+
+.button_div {
+	margin-top: 5px;
+}
+
+.button_label {
+	margin-left: 3px;
+}
+
+.button_field {
+	margin-left: 3px;
+}
\ No newline at end of file
diff --git a/client/apollo/css/information_editor_panel.css b/client/apollo/css/information_editor_panel.css
new file mode 100644
index 0000000..e4b493d
--- /dev/null
+++ b/client/apollo/css/information_editor_panel.css
@@ -0,0 +1,57 @@
+.webapolloInformationEditor {
+    background: #fafafa;
+    overflow: auto;
+}
+.webapolloInformationEditor > .header > h2.title {
+    padding: 7px 0 0.5em 6px;
+    margin: 0;
+}
+.webapolloInformationEditor .trackCount {
+    display: inline-block;
+    float: right;
+}
+
+.webapolloInformationEditor > .header > .textfilterContainer {
+    padding: 0 5px 5px 5px;
+}
+.webapolloInformationEditor > .header > .textfilterContainer > .textfilter {
+    margin: 0;
+}
+
+.webapolloInformationEditor .dijitTitlePaneTitle {
+    padding: 0 4px;
+}
+
+.webapolloInformationEditor .dijitTitlePaneTitleFocus:focus {
+    outline: none;
+}
+
+.webapolloInformationEditor label.tracklist-label {
+    display: block;
+}
+.webapolloInformationEditor > .information_list {
+    background: white;
+    padding: 10px;
+    border: 1px solid #bfbfbf;
+}
+.webapolloInformationEditor label.tracklist-label:hover {
+    background: #D2E1F1;
+}
+.webapolloInformationEditor label.tracklist-label.collapsed {
+    display: none;
+}
+
+.webapolloInformationEditor label.tracklist-label input.check {
+    position: relative;
+    top: 2px;
+    margin: 0 4px;
+}
+.dj_safari .webapolloInformationEditor label.tracklist-label input.check {
+    top: 0;
+}
+
+#hierarchicalTrackPane_splitter {
+    background: #fafafa;
+    width: 5px;
+    border-right: 1px solid #555;
+}
diff --git a/client/apollo/css/main.css b/client/apollo/css/main.css
new file mode 100644
index 0000000..e948da1
--- /dev/null
+++ b/client/apollo/css/main.css
@@ -0,0 +1,20 @@
+ at import url("../jslib/jqueryui/themes/base/jquery.ui.all.css");
+ at import url("genome.css");
+ at import url("webapollo_track_styles.css");
+ at import url("add_sequence_alteration.css");
+ at import url("get_sequence.css");
+ at import url("search_sequence.css");
+ at import url("get_history.css");
+ at import url("export.css");
+ at import url("maker_styles.css");
+/* for now, maker.css is not loaded by default
+   to use maker.css without modifying this file, just add 
+           "css": "plugins/WebApollo/css/maker.css"
+   entry in configuration (usually in trackList.json)  */
+/* @import url("maker.css"); */
+ at import url("custom_track_styles.css");
+ at import url("annotation_info_editor.css");
+ at import url("confirm_dialog.css");
+ at import url("information_editor_panel.css");
+ at import url("maker_darkbackground.css");
+ at import url("flat.css");
diff --git a/client/apollo/css/maker.css b/client/apollo/css/maker.css
new file mode 100644
index 0000000..40204dc
--- /dev/null
+++ b/client/apollo/css/maker.css
@@ -0,0 +1,292 @@
+/* styles for MAKER tracks and additional customization of WebApollo */
+
+div.track  {
+   /* based on CoolWorld color palette from kuler.adobe.com */
+   background-color: #142933;
+}
+
+
+div.track_annotations {
+   /* based on CoolWorld color palette from kuler.adobe.com */
+    background-color: #FFF7cc;
+    /* background-color: #FFFFDD; */
+}
+
+div.track_dna {
+    background-color: #FFF7cc;
+}
+
+div#overviewtrack_overview_loc_track {
+    background-color: transparent;
+}
+
+div#gridtrack {
+    background-color: transparent;
+}
+
+div.gridline_major {
+/*    border-color: #bbb; */
+    border-color: transparent;
+}
+
+div.gridline_minor {
+/*    border-color: #eee; */
+    border-color: transparent;
+}
+
+.feature-label {
+    color: gray;
+}
+
+.plus-trellis-arrowhead,
+.plus-webapollo-arrowhead,
+.plus-annot-arrowhead {
+    background-image: url('../img/plus-arrowhead.png');
+}
+
+.minus-trellis-arrowhead,
+.minus-webapollo-arrowhead,
+.minus-annot-arrowhead {
+    background-image: url('../img/minus-arrowhead.png');
+}
+
+.annot-render {
+    /* height: 4px; original MAKER choice */
+    /* margin-top: 6px; original MAKER choice */
+    /* background-color: #0460d9; original color choice based on kuler.adobe.com */
+    /* adjusted background-color, height, margin-top to better see DNA residues overlay when zoomed in */
+    height: 6px;
+    margin-top: 5px;
+    background-color: #A4BBFF; 
+}
+ 
+.annot-CDS, 
+.plus-annot-CDS,
+.minus-annot-CDS {
+    height: 16px;
+    margin-top: 0px; 
+    /* background-color: #0460d9; original color choice based on kuler.adobe.com */
+    background-color: rgb(79, 134, 207);  /* adjusted to better see DNA residues overlay when zoomed in */
+    border: 2px solid rgb(15,66,133);
+}
+
+.annot-UTR, 
+.plus-annot-UTR, 
+.minus-annot-UTR {
+    height: 16px; 
+    margin-top: 0px; 
+    background-color: #FFF7cc;
+    border: 2px solid rgb(15,66,133);
+}
+
+/*  nucleotide alignments from MAKER */
+.blastn-part, 
+.plus-blastn-part, 
+.minus-blastn-part  {
+    background-color: rgb(102,204,0);
+}
+
+.est2genome-part, 
+.plus-est2genome-part, 
+.minus-est2genome-part   {
+    background-color: rgb(250,250,210);
+}
+
+.est_gff-part, 
+.plus-est_gff-part, 
+.minus-est_gff-part  {
+    background-color: rgb(251,255,179);
+}
+
+/*  protein alignments from MAKER */
+.blastx-part, 
+.plus-blastx-part, 
+.minus-blastx-part  {
+    background-color: pink;
+}
+
+.protein2genome-part, 
+.plus-protein2genome-part, 
+.minus-protein2genome-part  {
+    background-color: rgb(102,204,0);
+}
+
+/* ???? not found in gff3.tiers 
+.protein_gff-part, 
+.plus-protein_gff-part, 
+.minus-protein_gff-part  {
+
+}
+*/
+
+.tblastx-part, 
+.plus-tblastx-part, 
+.minus-tblastx-part  {
+    background-color: rgb(102,51,102);
+}
+
+/*  ????  couldn't find in gff3.tiers
+.cdna2genome-part, 
+.plus-cdna2genome-part, 
+.minus-cdna2genome-part  {
+
+}
+*/
+
+/*  ????  couldn't find in gff3.tiers
+.altest_gff-part, 
+.plus-altest_gff-part, 
+.minus-altest_gff-part  {
+
+}
+*/
+
+.gvf,
+.plus-gvf,
+.minus-gvf  {
+    height: 16px;
+    background-color: pink;
+    min-width: 4px;
+}
+
+.maker-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(51,102,255);
+}
+
+.maker-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(51,102,255);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+.augustus-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(255,204,255);
+}
+
+.augustus-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(255,204,255);
+    background-color: #142933;
+}
+
+.snap-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(153,255,204);
+}
+
+.snap-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(153,255,204);
+    background-color: #142933;
+}
+
+.genemark-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(103,155,104);
+}
+
+.genemark-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+
+/* ???? not sure about pred_gff, same as GeneMark? 
+.pred_gff-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(103,155,104);
+}
+.pred_gff-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+*/
+
+/* ???? couldn't find model_gff in gff3.tiers 
+.model_gff-CDS {
+
+}
+.model_gff-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+*/
+
+.fgenesh-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(55,153,255);
+}
+
+.fgenesh-UTR {
+    height: 80%;
+    top: 10%;
+/*    background-color: rgb(255,204,255); */
+    border-style: solid;
+    border-color: rgb(255,153,255);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+/*  for MAKER repeatmasker, using just regular render-class (gray line) */
+/*  ???? for MAKER repeat_gff, just use regular render-class (gray line)? */
+
+/*  ???? repeatrunner is different than repeatmasker in gff3.tiers, 
+    should we make it different also for webapollo?
+.repeatrunner, 
+.plus-repeatrunner, 
+.minus-repeatrunner  {
+}
+*/
+
+.left-edge-match  {
+    border-left: solid red 3px;
+}
+
+.right-edge-match  {
+    border-right: solid red 3px;
+}
+
+.cds-frame0 {
+    background-color: #FF8080;
+}
+
+.cds-frame1 {
+    background-color: #80FF80; 
+}
+
+.cds-frame2 {
+    background-color: #8080FF;
+}
+
+
+.neg-cds-frame0 {
+    background-color: #8080FF;
+}
+
+.neg-cds-frame1 {
+    background-color: #80FF80;
+}
+
+.neg-cds-frame2 {
+    background-color: #FF8080;
+}
+
+/* end of MAKER styles */
\ No newline at end of file
diff --git a/client/apollo/css/maker_darkbackground.css b/client/apollo/css/maker_darkbackground.css
new file mode 100644
index 0000000..47c9dab
--- /dev/null
+++ b/client/apollo/css/maker_darkbackground.css
@@ -0,0 +1,357 @@
+/* styles for MAKER tracks and additional customization of WebApollo */
+.Dark .track_jbrowse_view_track_wiggle_xyplot .canvas-track {
+    background-color: hsla(240, 2%, 18%, 1);
+}
+
+.Dark .track_jbrowse_view_track_wiggle_density .canvas-track {
+    background-color: hsla(240, 2%, 18%, 1);
+}
+.Dark .wigglePositionIndicator {
+	background: white;
+}
+
+.Dark div.track  {
+    /* based on CoolWorld color palette from kuler.adobe.com */
+    /*background-color: #142933;*/
+    border:0px;
+}
+.Dark .sequence_blur {
+    background-color: #999;
+}
+.Dark div.pin_underlay {
+    background-color: #142933;
+}
+
+
+.Dark div.outerTrackContainer {
+    background-color: rgb(59, 59, 61);
+}
+
+
+.Dark div.gridline_major {
+    border-color : rgb(41,40,38);
+}
+
+.Dark div.gridline_minor {
+    border-color : rgba(96,96, 104, 0.72);
+}
+
+
+.Dark div#overviewtrack_overview_loc_track {
+    background-color: transparent;
+}
+
+.Dark div#gridtrack {
+    background-color: transparent;
+}
+
+
+.Dark .feature-name {
+    color: #FAFFF9;
+}
+
+.Dark div.feature-description {
+    color: #bbb;
+}
+/* UI elements */
+.Dark div.locationTrap {
+    background-color: #929292;
+	border-bottom-color: #0b2648;
+}
+
+.Dark .pin_underlay.track.block {
+    background-color: rgb(59, 59, 61);
+}
+
+.Dark .dna-residues.forward-strand,
+.Dark .dna-residues.reverse-strand {
+    background-color: transparent;
+}
+
+.Dark div.wa-sequence .dna-residues.forward-strand {
+    color: rgb(240, 240, 240);
+    z-index: 5;
+}
+
+.Dark div.wa-sequence .dna-residues.reverse-strand {
+    color: rgb(170, 169, 169);
+    z-index: 5;
+}
+/*  nucleotide alignments from MAKER */
+.Dark .blastn-part,
+.Dark .plus-blastn-part,
+.Dark .minus-blastn-part  {
+    background-color: rgb(102,204,0);
+}
+
+.Dark .est2genome-part,
+.Dark .plus-est2genome-part,
+.Dark .minus-est2genome-part   {
+    background-color: rgb(250,250,210);
+}
+
+.Dark .est_gff-part,
+.Dark .plus-est_gff-part,
+.Dark .minus-est_gff-part  {
+    background-color: rgb(251,255,179);
+}
+
+.Dark .mike-repeat-part,
+.Dark .plus-mike-repeat-part,
+.Dark .minus-mike-repeat-part  {
+    background-color: rgb(255,0,0);
+}
+
+.Dark .mike-snap-part,
+.Dark .plus-mike-snap-part,
+.Dark .minus-mike-snap-part  {
+    background-color: rgb(153,255,204);
+}
+
+.Dark .mike-augustus-part,
+.Dark .plus-mike-augustus-part,
+.Dark .minus-mike-augustus-part  {
+    background-color: rgb(255,204,255);
+}
+
+/*  protein alignments from MAKER */
+.Dark .blastx-part,
+.Dark .plus-blastx-part,
+.Dark .minus-blastx-part  {
+    background-color: pink;
+}
+
+.Dark .protein2genome-part,
+.Dark .plus-protein2genome-part,
+.Dark .minus-protein2genome-part  {
+    background-color: rgb(255,255,0);
+}
+
+/* ???? not found in gff3.tiers
+.Dark .protein_gff-part,
+.Dark .plus-protein_gff-part,
+.Dark .minus-protein_gff-part  {
+}
+*/
+
+.Dark .tblastx-part,
+.Dark .plus-tblastx-part,
+.Dark .minus-tblastx-part  {
+    background-color: rgb(102,51,102);
+}
+
+/*  ????  couldn't find in gff3.tiers
+.Dark .cdna2genome-part,
+.Dark .plus-cdna2genome-part,
+.Dark .minus-cdna2genome-part  {
+}
+*/
+
+/*  ????  couldn't find in gff3.tiers
+.Dark .altest_gff-part,
+.Dark .plus-altest_gff-part,
+.Dark .minus-altest_gff-part  {
+}
+*/
+
+.Dark .maker-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(51,102,255);
+}
+
+.Dark .maker-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(51,102,255);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+.Dark .upd-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(0,204,0);
+}
+
+.Dark .upd-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(0,204,0);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+.Dark .TAIR-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(255,140,0);
+}
+
+.Dark .TAIR-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(255,140,0);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+.Dark .TAIR-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(255,140,0);
+}
+
+.Dark .TAIR-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(255,140,0);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+.Dark .psu-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(153,0,76);
+}
+
+.Dark .psu-UTR {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(153,0,76);
+}
+
+.Dark .nc-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(255,0,127);
+}
+
+.Dark .nc-UTR {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(255,0,127);
+}
+
+.Dark .alt-CDS  {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(138,43,226);
+}
+
+.Dark .alt-UTR {
+    height: 80%;
+    top: 10%;
+    border-style: solid;
+    border-color: rgb(138,43,226);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+
+.Dark .snap-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(153,255,204);
+}
+
+.Dark .snap-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(153,255,204);
+    background-color: #142933;
+}
+
+.Dark .genemark-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(103,155,104);
+}
+
+.Dark .genemark-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+
+/* ???? not sure about pred_gff, same as GeneMark?
+.Dark .pred_gff-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(103,155,104);
+}
+.Dark .pred_gff-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+*/
+
+/* ???? couldn't find model_gff in gff3.tiers
+.Dark .model_gff-CDS {
+}
+.Dark .model_gff-UTR {
+    height: 80%;
+    top: 10%;
+    border: 1px solid rgb(103,155,104);
+    background-color: #142933;
+}
+*/
+
+.Dark .fgenesh-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(55,153,255);
+}
+
+.Dark .fgenesh-UTR {
+    height: 80%;
+    top: 10%;
+    /*    background-color: rgb(255,204,255); */
+    border-style: solid;
+    border-color: rgb(255,153,255);
+    border-width: 1px;
+    background-color: #142933;
+}
+
+/*  for MAKER repeatmasker, using just regular render-class (gray line) */
+/*  ???? for MAKER repeat_gff, just use regular render-class (gray line)? */
+
+/*  ???? repeatrunner is different than repeatmasker in gff3.tiers,
+    should we make it different also for webapollo?
+.Dark .repeatrunner,
+.Dark .plus-repeatrunner,
+.Dark .minus-repeatrunner  {
+}
+*/
+
+.Dark .left-edge-match  {
+    border-left: solid black 2px !important;
+}
+
+.Dark .right-edge-match  {
+    border-right: solid black 2px !important;
+}
+
+.Dark .selected-annotation {
+	outline-color: black;
+}
+
+/* start NeatHTMLFeatures styles */
+
+.Dark div.neat-UTR {
+    background-color: #DDD;
+}
+.Dark polyline.neat-intron {
+    stroke: lightgrey;
+}
+/* end NeatHTMLFeatures styles */
+
+/* end of MAKER styles */
diff --git a/client/apollo/css/maker_styles.css b/client/apollo/css/maker_styles.css
new file mode 100644
index 0000000..5d2ac10
--- /dev/null
+++ b/client/apollo/css/maker_styles.css
@@ -0,0 +1,448 @@
+/*MAKER CSS with colors for common features*/
+
+/*SNAP*/
+.snap-exon,
+.plus-snap-exon,
+.minus-snap-exon,
+.snap-five_prime_UTR,
+.plus-snap-five_prime_UTR,
+.minus-snap-five_prime_UTR,
+.snap-three_prime_UTR,
+.plus-snap-three_prime_UTR,
+.minus-snap-three_prime_UTR {
+    position: absolute;
+    height: 7px;
+    background-color: #99FFCC;
+    border-style: solid;
+    border-color: #D88;
+    border-width: 2px 0px 2px 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+.Apollo .snap-exon,
+.Apollo .plus-snap-exon,
+.Apollo .minus-snap-exon,
+.Apollo .snap-five_prime_UTR,
+.Apollo .plus-snap-five_prime_UTR,
+.Apollo .minus-snap-five_prime_UTR,
+.Apollo .snap-three_prime_UTR,
+.Apollo .plus-snap-three_prime_UTR,
+.Apollo .minus-snap-three_prime_UTR {
+    position: absolute;
+    height: 80%;
+    top: 10%;
+    background-color: #99DDCC;
+    border: 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+/*Augustus*/
+.augustus-exon,
+.plus-augustus-exon,
+.minus-augustus-exon,
+.augustus-five_prime_UTR,
+.plus-augustus-five_prime_UTR,
+.minus-augustus-five_prime_UTR,
+.augustus-three_prime_UTR,
+.plus-augustus-three_prime_UTR,
+.minus-augustus-three_prime_UTR {
+    position: absolute;
+    height: 7px;
+    background-color: #FFCCFF;
+    border-style: solid;
+    border-color: #D88;
+    border-width: 2px 0px 2px 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+
+.Apollo .augustus-exon,
+.Apollo .plus-augustus-exon,
+.Apollo .minus-augustus-exon,
+.Apollo .augustus-five_prime_UTR,
+.Apollo .plus-augustus-five_prime_UTR,
+.Apollo .minus-augustus-five_prime_UTR,
+.Apollo .augustus-three_prime_UTR,
+.Apollo .plus-augustus-three_prime_UTR,
+.Apollo .minus-augustus-three_prime_UTR {
+    position: absolute;
+    height: 80%;
+    top: 10%;
+    background-color: #FFCCFF;
+    border-style: solid;
+    border-color: #D88;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+/*GeneMark*/
+.Apollo .genemark-exon,
+.Apollo .plus-genemark-exon,
+.Apollo .minus-genemark-exon,
+.Apollo .genemark-five_prime_UTR,
+.Apollo .plus-genemark-five_prime_UTR,
+.Apollo .minus-genemark-five_prime_UTR,
+.Apollo .genemark-three_prime_UTR,
+.Apollo .plus-genemark-three_prime_UTR,
+.Apollo .minus-genemark-three_prime_UTR {
+    position: absolute;
+    height: 80%;
+    top: 10%;
+    background-color: #679B68;
+    border-style: solid;
+    border-color: #D88;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+/*GeneMark*/
+.genemark-exon,
+.plus-genemark-exon,
+.minus-genemark-exon,
+.genemark-five_prime_UTR,
+.plus-genemark-five_prime_UTR,
+.minus-genemark-five_prime_UTR,
+.genemark-three_prime_UTR,
+.plus-genemark-three_prime_UTR,
+.minus-genemark-three_prime_UTR {
+    position: absolute;
+    height: 7px;
+    background-color: #679B68;
+    border-style: solid;
+    border-color: #D88;
+    border-width: 2px 0px 2px 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*FGENESH*/
+.Apollo .fgenesh-exon,
+.Apollo .plus-fgenesh-exon,
+.Apollo .minus-fgenesh-exon,
+.Apollo .fgenesh-five_prime_UTR,
+.Apollo .plus-fgenesh-five_prime_UTR,
+.Apollo .minus-fgenesh-five_prime_UTR,
+.Apollo .fgenesh-three_prime_UTR,
+.Apollo .plus-fgenesh-three_prime_UTR,
+.Apollo .minus-fgenesh-three_prime_UTR {
+    position: absolute;
+    height: 7px;
+    background-color: #FF99FF;
+    border-style: solid;
+    border-color: #D88;
+    border-width: 2px 0px 2px 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+
+/*FGENESH*/
+.fgenesh-exon,
+.plus-fgenesh-exon,
+.minus-fgenesh-exon,
+.fgenesh-five_prime_UTR,
+.plus-fgenesh-five_prime_UTR,
+.minus-fgenesh-five_prime_UTR,
+.fgenesh-three_prime_UTR,
+.plus-fgenesh-three_prime_UTR,
+.minus-fgenesh-three_prime_UTR {
+    position: absolute;
+    height: 7px;
+    background-color: #FF99FF;
+    border-style: solid;
+    border-color: #D88;
+    border-width: 2px 0px 2px 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*protein2genome*/
+.Apollo .protein2genome_part,
+.Apollo .plus-protein2genome_part,
+.Apollo .minus-protein2genome_part {
+    position: absolute;
+    height: 60%;
+    margin-top: 0px;
+    top: 20%;
+    background-color: #FFFF00;
+    border-style: solid;
+    border-color: #6E6E6E;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+/*protein2genome*/
+.protein2genome_part,
+.plus-protein2genome_part,
+.minus-protein2genome_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #FFFF00;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*BLASTN*/
+.blastn_part,
+.plus-blastn_part,
+.minus-blastn_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #66CC00;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*BLASTN*/
+.Apollo .blastn_part,
+.Apollo .plus-blastn_part,
+.Apollo .minus-blastn_part {
+    position: absolute;
+    height: 60%;
+    margin-top: 0px;
+    top: 20%;
+    background-color: #66CC00;
+    border-style: solid;
+    border-color: #6E6E6E;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+/*BLASTX*/
+.blastx_part,
+.plus-blastx_part,
+.minus-blastx_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #FF00FF;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*BLASTX*/
+.Apollo .blastx_part,
+.Apollo .plus-blastx_part,
+.Apollo .minus-blastx_part {
+    position: absolute;
+    height: 60%;
+    margin-top: 0;
+    top: 20%;
+    background-color: #FF00FF;
+    border-style: solid;
+    border-color: #6E6E6E;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+
+/*TBLASTX*/
+.tblastx_part,
+.plus-tblastx_part,
+.minus-tblastx_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #663366;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*TBLASTX*/
+.Apollo .tblastx_part,
+.Apollo .plus-tblastx_part,
+.Apollo .minus-tblastx_part {
+    position: absolute;
+    height: 60%;
+    margin-top: 0px;
+    top: 20%;
+    background-color: #663366;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+/*est2genome*/
+.est2genome_part,
+.plus-est2genome_part,
+.minus-est2genome_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #FAFAD2;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+/*est2genome*/
+.Apollo .est2genome_part,
+.Apollo .plus-est2genome_part,
+.Apollo .minus-est2genome_part {
+    position: absolute;
+    height: 60%;
+    top: 20%;
+    margin-top: 0px;
+    background-color: #FAFAD2;
+    border-style: solid;
+    border-color: #6E6E6E;
+    z-index: 8;
+    min-width: 1px;
+}
+
+/*repeat*/
+.repeat_part,
+.plus-repeat_part,
+.minus-repeat_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #FF0000;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+/*repeat*/
+.Apollo .repeat_part,
+.Apollo .plus-repeat_part,
+.Apollo .minus-repeat_part {
+    position: absolute;
+    height: 60%;
+    top: 20%;
+    margin-top: 0px;
+    background-color: #FF0000;
+    border-style: solid;
+    border-color: #6E6E6E;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+/*cdna2genome*/
+.cdna2genome_part,
+.plus-cdna2genome_part,
+.minus-cdna2genome_part {
+    position: absolute;
+    height: 4px;
+    margin-top: -2px;
+    background-color: #8C468C;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+
+/*cdna2genome*/
+.Apollo .cdna2genome_part,
+.Apollo .plus-cdna2genome_part,
+.Apollo .minus-cdna2genome_part {
+    position: absolute;
+    height: 60%;
+    margin-top: 0px;
+    top: 20%;
+    background-color: #8C468C;
+    border-style: solid;
+    border-color: #6E6E6E;
+    border-width: 1px 1px 1px 1px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+
+
+/*cdna2genome*/
+.Apollo .exon,
+.Apollo .minus-exon,
+.Apollo .plus-exon {
+    position: absolute;
+    height: 60%;
+    margin-top: 0px;
+    top: 20%;
+    background-color: #6C468C;
+    border: 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+/*cdna2genome*/
+.Apollo .CDS {
+    position: absolute;
+    height: 80%;
+    margin-top: 0px;
+    top: 10%;
+    background-color: #8C468C;
+    border: 0px;
+    z-index: 8;
+    min-width: 1px;
+    cursor: pointer;
+}
+
+
+.Apollo .feature {
+    background-image: none;
+}
+ 
+.Apollo .generic_parent {
+    background-image: none;
+}
+
+.Apollo .transcript, .plus-transcript, .minus-transcript {
+    background: white;
+}
+
+.Apollo .plus-feature {
+    background-color: transparent;
+}
+.Apollo .minus-feature {
+    background-color: transparent;
+}
diff --git a/client/apollo/css/search_sequence.css b/client/apollo/css/search_sequence.css
new file mode 100644
index 0000000..91260bb
--- /dev/null
+++ b/client/apollo/css/search_sequence.css
@@ -0,0 +1,76 @@
+ at CHARSET "UTF-8";
+
+.search_sequence_label {
+	font-size: 1.5em;
+}
+
+.search_sequence_input {
+	width: 50em;
+	height: 3.5em;
+}
+
+.search_sequence_area {
+	margin-bottom: 10px;
+}
+
+.search_all_ref_seqs_area {
+	margin-top: 5px;
+	margin-bottom: 5px;
+}
+
+.search_all_ref_seqs_checkbox {
+	margin-right: 3px;
+}
+
+.search_all_ref_seqs_label {
+	font-size: 1em;
+}
+
+.search_sequence_matches_row:hover {
+	background-color: #000099;
+	color: white;
+}
+
+.search_sequence_matches_generic_field {
+	width: 6.3em;
+	margin-right: 0.2em;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	font-size: 1.6em;
+	background-color: inherit;
+	display: table-cell;
+}
+
+.search_sequence_matches_header {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches_field {
+	margin-top: 0px;
+	margin-bottom: 0px;
+	float: left;
+	overflow: hidden;
+	width: 6em;
+}
+
+.search_sequence_tools {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches {
+	max-height: 25em;
+	overflow: auto;
+}
+
+.search_sequence_matches_row {
+	display: table-row;
+}
+
+
+.search_sequence_message {
+	font-size: 1.6em;
+}
+
+.search_sequence {
+	font-size: 9px;
+}
diff --git a/client/apollo/css/webapollo_track_styles.css b/client/apollo/css/webapollo_track_styles.css
new file mode 100644
index 0000000..abf0281
--- /dev/null
+++ b/client/apollo/css/webapollo_track_styles.css
@@ -0,0 +1,882 @@
+
+.track .global_highlight {
+    background: rgba( 255, 255, 0, 0.5 );
+}
+
+.feature, .plus-feature, .minus-feature {
+  height: 10px;
+}
+
+.webapollo-CDS {
+    height: 80%;
+    top: 10%;
+    background-color: rgb(218, 170, 241);
+    border-style: solid;
+    border-color: rgb(173, 92, 194);
+    border-width: 1px;
+}
+
+.webapollo-UTR {
+    height: 50%;
+    top: 25%;
+    background-color: rgb(173, 92, 194);
+}
+
+.bam-read{
+    height: 5px;
+    background-color: #AACDDC;
+    z-index: 8;
+}
+
+.plus-bam-read {
+    height: 5px;
+    background-color: #EC8B8B;
+    z-index: 8;
+}
+
+.minus-bam-read {
+    height: 5px;
+    background-color: #898FD8;
+    z-index: 8;
+}
+
+
+.track.track_annotations {
+    background-color: #FFFFDD;
+}
+
+.Dark .track.track_annotations {
+    background-color: #676767;
+}
+
+div.track_localannot {
+    background-color: #DDDDCC;
+}
+
+/* 
+   To support WebApollo with sequence alteration features shown on SequenceTrack, 
+   sequence style MUST NOT have a z-index specified 
+*/
+div.wa-sequence {
+    position: absolute;
+    left: 0px;
+    font-family: monospace;
+    font-size: 10px;
+    letter-spacing: 2px;
+    padding-left: 2px;
+    cursor: pointer; 
+    user-select: none;
+}
+
+/* 
+   block_seq_container are SequenceTrack divs that are child of .block and parent of .dna-residues
+   need to break this out from div.sequence because .sequence is also used for determining 
+        font character width and height in GenomeView.calculateSequenceCharacterSize(), 
+	and for that don't want a specified width
+*/
+div.block-seq-container {
+    top: 15px;
+    width: 100%;
+}
+
+div.wa-sequence .dna-container  {
+    position: absolute;
+    width: 100%;
+}
+
+div.wa-sequence .dna-residues.forward-strand {
+    color: black;
+    z-index: 5;
+}
+.Dark div.wa-sequence .dna-residues.forward-strand {
+	color: white;
+}
+
+div.wa-sequence .dna-residues.reverse-strand {
+    color: gray;
+    border-top: 1px solid lightgray;
+    z-index: 5;
+    /* outline: 1px solid pink; */
+}
+.Dark div.wa-sequence .dna-residues.reverse-strand {
+	color: lightgray;
+}
+
+div.wa-sequence .aa-residues  {
+    color: black;
+    z-index: 5;
+}
+.Dark div.wa-sequence .aa-residues  {
+    color: white;
+}
+
+.aa-residues.cds-frame0 {
+    background-color: #999999; 
+    z-index: 5;
+}
+
+.aa-residues.cds-frame1 {
+    background-color: #BBBBBB; 
+    z-index: 5;
+}
+
+.aa-residues.cds-frame2 {
+    background-color: #DDDDDD; 
+    z-index: 5;
+}
+
+/* highlighting of dna residues in DNA track on mouseover */
+div.wa-sequence .dna-highlighted {
+    background: #F9BF3A
+}
+
+div.wa-sequence .highlighted {
+    background: #ff0;
+}
+
+div.annot-sequence  {
+    position: absolute;
+    left: 0px;
+    font-family: monospace;
+    font-size: 10px;
+    letter-spacing: 2px;
+    padding-left: 2px;
+    z-index: 15;
+    /*  
+	need pointer-events:none so that any events pass through annot-sequence overlay
+	and onto the block or feature div underneath
+    */ 
+    pointer-events: none;
+
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -o-user-select: none;
+    user-select: none;
+}
+
+
+/********************************************************
+*   invisible containers, 
+*   for features that also have a renderClass that gets centered, 
+*      and for subfeatures that have rendered children 
+*      (currently only subfeatures like this are exons, which have CDS and UTR child divs)
+********************************************************/
+.container-16px, 
+.plus-container-16px, 
+.minus-container-16px  {
+    height: 16px;
+    background-color: transparent;
+}
+
+.container-14px, 
+.plus-container-14px, 
+.minus-container-14px  {
+    height: 14px;
+    background-color: transparent;
+}
+
+.container-12px, 
+.plus-container-12px, 
+.minus-container-12px  {
+    height: 12px;
+    background-color: transparent;
+}
+
+.container-10px, 
+.plus-container-10px, 
+.minus-container-10px  {
+    height: 10px;
+    background-color: transparent;
+}
+
+.container-8px, 
+.plus-container-8px, 
+.minus-container-8px {
+    height: 8px;
+    background-color: transparent;
+}
+
+/* intended for subfeatures that have rendered children, 
+*  want to size subfeature container to same height as parent feature
+*      (currently only subfeatures like this are exons, which have CDS and UTR child divs)
+*/
+.container-100pct, 
+.plus-container-100pct, 
+.minus-container-100pct {
+    height: 100%;
+    background-color: transparent;
+}
+
+.feature-render  {
+    position: absolute;
+    min-width: 1px;
+    width: 100%;
+    /* feature render div may be added to feature div _after_ subfeature divs, so 
+       if want subfeature divs in front of feature render div, make sure feature render div has lower 
+       z-index than subfeature divs */
+    z-index: 2;  
+}
+
+/**
+ *   Basic boxes for subfeatures (and for "render-*" rendering div for features with container divs)
+ *   height is % of parent feature, top % determined based on height to ensure vertically centered
+ */
+.gray-center-30pct  {
+    background-color: gray;
+    height: 30%;
+    top: 35%;
+}
+
+
+.gray-center-50pct  {
+    background-color: lightgray;
+    height: 50%;
+    top: 25%;
+}
+
+
+.gray-center-20pct  {
+    position:absolute;
+    background-color: gray;
+    min-width: 1px;
+    width: 100%;
+    height: 20%;
+    top: 40%;
+    /* annotline div may be added to annot div _after_ child feature divs, so 
+       if want child divs in front of annotline, make sure has lower 
+       z-index than child divs */
+    z-index: 2;  
+}
+
+.gray-center-10pct  {
+    position:absolute;
+    background-color: gray;
+    min-width: 1px;
+    width: 100%;
+    height: 10%;
+    top: 45%;
+    /* annotline div may be added to annot div _after_ child feature divs, so 
+       if want child divs in front of annotline, make sure has lower 
+       z-index than child divs */
+    z-index: 2;  
+}
+
+
+
+
+.pink-90pct, 
+.plus-pink-90pct, 
+.minus-pink-90pct  {
+    height: 90%;
+    top: 5%;
+    background-color: #D8BDEB;
+    border: 1px solid #01F;
+}
+
+.pink-12px, 
+.plus-pink-12px, 
+.minus-pink-12px {
+    background-color: #D8BDEB;
+    border: 1px solid #01F;
+    height: 12px;
+}
+
+.pink-16px, 
+.plus-pink-16px, 
+.minus-pink-16px {
+    background-color: #D8BDEB;
+    border: 1px solid #01F;
+    height: 16px;
+}
+
+.purple-60pct, 
+.plus-purple-60pct, 
+.minus-purple-60pct  {
+    background-color: #8F408F;
+    height: 60%;
+    top: 20%;
+}
+
+.purple-8px, 
+.plus-purple-8px, 
+.minus-purple-8px {
+    background-color: #8F408F;
+    height: 8px; 
+}
+
+.darkblue-80pct, 
+.plus-darkblue-80pct, 
+.minus-darkblue-80pct  {
+    background-color: #1F3DDE;
+    height: 80%;
+    top: 10%;
+}
+
+.bluegreen-80pct, 
+.plus-bluegreen-80pct, 
+.minus-bluegreen-80pct  {
+    background-color: #3BA08E;
+    height: 80%;
+    top: 10%;
+}
+
+
+.brightgreen-80pct, 
+.plus-brightgreen-80pct, 
+.minus-brightgreen-80pct  {
+    background-color: #21D61F;
+    border: 1px solid #555;
+    height: 80%;
+    top: 10%;
+}
+
+.darkgreen-60pct, 
+.plus-darkgreen-60pct, 
+.minus-darkgreen-60pct {
+    height: 60%;
+    top: 20%;
+    background-color: #8DB890;
+}
+
+.trellis-CDS, 
+.plus-trellis-CDS, 
+.minus-trellis-CDS {
+    background-color: gold;
+    border: 1px solid gray;
+    height: 80%;
+    top: 10%;
+}
+
+.trellis-UTR, 
+.plus-trellis-UTR, 
+.minus-trellis-UTR  {
+    background-color: #B39700;
+    height: 60%;
+    top: 20%;
+}
+
+.trellis-match-part, 
+.plus-trellis-match-part, 
+.minus-trellis-match-part  {
+    background-color: #1F3DDE;
+    height: 60%;
+    top: 10%;
+}
+
+/* CIGAR string "M" subfeature, indicating "alignment match" (can be a sequence match or mismatch) of aligned sequence relative to viewed sequence */
+.cigarM {
+    height: 100%; 
+    background-color: #1B8A99;
+    z-index: 8;   /* rendered below most other subfeatures */
+    min-width: 1px;
+}
+
+.plus-cigarM {
+    height: 100%;
+    /*background-color: #898FD8;*/
+    background-color: indianred;
+    /*background-color: blue;*/
+    z-index: 8;   /* rendered below most other subfeatures */
+    min-width: 1px;
+}
+
+.minus-cigarM  {
+    height: 100%;
+    /*background-color: #EC8B8B;*/
+    background-color: royalblue;
+    /*background-color: red;*/
+    z-index: 8;   /* rendered below most other subfeatures */
+    min-width: 1px;
+}
+
+/* CIGAR string "=" (or "E") subfeature, indicating exact sequence match of aligned sequence relative to viewed sequence */
+.cigarEQ, 
+.plus-cigarEQ, 
+.minus-cigarEQ  {
+    height: 100%; 
+    background-color: #1B8A99;
+    z-index: 10; /* rendered above more generic "M" type */
+    min-width: 1px;
+}
+
+/* CIGAR string "X" subfeature, indicating mismatch of aligned sequence relative to viewed sequence */
+.cigarX, 
+.plus-cigarX, 
+.minus-cigarX  {
+    height: 100%; 
+    background-color: rgb(182, 167, 0);  
+    z-index: 11;  /* rendered above matches, but below deletions and insertions */
+    min-width: 3px;
+}
+
+
+
+/* align_insertion, align_deletion, align_skip, align_mismatch are assigned to 
+   non-feature child divs of bam-read features in DraggableAlignments._drawMismatches()
+*/
+.align_insertion {
+    background-color: #00FF00;
+    height: 100%;
+    min-width: 2px;
+    z-index: 20;
+}
+
+.align_deletion {
+    background-color: #FF0000;
+    height: 100%;
+    min-width: 2px;
+    z-index: 20;
+}
+
+.align_skip {
+}
+
+.align_mismatch {
+    background-color: #D8C046;
+    height: 100%;
+    min-width: 2px;
+    z-index: 12;
+}
+
+/** container */
+.annot,  
+.plus-annot,
+.minus-annot {
+  height: 16px;
+  background-color: transparent;
+  user-select: none;
+}
+
+.annot-render {
+    height: 8px;
+    margin-top: 4px;
+    background-color: #A4BBFF;
+}
+
+.annot-apollo{
+
+}
+ 
+.annot-CDS, 
+.plus-annot-CDS,
+.minus-annot-CDS {
+    height: 16px;
+    background-color: #30AAFF;
+    border: 1px solid #01F;
+}
+
+.Dark .annot-CDS,
+.Dark .plus-annot-CDS,
+.Dark .minus-annot-CDS {
+    height: 16px;
+    background-color: #30AAFF;
+    border: 1px solid hsla(205, 100%, 25%, 1);
+}
+
+.annot-UTR, x
+.plus-annot-UTR, 
+.minus-annot-UTR {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: #2088FF;
+}
+
+.noncanonical-splice-site, 
+.plus-noncanonical-splice-site,  
+.minus-noncanonical-splice-site  { 
+    margin-left: -8px;
+    /* margin-top: -11px;  */
+    /*  moved noncanonical icon to bottom of annotation, prefer top of annotation but 
+	need to have a some padding at top of track before that works */
+    margin-top: 9px;  
+    padding-left: 8px;
+    padding-right: 8px;
+    position: absolute;
+    height: 16px;
+    z-index: 100;
+    background-color: transparent;
+    background-image: url('../img/exclamation_circle_orange.png');
+    pointer-events: none;   /*  attempting to route around issue with centered non-canon splice sites preventing edge-drag */
+/*    background-image: url('img/exclamation_circle_orange.png');  */
+/*    background-image: url('img/warning_exclamation_small.png'); */
+/*    background-image: url('img/warning_exclamation.png');  */
+/*    background-image: url('img/marker_rounded_red.png'); */
+/*    background-image: url('img/marker_squared_red.png'); */
+}
+
+
+/* 
+   for styles of features that are on SequenceTrack, 
+   z-index MUST be > z-index of 
+       (div.sequence .dna-residues.forward-strand) and 
+       (div.sequence .dna-residues.reverse-strand) styles
+*/
+.Apollo .sequence-alteration.deletion, 
+.Apollo .sequence-alteration.plus-deletion,
+.Apollo .sequence-alteration.minus-deletion {
+    border-style: solid;
+    border-color: blue;
+    border-width: 1px;
+    height: 100%;
+    background-color: rgba(150,0,0,0.3);
+    z-index: 20;  
+}
+
+.Apollo .sequence-alteration.insertion, 
+.Apollo .sequence-alteration.plus-insertion,
+.Apollo .sequence-alteration.minus-insertion {
+    border-style: solid;
+    border-color: green;
+    border-width: 1px;
+    height: 100%;
+    background-color: rgba(0,150,0,0.3);
+    z-index: 20;  
+}
+
+.Apollo .sequence-alteration.substitution, 
+.Apollo .sequence-alteration.plus-substitution,
+.Apollo .sequence-alteration.minus-substitution {
+    border-style: solid;
+    border-color: blue;
+    border-width: 1px;
+    height: 100%;
+    background-color: rgba(250,250,0,0.3);
+    z-index: 20;  
+}
+
+.plus-trellis-arrowhead,
+.plus-webapollo-arrowhead,
+.plus-annot-arrowhead  {
+    position: absolute;
+    width: 12px;
+    height: 100%;
+    background-image: url('../img/plus-arrowhead.png');
+    background-repeat: no-repeat;
+    background-position: left center;  /* center image vertically */
+    z-index: 15;
+}
+
+.minus-trellis-arrowhead,
+.minus-webapollo-arrowhead,
+.minus-annot-arrowhead {
+    position: absolute;
+    width: 12px;
+    height: 100%;
+    background-image: url('../img/minus-arrowhead.png');
+    background-repeat: no-repeat;
+    background-position: right center;  /* center image vertically */
+    z-index: 15;
+}
+
+.colorCds .cds-frame0 {
+    background-color: #FF8080;
+}
+
+.colorCds .cds-frame1 {
+    background-color: #80FF80;
+}
+
+.colorCds .cds-frame2 {
+    background-color: #8080FF;
+}
+
+.colorCds .neg-cds-frame0 {
+    background-color: #8080FF;
+}
+
+.colorCds .neg-cds-frame1 {
+    background-color: #80FF80;
+}
+
+.colorCds .neg-cds-frame2 {
+    background-color: #FF8080;
+}
+
+
+.Dark.colorCds .dna-residues.cds-frame0 {
+	background-color: #902748
+}
+
+.Dark.colorCds .dna-residues.cds-frame1 {
+	background-color: #366f4b;
+}
+
+.Dark.colorCds .dna-residues.cds-frame2 {
+	background-color: #4e408e;
+}
+
+.Dark div.block.height_overflow .height_overflow_message {
+	color: white;
+	text-shadow: none;
+}
+
+.Dark div.ruler svg g line {
+	stroke: white;
+}
+
+.Dark.colorCds .feature .cds-frame0 {
+    border: 1px solid hsla(0, 100%, 25%, 1);
+}
+
+.Dark.colorCds .feature .cds-frame1 {
+    border: 1px solid hsla(120, 100%, 25%, 1);
+}
+
+.Dark.colorCds .feature .cds-frame2 {
+    border: 1px solid hsla(240, 100%, 25%, 1);
+}
+
+
+.Dark .track .loading {
+	background: rgb(59, 59, 61);
+	color: white;
+}
+
+/**
+*   appearance of resizing box when dragging annotation edges
+*   if case browser doesn't support transparency via "rgba", fall back on solid background?
+*/
+.ui-resizable-helper  {
+    border: 2px dotted red;
+    background: rgb(100, 150, 255);
+    background: rgba(100, 150, 255, 0.5);
+    
+}
+
+/**
+*  By default, no styling associated with custom multifeature draggable helper
+*  But leaving here for easing toggling of diagnostic rendering
+*/
+.custom-multifeature-draggable-helper  {
+/*  
+    outline-style: dotted; 
+    outline-color: red;
+    outline-width: 4px; 
+*/
+}
+
+/**
+*  appearance of annotations (features in AnnoTracks)  when they are drop targets and are hovered over 
+*/
+.annot-drop-hover  {
+    outline-style: solid; 
+    outline-color: green;
+    outline-width: 4px; 
+}
+
+/* 
+*  overriding JQueryUI .ui-resizable style, which changes anything getting resized to "position:absolute"
+*  for this to work this CSS _must_ come after jquery-ui.css in load order
+*/
+.ui-resizable  {
+    position: absolute;
+}
+
+/* style to highlight left feature edges that match selected feature(s) edges 
+*  setting box-sizing to border-box keeps the border _inside_ the element (rather than outside)
+*  (-moz*, -webkit*, -ms* prefixed versions needed because box-sizing property name currently differs across browsers)
+*/
+.left-edge-match  {
+    border-left: solid red 2px !important;
+    box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+}
+
+/* style to highlight right feature edges that match selected feature(s) edges 
+*  setting box-sizing to border-box keeps the border _inside_ the element (rather than outside)
+*  (-moz*, -webkit*, -ms* prefixed versions needed because box-sizing property name currently differs across browsers)
+*/
+.right-edge-match  {
+    border-right: solid red 2px !important;
+    box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+}
+
+
+/* to make sure selection style overrides any other styles in this stylesheet, make sure to put selection style at end */
+.selected-feature {
+    outline-style: solid; 
+    outline-color: red;
+    outline-width: 3px;
+}
+
+.shadow  { 
+    -moz-box-shadow: 5px 5px 5px #444;
+    -webkit-box-shadow: 5px 5px 5px #444;
+    box-shadow: 5px 5px 5px #444;
+}
+
+/* 
+*  selection of annotations is separate from selection of features 
+*/
+.selected-annotation  {
+    outline-style: solid;
+    outline-color: red;
+    outline-width: 3px;
+}
+
+/* hiding close button for AnnotTracks, so can't be removed */
+div.track_webapollo_view_track_annottrack div.track-label .track-close-button {
+    visibility: hidden;
+}
+
+/* hiding close button for SequenceTracks, so can't be removed */
+div.track_webapollo_view_track_annotsequencetrack div.track-label .track-close-button {
+    visibility: hidden;
+}
+/* maker    background-color: rgb(255,204,204); */
+/* blastn   background-color: rgb(102,204,102); */
+/* blastx     background-color: rgb(0,200,204); */
+/* tblastx    background-color: rgb(0,200,104); */
+/* snap       background-color: rgb(153,100,204); */
+/* est2genome     background-color: rgb(100,100,210); */
+/* protein2genome   background-color: rgb(117,150,255); */
+/* repeatmasker     background-color: red; */
+/* repeatrunner     background-color: rgb(255,152,255); */
+/* default-alignment-block    background-color: #C87C8E; */
+/* nvit-alignment    background-color: #848DBF; */
+
+.stop_codon_read_through, 
+.plus-stop_codon_read_through,
+.minus-stop_codon_read_through {
+    position: absolute;
+    height: 16px;
+    background-color: purple;
+    border-style: solid;
+    border-color: #01F;
+    border-width: 1px;
+    cursor: pointer;
+    z-index: 15;
+    min-width: 1px;
+    box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+}
+
+.pseudogene,
+.plus-pseudogene, 
+.minus-pseudogene {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(153,153,255);
+}
+
+.tRNA,
+.plus-tRNA, 
+.minus-tRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.snRNA,
+.plus-snRNA, 
+.minus-snRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.snoRNA,
+.plus-snoRNA, 
+.minus-snoRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.ncRNA,
+.plus-ncRNA, 
+.minus-ncRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.miRNA,
+.plus-miRNA, 
+.minus-miRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.rRNA,
+.plus-rRNA, 
+.minus-rRNA {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: rgb(0,204,0);
+}
+
+.repeat_region,
+.plus-repeat_region, 
+.minus-repeat_region {
+    height: 12px; 
+    margin-top: 2px;
+    background-color: magenta;
+}
+
+.transposable_element,
+.plus-transposable_element, 
+.minus-transposable_element {
+	height: 12px;
+	border-left-color: blue;
+	border-right-color: blue;
+	border-width: 0px 2px 0px 2px;
+	border-style: solid;
+	background-color: transparent;
+	z-index: 10;
+}
+
+.transposable_element-render {
+    height: 3px;
+    margin-top: 4px;
+    background-color: blue;
+}
+
+.blue-ibeam {
+	height: 12px;
+	border-left-color: blue;
+	border-right-color: blue;
+	border-width: 0px 2px 0px 2px;
+	border-style: solid;
+	background-color: transparent;
+	z-index: 10;
+}
+
+.blue-ibeam-render {
+	height: 3px;
+    margin-top: 4px;
+    background-color: blue;
+}
+
+.magenta-80pct,
+.plus-magenta-80pct, 
+.minus-magenta-80pct {
+	top: 10%;
+	height: 80%;
+    background-color: magenta;
+    border: 1px solid #01F;
+}
+
+.light-purple-80pct,
+.plus-light-purple-80pct, 
+.minus-light-purple-80pct {
+	top: 10%;
+	height: 80%;
+    background-color: rgb(153,153,255);
+    border: 1px solid #01F;
+}
+
+.locked-annotation  {
+    outline-style: solid;
+    outline-color: blue;
+    outline-width: 3px;
+}
+
diff --git a/client/apollo/img/ApolloLogo_100x36.png b/client/apollo/img/ApolloLogo_100x36.png
new file mode 100644
index 0000000..aa04f44
Binary files /dev/null and b/client/apollo/img/ApolloLogo_100x36.png differ
diff --git a/client/apollo/img/exclamation_circle_orange.png b/client/apollo/img/exclamation_circle_orange.png
new file mode 100644
index 0000000..070685c
Binary files /dev/null and b/client/apollo/img/exclamation_circle_orange.png differ
diff --git a/client/apollo/img/loading.gif b/client/apollo/img/loading.gif
new file mode 100644
index 0000000..f2a1bc0
Binary files /dev/null and b/client/apollo/img/loading.gif differ
diff --git a/client/apollo/img/marker_rounded_red.png b/client/apollo/img/marker_rounded_red.png
new file mode 100644
index 0000000..c8d93da
Binary files /dev/null and b/client/apollo/img/marker_rounded_red.png differ
diff --git a/client/apollo/img/marker_squared_red.png b/client/apollo/img/marker_squared_red.png
new file mode 100644
index 0000000..ccb4404
Binary files /dev/null and b/client/apollo/img/marker_squared_red.png differ
diff --git a/client/apollo/img/minus-arrowhead.png b/client/apollo/img/minus-arrowhead.png
new file mode 100644
index 0000000..86568c6
Binary files /dev/null and b/client/apollo/img/minus-arrowhead.png differ
diff --git a/client/apollo/img/plus-arrowhead.png b/client/apollo/img/plus-arrowhead.png
new file mode 100644
index 0000000..04110b9
Binary files /dev/null and b/client/apollo/img/plus-arrowhead.png differ
diff --git a/client/apollo/img/small-minus-arrowhead.png b/client/apollo/img/small-minus-arrowhead.png
new file mode 100644
index 0000000..eeb6260
Binary files /dev/null and b/client/apollo/img/small-minus-arrowhead.png differ
diff --git a/client/apollo/img/small-plus-arrowhead.png b/client/apollo/img/small-plus-arrowhead.png
new file mode 100644
index 0000000..f4daf7e
Binary files /dev/null and b/client/apollo/img/small-plus-arrowhead.png differ
diff --git a/client/apollo/img/user_icon_16px.png b/client/apollo/img/user_icon_16px.png
new file mode 100644
index 0000000..b79692c
Binary files /dev/null and b/client/apollo/img/user_icon_16px.png differ
diff --git a/client/apollo/img/warning_exclamation.png b/client/apollo/img/warning_exclamation.png
new file mode 100644
index 0000000..3dd3b55
Binary files /dev/null and b/client/apollo/img/warning_exclamation.png differ
diff --git a/client/apollo/img/warning_exclamation_small.png b/client/apollo/img/warning_exclamation_small.png
new file mode 100644
index 0000000..5665cd3
Binary files /dev/null and b/client/apollo/img/warning_exclamation_small.png differ
diff --git a/client/apollo/img/webapollo_favicon.ico b/client/apollo/img/webapollo_favicon.ico
new file mode 100644
index 0000000..96cd853
Binary files /dev/null and b/client/apollo/img/webapollo_favicon.ico differ
diff --git a/client/apollo/js/BioFeatureUtils.js b/client/apollo/js/BioFeatureUtils.js
new file mode 100644
index 0000000..1c21352
--- /dev/null
+++ b/client/apollo/js/BioFeatureUtils.js
@@ -0,0 +1,59 @@
+define( ['dojo/_base/declare'],
+        function( declare ) {
+return declare( null, {
+
+//  returns modified parent, or null if removal of child would result in parent with empty child list
+//  if want to go "only creation, no destruction" route,
+//      could clone parent and return clone instead of original...
+//  assumes already populated:
+//     child.track
+//     child.parent
+//     child.parent.track
+//
+removeChild: function(child)  {
+    // console.log("called BioFeatureUtils.removeChild");
+    var parent = child.parent();
+    var fields = parent.track.fields;
+    var subfields = parent.track.subFields;
+    var children = parent[fields["subfeatures"]];
+    console.log(children);
+    var index = children.indexOf(child);
+    console.log(index);
+    if (index < 0)  {
+        // console.log("BioFeatureUtils ERROR: child not found in parent!!");
+        return parent;
+    }
+    children.splice(index, 1);
+    //    console.log(children);
+    var clength = children.length;
+    if (children.length === 0)  {
+        // console.log("parent has no more children");
+        return null;
+    }
+    else  {
+        // console.log("rechecking parent bounds");
+        var prevmin = parent[fields["start"]];
+        var prevmax = parent[fields["end"]];
+        var sibling = children[0];
+        var newmin = sibling[subfields["start"]];
+        var newmax = sibling[subfields["end"]];
+        for (var cindex = 1; cindex<clength; cindex++)  {
+            sibling = children[cindex];
+            newmin = Math.min(newmin, sibling[subfields["start"]]);
+            newmax = Math.max(newmax, sibling[subfields["end"]]);
+        }
+        // console.log("checked all child bounds");
+        if (newmin !== prevmin)  {
+            // console.log("changing parent min: " + newmin);
+            parent[fields["start"]] = newmin;
+        }
+        if (newmax !=  prevmax)  {
+            // console.log("changing parent max: " + newmin);
+            parent[fields["end"]] = newmax;
+        }
+        // console.log("returning from BioFeatureUtils.removeChild");
+        return parent;
+    }
+}
+});
+});
diff --git a/client/apollo/js/EUtils.js b/client/apollo/js/EUtils.js
new file mode 100644
index 0000000..c0bbaba
--- /dev/null
+++ b/client/apollo/js/EUtils.js
@@ -0,0 +1,81 @@
+define( [
+            'dojo/_base/declare'
+], 
+function( declare ) {
+
+function EUtils(contextPath, errorHandler) {
+    this.url = contextPath + "/ProxyService?proxy=eutils";
+    this.errorHandler = errorHandler;
+};
+
+EUtils.prototype.validateId = function(db, id) {
+    var valid;
+    var thisB = this;
+    dojo.xhrGet( {
+        url: this.url + "&operation=search&db=" + db + "&id=" + id,
+        handleAs: "json",
+        timeout: 5000 * 1000, // Time in milliseconds
+        sync: true,
+        load: function(response, ioArgs) {
+            valid = !response.eSearchResult.ErrorList;
+        }, 
+        error: function(response, ioArgs) {
+            thisB.errorHandler(response);
+        }
+    });
+    return valid;
+};
+
+EUtils.prototype.fetch = function(db, id) {
+    var record;
+    var thisB = this;
+    dojo.xhrGet( {
+        url: this.url + "&operation=fetch&db=" + db + "&id=" + id,
+        handleAs: "json",
+        timeout: 5000 * 1000, // Time in milliseconds
+        sync: true,
+        load: function(response, ioArgs) {
+            record = response.PubmedArticleSet.PubmedArticle ? response : null;
+        }, 
+        error: function(response, ioArgs) {
+            thisB.errorHandler(response);
+        }
+    });
+    return record;
+};
+
+return EUtils;
+
+});
+    
+/*
+define( [
+            'dojo/_base/declare',
+            'dojo/io/script'
+], 
+function( declare, dojoScript ) {
+
+var eutilsUrl = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"; //?retmode=xml&";
+
+function EUtils() {
+};
+
+EUtils.validateId = function(db, id, success, error) {
+    dojoScript.get({
+        url: "http://jsonproxy.appspot.com/proxy",
+        callbackParamName: "callback",
+        content: {
+            url: eutilsUrl + "?db=" + db + "&term=" + id + "[uid]",
+        },
+        load: function(response) {
+            response.eSearchResult.ErrorList ? error("Invalid " + db + " ID: " + id) : success();
+        }
+    });
+
+};
+
+return EUtils;
+    
+});
+
+*/
diff --git a/client/apollo/js/FeatureEdgeMatchManager.js b/client/apollo/js/FeatureEdgeMatchManager.js
new file mode 100644
index 0000000..81da231
--- /dev/null
+++ b/client/apollo/js/FeatureEdgeMatchManager.js
@@ -0,0 +1,189 @@
+ define( ['dojo/_base/declare',
+          'jquery',
+          'WebApollo/View/Track/DraggableHTMLFeatures',
+          'WebApollo/View/Track/AnnotTrack'
+         ],
+        function( declare, $, DraggableFeatureTrack, AnnotTrack ) {
+
+var FeatureEdgeMatchManager = declare( null,
+                                       
+{
+
+    constructor: function() {
+        this.SHOW_EDGE_MATCHES = true,
+        this.selection_managers = [];
+        this.unmatchableTypes = {};
+
+        this.verbose_edges = false;
+        this.unedgeableTypes = { "wholeCDS" : true };
+    },
+
+    addSelectionManager: function( manager )  {
+        if ( dojo.indexOf( this.selection_managers, manager ) < 0 ) {
+            this.selection_managers.push( manager );
+            manager.addListener(this);
+            dojo.mixin( this.unmatchableTypes, manager.unselectableTypes );
+        }
+
+    }, 
+
+    setBrowser: function( browser ) {
+        browser.subscribe('/jbrowse/v1/n/tracks/redraw', dojo.hitch( this, function() {
+            this.selectionCleared();
+            for (var k=0; k < this.selection_managers.length; k++)  {
+                var selected = this.selection_managers[k].getSelection();    
+                for (var i = 0; i < selected.length; ++i) {
+                    this.selectionAdded(selected[i]);
+                }
+            }
+        }));
+    },
+
+    /**
+     *  since FeatureEdgeMatcher singleton is listening to both feature selection manager
+     *    and annot selection manager, one of the manager clearing a selection does not
+     *    mean the other selection is cleared
+     *  so in edge-matcher selectionCleared(), after removing edge-match styling, rechecking
+     *    selections and redoing styles for any remaining selections
+     */
+    selectionCleared: function(selected)  {
+        if( this.SHOW_EDGE_MATCHES )  {
+            $(".left-edge-match").removeClass("left-edge-match");
+            $(".right-edge-match").removeClass("right-edge-match");
+            for (var k=0; k < this.selection_managers.length; k++)  {
+                var selected = this.selection_managers[k].getSelection();
+                for (var i = 0; i < selected.length; ++i) {
+                    var selection_record = selected[i];
+//                    this.selectionAdded(selection_record);
+                }
+            }
+        }
+    },
+
+    selectionRemoved: function(feat)  {
+        // for now, brute force it -- remove all edge-match styling,
+        //    then re-add current selections one at a time
+        //
+        //  since selectionCleared is now redoing selections after clearing styles,
+        //      selectionRemoved() and selectionCleared() are now equivalent operations
+        if( this.SHOW_EDGE_MATCHES ) {
+            this.selectionCleared();
+        }
+    },
+
+    // feat may be a feature or subfeature?
+    // experimenting with highlighting edges of features that match selected features (or their subfeatures)
+    // still assuming index 0 for start, index 1 for end
+    // assumes all tracks have two-level features, and thus track.fields and track.subFields are populated
+    selectionAdded: function( rec )  {
+        var feat = rec.feature;
+        if ( ! this.SHOW_EDGE_MATCHES ) {
+            return;
+        }
+        var source_feat = feat;
+        var verbose_edges = this.verbose_edges;
+        if (verbose_edges)  { console.log("EdgeMatcher.selectionAdded called"); }
+
+        var source_subfeats = source_feat.get('subfeatures');
+        if (! source_subfeats || source_subfeats.length === 0) {
+            source_subfeats = [ source_feat ];
+        }
+
+        if( verbose_edges ) {
+            console.dir(source_subfeats);
+        }
+
+        var sourceid = source_feat.id();
+
+        var qmin = source_feat.get('start');
+        var qmax = source_feat.get("end");
+
+        if (verbose_edges)  { console.log("qmin = " + qmin + ", qmax = " + qmax); }
+        var unmatchableTypes = this.unmatchableTypes;
+        var unedgeableTypes = this.unedgeableTypes;
+
+        var ftracks = $("div.track").each( function(index, trackdiv)  {
+            var target_track = trackdiv.track;
+            // only DraggableHTMLFeatures and descendants should have track.edge_matchin_enabled
+            if (target_track && target_track.store && target_track.edge_matching_enabled)  {
+                if (verbose_edges)  {
+                    console.log("edge matching for: " + target_track.name);
+                }
+
+                var featureStore = target_track.store;
+
+                // only look at features that overlap source_feat min/max
+                // NCList.iterate only calls function for features that overlap qmin/qmax coords
+                var query =  { ref: target_track.refSeq.name, start: qmin, end: qmax };
+                featureStore.getFeatures(query, function(target_feat, path) {
+                    // some stores invoke the callback (with target_feat = undefined) even if no features meet query, so catching this case
+                    if (! target_feat)  { return; }  
+                    if (verbose_edges)  {  console.log("========="); console.log("checking feature: "); console.log(target_feat); }
+                    var target_subfeats = target_feat.get('subfeatures');
+                    if (! target_subfeats) {
+                        target_subfeats = [ target_feat ];
+                    }
+                    if (verbose_edges)  { console.log(target_subfeats); }
+
+                    if (source_subfeats instanceof Array &&
+                        target_subfeats instanceof Array)  {
+                        var tid = target_feat.id();
+                        if (verbose_edges)  {  console.log("found overlap"); console.log(target_feat); }
+                        if (tid)  {
+                            var tdiv = target_track.getFeatDiv(target_feat);
+                            if (verbose_edges)  { console.log(tdiv); }
+                            if (tdiv)  {  // only keep going if target feature.uid already populated
+                                // console.log(rsubdivs);
+                                for (var i=0; i < source_subfeats.length; i++)  {
+                                    var ssfeat = source_subfeats[i];
+                                    var sstype = ssfeat.get('type');
+                                    // don't do matching for source features of type registered as unmatchable
+                                    if (unmatchableTypes[sstype] || unedgeableTypes[sstype]) {
+                                        continue;
+                                    }
+
+                                    var ssmin = ssfeat.get('start');
+                                    var ssmax = ssfeat.get('end');
+                                    for (var k=0; k < target_subfeats.length; k++)  {
+                                        var tsfeat = target_subfeats[k];
+                                        var tstype = tsfeat.get('type');
+                                        // don't do matching for target features of type registered as unmatchable
+                                        if (unmatchableTypes[tstype] || unedgeableTypes[tstype]) {
+                                            continue;
+                                        }
+                                        var tsmin = tsfeat.get('start');
+                                        var tsmax = tsfeat.get('end');
+                                        if (ssmin === tsmin || ssmax === tsmax)  {
+                                            var tsid = tsfeat.id();
+                                            if (tsid)   {
+                                                var tsubdiv = target_track.getFeatDiv(tsfeat);
+                                                if (tsubdiv)  {
+                                                    var $tsubdiv = $(tsubdiv);
+                                                    if (ssmin === tsmin)  {
+                                                        $(tsubdiv).addClass("left-edge-match");
+                                                    }
+                                                    if (ssmax === tsmax)  {
+                                                        $(tsubdiv).addClass("right-edge-match");
+                                                    }
+                                                }
+                                            }
+                                        }
+
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }, function() {} );  // empty function for no-op on finishing
+            }
+        } );
+    }
+});
+
+// NOTE: this is a singleton
+return new FeatureEdgeMatchManager();
+});
+
+
+
+
diff --git a/client/apollo/js/FeatureSelectionManager.js b/client/apollo/js/FeatureSelectionManager.js
new file mode 100644
index 0000000..807825e
--- /dev/null
+++ b/client/apollo/js/FeatureSelectionManager.js
@@ -0,0 +1,184 @@
+define( [ 'dojo/_base/declare' ],
+        function( declare ) {
+
+return declare( null,
+{
+    /**
+     *  Selection manager for bio-features
+     *     handles deselection of any ancestors/descendants of selected features
+     *        (which is desired behavior for feature selection)
+     *     assumes features have had their parent property set before calling selection manager methods
+     *  Sends selectionAdded() and selectionRemoved() function calls to listeners
+     */
+    constructor: function()  {
+        this.selected = [];
+        this.listeners = [];
+        this.clearOnAdd = [];
+        this.unselectableTypes = { "non_canonical_five_prime_splice_site" : true, 
+                                   "non_canonical_three_prime_splice_site" : true,
+                                   "stop_codon_read_through" : true};
+    },
+
+    /**
+     *  sets an array of other FeatureSelectionManagers to call clearSelection() on 
+     *     whenever addToSelection() is called on _this_ FeatureSelectionManager
+     *     effectively ensures that selection is mutually exclusive between this manager 
+     *        and the set of other managers passed in as setClearOnAdd args
+     */
+    //FeatureSelectionManager.prototype.setClearOnAdd = function(other_smanagers)  {
+    //    this.clearOnAdd = other_smanagers;
+    //};
+    addMutualExclusion: function(other_smanager)  {
+        this.clearOnAdd.push(other_smanager);
+    },
+
+    // adding a parent should remove all children
+    // adding a child should remove all parents
+    // attempting to add a feature that's already part of the selection does nothing (and doesn't trigger listener calls)
+    addToSelection: function( rec, keepOtherTracksSelection )  {
+
+        // if this selection manager has had setClearOnAdd(others)
+        // called to set other selection managers to clear selection
+        // from when
+        if (this.clearOnAdd && !keepOtherTracksSelection)  {
+            for (var i=0; i<this.clearOnAdd.length; i++)  {
+                this.clearOnAdd[i].clearSelection();
+            }
+        }
+        //    console.log("called FeatureselectionManager.addToSelection()");
+        // do nothing if feat is already in selection
+        if ( this.isSelected( rec ) )  {
+            console.log("called FeatureSelectionManager.addToSelection(), but feature already in selection");
+            return;
+        }
+        // remove any children of the selected feature (originating from same track)
+        var selarray = this.selected;
+        var slength = selarray.length;
+        for ( var sindex = 0; sindex < slength; sindex++ )  {
+            var srec = selarray[sindex];
+            if ( srec.feature.parent() == rec.feature && srec.track == rec.track )  {
+                this._removeSelectionAt(sindex);
+                slength--;
+            }
+        }
+
+        // remove any parents of the selected feature (originating from same track)
+        var parent = rec.feature.parent();
+        if( parent )  {
+            this.removeFromSelection( { feature: parent, track: rec.track } );
+        }
+        selarray.push( rec );
+        var lislength = this.listeners.length;
+        for (var lindex = 0; lindex < lislength; lindex++)  {
+            var listener = this.listeners[lindex];
+            listener.selectionAdded( rec, this );
+        }
+        //    console.log("done calling FeatureselectionManager.addToSelection()");
+    },
+
+    /**
+     *  attempting to remove a feature that isn't selected does nothing (and doesn't trigger listener calls)
+     */
+    removeFromSelection: function( rec )  {
+        var index = this._indexOf( rec );
+        if (index >= 0)  {
+            this._removeSelectionAt(index);
+        }
+    },
+
+    _removeSelectionAt: function( index )  {
+        var rec = this.selected[index];
+        this.selected.splice(index, 1);
+        var lislength = this.listeners.length;
+        for (var lindex = 0; lindex<lislength; lindex++)  {
+            var listener = this.listeners[lindex];
+            listener.selectionRemoved(rec, this);
+        }
+    },
+
+    _indexOf: function( rec ) {
+        var index = -1;
+        for( var i = 0; i < this.selected.length; i++ ) {
+            if( this.selected[i].feature.id() == rec.feature.id() && this.selected[i].track == rec.track )
+                index = i;
+        }
+        return index;
+    },
+
+    /**
+     *  clearing an empty selection does nothing (and doesn't trigger listener calls)
+     *
+     *  intended for optimizing when selection is cleared, rather than 
+     *     multiple calls to removeSelectionAt (and subsequent multiple calls to listeners.selectionRemoved();
+     */
+    clearSelection: function()  {
+        //    console.log("called FeatureselectionManager.clearSelection()");
+        var previous_selected = this.selected;
+        this.selected = [];
+        var lislength = this.listeners.length;
+        for (var lindex=0; lindex<lislength; lindex++)  {
+            var listener = this.listeners[lindex];
+            listener.selectionCleared(previous_selected, this);
+        }
+        /*
+          for (var sindex in previous_selected)  {
+          var feat = previous_selected[sindex];
+          for (var lindex in this.listeners)  {
+          var listener = this.listeners[lindex];
+          listener.selectionRemoved(feat);
+          }
+          }
+        */
+        //  console.log("done calling FeatureselectionManager.clearSelection()");
+    },
+    
+    clearAllSelection: function() {
+            this.clearSelection();
+            for (var i = 0; i < this.clearOnAdd.length; ++i) {
+                    this.clearOnAdd[i].clearSelection();
+            }
+    },
+
+    isSelected: function( rec )  {
+        return this._indexOf( rec ) >= 0;
+    },
+
+    /**
+     * returns array of currently selected feature records { feature: feature, track: track }
+     * this is a (shallow) copy of the selected array, therefore a snapshot of what is selected 
+     *     as of when getSelection is called
+     *  so if selection changes, previous value returned from getSelection will not change
+     */
+    getSelection: function()  {
+        //    return this.selected;
+        //    return this.selected.slice(0);  // return shallow copy of array
+        return this.selected.slice(0, this.selected.length);  // return shallow copy of array
+    },
+
+    /**
+     *  since getSelection now returns feature records { feature: feature, track: track }, 
+     *  also want a method that returns only the feautures (not wrapped in records)
+     */
+    getSelectedFeatures: function()  {
+        var selfeats = new Array(this.selected.length);
+        for (var i=0; i<this.selected.length; i++)  {
+            selfeats[i] = this.selected[i].feature;
+        }
+        return selfeats;
+    }, 
+
+    addListener: function( listener )  {
+        var index = dojo.indexOf(this.listeners, listener);
+        if( index < 0 )  {  // only add if not already in listener list
+            this.listeners.push(listener);
+        }
+    },
+
+    removeListener: function( listener )  {
+        var index = dojo.indexOf(this.listeners, listener);
+        if( index >= 0 )  {  // only remove if already in listener list
+            this.listeners.splice(index, 1);
+        }
+    }
+});
+});
\ No newline at end of file
diff --git a/client/apollo/js/FormatUtils.js b/client/apollo/js/FormatUtils.js
new file mode 100644
index 0000000..afffd78
--- /dev/null
+++ b/client/apollo/js/FormatUtils.js
@@ -0,0 +1,25 @@
+define( [ ],
+function() {
+
+    var FormatUtils = {};
+    
+    FormatUtils.formatDate = function(millis) {
+        var date = new Date(millis);
+        var year = date.getFullYear();
+        var month = date.getMonth() + 1;
+        var day = ("" + date.getDate()).length == 1 ? "0" + date.getDate() : date.getDate();
+        if (String(month).length == 1) {
+            month = "0" + month;
+        }
+        return year + "-" + month + "-" + day;
+    };
+    
+    FormatUtils.formatTime = function(millis) {
+        var date = new Date(millis);
+        var hours = date.getHours();
+        var minutes = ("" + date.getMinutes()).length == 1 ? "0" + date.getMinutes() : date.getMinutes();
+        return + hours + ":" + minutes;
+    };
+    
+    return FormatUtils;
+});
diff --git a/client/apollo/js/InformationEditor.js b/client/apollo/js/InformationEditor.js
new file mode 100644
index 0000000..1ac2201
--- /dev/null
+++ b/client/apollo/js/InformationEditor.js
@@ -0,0 +1,72 @@
+define(
+    [   
+        'dojo/_base/declare',
+        'dojo/_base/array',
+        'dojo/_base/lang',
+        'dijit/TitlePane',
+        'dijit/layout/ContentPane',
+        'JBrowse/Util',
+        'dojo/dom-construct',
+        'JBrowse/View/TrackList/_TextFilterMixin'
+    ],  
+    function (
+        declare,
+        array,
+        lang,
+        TitlePane,
+        ContentPane,
+        Util,
+        dom,
+        _TextFilterMixin
+    ) { 
+
+var dojof = Util.dojof;
+return declare(
+        'WebApollo.View.InformationEditor',
+        [ ContentPane, _TextFilterMixin ],
+{
+    region: 'left',
+    splitter: true,
+    style: 'width: 25%',
+
+    id: 'informationEditor',
+    baseClass: 'webapolloInformationEditor',
+
+    /** 
+      * Track selector with facets and text searching.
+      * @constructs
+      */  
+    constructor: function() {
+        console.log('Testing InformationEditor');
+    },
+
+    postCreate: function() {
+        console.log('Testing InformationEditor postCreate');
+        this.placeAt( this.browser.container );
+    },
+    buildRendering: function() {
+        this.inherited(arguments);
+       
+        var topPane = new ContentPane({ className: 'header' }); 
+        this.addChild( topPane );
+        dom.create(
+            'h2',
+            { className: 'title',
+              innerHTML: 'Annotation Information Editor'
+            },
+            topPane.containerNode );
+
+        this._makeTextFilterNodes(
+            dom.create('div',
+                { className: 'textfilterContainer' },
+            topPane.containerNode )
+        );  
+        this._updateTextFilterControl();
+
+        this.informationList =
+        { pane: new ContentPane({ id: 'informationEditorList', className: 'information_list' }).placeAt( this.containerNode )
+        };
+    }
+});
+
+});
diff --git a/client/apollo/js/JSONUtils.js b/client/apollo/js/JSONUtils.js
new file mode 100644
index 0000000..af26a06
--- /dev/null
+++ b/client/apollo/js/JSONUtils.js
@@ -0,0 +1,422 @@
+define([ 'dojo/_base/declare',
+         'dojo/_base/array',
+         'JBrowse/Util',
+         'JBrowse/Model/SimpleFeature', 
+         'WebApollo/SequenceOntologyUtils'
+       ],
+       function( declare, array, Util, SimpleFeature, SeqOnto ) {
+
+function JSONUtils() {
+}
+
+JSONUtils.verbose_conversion = false;
+
+/**
+*  creates a feature in JBrowse JSON format
+*  takes as arguments:
+*      afeature: feature in ApolloEditorService JSON format,
+*      arep: ArrayRepr for kind of JBrowse feature to output
+*      OLD: fields: array specifying order of fields for JBrowse feature
+*      OLD: subfields:  array specifying order of fields for subfeatures of JBrowse feature
+*   "CDS" type feature in Apollo JSON format is from genomic start of translation to genomic end of translation,
+*          (+ stop codon), regardless of intons, so one per transcript (usually)
+*   "CDS" type feature in JBrowse JSON format is a CDS _segment_, which are piecewise and broken up by introns
+*          therefore commonyly have multiple CDS segments
+*
+*/
+// JSONUtils.createJBrowseFeature = function(afeature, fields, subfields)  {
+var JAFeature = declare( SimpleFeature, {
+    "-chains-": {
+        constructor: "manual"
+    },
+    constructor: function( afeature, parent ) {
+        this.afeature = afeature;
+        if (parent)  { this._parent = parent; }
+        
+        // get the main data
+        var loc = afeature.location;
+        var pfeat = this;
+        this.data = {
+            start: loc.fmin,
+            end: loc.fmax,
+            strand: loc.strand,
+            name: afeature.name,
+            parent_id: afeature.parent_id,
+            parent_type: afeature.parent_type ? afeature.parent_type.name : undefined,
+            type: afeature.type.name, 
+            properties: afeature.properties
+        };
+
+        if (this.data.type === "CDS")  { 
+            this.data.type = "wholeCDS"; 
+        }
+        else if (this.data.type === "stop_codon_read_through") {
+            parent.data.readThroughStopCodon = true;
+        }
+    
+        this._uniqueID = afeature.uniquename;
+
+        // this doesn't work, since can be multiple properties with same CV term (comments, for example)
+        //   could create arrray for each flattened cv-name for multiple values, but not sure what the point would be over 
+        //   just making sure can access via get('properties') via above assignment into data object
+        // parse the props
+/*      var props = afeature.properties;
+        dojo.forEach( props, function( p ) {
+            var pn = p.type.cv.name+':'+p.type.name;
+            this.data[pn] = p.value;
+        }, this);
+*/
+
+        if (afeature.properties) {
+            for (var i = 0; i < afeature.properties.length; ++i) {
+                var property = afeature.properties[i];
+                if (property.type.name == "comment" && property.value == "Manually set translation start") {
+                    // jfeature.manuallySetTranslationStart = true;
+                    this.data.manuallySetTranslationStart = true;   // so can call feat.get('manuallySetTranslationStart')
+                    if (this.parent())  { parent.data.manuallySetTranslationStart = true; }
+                }
+                else if (property.type.name == "comment" && property.value == "Manually set translation end") {
+                    this.data.manuallySetTranslationEnd = true;   // so can call feat.get('manuallySetTranslationEnd')
+                    if (this.parent())  { parent.data.manuallySetTranslationEnd = true; }
+                }
+                else if (property.type.name == "owner") {
+                    this.data.owner = property.value;
+                }
+                else if (property.type.name == "feature_property") {
+                    if (property.value == "locked=true") {
+                        this.data.locked = true;
+                    }
+                }
+            }
+        }
+        
+        if (!parent) {
+            if (afeature.children) {
+                var descendants = [];
+                for (var i = 0; i < afeature.children.length; ++i) {
+                    var child = afeature.children[i];
+                    if (child.children) {
+                        for (var j = 0; j < child.children.length; ++j) {
+                            JSONUtils.flattenFeature(child.children[j], descendants);
+                        }
+                    }
+                }
+                afeature.children = afeature.children.concat(descendants);
+            }
+            else {
+                var child = dojo.clone(afeature);
+                child.uniquename += "-clone";
+                this.set("cloned_subfeatures", true);
+                afeature.children = [ child ];
+            }
+        }
+        
+        // moved subfeature assignment to bottom of feature construction, since subfeatures may need to call method on their parent
+        //     only thing subfeature constructor won't have access to is parent.data.subfeatures
+        // get the subfeatures              
+        this.data.subfeatures = array.map( afeature.children, function(s) {
+            return new JAFeature( s, pfeat);
+        } );
+
+    },
+    
+    getUniqueName: function() {
+        if (this.parent() && this.parent().get("cloned_subfeatures")) {
+            return this.parent().id();
+        }
+        return this.id();
+    }
+});
+
+JSONUtils.JAFeature = JAFeature;
+
+JSONUtils.createJBrowseFeature = function( afeature )  {
+    return new JAFeature( afeature );
+};
+
+JSONUtils.flattenFeature = function(feature, descendants) {
+    descendants.push(feature);
+    if (feature.children) {
+        for (var i = 0; i < feature.children.length; ++i) {
+            JSONUtils.flattenFeature(feature.children[i], descendants);
+        }
+        feature.children = [];
+    }
+};
+
+
+/**
+ *  takes any JBrowse feature, returns a SimpleFeature "copy", 
+ *        for which all properties returned by tags() are mutable (has set() method)
+ *  needed since JBrowse features no longer necessarily mutable
+ *    feature requirements:
+ *         functions: id, parent, tags, get
+ *         if subfeatures, then returned as array by feature.get('subfeatures')
+ *      
+ */
+JSONUtils.makeSimpleFeature = function(feature, parent)  {
+    var result = new SimpleFeature({id: feature.id(), parent: (parent ? parent : feature.parent()) });
+    var ftags = feature.tags();
+    for (var tindex = 0; tindex < ftags.length; tindex++)  {  
+        var tag = ftags[tindex];
+        // forcing lower case, since still having case issues with NCList features
+        result.set(tag.toLowerCase(), feature.get(tag.toLowerCase()));
+    }
+    var subfeats = feature.get('subfeatures');
+    if (subfeats && (subfeats.length > 0))  {
+        var simple_subfeats = [];
+        for (var sindex = 0; sindex < subfeats.length; sindex++)  {
+            var simple_subfeat = JSONUtils.makeSimpleFeature(subfeats[sindex], result);
+            simple_subfeats.push(simple_subfeat);
+        }
+        result.set('subfeatures', simple_subfeats);
+    }
+    return result;
+};
+
+/**
+*  creates a sequence alteration in JBrowse JSON format
+*  takes as arguments:
+*      arep: ArrayRepr for kind of JBrowse feature to output
+*      afeature: sequence alteration in ApolloEditorService JSON format,
+*/
+JSONUtils.createJBrowseSequenceAlteration = function( afeature )  {
+    var loc = afeature.location; 
+    var uid = afeature.uniquename;
+    var justification;
+    for (var i = 0; i < afeature.properties.length; i++) {
+        if (afeature.properties[i].type.name === "justification") {
+            justification = afeature.properties[i].value;
+        }
+    }
+
+    return new SimpleFeature({
+        data: {
+            start:    loc.fmin,
+            end:      loc.fmax,
+            strand:   loc.strand,
+            id:       uid,
+            type:     afeature.type.name,
+            residues: afeature.residues,
+            seq:      afeature.residues,
+            justification: justification
+        },
+        id: uid
+    });
+};
+
+
+/** 
+*  creates a feature in ApolloEditorService JSON format
+*  takes as argument:
+*       jfeature: a feature in JBrowse JSON format, 
+*       fields: array specifying order of fields in jfeature
+*       subfields: array specifying order of fields in subfeatures of jfeature
+*       specified_type (optional): type passed in that overrides type info for jfeature
+*  ApolloEditorService format:
+*    { 
+*       "location" : { "fmin": fmin, "fmax": fmax, "strand": strand }, 
+*       "type": { "cv": { "name":, cv },   // typical cv name: "SO" (Sequence Ontology)
+*                 "name": cvterm },        // typical name: "transcript"
+*       "children": { __recursive ApolloEditorService feature__ }
+*    }
+* 
+*   For ApolloEditorService "add_feature" call to work, need to have "gene" as toplevel feature, 
+*         then "transcript", then ???
+*                 
+*    JBrowse JSON fields example: ["start", "end", "strand", "id", "subfeatures"]
+*
+*    type handling
+*    if specified_type arg present, it determines type name
+*    else if fields has a "type" field, use that to determine type name
+*    else don't include type 
+*
+*    ignoring JBrowse ID / name fields for now
+*    currently, for features with lazy-loaded children, ignores children 
+*/
+JSONUtils.createApolloFeature = function( jfeature, specified_type, useName, specified_subtype )   {
+    var diagnose =  (JSONUtils.verbose_conversion && jfeature.children() && jfeature.children().length > 0);
+    if (diagnose)  { 
+        console.log("converting JBrowse feature to Apollo feture, specified type: " + specified_type); 
+        console.log(jfeature);
+    }
+
+    var afeature = new Object();
+    var astrand;
+    // Apollo feature strand must be an integer
+    //     either 1 (plus strand), -1 (minus strand), or 0? (not stranded or strand is unknown?)
+    switch (jfeature.get('strand')) {  // strand
+    case 1:
+    case '+':
+        astrand = 1; break;
+    case -1:
+    case '-':
+        astrand = -1; break;
+    default:
+        astrand = 0; // either not stranded or strand is uknown
+    }
+    
+    afeature.location = {
+        "fmin": jfeature.get('start'),
+        "fmax": jfeature.get('end'),
+        "strand": astrand
+    };
+
+    var typename;
+    if (specified_type)  {
+        typename = specified_type;
+    }
+    else if ( jfeature.get('type') ) {
+        typename = jfeature.get('type');
+    }
+
+    if (typename)  {
+        afeature.type = {
+            "cv": {
+            "name": "sequence"
+        }
+    };
+    afeature.type.name = typename;
+    }
+
+    // if (useName && name) {
+    //     afeature.name = name;
+    // }
+
+    var id = jfeature.get('id');
+    var name = jfeature.get('name');
+    if (useName) {
+        // using 'id' attribute in the absence of 'name' attribute
+        name !== undefined ? afeature.name = name : afeature.name = id;
+    }
+    
+    /*
+    afeature.properties = [];
+    var property = { value : "source_id=" + jfeature.get('id'),
+            type : {
+                    cv: {
+                        name: "feature_property"
+                    },
+                    name: "feature_property"
+            }
+    };
+    afeature.properties.push(property);
+    */
+
+    if (diagnose) { console.log("converting to Apollo feature: " + typename); }
+    var subfeats;
+    // use filteredsubs if present instead of subfeats?
+    //    if (jfeature.filteredsubs)  { subfeats = jfeature.filteredsubs; }
+    //    else  { subfeats = jfeature.get('subfeatures'); }
+    subfeats = jfeature.get('subfeatures'); 
+    if( subfeats && subfeats.length )  {
+        afeature.children = [];
+        var slength = subfeats.length;
+        var cds;
+        var cdsFeatures = [];
+        var foundExons = false;
+        
+        var updateCds = function(subfeat) {
+            if (!cds) {
+                cds = new SimpleFeature({id: "cds", parent: jfeature});
+                cds.set('start', subfeat.get('start'));
+                cds.set('end', subfeat.get('end'));
+                cds.set('strand', subfeat.get('strand'));
+                cds.set('type', 'CDS');
+            }
+            else {
+                if (subfeat.get("start") < cds.get("start")) {
+                    cds.set("start", subfeat.get("start"));
+                }
+                if (subfeat.get("end") > cds.get("end")) {
+                    cds.set("end", subfeat.get("end"));
+                }
+            }
+        };
+        
+        for (var i=0; i<slength; i++)  {
+            var subfeat = subfeats[i];
+            var subtype = subfeat.get('type');
+                var converted_subtype = specified_subtype || subtype;
+                if (!specified_subtype) {
+                    if (SeqOnto.exonTerms[subtype])  {
+                        // definitely an exon, leave exact subtype as is 
+                        // converted_subtype = "exon"
+                    }
+                    else if (subtype === "wholeCDS" || subtype === "polypeptide") {
+                        // normalize to "CDS" sequnce ontology term
+                        // converted_subtype = "CDS";
+                        updateCds(subfeat);
+                        converted_subtype = null;
+                    }
+                    else if (SeqOnto.cdsTerms[subtype])  {
+                        // other sequence ontology CDS terms, leave unchanged
+                        updateCds(subfeat);
+                        converted_subtype = null;
+                        cdsFeatures.push(subfeat);
+                    }
+                    else if (SeqOnto.spliceTerms[subtype])  {  
+                        // splice sites -- filter out?  leave unchanged?
+                        // 12/16/2012 filtering out for now, causes errors in AnnotTrack duplication operation
+                        converted_subtype = null;  // filter out
+                    }
+                    else if (SeqOnto.startCodonTerms[subtype] || SeqOnto.stopCodonTerms[subtype])  {
+                        // start and stop codons -- filter out?  leave unchanged?
+                        // 12/16/2012 filtering out for now, causes errors in AnnotTrack createAnnotation operation
+                        converted_subtype = null;  // filter out
+                    }
+                    else if (SeqOnto.intronTerms[subtype])  {
+                        // introns -- filter out?  leave unchanged?
+                        converted_subtype = null;  // filter out
+                    }
+                    else if (SeqOnto.utrTerms[subtype]) {
+                        // filter out UTR
+                        converted_subtype = null;
+                    }
+                    else  { 
+                        // convert everything else to exon???
+                        // need to do this since server only creates exons for "exon" and descendant terms
+                        converted_subtype = "exon";
+                    }
+                }
+                if (SeqOnto.exonTerms[subtype]) {
+                    foundExons = true;
+                }
+                if (converted_subtype)  {
+                afeature.children.push( JSONUtils.createApolloFeature( subfeat, converted_subtype ) );
+                    if (diagnose)  { console.log("    subfeat original type: " + subtype + ", converted type: " + converted_subtype); }
+                }
+                else {
+                    if (diagnose)  { console.log("    edited out subfeature, type: " + subtype); }
+                }
+        }
+        if (cds) {
+            afeature.children.push( JSONUtils.createApolloFeature( cds, "CDS"));
+            if (!foundExons) {
+                for (var i = 0; i < cdsFeatures.length; ++i) {
+                    afeature.children.push(JSONUtils.createApolloFeature(cdsFeatures[i], "exon"));
+                }
+            }
+        }
+    }
+    else if ( specified_type === 'transcript' )  {
+        // special casing for Apollo "transcript" features being created from 
+        //    JBrowse top-level features that have no children
+        // need to create an artificial exon child the same size as the transcript
+        var fake_exon = new SimpleFeature({id: jfeature.id()+"_dummy_exon", parent: jfeature});
+        fake_exon.set('start', jfeature.get('start'));
+        fake_exon.set('end', jfeature.get('end'));
+        fake_exon.set('strand', jfeature.get('strand'));
+        fake_exon.set('type', 'exon');
+        afeature.children = [ JSONUtils.createApolloFeature( fake_exon ) ];
+    }
+    if (diagnose)  { console.log("result:"); console.log(afeature); }
+    return afeature;
+};
+
+// experimenting with forcing export of JSONUtils into global namespace...
+window.JSONUtils = JSONUtils;
+
+return JSONUtils;
+ 
+});
\ No newline at end of file
diff --git a/client/apollo/js/Permission.js b/client/apollo/js/Permission.js
new file mode 100644
index 0000000..dfbc694
--- /dev/null
+++ b/client/apollo/js/Permission.js
@@ -0,0 +1,11 @@
+define([],
+       function() {
+
+return {
+    NONE: 0x0,
+    READ: 0x1,
+    WRITE: 0x2,
+    ADMIN: 0x8
+};
+
+});
\ No newline at end of file
diff --git a/client/apollo/js/SequenceFeatureDialog.js b/client/apollo/js/SequenceFeatureDialog.js
new file mode 100644
index 0000000..3b276bd
--- /dev/null
+++ b/client/apollo/js/SequenceFeatureDialog.js
@@ -0,0 +1,170 @@
+define( [
+            'dojo/_base/declare',
+            'dijit/Menu',
+            'dijit/MenuItem', 
+            'dijit/MenuSeparator', 
+            'dijit/PopupMenuItem',
+            'dijit/form/Button',
+            'dijit/form/DropDownButton',
+            'dijit/DropDownMenu',
+            'dijit/form/ComboBox',
+            'dijit/form/TextBox',
+            'dijit/form/ValidationTextBox',
+            'dijit/form/RadioButton',
+            'dojox/widget/DialogSimple',
+            'dojox/grid/DataGrid',
+            'dojox/grid/cells/dijit',
+            'dojo/data/ItemFileWriteStore',
+            'WebApollo/JSONUtils',
+            'WebApollo/BioFeatureUtils',
+            'WebApollo/Permission', 
+            'WebApollo/SequenceSearch', 
+            'WebApollo/EUtils',
+            'WebApollo/SequenceOntologyUtils',
+            'WebApollo/FormatUtils'
+        ],
+function(declare, dijitMenu, dijitMenuItem, dijitMenuSeparator , dijitPopupMenuItem, dijitButton,
+        dijitDropDownButton, dijitDropDownMenu, dijitComboBox, dijitTextBox, dijitValidationTextBox, 
+        dijitRadioButton, dojoxDialogSimple, dojoxDataGrid, dojoxCells, dojoItemFileWriteStore,
+        JSONUtils, BioFeatureUtils, Permission, SequenceSearch, EUtils, SequenceOntologyUtils, FormatUtils) {
+
+var context_path = "..";
+
+return declare(null,
+{
+    constructor: function() {
+    
+    },
+
+    getSequenceForSelectedFeatures: function(records) {
+        var track = this;
+
+        var content = dojo.create("div", { className: "get_sequence" });
+        var textArea = dojo.create("textarea", { className: "sequence_area", readonly: true }, content);
+        var form = dojo.create("form", { }, content);
+        var peptideButtonDiv = dojo.create("div", { className: "first_button_div" }, form);
+        var peptideButton = dojo.create("input", { type: "radio", name: "type", checked: true }, peptideButtonDiv);
+        var peptideButtonLabel = dojo.create("label", { innerHTML: "Peptide sequence", className: "button_label" }, peptideButtonDiv);
+        var cdnaButtonDiv = dojo.create("div", { className: "button_div" }, form);
+        var cdnaButton = dojo.create("input", { type: "radio", name: "type" }, cdnaButtonDiv);
+        var cdnaButtonLabel = dojo.create("label", { innerHTML: "cDNA sequence", className: "button_label" }, cdnaButtonDiv);
+        var cdsButtonDiv = dojo.create("div", { className: "button_div" }, form);
+        var cdsButton = dojo.create("input", { type: "radio", name: "type" }, cdsButtonDiv);
+        var cdsButtonLabel = dojo.create("label", { innerHTML: "CDS sequence", className: "button_label" }, cdsButtonDiv);
+        var genomicButtonDiv = dojo.create("div", { className: "button_div" }, form);
+        var genomicButton = dojo.create("input", { type: "radio", name: "type" }, genomicButtonDiv);
+        var genomicButtonLabel = dojo.create("label", { innerHTML: "Genomic sequence", className: "button_label" }, genomicButtonDiv);
+        var genomicWithFlankButtonDiv = dojo.create("div", { className: "button_div" }, form);
+        var genomicWithFlankButton = dojo.create("input", { type: "radio", name: "type" }, genomicWithFlankButtonDiv);
+        var genomicWithFlankButtonLabel = dojo.create("label", { innerHTML: "Genomic sequence +/-", className: "button_label" }, genomicWithFlankButtonDiv);
+        var genomicWithFlankField = dojo.create("input", { type: "text", size: 5, className: "button_field", value: "500" }, genomicWithFlankButtonDiv);
+        var genomicWithFlankFieldLabel = dojo.create("label", { innerHTML: "bases", className: "button_label" }, genomicWithFlankButtonDiv);
+
+        var fetchSequence = function(type) {
+            var features = '"features": [';
+            for (var i = 0; i < records.length; ++i)  {
+                var record = records[i];
+                var annot = record.feature;
+                var seltrack = record.track;
+                var uniqueName = annot.getUniqueName();
+                // just checking to ensure that all features in selection are
+                // from this track
+                if (seltrack === track)  {
+                    var trackdiv = track.div;
+                    var trackName = track.getUniqueTrackName();
+
+                    if (i > 0) {
+                        features += ',';
+                    }
+                    features += ' { "uniquename": "' + uniqueName + '" } ';
+                }
+            }
+            features += ']';
+            var operation = "get_sequence";
+            var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"';
+                var flank = 0;
+                if (type == "genomic_with_flank") {
+                        flank = dojo.attr(genomicWithFlankField, "value");
+                        postData += ', "flank": ' + flank;
+                        type = "genomic";
+                }
+                postData += ', "type": "' + type + '" }';
+                dojo.xhrPost( {
+                    postData: postData,
+                    url: context_path + "/AnnotationEditorService",
+                    handleAs: "json",
+                    timeout: 5000 * 1000, // Time in milliseconds
+                    load: function(response, ioArgs) {
+                        var textAreaContent = "";
+                        for (var i = 0; i < response.features.length; ++i) {
+                                var feature = response.features[i];
+                                var cvterm = feature.type;
+                                var residues = feature.residues;
+                                var loc = feature.location;
+                                textAreaContent += ">" + feature.uniquename + " (" + cvterm.cv.name + ":" + cvterm.name + ") " + residues.length + " residues [" + track.refSeq.name + ":" + (loc.fmin + 1) + "-" + loc.fmax + " " + (loc.strand == -1 ? "-" : loc.strand == 1 ? "+" : "no") + " strand] ["+ type + (flank > 0 ? " +/- " + flank + " bases" : "") + "]\n";
+                                var lineLength = 70;
+                                for (var j = 0; j < residues.length; j += lineLength) {
+                                        textAreaContent += residues.substr(j, lineLength) + "\n";
+                                }
+                        }
+                        dojo.attr(textArea, "innerHTML", textAreaContent);
+                    },
+                    // The ERROR function will be called in an error case.
+                    error: function(response, ioArgs) {
+                                track.handleError(response);
+                        console.log("Annotation server error--maybe you forgot to login to the server?");
+                        console.error("HTTP status code: ", ioArgs.xhr.status);
+                        //
+                        // dojo.byId("replace").innerHTML = 'Loading the
+                        // resource from the server did not work';
+                        return response;
+                    }
+
+                });
+        };
+        var callback = function(event) {
+            var type;
+            var target = event.target || event.srcElement;
+            if (target == peptideButton || target == peptideButtonLabel) {
+                    dojo.attr(peptideButton, "checked", true);
+                    type = "peptide";
+            }
+            else if (target == cdnaButton || target == cdnaButtonLabel) {
+                    dojo.attr(cdnaButton, "checked", true);
+                    type = "cdna";
+            }
+            else if (target == cdsButton || target == cdsButtonLabel) {
+                    dojo.attr(cdsButton, "checked", true);
+                    type = "cds";
+            }
+            else if (target == genomicButton || target == genomicButtonLabel) {
+                    dojo.attr(genomicButton, "checked", true);
+                    type = "genomic";
+            }
+            else if (target == genomicWithFlankButton || target == genomicWithFlankButtonLabel) {
+                    dojo.attr(genomicWithFlankButton, "checked", true);
+                    type = "genomic_with_flank";
+            }
+            fetchSequence(type);
+        };
+
+        dojo.connect(peptideButton, "onchange", null, callback);
+        dojo.connect(peptideButtonLabel, "onclick", null, callback);
+        dojo.connect(cdnaButton, "onchange", null, callback);
+        dojo.connect(cdnaButtonLabel, "onclick", null, callback);
+        dojo.connect(cdsButton, "onchange", null, callback);
+        dojo.connect(cdsButtonLabel, "onclick", null, callback);
+        dojo.connect(genomicButton, "onchange", null, callback);
+        dojo.connect(genomicButtonLabel, "onclick", null, callback);
+        dojo.connect(genomicWithFlankButton, "onchange", null, callback);
+        dojo.connect(genomicWithFlankButtonLabel, "onclick", null, callback);
+
+        fetchSequence("peptide");
+        this.openDialog("Sequence", content);
+    }
+});
+
+});
+
+
diff --git a/client/apollo/js/SequenceOntologyUtils.js b/client/apollo/js/SequenceOntologyUtils.js
new file mode 100644
index 0000000..9bc1b1f
--- /dev/null
+++ b/client/apollo/js/SequenceOntologyUtils.js
@@ -0,0 +1,415 @@
+define( [],
+        function() {
+
+var SequenceOntologyUtils = {};
+            
+/**
+ *  Sequence Ontology feature types that are known to not have CDS children
+ *  NOT a complete list (complete list would be extensive)
+ */
+SequenceOntologyUtils.neverHasCDS = {
+    match: true, 
+    nucleotide_match: true, 
+    expressed_sequence_match: true, 
+    cDNA_match: true, 
+    EST_match: true, 
+    RST_match: true, 
+    UST_match: true, 
+    primer_match: true, 
+    tranlated_nucleotide_match: true, 
+    protein_match: true, 
+    protein_hmm_match: true, 
+    alignment: true, 
+    repeat: true,
+    repeat_region: true,
+    transposable_element: true
+};
+            
+SequenceOntologyUtils.neverHasExons = {
+    match: true, 
+    nucleotide_match: true, 
+    expressed_sequence_match: true, 
+    cDNA_match: true, 
+    EST_match: true, 
+    RST_match: true, 
+    UST_match: true, 
+    primer_match: true, 
+    tranlated_nucleotide_match: true, 
+    protein_match: true, 
+    protein_hmm_match: true, 
+    alignment: true, 
+    repeat: true,
+    repeat_region: true,
+    transposable_element: true
+};
+
+
+/** 
+ *  flattened Sequence Ontology for Gene
+ *     Gene and is-a descendants
+ */
+SequenceOntologyUtils.geneTerms = {
+    gene: true, 
+    gene_cassette: true, 
+    gene_rearranged_at_DNA_level: true, 
+    gene_silenced_by_DNA_methylation: true, 
+    gene_silenced_by_DNA_modification: true, 
+    gene_silenced_by_RNA_interference: true, 
+    gene_silenced_by_histone_deacetylation: true, 
+    gene_silenced_by_histone_methylation: true, 
+    gene_silenced_by_histone_modification: true, 
+    gene_with_dicistronic_mRNA: true, 
+    gene_with_dicistronic_primary_transcript: true, 
+    gene_with_dicistronic_transcript: true, 
+    gene_with_edited_transcript: true, 
+    gene_with_mRNA_recoded_by_translational_bypass: true, 
+    gene_with_mRNA_with_frameshift: true, 
+    gene_with_non_canonical_start_codon: true, 
+    gene_with_polyadenylated_mRNA: true, 
+    gene_with_polycistronic_transcript: true, 
+    gene_with_recoded_mRNA: true, 
+    gene_with_start_codon_CUG: true, 
+    gene_with_stop_codon_read_through: true, 
+    gene_with_stop_codon_redefined_as_pyrrolysine: true, 
+    gene_with_stop_codon_redefined_as_selenocysteine: true, 
+    gene_with_trans_spliced_transcript: true, 
+    gene_with_transcript_with_translational_frameshift: true, 
+    kinetoplast_gene: true, 
+    leucoplast_gene: true, 
+    lincRNA_gene: true, 
+    maternally_imprinted_gene: true, 
+    maxicircle_gene: true, 
+    miRNA_gene: true, 
+    minicircle_gene: true, 
+    mt_gene: true, 
+    ncRNA_gene: true, 
+    negatively_autoregulated_gene: true, 
+    nuclear_gene: true, 
+    nucleomorph_gene: true, 
+    paternally_imprinted_gene: true, 
+    piRNA_gene: true, 
+    plasmid_gene: true, 
+    plastid_gene: true, 
+    positional_candidate_gene: true, 
+    positively_autoregulated_gene: true, 
+    post_translationally_regulated_gene: true, 
+    predicted_gene: true, 
+    proplastid_gene: true, 
+    protein_coding_gene: true, 
+    proviral_gene: true, 
+    rRNA_gene: true, 
+    recombinationally_inverted_gene: true, 
+    recombinationally_rearranged_gene: true, 
+    recombinationally_rearranged_vertebrate_immune_system_gene: true, 
+    rescue_gene: true, 
+    retrogene: true, 
+    scRNA_gene: true, 
+    silenced_gene: true, 
+    snoRNA_gene: true, 
+    stRNA_gene: true, 
+    tRNA_gene: true, 
+    telomerase_RNA_gene: true, 
+    tmRNA_gene: true, 
+    transgene: true, 
+    translationally_regulated_gene: true, 
+    transposable_element_gene: true, 
+    wild_type_rescue_gene: true
+};
+
+/** 
+ *  flattened Sequence Ontology for Transcript
+ *  Transcript and is-a descendants (including mRNA)
+ */
+SequenceOntologyUtils.transcriptTerms = {
+    ARIA: true, 
+    ARRET: true, 
+    C_D_box_snoRNA: true, 
+    C_D_box_snoRNA_primary_transcript: true, 
+    CsrB_RsmB_RNA: true, 
+    DsrA_RNA: true, 
+    GcvB_RNA: true, 
+    H_ACA_box_snoRNA: true, 
+    H_ACA_box_snoRNA_primary_transcript: true, 
+    MicF_RNA: true, 
+    OxyS_RNA: true, 
+    RNA_6S: true, 
+    RNase_MRP_RNA: true, 
+    RNase_P_RNA: true, 
+    RRE_RNA: true, 
+    RprA_RNA: true, 
+    SRP_RNA: true, 
+    SRP_RNA_primary_transcript: true, 
+    TERRA: true, 
+    U11_snRNA: true, 
+    U12_snRNA: true, 
+    U14_snoRNA: true, 
+    U14_snoRNA_primary_transcript: true, 
+    U1_snRNA: true, 
+    U2_snRNA: true, 
+    U3_snoRNA: true, 
+    U4_snRNA: true, 
+    U4atac_snRNA: true, 
+    U5_snRNA: true, 
+    U6_snRNA: true, 
+    U6atac_snRNA: true, 
+    Y_RNA: true, 
+    aberrant_processed_transcript: true, 
+    alanine_tRNA_primary_transcript: true, 
+    alanyl_tRNA: true, 
+    alternatively_spliced_transcript: true, 
+    anti_ARRET: true, 
+    antisense_RNA: true, 
+    antisense_lncRNA: true, 
+    antisense_primary_transcript: true, 
+    arginine_tRNA_primary_transcript: true, 
+    arginyl_tRNA: true, 
+    asparagine_tRNA_primary_transcript: true, 
+    asparaginyl_tRNA: true, 
+    aspartic_acid_tRNA_primary_transcript: true, 
+    aspartyl_tRNA: true, 
+    capped_mRNA: true, 
+    capped_primary_transcript: true, 
+    class_II_RNA: true, 
+    class_I_RNA: true, 
+    consensus_mRNA: true, 
+    cysteine_tRNA_primary_transcript: true, 
+    cysteinyl_tRNA: true, 
+    dicistronic_mRNA: true, 
+    dicistronic_primary_transcript: true, 
+    dicistronic_transcript: true, 
+    edited_mRNA: true, 
+    edited_transcript: true, 
+    edited_transcript_by_A_to_I_substitution: true, 
+    enhancerRNA: true, 
+    enzymatic_RNA: true, 
+    exemplar_mRNA: true, 
+    glutamic_acid_tRNA_primary_transcript: true, 
+    glutamine_tRNA_primary_transcript: true, 
+    glutaminyl_tRNA: true, 
+    glutamyl_tRNA: true, 
+    glycine_tRNA_primary_transcript: true, 
+    glycyl_tRNA: true, 
+    guide_RNA: true, 
+    histidine_tRNA_primary_transcript: true, 
+    histidyl_tRNA: true, 
+    intronic_lncRNA: true, 
+    isoleucine_tRNA_primary_transcript: true, 
+    isoleucyl_tRNA: true, 
+    large_subunit_rRNA: true, 
+    leucine_tRNA_primary_transcript: true, 
+    leucyl_tRNA: true, 
+    lincRNA: true, 
+    lnc_RNA: true, 
+    lysine_tRNA_primary_transcript: true, 
+    lysyl_tRNA: true, 
+    mRNA: true, 
+    mRNA_recoded_by_codon_redefinition: true, 
+    mRNA_recoded_by_translational_bypass: true, 
+    mRNA_with_frameshift: true, 
+    mRNA_with_minus_1_frameshift: true, 
+    mRNA_with_minus_2_frameshift: true, 
+    mRNA_with_plus_1_frameshift: true, 
+    mRNA_with_plus_2_frameshift: true, 
+    mature_transcript: true, 
+    methionine_tRNA_primary_transcript: true, 
+    methionyl_tRNA: true, 
+    methylation_guide_snoRNA: true, 
+    methylation_guide_snoRNA_primary_transcript: true, 
+    miRNA: true, 
+    miRNA_primary_transcript: true, 
+    mini_exon_donor_RNA: true, 
+    monocistronic_mRNA: true, 
+    monocistronic_primary_transcript: true, 
+    monocistronic_transcript: true, 
+    ncRNA: true, 
+    nc_primary_transcript: true, 
+    phenylalanine_tRNA_primary_transcript: true, 
+    phenylalanyl_tRNA: true, 
+    piRNA: true, 
+    polyadenylated_mRNA: true, 
+    polycistronic_mRNA: true, 
+    polycistronic_primary_transcript: true, 
+    polycistronic_transcript: true, 
+    pre_edited_mRNA: true, 
+    primary_transcript: true, 
+    processed_transcript: true, 
+    proline_tRNA_primary_transcript: true, 
+    prolyl_tRNA: true, 
+    protein_coding_primary_transcript: true, 
+    pseudouridylation_guide_snoRNA: true, 
+    pyrrolysine_tRNA_primary_transcript: true, 
+    pyrrolysyl_tRNA: true, 
+    rRNA: true, 
+    rRNA_16S: true, 
+    rRNA_18S: true, 
+    rRNA_21S: true, 
+    rRNA_23S: true, 
+    rRNA_25S: true, 
+    rRNA_28S: true, 
+    rRNA_5S: true, 
+    rRNA_5_8S: true, 
+    rRNA_cleavage_RNA: true, 
+    rRNA_cleavage_snoRNA_primary_transcript: true, 
+    rRNA_large_subunit_primary_transcript: true, 
+    rRNA_primary_transcript: true, 
+    rRNA_small_subunit_primary_transcript: true, 
+    rasiRNA: true, 
+    recoded_mRNA: true, 
+    regional_centromere_outer_repeat_transcript: true, 
+    ribozyme: true, 
+    scRNA: true, 
+    scRNA_primary_transcript: true, 
+    selenocysteine_tRNA_primary_transcript: true, 
+    selenocysteinyl_tRNA: true, 
+    serine_tRNA_primary_transcript: true, 
+    seryl_tRNA: true, 
+    siRNA: true, 
+    small_regulatory_ncRNA: true, 
+    small_subunit_rRNA: true, 
+    snRNA: true, 
+    snRNA_primary_transcript: true, 
+    snoRNA: true, 
+    snoRNA_primary_transcript: true, 
+    spot_42_RNA: true, 
+    stRNA: true, 
+    stRNA_primary_transcript: true, 
+    tRNA: true, 
+    tRNA_primary_transcript: true, 
+    tasiRNA: true, 
+    tasiRNA_primary_transcript: true, 
+    telomerase_RNA: true, 
+    telomeric_transcript: true, 
+    threonine_tRNA_primary_transcript: true, 
+    threonyl_tRNA: true, 
+    tmRNA: true, 
+    tmRNA_primary_transcript: true, 
+    trans_spliced_mRNA: true, 
+    trans_spliced_transcript: true, 
+    transcript: true, 
+    transcript_bound_by_nucleic_acid: true, 
+    transcript_bound_by_protein: true, 
+    transcript_with_translational_frameshift: true, 
+    tryptophan_tRNA_primary_transcript: true, 
+    tryptophanyl_tRNA: true, 
+    tyrosine_tRNA_primary_transcript: true, 
+    tyrosyl_tRNA: true, 
+    valine_tRNA_primary_transcript: true, 
+    valyl_tRNA: true, 
+    vault_RNA: true
+};
+
+/** 
+ *  flattened Sequence Ontology for UTR
+ *  UTR and is-a descendants
+ *  also including some other terms, relationship to UTR is noted 
+ */
+SequenceOntologyUtils.utrTerms = {
+    UTR: true, 
+    three_prime_UTR: true, 
+    five_prime_UTR: true, 
+    internal_UTR: true, 
+    untranslated_region_polycistronic_mRNA: true, 
+    UTR_region: true,   /* part_of */
+    /* not including UTR_region descendants, not appropriate:
+       AU_rich_element, Bruno_response_element, iron_responsive_element, upstream_AUG_codon
+    */
+    /* not including five_prime_open_reading_frame (part-of UTR) */
+    noncoding_region_of_exon: true,  /* part_of exon */
+    five_prime_coding_exon_noncoding_region: true,  /* part_of exon */
+    three_prime_coding_exon_noncoding_region: true  /* part_of exon */
+};
+
+/**
+ *  flattened Sequence Ontology for CDS
+ *  CDS and is-a children
+ *  also including some other terms, relationship to CDS is noted 
+ */
+SequenceOntologyUtils.cdsTerms = {
+    CDS: true, 
+    CDS_fragment: true, 
+    CDS_independently_known: true, 
+    CDS_predicted: true, 
+    CDS_supported_by_sequence_similarity_data: true, 
+    CDS_supported_by_EST_or_cDNA_data: true, 
+    CDS_supported_by_domain_match_data: true, 
+    orphan_CDS: true, 
+    edited_CDS: true, 
+    transposable_element_CDS: true, 
+    polypeptide: true, /* part_of */
+    CDS_region: true,   /* part_of */
+    /* not including CDS_region descendants, not appropriate: 
+       coding_end, coding_start, codon, etc.
+    */
+    coding_region_of_exon: true, /* part_of exon */
+    five_prime_coding_exon_coding_region: true, /* part_of exon */
+    three_prime_coding_exon_coding_region: true /* part_of exon */
+}; 
+
+/**
+ *  flattened Sequence Ontology for exon
+ *  exon and is-a children
+ *  also including some other terms, relationship to exon is noted 
+ */
+SequenceOntologyUtils.exonTerms = {
+    exon: true, 
+    exon_of_single_exon_gene: true, 
+    coding_exon: true, 
+    five_prime_coding_exon: true, 
+    three_prime_coding_exon: true, 
+    interior_coding_exon: true, 
+    non_coding_exon: true, 
+    five_prime_noncoding_exon: true, 
+    three_prime_noncoding_exon: true, 
+    interior_exon: true, 
+    decayed_exon: true, /* non_functional_homolog_of */
+    pseudogenic_exon: true, /* non_functional_homolog_of */
+    exon_region: true /* part_of */
+    /*  not including descendants of exon_region that are synonymous with UTR or CDS terms
+        coding_region_of_exon: true, 
+        five_prime_coding_exon_coding_region: true,
+        three_prime_coding_exon_coding_region: true,
+        noncoding_region_of_exon: true,
+        five_prime_coding_exon_noncoding_region: true,
+        three_prime_coding_exon_noncoding_region: true 
+    */
+};
+
+SequenceOntologyUtils.startCodonTerms = {
+    start_codon: true, 
+    non_canonical_start_codon: true 
+};
+
+SequenceOntologyUtils.stopCodonTerms = {
+    stop_codon: true
+};
+
+/* not yet complete */
+SequenceOntologyUtils.spliceTerms = {
+    splice_site: true, 
+    cis_splice_site: true, 
+    five_prime_cis_splice_site: true, 
+    recursive_splice_site: true, 
+    three_prime_cis_splice_site: true, 
+    canonical_five_prime_splice_site: true, 
+    canonical_three_prime_splice_site: true, 
+    non_canonical_five_prime_splice_site: true, 
+    non_canonical_three_prime_splice_site: true, 
+    non_canonical_splice_site: true 
+};
+
+/* not yet complete? */
+SequenceOntologyUtils.intronTerms = {
+    intron: true, 
+    five_prime_intron: true, 
+    three_prime_intron: true, 
+    interior_intron: true, 
+    UTR_intron: true, 
+    twintron: true, 
+    spliceosomal_intron: true, 
+    autocatalytically_spliced_intron: true, 
+    endonuclease_spliced_intron: true, 
+    mobile_intron: true
+};
+
+return SequenceOntologyUtils;
+});
\ No newline at end of file
diff --git a/client/apollo/js/SequenceSearch.js b/client/apollo/js/SequenceSearch.js
new file mode 100644
index 0000000..3bad31e
--- /dev/null
+++ b/client/apollo/js/SequenceSearch.js
@@ -0,0 +1,243 @@
+define( [
+            'dojo/_base/declare',
+            'dojo/_base/array',
+            'dojo/dom-construct',
+            'dojo/dom-attr',
+            'dojo/dom',
+            'jquery'
+], 
+    function( declare,
+        array,
+        domConstruct,
+        domAttr,
+        dom,
+        $ ) {
+
+return declare(null, {
+constructor: function(contextPath) {
+    this.contextPath = contextPath;
+},
+
+setRedirectCallback: function(callback) {
+    this.redirectCallback = callback;
+},
+
+setErrorCallback: function(callback) {
+    this.errorCallback = callback;
+},
+
+searchSequence: function(trackName, refSeqName, starts) {
+    var operation = "search_sequence";
+    var contextPath = this.contextPath;
+    var redirectCallback = this.redirectCallback;
+    var errorCallback = this.errorCallback;
+    /*
+    <div class="search_sequence">
+        <div class="search_sequence_tools">
+            <select class="search_sequence_tools_select" />
+            <div class="search_sequence_area">
+                <div class="search_sequence_label">Enter sequence</div>
+                <div><textarea class="search_sequence_input"></textarea></div>
+                <div class="search_all_ref_seqs_area">
+                <input type="checkbox" class="search_all_ref_seqs_checkbox">Search all genomic sequences</div>
+                <div><button>Search</button></div>
+            </div>
+            <div class="search_sequence_message">No matches found</div>
+            <div><img class="waiting_image" src="plugins/WebApollo/img/loading.gif" /></div>
+        </div>
+    </div>
+    */
+
+    var content = dojo.create("div", { className: "search_sequence" });
+    var sequenceToolsDiv = dojo.create("div", { className: "search_sequence_tools" }, content);
+    var sequenceToolsSelect = dojo.create("select", {className: "search_sequence_tools_select"}, sequenceToolsDiv);
+    var sequenceDiv = dojo.create("div", { className: "search_sequence_area" }, content);
+    var sequenceLabel = dojo.create("div", { className: "search_sequence_label", innerHTML: "Enter sequence" }, sequenceDiv);
+    var sequenceFieldDiv = dojo.create("div", { }, sequenceDiv);
+    var sequenceField = dojo.create("textarea", { className: "search_sequence_input" }, sequenceFieldDiv);
+    var searchAllRefSeqsDiv = dojo.create("div", { className: "search_all_ref_seqs_area" }, sequenceDiv);
+    var searchAllRefSeqsCheckbox = dojo.create("input", { className: "search_all_ref_seqs_checkbox", type: "checkbox" }, searchAllRefSeqsDiv);
+    var searchAllRefSeqsLabel = dojo.create("span", { className: "search_all_ref_seqs_label", innerHTML: "Search all genomic sequences" }, searchAllRefSeqsDiv);
+    var sequenceButtonDiv = dojo.create("div", { }, sequenceDiv);
+    var sequenceButton = dojo.create("button", { innerHTML: "Search" }, sequenceButtonDiv);
+    var messageDiv = dojo.create("div", { className: "search_sequence_message", innerHTML: "No matches found" }, content);
+    var waitingDiv = dojo.create("div", { innerHTML: "<img class='waiting_image' src='plugins/WebApollo/img/loading.gif' />"}, content);
+    var headerDiv = dojo.create("div", { className: "search_sequence_matches_header" }, content);
+    dojo.create("span", { innerHTML: "ID", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    dojo.create("span", { innerHTML: "Start", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    dojo.create("span", { innerHTML: "End", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    dojo.create("span", { innerHTML: "Score", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    dojo.create("span", { innerHTML: "Significance", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    dojo.create("span", { innerHTML: "Identity", className: "search_sequence_matches_header_field search_sequence_matches_generic_field" }, headerDiv);
+    var matchDiv = dojo.create("div", { className: "search_sequence_matches" }, content);
+    var matches = dojo.create("div", { }, matchDiv);
+
+    dojo.style(messageDiv, { display: "none" });
+    dojo.style(matchDiv, { display: "none" });
+    dojo.style(headerDiv, { display: "none" });
+    dojo.style(waitingDiv, { display: "none" });
+    if (!refSeqName) {
+        dojo.style(searchAllRefSeqsDiv, { display: "none" });
+    }
+
+    var getSequenceSearchTools = function() {
+        var ok = false;
+        var operation = "get_sequence_search_tools";
+        var request={
+            "track": trackName,
+            "operation": operation 
+        };
+        dojo.xhrPost( {
+            postData: JSON.stringify(request), 
+            url: contextPath + "/AnnotationEditorService",
+            sync: true,
+            handleAs: "json",
+            timeout: 5000 * 1000, // Time in milliseconds
+            load: function(response, ioArgs) {
+                if(response.error) {
+                    ok = false;
+                    alert(response.error);
+                }
+                if (response.sequence_search_tools.length == 0) {
+                    ok = false;
+                    return;
+                }
+                ok = true;
+                for(var key in response.sequence_search_tools) {
+                    if (response.sequence_search_tools.hasOwnProperty(key)) {
+                        dojo.create("option", { innerHTML: response.sequence_search_tools[key].name, id: key }, sequenceToolsSelect);
+                    }
+                }
+            },
+            error: function(response, ioArgs) {
+                errorCallback(response);
+                return response;
+            }
+        });
+        return ok;
+    };
+    
+    var search = function() {
+        var residues = dojo.attr(sequenceField, "value").toUpperCase();
+        var ok = true;
+        if (residues.length == 0) {
+            alert("No sequence entered");
+            ok = false;
+        }
+        else if (residues.match(/[^ACDEFGHIKLMNPQRSTVWXY\n]/)) {
+            alert("The sequence should only contain non redundant IUPAC nucleotide or amino acid codes (except for N/X)");
+            ok = false;
+        }
+        var searchAllRefSeqs = dojo.attr(searchAllRefSeqsCheckbox, "checked");
+        if (ok) {
+            dojo.style(waitingDiv, { display: "block"} );
+            dojo.style(matchDiv, { display: "none"} );
+            dojo.style(headerDiv, { display: "none" });
+            var postobj={
+                "track": trackName,
+                "search": {
+                    "key": sequenceToolsSelect.options[sequenceToolsSelect.selectedIndex].id,
+                    "residues": residues.replace(/(\r\n|\n|\r)/gm,"")
+                },
+                "operation": operation
+            };
+            if(!searchAllRefSeqs) {
+                postobj.search.database_id=refSeqName;
+            }
+
+            dojo.xhrPost( {
+                postData: JSON.stringify(postobj),
+                url: contextPath + "/AnnotationEditorService",
+                handleAs: "json",
+                timeout: 5000 * 1000, // Time in milliseconds
+                load: function(response, ioArgs) {
+                    dojo.style(waitingDiv, { display: "none"} );
+                    if(response.error) {
+                        alert(response.error);
+                        return;
+                    }
+                    while (matches.hasChildNodes()) {
+                        matches.removeChild(matches.lastChild);
+                    }
+                    if (response.matches.length == 0) {
+                        dojo.style(messageDiv, { display: "block" });
+                        dojo.style(matchDiv, { display: "none" });
+                        dojo.style(headerDiv, { display: "none" });
+                        return;
+                    }
+                    dojo.style(messageDiv, { display: "none" });
+                    dojo.style(headerDiv, { display: "block"} );
+                    dojo.style(matchDiv, { display: "block"} );
+                    
+                    var returnedMatches = response.matches;
+                    returnedMatches.sort(function(match1, match2) {
+                        return match2.rawscore - match1.rawscore;
+                    });
+                    var maxNumberOfHits = 100;
+                    
+                    for (var i = 0; i < returnedMatches.length && i < maxNumberOfHits; ++i) {
+                        var match = returnedMatches[i];
+                        var query = match.query;
+                        var subject = match.subject;
+                        var refSeqStart = starts[subject.feature.uniquename] || 0;
+                        var refSeqEnd = starts[subject.feature.uniquename] || 0;
+                        subject.location.fmin += refSeqStart;
+                        subject.location.fmax += refSeqStart;
+                        var subjectStart = subject.location.fmin;
+                        var subjectEnd = subject.location.fmax;
+                        if (subject.location.strand == -1) {
+                            var tmp = subjectStart;
+                            subjectStart = subjectEnd;
+                            subjectEnd = tmp;
+                        }
+                        var rawscore = match.rawscore;
+                        var significance = match.significance;
+                        var identity = match.identity;
+                        var row = dojo.create("div", { className: "search_sequence_matches_row" }, matches);
+                        var subjectIdColumn = dojo.create("span", { innerHTML: subject.feature.uniquename, className: "search_sequence_matches_field search_sequence_matches_generic_field", title: subject.feature.uniquename }, row);
+                        var subjectStartColumn = dojo.create("span", { innerHTML: subjectStart, className: "search_sequence_matches_field search_sequence_matches_generic_field" }, row);
+                        var subjectEndColumn = dojo.create("span", { innerHTML: subjectEnd, className: "search_sequence_matches_field search_sequence_matches_generic_field" }, row);
+                        var scoreColumn = dojo.create("span", { innerHTML: match.rawscore, className: "search_sequence_matches_field search_sequence_matches_generic_field" }, row);
+                        var significanceColumn = dojo.create("span", { innerHTML: match.significance, className: "search_sequence_matches_field search_sequence_matches_generic_field" }, row);
+                        var identityColumn = dojo.create("span", { innerHTML : match.identity, className: "search_sequence_matches_field search_sequence_matches_generic_field" }, row);
+                        dojo.connect(row, "onclick", function(id, fmin, fmax) {
+                            return function() {
+                                redirectCallback(id, fmin, fmax);
+                            };
+                        }(subject.feature.uniquename, subject.location.fmin, subject.location.fmax));
+                    }
+                },
+                // The ERROR function will be called in an error case.
+                error: function(response, ioArgs) { // 
+                    errorCallback(response);
+                    return response;
+                }
+
+            });
+        }
+    };
+    
+    dojo.connect(sequenceField, "onkeypress", function(event) {
+        if (event.keyCode == dojo.keys.ENTER) {
+            event.preventDefault();
+            search();
+        }
+    });
+    dojo.connect(sequenceButton, "onclick", search);
+    dojo.connect(searchAllRefSeqsLabel, "onclick", function() {
+        dojo.attr(searchAllRefSeqsCheckbox, "checked", !searchAllRefSeqsCheckbox.checked);
+    });
+
+    if (!getSequenceSearchTools()) {
+        alert("No search plugins setup");
+        return null;
+    }
+    
+    return content;
+}
+
+});
+
+
+});
+
diff --git a/client/apollo/js/Store/SeqFeature/ApolloGFF3.js b/client/apollo/js/Store/SeqFeature/ApolloGFF3.js
new file mode 100644
index 0000000..3f9c818
--- /dev/null
+++ b/client/apollo/js/Store/SeqFeature/ApolloGFF3.js
@@ -0,0 +1,132 @@
+define( [   'dojo/_base/declare',
+            'dojo/_base/lang',
+            'dojo/_base/array',
+            'dojo/Deferred',
+            'JBrowse/Model/SimpleFeature',
+            'JBrowse/Store/SeqFeature',
+            'JBrowse/Store/DeferredFeaturesMixin',
+            'JBrowse/Store/DeferredStatsMixin',
+            'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
+            'JBrowse/Store/SeqFeature/GFF3/Parser', 
+            'JBrowse/Store/SeqFeature/GFF3', 
+            'WebApollo/SequenceOntologyUtils'
+        ],
+        function(
+            declare,
+            lang,
+            array,
+            Deferred,
+            SimpleFeature,
+            SeqFeatureStore,
+            DeferredFeatures,
+            DeferredStats,
+            GlobalStatsEstimationMixin,
+            Parser, 
+            GFF3, 
+            SeqOnto
+        ) 
+        {
+            
+return declare( [ GFF3 ], 
+{
+    /* overriding _loadFeatures to handle common case of three-level gene-transcript-exon hierarchy, 
+     *   when three-level hierarchy is fully represented in the data model, currently HTMLFeatures and DraggableHTMLFeatures can't render it correctly 
+     *   (always renders the top two levels of a feature hierarchy)
+     *   therefore when detecting this case, ApolloGFF3 removes the top "gene" level of the hierarchy
+     */
+    _loadFeatures: function() {
+        //        this.inherited( arguments );
+        var thisB = this;
+        var features = this.bareFeatures = [];
+
+        var featuresSorted = true;
+        var seenRefs = this.refSeqs = {};
+        var parser = new Parser(
+            {
+                featureCallback: function(fs) {
+                    array.forEach( fs, function( feature ) {
+                                       var prevFeature = features[ features.length-1 ];
+                                       var regRefName = thisB.browser.regularizeReferenceName( feature.seq_id );
+                                       if( regRefName in seenRefs && prevFeature && prevFeature.seq_id != feature.seq_id )
+                                           featuresSorted = false;
+                                       if( prevFeature && prevFeature.seq_id == feature.seq_id && feature.start < prevFeature.start )
+                                           featuresSorted = false;
+
+                                       if( !( regRefName in seenRefs ))
+                                           seenRefs[ regRefName ] = features.length;
+
+                                       features.push( feature );
+                                   });
+                },
+                endCallback:     function()  {
+                    var newfeats = [];
+                    for (var i=0; i<features.length; i++) {
+                        var oldfeat = features[i];
+                        var type = oldfeat.type;
+                        var transformed = false;
+                        if (SeqOnto.geneTerms[type] && oldfeat.child_features)  {
+                            // special-casing for typical gene-mRNA-exon hierarchy:
+                            // if it's a gene, and find transcript term underneath, promote all children to top and strip out parent attribute
+                            var subfeats = oldfeat.child_features;
+                            var has_transcript = false;
+                            for (var k=0; k<subfeats.length; k++)  {
+                                var subfeat = subfeats[k];
+                                if (lang.isArray(subfeat)) { 
+                                    // hack due to weirdness in current GFF3 parser data struct where all subfeats are wrapped with an extra array
+                                    subfeat = subfeat[0];
+                                }
+                                if (SeqOnto.transcriptTerms[subfeat.type]) { has_transcript = true; }
+                                break;
+                            }
+                            if (has_transcript) {
+                                for (k=0; k<subfeats.length; k++) {
+                                    var subfeat = subfeats[k];
+                                    // hack due to weirdness in current GFF3 parser data struct where all subfeats are wrapped with an extra array
+                                    if (lang.isArray(subfeat)) { subfeat = subfeat[0]; }
+                                    var test = true;
+                                    if (subfeat.attributes.Parent) {
+                                        subfeat.attributes.ParentGene = subfeat.attributes.Parent; 
+                                        // just nulling out causes errors when GFF3._formatData tries to join attributes
+                                        // so need to delete, even though delete is a much slower operation
+                                        // subfeat.attributes.Parent = null;
+                                        delete subfeat.attributes.Parent;
+                                    }
+                                    newfeats.push( subfeat );
+                                }
+                                transformed = true;
+                            }
+                        }
+                        if (! transformed) {
+                            newfeats.push( oldfeat );
+                        }
+                    }
+                    thisB.bareFeatures = newfeats;
+                    features = newfeats;
+                    if( ! featuresSorted ) {
+                        features.sort( thisB._compareFeatureData );
+                        // need to rebuild the refseq index if changing the sort order
+                        thisB._rebuildRefSeqs( features );
+                    }
+
+                    thisB._estimateGlobalStats()
+                         .then( function( stats ) {
+                                    thisB.globalStats = stats;
+                                    thisB._deferred.stats.resolve();
+                                });
+
+                    thisB._deferred.features.resolve( features );
+                }
+            });
+
+        // parse the whole file and store it
+        this.data.fetchLines(
+            function( line ) {
+                parser.addLine(line);
+            },
+            lang.hitch( parser, 'finish' ),
+            lang.hitch( this, '_failAllDeferred' )
+        );
+    }
+
+});
+});
diff --git a/client/apollo/js/Store/SeqFeature/PseudoNCList.js b/client/apollo/js/Store/SeqFeature/PseudoNCList.js
new file mode 100644
index 0000000..5af8528
--- /dev/null
+++ b/client/apollo/js/Store/SeqFeature/PseudoNCList.js
@@ -0,0 +1,22 @@
+/**
+*   notes on PseudoNCList needed for porting Trellis data loading to jbrowse_1.7 branch
+*   mostly need to override ID assignment based on position in NCList, 
+*      and replace with unique IDs already present in feature arrays
+*
+*  inherit from jbrowse Store/NCList, but override:
+
+_decorate_feature: function(accessors, feature, id, parent)  {
+       feature.get = accessors.get;
+       if (config.uniqueIdField)  {
+           otherid = feature.get(uniqueIdField)
+       }
+       var uid;
+       if (otherid)  { uid = otherid; }
+       else  { uid = id; }
+       this.inherited( accessors, feature, uid, parent );
+}
+
+
+*/
+
+
diff --git a/client/apollo/js/Store/SeqFeature/ScratchPad.js b/client/apollo/js/Store/SeqFeature/ScratchPad.js
new file mode 100644
index 0000000..418ba3c
--- /dev/null
+++ b/client/apollo/js/Store/SeqFeature/ScratchPad.js
@@ -0,0 +1,75 @@
+define( ['dojo/_base/declare',
+         'JBrowse/Store/SeqFeature'
+        ],
+        function( declare, SeqFeatureStore ) {
+
+return declare( SeqFeatureStore,
+{
+    constructor: function( args ) {
+        this.refSeq = args.refSeq;
+        this.features = {};
+        this.sorted_feats = [];
+        this._calculateStats();
+    },
+
+    insert: function( feature ) {
+        this.features[ feature.id() ] = feature;
+        this._calculateStats();
+    },
+
+    replace: function( feature ) {
+        this.features[ feature.id() ] = feature;
+        this._calculateStats();
+    }, 
+
+
+    deleteFeatureById: function( id ) {
+        delete  this.features[ id ];
+        this._calculateStats();
+    },
+    
+
+    /* if feature with given id is present in store, return it.  Otherwise return null */
+    getFeatureById: function( id )  {
+        return this.features[ id ];
+    },
+
+    _calculateStats: function() {
+        var minStart = Infinity;
+        var maxEnd = -Infinity;
+        var featureCount = 0;
+        for( var id in this.features ) {
+            var f = this.features[id];
+            var s = f.get('start');
+            var e = f.get('end');
+            if( s < minStart )
+                minStart = s;
+
+            if( e > maxEnd )
+                maxEnd = e;
+
+            featureCount++;
+        }
+
+        this.globalStats = {
+            featureDensity: featureCount/(this.refSeq.end - this.refSeq.start +1), 
+            featureCount: featureCount,
+            minStart: minStart, /* 5'-most feature start */
+            maxEnd: maxEnd,     /* 3'-most feature end */
+            span: (maxEnd-minStart+1)  /* min span containing all features */
+        };
+    },
+
+    getFeatures: function( query, featCallback, endCallback, errorCallback ) {
+        var start = query.start;
+        var end = query.end;
+        for( var id in this.features ) {
+            var f = this.features[id];
+            if(! ( f.get('end') < start  || f.get('start') > end ) ) {
+                featCallback( f );
+            }
+        }
+        if (endCallback)  { endCallback() }
+    }
+});
+});
diff --git a/client/apollo/js/TrackConfigTransformer.js b/client/apollo/js/TrackConfigTransformer.js
new file mode 100644
index 0000000..b354bb4
--- /dev/null
+++ b/client/apollo/js/TrackConfigTransformer.js
@@ -0,0 +1,57 @@
+/**
+ *  TrackConfigTransformer takes JBrowse track.config object and, if needed, 
+ *      modifies in place to use WebApollo-specific track types, etc.
+ */
+define( [ 'dojo/_base/declare' ],
+        function( declare ) {
+
+return declare( null, {
+
+
+constructor: function( args )  {
+   
+    this.transformers=[];
+    var browser=args.browser;
+    this.overridePlugins=browser.config.overridePlugins;
+    this.transformers["JBrowse/View/Track/HTMLFeatures"] = function(trackConfig) {
+        trackConfig.type = "WebApollo/View/Track/DraggableHTMLFeatures";
+    };
+
+    this.transformers["JBrowse/View/Track/CanvasFeatures"] = function(trackConfig) {
+        trackConfig.type = "WebApollo/View/Track/WebApolloCanvasFeatures";
+    };
+
+    this.transformers["JBrowse/View/Track/Sequence"] = function(trackConfig) {
+        trackConfig.type = "WebApollo/View/Track/AnnotSequenceTrack";
+        trackConfig.storeClass = "WebApollo/Store/SeqFeature/ScratchPad";
+        trackConfig.style = { className: "{type}", 
+                              uniqueIdField : "id" };
+        trackConfig.compress = 0;
+        trackConfig.subfeatures = 1;
+    };
+    
+    this.transformers["JBrowse/View/Track/Alignments"] = function(trackConfig) {
+        if(!trackConfig.overrideDraggable&&!browser.config.overrideDraggable) {
+            trackConfig.type = "WebApollo/View/Track/DraggableAlignments";
+        }
+    };
+
+    this.transformers["JBrowse/View/Track/Alignments2"] = function(trackConfig) {
+        trackConfig.type = "WebApollo/View/Track/WebApolloAlignments2";
+    };
+
+},
+
+transform: function(trackConfig) {
+    if (trackConfig.overridePlugins||this.overridePlugins) {
+        return;
+    }
+    if (this.transformers[trackConfig.type]) {
+        var transformer = this.transformers[trackConfig.type];
+        transformer(trackConfig);
+    }
+}
+                    
+});
+
+});
diff --git a/client/apollo/js/View/Dialog/Help.js b/client/apollo/js/View/Dialog/Help.js
new file mode 100644
index 0000000..3665eda
--- /dev/null
+++ b/client/apollo/js/View/Dialog/Help.js
@@ -0,0 +1,60 @@
+define( [
+            'dojo/_base/declare'
+        ],
+        function(
+            declare
+        ) {
+return declare( null, {
+    defaultHelp: function() {
+        return ''
+                + '<div class="help_dialog">'
+                + '<div class="main" style="float: left; width: 49%;">'
+                + '<dl>'
+                + '<dt>Navigation</dt>'
+                + '<dd><ul>'
+                + '    <li>Move the view by clicking and dragging in the track area, or by clicking <img class="icon nav" id="moveLeftSmall" src="'+this.browser.resolveUrl('img/Empty.png')+'">  or <img class="icon nav" id="moveRightSmall" src="'+this.browser.resolveUrl('img/Empty.png')+'"> in the navigation bar, or by pressing the left and right arrow keys.</li>'
+                + '    <li>Center the view at a point by clicking on either the track scale bar or overview bar, or by shift-clicking in the track area.</li>'
+                + '</ul></dd>'
+                + '<dt>Zooming</dt>'
+                + '<dd><ul>'
+                + '    <li>Zoom in and out by clicking <img class="icon nav" id="zoomInSmall" src="'+this.browser.resolveUrl('img/Empty.png')+'"> or <img class="icon nav" id="zoomOutSmall" src="'+this.browser.resolveUrl('img/Empty.png')+'"> in the navigation bar, or by pressing the up and down arrow keys while holding down "shift".</li>'
+                + '    <li>Select a region and zoom to it ("rubber-band" zoom) by clicking and dragging in the overview or track scale bar, or shift-clicking and dragging in the track area.</li>'
+                + '    </ul>'
+                + '</dd>'
+                + '<dt>Searching</dt>'
+                + '<dd><ul>'
+                + '    <li>Jump to a feature or reference sequence by typing its name in the location box and pressing Enter.</li>'
+                + '    <li>Jump to a specific region by typing the region into the location box as: <span class="example">ref:start..end</span>.</li>'
+                + '    </ul>'
+                + '</dd>'
+                + '</dl>'
+                + '</div>'
+                + '<div class="main" style="float: right; width: 49%;">'
+                + '<dl>'
+                + '<dt>Annotating features</dt>'
+                + '<dd><ul><li>Click-and-drag features to the User-created annotations or right click features and select "Create new annotation".</li>'
+                + '<li>Use "edge matching" function, shown as red highlight, to match exon boundaries to evidence from gene models or alignments.</li>'
+                + '<li>Use "Color by CDS" to highlight the calculated translation frame for annotations and evidence features.</li>'
+                + '<li>Add details for each annotation using the "Information Editor" dialog.</li></ul></dd>'
+                + '<dt>Annotation shortcuts</dt>'
+                + '<dd>'
+                + '    <ul>'
+                + '        <li>Use [ and ] to jump between splice sites in a given annotation on the User-created annotation area.</dt>'
+                + '        <li>Use { and } to jump to the nearest gene on the User-created annotation area.</dt>'
+                + '        <li>Select a feature in the User-created annotation area and press alt-click to quickly reach the "Information editor".</dt>'
+                + '    </ul>'
+                + '</dd>'
+                + '<div style="float: left; width: 49%;"><dt>Apollo links</dt>'
+                + '<dd><ul>'
+                + '<li><a href="http://genomearchitect.org/users-guide/" target="_blank">Apollo User Guide</a></li>'
+                + '<li><a href="http://genomearchitect.readthedocs.io/en/latest/" target="_blank">Apollo Configuration and Installation Guide</a></li>'
+                + '</ul></dd>'
+                + '</div>'
+                + '<div style="float: right; width:49%; margin-top: 10px;"><a href="http://genomearchitect.org" target="_blank"><img src="plugins/WebApollo/img/ApolloLogo_100x36.png"/></a></div>'
+                + '</dl>'
+                + '</div>'
+                + '</div>'
+            ;
+    }
+});
+});
diff --git a/client/apollo/js/View/Track/AnnotSequenceTrack.js b/client/apollo/js/View/Track/AnnotSequenceTrack.js
new file mode 100644
index 0000000..9dc6c33
--- /dev/null
+++ b/client/apollo/js/View/Track/AnnotSequenceTrack.js
@@ -0,0 +1,18 @@
+define( [
+    'dojo/_base/declare',
+    'WebApollo/View/Track/SequenceTrack'
+],
+        function( declare, SequenceTrack) {
+
+var AnnotSequenceTrack = declare( SequenceTrack, 
+{   
+    // AnnotSequenceTrack is just a stub for now, all functionality is in SequenceTrack superclass
+    //    intent is to eventually move sequence alteration functionality out of SequenceTrack and into AnnotSequenceTrack subclass
+    constructor: function(args)  {  }
+    // , test: function()  { console.log("called AnnotSequenceTrack.test"); }
+});
+
+return AnnotSequenceTrack;
+
+});
+
diff --git a/client/apollo/js/View/Track/AnnotTrack.js b/client/apollo/js/View/Track/AnnotTrack.js
new file mode 100644
index 0000000..83d447c
--- /dev/null
+++ b/client/apollo/js/View/Track/AnnotTrack.js
@@ -0,0 +1,5934 @@
+define([
+        'dojo/_base/declare',
+        'dojo/_base/array',
+        'dojo/on',
+        'dojo/request',
+        'jquery',
+        'jqueryui/draggable',
+        'jqueryui/droppable',
+        'jqueryui/resizable',
+        'jqueryui/autocomplete',
+        'jqueryui/dialog',
+        'dijit/registry',
+        'dijit/Menu',
+        'dijit/MenuItem',
+        'dijit/MenuSeparator',
+        'dijit/PopupMenuItem',
+        'dijit/form/Button',
+        'dijit/form/DropDownButton',
+        'dijit/DropDownMenu',
+        'dijit/form/ComboBox',
+        'dijit/form/TextBox',
+        'dijit/form/ValidationTextBox',
+        'dijit/form/RadioButton',
+        'dojox/widget/DialogSimple',
+        'dojox/grid/DataGrid',
+        'dojox/grid/cells/dijit',
+        'dojo/data/ItemFileWriteStore',
+        'WebApollo/View/Track/DraggableHTMLFeatures',
+        'WebApollo/FeatureSelectionManager',
+        'WebApollo/JSONUtils',
+        'WebApollo/BioFeatureUtils',
+        'WebApollo/Permission',
+        'WebApollo/SequenceSearch',
+        'WebApollo/EUtils',
+        'WebApollo/SequenceOntologyUtils',
+        'JBrowse/Model/SimpleFeature',
+        'JBrowse/Util',
+        'JBrowse/View/GranularRectLayout',
+        'JBrowse/View/ConfirmDialog',
+        'dojo/request/xhr',
+        'dojox/widget/Standby',
+        'dijit/Tooltip',
+        'WebApollo/FormatUtils',
+        'dijit/form/Select',
+        'dojo/store/Memory',
+        'dojo/data/ObjectStore'
+    ],
+    function (declare,
+              array,
+              on,
+              request,
+              $,
+              draggable,
+              droppable,
+              resizable,
+              autocomplete,
+              dialog,
+              registry,
+              dijitMenu,
+              dijitMenuItem,
+              dijitMenuSeparator,
+              dijitPopupMenuItem,
+              dijitButton,
+              dijitDropDownButton,
+              dijitDropDownMenu,
+              dijitComboBox,
+              dijitTextBox,
+              dijitValidationTextBox,
+              dijitRadioButton,
+              dojoxDialogSimple,
+              dojoxDataGrid,
+              dojoxCells,
+              dojoItemFileWriteStore,
+              DraggableFeatureTrack,
+              FeatureSelectionManager,
+              JSONUtils,
+              BioFeatureUtils,
+              Permission,
+              SequenceSearch,
+              EUtils,
+              SequenceOntologyUtils,
+              SimpleFeature,
+              Util,
+              Layout,
+              ConfirmDialog,
+              xhr,
+              Standby,
+              Tooltip,
+              FormatUtils,
+              Select,
+              Memory,
+              ObjectStore) {
+
+        var listener;
+        var client;
+
+        var annot_context_menu;
+        var contextMenuItems;
+
+        var context_path = "..";
+
+        var AnnotTrack = declare(DraggableFeatureTrack, {
+            constructor: function (args) {
+                this.isWebApolloAnnotTrack = true;
+                this.has_custom_context_menu = true;
+                this.exportAdapters = [];
+
+                this.selectionManager = this.setSelectionManager(this.webapollo.annotSelectionManager);
+
+                this.selectionClass = "selected-annotation";
+                this.annot_under_mouse = null;
+
+                /**
+                 * only show residues overlay if "pointer-events" CSS property is
+                 * supported (otherwise will interfere with passing of events to
+                 * features beneath the overlay)
+                 */
+                this.useResiduesOverlay = 'pointerEvents' in document.body.style;
+                this.FADEIN_RESIDUES = false;
+
+                var thisB = this;
+
+                this.annotMouseDown = function (event) {
+                    thisB.onAnnotMouseDown(event);
+                };
+
+                this.verbose_create = false;
+                this.verbose_add = false;
+                this.verbose_delete = false;
+                this.verbose_drop = false;
+                this.verbose_click = false;
+                this.verbose_resize = false;
+                this.verbose_mousedown = false;
+                this.verbose_mouseenter = false;
+                this.verbose_mouseleave = false;
+                this.verbose_render = false;
+                this.verbose_server_notification = false;
+
+                var track = this;
+
+
+                this.gview.browser.subscribe("/jbrowse/v1/n/navigate", dojo.hitch(this, function (currRegion) {
+                    if (currRegion.ref != this.refSeq.name) {
+
+                        // we socket changes
+                        if (this.listener) {
+                            this.listener.close();
+                        }
+                    }
+
+                }));
+
+                this.gview.browser.setGlobalKeyboardShortcut('[', track, 'scrollToPreviousEdge');
+                this.gview.browser.setGlobalKeyboardShortcut(']', track, 'scrollToNextEdge');
+
+                this.gview.browser.setGlobalKeyboardShortcut('}', track, 'scrollToNextTopLevelFeature');
+                this.gview.browser.setGlobalKeyboardShortcut('{', track, 'scrollToPreviousTopLevelFeature');
+
+                this.topLevelParents = {};
+            },
+
+            renderExonSegments: function (subfeature, subDiv, cdsMin, cdsMax,
+                                          displayStart, displayEnd, priorCdsLength, reverse) {
+                var utrClass;
+                var parentType = subfeature.parent().afeature.parent_type;
+                if (!this.isProteinCoding(subfeature.parent())) {
+                    var clsName = parentType && parentType.name == "pseudogene" ? "pseudogene" : subfeature.parent().get("type");
+                    var cfg = this.config.style.alternateClasses[clsName];
+                    utrClass = cfg.className;
+                }
+                return this.inherited(arguments, [subfeature, subDiv, cdsMin, cdsMax, displayStart, displayEnd, priorCdsLength, reverse, utrClass]);
+            },
+
+            _defaultConfig: function () {
+                var thisConfig = this.inherited(arguments);
+                thisConfig.menuTemplate = null;
+                thisConfig.noExport = true;  // turn off default "Save track data" "
+                thisConfig.style.centerChildrenVertically = false;
+                thisConfig.pinned = true;
+                return thisConfig;
+            },
+
+
+            setViewInfo: function (genomeView, numBlocks,
+                                   trackDiv, labelDiv,
+                                   widthPct, widthPx, scale) {
+
+                this.inherited(arguments);
+                var track = this;
+
+                // to initAnnotContextMenu() once permissions are returned by server
+                var success = this.getPermission(function () {
+                    track.initAnnotContextMenu();
+                });
+
+                var standby = new Standby({
+                    target: track.div,
+                    color: "transparent",
+                    image: "plugins/WebApollo/img/loading.gif"
+                });
+                document.body.appendChild(standby.domNode);
+                standby.startup();
+                standby.show();
+
+
+                if (!this.webapollo.loginMenuInitialized && this.browser.config.show_nav && this.browser.config.show_menu) {
+                    this.webapollo.initLoginMenu(this.username);
+                }
+                if (!this.webapollo.searchMenuInitialized && this.permission && this.browser.config.show_nav && this.browser.config.show_menu) {
+                    this.webapollo.initSearchMenu();
+                }
+                this.initSaveMenu();
+                this.initPopupDialog();
+
+                if (success) {
+                    track.createAnnotationChangeListener(0);
+
+                    var query = {
+                        "clientToken": track.getClientToken(),
+                        "track": track.getUniqueTrackName(),
+                        "operation": "get_features",
+                        "organism": track.webapollo.organism
+                    };
+
+                    xhr(context_path + "/AnnotationEditorService", {
+                        handleAs: "json",
+                        data: JSON.stringify(query),
+                        method: "post"
+                    }).then(function (response, ioArgs) {
+                        var responseFeatures = response.features;
+                        if (!responseFeatures) {
+                            alert("Error: " + JSON.stringify(response));
+                            console.log(response);
+                            return;
+                        }
+
+                        for (var i = 0; i < responseFeatures.length; i++) {
+                            var jfeat = JSONUtils.createJBrowseFeature(responseFeatures[i]);
+                            track.store.insert(jfeat);
+                            track.processParent(responseFeatures[i], "ADD");
+                        }
+                        track.changed();
+                        standby.hide();
+
+                    }, function (response, ioArgs) {
+                        console.log("Annotation server error--maybe you forgot to login to the server?");
+                        track.handleError({responseText: response.response.text});
+                        return response;
+                    });
+                }
+
+                if (success) {
+                    this.makeTrackDroppable();
+                    this.hide();
+                    this.show();
+                }
+                else {
+                    this.hide();
+                    if (this.browser.config.disableJBrowseMode) {
+                        this.login();
+                    }
+                }
+
+            },
+
+            generateRandomNumber: function(length){
+                var string = '';
+                while(string.length<length){
+                    string += Math.floor(Math.random()*1000);
+                }
+                return string ;
+            },
+
+            getClientToken: function () {
+                if (this.runningApollo()) {
+                    return this.getApollo().getClientToken();
+                }
+                else{
+                    var returnItem = window.sessionStorage.getItem("clientToken");
+                    if (!returnItem) {
+                        var randomNumber = this.generateRandomNumber(20);
+                        window.sessionStorage.setItem("clientToken", randomNumber);
+                    }
+                    return window.sessionStorage.getItem("clientToken");
+                }
+            },
+
+            createAnnotationChangeListener: function (numTry) {
+                //this.listener = new SockJS(context_path+"/stomp");
+                var stomp_url = window.location.href;
+                var index = stomp_url.search('/jbrowse');
+                stomp_url = stomp_url.substr(0, index) + '/stomp/';
+                var numberIndex = stomp_url.search('/[0-9]+/');
+                var stompIndex = stomp_url.search('/stomp/');
+                stomp_url = stomp_url.substr(0,numberIndex) + stomp_url.substr(stompIndex);
+
+                this.listener = new SockJS(stomp_url);
+                this.client = Stomp.over(this.listener);
+                this.client.debug = function (str) {
+                    if (this.verbose_server_notification) {
+                        console.log(str);
+                    }
+                };
+                var client = this.client;
+                var track = this;
+                var browser = this.gview.browser;
+                var apolloMainPanel = this.getApollo();
+
+                console.log('Registering Apollo listeners.');
+
+                browser.subscribe("/jbrowse/v1/n/navigate", dojo.hitch(this, function (currRegion) {
+                    apolloMainPanel.handleNavigationEvent(JSON.stringify(currRegion));
+                }));
+
+                var navigateToLocation = function(urlObject) {
+                    if(urlObject.exact){
+                        browser.callLocation(urlObject.url);
+                    }
+                    else{
+                        var location = Util.parseLocString( urlObject.url);
+                        browser.showRegion(location);
+                    }
+                };
+
+                var sendTracks = function (trackList, visibleTrackNames, showLabels) {
+                    var filteredTrackList = [];
+                    for (var trackConfigIndex in trackList) {
+                        var filteredTrack = {};
+                        var trackConfig = trackList[trackConfigIndex];
+                        var visible = visibleTrackNames.indexOf(trackConfig.label) >= 0 || showLabels.indexOf(trackConfig.label) >= 0;
+                        filteredTrack.label = trackConfig.label;
+                        filteredTrack.key = trackConfig.key;
+                        filteredTrack.name = trackConfig.name;
+                        filteredTrack.type = trackConfig.type;
+                        filteredTrack.category = trackConfig.category;
+                        filteredTrack.urlTemplate = trackConfig.urlTemplate;
+                        filteredTrack.visible = visible;
+                        filteredTrackList.push(filteredTrack);
+                    }
+
+                    // if for some reason this method is called in the wrong place, we catch the error
+                    if(apolloMainPanel){
+                        apolloMainPanel.loadTracks(JSON.stringify(filteredTrackList));
+                    }
+                };
+
+
+
+                var handleTrackVisibility = function (trackInfo) {
+                    var command = trackInfo.command;
+                    if (command == "show") {
+                        browser.publish('/jbrowse/v1/v/tracks/show', [browser.trackConfigsByName[trackInfo.label]]);
+                    }
+                    else if (command == "hide") {
+                        browser.publish('/jbrowse/v1/v/tracks/hide', [browser.trackConfigsByName[trackInfo.label]]);
+                    }
+                    else if (command == "list") {
+                        var trackList = browser.trackConfigsByName;
+                        var visibleTrackNames = browser.view.visibleTrackNames();
+                        var showLabels = array.map(trackInfo.labels, function (track) {
+                            return track.label;
+                        });
+                        sendTracks(trackList, visibleTrackNames, showLabels);
+                    }
+                    else {
+                        console.error('unknown command: ' + command);
+                    }
+                };
+                browser.subscribe('/jbrowse/v1/c/tracks/show', function (labels) {
+                    console.log("show update");
+                    handleTrackVisibility({command: "list", labels: labels});
+                });
+                browser.subscribe('/jbrowse/v1/c/tracks/hide', function () {
+                    console.log("hide update");
+                    handleTrackVisibility({command: "list"});
+                });
+
+                function handleMessage(event){
+                    var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
+                    var hostUrl = window.location.protocol +"//" + window.location.hostname ;
+
+                    // if non-80 or non-specified
+                    if(window.location.port && window.location.port!= "" && window.location.port != "80"){
+                        hostUrl = hostUrl + ":" + window.location.port;
+                    }
+                    if (origin !== hostUrl){
+                        console.error("Bad Host Origin: "+origin );
+                        return;
+                    }
+
+                    if(event.data.description === "navigateToLocation"){
+                        navigateToLocation(event.data);
+                    }
+                    else
+                    if(event.data.description === "handleTrackVisibility"){
+                        handleTrackVisibility(event.data);
+                    }
+                    else{
+                        console.log("Unknown command: "+event.data.description);
+                    }
+                }
+                window.addEventListener("message",handleMessage,true);
+
+
+                client.connect({}, function () {
+                    // TODO: at some point enable "user" to websockets for chat, private notes, notify @someuser, etc.
+                    var organism = JSON.parse(apolloMainPanel.getCurrentOrganism());
+                    var sequence = JSON.parse(apolloMainPanel.getCurrentSequence());
+                    var user = JSON.parse(apolloMainPanel.getCurrentUser());
+                    client.subscribe("/topic/AnnotationNotification/" + organism.id + "/" + sequence.id, dojo.hitch(track, 'annotationNotification'));
+                    client.subscribe("/topic/AnnotationNotification/user/" + user.email, dojo.hitch(track, 'annotationNotification'));
+                });
+                console.log('connection established');
+            },
+            // TODO: need a better function and to not have it be double-encoded
+            isJSON: function(str) {
+                if(!str) return false ;
+                try{
+                    JSON.parse(str);
+                    return true
+                }
+                catch(e){
+                    return false
+                }
+            },
+            annotationNotification: function (message) {
+                var track = this;
+                var changeData;
+
+                try {
+                    if (track.verbose_server_notification) {
+                        console.log(message.body);
+                    }
+
+                    if(this.isJSON(message.body)){
+                        changeData = JSON.parse(message.body);
+                    }
+                    else{
+                        changeData = {} ;
+                        changeData.operation = 'ERROR';
+                        changeData.username = track.username ;
+                        changeData.error_message = message.body ;
+                    }
+
+                    if (track.verbose_server_notification) {
+                        console.log(changeData.operation + " command from server: ");
+                        console.log(JSON.stringify(changeData));
+                    }
+
+                    if (changeData.operation == "logout" && changeData.username == track.username) {
+                        if(track.getClientToken()!=changeData.clientToken){
+                            track.logout();
+                        }
+                        else{
+                            alert("You have been logged out or your session has expired");
+                            if (this.getApollo()) {
+                                parent.location.reload();
+                            }
+                            else {
+                                location.reload();
+                            }
+                        }
+                        return;
+                    }
+
+                    if (changeData.operation == "ERROR" && changeData.username == track.username) {
+                        var myDialog = new dijit.Dialog({
+                            title: "Error Performing Operation",
+                            content: changeData.error_message,
+                            style: "width: 300px"
+                        }).show();
+                        return;
+                    }
+
+                    if (changeData.operation == "ADD") {
+                        if (changeData.sequenceAlterationEvent) {
+                            track.getSequenceTrack().annotationsAddedNotification(changeData.features);
+                        }
+                        else {
+                            track.annotationsAddedNotification(changeData.features);
+                        }
+                        if (this.runningApollo()) this.getApollo().handleFeatureAdded(JSON.stringify(changeData.features));
+                    }
+                    else if (changeData.operation == "DELETE") {
+                        if (changeData.sequenceAlterationEvent) {
+                            track.getSequenceTrack().annotationsDeletedNotification(changeData.features);
+                        }
+                        else {
+                            track.annotationsDeletedNotification(changeData.features);
+                        }
+                        if (this.runningApollo()) this.getApollo().handleFeatureDeleted(JSON.stringify(changeData.features));
+                    }
+                    else if (changeData.operation == "UPDATE") {
+                        if (changeData.sequenceAlterationEvent) {
+                            track.getSequenceTrack().annotationsUpdatedNotification(changeData.features);
+                        }
+                        else {
+                            track.annotationsUpdatedNotification(changeData.features);
+                        }
+
+                        // changes are not propagated to the selection, so we are doing that here and the re-adding
+                        // see: https://github.com/GMOD/Apollo/issues/645
+                        var selections = track.selectionManager.getSelection();
+                        for(var sin in selections){
+                            // track.selectionRemoved(selections[sin],track.selectionManager);
+                            var selection = selections[sin];
+                            var uniqueId = selection.feature._uniqueID;
+                            for(var featureIndex in changeData.features){
+                                var changedFeature = changeData.features[featureIndex];
+                                // if they are both transcripts
+                                if(changedFeature.uniquename===uniqueId){
+                                    selection.feature.data.strand = changedFeature.location.strand;
+                                }
+                                else
+                                    // if we select an exon, then let's see what happens here
+                                if(!selection.feature.data.parent_type || selection.feature.data.parent_type.indexOf('gene')<0){
+                                    // we want the uniqueId to be the parent
+                                    if(selection.feature._parent._uniqueID==changedFeature.uniquename){
+                                        selection.feature._parent.strand = changedFeature.location.strand ;
+                                        selection.feature._parent.data.strand = changedFeature.location.strand ;
+                                        selection.feature.data.strand = changedFeature.location.strand ;
+                                    }
+                                }
+                            }
+                            track.selectionAdded(selection,track.selectionManager);
+                        }
+
+                        if (this.runningApollo()) this.getApollo().handleFeatureDeleted(JSON.stringify(changeData.features));
+
+
+
+                    }
+                    else {
+                        console.log('unknown command: ', changeData.operation);
+                    }
+
+                    track.changed();
+                } catch (e) {
+                    console.log(e);
+                }
+            },
+
+            /**
+             * received notification from server ChangeNotificationListener that
+             * annotations were added
+             */
+            annotationsAddedNotification: function (responseFeatures) {
+                for (var i = 0; i < responseFeatures.length; ++i) {
+                    var feat = JSONUtils.createJBrowseFeature(responseFeatures[i]);
+                    var id = responseFeatures[i].uniquename;
+                    if (!this.store.getFeatureById(id)) {
+                        this.store.insert(feat);
+                        this.processParent(responseFeatures[i], "ADD");
+                    }
+                }
+            },
+
+            /**
+             * received notification from server ChangeNotificationListener that
+             * annotations were deleted
+             */
+            annotationsDeletedNotification: function (responseFeatures) {
+                for (var i = 0; i < responseFeatures.length; ++i) {
+                    var id_to_delete = responseFeatures[i].uniquename;
+                    this.store.deleteFeatureById(id_to_delete);
+                    this.processParent(responseFeatures[i], "DELETE");
+                }
+            },
+
+            /*
+             * received notification from server ChangeNotificationListener that
+             * annotations were updated currently handled as if receiving DELETE
+             * followed by ADD command
+             */
+            annotationsUpdatedNotification: function (responseFeatures) {
+                // this.annotationsDeletedNotification(annots);
+                // this.annotationsAddedNotification(annots);
+                var selfeats = this.selectionManager.getSelectedFeatures();
+
+                for (var i = 0; i < responseFeatures.length; ++i) {
+                    var id = responseFeatures[i].uniquename;
+                    var feat = JSONUtils.createJBrowseFeature(responseFeatures[i]);
+                    this.store.replace(feat);
+                    this.processParent(responseFeatures[i], "UPDATE");
+                }
+            },
+
+            /**
+             * overriding renderFeature to add event handling right-click context menu
+             */
+            renderFeature: function (feature, uniqueId, block, scale, labelScale, descriptionScale,
+                                     containerStart, containerEnd, history) {
+                // if (uniqueId.length > 20) {
+                //     feature.short_id = uniqueId;
+                // }
+                var track = this;
+                // var featDiv = this.inherited( arguments );
+
+                var rclass;
+                var clsName;
+                var type = feature.afeature.type;
+                if (!this.isProteinCoding(feature)) {
+                    var topLevelAnnotation = AnnotTrack.getTopLevelAnnotation(feature);
+                    var parentType = feature.afeature.parent_type ? feature.afeature.parent_type.name : null;
+                    var cfg = this.config.style.alternateClasses[feature.get("type")] || this.config.style.alternateClasses[parentType];
+                    if (cfg) {
+                        rclass = cfg.renderClassName;
+                        if (!topLevelAnnotation.afeature.parent_type) {
+                            clsName = cfg.className;
+                        }
+                    }
+                }
+                var featDiv = DraggableFeatureTrack.prototype.renderFeature.call(this, feature, uniqueId, block, scale, labelScale, descriptionScale, containerStart, containerEnd, rclass, clsName);
+
+                if (featDiv && featDiv != null && !history) {
+                    annot_context_menu.bindDomNode(featDiv);
+                    $(featDiv).droppable({
+                            accept: ".selected-feature",   // only accept draggables that
+                            // are selected feature divs
+                            tolerance: "pointer",
+                            hoverClass: "annot-drop-hover",
+                            over: function (event, ui) {
+                                track.annot_under_mouse = event.target;
+                            },
+                            out: function (event, ui) {
+                                track.annot_under_mouse = null;
+                            },
+                            drop: function (event, ui) {
+                                // ideally in the drop() on annot div is where would handle
+                                // adding feature(s) to annot,
+                                // but JQueryUI droppable doesn't actually call drop unless
+                                // draggable helper div is actually
+                                // over the droppable -- even if tolerance is set to pointer
+                                // tolerance=pointer will trigger hover styling when over
+                                // droppable,
+                                // as well as call to over method (and out when leave
+                                // droppable)
+                                // BUT location of pointer still does not influence actual
+                                // dropping and drop() call
+                                // therefore getting around this by handling hover styling
+                                // here based on pointer over annot,
+                                // but drop-to-add part is handled by whole-track droppable,
+                                // and uses annot_under_mouse
+                                // tracking variable to determine if drop was actually on
+                                // top of an annot instead of
+                                // track whitespace
+                                if (track.verbose_drop) {
+                                    console.log("dropped feature on annot:");
+                                    console.log(featDiv);
+                                }
+                            }
+                        })
+                        .click(function (event) {
+                            if (event.altKey) {
+                                track.getAnnotationInfoEditor();
+                            }
+                        })
+                    ;
+                }
+
+                if (!history) {
+                    var label = "Type: " + type.name + "<br/>Owner: " + feature.afeature.owner + "<br/>Last modified: " + FormatUtils.formatDate(feature.afeature.date_last_modified) + " " + FormatUtils.formatTime(feature.afeature.date_last_modified);
+                    new Tooltip({
+                        connectId: featDiv,
+                        label: label,
+                        position: ["above"],
+                        showDelay: 600
+                    });
+                }
+
+                if (feature.get("locked")) {
+                    dojo.addClass(featDiv, "locked-annotation");
+                }
+
+                return featDiv;
+            },
+
+            renderSubfeature: function (feature, featDiv, subfeature,
+                                        displayStart, displayEnd, block) {
+                var subdiv = this.inherited(arguments);
+
+                if (this.canEdit(feature)) {
+                    /**
+                     * setting up annotation resizing via pulling of left/right edges but if
+                     * subfeature is not selectable, do not bind mouse down
+                     */
+                    if (subdiv && subdiv != null && (!this.selectionManager.unselectableTypes[subfeature.get('type')])) {
+                        $(subdiv).bind("mousedown", this.annotMouseDown);
+                    }
+                }
+
+                return subdiv;
+            },
+
+            /**
+             * get the GenomeView's sequence track -- maybe move this to GenomeView?
+             * WebApollo assumes there is only one SequenceTrack if there are multiple
+             * SequenceTracks, getSequenceTrack returns first one found iterating
+             * through tracks list
+             */
+            getSequenceTrack: function () {
+                if (this.seqTrack) {
+                    return this.seqTrack;
+                }
+                else {
+                    var tracks = this.gview.tracks;
+                    for (var i = 0; i < tracks.length; i++) {
+                        // if (tracks[i] instanceof SequenceTrack) {
+                        // if (tracks[i].config.type == "WebApollo/View/Track/AnnotSequenceTrack") {
+                        if (tracks[i].isWebApolloSequenceTrack) {
+                            this.seqTrack = tracks[i];
+                            // tracks[i].setAnnotTrack(this);
+                            break;
+                        }
+                    }
+                }
+                return this.seqTrack;
+            },
+
+            onFeatureMouseDown: function (event) {
+
+                // _not_ calling DraggableFeatureTrack.prototyp.onFeatureMouseDown --
+                // don't want to allow dragging (at least not yet)
+                // event.stopPropagation();
+                this.last_mousedown_event = event;
+                var ftrack = this;
+                if (ftrack.verbose_selection || ftrack.verbose_drag) {
+                    console.log("AnnotTrack.onFeatureMouseDown called, genome coord: " + this.getGenomeCoord(event));
+                }
+                this.handleFeatureSelection(event);
+            },
+
+            /**
+             * handles mouse down on an annotation subfeature to make the annotation
+             * resizable by pulling the left/right edges
+             */
+            onAnnotMouseDown: function (event) {
+                var track = this;
+                // track.last_mousedown_event = event;
+                var verbose_resize = track.verbose_resize;
+                if (verbose_resize || track.verbose_mousedown) {
+                    console.log("AnnotTrack.onAnnotMouseDown called");
+                }
+                event = event || window.event;
+                var elem = (event.currentTarget || event.srcElement);
+                // need to redo getLowestFeatureDiv
+                // var featdiv = DraggableFeatureTrack.prototype.getLowestFeatureDiv(elem);
+                var featdiv = track.getLowestFeatureDiv(elem);
+
+                this.currentResizableFeature = featdiv.subfeature;
+                this.makeResizable(featdiv);
+                event.stopPropagation();
+            },
+
+            makeResizable: function (featdiv) {
+                var track = this;
+                var verbose_resize = this.verbose_resize;
+                if (featdiv && (featdiv != null)) {
+                    if (dojo.hasClass(featdiv, "ui-resizable")) {
+                        if (verbose_resize) {
+                            console.log("already resizable");
+                            console.log(featdiv);
+                        }
+                    }
+                    else {
+                        if (verbose_resize) {
+                            console.log("making annotation resizable");
+                            console.log(featdiv);
+                        }
+                        var scale = track.gview.bpToPx(1);
+
+                        // if zoomed int to showing sequence residues, then make
+                        // edge-dragging snap to interbase pixels
+                        var gridvals;
+                        var charSize = track.webapollo.getSequenceCharacterSize();
+                        if (scale === charSize.width) {
+                            gridvals = [track.gview.charWidth, 1];
+                        }
+                        else {
+                            gridvals = false;
+                        }
+                        $(featdiv).resizable({
+                            handles: "e, w",
+                            helper: "ui-resizable-helper",
+                            autohide: false,
+                            grid: gridvals,
+
+                            stop: function (event, ui) {
+                                if (verbose_resize) {
+                                    console.log("resizable.stop() called, event:");
+                                    console.dir(event);
+                                    console.log("ui:");
+                                    console.dir(ui);
+                                }
+                                var gview = track.gview;
+                                var oldPos = ui.originalPosition;
+                                var newPos = ui.position;
+                                var oldSize = ui.originalSize;
+                                var newSize = ui.size;
+                                var leftDeltaPixels = newPos.left - oldPos.left;
+                                var leftDeltaBases = Math.round(gview.pxToBp(leftDeltaPixels));
+                                var oldRightEdge = oldPos.left + oldSize.width;
+                                var newRightEdge = newPos.left + newSize.width;
+                                var rightDeltaPixels = newRightEdge - oldRightEdge;
+                                var rightDeltaBases = Math.round(gview.pxToBp(rightDeltaPixels));
+                                if (verbose_resize) {
+                                    console.log("left edge delta pixels: " + leftDeltaPixels);
+                                    console.log("left edge delta bases: " + leftDeltaBases);
+                                    console.log("right edge delta pixels: " + rightDeltaPixels);
+                                    console.log("right edge delta bases: " + rightDeltaBases);
+                                }
+                                var subfeat = ui.originalElement[0].subfeature;
+                                var fmin = subfeat.get('start') + leftDeltaBases;
+                                var fmax = subfeat.get('end') + rightDeltaBases;
+                                var operation = subfeat.get("type") == "exon" ? "set_exon_boundaries" : "set_boundaries";
+                                var postData = {
+                                    "track": track.getUniqueTrackName(),
+                                    "features": [
+                                        {
+                                            "uniquename": subfeat.getUniqueName(),
+                                            "location": {
+                                                "fmin": fmin, "fmax": fmax
+                                            }
+                                        }
+                                    ],
+                                    "operation": operation
+                                };
+                                track.executeUpdateOperation(JSON.stringify(postData));
+                                track.changed();
+                            }
+                        });
+                    }
+                }
+            },
+
+            /**
+             * feature click no-op (to override FeatureTrack.onFeatureClick, which
+             * conflicts with mouse-down selection
+             */
+            onFeatureClick: function (event) {
+
+                if (this.verbose_click) {
+                    console.log("in AnnotTrack.onFeatureClick");
+                }
+                event = event || window.event;
+                var elem = (event.currentTarget || event.srcElement);
+                var featdiv = this.getLowestFeatureDiv(elem);
+                if (featdiv && (featdiv != null)) {
+                    if (this.verbose_click) {
+                        console.log(featdiv);
+                    }
+                }
+                // do nothing
+                // event.stopPropagation();
+            },
+
+            /* feature_records ==> { feature: the_feature, track: track_feature_is_from } */
+            addToAnnotation: function (annot, feature_records) {
+                var target_track = this;
+
+                var subfeats = [];
+                var allSameStrand = 1;
+                for (var i = 0; i < feature_records.length; ++i) {
+                    var feature_record = feature_records[i];
+                    var original_feat = feature_record.feature;
+                    var feat = JSONUtils.makeSimpleFeature(original_feat);
+                    var isSubfeature = !!feat.parent();  // !! is
+                    // shorthand for
+                    // returning
+                    // true if value
+                    // is defined
+                    // and non-null
+                    var annotStrand = annot.get('strand');
+                    if (isSubfeature) {
+                        var featStrand = feat.get('strand');
+                        var featToAdd = feat;
+                        if (featStrand != annotStrand) {
+                            allSameStrand = 0;
+                            featToAdd.set('strand', annotStrand);
+                        }
+                        subfeats.push(featToAdd);
+                    }
+                    else {  // top-level feature
+                        var source_track = feature_record.track;
+                        var subs = feat.get('subfeatures');
+                        if (subs && subs.length > 0) {  // top-level
+                            // feature with
+                            // subfeatures
+                            for (var i = 0; i < subs.length; ++i) {
+                                var subfeat = subs[i];
+                                var featStrand = subfeat.get('strand');
+                                var featToAdd = subfeat;
+                                if (featStrand != annotStrand) {
+                                    allSameStrand = 0;
+                                    featToAdd.set('strand', annotStrand);
+                                }
+                                subfeats.push(featToAdd);
+                            }
+                            // $.merge(subfeats, subs);
+                        }
+                        else {  // top-level feature without subfeatures
+                            // make exon feature
+                            var featStrand = feat.get('strand');
+                            var featToAdd = feat;
+                            if (featStrand != annotStrand) {
+                                allSameStrand = 0;
+                                featToAdd.set('strand', annotStrand);
+                            }
+                            featToAdd.set('type', 'exon');
+                            subfeats.push(featToAdd);
+                        }
+                    }
+                }
+
+                if (!allSameStrand && !confirm("Adding features of opposite strand.  Continue?")) {
+                    return;
+                }
+
+                var featuresString = "";
+                for (var i = 0; i < subfeats.length; ++i) {
+                    var subfeat = subfeats[i];
+                    // if (subfeat[target_track.subFields["type"]] !=
+                    // "wholeCDS")
+                    var source_track = subfeat.track;
+                    if (subfeat.get('type') != "wholeCDS") {
+                        var jsonFeature = JSONUtils.createApolloFeature(subfeats[i], "exon");
+                        featuresString += ", " + JSON.stringify(jsonFeature);
+                    }
+                }
+                // var parent = JSONUtils.createApolloFeature(annot, target_track.fields,
+                // target_track.subfields);
+                // parent.uniquename = annot[target_track.fields["name"]];
+                var postData = '{ "track": "' + target_track.getUniqueTrackName() + '", "features": [ {"uniquename": "' + annot.id() + '"}' + featuresString + '], "operation": "add_exon" }';
+                target_track.executeUpdateOperation(postData);
+            },
+
+            makeTrackDroppable: function () {
+                var target_track = this;
+                var target_trackdiv = target_track.div;
+                if (target_track.verbose_drop) {
+                    console.log("making track a droppable target: ");
+                    console.log(this);
+                    console.log(target_trackdiv);
+                }
+                $(target_trackdiv).droppable({
+                    // only accept draggables that are selected feature divs
+                    accept: ".selected-feature",
+                    // switched to using deactivate() rather than drop() for drop
+                    // handling
+                    // this fixes bug where drop targets within track (feature divs)
+                    // were lighting up as drop target,
+                    // but dropping didn't actually call track.droppable.drop()
+                    // (see explanation in feature droppable for why we catch drop at
+                    // track div rather than feature div child)
+                    // cause is possible bug in JQuery droppable where droppable over(),
+                    // drop() and hoverclass
+                    // collision calcs may be off (at least when tolerance=pointer)?
+                    //
+                    // Update 3/2012
+                    // deactivate behavior changed? Now getting called every time
+                    // dragged features are release,
+                    // regardless of whether they are over this track or not
+                    // so added another hack to get around drop problem
+                    // combination of deactivate and keeping track via over()/out() of
+                    // whether drag is above this track when released
+                    // really need to look into actual drop calc fix -- maybe fixed in
+                    // new JQuery releases?
+                    //
+                    // drop: function(event, ui) {
+                    over: function (event, ui) {
+                        target_track.track_under_mouse_drag = true;
+                        if (target_track.verbose_drop) {
+                            console.log("droppable entered AnnotTrack")
+                        }
+                        ;
+                    },
+                    out: function (event, ui) {
+                        target_track.track_under_mouse_drag = false;
+                        if (target_track.verbose_drop) {
+                            console.log("droppable exited AnnotTrack")
+                        }
+                        ;
+                    },
+                    deactivate: function (event, ui) {
+                        // console.log("trackdiv droppable detected: draggable
+                        // deactivated");
+                        // "this" is the div being dropped on, so same as
+                        // target_trackdiv
+                        if (target_track.verbose_drop) {
+                            console.log("draggable deactivated");
+                        }
+
+                        var dropped_feats = target_track.webapollo.featSelectionManager.getSelection();
+                        // problem with making individual annotations droppable, so
+                        // checking for "drop" on annotation here,
+                        // and if so re-routing to add to existing annotation
+                        if (target_track.annot_under_mouse != null) {
+                            if (target_track.verbose_drop) {
+                                console.log("draggable dropped onto annot: ");
+                                console.log(target_track.annot_under_mouse.feature);
+                            }
+                            target_track.addToAnnotation(target_track.annot_under_mouse.feature, dropped_feats);
+                        }
+                        else if (target_track.track_under_mouse_drag) {
+                            if (target_track.verbose_drop) {
+                                console.log("draggable dropped on AnnotTrack");
+                            }
+                            target_track.createAnnotations(dropped_feats);
+                        }
+                        // making sure annot_under_mouse is cleared
+                        // (should do this in the drop? but need to make sure _not_ null
+                        // when
+                        target_track.annot_under_mouse = null;
+                        target_track.track_under_mouse_drag = false;
+                    }
+                });
+                if (target_track.verbose_drop) {
+                    console.log("finished making droppable target");
+                }
+            },
+
+            createAnnotations: function (selection_records) {
+                console.log('createAnnotations');
+                var target_track = this;
+                var featuresToAdd = new Array();
+                var parentFeatures = new Object();
+                var subfeatures = [];
+                var strand;
+                var parentFeature;
+                for (var i in selection_records) {
+                    console.log('selection ' + i);
+                    var dragfeat = selection_records[i].feature;
+                    var is_subfeature = !!dragfeat.parent();  // !! is shorthand
+                    // for returning
+                    // true if value is
+                    // defined and
+                    // non-null
+
+                    var parent = is_subfeature ? dragfeat.parent() : dragfeat;
+                    var parentId = parent.id();
+                    parentFeatures[parentId] = parent;
+
+                    if (strand == undefined) {
+                        strand = dragfeat.get("strand");
+                    }
+                    else if (strand != dragfeat.get("strand")) {
+                        strand = -2;
+                    }
+
+                    if (is_subfeature) {
+                        subfeatures.push(dragfeat);
+                        if (!parentFeature) {
+                            parentFeature = dragfeat.parent();
+                        }
+                    }
+                    else {
+                        var children = dragfeat.get("subfeatures");
+                        if (children) {
+                            for (var j = 0; j < children.length; ++j) {
+                                subfeatures.push(children[j]);
+                            }
+                        }
+                        else {
+                            subfeatures.push(dragfeat);
+                        }
+
+                        if (!parentFeature) {
+                            parentFeature = dragfeat;
+                        }
+                    }
+                }
+
+                function process() {
+                    var keys = Object.keys(parentFeatures);
+                    var singleParent = keys.length == 1;
+                    var featureToAdd;
+                    if (singleParent) {
+                        featureToAdd = JSONUtils.makeSimpleFeature(parentFeatures[keys[0]]);
+                    }
+                    else {
+                        featureToAdd = new SimpleFeature({data: {strand: strand}});
+                    }
+                    if (!featureToAdd.get('name')) {
+                        featureToAdd.set('name', featureToAdd.get('id'));
+                    }
+                    featureToAdd.set("strand", strand);
+                    var fmin;
+                    var fmax;
+                    featureToAdd.set('subfeatures', new Array());
+                    array.forEach(subfeatures, function (subfeature) {
+                        if (!singleParent && SequenceOntologyUtils.cdsTerms[subfeature.get("type")]) {
+                            return;
+                        }
+                        var dragfeat = JSONUtils.makeSimpleFeature(subfeature);
+                        dragfeat.set("strand", strand);
+                        var childFmin = dragfeat.get('start');
+                        var childFmax = dragfeat.get('end');
+                        if (fmin === undefined || childFmin < fmin) {
+                            fmin = childFmin;
+                        }
+                        if (fmax === undefined || childFmax > fmax) {
+                            fmax = childFmax;
+                        }
+                        featureToAdd.get("subfeatures").push(dragfeat);
+                    });
+                    if (!subfeatures.length) {
+                        featureToAdd = new SimpleFeature({
+                            data: {
+                                strand: strand,
+                                start: featureToAdd.get('start'),
+                                end: featureToAdd.get('end'),
+                                subfeatures: [featureToAdd]
+                            }
+                        });
+                    }
+
+                    if (fmin) featureToAdd.set("start", fmin);
+                    if (fmax) featureToAdd.set("end", fmax);
+                    var afeat = JSONUtils.createApolloFeature(featureToAdd, "mRNA", true);
+                    featuresToAdd.push(afeat);
+
+                    var postData = {
+                        "track": target_track.getUniqueTrackName(),
+                        "features": featuresToAdd,
+                        "operation": "add_transcript"
+                    };
+                    target_track.executeUpdateOperation(JSON.stringify(postData));
+                };
+
+                console.log('process: ' + strand);
+                if (strand == -2) {
+                    var content = dojo.create("div");
+                    var message = dojo.create("div", {
+                        className: "confirm_message",
+                        innerHTML: "Creating annotation with subfeatures in opposing strands.  Choose strand:"
+                    }, content);
+                    var buttonsDiv = dojo.create("div", {className: "confirm_buttons"}, content);
+                    var plusButton = dojo.create("button", {
+                        className: "confirm_button",
+                        innerHTML: "Plus"
+                    }, buttonsDiv);
+                    var minusButton = dojo.create("button", {
+                        className: "confirm_button",
+                        innerHTML: "Minus"
+                    }, buttonsDiv);
+                    var cancelButton = dojo.create("button", {
+                        className: "confirm_button",
+                        innerHTML: "Cancel"
+                    }, buttonsDiv);
+                    dojo.connect(plusButton, "onclick", function () {
+                        strand = 1;
+                        target_track.closeDialog();
+                    });
+                    dojo.connect(minusButton, "onclick", function () {
+                        strand = -1;
+                        target_track.closeDialog();
+                    });
+                    dojo.connect(cancelButton, "onclick", function () {
+                        target_track.closeDialog();
+                    });
+                    var handle = dojo.connect(AnnotTrack.popupDialog, "onHide", function () {
+                        dojo.disconnect(handle);
+                        if (strand != -2) {
+                            process();
+                        }
+                    });
+                    this.openDialog("Confirm", content);
+                    return;
+                }
+                else {
+                    process();
+                }
+
+            },
+
+            createGenericAnnotations: function (feats, type, subfeatType, topLevelType) {
+                var target_track = this;
+                var featuresToAdd = new Array();
+                var parentFeatures = new Object();
+                for (var i in feats) {
+                    var dragfeat = feats[i];
+
+                    var is_subfeature = !!dragfeat.parent();  // !! is shorthand
+                    // for returning
+                    // true if value is
+                    // defined and
+                    // non-null
+                    var parentId = is_subfeature ? dragfeat.parent().id() : dragfeat.id();
+
+                    if (parentFeatures[parentId] === undefined) {
+                        parentFeatures[parentId] = new Array();
+                        parentFeatures[parentId].isSubfeature = is_subfeature;
+                    }
+                    parentFeatures[parentId].push(dragfeat);
+                }
+
+                for (var i in parentFeatures) {
+                    var featArray = parentFeatures[i];
+                    if (featArray.isSubfeature) {
+                        var parentFeature = featArray[0].parent();
+                        var fmin;
+                        var fmax;
+                        // var featureToAdd = $.extend({}, parentFeature);
+                        var featureToAdd = JSONUtils.makeSimpleFeature(parentFeature);
+                        featureToAdd.set('subfeatures', new Array());
+                        for (var k = 0; k < featArray.length; ++k) {
+                            // var dragfeat = featArray[k];
+                            var dragfeat = JSONUtils.makeSimpleFeature(featArray[k]);
+                            var childFmin = dragfeat.get('start');
+                            var childFmax = dragfeat.get('end');
+                            if (fmin === undefined || childFmin < fmin) {
+                                fmin = childFmin;
+                            }
+                            if (fmax === undefined || childFmax > fmax) {
+                                fmax = childFmax;
+                            }
+                            featureToAdd.get("subfeatures").push(dragfeat);
+                        }
+                        featureToAdd.set("start", fmin);
+                        featureToAdd.set("end", fmax);
+                        var afeat = JSONUtils.createApolloFeature(featureToAdd, type, true, subfeatType);
+                        if (topLevelType) {
+                            var topLevel = new Object();
+                            topLevel.location = dojo.clone(afeat.location);
+                            topLevel.type = dojo.clone(afeat.type);
+                            topLevel.type.name = topLevelType;
+                            topLevel.children = new Array();
+                            topLevel.children.push(afeat);
+                            afeat = topLevel;
+                        }
+                        featuresToAdd.push(afeat);
+                    }
+                    else {
+                        for (var k = 0; k < featArray.length; ++k) {
+                            var dragfeat = featArray[k];
+                            var afeat = JSONUtils.createApolloFeature(dragfeat, type, true, subfeatType);
+                            if (topLevelType) {
+                                var topLevel = new Object();
+                                topLevel.location = dojo.clone(afeat.location);
+                                topLevel.type = dojo.clone(afeat.type);
+                                topLevel.type.name = topLevelType;
+                                topLevel.children = new Array();
+                                topLevel.children.push(afeat);
+                                afeat = topLevel;
+                            }
+                            featuresToAdd.push(afeat);
+                        }
+                    }
+                }
+                var postData = '{ "track": "' + target_track.getUniqueTrackName() + '", "features": ' + JSON.stringify(featuresToAdd) + ', "operation": "add_feature" }';
+                target_track.executeUpdateOperation(postData);
+            },
+
+            createGenericOneLevelAnnotations: function (feats, type, strandless) {
+                var target_track = this;
+                var featuresToAdd = new Array();
+                var parentFeatures = new Object();
+                for (var i in feats) {
+                    var dragfeat = feats[i];
+
+                    var is_subfeature = !!dragfeat.parent();  // !! is shorthand
+                    // for returning
+                    // true if value is
+                    // defined and
+                    // non-null
+                    var parentId = is_subfeature ? dragfeat.parent().id() : dragfeat.id();
+
+                    if (parentFeatures[parentId] === undefined) {
+                        parentFeatures[parentId] = new Array();
+                        parentFeatures[parentId].isSubfeature = is_subfeature;
+                    }
+                    parentFeatures[parentId].push(dragfeat);
+                }
+
+                for (var i in parentFeatures) {
+                    var featArray = parentFeatures[i];
+                    if (featArray.isSubfeature) {
+                        var parentFeature = featArray[0].parent();
+                        var fmin;
+                        var fmax;
+                        // var featureToAdd = $.extend({}, parentFeature);
+                        var featureToAdd = JSONUtils.makeSimpleFeature(parentFeature);
+                        featureToAdd.set('subfeatures', new Array());
+                        for (var k = 0; k < featArray.length; ++k) {
+                            // var dragfeat = featArray[k];
+                            var dragfeat = JSONUtils.makeSimpleFeature(featArray[k]);
+                            var childFmin = dragfeat.get('start');
+                            var childFmax = dragfeat.get('end');
+                            if (fmin === undefined || childFmin < fmin) {
+                                fmin = childFmin;
+                            }
+                            if (fmax === undefined || childFmax > fmax) {
+                                fmax = childFmax;
+                            }
+                            // featureToAdd.get("subfeatures").push( dragfeat );
+                        }
+                        featureToAdd.set("start", fmin);
+                        featureToAdd.set("end", fmax);
+                        if (strandless) {
+                            featureToAdd.set("strand", 0);
+                        }
+                        var afeat = JSONUtils.createApolloFeature(featureToAdd, type, true);
+                        /*
+                         * if (topLevelType) { var topLevel = new Object();
+                         * topLevel.location = dojo.clone(afeat.location);
+                         * topLevel.type = dojo.clone(afeat.type);
+                         * topLevel.type.name = topLevelType; topLevel.children =
+                         * new Array(); topLevel.children.push(afeat); afeat =
+                         * topLevel; }
+                         */
+                        featuresToAdd.push(afeat);
+                    }
+                    else {
+                        for (var k = 0; k < featArray.length; ++k) {
+                            var dragfeat = JSONUtils.makeSimpleFeature(featArray[k]);
+                            var subfeat = dragfeat.get("subfeatures");
+                            if (subfeat) subfeat.length = 0; // clear
+                            // subfeatures
+                            if (strandless) {
+                                dragfeat.set("strand", 0);
+                            }
+                            dragfeat.set("name", featArray[k].get("name"));
+                            var afeat = JSONUtils.createApolloFeature(dragfeat, type, true);
+                            /*
+                             * if (topLevelType) { var topLevel = new
+                             * Object(); topLevel.location =
+                             * dojo.clone(afeat.location); topLevel.type =
+                             * dojo.clone(afeat.type); topLevel.type.name =
+                             * topLevelType; topLevel.children = new
+                             * Array(); topLevel.children.push(afeat); afeat =
+                             * topLevel; }
+                             */
+                            featuresToAdd.push(afeat);
+                        }
+                    }
+                }
+                var postData = '{ "track": "' + target_track.getUniqueTrackName() + '", "features": ' + JSON.stringify(featuresToAdd) + ', "operation": "add_feature" }';
+                target_track.executeUpdateOperation(postData);
+            },
+
+            duplicateSelectedFeatures: function () {
+                var selected = this.selectionManager.getSelection();
+                var selfeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                this.duplicateAnnotations(selfeats);
+            },
+
+            duplicateAnnotations: function (feats) {
+                var track = this;
+                var featuresToAdd = new Array();
+                var subfeaturesToAdd = new Array();
+                var proteinCoding = false;
+                for (var i in feats) {
+                    var feat = feats[i];
+                    var is_subfeature = !!feat.parent();  // !! is shorthand
+                                                          // for returning
+                                                          // true if value is
+                                                          // defined and
+                                                          // non-null
+                    if (is_subfeature) {
+                        subfeaturesToAdd.push(feat);
+                    }
+                    else {
+                        // featuresToAdd.push( JSONUtils.createApolloFeature( feat, "transcript") );
+                        featuresToAdd.push(feat);
+                    }
+                }
+                if (subfeaturesToAdd.length > 0) {
+                    var feature = new SimpleFeature();
+                    var subfeatures = new Array();
+                    feature.set('subfeatures', subfeatures);
+                    var fmin;
+                    var fmax;
+                    var strand;
+                    for (var i = 0; i < subfeaturesToAdd.length; ++i) {
+                        var subfeature = subfeaturesToAdd[i];
+                        if (fmin === undefined || subfeature.get('start') < fmin) {
+                            fmin = subfeature.get('start');
+                        }
+                        if (fmax === undefined || subfeature.get('end') > fmax) {
+                            fmax = subfeature.get('end');
+                        }
+                        if (strand === undefined) {
+                            strand = subfeature.get('strand');
+                        }
+                        subfeatures.push(subfeature);
+                    }
+                    feature.set('start', fmin);
+                    feature.set('end', fmax);
+                    feature.set('strand', strand);
+                    feature.set('type', subfeaturesToAdd[0].parent().get('type'));
+                    feature.afeature = subfeaturesToAdd[0].parent().afeature;
+                    // featuresToAdd.push( JSONUtils.createApolloFeature( feature, "transcript") );
+                    featuresToAdd.push(feature);
+                }
+                for (var i = 0; i < featuresToAdd.length; ++i) {
+                    var feature = featuresToAdd[i];
+                    if (this.isProteinCoding(feature)) {
+                        var feats = [JSONUtils.createApolloFeature(feature, "mRNA")];
+                        var postData = '{ "track": "' + track.getUniqueTrackName() + '", "features": ' + JSON.stringify(feats) + ', "operation": "add_transcript" }';
+                        track.executeUpdateOperation(postData);
+                    }
+                    else if (feature.afeature.parent_id) {
+                        var feats = [feature];
+                        this.createGenericAnnotations([feature], feature.get("type"), feature.get("subfeatures")[0].get("type"), feature.afeature.parent_type.name);
+                    }
+                    else {
+                        if (!feature.get("name")) {
+                            feature.set("name", feature.afeature.name);
+                        }
+                        var feats = [feature];
+                        this.createGenericOneLevelAnnotations([feature], feature.get("type"), feature.get("strand") == 0);
+                    }
+                }
+            },
+
+            /**
+             * If there are multiple AnnotTracks, each has a separate
+             * FeatureSelectionManager (contrasted with DraggableFeatureTracks, which
+             * all share the same selection and selection manager
+             */
+            deleteSelectedFeatures: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.deleteAnnotations(selected);
+            },
+
+            getApollo: function(){
+                return window.parent;
+            },
+
+            runningApollo: function(){
+                return (this.getApollo() && typeof this.getApollo().getEmbeddedVersion == 'function' && this.getApollo().getEmbeddedVersion() == 'ApolloGwt-2.0') ;
+            },
+
+            deleteAnnotations: function (records) {
+                var track = this;
+                var features = '"features": [';
+                var uniqueNames = [];
+                var parents = {};
+                var toBeDeleted = [];
+                for (var i in records) {
+                    var record = records[i];
+                    var selfeat = record.feature;
+                    var seltrack = record.track;
+                    var uniqueName = selfeat.getUniqueName();
+                    // just checking to ensure that all features in selection are from
+                    // this track --
+                    // if not, then don't try and delete them
+                    if (seltrack === track) {
+                        var trackdiv = track.div;
+                        var trackName = track.getUniqueTrackName();
+                        if (!selfeat.parent()) {
+                            track.confirmDeleteAnnotations(track, [selfeat], "Deleting feature " + selfeat.get("name") + " cannot be undone.  Are you sure you want to delete?");
+                        }
+                        else {
+                            var children = parents[selfeat.parent().id()] || (parents[selfeat.parent().id()] = []);
+                            children.push(selfeat);
+                        }
+                    }
+                }
+                for (var id in parents) {
+                    var children = parents[id];
+                    if (SequenceOntologyUtils.exonTerms[children[0].get("type")]) {
+                        var numExons = 0;
+                        var subfeatures = children[0].parent().get("subfeatures");
+                        for (var i = 0; i < subfeatures.length; ++i) {
+                            if (SequenceOntologyUtils.exonTerms[subfeatures[i].get("type")]) {
+                                ++numExons;
+                            }
+                        }
+                        if (numExons == children.length) {
+                            track.confirmDeleteAnnotations(track, [children[0]], "Deleting feature " + children[0].parent().get("name") + " cannot be undone.  Are you sure you want to delete?");
+                            continue;
+                        }
+                    }
+                    else if (children.length == children[0].parent().get("subfeatures").length) {
+                        track.confirmDeleteAnnotations(track, [children[0]], "Deleting feature " + children[0].parent().get("name") + " cannot be undone.  Are you sure you want to delete?");
+                        continue;
+                    }
+                    for (var i = 0; i < children.length; ++i) {
+                        toBeDeleted.push(children[i].getUniqueName());
+                    }
+                }
+                for (var i = 0; i < toBeDeleted.length; ++i) {
+                    if (i > 0) {
+                        features += ',';
+                    }
+                    features += ' { "uniquename": "' + toBeDeleted[i] + '" } ';
+                    uniqueNames.push(toBeDeleted[i]);
+                }
+                features += ']';
+                if (uniqueNames.length == 0) {
+                    return;
+                }
+                if (this.verbose_delete) {
+                    console.log("annotations to delete:");
+                    console.log(features);
+                }
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "delete_feature" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            confirmDeleteAnnotations: function (track, selectedFeatures, message) {
+                var confirm = new ConfirmDialog({
+                    title: 'Delete feature',
+                    message: message,
+                    confirmLabel: 'Yes',
+                    denyLabel: 'Cancel'
+                }).show(function (confirmed) {
+                    if (confirmed) {
+                        var postData = {};
+                        postData.track = track.getUniqueTrackName();
+                        postData.operation = "delete_feature";
+                        postData.features = [];
+                        for (var i = 0; i < selectedFeatures.length; i++) {
+                            postData.features[i] = {uniquename: selectedFeatures[i].getUniqueName()};
+                        }
+                        track.executeUpdateOperation(JSON.stringify(postData));
+                    }
+                });
+            },
+
+            mergeSelectedFeatures: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.mergeAnnotations(selected);
+            },
+
+            mergeAnnotations: function (selection) {
+                var track = this;
+                var annots = [];
+                for (var i = 0; i < selection.length; i++) {
+                    annots[i] = selection[i].feature;
+                }
+
+                var sortedAnnots = track.sortAnnotationsByLocation(annots);
+                var leftAnnot = sortedAnnots[0];
+                var rightAnnot = sortedAnnots[sortedAnnots.length - 1];
+                var trackName = this.getUniqueTrackName();
+
+                /*
+                 * for (var i in annots) { var annot = annots[i]; // just checking to
+                 * ensure that all features in selection are from this track -- // if
+                 * not, then don't try and delete them if (annot.track === track) { var
+                 * trackName = track.getUniqueTrackName(); if (leftAnnot == null ||
+                 * annot[track.fields["start"]] < leftAnnot[track.fields["start"]]) {
+                 * leftAnnot = annot; } if (rightAnnot == null ||
+                 * annot[track.fields["end"]] > rightAnnot[track.fields["end"]]) {
+                 * rightAnnot = annot; } } }
+                 */
+
+                var features;
+                var operation;
+                // merge exons
+                if (leftAnnot.parent() && rightAnnot.parent() && leftAnnot.parent() == rightAnnot.parent()) {
+                    features = '"features": [ { "uniquename": "' + leftAnnot.id() + '" }, { "uniquename": "' + rightAnnot.id() + '" } ]';
+                    operation = "merge_exons";
+                }
+                // merge transcripts
+                else {
+                    var leftTranscriptId = leftAnnot.parent() ? leftAnnot.parent().id() : leftAnnot.id();
+                    var rightTranscriptId = rightAnnot.parent() ? rightAnnot.parent().id() : rightAnnot.id();
+                    features = '"features": [ { "uniquename": "' + leftTranscriptId + '" }, { "uniquename": "' + rightTranscriptId + '" } ]';
+                    operation = "merge_transcripts";
+                }
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            splitSelectedFeatures: function (event) {
+                // var selected = this.selectionManager.getSelection();
+                var selected = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                this.splitAnnotations(selected, event);
+            },
+
+            splitAnnotations: function (annots, event) {
+                // can only split on max two elements
+                if (annots.length > 2) {
+                    return;
+                }
+                var track = this;
+                var sortedAnnots = track.sortAnnotationsByLocation(annots);
+                var leftAnnot = sortedAnnots[0];
+                var rightAnnot = sortedAnnots[sortedAnnots.length - 1];
+                var trackName = track.getUniqueTrackName();
+
+                /*
+                 * for (var i in annots) { var annot = annots[i]; // just checking to
+                 * ensure that all features in selection are from this track -- // if
+                 * not, then don't try and delete them if (annot.track === track) { var
+                 * trackName = track.getUniqueTrackName(); if (leftAnnot == null ||
+                 * annot[track.fields["start"]] < leftAnnot[track.fields["start"]]) {
+                 * leftAnnot = annot; } if (rightAnnot == null ||
+                 * annot[track.fields["end"]] > rightAnnot[track.fields["end"]]) {
+                 * rightAnnot = annot; } } }
+                 */
+                var features;
+                var operation;
+                // split exon
+                if (leftAnnot == rightAnnot) {
+                    var coordinate = this.getGenomeCoord(event);
+                    features = '"features": [ { "uniquename": "' + leftAnnot.id() + '", "location": { "fmax": ' + coordinate + ', "fmin": ' + (coordinate + 1) + ' } } ]';
+                    operation = "split_exon";
+                }
+                // split transcript
+                else if (leftAnnot.parent() == rightAnnot.parent()) {
+                    features = '"features": [ { "uniquename": "' + leftAnnot.id() + '" }, { "uniquename": "' + rightAnnot.id() + '" } ]';
+                    operation = "split_transcript";
+                }
+                else {
+                    return;
+                }
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            makeIntron: function (event) {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.makeIntronInExon(selected, event);
+            },
+
+            makeIntronInExon: function (records, event) {
+                if (records.length > 1) {
+                    return;
+                }
+                var track = this;
+                var annot = records[0].feature;
+                var coordinate = this.getGenomeCoord(event);
+                var features = '"features": [ { "uniquename": "' + annot.id() + '", "location": { "fmin": ' + coordinate + ' } } ]';
+                var operation = "make_intron";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            setTranslationStart: function (event, setStart) {
+                // var selected = this.selectionManager.getSelection();
+                var selfeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                this.setTranslationStartInCDS(selfeats, event);
+            },
+
+            setTranslationStartInCDS: function (annots, event) {
+                if (annots.length > 1) {
+                    return;
+                }
+                var track = this;
+                var annot = annots[0];
+                // var coordinate = this.gview.getGenomeCoord(event);
+// var coordinate = Math.floor(this.gview.absXtoBp(event.pageX));
+                var coordinate = this.getGenomeCoord(event);
+                console.log("called setTranslationStartInCDS to: " + coordinate);
+
+                var setStart = annot.parent() ? !annot.parent().get("manuallySetTranslationStart") : !annot.get("manuallySetTranslationStart");
+                var uid = annot.parent() ? annot.parent().id() : annot.id();
+                var features = '"features": [ { "uniquename": "' + uid + '"' + (setStart ? ', "location": { "fmin": ' + coordinate + ' }' : '') + ' } ]';
+                var operation = "set_translation_start";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            setTranslationEnd: function (event) {
+                // var selected = this.selectionManager.getSelection();
+                var selfeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                this.setTranslationEndInCDS(selfeats, event);
+            },
+
+            setTranslationEndInCDS: function (annots, event) {
+                if (annots.length > 1) {
+                    return;
+                }
+                var track = this;
+                var annot = annots[0];
+                // var coordinate = this.gview.getGenomeCoord(event);
+// var coordinate = Math.floor(this.gview.absXtoBp(event.pageX));
+                var coordinate = this.getGenomeCoord(event);
+                console.log("called setTranslationEndInCDS to: " + coordinate);
+
+                var setEnd = annot.parent() ? !annot.parent().get("manuallySetTranslationEnd") : !annot.get("manuallySetTranslationEnd");
+                var uid = annot.parent() ? annot.parent().id() : annot.id();
+                var features = '"features": [ { "uniquename": "' + uid + '"' + (setEnd ? ', "location": { "fmax": ' + coordinate + ' }' : '') + ' } ]';
+                var operation = "set_translation_end";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            flipStrand: function () {
+                var selected = this.selectionManager.getSelection();
+                this.flipStrandForSelectedFeatures(selected);
+            },
+
+            flipStrandForSelectedFeatures: function (records) {
+                var track = this;
+                var uniqueNames = new Object();
+                for (var i in records) {
+                    var record = records[i];
+                    var selfeat = record.feature;
+                    var seltrack = record.track;
+                    var topfeat = AnnotTrack.getTopLevelAnnotation(selfeat);
+                    var uniqueName = topfeat.id();
+                    // just checking to ensure that all features in selection are from
+                    // this track
+                    if (seltrack === track) {
+                        uniqueNames[uniqueName] = 1;
+                    }
+                }
+                var features = '"features": [';
+                var i = 0;
+                for (var uniqueName in uniqueNames) {
+                    var trackdiv = track.div;
+                    var trackName = track.getUniqueTrackName();
+
+                    if (i > 0) {
+                        features += ',';
+                    }
+                    features += ' { "uniquename": "' + uniqueName + '" } ';
+                    ++i;
+                }
+                features += ']';
+                var operation = "flip_strand";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            setLongestORF: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.setLongestORFForSelectedFeatures(selected);
+            },
+
+            setLongestORFForSelectedFeatures: function (selection) {
+                var track = this;
+                var features = '"features": [';
+                for (var i in selection) {
+                    var annot = AnnotTrack.getTopLevelAnnotation(selection[i].feature);
+                    var atrack = selection[i].track;
+                    var uniqueName = annot.id();
+                    // just checking to ensure that all features in selection are from
+                    // this track
+                    if (atrack === track) {
+                        var trackdiv = track.div;
+                        var trackName = track.getUniqueTrackName();
+
+                        if (i > 0) {
+                            features += ',';
+                        }
+                        features += ' { "uniquename": "' + uniqueName + '" } ';
+                    }
+                }
+                features += ']';
+                var operation = "set_longest_orf";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                track.executeUpdateOperation(postData);
+            },
+
+            setReadthroughStopCodon: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.setReadthroughStopCodonForSelectedFeatures(selected);
+            },
+
+            setReadthroughStopCodonForSelectedFeatures: function (selection) {
+                var track = this;
+                var features = '"features": [';
+                for (var i in selection) {
+                    var annot = AnnotTrack.getTopLevelAnnotation(selection[i].feature);
+                    var atrack = selection[i].track;
+                    var uniqueName = annot.id();
+                    // just checking to ensure that all features in selection are from
+                    // this track
+                    if (atrack === track) {
+                        var trackdiv = track.div;
+                        var trackName = track.getUniqueTrackName();
+
+                        if (i > 0) {
+                            features += ',';
+                        }
+                        features += ' { "uniquename": "' + uniqueName + '", "readthrough_stop_codon": ' + !annot.data.readThroughStopCodon + '} ';
+                    }
+                }
+                features += ']';
+                var operation = "set_readthrough_stop_codon";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setAsFivePrimeEnd: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.webapollo.featSelectionManager.clearSelection();
+                this.setAsFivePrimeEndForSelectedFeatures(selectedAnnots, selectedEvidence);
+            },
+
+            setAsFivePrimeEndForSelectedFeatures: function (selectedAnnots, selectedEvidence) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var evidence = selectedEvidence[0].feature;
+                var uniqueName = annot.id();
+                var fmin, fmax;
+                if (annot.get("strand") == -1) {
+                    fmin = annot.get("start");
+                    fmax = evidence.get("end");
+                }
+                else {
+                    fmin = evidence.get("start");
+                    fmax = annot.get("end");
+                }
+                var features = '"features": [ { "uniquename": "' + uniqueName + '", "location": { "fmin": ' + fmin + ', "fmax": ' + fmax + ' } } ]';
+                var operation = "set_exon_boundaries";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setAsThreePrimeEnd: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.webapollo.featSelectionManager.clearSelection();
+                this.setAsThreePrimeEndForSelectedFeatures(selectedAnnots, selectedEvidence);
+            },
+
+            setAsThreePrimeEndForSelectedFeatures: function (selectedAnnots, selectedEvidence) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var evidence = selectedEvidence[0].feature;
+                var uniqueName = annot.id();
+                var fmin, fmax;
+                if (annot.get("strand") == -1) {
+                    fmin = evidence.get("start");
+                    fmax = annot.get("end");
+                }
+                else {
+                    fmin = annot.get("start");
+                    fmax = evidence.get("end");
+                }
+                var features = '"features": [ { "uniquename": "' + uniqueName + '", "location": { "fmin": ' + fmin + ', "fmax": ' + fmax + ' } } ]';
+                var operation = "set_exon_boundaries";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setBothEnds: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.webapollo.featSelectionManager.clearSelection();
+                this.setBothEndsForSelectedFeatures(selectedAnnots, selectedEvidence);
+            },
+
+            setBothEndsForSelectedFeatures: function (selectedAnnots, selectedEvidence) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var evidence = selectedEvidence[0].feature;
+                var uniqueName = annot.id();
+                var fmin, fmax;
+                if (annot.get("strand") == -1) {
+                    fmin = evidence.get("start");
+                    fmax = evidence.get("end");
+                }
+                else {
+                    fmin = evidence.get("start");
+                    fmax = evidence.get("end");
+                }
+                var features = '"features": [ { "uniquename": "' + uniqueName + '", "location": { "fmin": ' + fmin + ', "fmax": ' + fmax + ' } } ]';
+                var operation = "set_exon_boundaries";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setToDownstreamDonor: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.setToDownstreamDonorForSelectedFeatures(selectedAnnots);
+            },
+
+            setToDownstreamDonorForSelectedFeatures: function (selectedAnnots) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var uniqueName = annot.id();
+                var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                var operation = "set_to_downstream_donor";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setToUpstreamDonor: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.setToUpstreamDonorForSelectedFeatures(selectedAnnots);
+            },
+
+            setToUpstreamDonorForSelectedFeatures: function (selectedAnnots) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var uniqueName = annot.id();
+                var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                var operation = "set_to_upstream_donor";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setToDownstreamAcceptor: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.setToDownstreamAcceptorForSelectedFeatures(selectedAnnots);
+            },
+
+            setToDownstreamAcceptorForSelectedFeatures: function (selectedAnnots) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var uniqueName = annot.id();
+                var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                var operation = "set_to_downstream_acceptor";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            setToUpstreamAcceptor: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.setToUpstreamAcceptorForSelectedFeatures(selectedAnnots);
+            },
+
+            setToUpstreamAcceptorForSelectedFeatures: function (selectedAnnots) {
+                var track = this;
+                var annot = selectedAnnots[0].feature;
+                var uniqueName = annot.id();
+                var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                var operation = "set_to_upstream_acceptor";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            lockAnnotation: function () {
+                var selectedAnnots = this.selectionManager.getSelection();
+                this.selectionManager.clearAllSelection();
+                this.lockAnnotationForSelectedFeatures(selectedAnnots);
+            },
+
+            lockAnnotationForSelectedFeatures: function (selectedAnnots) {
+                var track = this;
+                var annot = AnnotTrack.getTopLevelAnnotation(selectedAnnots[0].feature);
+                var uniqueName = annot.id();
+                var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                var operation = annot.get("locked") ? "unlock_feature" : "lock_feature";
+                var trackName = track.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"}';
+                track.executeUpdateOperation(postData);
+            },
+
+            getAnnotationInfoEditor: function () {
+                var selected = this.selectionManager.getSelection();
+                this.getAnnotationInfoEditorForSelectedFeatures(selected);
+            },
+
+            changeAnnotationType: function(type) {
+                var selected = this.selectionManager.getSelection();
+                if (selected[0].feature.afeature.type.name === type) {
+                    this.alertAnnotationType(selected[0], type);
+                }
+                else {
+                    this.changeAnnotations(selected[0], type);
+                    this.selectionManager.clearSelection();
+                }
+            },
+
+            changeAnnotations: function(record, type) {
+                var track = this;
+                var selectedFeature = record.feature;
+                var selectedTrack = record.track;
+                var uniqueName = selectedFeature.afeature.type.name === "exon" ? selectedFeature.afeature.parent_id : selectedFeature.getUniqueName();
+
+                if (selectedTrack == track) {
+                    var trackdiv = track.div;
+                    var trackName = track.getUniqueTrackName();
+                    var features = [{ uniquename: uniqueName, type: type }];
+                    var postData = { track: trackName, features: features, operation: "change_annotation_type" };
+                    track.executeUpdateOperation(JSON.stringify(postData));
+                }
+            },
+
+            alertAnnotationType: function(selectedFeature, type) {
+                var message = "Feature " + selectedFeature.feature.afeature.name + " is already of type " + type;
+                var confirm = new ConfirmDialog({
+                    title: 'Change annotation type',
+                    message: message,
+                    confirmLabel: 'OK',
+                    denyLabel: 'Cancel'
+                }).show();
+            },
+
+            getAnnotationInfoEditorForSelectedFeatures: function (records) {
+                var track = this;
+                var record = records[0];
+                var annot = AnnotTrack.getTopLevelAnnotation(record.feature);
+                var seltrack = record.track;
+                // just checking to ensure that all features in selection are from this
+                // track
+                if (seltrack !== track) {
+                    return;
+                }
+                track.getAnnotationInfoEditorConfigs(track.getUniqueTrackName());
+                var content = dojo.create("div", {'class': "annotation_info_editor_container"});
+                if (annot.afeature.parent_id) {
+                    var selectorDiv = dojo.create("div", {'class': "annotation_info_editor_selector"}, content);
+                    var selectorLabel = dojo.create("label", {
+                        innerHTML: "Select " + annot.get("type"),
+                        'class': "annotation_info_editor_selector_label"
+                    }, selectorDiv);
+                    var data = [];
+                    var feats = track.topLevelParents[annot.afeature.parent_id];
+                    for (var i in feats) {
+                        var feat = feats[i];
+                        data.push({id: feat.uniquename, label: feat.name});
+                    }
+                    var store = new Memory({
+                        data: data
+                    });
+                    var os = new ObjectStore({objectStore: store});
+                    var selector = new Select({store: os});
+                    selector.placeAt(selectorDiv);
+                    selector.attr("value", annot.afeature.uniquename);
+                    selector.attr("style", "width: 50%;");
+                    var first = true;
+                    dojo.connect(selector, "onChange", function (id) {
+                        if (!first) {
+                            dojo.destroy("child_annotation_info_editor");
+                            annotContent = track.createAnnotationInfoEditorPanelForFeature(id, track.getUniqueTrackName(), selector, true);
+                            dojo.attr(annotContent, "class", "annotation_info_editor");
+                            dojo.attr(annotContent, "id", "child_annotation_info_editor");
+                            dojo.place(annotContent, content);
+                        }
+                        first = false;
+                    });
+                }
+                var numItems = 0;
+                // if annotation has parent, get comments for parent
+                if (annot.afeature.parent_id) {
+                    var parentContent = this.createAnnotationInfoEditorPanelForFeature(annot.afeature.parent_id, track.getUniqueTrackName());
+                    dojo.attr(parentContent, "class", "parent_annotation_info_editor");
+                    dojo.place(parentContent, content);
+                    ++numItems;
+                }
+                var annotContent = this.createAnnotationInfoEditorPanelForFeature(annot.id(), track.getUniqueTrackName(), selector, false);
+                dojo.attr(annotContent, "class", "annotation_info_editor");
+                dojo.attr(annotContent, "id", "child_annotation_info_editor");
+                dojo.place(annotContent, content);
+                ++numItems;
+                dojo.attr(content, "style", "width:" + (numItems == 1 ? "28" : "58") + "em;");
+                track.openDialog("Information Editor", content);
+                AnnotTrack.popupDialog.resize();
+                AnnotTrack.popupDialog._position();
+
+
+            },
+
+            getAnnotationInfoEditorConfigs: function (trackName) {
+                var track = this;
+                if (track.annotationInfoEditorConfigs) {
+                    return;
+                }
+                var operation = "get_annotation_info_editor_configuration";
+                var postData = {"track": trackName, "operation": operation};
+                xhr.post(context_path + "/AnnotationEditorService", {
+                    sync: true,
+                    data: JSON.stringify(postData),
+                    handleAs: "json",
+                    timeout: 5000 * 1000
+                }).then(function (response) {
+                        track.annotationInfoEditorConfigs = {};
+                        for (var i = 0; i < response.annotation_info_editor_configs.length; ++i) {
+                            var config = response.annotation_info_editor_configs[i];
+                            for (var j = 0; j < config.supported_types.length; ++j) {
+                                track.annotationInfoEditorConfigs[config.supported_types[j]] = config;
+                            }
+                        }
+                    }
+                );
+            },
+
+
+            createAnnotationInfoEditorPanelForFeature: function (uniqueName, trackName, selector, reload) {
+                var track = this;
+                var feature = this.store.getFeatureById(uniqueName);
+                var hasWritePermission = this.canEdit(this.store.getFeatureById(uniqueName));
+                var content = dojo.create("span");
+
+                var header = dojo.create("div", {className: "annotation_info_editor_header"}, content);
+
+                var nameDiv = dojo.create("div", {'class': "annotation_info_editor_field_section"}, content);
+                var nameLabel = dojo.create("label", {
+                    innerHTML: "Name",
+                    'class': "annotation_info_editor_label"
+                }, nameDiv);
+                var nameField = new dijitTextBox({'class': "annotation_editor_field"});
+                var nameLabelss = "Follow GenBank or UniProt-SwissProt guidelines for gene, protein, and CDS nomenclature.";
+                dojo.place(nameField.domNode, nameDiv);
+                // var nameField = new dojo.create("input", { type: "text" }, nameDiv);
+
+                new Tooltip({
+                    connectId: nameDiv,
+                    label: nameLabelss,
+                    position: ["above"],
+                    showDelay: 600
+                });
+
+                var symbolDiv = dojo.create("div", {'class': "annotation_info_editor_field_section"}, content);
+                var symbolLabel = dojo.create("label", {
+                    innerHTML: "Symbol",
+                    'class': "annotation_info_editor_label"
+                }, symbolDiv);
+                var symbolField = new dijitTextBox({'class': "annotation_editor_field"});
+                dojo.place(symbolField.domNode, symbolDiv);
+
+                var descriptionDiv = dojo.create("div", {'class': "annotation_info_editor_field_section"}, content);
+                var descriptionLabel = dojo.create("label", {
+                    innerHTML: "Description",
+                    'class': "annotation_info_editor_label"
+                }, descriptionDiv);
+                var descriptionField = new dijitTextBox({'class': "annotation_editor_field"});
+                dojo.place(descriptionField.domNode, descriptionDiv);
+
+                var dateCreationDiv = dojo.create("div", {'class': "annotation_info_editor_field_section"}, content);
+                var dateCreationLabel = dojo.create("label", {
+                    innerHTML: "Created",
+                    'class': "annotation_info_editor_label"
+                }, dateCreationDiv);
+                var dateCreationField = new dijitTextBox({'class': "annotation_editor_field", readonly: true});
+                dojo.place(dateCreationField.domNode, dateCreationDiv);
+
+                var dateLastModifiedDiv = dojo.create("div", {'class': "annotation_info_editor_field_section"}, content);
+                var dateLastModifiedLabel = dojo.create("label", {
+                    innerHTML: "Last modified",
+                    'class': "annotation_info_editor_label"
+                }, dateLastModifiedDiv);
+                var dateLastModifiedField = new dijitTextBox({'class': "annotation_editor_field", readonly: true});
+                dojo.place(dateLastModifiedField.domNode, dateLastModifiedDiv);
+
+                var statusDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var statusLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "Status"
+                }, statusDiv);
+                var statusFlags = dojo.create("div", {'class': "status"}, statusDiv);
+                var statusRadios = [];
+
+                var dbxrefsDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var dbxrefsLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "DBXRefs"
+                }, dbxrefsDiv);
+                var dbxrefsTable = dojo.create("div", {
+                    'class': "dbxrefs",
+                    id: "dbxrefs_" + (selector ? "child" : "parent")
+                }, dbxrefsDiv);
+                var dbxrefButtonsContainer = dojo.create("div", {style: "text-align: center;"}, dbxrefsDiv);
+                var dbxrefButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, dbxrefButtonsContainer);
+                var addDbxrefButton = dojo.create("button", {
+                    innerHTML: "Add",
+                    'class': "annotation_info_editor_button"
+                }, dbxrefButtons);
+                var deleteDbxrefButton = dojo.create("button", {
+                    innerHTML: "Delete",
+                    'class': "annotation_info_editor_button"
+                }, dbxrefButtons);
+                var dbxrefss = "Use this field to identify cross-references of this genomic element in other databases (e.g. GenBank ID for a cDNA from this gene in the same species or a miRNA ID from miRBase). Do not use this field to list IDs from similar genomic elements from other species, even if you used them as evidence for this as annotation.";
+                new Tooltip({
+                    connectId: dbxrefsDiv,
+                    label: dbxrefss,
+                    position: ["above"],
+                    showDelay: 600
+                });
+
+                var attributesDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var attributesLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "Attributes"
+                }, attributesDiv);
+                var attributesTable = dojo.create("div", {
+                    'class': "attributes",
+                    id: "attributes_" + (selector ? "child" : "parent")
+                }, attributesDiv);
+                var attributeButtonsContainer = dojo.create("div", {style: "text-align: center;"}, attributesDiv);
+                var attributeButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, attributeButtonsContainer);
+                var addAttributeButton = dojo.create("button", {
+                    innerHTML: "Add",
+                    'class': "annotation_info_editor_button"
+                }, attributeButtons);
+                var deleteAttributeButton = dojo.create("button", {
+                    innerHTML: "Delete",
+                    'class': "annotation_info_editor_button"
+                }, attributeButtons);
+
+                var pubmedIdsDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var pubmedIdsLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "PubMed IDs"
+                }, pubmedIdsDiv);
+                var pubmedIdsTable = dojo.create("div", {
+                    'class': "pubmed_ids",
+                    id: "pubmd_ids_" + (selector ? "child" : "parent")
+                }, pubmedIdsDiv);
+                var pubmedIdButtonsContainer = dojo.create("div", {style: "text-align: center;"}, pubmedIdsDiv);
+                var pubmedIdButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, pubmedIdButtonsContainer);
+                var addPubmedIdButton = dojo.create("button", {
+                    innerHTML: "Add",
+                    'class': "annotation_info_editor_button"
+                }, pubmedIdButtons);
+                var deletePubmedIdButton = dojo.create("button", {
+                    innerHTML: "Delete",
+                    'class': "annotation_info_editor_button"
+                }, pubmedIdButtons);
+                var pubmedss = "Use this field to indicate that this genomic element has been mentioned in a publication, or that a publication supports your functional annotations using GO IDs. Do not use this field to list publications containing related or similar genomic elements from other species that you may have used as evidence for this annotation.";
+                new Tooltip({
+                    connectId: pubmedIdsDiv,
+                    label: pubmedss,
+                    position: ["above"],
+                    showDelay: 600
+                });
+                var goIdsDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var goIdsLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "Gene Ontology IDs"
+                }, goIdsDiv);
+                var goIdsTable = dojo.create("div", {
+                    'class': "go_ids",
+                    id: "go_ids_" + (selector ? "child" : "parent")
+                }, goIdsDiv);
+                var goIdButtonsContainer = dojo.create("div", {style: "text-align: center;"}, goIdsDiv);
+                var goIdButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, goIdButtonsContainer);
+                var addGoIdButton = dojo.create("button", {
+                    innerHTML: "Add",
+                    'class': "annotation_info_editor_button"
+                }, goIdButtons);
+                var deleteGoIdButton = dojo.create("button", {
+                    innerHTML: "Delete",
+                    'class': "annotation_info_editor_button"
+                }, goIdButtons);
+
+
+                var commentsDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                var commentsLabel = dojo.create("div", {
+                    'class': "annotation_info_editor_section_header",
+                    innerHTML: "Comments"
+                }, commentsDiv);
+                var commentsTable = dojo.create("div", {
+                    'class': "comments",
+                    id: "comments_" + (selector ? "child" : "parent")
+                }, commentsDiv);
+                var commentButtonsContainer = dojo.create("div", {style: "text-align: center;"}, commentsDiv);
+                var commentButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, commentButtonsContainer);
+                var addCommentButton = dojo.create("button", {
+                    innerHTML: "Add",
+                    'class': "annotation_info_editor_button"
+                }, commentButtons);
+                var deleteCommentButton = dojo.create("button", {
+                    innerHTML: "Delete",
+                    'class': "annotation_info_editor_button"
+                }, commentButtons);
+
+                //var replacementDiv = dojo.create("div", {'class': "annotation_info_editor_section"}, content);
+                //var replacementLabel = dojo.create("div", {
+                //    'class': "annotation_info_editor_section_header",
+                //    innerHTML: "Replace model(s)"
+                //}, replacementDiv);
+                //
+                //
+                //var replacementsTable = dojo.create("div", {
+                //    'class': "replacement",
+                //    id: "replacement_" + (selector ? "child" : "parent")
+                //}, replacementDiv);
+                //var replacementButtonsContainer = dojo.create("div", {style: "text-align: center;"}, replacementDiv);
+                //var replacementButtons = dojo.create("div", {'class': "annotation_info_editor_button_group"}, replacementButtonsContainer);
+                //var addreplacementButton = dojo.create("button", {
+                //    innerHTML: "Add",
+                //    'class': "annotation_info_editor_button"
+                //}, replacementButtons);
+                //var deletereplacementButton = dojo.create("button", {
+                //    innerHTML: "Delete",
+                //    'class': "annotation_info_editor_button"
+                //}, replacementButtons);
+                //
+                //
+                //var trackNames = Object.keys(JBrowse.trackConfigsByName);
+                //var replacements = new Select({
+                //    name: "replacementSelection",
+                //    options: array.map(trackNames, function(trackName) {
+                //        return {value: track.browser.trackConfigsByName[trackName].label, label: track.browser.trackConfigsByName[trackName].key}
+                //    })
+                //});
+                //
+                //
+                //replacements.placeAt(replacementDiv).startup();
+                //track.featureReplace = track.featureReplace ||new Select({
+                //    name: "featureSelection",
+                //    options: []
+                //});
+                //on(replacements, "change", function(selection) {
+                //    console.log(track.browser.trackConfigsByName,selection,replacements);
+                //    track.browser.getStore(track.browser.trackConfigsByName[selection].store, function(store) {
+                //        console.log("Received store",store);
+                //        store.getFeatures({ref: feature.afeature.sequence, start: feature.get('start'), end: feature.get('end')}, function(f) {
+                //            console.log(track.featureReplace.options,f);
+                //            if(f) {
+                //                track.featureReplace.options.push({value: f.get('id'), label: f.get('name')});
+                //                track.featureReplace.placeAt(replacementDiv).startup();
+                //            }
+                //        })
+                //    });
+                //});
+                //
+                //on(track.featureReplace, "change", function(selection) {
+                //    console.log(uniqueName);
+                //    track.executeUpdateOperation(JSON.stringify({ "features": [{
+                //            non_reserved_properties: [{
+                //                tag: "replace",
+                //                value: selection
+                //            }],
+                //            uniquename: uniqueName
+                //        }],
+                //        operation: "add_non_reserved_properties"
+                //    }))
+                //});
+
+
+                if (!hasWritePermission) {
+                    nameField.set("disabled", true);
+                    symbolField.set("disabled", true);
+                    descriptionField.set("disabled", true);
+                    dateCreationField.set("disabled", true);
+                    dateLastModifiedField.set("disabled", true);
+                    dojo.attr(addDbxrefButton, "disabled", true);
+                    dojo.attr(deleteDbxrefButton, "disabled", true);
+                    dojo.attr(addAttributeButton, "disabled", true);
+                    dojo.attr(deleteAttributeButton, "disabled", true);
+                    dojo.attr(addPubmedIdButton, "disabled", true);
+                    dojo.attr(deletePubmedIdButton, "disabled", true);
+                    dojo.attr(addGoIdButton, "disabled", true);
+                    dojo.attr(deleteGoIdButton, "disabled", true);
+                    dojo.attr(addCommentButton, "disabled", true);
+                    dojo.attr(deleteCommentButton, "disabled", true);
+                    //dojo.attr(addreplacementButton, "disabled", true);
+                    //dojo.attr(deletereplacementButton, "disabled", true);
+                }
+
+                var pubmedIdDb = "PMID";
+                var goIdDb = "GO";
+                var cannedComments;
+                var cannedKeys;
+                var cannedValues;
+
+                var timeout = 100;
+
+                var escapeString = function (str) {
+                    return str.replace(/(["'])/g, "\\$1");
+                };
+
+                function init() {
+                    var postData = JSON.stringify({
+                        features: [
+                            {
+                                uniquename: uniqueName
+                            }
+                        ],
+                        operation: "get_annotation_info_editor_data",
+                        track: trackName,
+                        clientToken: track.getClientToken()
+                    });
+                    dojo.xhrPost({
+                        sync: true,
+                        postData: postData,
+                        url: context_path + "/AnnotationEditorService",
+                        handleAs: "json",
+                        timeout: 5000 * 1000, // Time in milliseconds
+                        load: function (response, ioArgs) {
+                            var feature = response.features[0];
+                            var config = track.annotationInfoEditorConfigs[feature.type.cv.name + ":" + feature.type.name] || track.annotationInfoEditorConfigs["default"];
+                            initType(feature);
+                            initName(feature);
+                            initSymbol(feature);
+                            initDescription(feature);
+                            initDates(feature);
+                            // initStatus(feature, config);
+                            initStatus(feature);
+                            initDbxrefs(feature, config);
+                            initAttributes(feature, config);
+                            initPubmedIds(feature, config);
+                            initGoIds(feature, config);
+                            initComments(feature, config);
+
+                        }
+                    });
+                };
+
+                function initTable(domNode, tableNode, table, timeout) {
+                    var id = dojo.attr(tableNode, "id");
+                    var node = dojo.byId(id);
+                    if (!node) {
+                        setTimeout(function () {
+                            initTable(domNode, tableNode, table, timeout);
+                            return;
+                        }, timeout);
+                        return;
+                    }
+                    dojo.place(domNode, tableNode, "first");
+                    table.startup();
+                }
+
+                var initType = function (feature) {
+                    header.innerHTML = feature.type.name;
+                };
+
+                var initName = function (feature) {
+                    if (feature.name) {
+                        nameField.set("value", feature.name);
+                    }
+                    var oldName;
+                    dojo.connect(nameField, "onFocus", function () {
+                        oldName = nameField.get("value");
+                    });
+                    dojo.connect(nameField, "onBlur", function () {
+                        var newName = nameField.get("value");
+                        if (oldName != newName) {
+                            updateName(newName);
+                            if (selector) {
+                                var select = selector.store.get(feature.uniquename).then(function (select) {
+                                    selector.store.setValue(select, "label", newName);
+                                });
+                            }
+                        }
+                    });
+                };
+
+                var initSymbol = function (feature) {
+                    if (feature.symbol) {
+                        symbolField.set("value", feature.symbol);
+                    }
+                    var oldSymbol;
+                    dojo.connect(symbolField, "onFocus", function () {
+                        oldSymbol = symbolField.get("value");
+                    });
+                    dojo.connect(symbolField, "onBlur", function () {
+                        var newSymbol = symbolField.get("value");
+                        if (oldSymbol != newSymbol) {
+                            updateSymbol(newSymbol);
+                        }
+                    });
+                };
+
+                var initDescription = function (feature) {
+                    if (feature.description) {
+                        descriptionField.set("value", feature.description);
+                    }
+                    var oldDescription;
+                    dojo.connect(descriptionField, "onFocus", function () {
+                        oldDescription = descriptionField.get("value");
+                    });
+                    dojo.connect(descriptionField, "onBlur", function () {
+                        var newDescription = descriptionField.get("value");
+                        if (oldDescription != newDescription) {
+                            updateDescription(newDescription);
+                        }
+                    });
+                };
+
+                var initDates = function (feature) {
+                    if (feature.date_creation) {
+                        dateCreationField.set("value", FormatUtils.formatDate(feature.date_creation));
+                    }
+                    if (feature.date_last_modified) {
+                        dateLastModifiedField.set("value", FormatUtils.formatDate(feature.date_last_modified));
+                    }
+                };
+
+                var initStatus = function (feature) {
+                    var maxLength = 0;
+                    var status = feature.available_statuses;
+                    if (status) {
+                        for (var i = 0; i < status.length; ++i) {
+                            if (status[i].length > maxLength) {
+                                maxLength = status[i].length;
+                            }
+                        }
+                        for (var i = 0; i < status.length; ++i) {
+                            var statusRadioDiv = dojo.create("span", {
+                                'class': "annotation_info_editor_radio",
+                                style: "width:" + (maxLength * 0.75) + "em; display: inline;"
+                            }, statusFlags);
+                            var statusRadio = new dijitRadioButton({
+                                value: status[i],
+                                name: "status_" + uniqueName,
+                                checked: status[i] == feature.status ? true : false
+                            });
+                            if (!hasWritePermission) {
+                                statusRadio.set("disabled", true);
+                            }
+                            dojo.place(statusRadio.domNode, statusRadioDiv);
+                            var statusLabel = dojo.create("label", {
+                                innerHTML: status[i],
+                                'class': "annotation_info_editor_radio_label"
+                            }, statusRadioDiv);
+                            statusRadios[status[i]] = statusRadio;
+                            dojo.connect(statusRadio, "onMouseDown", function (div, radio, label) {
+                                return function (event) {
+                                    if (radio.checked) {
+                                        deleteStatus();
+                                        dojo.place(new dijitRadioButton({
+                                            value: status[i],
+                                            name: "status_" + uniqueName,
+                                            checked: false
+                                        }).domNode, radio.domNode, "replace");
+                                    }
+                                };
+                            }(statusRadioDiv, statusRadio, statusLabel));
+                            dojo.connect(statusRadio, "onChange", function (label) {
+                                return function (selected) {
+                                    if (selected && hasWritePermission) {
+                                        updateStatus(label);
+                                    }
+                                };
+                            }(status[i]));
+                        }
+                    }
+                    else {
+                        dojo.style(statusDiv, "display", "none");
+                    }
+                };
+
+                var initDbxrefs = function (feature, config) {
+                    if (config.hasDbxrefs) {
+                        var oldDb;
+                        var oldAccession;
+                        var dbxrefs = new dojoItemFileWriteStore({
+                            data: {
+                                items: []
+                            }
+                        });
+                        for (var i = 0; i < feature.dbxrefs.length; ++i) {
+                            var dbxref = feature.dbxrefs[i];
+                            if (dbxref.db != pubmedIdDb && dbxref.db != goIdDb) {
+                                dbxrefs.newItem({db: dbxref.db, accession: dbxref.accession});
+                            }
+                        }
+                        var dbxrefTableLayout = [{
+                            cells: [
+                                {
+                                    name: 'DB',
+                                    field: 'db',
+                                    width: '40%',
+                                    formatter: function (db) {
+                                        if (!db) {
+                                            return "Enter new DB";
+                                        }
+                                        return db;
+                                    },
+                                    editable: hasWritePermission
+                                },
+                                {
+                                    name: 'Accession',
+                                    field: 'accession',
+                                    width: '60%',
+                                    formatter: function (accession) {
+                                        if (!accession) {
+                                            return "Enter new accession";
+                                        }
+                                        return accession;
+                                    },
+                                    editable: hasWritePermission
+                                }
+                            ]
+                        }];
+
+                        var dbxrefTable = new dojoxDataGrid({
+                            singleClickEdit: true,
+                            store: dbxrefs,
+                            updateDelay: 0,
+                            structure: dbxrefTableLayout
+                        });
+
+                        var handle = dojo.connect(AnnotTrack.popupDialog, "onFocus", function () {
+                            initTable(dbxrefTable.domNode, dbxrefsTable, dbxrefTable);
+                            dojo.disconnect(handle);
+                        });
+                        if (reload) {
+                            initTable(dbxrefTable.domNode, dbxrefsTable, dbxrefTable, timeout);
+                        }
+
+
+                        var dirty = false;
+                        dojo.connect(dbxrefTable, "onStartEdit", function (inCell, inRowIndex) {
+                            if (!dirty) {
+                                oldDb = dbxrefTable.store.getValue(dbxrefTable.getItem(inRowIndex), "db");
+                                oldAccession = dbxrefTable.store.getValue(dbxrefTable.getItem(inRowIndex), "accession");
+                                dirty = true;
+                            }
+                        });
+
+                        dojo.connect(dbxrefTable, "onCancelEdit", function (inRowIndex) {
+                            dbxrefTable.store.setValue(dbxrefTable.getItem(inRowIndex), "db", oldDb);
+                            dbxrefTable.store.setValue(dbxrefTable.getItem(inRowIndex), "accession", oldAccession);
+                            dirty = false;
+                        });
+
+                        dojo.connect(dbxrefTable, "onApplyEdit", function (inRowIndex) {
+                            var newDb = dbxrefTable.store.getValue(dbxrefTable.getItem(inRowIndex), "db");
+                            var newAccession = dbxrefTable.store.getValue(dbxrefTable.getItem(inRowIndex), "accession");
+                            if (!newDb || !newAccession) {
+                            }
+                            else if (!oldDb || !oldAccession) {
+                                addDbxref(newDb, newAccession);
+                            }
+                            else {
+                                if (newDb != oldDb || newAccession != oldAccession) {
+                                    updateDbxref(oldDb, oldAccession, newDb, newAccession);
+                                }
+                            }
+                            dirty = false;
+                        });
+
+                        dojo.connect(addDbxrefButton, "onclick", function () {
+                            dbxrefTable.store.newItem({db: "", accession: ""});
+                            dbxrefTable.scrollToRow(dbxrefTable.rowCount);
+                        });
+
+                        dojo.connect(deleteDbxrefButton, "onclick", function () {
+                            var toBeDeleted = new Array();
+                            var selected = dbxrefTable.selection.getSelected();
+                            for (var i = 0; i < selected.length; ++i) {
+                                var item = selected[i];
+                                var db = dbxrefTable.store.getValue(item, "db");
+                                var accession = dbxrefTable.store.getValue(item, "accession");
+                                toBeDeleted.push({db: db, accession: accession});
+                            }
+                            dbxrefTable.removeSelectedRows();
+                            deleteDbxrefs(toBeDeleted);
+                        });
+                    }
+                    else {
+                        dojo.style(dbxrefsDiv, "display", "none");
+                    }
+                };
+
+                var initAttributes = function (feature, config) {
+                    if (config.hasAttributes) {
+                        cannedKeys = feature.canned_keys;
+                        cannedValues = feature.canned_values;
+                        var oldTag;
+                        var oldValue;
+                        var attributes = new dojoItemFileWriteStore({
+                            data: {
+                                items: []
+                            }
+                        });
+                        for (var i = 0; i < feature.non_reserved_properties.length; ++i) {
+                            var attribute = feature.non_reserved_properties[i];
+                            attributes.newItem({tag: attribute.tag, value: attribute.value});
+                        }
+
+
+                        var attributeTableLayout = [{
+                            cells: [
+                                {
+                                    name: 'Tag',
+                                    field: 'tag',
+                                    width: '40%',
+                                    type: dojox.grid.cells.ComboBox,
+                                    //type: dojox.grid.cells.Select,
+                                    options: cannedKeys,
+                                    formatter: function (tag) {
+                                        if (!tag) {
+                                            return "Enter new tag";
+                                        }
+                                        return tag;
+                                    },
+                                    editable: hasWritePermission
+                                },
+                                {
+                                    name: 'Value',
+                                    field: 'value',
+                                    width: '60%',
+                                    type: dojox.grid.cells.ComboBox,
+                                    //type: dojox.grid.cells.Select,
+                                    options: cannedValues,
+                                    formatter: function (value) {
+                                        if (!value) {
+                                            return "Enter new value";
+                                        }
+                                        return value;
+                                    },
+                                    editable: hasWritePermission
+                                }
+                            ]
+                        }];
+
+                        var attributeTable = new dojoxDataGrid({
+                            singleClickEdit: true,
+                            store: attributes,
+                            updateDelay: 0,
+                            structure: attributeTableLayout
+                        });
+
+                        var handle = dojo.connect(AnnotTrack.popupDialog, "onFocus", function () {
+                            initTable(attributeTable.domNode, attributesTable, attributeTable);
+                            dojo.disconnect(handle);
+                        });
+                        if (reload) {
+                            initTable(attributeTable.domNode, attributesTable, attributeTable, timeout);
+                        }
+
+                        var dirty = false;
+
+                        dojo.connect(attributeTable, "onStartEdit", function (inCell, inRowIndex) {
+                            if (!dirty) {
+                                oldTag = attributeTable.store.getValue(attributeTable.getItem(inRowIndex), "tag");
+                                oldValue = attributeTable.store.getValue(attributeTable.getItem(inRowIndex), "value");
+                                dirty = true;
+                            }
+                        });
+
+                        dojo.connect(attributeTable, "onCancelEdit", function (inRowIndex) {
+                            attributeTable.store.setValue(attributeTable.getItem(inRowIndex), "tag", oldTag);
+                            attributeTable.store.setValue(attributeTable.getItem(inRowIndex), "value", oldValue);
+                            dirty = false;
+                        });
+
+                        dojo.connect(attributeTable, "onApplyEdit", function (inRowIndex) {
+                            var newTag = attributeTable.store.getValue(attributeTable.getItem(inRowIndex), "tag");
+                            var newValue = attributeTable.store.getValue(attributeTable.getItem(inRowIndex), "value");
+                            if (!newTag || !newValue) {
+                            }
+                            else if (!oldTag || !oldValue) {
+                                addAttribute(newTag, newValue);
+                            }
+                            else {
+                                if (newTag != oldTag || newValue != oldValue) {
+                                    updateAttribute(oldTag, oldValue, newTag, newValue);
+                                }
+                            }
+                            dirty = false;
+                        });
+
+                        dojo.connect(addAttributeButton, "onclick", function () {
+                            attributeTable.store.newItem({tag: "", value: ""});
+                            attributeTable.scrollToRow(attributeTable.rowCount);
+                        });
+
+                        dojo.connect(deleteAttributeButton, "onclick", function () {
+                            var toBeDeleted = new Array();
+                            var selected = attributeTable.selection.getSelected();
+                            for (var i = 0; i < selected.length; ++i) {
+                                var item = selected[i];
+                                var tag = attributeTable.store.getValue(item, "tag");
+                                var value = attributeTable.store.getValue(item, "value");
+                                toBeDeleted.push({tag: tag, value: value});
+                            }
+                            attributeTable.removeSelectedRows();
+                            deleteAttributes(toBeDeleted);
+                        });
+                    }
+                    else {
+                        dojo.style(attributesDiv, "display", "none");
+                    }
+
+                };
+
+                var initPubmedIds = function (feature, config) {
+                    if (config.hasPubmedIds) {
+                        var oldPubmedId;
+                        var pubmedIds = new dojoItemFileWriteStore({
+                            data: {
+                                items: []
+                            }
+                        });
+                        for (var i = 0; i < feature.dbxrefs.length; ++i) {
+                            var dbxref = feature.dbxrefs[i];
+                            if (dbxref.db == pubmedIdDb) {
+                                pubmedIds.newItem({pubmed_id: dbxref.accession});
+                            }
+                        }
+                        var pubmedIdTableLayout = [{
+                            cells: [
+                                {
+                                    name: 'PubMed ID',
+                                    field: 'pubmed_id',
+                                    width: '100%',
+                                    formatter: function (pubmedId) {
+                                        if (!pubmedId) {
+                                            return "Enter new PubMed ID";
+                                        }
+                                        return pubmedId;
+                                    },
+                                    editable: hasWritePermission
+                                }
+                            ]
+                        }];
+
+                        var pubmedIdTable = new dojoxDataGrid({
+                            singleClickEdit: true,
+                            store: pubmedIds,
+                            updateDelay: 0,
+                            structure: pubmedIdTableLayout
+                        });
+
+                        var handle = dojo.connect(AnnotTrack.popupDialog, "onFocus", function () {
+                            initTable(pubmedIdTable.domNode, pubmedIdsTable, pubmedIdTable);
+                            dojo.disconnect(handle);
+                        });
+                        if (reload) {
+                            initTable(pubmedIdTable.domNode, pubmedIdsTable, pubmedIdTable, timeout);
+                        }
+
+                        dojo.connect(pubmedIdTable, "onStartEdit", function (inCell, inRowIndex) {
+                            oldPubmedId = pubmedIdTable.store.getValue(pubmedIdTable.getItem(inRowIndex), "pubmed_id");
+                        });
+
+                        dojo.connect(pubmedIdTable, "onApplyEdit", function (inRowIndex) {
+                            var newPubmedId = pubmedIdTable.store.getValue(pubmedIdTable.getItem(inRowIndex), "pubmed_id");
+                            if (!newPubmedId) {
+                            }
+                            else if (!oldPubmedId) {
+                                addPubmedId(pubmedIdTable, inRowIndex, newPubmedId);
+                            }
+                            else {
+                                if (newPubmedId != oldPubmedId) {
+                                    updatePubmedId(pubmedIdTable, inRowIndex, oldPubmedId, newPubmedId);
+                                }
+                            }
+                        });
+
+                        dojo.connect(addPubmedIdButton, "onclick", function () {
+                            pubmedIdTable.store.newItem({pubmed_id: ""});
+                            pubmedIdTable.scrollToRow(pubmedIdTable.rowCount);
+                        });
+
+                        dojo.connect(deletePubmedIdButton, "onclick", function () {
+                            var toBeDeleted = new Array();
+                            var selected = pubmedIdTable.selection.getSelected();
+                            for (var i = 0; i < selected.length; ++i) {
+                                var item = selected[i];
+                                var pubmedId = pubmedIdTable.store.getValue(item, "pubmed_id");
+                                toBeDeleted.push({db: pubmedIdDb, accession: pubmedId});
+                            }
+                            pubmedIdTable.removeSelectedRows();
+                            deletePubmedIds(toBeDeleted);
+                        });
+                    }
+                    else {
+                        dojo.style(pubmedIdsDiv, "display", "none");
+                    }
+                };
+
+                var initGoIds = function (feature, config) {
+                    if (config.hasGoIds) {
+                        var oldGoId;
+                        var dirty = false;
+                        var valid = true;
+                        var editingRow = 0;
+                        var goIds = new dojoItemFileWriteStore({
+                            data: {
+                                items: []
+                            }
+                        });
+                        for (var i = 0; i < feature.dbxrefs.length; ++i) {
+                            var dbxref = feature.dbxrefs[i];
+                            if (dbxref.db == goIdDb) {
+                                goIds.newItem({go_id: goIdDb + ":" + dbxref.accession});
+                            }
+                        }
+                        var goIdTableLayout = [{
+                            cells: [
+                                {
+                                    name: 'Gene Ontology ID',
+                                    field: 'go_id', // '_item',
+                                    width: '100%',
+                                    type: declare(dojox.grid.cells._Widget, {
+                                        widgetClass: dijitTextBox,
+                                        createWidget: function (inNode, inDatum, inRowIndex) {
+                                            var widget = new this.widgetClass(this.getWidgetProps(inDatum), inNode);
+                                            var textBox = widget.domNode.childNodes[0].childNodes[0];
+                                            dojo.connect(textBox, "onkeydown", function (event) {
+                                                if (event.keyCode == dojo.keys.ENTER) {
+                                                    if (dirty) {
+                                                        dirty = false;
+                                                        valid = validateGoId(textBox.value) ? true : false;
+                                                    }
+                                                }
+                                            });
+                                            var original = 'http://golr.geneontology.org/';
+                                            //var original = 'http://golr.geneontology.org/solr/';
+                                            //var original = 'http://golr.berkeleybop.org/solr/';
+                                            var encoded_original = encodeURI(original);
+                                            encoded_original = encoded_original.replace(/:/g, "%3A");
+                                            encoded_original = encoded_original.replace(/\//g, "%2F");
+
+                                            //var gserv = context_path + "/proxy/request/http/golr.geneontology.org%2Fsolr%2Fselect/json/";
+                                            var gserv = context_path + "/proxy/request/" + encoded_original;
+                                            var gconf = new bbop.golr.conf(amigo.data.golr);
+                                            var args = {
+                                                label_template: '{{annotation_class_label}} [{{annotation_class}}]',
+                                                value_template: '{{annotation_class}}',
+                                                list_select_callback: function (doc) {
+                                                    dirty = false;
+                                                    valid = true;
+                                                    goIdTable.store.setValue(goIdTable.getItem(editingRow), "go_id", doc.annotation_class);
+                                                }
+                                            };
+                                            var auto = new bbop.widget.search_box(gserv, gconf, textBox, args);
+                                            auto.set_personality('bbop_term_ac');
+                                            auto.add_query_filter('document_category', 'ontology_class');
+                                            auto.add_query_filter('source', '(biological_process OR molecular_function OR cellular_component)');
+                                            return widget;
+                                        }
+                                    }),
+                                    formatter: function (goId, rowIndex, cell) {
+                                        if (!goId) {
+                                            return "Enter new Gene Ontology ID";
+                                        }
+                                        return goId;
+                                    },
+                                    editable: hasWritePermission
+                                }
+                            ]
+                        }];
+
+                        var goIdTable = new dojoxDataGrid({
+                            singleClickEdit: true,
+                            store: goIds,
+                            updateDelay: 0,
+                            structure: goIdTableLayout
+                        });
+
+                        var handle = dojo.connect(AnnotTrack.popupDialog, "onFocus", function () {
+                            initTable(goIdTable.domNode, goIdsTable, goIdTable);
+                            dojo.disconnect(handle);
+                        });
+                        if (reload) {
+                            initTable(goIdTable.domNode, goIdsTable, goIdTable, timeout);
+                        }
+
+                        dojo.connect(goIdTable, "onStartEdit", function (inCell, inRowIndex) {
+                            editingRow = inRowIndex;
+                            oldGoId = goIdTable.store.getValue(goIdTable.getItem(inRowIndex), "go_id");
+                            dirty = true;
+                        });
+
+                        // dojo.connect(goIdTable, "onApplyCellEdit", function(inValue, inRowIndex, inCellIndex) {
+                        dojo.connect(goIdTable.store, "onSet", function (item, attribute, oldValue, newValue) {
+                            if (dirty) {
+                                return;
+                            }
+                            // var newGoId = goIdTable.store.getValue(goIdTable.getItem(inRowIndex),
+                            // "go_id");
+                            var newGoId = newValue;
+                            if (!newGoId) {
+                            }
+                            else if (!oldGoId) {
+                                addGoId(goIdTable, editingRow, newGoId, valid);
+                            }
+                            else {
+                                if (newGoId != oldGoId) {
+                                    // updateGoId(goIdTable, editingRow, oldGoId, newGoId);
+                                    updateGoId(goIdTable, item, oldGoId, newGoId, valid);
+                                }
+                            }
+                            goIdTable.render();
+                        });
+
+                        dojo.connect(addGoIdButton, "onclick", function () {
+                            goIdTable.store.newItem({go_id: ""});
+                            goIdTable.scrollToRow(goIdTable.rowCount);
+                        });
+
+                        dojo.connect(deleteGoIdButton, "onclick", function () {
+                            var toBeDeleted = new Array();
+                            var selected = goIdTable.selection.getSelected();
+                            for (var i = 0; i < selected.length; ++i) {
+                                var item = selected[i];
+                                var goId = goIdTable.store.getValue(item, "go_id");
+                                toBeDeleted.push({db: goIdDb, accession: goId.substr(goIdDb.length + 1)});
+                            }
+                            goIdTable.removeSelectedRows();
+                            deleteGoIds(toBeDeleted);
+                        });
+                    }
+                    else {
+                        dojo.style(goIdsDiv, "display", "none");
+                    }
+                };
+
+
+                var initComments = function (feature, config) {
+                    if (config.hasComments) {
+                        cannedComments = feature.canned_comments;
+                        var oldComment;
+                        var comments = new dojoItemFileWriteStore({
+                            data: {
+                                items: []
+                            }
+                        });
+                        for (var i = 0; i < feature.comments.length; ++i) {
+                            var comment = feature.comments[i];
+                            comments.newItem({comment: comment});
+                        }
+                        var commentTableLayout = [{
+                            cells: [
+                                {
+                                    name: 'Comment',
+                                    field: 'comment',
+                                    editable: hasWritePermission,
+                                    type: dojox.grid.cells.ComboBox,
+                                    options: cannedComments,
+                                    formatter: function (comment) {
+                                        if (!comment) {
+                                            return "Enter new comment";
+                                        }
+                                        return comment;
+                                    },
+                                    width: "100%"
+                                }
+                            ]
+                        }];
+                        var commentTable = new dojoxDataGrid({
+                            singleClickEdit: true,
+                            store: comments,
+                            structure: commentTableLayout,
+                            updateDelay: 0
+                        });
+
+                        var handle = dojo.connect(AnnotTrack.popupDialog, "onFocus", function () {
+                            initTable(commentTable.domNode, commentsTable, commentTable);
+                            dojo.disconnect(handle);
+                        });
+                        if (reload) {
+                            initTable(commentTable.domNode, commentsTable, commentTable, timeout);
+                        }
+
+                        dojo.connect(commentTable, "onStartEdit", function (inCell, inRowIndex) {
+                            oldComment = commentTable.store.getValue(commentTable.getItem(inRowIndex), "comment");
+                        });
+
+                        dojo.connect(commentTable, "onApplyCellEdit", function (inValue, inRowIndex, inFieldIndex) {
+                            var newComment = inValue;
+                            if (!newComment) {
+                                // alert("No comment");
+                            }
+                            else if (!oldComment) {
+                                addComment(newComment);
+                            }
+                            else {
+                                if (newComment != oldComment) {
+                                    updateComment(oldComment, newComment);
+                                }
+                            }
+                        });
+
+                        dojo.connect(addCommentButton, "onclick", function () {
+                            commentTable.store.newItem({comment: undefined});
+                            commentTable.scrollToRow(commentTable.rowCount);
+                        });
+
+                        dojo.connect(deleteCommentButton, "onclick", function () {
+                            var toBeDeleted = new Array();
+                            var selected = commentTable.selection.getSelected();
+                            for (var i = 0; i < selected.length; ++i) {
+                                var comment = commentTable.store.getValue(selected[i], "comment");
+                                toBeDeleted.push(comment);
+                            }
+                            commentTable.removeSelectedRows();
+                            deleteComments(toBeDeleted);
+                        });
+                    }
+                    else {
+                        dojo.style(commentsDiv, "display", "none");
+                    }
+                };
+
+
+                function updateTimeLastUpdated() {
+                    var date = new Date();
+                    dateLastModifiedField.set("value", FormatUtils.formatDate(date.getTime()));
+                }
+
+                var updateName = function (name) {
+                    name = escapeString(name);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "name": "' + name + '" } ]';
+                    var operation = "set_name";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateSymbol = function (symbol) {
+                    symbol = escapeString(symbol);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "symbol": "' + symbol + '" } ]';
+                    var operation = "set_symbol";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateDescription = function (description) {
+                    description = escapeString(description);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "description": "' + description + '" } ]';
+                    var operation = "set_description";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var deleteStatus = function () {
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "status": "' + status + '" } ]';
+                    var operation = "delete_status";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateStatus = function (status) {
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "status": "' + status + '" } ]';
+                    var operation = "set_status";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var addDbxref = function (db, accession) {
+                    db = escapeString(db);
+                    accession = escapeString(accession);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": [ { "db": "' + db + '", "accession": "' + accession + '" } ] } ]';
+                    var operation = "add_non_primary_dbxrefs";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var deleteDbxrefs = function (dbxrefs) {
+                    for (var i = 0; i < dbxrefs.length; ++i) {
+                        dbxrefs[i].accession = escapeString(dbxrefs[i].accession);
+                        dbxrefs[i].db = escapeString(dbxrefs[i].db);
+                    }
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": ' + JSON.stringify(dbxrefs) + ' } ]';
+                    var operation = "delete_non_primary_dbxrefs";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateDbxref = function (oldDb, oldAccession, newDb, newAccession) {
+                    oldDb = escapeString(oldDb);
+                    oldAccession = escapeString(oldAccession);
+                    newDb = escapeString(newDb);
+                    newAccession = escapeString(newAccession);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "old_dbxrefs": [ { "db": "' + oldDb + '", "accession": "' + oldAccession + '" } ], "new_dbxrefs": [ { "db": "' + newDb + '", "accession": "' + newAccession + '" } ] } ]';
+                    var operation = "update_non_primary_dbxrefs";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var addAttribute = function (tag, value) {
+                    tag = escapeString(tag);
+                    value = escapeString(value);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "non_reserved_properties": [ { "tag": "' + tag + '", "value": "' + value + '" } ] } ]';
+                    var operation = "add_non_reserved_properties";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var deleteAttributes = function (attributes) {
+                    for (var i = 0; i < attributes.length; ++i) {
+                        attributes[i].tag = escapeString(attributes[i].tag);
+                        attributes[i].value = escapeString(attributes[i].value);
+                    }
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "non_reserved_properties": ' + JSON.stringify(attributes) + ' } ]';
+                    var operation = "delete_non_reserved_properties";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateAttribute = function (oldTag, oldValue, newTag, newValue) {
+                    oldTag = escapeString(oldTag);
+                    oldValue = escapeString(oldValue);
+                    newTag = escapeString(newTag);
+                    newValue = escapeString(newValue);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "old_non_reserved_properties": [ { "tag": "' + oldTag + '", "value": "' + oldValue + '" } ], "new_non_reserved_properties": [ { "tag": "' + newTag + '", "value": "' + newValue + '" } ] } ]';
+                    var operation = "update_non_reserved_properties";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var confirmPubmedEntry = function (record) {
+                    return confirm("Publication title: '" + record.PubmedArticleSet.PubmedArticle.MedlineCitation.Article.ArticleTitle + "'");
+                };
+
+                var addPubmedId = function (pubmedIdTable, row, pubmedId) {
+                    var eutils = new EUtils(context_path, track.handleError);
+                    var record = eutils.fetch("pubmed", pubmedId);
+                    if (record) {
+                        // if (eutils.validateId("pubmed", pubmedId)) {
+                        if (confirmPubmedEntry(record)) {
+                            var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": [ { "db": "' + pubmedIdDb + '", "accession": "' + pubmedId + '" } ] } ]';
+                            var operation = "add_non_primary_dbxrefs";
+                            var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                            track.executeUpdateOperation(postData);
+                            updateTimeLastUpdated();
+                        }
+                        else {
+                            pubmedIdTable.store.deleteItem(pubmedIdTable.getItem(row));
+                        }
+                    }
+                    else {
+                        alert("Invalid ID " + pubmedId + " - Removing entry");
+                        pubmedIdTable.store.deleteItem(pubmedIdTable.getItem(row));
+                    }
+                };
+
+                var deletePubmedIds = function (pubmedIds) {
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": ' + JSON.stringify(pubmedIds) + ' } ]';
+                    var operation = "delete_non_primary_dbxrefs";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updatePubmedId = function (pubmedIdTable, row, oldPubmedId, newPubmedId) {
+                    var eutils = new EUtils(context_path, track.handleError);
+                    var record = eutils.fetch("pubmed", newPubmedId);
+                    // if (eutils.validateId("pubmed", newPubmedId)) {
+                    if (record) {
+                        if (confirmPubmedEntry(record)) {
+                            var features = '"features": [ { "uniquename": "' + uniqueName + '", "old_dbxrefs": [ { "db": "' + pubmedIdDb + '", "accession": "' + oldPubmedId + '" } ], "new_dbxrefs": [ { "db": "' + pubmedIdDb + '", "accession": "' + newPubmedId + '" } ] } ]';
+                            var operation = "update_non_primary_dbxrefs";
+                            var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                            track.executeUpdateOperation(postData);
+                            updateTimeLastUpdated();
+                        }
+                        else {
+                            pubmedIdTable.store.setValue(pubmedIdTable.getItem(row), "pubmed_id", oldPubmedId);
+                        }
+                    }
+                    else {
+                        alert("Invalid ID " + newPubmedId + " - Undoing update");
+                        pubmedIdTable.store.setValue(pubmedIdTable.getItem(row), "pubmed_id", oldPubmedId);
+                    }
+                };
+
+                var validateGoId = function (goId) {
+                    var regex = new RegExp("^" + goIdDb + ":(\\d{7})$");
+                    return regex.exec(goId);
+                };
+
+                var addGoId = function (goIdTable, row, goId, valid) {
+                    // if (match = validateGoId(goId)) {
+                    if (valid) {
+                        var goAccession = goId.substr(goIdDb.length + 1);
+                        var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": [ { "db": "' + goIdDb + '", "accession": "' + goAccession + '" } ] } ]';
+                        var operation = "add_non_primary_dbxrefs";
+                        var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                        track.executeUpdateOperation(postData);
+                        updateTimeLastUpdated();
+                    }
+                    else {
+                        alert("Invalid ID " + goId + " - Must be formatted as 'GO:#######' - Removing entry");
+                        goIdTable.store.deleteItem(goIdTable.getItem(row));
+                        goIdTable.close();
+                    }
+                };
+
+                var deleteGoIds = function (goIds) {
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "dbxrefs": ' + JSON.stringify(goIds) + ' } ]';
+                    var operation = "delete_non_primary_dbxrefs";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+// var updateGoId = function(goIdTable, row, oldGoId, newGoId) {
+                var updateGoId = function (goIdTable, item, oldGoId, newGoId, valid) {
+// if (match = validateGoId(newGoId)) {
+                    if (valid) {
+                        var oldGoAccession = oldGoId.substr(goIdDb.length + 1);
+                        var newGoAccession = newGoId.substr(goIdDb.length + 1);
+                        var features = '"features": [ { "uniquename": "' + uniqueName + '", "old_dbxrefs": [ { "db": "' + goIdDb + '", "accession": "' + oldGoAccession + '" } ], "new_dbxrefs": [ { "db": "' + goIdDb + '", "accession": "' + newGoAccession + '" } ] } ]';
+                        var operation = "update_non_primary_dbxrefs";
+                        var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                        track.executeUpdateOperation(postData);
+                        updateTimeLastUpdated();
+                    }
+                    else {
+                        alert("Invalid ID " + newGoId + " - Undoing update");
+// goIdTable.store.setValue(goIdTable.getItem(row), "go_id", oldGoId);
+                        goIdTable.store.setValue(item, "go_id", oldGoId);
+                        goIdTable.close();
+                    }
+                };
+
+                var addComment = function (comment) {
+                    comment = escapeString(comment);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "comments": [ "' + comment + '" ] } ]';
+                    var operation = "add_comments";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var deleteComments = function (comments) {
+                    for (var i = 0; i < comments.length; ++i) {
+                        comments[i] = escapeString(comments[i]);
+                    }
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "comments": ' + JSON.stringify(comments) + ' } ]';
+                    var operation = "delete_comments";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var updateComment = function (oldComment, newComment) {
+                    if (oldComment == newComment) {
+                        return;
+                    }
+                    oldComment = escapeString(oldComment);
+                    newComment = escapeString(newComment);
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '", "old_comments": [ "' + oldComment + '" ], "new_comments": [ "' + newComment + '"] } ]';
+                    var operation = "update_comments";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    track.executeUpdateOperation(postData);
+                    updateTimeLastUpdated();
+                };
+
+                var getCannedComments = function () {
+                    var features = '"features": [ { "uniquename": "' + uniqueName + '" } ]';
+                    var operation = "get_canned_comments";
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }';
+                    dojo.xhrPost({
+                        postData: postData,
+                        url: context_path + "/AnnotationEditorService",
+                        handleAs: "json",
+                        sync: true,
+                        timeout: 5000 * 1000, // Time in milliseconds
+                        load: function (response, ioArgs) {
+                            var feature = response.features[0];
+                            cannedComments = feature.comments;
+                        },
+                        // The ERROR function will be called in an error case.
+                        error: function (response, ioArgs) {
+                            track.handleError(response);
+                            console.error("HTTP status code: ", ioArgs.xhr.status);
+                            return response;
+                        }
+                    });
+                };
+
+                init();
+                return content;
+            },
+
+            undo: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.undoSelectedFeatures(selected);
+            },
+
+            undoSelectedFeatures: function (records) {
+                var track = this;
+                var uniqueNames = [];
+                for (var i in records) {
+                    var record = records[i];
+                    var selfeat = record.feature;
+                    var seltrack = record.track;
+                    var topfeat = AnnotTrack.getTopLevelAnnotation(selfeat);
+                    var uniqueName = topfeat.id();
+                    // just checking to ensure that all features in selection are from
+                    // this track
+                    if (seltrack === track) {
+                        var trackdiv = track.div;
+                        var trackName = track.getUniqueTrackName();
+                        uniqueNames.push(uniqueName);
+                    }
+                }
+                this.undoFeaturesByUniqueName(uniqueNames, 1);
+            },
+
+            undoFeaturesByUniqueName: function (uniqueNames, count) {
+                var track = this;
+                var features = '"features": [';
+                for (var i = 0; i < uniqueNames.length; ++i) {
+                    var uniqueName = uniqueNames[i];
+                    if (i > 0) {
+                        features += ',';
+                    }
+                    features += ' { "uniquename": "' + uniqueName + '" } ';
+                }
+                features += ']';
+                var operation = "undo";
+                var trackName = this.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"' + (count ? ', "count": ' + count : '') + '}';
+                this.executeUpdateOperation(postData, function (response) {
+                    if (response && response.confirm) {
+                        if (track.handleConfirm(response.confirm)) {
+                            postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '", "confirm": true }';
+                            track.executeUpdateOperation(postData);
+                        }
+                    }
+                });
+            },
+
+            redo: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.redoSelectedFeatures(selected);
+            },
+
+            redoSelectedFeatures: function (records) {
+                var track = this;
+                var uniqueNames = [];
+                var features = '"features": [';
+                for (var i in records) {
+                    var record = records[i];
+                    var selfeat = record.feature;
+                    var seltrack = record.track;
+                    var topfeat = AnnotTrack.getTopLevelAnnotation(selfeat);
+                    var uniqueName = topfeat.id();
+                    // just checking to ensure that all features in selection are from
+                    // this track
+                    if (seltrack === track) {
+                        uniqueNames.push(uniqueName);
+                    }
+                }
+                this.redoFeaturesByUniqueName(uniqueNames, 1);
+            },
+
+            redoFeaturesByUniqueName: function (uniqueNames, count) {
+                var features = '"features": [';
+                for (var i = 0; i < uniqueNames.length; ++i) {
+                    var uniqueName = uniqueNames[i];
+                    if (i > 0) {
+                        features += ',';
+                    }
+                    features += ' { "uniquename": "' + uniqueName + '" } ';
+                }
+                features += ']';
+                var operation = "redo";
+                var trackName = this.getUniqueTrackName();
+                var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"' + (count ? ', "count": ' + count : '') + '}';
+                this.executeUpdateOperation(postData);
+            },
+
+            getHistory: function () {
+                var selected = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                this.getHistoryForSelectedFeatures(selected);
+            },
+
+            getHistoryForSelectedFeatures: function (selected) {
+                var track = this;
+                var content = dojo.create("div");
+                var historyDiv = dojo.create("div", {className: "history_div"}, content);
+                var historyTable = dojo.create("div", {className: "history_table"}, historyDiv);
+                var historyHeader = dojo.create("div", {
+                    className: "history_header",
+                    innerHTML: "<span class='history_header_column history_column_operation history_column'>Operation</span><span class='history_header_column history_column'>Editor</span><span class='history_header_column history_column history_column_date'>Date</span><span class='history_header_column history_set'>Revert</span> "
+                }, historyTable);
+                var historyRows = dojo.create("div", {className: "history_rows"}, historyTable);
+                var historyPreviewDiv = dojo.create("div", {className: "history_preview"}, historyDiv);
+                var history;
+                var selectedIndex = 0;
+                var minFmin;
+                var maxFmax;
+                var current;
+                var historyMenu;
+                var canEdit = this.canEdit(selected[0].feature);
+
+                function revert() {
+                    if (selectedIndex == current) {
+                        return;
+                    }
+                    if (selectedIndex < current) {
+                        track.undoFeaturesByUniqueName([history[0].features[0].uniquename], current - selectedIndex);
+                    }
+                    else if (selectedIndex > current) {
+                        track.redoFeaturesByUniqueName([history[0].features[0].uniquename], selectedIndex - current);
+                    }
+                    history[selectedIndex].current = true;
+                    history[current].current = false;
+                    dojo.attr(historyRows.childNodes.item(selectedIndex), "class", history[selectedIndex].current ? "history_row history_row_current" : "history_row");
+                    dojo.attr(historyRows.childNodes.item(current), "class", "history_row");
+                    current = selectedIndex;
+                    cleanUpHistoryTable();
+                    displayHistory();
+                };
+
+                var cleanUpHistoryTable = function() {
+                    while (historyRows.hasChildNodes()) {
+                        historyRows.removeChild(historyRows.lastChild);
+                    }
+                };
+
+                function initMenu() {
+                    historyMenu = new dijitMenu({});
+                    historyMenu.addChild(new dijitMenuItem({
+                        label: "Set as current",
+                        onClick: function () {
+                            revert();
+                        }
+                    }));
+                    historyMenu.startup();
+                }
+
+                var cleanupDiv = function (div) {
+                    if (div.style.top) {
+                        div.style.top = null;
+                    }
+                    if (div.style.visibility) {
+                        div.style.visibility = null;
+                    }
+
+                    // if feature-render annot-render ,
+                    // remove and add: gray-center-10pct
+                    if(div.className.indexOf("feature-render")>=0 &&  div.className.indexOf("annot-render")>=0 ){
+                        div.className = "gray-center-10pct";
+                    }
+// annot_context_menu.unBindDomNode(div);
+                    $(div).unbind();
+                    for (var i = 0; i < div.childNodes.length; ++i) {
+                        cleanupDiv(div.childNodes[i]);
+                    }
+                };
+
+                var displayPreview = function (index) {
+                    var historyItem = history[index];
+                    var afeature = historyItem.features[0];
+                    var jfeature = JSONUtils.createJBrowseFeature(afeature);
+                    var fmin = afeature.location.fmin;
+                    var fmax = afeature.location.fmax;
+                    var maxLength = maxFmax - minFmin;
+// track.featureStore._add_getters(track.attrs.accessors().get, jfeature);
+                    historyPreviewDiv.featureLayout = new Layout(fmin, fmax);
+                    historyPreviewDiv.featureNodes = new Array();
+                    historyPreviewDiv.startBase = minFmin - (maxLength * 0.1);
+                    historyPreviewDiv.endBase = maxFmax + (maxLength * 0.1);
+                    var coords = dojo.position(historyPreviewDiv);
+                    // setting labelScale and descriptionScale parameter to 100 px/bp,
+                    // so neither should get triggered
+                    var featDiv = track.renderFeature(jfeature, jfeature.uid, historyPreviewDiv, coords.w / (maxLength), 100, 100, minFmin, maxFmax, true);
+                    cleanupDiv(featDiv);
+
+                    historyMenu.bindDomNode(featDiv);
+
+                    while (historyPreviewDiv.hasChildNodes()) {
+                        historyPreviewDiv.removeChild(historyPreviewDiv.lastChild);
+                    }
+                    historyPreviewDiv.appendChild(featDiv);
+                    dojo.attr(historyRows.childNodes.item(selectedIndex), "class", history[selectedIndex].current ? "history_row history_row_current" : "history_row");
+                    dojo.attr(historyRows.childNodes.item(index), "class", "history_row history_row_selected");
+                    selectedIndex = index;
+                };
+
+                var cleanOperation = function (inputString) {
+                    return inputString.charAt(0) + inputString.toLowerCase().replace(new RegExp("_", 'g'), " ").slice(1);
+                };
+
+
+                var displayHistory = function () {
+                    for (var i = 0; i < history.length; ++i) {
+                        var historyItem = history[i];
+                        var rowCssClass = "history_row";
+                        var row = dojo.create("div", {className: rowCssClass}, historyRows);
+                        var columnCssClass = "history_column";
+                        dojo.create("span", {
+                            className: columnCssClass + " history_column_operation ",
+                            innerHTML: cleanOperation(historyItem.operation)
+                        }, row);
+                        dojo.create("span", {className: columnCssClass, innerHTML: historyItem.editor}, row);
+                        dojo.create("span", {
+                            className: columnCssClass + " history_column_date",
+                            innerHTML: historyItem.date
+                        }, row);
+                        var afeature = historyItem.features[0];
+                        var fmin = afeature.location.fmin;
+                        var fmax = afeature.location.fmax;
+                        if (minFmin == undefined || fmin < minFmin) {
+                            minFmin = fmin;
+                        }
+                        if (maxFmax == undefined || fmax > maxFmax) {
+                            maxFmax = fmax;
+                        }
+
+                        if (historyItem.current) {
+                            current = i;
+                        }
+
+                        var labelText = "↑";
+                        var isCurrent = true;
+
+                        if (typeof current !== 'undefined') {
+                            if (i == current) {
+                                labelText = "";
+                                isCurrent = false;
+                            }
+                            else if (i > current) {
+                                labelText = "↓";
+                            }
+                            else if (i < current) {
+                                labelText = "↑";
+                            }
+                        }
+
+
+                        if (isCurrent) {
+                            var revertButton = new dijitButton({
+                                label: labelText,
+                                showLabel: true,
+                                style: "color: black",
+                                title: labelText == "↑" ? "Undo" : "Redo",
+                                //iconClass: "dijitIconUndo",
+                                'class': "revert_button",
+                                onClick: function (index) {
+                                    return function () {
+                                        selectedIndex = index;
+                                        revert();
+                                    }
+                                }(i)
+                            });
+
+                            if (!canEdit) {
+                                revertButton.set("disabled", true);
+                            }
+                            dojo.place(revertButton.domNode, row);
+                        }
+                        else {
+                            var currentLabel = new dijitButton({
+                                label: labelText,
+                                showLabel: false,
+                                style: "color: black",
+                                title: 'Original',
+                                iconClass: "dijitIconBookmark",
+                                'class': "revert_button",
+                                onClick: function (index) {
+                                    return function () {
+                                        selectedIndex = index;
+                                        revert();
+                                    }
+                                }(i)
+                            });
+                            dojo.place(currentLabel.domNode, row);
+                        }
+
+                        dojo.connect(row, "onclick", row, function (index) {
+                            return function () {
+                                displayPreview(index);
+                            };
+                        }(i));
+
+                        dojo.connect(row, "oncontextmenu", row, function (index) {
+                            return function () {
+                                displayPreview(index);
+                            };
+                        }(i));
+
+                        historyMenu.bindDomNode(row);
+
+                    }
+                    displayPreview(current);
+                    var coords = dojo.position(row);
+                    historyRows.scrollTop = selectedIndex * coords.h;
+                };
+
+                var fetchHistory = function () {
+                    var features = '"features": [';
+                    for (var i in selected) {
+                        var record = selected[i];
+                        var annot = AnnotTrack.getTopLevelAnnotation(record.feature);
+                        var uniqueName = annot.id();
+                        // just checking to ensure that all features in selection are
+                        // from this track
+                        if (record.track === track) {
+                            var trackdiv = track.div;
+                            var trackName = track.getUniqueTrackName();
+
+                            if (i > 0) {
+                                features += ',';
+                            }
+                            features += ' { "uniquename": "' + uniqueName + '" } ';
+                        }
+                    }
+                    features += ']';
+                    var operation = "get_history_for_features";
+                    var trackName = track.getUniqueTrackName();
+                    dojo.xhrPost({
+                        postData: '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }',
+                        url: context_path + "/AnnotationEditorService",
+                        handleAs: "json",
+                        timeout: 5000 * 1000, // Time in milliseconds
+                        load: function (response, ioArgs) {
+                            var features = response.features;
+// for (var i = 0; i < features.length; ++i) {
+// displayHistory(features[i].history);
+// }
+                            history = features[i].history;
+                            displayHistory();
+                        },
+                        // The ERROR function will be called in an error case.
+                        error: function (response, ioArgs) { //
+                            track.handleError(response);
+                            return response; //
+                        }
+
+                    });
+                };
+
+                initMenu();
+                fetchHistory();
+                this.openDialog("History", content);
+                AnnotTrack.popupDialog.resize();
+                AnnotTrack.popupDialog._position();
+// this.popupDialog.hide();
+// this.openDialog("History", content);
+            },
+
+            getAnnotationInformation: function () {
+                var selected = this.selectionManager.getSelection();
+                this.getInformationForSelectedAnnotations(selected);
+            },
+
+            getInformationForSelectedAnnotations: function (records) {
+                var track = this;
+                var features = '"features": [';
+                var seqtrack = track.getSequenceTrack();
+                for (var i in records) {
+                    var record = records[i];
+                    var selfeat = record.feature;
+                    var seltrack = record.track;
+                    var topfeat = AnnotTrack.getTopLevelAnnotation(selfeat);
+                    var uniqueName = topfeat.id();
+                    // just checking to ensure that all features in selection are from
+                    // this annotation track
+                    // (or from sequence annotation track);
+                    if (seltrack === track || (seqtrack && (seltrack === seqtrack))) {
+                        var trackdiv = track.div;
+                        var trackName = track.getUniqueTrackName();
+
+                        if (i > 0) {
+                            features += ',';
+                        }
+                        features += ' { "uniquename": "' + uniqueName + '" } ';
+                    }
+                }
+                features += ']';
+                var operation = "get_information";
+                var trackName = track.getUniqueTrackName();
+                var information = "";
+                dojo.xhrPost({
+                    postData: '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '" }',
+                    url: context_path + "/AnnotationEditorService",
+                    handleAs: "json",
+                    timeout: 5000 * 1000, // Time in milliseconds
+                    load: function (response, ioArgs) {
+                        for (var i = 0; i < response.features.length; ++i) {
+                            var feature = response.features[i];
+                            if (i > 0) {
+                                information += "<hr/>";
+                            }
+                            information += "Unique id: " + feature.uniquename + "<br/>";
+                            information += "Date of creation: " + feature.time_accessioned + "<br/>";
+                            information += "Owner: " + feature.owner + "<br/>";
+                            if (feature.parent_ids) {
+                                information += "Parent ids: " + feature.parent_ids + "<br/>";
+                            }
+                        }
+                        if (feature.justification) {
+                            information += "Justification: " + feature.justification + "<br/>";
+                        }
+                        track.openDialog("Annotation information", information);
+                    },
+                    // The ERROR function will be called in an error case.
+                    error: function (response, ioArgs) {
+                        track.handleError(response);
+                        console.log("Annotation server error--maybe you forgot to login to the server?");
+                        console.error("HTTP status code: ", ioArgs.xhr.status);
+                        //
+                        // dojo.byId("replace").innerHTML = 'Loading the resource
+                        // from the server did not work';
+                        return response;
+                    }
+                });
+            },
+
+            getGff3: function () {
+                var selected = this.selectionManager.getSelection();
+                this.getGff3ForSelectedFeatures(selected);
+            },
+
+            getGff3ForSelectedFeatures: function (records) {
+                var track = this;
+
+                var content = dojo.create("div", {className: "get_gff3"});
+                var textArea = dojo.create("textarea", {className: "gff3_area", readonly: true}, content);
+
+                var fetchGff3 = function () {
+                    var features = '"features": [';
+                    for (var i = 0; i < records.length; ++i) {
+                        var record = records[i];
+                        var annot = record.feature;
+                        var seltrack = record.track;
+                        var uniqueName = annot.getUniqueName();
+                        // just checking to ensure that all features in selection are
+                        // from this track
+                        if (seltrack === track) {
+                            var trackdiv = track.div;
+                            var trackName = track.getUniqueTrackName();
+
+                            if (i > 0) {
+                                features += ',';
+                            }
+                            features += ' { "uniquename": "' + uniqueName + '" } ';
+                        }
+                    }
+                    features += ']';
+                    var operation = "get_gff3";
+                    var trackName = track.getUniqueTrackName();
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"';
+                    postData += ' }';
+                    dojo.xhrPost({
+                        postData: postData,
+                        url: context_path + "/AnnotationEditorService",
+                        handleAs: "text",
+                        timeout: 5000 * 1000, // Time in milliseconds
+                        load: function (response, ioArgs) {
+                            var textAreaContent = response;
+                            dojo.attr(textArea, "innerHTML", textAreaContent);
+                        },
+                        // The ERROR function will be called in an error case.
+                        error: function (response, ioArgs) {
+                            track.handleError(response);
+                            console.log(response);
+                            console.log("Annotation server error--maybe you forgot to login to the server?");
+                            console.error("HTTP status code: ", ioArgs.xhr.status);
+                            //
+                            // dojo.byId("replace").innerHTML = 'Loading the
+                            // resourcgetFeaturee from the server did not work';
+                            return response;
+                        }
+
+                    });
+                };
+                fetchGff3(records);
+
+                this.openDialog("GFF3", content);
+            },
+
+            getSequence: function () {
+                var selected = this.selectionManager.getSelection();
+                this.getSequenceForSelectedFeatures(selected);
+            },
+
+            getSequenceForSelectedFeatures: function (records) {
+                var track = this;
+
+                var content = dojo.create("div", {className: "get_sequence"});
+                var textArea = dojo.create("textarea", {className: "sequence_area", readonly: true}, content);
+                var form = dojo.create("form", {}, content);
+                var peptideButtonDiv = dojo.create("div", {className: "first_button_div"}, form);
+                var peptideButton = dojo.create("input", {
+                    type: "radio",
+                    name: "type",
+                    checked: true
+                }, peptideButtonDiv);
+                var peptideButtonLabel = dojo.create("label", {
+                    innerHTML: "Peptide sequence",
+                    className: "button_label"
+                }, peptideButtonDiv);
+                var cdnaButtonDiv = dojo.create("div", {className: "button_div"}, form);
+                var cdnaButton = dojo.create("input", {type: "radio", name: "type"}, cdnaButtonDiv);
+                var cdnaButtonLabel = dojo.create("label", {
+                    innerHTML: "cDNA sequence",
+                    className: "button_label"
+                }, cdnaButtonDiv);
+                var cdsButtonDiv = dojo.create("div", {className: "button_div"}, form);
+                var cdsButton = dojo.create("input", {type: "radio", name: "type"}, cdsButtonDiv);
+                var cdsButtonLabel = dojo.create("label", {
+                    innerHTML: "CDS sequence",
+                    className: "button_label"
+                }, cdsButtonDiv);
+                var genomicButtonDiv = dojo.create("div", {className: "button_div"}, form);
+                var genomicButton = dojo.create("input", {type: "radio", name: "type"}, genomicButtonDiv);
+                var genomicButtonLabel = dojo.create("label", {
+                    innerHTML: "Genomic sequence",
+                    className: "button_label"
+                }, genomicButtonDiv);
+                var genomicWithFlankButtonDiv = dojo.create("div", {className: "button_div"}, form);
+                var genomicWithFlankButton = dojo.create("input", {
+                    type: "radio",
+                    name: "type"
+                }, genomicWithFlankButtonDiv);
+                var genomicWithFlankButtonLabel = dojo.create("label", {
+                    innerHTML: "Genomic sequence +/-",
+                    className: "button_label"
+                }, genomicWithFlankButtonDiv);
+                var genomicWithFlankField = dojo.create("input", {
+                    type: "text",
+                    size: 5,
+                    className: "button_field",
+                    value: "500"
+                }, genomicWithFlankButtonDiv);
+                var genomicWithFlankFieldLabel = dojo.create("label", {
+                    innerHTML: "bases",
+                    className: "button_label"
+                }, genomicWithFlankButtonDiv);
+
+                var fetchSequence = function (type) {
+                    var features = '"features": [';
+                    for (var i = 0; i < records.length; ++i) {
+                        var record = records[i];
+                        var annot = record.feature;
+                        var seltrack = record.track;
+                        var uniqueName = annot.getUniqueName();
+                        // just checking to ensure that all features in selection are
+                        // from this track
+                        if (seltrack === track) {
+                            var trackdiv = track.div;
+                            var trackName = track.getUniqueTrackName();
+
+                            if (i > 0) {
+                                features += ',';
+                            }
+                            features += ' { "uniquename": "' + uniqueName + '" } ';
+                        }
+                    }
+                    features += ']';
+                    var operation = "get_sequence";
+                    var trackName = track.getUniqueTrackName();
+                    var postData = '{ "track": "' + trackName + '", ' + features + ', "operation": "' + operation + '"';
+                    var flank = 0;
+                    if (type == "genomic_with_flank") {
+                        flank = dojo.attr(genomicWithFlankField, "value");
+                        postData += ', "flank": ' + flank;
+                        type = "genomic";
+                    }
+                    postData += ', "type": "' + type + '" }';
+                    dojo.xhrPost({
+                        postData: postData,
+                        url: context_path + "/AnnotationEditorService",
+                        handleAs: "json",
+                        timeout: 5000 * 1000, // Time in milliseconds
+                        load: function (response, ioArgs) {
+                            var textAreaContent = "";
+                            for (var i = 0; i < response.features.length; ++i) {
+                                var feature = response.features[i];
+                                var cvterm = feature.type;
+                                var residues = feature.residues;
+                                var loc = feature.location;
+                                textAreaContent += ">" + feature.uniquename + " (" + cvterm.cv.name + ":" + cvterm.name + ") " + residues.length + " residues [" + track.refSeq.name + ":" + (loc.fmin + 1) + "-" + loc.fmax + " " + (loc.strand == -1 ? "-" : loc.strand == 1 ? "+" : "no") + " strand] [" + type + (flank > 0 ? " +/- " + flank + " bases" : "") + "]\n";
+                                var lineLength = 70;
+                                for (var j = 0; j < residues.length; j += lineLength) {
+                                    textAreaContent += residues.substr(j, lineLength) + "\n";
+                                }
+                            }
+                            dojo.attr(textArea, "innerHTML", textAreaContent);
+                        },
+                        // The ERROR function will be called in an error case.
+                        error: function (response, ioArgs) {
+                            track.handleError(response);
+                            console.log("Annotation server error--maybe you forgot to login to the server?");
+                            console.error("HTTP status code: ", ioArgs.xhr.status);
+                            //
+                            // dojo.byId("replace").innerHTML = 'Loading the
+                            // resource from the server did not work';
+                            return response;
+                        }
+
+                    });
+                };
+                var callback = function (event) {
+                    var type;
+                    var target = event.target || event.srcElement;
+                    if (target == peptideButton || target == peptideButtonLabel) {
+                        dojo.attr(peptideButton, "checked", true);
+                        type = "peptide";
+                    }
+                    else if (target == cdnaButton || target == cdnaButtonLabel) {
+                        dojo.attr(cdnaButton, "checked", true);
+                        type = "cdna";
+                    }
+                    else if (target == cdsButton || target == cdsButtonLabel) {
+                        dojo.attr(cdsButton, "checked", true);
+                        type = "cds";
+                    }
+                    else if (target == genomicButton || target == genomicButtonLabel) {
+                        dojo.attr(genomicButton, "checked", true);
+                        type = "genomic";
+                    }
+                    else if (target == genomicWithFlankButton || target == genomicWithFlankButtonLabel) {
+                        dojo.attr(genomicWithFlankButton, "checked", true);
+                        type = "genomic_with_flank";
+                    }
+                    fetchSequence(type);
+                };
+
+                dojo.connect(peptideButton, "onchange", null, callback);
+                dojo.connect(peptideButtonLabel, "onclick", null, callback);
+                dojo.connect(cdnaButton, "onchange", null, callback);
+                dojo.connect(cdnaButtonLabel, "onclick", null, callback);
+                dojo.connect(cdsButton, "onchange", null, callback);
+                dojo.connect(cdsButtonLabel, "onclick", null, callback);
+                dojo.connect(genomicButton, "onchange", null, callback);
+                dojo.connect(genomicButtonLabel, "onclick", null, callback);
+                dojo.connect(genomicWithFlankButton, "onchange", null, callback);
+                dojo.connect(genomicWithFlankButtonLabel, "onclick", null, callback);
+
+                fetchSequence("peptide");
+                this.openDialog("Sequence", content);
+            },
+
+            searchSequence: function () {
+                var track = this;
+                var starts = new Object();
+                var browser = track.gview.browser;
+                for (var i in browser.allRefs) {
+                    var refSeq = browser.allRefs[i];
+                    starts[refSeq.name] = refSeq.start;
+                }
+                var search = new SequenceSearch(context_path);
+                search.setRedirectCallback(function (id, fmin, fmax) {
+                    var loc = id + ":" + fmin + "-" + fmax;
+                    var locobj = {
+                        ref: id,
+                        start: fmin,
+                        end: fmax
+                    };
+                    if (id == track.refSeq.name) {
+                        // track.gview.browser.navigateTo(loc);
+                        var highlightSearchedRegions = track.gview.browser.config.highlightSearchedRegions;
+                        track.gview.browser.config.highlightSearchedRegions = true;
+                        track.gview.browser.showRegionWithHighlight(locobj);
+                        track.gview.browser.config.highlightSearchedRegions = highlightSearchedRegions;
+                        // AnnotTrack.popupDialog.hide();
+                    }
+                    else {
+                        // var url = window.location.toString().replace(/loc=.+/, "loc=" +
+                        // loc);
+                        // window.location.replace(url);
+                        var highlightSearchedRegions = track.gview.browser.config.highlightSearchedRegions;
+                        track.gview.browser.config.highlightSearchedRegions = true;
+                        track.gview.browser.showRegionWithHighlight(locobj);
+                        track.gview.browser.config.highlightSearchedRegions = highlightSearchedRegions;
+                        // AnnotTrack.popupDialog.hide();
+                    }
+                });
+                search.setErrorCallback(function (response) {
+                    track.handleError(response);
+                });
+                var content = search.searchSequence(track.getUniqueTrackName(), track.refSeq.name, starts);
+                if (content) {
+                    this.openDialog("Search sequence", content);
+                }
+            },
+
+            exportData: function (key, options) {
+                var track = this;
+                var adapter = key;
+                var highlight = track.gview.browser.getHighlight();
+                var content = dojo.create("div");
+                var waitingDiv = dojo.create("div", {innerHTML: "<img class='waiting_image' src='plugins/WebApollo/img/loading.gif' />"}, content);
+                var responseDiv = dojo.create("div", {className: "export_response"}, content);
+
+                if(key=="highlighted region" && highlight==null){
+                    alert('You must highlight a region of the genome to export it.');
+                    return ;
+                }
+
+                if(highlight){
+                    options += "&region="+highlight ;
+                }
+                var url = context_path + "/IOService?operation=write&adapter=" + adapter + "&sequences=" + track.getUniqueTrackName() + "&" + options;
+
+                dojo.xhrGet({
+                    url: url ,
+                    handleAs: "json",
+                    load: function (response, ioArgs) {
+                        dojo.create("a", {
+                            innerHTML: response.filename,
+                            href: context_path + "/IOService/download?uuid=" + response.uuid + "&exportType=" + response.exportType + "&seqType=" + response.sequenceType + "&format=" + response.format
+                        }, content);
+                        dojo.style(waitingDiv, {display: "none"});
+                    },
+                    // The ERROR function will be called in an error case.
+                    error: function (response, ioArgs) {
+                        dojo.style(waitingDiv, {display: "none"});
+                        responseDiv.innerHTML = "Unable to export data";
+                        track.handleError(response);
+                    }
+                });
+                track.openDialog("Export " + key, content);
+            },
+
+            zoomToBaseLevel: function (event) {
+                var coordinate = this.getGenomeCoord(event);
+                this.gview.zoomToBaseLevel(event, coordinate);
+            },
+
+
+            scrollToNextEdge: function (event) {
+                // var coordinate = this.getGenomeCoord(event);
+                var track = this;
+                var vregion = this.gview.visibleRegion();
+                var coordinate = (vregion.start + vregion.end) / 2;
+                var selected = this.selectionManager.getSelection();
+
+                function centerAtBase(position) {
+                    track.gview.centerAtBase(position, false);
+                    track.selectionManager.removeFromSelection(selected[0]);
+                    var subfeats = selfeat.get("subfeatures");
+                    for (var i = 0; i < subfeats.length; ++i) {
+                        if (track.selectionManager.unselectableTypes[subfeats[i].get("type")]) {
+                            continue;
+                        }
+                        // skip CDS features
+                        if (SequenceOntologyUtils.cdsTerms[subfeats[i].get("type")] || subfeats[i].get("type") == "wholeCDS") {
+                            continue;
+                        }
+                        if (position >= subfeats[i].get("start") && position <= subfeats[i].get("end")) {
+                            track.selectionManager.addToSelection({feature: subfeats[i], track: track});
+                            break;
+                        }
+                    }
+                };
+                if (selected && (selected.length > 0)) {
+
+
+                    var selfeat = selected[0].feature;
+                    // find current center genome coord, compare to subfeatures,
+                    // figure out nearest subfeature right of center of view
+                    // if subfeature overlaps, go to right edge
+                    // else go to left edge
+                    // if to left, move to left edge
+                    // if to right,
+                    while (selfeat.parent()) {
+                        selfeat = selfeat.parent();
+                    }
+                    // only support scrolling if the feature isn't fully visible
+                    if (vregion.start <= selfeat.get("start") && vregion.end >= selfeat.get("end")) {
+                        return;
+                    }
+                    var coordDelta = Number.MAX_VALUE;
+                    var pmin = selfeat.get('start');
+                    var pmax = selfeat.get('end');
+                    if ((coordinate - pmax) > 10) {
+                        centerAtBase(pmin);
+                    }
+                    else {
+                        var childfeats = selfeat.children();
+                        for (var i = 0; i < childfeats.length; i++) {
+                            var cfeat = childfeats[i];
+                            var cmin = cfeat.get('start');
+                            var cmax = cfeat.get('end');
+                            // if (cmin > coordinate) {
+                            if ((cmin - coordinate) > 10) { // fuzz factor of 10 bases
+                                coordDelta = Math.min(coordDelta, cmin - coordinate);
+                            }
+                            // if (cmax > coordinate) {
+                            if ((cmax - coordinate) > 10) { // fuzz factor of 10 bases
+                                coordDelta = Math.min(coordDelta, cmax - coordinate);
+                            }
+                        }
+                        // find closest edge right of current coord
+                        if (coordDelta != Number.MAX_VALUE) {
+                            var newCenter = coordinate + coordDelta;
+                            centerAtBase(newCenter);
+                        }
+                    }
+                }
+            },
+
+            scrollToPreviousEdge: function (event) {
+                // var coordinate = this.getGenomeCoord(event);
+                var track = this;
+                var vregion = this.gview.visibleRegion();
+                var coordinate = (vregion.start + vregion.end) / 2;
+                var selected = this.selectionManager.getSelection();
+
+                function centerAtBase(position) {
+                    track.gview.centerAtBase(position, false);
+                    track.selectionManager.removeFromSelection(selected[0]);
+                    var subfeats = selfeat.get("subfeatures");
+                    for (var i = 0; i < subfeats.length; ++i) {
+                        if (track.selectionManager.unselectableTypes[subfeats[i].get("type")]) {
+                            continue;
+                        }
+                        // skip CDS features
+                        if (SequenceOntologyUtils.cdsTerms[subfeats[i].get("type")] || subfeats[i].get("type") == "wholeCDS") {
+                            continue;
+                        }
+                        if (position >= subfeats[i].get("start") && position <= subfeats[i].get("end")) {
+                            track.selectionManager.addToSelection({feature: subfeats[i], track: track});
+                            break;
+                        }
+                    }
+                };
+                if (selected && (selected.length > 0)) {
+
+
+                    var selfeat = selected[0].feature;
+                    // find current center genome coord, compare to subfeatures,
+                    // figure out nearest subfeature right of center of view
+                    // if subfeature overlaps, go to right edge
+                    // else go to left edge
+                    // if to left, move to left edge
+                    // if to right,
+                    while (selfeat.parent()) {
+                        selfeat = selfeat.parent();
+                    }
+                    // only support scrolling if the feature isn't fully visible
+                    if (vregion.start <= selfeat.get("start") && vregion.end >= selfeat.get("end")) {
+                        return;
+                    }
+                    var coordDelta = Number.MAX_VALUE;
+                    var pmin = selfeat.get('start');
+                    var pmax = selfeat.get('end');
+                    if ((pmin - coordinate) > 10) {
+                        centerAtBase(pmax);
+                    }
+                    else {
+                        var childfeats = selfeat.children();
+                        for (var i = 0; i < childfeats.length; i++) {
+                            var cfeat = childfeats[i];
+                            var cmin = cfeat.get('start');
+                            var cmax = cfeat.get('end');
+                            // if (cmin > coordinate) {
+                            if ((coordinate - cmin) > 10) { // fuzz factor of 10 bases
+                                coordDelta = Math.min(coordDelta, coordinate - cmin);
+                            }
+                            // if (cmax > coordinate) {
+                            if ((coordinate - cmax) > 10) { // fuzz factor of 10 bases
+                                coordDelta = Math.min(coordDelta, coordinate - cmax);
+                            }
+                        }
+                        // find closest edge right of current coord
+                        if (coordDelta != Number.MAX_VALUE) {
+                            var newCenter = coordinate - coordDelta;
+                            centerAtBase(newCenter);
+                        }
+                    }
+                }
+            },
+
+            scrollToNextTopLevelFeature: function () {
+                var selected = this.selectionManager.getSelection();
+                if (!selected || !selected.length) {
+                    return;
+                }
+                var features = [];
+                for (var i in this.store.features) {
+                    features.push(this.store.features[i]);
+                }
+                this.sortAnnotationsByLocation(features);
+                var idx = this.binarySearch(features, AnnotTrack.getTopLevelAnnotation(selected[0].feature));
+                if (idx < 0 || idx >= features.length - 1) {
+                    return;
+                }
+                this.gview.centerAtBase(features[idx + 1].get("start"));
+                this.selectionManager.removeFromSelection({feature: selected[0].feature, track: this});
+                this.selectionManager.addToSelection({feature: features[idx + 1], track: this});
+            },
+
+            scrollToPreviousTopLevelFeature: function () {
+                var selected = this.selectionManager.getSelection();
+                if (!selected || !selected.length) {
+                    return;
+                }
+                var features = [];
+                for (var i in this.store.features) {
+                    features.push(this.store.features[i]);
+                }
+                this.sortAnnotationsByLocation(features);
+                var idx = this.binarySearch(features, AnnotTrack.getTopLevelAnnotation(selected[0].feature));
+                if (idx <= 0 || idx > features.length - 1) {
+                    return;
+                }
+                this.gview.centerAtBase(features[idx - 1].get("end"));
+                this.selectionManager.removeFromSelection({feature: selected[0].feature, track: this});
+                this.selectionManager.addToSelection({feature: features[idx - 1], track: this});
+            },
+
+            binarySearch: function (features, feature) {
+                var from = 0;
+                var to = features.length - 1;
+                while (from <= to) {
+                    var mid = from + ((to - from) >> 1);
+                    if (feature.get("start") == features[mid].get("start") && feature.get("end") == features[mid].get("end") && feature.id() == features[mid].id()) {
+                        return mid;
+                    }
+                    if (feature.get("start") == features[mid].get("start")) {
+                        if (feature.get("end") < features[mid].get("end")) {
+                            to = mid - 1;
+                        }
+                        else if (feature.get("end") > features[mid].get("end")) {
+                            from = mid + 1;
+                        }
+                        else {
+                            if (feature.id() < features[mid].id()) {
+                                to = mid - 1;
+                            }
+                            else {
+                                from = mid + 1;
+                            }
+                        }
+                    }
+                    else if (feature.get("start") < features[mid].get("start")) {
+                        to = mid - 1;
+                    }
+                    else {
+                        from = mid + 1;
+                    }
+                }
+                return -1;
+            },
+
+            zoomBackOut: function (event) {
+                this.gview.zoomBackOut(event);
+            },
+
+            handleError: function (response) {
+                console.log("ERROR: ");
+                console.log(response);  // in Firebug, allows retrieval of stack trace,
+                                        // jump to code, etc.
+                var error = response.responseText && response.responseText.match("^<") != "<" ? JSON.parse(response.responseText) : response.response.data;
+                if (error && error.error) {
+                    alert(error.error);
+                    console.log(error.error);
+                    return false;
+                }
+            },
+
+            handleConfirm: function (response) {
+                return confirm(response);
+            },
+
+            logout: function () {
+                dojo.xhrPost({
+                    url: context_path + "/Login?operation=logout",
+                    handleAs: "json",
+                    timeout: 5 * 1000, // Time in milliseconds
+                    // The LOAD function will be called on a successful response.
+                    load: function (response, ioArgs) { //
+                        if (this.getApollo()) {
+                            this.getApollo().location.reload();
+                        }
+                        else {
+                            window.location.reload();
+                        }
+                    },
+                    error: function (response, ioArgs) { //
+                        console.log('Failed to log out cleanly.  May already be logged out.');
+                    }
+                });
+            },
+
+            showAnnotatorPanel: function(){
+                // http://asdfasfasdf/asfsdf/asdfasdf/apollo/<organism ID / client token>/jbrowse/index.html?loc=Group9.10%3A501752..501878&highlight=&tracklist=1&tracks=DNA%2CAnnotations&nav=1&overview=1
+                // to
+                // /apollo/annotator/loadLink?loc=Group9.10:501765..501858&organism=16&tracks=&clientToken=1315746673267340807380563276
+                var hrefString = window.location.href;
+                var hrefTokens = hrefString.split("\/");
+                var organism ;
+                for(var h in hrefTokens){
+                    // alert(hrefTokens[h]);
+                    if(hrefTokens[h]=="jbrowse"){
+                        organism = hrefTokens[h-1] ;
+                    }
+                }
+
+                // NOTE: Here is where you customize your view into Apollo, by adding / changing parameters
+
+                var jbrowseString = "/jbrowse/index.html?";
+                var jbrowseIndex = hrefString.indexOf(jbrowseString);
+                var params = hrefString.substring(jbrowseIndex + jbrowseString.length);
+                params = params.replace("tracklist=1","tracklist=0");
+                var finalString =  "../../annotator/loadLink?"+params + "&organism=" + organism ;
+                if(params.indexOf("&clientToken=")<0){
+                    finalString += "&clientToken="+this.getClientToken();
+                }
+                window.location.href = finalString;
+            },
+
+            login: function () {
+                this.showAnnotatorPanel();
+                // var track = this;
+                // dojo.xhrGet({
+                //     url: context_path + "/Login",
+                //     handleAs: "text",
+                //     timeout: 5 * 60,
+                //     load: function (response, ioArgs) {
+                //         var dialog = new dojoxDialogSimple({
+                //             preventCache: true,
+                //             refreshOnShow: true,
+                //             executeScripts: true
+                //
+                //         });
+                //         if (track.browser.config.disableJBrowseMode) {
+                //             dialog.hide = function () {
+                //             };
+                //         }
+                //         dialog.startup();
+                //         dialog.set("title", "Login");
+                //         dialog.set("content", response);
+                //         dialog.show();
+                //     }
+                // });
+            },
+
+            initLoginMenu: function () {
+                var track = this;
+                var browser = this.gview.browser;
+                var loginButton;
+                if (this.permission) {   // permission only set if permission request
+                    // succeeded
+                    browser.addGlobalMenuItem('user',
+                        new dijitMenuItem({
+                            label: 'Logout',
+                            onClick: function () {
+                                console.log("clicked stub for logging out");
+                                // attempted to do
+                                // client-side session
+                                // cookie deletion, but
+                                // doesn't
+                                // work because JSESSIONID
+                                // is flagged as "HttpOnly"
+                                // document.cookie =
+                                // "JSESSIONID=;
+                                // path=/ApolloWeb/";
+
+                                // reload page after
+                                // removing session cookie?
+                                dojo.xhrPost({
+                                    url: context_path + "/Login?operation=logout",
+                                    handleAs: "json",
+                                    timeout: 5 * 1000, // Time
+                                    // in
+                                    // milliseconds
+                                    // The LOAD function
+                                    // will be called on a
+                                    // successful response.
+                                    load: function (response, ioArgs) { //
+                                        if (this.getApollo()){
+                                          this.getApollo().location.reload();
+                                        }
+                                        else {
+                                            window.location.reload();
+                                        }
+                                    },
+                                    error: function (response, ioArgs) { //
+                                        alert('Failed to log out cleanly!  Please refresh your browser.');
+                                    }
+                                });
+                            }
+                        })
+                    );
+                    var userMenu = browser.makeGlobalMenu('user');
+                    loginButton = new dijitDropDownButton(
+                        {
+                            className: 'user',
+                            innerHTML: '<span class="usericon"></span>' + this.username,
+                            title: 'user logged in: UserName',
+                            dropDown: userMenu
+                        });
+                }
+                else {
+                    loginButton = new dijitButton(
+                        {
+                            className: 'login',
+                            innerHTML: "Login",
+                            onClick: function () {
+                                dojo.xhrGet({
+                                    url: context_path + "/Login?operation=login",
+                                    handleAs: "text",
+                                    timeout: 5 * 60,
+                                    load: function (response, ioArgs) {
+                                        track.openDialog("Login", response);
+                                    }
+                                });
+                            }
+                        });
+                }
+                browser.afterMilestone('initView', function () {
+                    // must append after menubar is created, plugin constructor called
+                    // before menubar exists,
+                    // browser.initView called after menubar exists
+                    browser.menuBar.appendChild(loginButton.domNode);
+                });
+            },
+
+            initAnnotContextMenu: function () {
+                var thisB = this;
+                contextMenuItems = new Array();
+                annot_context_menu = new dijit.Menu({});
+                var permission = thisB.permission;
+                var index = 0;
+                annot_context_menu.addChild(new dijit.MenuItem({
+                    label: "Get Sequence",
+                    onClick: function (event) {
+                        thisB.getSequence();
+                    }
+                }));
+                contextMenuItems["get_sequence"] = index++;
+
+                annot_context_menu.addChild(new dijit.MenuItem({
+                    label: "Get GFF3",
+                    onClick: function (event) {
+                        thisB.getGff3();
+                    }
+                }));
+                contextMenuItems["get_gff3"] = index++;
+
+                annot_context_menu.addChild(new dijit.MenuItem({
+                    label: "Zoom to Base Level",
+                    onClick: function (event) {
+                        if (thisB.getMenuItem("zoom_to_base_level").get("label") == "Zoom to Base Level") {
+                            thisB.zoomToBaseLevel(thisB.annot_context_mousedown);
+                        }
+                        else {
+                            thisB.zoomBackOut(thisB.annot_context_mousedown);
+                        }
+                    }
+                }));
+                contextMenuItems["zoom_to_base_level"] = index++;
+                if (!(permission & Permission.WRITE)) {
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Information Editor (alt-click)",
+                        onClick: function (event) {
+                            thisB.getAnnotationInfoEditor();
+                        }
+                    }));
+                    contextMenuItems["annotation_info_editor"] = index++;
+                }
+                if (permission & Permission.WRITE) {
+                    //annot_context_menu.addChild(new dijit.MenuSeparator());
+                    //index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Edit Information (alt-click)",
+                        onClick: function (event) {
+                            thisB.getAnnotationInfoEditor();
+                        }
+                    }));
+
+                    var changeAnnotationMenu = new dijitMenu();
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "gene",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("mRNA");
+                        }
+
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "pseudogene",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("transcript");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "rRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("rRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "snRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("snRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "snoRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("snoRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "tRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("tRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "ncRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("ncRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "miRNA",
+                        onClick: function(event) {
+                            thisB.changeAnnotationType("miRNA");
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "repeat_region",
+                        onClick: function(event) {
+                            var selected = thisB.selectionManager.getSelection();
+                            var selectedFeatureType = selected[0].feature.afeature.type.name === "exon" ?
+                                selected[0].feature.afeature.parent_type.name : selected[0].feature.afeature.type.name;
+                            if (selectedFeatureType != "transposable_element") {
+                                var message = "Warning: You will not be able to revert back to " + selectedFeatureType + " via 'Change annotation type' menu option, use 'Undo' instead. Do you want to proceed?";
+                                thisB.confirmChangeAnnotationType(thisB, [selected], "repeat_region", message);
+                            }
+                            else {
+                                thisB.changeAnnotationType("repeat_region");
+                            }
+                        }
+                    }));
+                    changeAnnotationMenu.addChild(new dijitMenuItem( {
+                        label: "transposable_element",
+                        onClick: function(event) {
+                            var selected = thisB.selectionManager.getSelection();
+                            var selectedFeatureType = selected[0].feature.afeature.type.name === "exon" ?
+                                selected[0].feature.afeature.parent_type.name : selected[0].feature.afeature.type.name;
+                            if (selectedFeatureType != "repeat_region") {
+                                var message = "Warning: You will not be able to revert back to " + selectedFeatureType + " via 'Change annotation type' menu option, use 'Undo' instead. Do you want to proceed?";
+                                thisB.confirmChangeAnnotationType(thisB, [selected], "transposable_element", message);
+                            }
+                            else {
+                                thisB.changeAnnotationType("transposable_element");
+                            }
+                        }
+                    }));
+                    contextMenuItems["annotation_info_editor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+                    var changeAnnotationMenuItem = new dijitPopupMenuItem( {
+                        label: "Change annotation type",
+                        popup: changeAnnotationMenu
+                    });
+                    annot_context_menu.addChild(changeAnnotationMenuItem);
+                    dojo.connect(changeAnnotationMenu, "onOpen", dojo.hitch(this, function() {
+                        this.updateChangeAnnotationTypeMenu(changeAnnotationMenu);
+                    }));
+                    contextMenuItems["annotation_info_editor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Delete",
+                        onClick: function () {
+                            thisB.deleteSelectedFeatures();
+                        }
+                    }));
+                    contextMenuItems["delete"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Merge",
+                        onClick: function () {
+                            thisB.mergeSelectedFeatures();
+                        }
+                    }));
+                    contextMenuItems["merge"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Split",
+                        onClick: function (event) {
+                            // use annot_context_mousedown instead of current event, since
+                            // want to split
+                            // at mouse position of event that triggered annot_context_menu
+                            // popup
+                            thisB.splitSelectedFeatures(thisB.annot_context_mousedown);
+                        }
+                    }));
+                    contextMenuItems["split"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Duplicate",
+                        onClick: function (event) {
+                            // use annot_context_mousedown instead of current event, since
+                            // want to split
+                            // at mouse position of event that triggered annot_context_menu
+                            // popup
+                            thisB.duplicateSelectedFeatures(thisB.annot_context_mousedown);
+                        }
+                    }));
+                    contextMenuItems["duplicate"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Make Intron",
+                        // use annot_context_mousedown instead of current event, since want
+                        // to split
+                        // at mouse position of event that triggered annot_context_menu
+                        // popup
+                        onClick: function (event) {
+                            thisB.makeIntron(thisB.annot_context_mousedown);
+                        }
+                    }));
+                    contextMenuItems["make_intron"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Move to Opposite Strand",
+                        onClick: function (event) {
+                            thisB.flipStrand();
+                        }
+                    }));
+                    contextMenuItems["flip_strand"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set Translation Start",
+                        // use annot_context_mousedown instead of current event, since want
+                        // to split
+                        // at mouse position of event that triggered annot_context_menu
+                        // popup
+                        onClick: function (event) {
+                            thisB.setTranslationStart(thisB.annot_context_mousedown);
+                        }
+                    }));
+                    contextMenuItems["set_translation_start"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set Translation End",
+                        // use annot_context_mousedown instead of current event, since want
+                        // to split
+                        // at mouse position of event that triggered annot_context_menu
+                        // popup
+                        onClick: function (event) {
+                            thisB.setTranslationEnd(thisB.annot_context_mousedown);
+                        }
+                    }));
+                    contextMenuItems["set_translation_end"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set Longest ORF",
+                        // use annot_context_mousedown instead of current event, since want
+                        // to split
+                        // at mouse position of event that triggered annot_context_menu
+                        // popup
+                        onClick: function (event) {
+                            thisB.setLongestORF();
+                        }
+                    }));
+                    contextMenuItems["set_longest_orf"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set Readthrough Stop Codon",
+                        onClick: function (event) {
+                            thisB.setReadthroughStopCodon();
+                        }
+                    }));
+                    contextMenuItems["set_readthrough_stop_codon"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set as 5' end",
+                        onClick: function (event) {
+                            thisB.setAsFivePrimeEnd();
+                        }
+                    }));
+                    contextMenuItems["set_as_five_prime_end"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set as 3' End",
+                        onClick: function (event) {
+                            thisB.setAsThreePrimeEnd();
+                        }
+                    }));
+                    contextMenuItems["set_as_three_prime_end"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set both Ends",
+                        onClick: function (event) {
+                            thisB.setBothEnds();
+                        }
+                    }));
+                    contextMenuItems["set_both_ends"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+                    contextMenuItems["set_downstream_donor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set to Downstream Splice Donor",
+                        onClick: function (event) {
+                            thisB.setToDownstreamDonor();
+                        }
+                    }));
+                    contextMenuItems["set_upstream_donor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set to Upstream Splice Donor",
+                        onClick: function (event) {
+                            thisB.setToUpstreamDonor();
+                        }
+                    }));
+                    contextMenuItems["set_downstream_acceptor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set to Downstream Splice Acceptor",
+                        onClick: function (event) {
+                            thisB.setToDownstreamAcceptor();
+                        }
+                    }));
+                    contextMenuItems["set_upstream_acceptor"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Set to Upstream Splice Acceptor",
+                        onClick: function (event) {
+                            thisB.setToUpstreamAcceptor();
+                        }
+                    }));
+                    annot_context_menu.addChild(new dijit.MenuSeparator());
+                    index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Undo",
+                        onClick: function (event) {
+                            thisB.undo();
+                        }
+                    }));
+                    contextMenuItems["undo"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Redo",
+                        onClick: function (event) {
+                            thisB.redo();
+                        }
+                    }));
+                    contextMenuItems["redo"] = index++;
+                    annot_context_menu.addChild(new dijit.MenuItem({
+                        label: "Show History",
+                        onClick: function (event) {
+                            thisB.getHistory();
+                        }
+                    }));
+                    contextMenuItems["history"] = index++;
+                }
+
+                annot_context_menu.onOpen = function (event) {
+                    // keeping track of mousedown event that triggered annot_context_menu popup
+                    // because need mouse position of that event for some actions
+                    thisB.annot_context_mousedown = thisB.last_mousedown_event;
+                    var selected = thisB.selectionManager.getSelection();
+                    if (selected.length == 0) {
+                        // if there is no selection then close the menu
+                        thisB.closeMenu();
+                    }
+                    else {
+                        if (thisB.permission & Permission.WRITE) {
+                            thisB.updateMenu();
+                        }
+                        dojo.forEach(this.getChildren(), function (item, idx, arr) {
+                            if (item instanceof dijit.MenuItem) {
+                                item._setSelected(false);
+                                // check for _onUnhover, since latest
+                                // dijit.MenuItem does not have _onUnhover()
+                                // method
+                                if (item._onUnhover) {
+                                    item._onUnhover();
+                                }
+                            }
+                        });
+                    }
+                };
+
+                annot_context_menu.startup();
+            },
+
+
+            /**
+             * Add AnnotTrack data save option to track label pulldown menu Trying to make
+             * it a replacement for default JBrowse data save option from ExportMixin
+             * (turned off JBrowse default via config.noExport = true)
+             */
+            initSaveMenu: function () {
+                var track = this;
+                dojo.xhrPost({
+                    sync: true,
+                    postData: '{ "track": "' + track.getUniqueTrackName() + '", "operation": "get_data_adapters" }',
+                    url: context_path + "/AnnotationEditorService",
+                    handleAs: "json",
+                    timeout: 5 * 1000, // Time in milliseconds
+                    // The LOAD function will be called on a successful response.
+                    load: function (response, ioArgs) { //
+                        var dataAdapters = response.data_adapters;
+                        for (var i = 0; i < dataAdapters.length; ++i) {
+                            var dataAdapter = dataAdapters[i];
+                            if (track.permission & dataAdapter.permission) {
+                                track.exportAdapters.push(dataAdapter);
+                            }
+                        }
+                        // remake track label pulldown menu so will include
+                        // dataAdapter submenu
+                        track.makeTrackMenu();
+                    },
+                    error: function (response, ioArgs) { //
+                    }
+                });
+            },
+
+            makeTrackMenu: function () {
+                this.inherited(arguments);
+                var track = this;
+                var options = this._trackMenuOptions();
+                if (options && options.length && this.label && this.labelMenuButton && this.exportAdapters.length > 0) {
+                    var dataAdaptersMenu = new dijit.Menu();
+                    for (var i = 0; i < this.exportAdapters.length; i++) {
+                        var dataAdapter = this.exportAdapters[i];
+                        if (dataAdapter.data_adapters) {
+                            var submenu = new dijit.Menu({
+                                label: dataAdapter.key
+                            });
+                            dataAdaptersMenu.addChild(new dijit.PopupMenuItem({
+                                label: dataAdapter.key,
+                                popup: submenu
+                            }));
+                            for (var j = 0; j < dataAdapter.data_adapters.length; ++j) {
+                                var subAdapter = dataAdapter.data_adapters[j];
+                                submenu.addChild(new dijit.MenuItem({
+                                    label: subAdapter.key,
+                                    onClick: function (key, options) {
+                                        return function () {
+                                            track.exportData(key, options);
+                                        };
+                                    }(subAdapter.key, subAdapter.options)
+                                }));
+                            }
+                        }
+                        else {
+                            dataAdaptersMenu.addChild(new dijit.MenuItem({
+                                label: dataAdapter.key,
+                                onClick: function (key, options) {
+                                    return function () {
+                                        track.exportData(key, options);
+                                    };
+                                }(dataAdapter.key, dataAdapter.options)
+                            }));
+                        }
+                    }
+                    // if there's a menu separator, add right before first seperator (which
+                    // is where default save is added),
+                    // otherwise add at end
+                    var mitems = this.trackMenu.getChildren();
+                    for (var mindex = 0; mindex < mitems.length; mindex++) {
+                        if (mitems[mindex].type == "dijit/MenuSeparator") {
+                            break;
+                        }
+                    }
+
+                    var savePopup = new dijit.PopupMenuItem({
+                        label: "Save track data",
+                        iconClass: 'dijitIconSave',
+                        popup: dataAdaptersMenu
+                    });
+                    this.trackMenu.addChild(savePopup, mindex);
+                }
+            },
+
+            _trackMenuOptions: function() {
+                var thisB = this;
+                var browser = this.browser;
+                var clabel = this.name + "-collapsed";
+                var options = this.inherited(arguments) || [];
+                // specifically removing these two options for AnnotTrack
+                options = this.webapollo.removeItemWithLabel(options, "Pin to top");
+                options = this.webapollo.removeItemWithLabel(options, "Delete track");
+                return options;
+            },
+
+            getPermission: function (callback) {
+                var thisB = this;
+                var loadCallback = callback;
+                var success = true;
+                dojo.xhrPost({
+                    sync: true,
+                    postData: '{ "track": "' + thisB.getUniqueTrackName() + '", "operation": "get_user_permission" ,"clientToken":' + thisB.getClientToken() + '}',
+                    url: context_path + "/AnnotationEditorService",
+                    handleAs: "json",
+                    timeout: 5 * 1000, // Time in milliseconds
+                    // The LOAD function will be called on a successful response.
+                    load: function (response, ioArgs) { //
+                        var permission = response.permission;
+                        thisB.permission = permission;
+                        var username = response.username;
+                        thisB.username = username;
+                        if (loadCallback) {
+                            loadCallback(permission);
+                        }
+                    },
+                    error: function (response, ioArgs) { //
+                        success = false;
+                    }
+                });
+                return success;
+            },
+
+            initPopupDialog: function () {
+                if (AnnotTrack.popupDialog) {
+                    return;
+                }
+                var track = this;
+                var id = "popup_dialog";
+
+                // deregister widget (needed if changing refseq without reloading page)
+                var widget = registry.byId(id);
+                if (widget) {
+                    widget.destroy();
+                }
+                AnnotTrack.popupDialog = new dojoxDialogSimple({
+                    preventCache: true,
+                    refreshOnShow: true,
+                    executeScripts: true,
+                    id: id
+                });
+                dojo.connect(AnnotTrack.popupDialog, "onHide", AnnotTrack.popupDialog, function () {
+                    document.activeElement.blur();
+                    track.selectionManager.clearSelection();
+                    if (track.getSequenceTrack()) {
+                        track.getSequenceTrack().clearHighlightedBases();
+                    }
+
+                    dojo.style(dojo.body(), 'overflow', 'auto');
+                    document.body.scroll = ''; // needed for ie6/7
+
+
+                });
+
+                dojo.connect(AnnotTrack.popupDialog, 'onShow', function () {
+                    dojo.style(dojo.body(), 'overflow', 'hidden');
+                    document.body.scroll = 'no'; // needed for ie6/7
+                });
+
+
+                AnnotTrack.popupDialog.startup();
+
+            },
+
+            getUniqueTrackName: function () {
+                return this.refSeq.name;
+            },
+
+            openDialog: function (title, data, width, height) {
+                AnnotTrack.popupDialog.set("title", title);
+                AnnotTrack.popupDialog.set("content", data);
+                AnnotTrack.popupDialog.set("style", "width:" + (width ? width : "auto") + ";height:" + (height ? height : "auto"));
+                AnnotTrack.popupDialog.show();
+            },
+
+            closeDialog: function () {
+                AnnotTrack.popupDialog.hide();
+            },
+
+            updateMenu: function () {
+                this.updateDeleteMenuItem();
+                this.updateSetTranslationStartMenuItem();
+                this.updateSetTranslationEndMenuItem();
+                this.updateSetLongestOrfMenuItem();
+                this.updateSetReadthroughStopCodonMenuItem();
+                this.updateMergeMenuItem();
+                this.updateSplitMenuItem();
+                this.updateMakeIntronMenuItem();
+                this.updateFlipStrandMenuItem();
+                this.updateAnnotationInfoEditorMenuItem();
+                this.updateUndoMenuItem();
+                this.updateRedoMenuItem();
+                this.updateZoomToBaseLevelMenuItem();
+                this.updateDuplicateMenuItem();
+                this.updateHistoryMenuItem();
+                this.updateSetAsFivePrimeEndMenuItem();
+                this.updateSetAsThreePrimeEndMenuItem();
+                this.updateSetBothEndsMenuItem();
+                this.updateSetNextDonorMenuItem();
+                this.updateSetPreviousDonorMenuItem();
+                this.updateSetNextAcceptorMenuItem();
+                this.updateSetPreviousAcceptorMenuItem();
+            },
+
+            closeMenu: function() {
+                dijit.popup.close(this.annot_context_menu);
+            },
+
+            updateChangeAnnotationTypeMenu: function(changeAnnotationMenu) {
+                var selected = this.selectionManager.getSelection();
+                var selectedType = selected[0].feature.afeature.type.name === "exon" ?
+                    selected[0].feature.afeature.parent_type.name : selected[0].feature.afeature.type.name;
+                var menuItems = changeAnnotationMenu.getChildren();
+                for (var i in menuItems) {
+                    if (selectedType === "mRNA") {
+                        if (menuItems[i].label === "gene") {
+                            menuItems[i].setDisabled(true);
+                        }
+                        else {
+                            menuItems[i].setDisabled(false);
+                        }
+                    }
+                    else if (selectedType === "transcript") {
+                        if (menuItems[i].label === "pseudogene") {
+                            menuItems[i].setDisabled(true);
+                        }
+                        else {
+                            menuItems[i].setDisabled(false);
+                        }
+                    }
+                    else if (selectedType === "miRNA" || selectedType == "snRNA" || selectedType === "snoRNA" ||
+                        selectedType === "rRNA" || selectedType === "tRNA" || selectedType === "ncRNA") {
+                        if (menuItems[i].label === selectedType) {
+                            menuItems[i].setDisabled(true);
+                        }
+                        else {
+                            menuItems[i].setDisabled(false);
+                        }
+                    }
+                    else if (selectedType === "repeat_region") {
+                        if (menuItems[i].label === "transposable_element") {
+                            menuItems[i].setDisabled(false);
+                        }
+                        else {
+                            menuItems[i].setDisabled(true);
+                        }
+                    }
+                    else if (selectedType === "transposable_element") {
+                        if (menuItems[i].label === "repeat_region") {
+                            menuItems[i].setDisabled(false);
+                        }
+                        else {
+                            menuItems[i].setDisabled(true);
+                        }
+                    }
+                    else {
+                        menuItems[i].setDisabled(false);
+                    }
+                }
+            },
+
+            updateDeleteMenuItem: function () {
+                var menuItem = this.getMenuItem("delete");
+                var selected = this.selectionManager.getSelection();
+                for (var i = 0; i < selected.length; ++i) {
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetTranslationStartMenuItem: function () {
+                var menuItem = this.getMenuItem("set_translation_start");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                for (var i = 0; i < selected.length; ++i) {
+                    if (!this.isProteinCoding(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+                var selectedFeat = selected[0].feature;
+                if (selectedFeat.parent()) {
+                    selectedFeat = selectedFeat.parent();
+                }
+                if (!this.canEdit(selectedFeat)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedFeat.get('manuallySetTranslationStart')) {
+                    menuItem.set("label", "Unset Translation Start");
+                }
+                else {
+                    menuItem.set("label", "Set Translation Start");
+                }
+            },
+
+            updateSetTranslationEndMenuItem: function () {
+                var menuItem = this.getMenuItem("set_translation_end");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                for (var i = 0; i < selected.length; ++i) {
+                    if (!this.isProteinCoding(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+                var selectedFeat = selected[0].feature;
+                if (selectedFeat.parent()) {
+                    selectedFeat = selectedFeat.parent();
+                }
+                if (!this.canEdit(selectedFeat)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedFeat.get('manuallySetTranslationEnd')) {
+                    menuItem.set("label", "Unset Translation End");
+                }
+                else {
+                    menuItem.set("label", "Set Translation End");
+                }
+            },
+
+            updateSetLongestOrfMenuItem: function () {
+                var menuItem = this.getMenuItem("set_longest_orf");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                for (var i = 0; i < selected.length; ++i) {
+                    if (!this.isProteinCoding(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+
+                menuItem.set("disabled", false);
+            },
+
+            updateSetReadthroughStopCodonMenuItem: function () {
+                var menuItem = this.getMenuItem("set_readthrough_stop_codon");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                for (var i = 0; i < selected.length; ++i) {
+                    if (!this.isProteinCoding(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+                var selectedFeat = selected[0].feature;
+                if (selectedFeat.parent()) {
+                    selectedFeat = selectedFeat.parent();
+                }
+                if (selectedFeat.get('readThroughStopCodon')) {
+                    menuItem.set("label", "Unset Readthrough Stop Codon");
+                }
+                else {
+                    menuItem.set("label", "Set Readthrough Stop Codon");
+                }
+            },
+
+            updateMergeMenuItem: function () {
+                var menuItem = this.getMenuItem("merge");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length < 2) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var strand = selected[0].feature.get('strand');
+                for (var i = 1; i < selected.length; ++i) {
+                    if (selected[i].feature.get('strand') != strand) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSplitMenuItem: function () {
+                var menuItem = this.getMenuItem("split");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 2) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selected.length == 1) {
+                    if (!selected[0].feature.parent()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!selected[0].feature.afeature.parent_id) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[0].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                var parent = selected[0].feature.parent();
+                if (!parent) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                for (var i = 1; i < selected.length; ++i) {
+                    if (selected[i].feature.parent() != parent) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateMakeIntronMenuItem: function () {
+                var menuItem = this.getMenuItem("make_intron");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!selected[0].feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selected[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (SequenceOntologyUtils.neverHasExons[selected[0].feature.get("type")]) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateFlipStrandMenuItem: function () {
+                var menuItem = this.getMenuItem("flip_strand");
+                var selected = this.selectionManager.getSelection();
+                for (var i = 0; i < selected.length; ++i) {
+                    if (selected[i].feature.get("strand") == 0) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                    if (!this.canEdit(selected[i].feature)) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+
+            },
+
+            updateAnnotationInfoEditorMenuItem: function () {
+                var menuItem = this.getMenuItem("annotation_info_editor");
+                var selected = this.selectionManager.getSelection();
+                var parent = AnnotTrack.getTopLevelAnnotation(selected[0].feature);
+                for (var i = 1; i < selected.length; ++i) {
+                    if (AnnotTrack.getTopLevelAnnotation(selected[i].feature) != parent) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateUndoMenuItem: function () {
+                var menuItem = this.getMenuItem("undo");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selected[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateRedoMenuItem: function () {
+                var menuItem = this.getMenuItem("redo");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selected[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+
+            updateHistoryMenuItem: function () {
+                var menuItem = this.getMenuItem("history");
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateZoomToBaseLevelMenuItem: function () {
+                var menuItem = this.getMenuItem("zoom_to_base_level");
+                if (!this.gview.isZoomedToBase()) {
+                    menuItem.set("label", "Zoom to Base Level");
+                }
+                else {
+                    menuItem.set("label", "Zoom Back Out");
+                }
+            },
+
+            updateDuplicateMenuItem: function () {
+                var menuItem = this.getMenuItem("duplicate");
+                var selected = this.selectionManager.getSelection();
+                var parent = AnnotTrack.getTopLevelAnnotation(selected[0].feature);
+                for (var i = 1; i < selected.length; ++i) {
+                    if (AnnotTrack.getTopLevelAnnotation(selected[i].feature) != parent) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetAsFivePrimeEndMenuItem: function () {
+                var menuItem = this.getMenuItem("set_as_five_prime_end");
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                if (selectedAnnots.length > 1 || selectedEvidence.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedEvidence.length == 0) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!selectedAnnots[0].feature.parent() || !selectedEvidence[0].feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedAnnots[0].feature.get("strand") != selectedEvidence[0].feature.get("strand")) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetAsThreePrimeEndMenuItem: function () {
+                var menuItem = this.getMenuItem("set_as_three_prime_end");
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                if (selectedAnnots.length > 1 || selectedEvidence.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedEvidence.length == 0) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!selectedAnnots[0].feature.parent() || !selectedEvidence[0].feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedAnnots[0].feature.get("strand") != selectedEvidence[0].feature.get("strand")) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetBothEndsMenuItem: function () {
+                var menuItem = this.getMenuItem("set_both_ends");
+                var selectedAnnots = this.selectionManager.getSelection();
+                var selectedEvidence = this.webapollo.featSelectionManager.getSelection();
+                if (selectedAnnots.length > 1 || selectedEvidence.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedEvidence.length == 0) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!selectedAnnots[0].feature.parent() || !selectedEvidence[0].feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (selectedAnnots[0].feature.get("strand") != selectedEvidence[0].feature.get("strand")) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetNextDonorMenuItem: function () {
+                var menuItem = this.getMenuItem("set_downstream_donor");
+                var selectedAnnots = this.selectionManager.getSelection();
+                if (selectedAnnots.length != 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var feature = selectedAnnots[0].feature;
+                if (!SequenceOntologyUtils.exonTerms[feature.get("type")]) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var subfeatures = feature.parent().get("subfeatures");
+                var exons = [];
+                for (var i = 0; i < subfeatures.length; ++i) {
+                    if (SequenceOntologyUtils.exonTerms[subfeatures[i].get("type")]) {
+                        exons.push(subfeatures[i]);
+                    }
+                }
+                this.sortAnnotationsByLocation(exons);
+                if (feature.get("strand") == -1) {
+                    if (feature.id() == exons[0].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                else {
+                    if (feature.id() == exons[exons.length - 1].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetPreviousDonorMenuItem: function () {
+                var menuItem = this.getMenuItem("set_upstream_donor");
+                var selectedAnnots = this.selectionManager.getSelection();
+                if (selectedAnnots.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var feature = selectedAnnots[0].feature;
+                if (!SequenceOntologyUtils.exonTerms[feature.get("type")]) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var subfeatures = feature.parent().get("subfeatures");
+                var exons = [];
+                for (var i = 0; i < subfeatures.length; ++i) {
+                    if (SequenceOntologyUtils.exonTerms[subfeatures[i].get("type")]) {
+                        exons.push(subfeatures[i]);
+                    }
+                }
+                this.sortAnnotationsByLocation(exons);
+                if (feature.get("strand") == -1) {
+                    if (feature.id() == exons[0].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                else {
+                    if (feature.id() == exons[exons.length - 1].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetNextAcceptorMenuItem: function () {
+                var menuItem = this.getMenuItem("set_downstream_acceptor");
+                var selectedAnnots = this.selectionManager.getSelection();
+                if (selectedAnnots.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var feature = selectedAnnots[0].feature;
+                if (!SequenceOntologyUtils.exonTerms[feature.get("type")]) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var subfeatures = feature.parent().get("subfeatures");
+                var exons = [];
+                for (var i = 0; i < subfeatures.length; ++i) {
+                    if (SequenceOntologyUtils.exonTerms[subfeatures[i].get("type")]) {
+                        exons.push(subfeatures[i]);
+                    }
+                }
+                this.sortAnnotationsByLocation(exons);
+                if (feature.get("strand") == -1) {
+                    if (feature.id() == exons[exons.length - 1].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                else {
+                    if (feature.id() == exons[0].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            updateSetPreviousAcceptorMenuItem: function () {
+                var menuItem = this.getMenuItem("set_upstream_acceptor");
+                var selectedAnnots = this.selectionManager.getSelection();
+                if (selectedAnnots.length > 1) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var feature = selectedAnnots[0].feature;
+                if (!SequenceOntologyUtils.exonTerms[feature.get("type")]) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!feature.parent()) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                if (!this.canEdit(selectedAnnots[0].feature)) {
+                    menuItem.set("disabled", true);
+                    return;
+                }
+                var subfeatures = feature.parent().get("subfeatures");
+                var exons = [];
+                for (var i = 0; i < subfeatures.length; ++i) {
+                    if (SequenceOntologyUtils.exonTerms[subfeatures[i].get("type")]) {
+                        exons.push(subfeatures[i]);
+                    }
+                }
+                this.sortAnnotationsByLocation(exons);
+                if (feature.get("strand") == -1) {
+                    if (feature.id() == exons[exons.length - 1].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                else {
+                    if (feature.id() == exons[0].id()) {
+                        menuItem.set("disabled", true);
+                        return;
+                    }
+                }
+                menuItem.set("disabled", false);
+            },
+
+            getMenuItem: function (operation) {
+                return annot_context_menu.getChildren()[contextMenuItems[operation]];
+            },
+
+            sortAnnotationsByLocation: function (annots) {
+                var track = this;
+                return annots.sort(function (annot1, annot2) {
+                    var start1 = annot1.get("start");
+                    var end1 = annot1.get("end");
+                    var start2 = annot2.get("start");
+                    var end2 = annot2.get('end');
+
+                    if (start1 != start2) {
+                        return start1 - start2;
+                    }
+                    else if (end1 != end2) {
+                        return end1 - end2;
+                    }
+                    else {
+                        return annot1.id().localeCompare(annot2.id());
+                    }
+                });
+            },
+
+            confirmChangeAnnotationType: function (track, selectedFeatures, destinationType, message) {
+                var confirm = new ConfirmDialog({
+                    title: 'Change annotation type',
+                    message: message,
+                    confirmLabel: 'Yes',
+                    denyLabel: 'Cancel'
+                }).show(function (confirmed) {
+                    if (confirmed) {
+                        track.changeAnnotationType(destinationType);
+                    }
+                });
+            },
+
+            /**
+             * handles adding overlay of sequence residues to "row" of selected feature
+             * (also handled in similar manner in fillBlock()); WARNING: this _requires_
+             * browser support for pointer-events CSS property, (currently supported by
+             * Firefox 3.6+, Chrome 4.0+, Safari 4.0+) (Exploring possible workarounds
+             * for IE, for example see:
+             * http://www.vinylfox.com/forwarding-mouse-events-through-layers/
+             * http://stackoverflow.com/questions/3680429/click-through-a-div-to-underlying-elements [
+             * see section on CSS conditional statement workaround for IE ] ) and must
+             * set "pointer-events: none" in CSS rule for div.annot-sequence otherwise,
+             * since sequence overlay is rendered on top of selected features (and is a
+             * sibling of feature divs), events intended for feature divs will get
+             * caught by overlay and not make it to the feature divs
+             */
+            selectionAdded: function (rec, smanager) {
+                var feat = rec.feature;
+                this.inherited(arguments);
+                var track = this;
+
+                // switched to only have most recent selected annot have residues overlay if
+                // zoomed to base level,
+                // rather than all selected annots
+                // therefore want to revove all prior residues overlay divs
+                if (rec.track === track) {
+                    // remove sequence text nodes
+                    $("div.annot-sequence", track.div).remove();
+                }
+
+                // want to get child of block, since want position relative to block
+                // so get top-level feature div (assumes top level feature is always
+                // rendered...)
+                var topfeat = AnnotTrack.getTopLevelAnnotation(feat);
+                var featdiv = track.getFeatDiv(topfeat);
+                if (featdiv) {
+                    if (this.currentResizableFeature && feat.id() == this.currentResizableFeature.id()) {
+                        this.makeResizable(this.getFeatDiv(feat));
+                    }
+                    var strand = topfeat.get('strand');
+                    var selectionYPosition = $(featdiv).position().top;
+                    var scale = track.gview.bpToPx(1);
+                    var charSize = track.webapollo.getSequenceCharacterSize();
+                    if (scale === charSize.width && track.useResiduesOverlay) {
+                        var seqTrack = this.getSequenceTrack();
+                        for (var bindex = this.firstAttached; bindex <= this.lastAttached; bindex++) {
+                            var blk = this.blocks[bindex];
+                            // seqTrack.getRange(block.startBase, block.endBase,
+                            // seqTrack.sequenceStore.getRange(this.refSeq,
+                            // block.startBase, block.endBase,
+                            // seqTrack.sequenceStore.getFeatures({ ref: this.refSeq.name, start:
+                            // block.startBase, end: block.endBase },
+                            // function(feat) {
+                            seqTrack.sequenceStore.getReferenceSequence(
+                                {ref: this.refSeq.name, start: blk.startBase, end: blk.endBase},
+                                function (block) {
+                                    return function (seq) {
+                                        // var start = feat.get('start');
+                                        // var end = feat.get('end');
+                                        // var seq = feat.get('seq');
+                                        var start = block.startBase;
+                                        var end = block.endBase;
+
+                                        // var ypos = $(topfeat).position().top;
+                                        // +2 hardwired adjustment to center (should be
+                                        // calc'd based on feature div dims?
+                                        var ypos = selectionYPosition + 2;
+                                        // checking to see if residues for this "row" of the
+                                        // block are already present
+                                        // ( either from another selection in same row, or
+                                        // previous rendering
+                                        // of same selection [which often happens when
+                                        // scrolling] )
+                                        // trying to avoid duplication both for efficiency
+                                        // and because re-rendering of text can
+                                        // be slighly off from previous rendering, leading
+                                        // to bold / blurry text when overlaid
+
+                                        var $seqdivs = $("div.annot-sequence", block.domNode);
+                                        var sindex = $seqdivs.length;
+                                        var add_residues = true;
+                                        if ($seqdivs && sindex > 0) {
+                                            for (var i = 0; i < sindex; i++) {
+                                                var sdiv = $seqdivs[i];
+                                                if ($(sdiv).position().top === ypos) {
+                                                    // console.log("residues already present
+                                                    // in block: " + bindex);
+                                                    add_residues = false;
+                                                }
+                                            }
+                                        }
+                                        if (add_residues) {
+                                            var seqNode = document.createElement("div");
+                                            seqNode.className = "annot-sequence";
+                                            if (strand == '-' || strand == -1) {
+                                                // seq = track.reverseComplement(seq);
+                                                seq = track.getSequenceTrack().complement(seq);
+                                            }
+                                            seqNode.appendChild(document.createTextNode(seq));
+                                            // console.log("ypos: " + ypos);
+                                            seqNode.style.cssText = "top: " + ypos + "px;";
+                                            block.domNode.appendChild(seqNode);
+                                            if (track.FADEIN_RESIDUES) {
+                                                $(seqNode).hide();
+                                                $(seqNode).fadeIn(1500);
+                                            }
+                                        }
+                                    };
+                                }(blk));
+                        }
+                    }
+                }
+            },
+
+            selectionRemoved: function (selected_record, smanager) {
+                // console.log("AnnotTrack.selectionRemoved() called");
+                this.inherited(arguments);
+                var track = this;
+                if (selected_record.track === track) {
+                    var feat = selected_record.feature;
+                    var featdiv = this.getFeatDiv(feat);
+                    // remove sequence text nodes
+                    // console.log("removing base residued text from selected annot");
+                    $("div.annot-sequence", track.div).remove();
+                    delete this.currentResizableFeature;
+                    $(featdiv).resizable("destroy");
+                }
+            },
+
+            startZoom: function (destScale, destStart, destEnd) {
+
+                this.inherited(arguments);
+
+                var selected = this.selectionManager.getSelection();
+                if (selected.length > 0) {
+                    // if selected annotations, then hide residues overlay
+                    // (in case zoomed in to base pair resolution and the residues
+                    // overlay is being displayed)
+                    $(".annot-sequence", this.div).css('display', 'none');
+                }
+            },
+
+            executeUpdateOperation: function (postData, loadCallback) {
+                if(!postData.hasOwnProperty('clientToken')){
+                    var postObject = JSON.parse(postData);
+                    postObject.clientToken = this.getClientToken();
+                    postData = JSON.stringify(postObject);
+                }
+                console.log('connected and sending notifications');
+                this.client.send("/app/AnnotationNotification", {}, JSON.stringify(postData));
+                console.log('sent notification message');
+            },
+
+            isProteinCoding: function (feature) {
+                var topLevelFeature = AnnotTrack.getTopLevelAnnotation(feature);
+                if (topLevelFeature.afeature.parent_type && topLevelFeature.afeature.parent_type.name == "gene" && (topLevelFeature.get("type") == "transcript" || topLevelFeature.get("type") == "mRNA")) {
+                    return true;
+                }
+                return false;
+            },
+
+            isLoggedIn: function () {
+                return this.username != undefined && this.username != 'Guest';
+            },
+
+            hasWritePermission: function () {
+                return this.permission & Permission.WRITE;
+            },
+
+            isAdmin: function () {
+                return this.permission & Permission.ADMIN;
+            },
+
+            canEdit: function (feature) {
+                if (feature) {
+                    feature = AnnotTrack.getTopLevelAnnotation(feature);
+                }
+                return this.hasWritePermission() && (feature ? !feature.get("locked") : true);
+            },
+
+            processParent: function (feature, operation) {
+                var parentId = feature.parent_id;
+                if (parentId) {
+                    var topLevelFeatures = this.topLevelParents[parentId] || (this.topLevelParents[parentId] = {});
+                    switch (operation) {
+                        case "ADD":
+                            topLevelFeatures[feature.uniquename] = feature;
+                            break;
+                        case "DELETE":
+                            delete topLevelFeatures[feature.uniquename];
+                            break;
+                        case "UPDATE":
+                            topLevelFeatures[feature.uniquename] = feature;
+                            break;
+                    }
+                }
+            }
+
+        });
+
+        AnnotTrack.getTopLevelAnnotation = function (annotation) {
+            while (annotation.parent()) {
+                annotation = annotation.parent();
+            }
+            return annotation;
+        };
+
+        return AnnotTrack;
+    });
+
+/*
+ * Copyright (c) 2010-2011 Berkeley Bioinformatics Open Projects (BBOP)
+ *
+ * This package and its accompanying libraries are free software; you can
+ * redistribute it and/or modify it under the terms of the LGPL (either version
+ * 2.1, or at your option, any later version) or the Artistic License 2.0. Refer
+ * to LICENSE for the full license text.
+ */
diff --git a/client/apollo/js/View/Track/DraggableAlignments.js b/client/apollo/js/View/Track/DraggableAlignments.js
new file mode 100644
index 0000000..86cefb2
--- /dev/null
+++ b/client/apollo/js/View/Track/DraggableAlignments.js
@@ -0,0 +1,127 @@
+define([
+           'dojo/_base/declare', 
+           'dojo/_base/array',
+           'JBrowse/View/Track/Alignments',
+           'WebApollo/View/Track/DraggableHTMLFeatures', 
+           'JBrowse/Util'
+       ],
+       function(
+           declare,
+           array, 
+           AlignmentsTrack,
+           DraggableTrack, 
+           Util
+       ) {
+
+return declare([ DraggableTrack, AlignmentsTrack ], {
+
+    constructor: function( args )  {
+    // forcing store to create subfeatures, unless config.subfeatures explicitly set to false
+    //     default is set to true in _defaultConfig()
+        //  this.store.createSubfeatures = this.config.subfeatures;
+        if (this.config.style.showSubfeatures) { this.store.createSubfeatures = true; }
+    }, 
+
+    _defaultConfig: function()  {
+        var thisConfig = Util.deepUpdate(
+//       return Util.deepUpdate(
+            dojo.clone( this.inherited(arguments) ),
+            {
+                layoutPitchY: 2, 
+//                subfeatures: true,
+                maxFeatureScreenDensity: 0.5,
+                style: {
+                    className: "bam-read", 
+                    renderClassName: null, 
+                    arrowheadClass: null, 
+                    centerChildrenVertically: false, 
+                    showSubfeatures: true, 
+                    showMismatches: true, 
+                    showLabels: false, 
+                    showMismatchResidues: true,  // when rendering mismatches, whether to render residues text when zoomed in and sizing works
+                    subfeatureClasses: {
+                        M: "cigarM", 
+        //              D: "cigarD",
+                        D: null, // not rendering deletions as subfeats, relying on drawMismatches instead
+                        N: "cigarN",
+                        E: "cigarEQ",  /* "=" converted to "E" in BAM/LazyFeature subfeature construction */
+                        X: "cigarX", 
+        //              I: "cigarI"
+                        I: null // not rendering insertions as subfeats, relying on drawMismatches instead
+                    }
+                }
+            }
+        );
+        return thisConfig;
+    }, 
+
+    /**
+     * draw base-mismatches on the feature
+     */
+    _drawMismatches: function( feature, featDiv, scale, displayStart, displayEnd ) {
+        var featLength = displayEnd - displayStart;
+        // recall: scale is pixels/basepair
+        // if ( featLength*scale > 1 && scale >= 1) {  // alternatively, also require zoomed in scale min (1px/bp  in this case)
+        if ( featLength*scale > 1) {     
+            var mismatches = this._getMismatches( feature );
+            var charSize = this.getCharacterMeasurements();
+            var drawChars = (this.config.style.showMismatchResidues && 
+                             (scale >= charSize.w) && 
+                             (charSize.h <= (this.glyphHeight + this.glyphHeightPad))
+                             );
+            array.forEach( mismatches, function( mismatch ) {
+                var start = feature.get('start') + mismatch.start;
+                // GAH: _MismatchesMixin._getMismatches is creating insertions with length = 1, 
+                //   but length of insertion should really by 0, since JBrowse internally uses zero-interbase coordinates
+                //   fixing here for now, since changing in _MismatchesMixin could have unexpected consequences
+                // if (mismatch.type == 'insertion') { mismatch.length = 0; }
+                var end = start + mismatch.length;
+
+                // if the feature has been truncated to where it doesn't cover
+                // this mismatch anymore, just skip this mismatch
+                if ( end <= displayStart || start >= displayEnd )
+                    return;
+
+                var base = mismatch.base;
+                var mDisplayStart = Math.max( start, displayStart );
+                var mDisplayEnd = Math.min( end, displayEnd );
+                var mDisplayWidth = mDisplayEnd - mDisplayStart;
+                var overall = dojo.create('span',  {
+                    className: 'align_'+mismatch.type + ' ' + mismatch.type + ' base_'+base.toLowerCase(),
+                    style: {
+                        position: 'absolute',
+                        left: 100 * ( mDisplayStart - displayStart)/featLength + '%',
+                        width: scale*mDisplayWidth>1 ? 100 * mDisplayWidth/featLength + '%' : '1px'
+                    }
+                }, featDiv );
+                overall.mismatch = mismatch;
+
+                // give the mismatch a mouseover if not drawing a character with the mismatch base
+                if( ! drawChars ) {
+                    if (mismatch.type == 'deletion') { overall.title = mismatch.length; }
+                    else  { overall.title = base; }
+                }
+
+                if( drawChars && mismatch.length <= 20 ) {
+                    for( var i = 0; i<mismatch.length; i++ ) {
+                        var basePosition = start + i;
+                        if( basePosition >= mDisplayStart && basePosition <= mDisplayEnd ) {
+                            dojo.create('span',{
+                                            className: 'base base_'+base.toLowerCase(),
+                                            style: {
+                                                position: 'absolute',
+                                                width: scale+'px',
+                                                left: (basePosition-mDisplayStart)/mDisplayWidth*100 + '%'
+                                            },
+                                            innerHTML: base
+                                        }, overall );
+                        }
+                    }
+                }
+            }, this );
+        }
+    }
+
+} );
+
+});
diff --git a/client/apollo/js/View/Track/DraggableBLASTFeatures.js b/client/apollo/js/View/Track/DraggableBLASTFeatures.js
new file mode 100644
index 0000000..63552ed
--- /dev/null
+++ b/client/apollo/js/View/Track/DraggableBLASTFeatures.js
@@ -0,0 +1,30 @@
+define( [
+         'dojo/_base/declare',
+         'WebApollo/View/Track/DraggableHTMLFeatures',
+		 'jquery'
+         ],
+	function(declare, DraggableFeatureTrack,$) {
+	    
+	    var DraggableBLASTFeatures = declare(DraggableFeatureTrack, {   
+		    constructor: function(args)  {  },
+		    
+		    bitscore_color: function (bitscore) {
+			var colors = ["#5e5e5e","#53739c","#508f62","#845e93","#bd4939"];
+			var thresholds = [72,104,136,168,200];
+			for (var i = 0; i < colors.length - 1 && bitscore >= thresholds[i]; i++) {}
+			return colors[i];
+		    },
+		    
+		    renderSubfeature: function(feature, featDiv, subfeature, displayStart, displayEnd, block)  {
+			var subfeatdiv = this.inherited(arguments);
+			var bitscore = parseInt(subfeature.get('bitscore'));
+			if (subfeatdiv && bitscore)  {  // just in case subFeatDiv doesn't actually get created
+			    var $subfeatdiv = $(subfeatdiv);
+			    $subfeatdiv.css("background-color", this.bitscore_color(bitscore));
+			}
+			return subfeatdiv;
+		    }
+		});
+	    
+	    return DraggableBLASTFeatures;
+	});
\ No newline at end of file
diff --git a/client/apollo/js/View/Track/DraggableHTMLFeatures.js b/client/apollo/js/View/Track/DraggableHTMLFeatures.js
new file mode 100644
index 0000000..0fa7c6b
--- /dev/null
+++ b/client/apollo/js/View/Track/DraggableHTMLFeatures.js
@@ -0,0 +1,1433 @@
+define( [
+            'dojo/_base/declare',
+            'dojo/_base/array',
+            'JBrowse/View/Track/HTMLFeatures',
+            'WebApollo/FeatureSelectionManager',
+            'dijit/Menu',
+            'dijit/MenuItem',
+            'dijit/CheckedMenuItem',
+            'dijit/MenuSeparator',
+            'dijit/PopupMenuItem',
+            'dijit/Dialog',
+            'jquery',
+            'jqueryui/draggable',
+            'JBrowse/Util', 
+            'JBrowse/Model/SimpleFeature', 
+            'WebApollo/SequenceOntologyUtils'
+        ],
+    function( declare,
+        array,
+        HTMLFeatureTrack,
+        FeatureSelectionManager,
+        dijitMenu,
+        dijitMenuItem, 
+        dijitCheckedMenuItem,
+        dijitMenuSeparator,
+        dijitPopupMenuItem,
+        dijitDialog,
+        $,
+        draggable,
+        Util, 
+        SimpleFeature,
+        SeqOnto ) {
+
+var debugFrame = false ;
+
+var draggableTrack = declare( HTMLFeatureTrack,
+
+{
+    // so is dragging
+    dragging: false,
+
+    _defaultConfig: function() {
+        return Util.deepUpdate(
+            dojo.clone( this.inherited(arguments) ),
+            {
+                style: {
+            // className: "{type}",   // feature classname gets set to feature.get('type')
+                    className: "container-16px", 
+                    renderClassName: "gray-center-30pct annot-apollo",
+                    arrowheadClass: "webapollo-arrowhead", 
+                    subfeatureClasses: {
+                        UTR: "webapollo-UTR",   
+                        CDS: "webapollo-CDS",   
+                        exon: "container-100pct", 
+                        intron: null,
+                        wholeCDS: null, 
+                        start_codon: null, 
+                        stop_codon: null, 
+                        match_part: "darkblue-80pct"
+                    }, 
+
+                    // renderClassName: 'DraggableFeatureTrack'  ???
+                    // setting minSubfeatureWidth to 1 insures subfeatures will almost always get drawn, 
+                    minSubfeatureWidth: 1, 
+                    centerChildrenVertically: false
+                },
+                events: {
+                    // need to map click to a null-op, to override default JBrowse click behavior for click on features 
+                    //     (JBrowse default is feature detail popup)
+                    click:     function(event) {
+                        // not quite a null-op, also need to suprress propagation of click recursively up through parent divs, 
+                        //    in order to stop default JBrowse behavior for click on tracks (which is to recenter view at click point)
+                        event.stopPropagation();
+                    }
+                    // WebApollo can't set up mousedown --> onFeatureMouseDown() in config.events, 
+                    //     because dojo.on used by JBrowse config-based event setup doesn't play nice with 
+                    //     JQuery event retriggering via _mousedown() for feature drag bootstrapping
+                    // also, JBrowse only sets these events for features, and WebApollo needs them to trigger for subfeatures as well
+                    // , mousedown: dojo.hitch( this, 'onFeatureMouseDown' ),
+                    // , dblclick:  dojo.hitch( this, 'onFeatureDoubleClick' )
+                }
+            }
+        );
+    },
+
+    constructor: function( args ) {
+        this.gview = this.browser.view;
+        // get a handle to on the main WA object
+        this.browser.getPlugin( 'WebApollo', dojo.hitch( this, function(p) {
+            this.webapollo = p;
+        }));
+
+        // DraggableFeatureTracks all share the same FeatureSelectionManager
+        //    if want subclasses to have different selection manager,
+        //    call this.setSelectionManager in subclass (after calling parent constructor)
+        this.setSelectionManager( this.webapollo.featSelectionManager );
+
+        // CSS class for selected features
+        // override if want subclass to have different CSS class for selected features
+        this.selectionClass = "selected-feature";
+        
+        //  DraggableFeatureTrack.selectionManager.addListener(this);
+
+        this.last_whitespace_mousedown_loc = null;
+        this.last_whitespace_mouseup_time = new Date();  // dummy timestamp
+        this.prev_selection = null;
+
+        this.verbose = false;
+        this.verbose_selection = false;
+        this.verbose_selection_notification = false;
+        this.verbose_drag = false;
+        this.drag_enabled = true;
+
+        this.feature_context_menu = null; 
+
+        /** hack to determine which tracks to apply edge matching to 
+            would rather do a check for whether track is instance of DraggableHTMLFeatures (or possibly HTMLFeatures), 
+                but use of dojo.declare() for classes means track object's class is actually base Object. 
+        */
+        this.edge_matching_enabled = true;
+    },
+
+
+    loadSuccess: function(trackInfo) {
+        /* if subclass indicates it has custom context menu, do not initialize default feature context menu */
+        if (! this.has_custom_context_menu) {
+            this.initFeatureContextMenu();
+            this.initFeatureDialog();
+        }
+        this.inherited( arguments );
+    },
+
+    setSelectionManager: function(selman)  {
+        if (this.selectionManager)  {
+            this.selectionManager.removeListener(this);
+        }
+        this.selectionManager = selman;
+        // FeatureSelectionManager listeners must implement
+        //     selectionAdded() and selectionRemoved() response methods
+        this.selectionManager.addListener(this);
+        return selman;
+    },
+
+    /**
+     *   only called once, during track setup ???
+     *
+     *   doublclick in track whitespace is used by JBrowse for zoom
+     *      but WebApollo/JBrowse uses single click in whitespace to clear selection
+     *
+     *   so this sets up mousedown/mouseup/doubleclick
+     *      kludge to restore selection after a double click to whatever selection was before
+     *      initiation of doubleclick (first mousedown/mouseup)
+     *
+     */
+    setViewInfo: function(genomeView, numBlocks,
+                          trackDiv, labelDiv,
+                          widthPct, widthPx, scale) {
+        this.inherited( arguments );
+
+        var $div = $(this.div);
+        var track = this;
+
+        // this.scale = scale;  // scale is in pixels per base
+
+        // setting up mousedown and mouseup handlers to enable click-in-whitespace to clear selection
+        //    (without conflicting with JBrowse drag-in-whitespace to scroll)
+        $div.bind('mousedown', function(event)  {
+                      var target = event.target;
+                      if (! (target.feature || target.subfeature))  {
+                          track.last_whitespace_mousedown_loc = [ event.pageX, event.pageY ];
+                      }
+                  } );
+        $div.bind('mouseup', function(event)  {
+                      var target = event.target;
+                      if (! (target.feature || target.subfeature))  {  // event not on feature, so must be on whitespace
+                          var xup = event.pageX;
+                          var yup = event.pageY;
+                          // if click in whitespace without dragging (no movement between mouse down and mouse up,
+                          //    and no shift modifier,
+                          //    then deselect all
+                          if (this.verbose_selection)  { console.log("mouse up on track whitespace"); }
+                          var eventModifier = event.shiftKey || event.altKey || event.metaKey || event.ctrlKey;
+                          if (track.last_whitespace_mousedown_loc &&
+                              xup === track.last_whitespace_mousedown_loc[0] &&
+                              yup === track.last_whitespace_mousedown_loc[1] &&
+                              (! eventModifier ))  {
+                                  var timestamp = new Date();
+                                  var prev_timestamp = track.last_whitespace_mouseup_time;
+                                  track.last_whitespace_mouseup_time = timestamp;
+                                  // if less than half a second, probably a doubleclick (or triple or more click...)
+                                  var probably_doubleclick = ((timestamp.getTime() - prev_timestamp.getTime()) < 500);
+                                  if (probably_doubleclick)  {
+                                      if (this.verbose_selection)  { console.log("mouse up probably part of a doubleclick"); }
+                                      // don't record selection state, want to keep prev_selection set
+                                      //    to selection prior to first mouseup of doubleclick
+                                  }
+                                  else {
+                                      track.prev_selection = track.selectionManager.getSelection();
+                                      if (this.verbose_selection)  {
+                                          console.log("recording prev selection");
+                                          console.log(track.prev_selection);
+                                      }
+                                  }
+                                  if (this.verbose_selection)  { console.log("clearing selection"); }
+                                  track.selectionManager.clearAllSelection();
+                              }
+                          else   {
+                              track.prev_selection = null;
+                          }
+                      }
+                      // regardless of what element it's over, mouseup clears out tracking of mouse down
+                      track.last_whitespace_mousedown_loc = null;
+                  } );
+        // kludge to restore selection after a double click to whatever selection was before
+        //      initiation of doubleclick (first mousedown/mouseup)
+        $div.bind('dblclick', function(event) {
+                      var target = event.target;
+                      // because of dblclick bound to features, will only bubble up to here on whitespace,
+                      //   but doing feature check just to make sure
+                      if (! (target.feature || target.subfeature))  {
+                          if (this.verbose_selection)  {
+                              console.log("double click on track whitespace");
+                              console.log("restoring selection after double click");
+                              console.log(track.prev_selection);
+                          }
+                          if (track.prev_selection)  {
+                              var plength = track.prev_selection.length;
+                              // restore selection
+                              for (var i = 0; i<plength; i++)  {
+                                  track.selectionManager.addToSelection(track.prev_selection[i]);
+                              }
+                          }
+                      }
+                      track.prev_selection = null;
+                  } );
+
+
+        /* track click diagnostic (and example of how to add additional track mouse listener?)  */
+        $div.bind("click", function(event) {
+                      // console.log("track click, base position: " + track.gview.getGenomeCoord(event));
+                      var target = event.target;
+                      if (target.feature || target.subfeature)  {
+                          event.stopPropagation();
+                      }
+                  } );
+
+    },
+
+    selectionAdded: function( rec, smanager) {
+        var track = this;
+        if( rec.track === track)  {
+            var featdiv = track.getFeatDiv( rec.feature );
+            if( track.verbose_selection_notification )  {
+                console.log("DFT.selectionAdded called: ");
+                console.log( rec );
+                console.log( featdiv );
+            }
+            if( featdiv )  {
+                var jq_featdiv = $(featdiv);
+                if (!jq_featdiv.hasClass(track.selectionClass))  {
+                    jq_featdiv.addClass(track.selectionClass);
+                }
+
+                //      track.showEdgeMatches(feat);
+            }
+        }
+    },
+
+    selectionCleared: function(selected, smanager) {
+        var track = this;
+        if (track.verbose_selection_notification)  {
+            console.log("DFT.selectionCleared called");
+        }
+
+        var slength = selected.length;
+        for (var i=0; i<slength; i++)  {
+            var rec = selected[i];
+            track.selectionRemoved( rec );
+        }
+    },
+
+    selectionRemoved: function( rec, smanager)  {
+        var track = this;
+        if( rec.track === track )  {
+            var featdiv = track.getFeatDiv( rec.feature );
+            if( track.verbose_selection_notification )  {
+                console.log("DFT.selectionRemoved called");
+                console.log( rec );
+                console.log( featdiv );
+            }
+            if( featdiv )  {
+                var jq_featdiv = $(featdiv);
+                if (jq_featdiv.hasClass(track.selectionClass))  {
+                    jq_featdiv.removeClass(track.selectionClass);
+                }
+
+                if (jq_featdiv.hasClass("ui-draggable"))  {
+                    jq_featdiv.draggable("destroy");
+                }
+                if (jq_featdiv.hasClass("ui-multidraggable"))  {
+                    jq_featdiv.multidraggable("destroy");
+                }
+            }
+
+        }
+    },
+
+    /**
+     *  overriding renderFeature to add event handling for mouseover, mousedown, mouseup
+     */
+    renderFeature: function(feature, uniqueId, block, scale, labelScale, descriptionScale, 
+                            containerStart, containerEnd, rclass, clsName ) {
+        var featdiv = this.inherited( arguments );
+        if( featdiv )  {  // just in case featDiv doesn't actually get created
+
+        var $featdiv = $(featdiv);
+        $featdiv.bind("mousedown", dojo.hitch( this, 'onFeatureMouseDown') );
+            $featdiv.bind("dblclick",  dojo.hitch( this, 'onFeatureDoubleClick') );
+            if (this.feature_context_menu  && (! this.has_custom_context_menu)) {
+                this.feature_context_menu.bindDomNode(featdiv);
+            }
+
+            // if renderClassName field exists in trackData.json for this track, then add a child
+            //    div to the featdiv with class for CSS styling set to renderClassName value
+            if (!rclass) {
+                rclass = this.config.style.renderClassName;
+            }
+            if (rclass)  {
+                // console.log("in FeatureTrack.renderFeature, creating annot div");
+                var rendiv = document.createElement("div");
+                dojo.addClass(rendiv, "feature-render");
+                dojo.addClass(rendiv, rclass);
+                if (Util.is_ie6) rendiv.appendChild(document.createComment());
+                featdiv.appendChild(rendiv);
+            }
+            if (clsName) {
+                dojo.removeClass(featdiv.firstChild, feature.get("type"));
+                dojo.addClass(featdiv.firstChild, clsName);
+            }
+        }
+        return featdiv;
+    },
+
+    renderSubfeature: function( feature, featDiv, subfeature,
+                                displayStart, displayEnd, block )  {
+
+        var subfeatdiv = this.inherited( arguments );
+        if (subfeatdiv)  {  // just in case subFeatDiv doesn't actually get created
+            var $subfeatdiv = $(subfeatdiv);
+            // adding pointer to track for each subfeatdiv
+            //   (could get this by DOM traversal, but shouldn't take much memory, and having it with each subfeatdiv is more convenient)
+            subfeatdiv.track = this;
+            subfeatdiv.subfeature = subfeature;
+            $subfeatdiv.bind("mousedown", dojo.hitch( this, 'onFeatureMouseDown' ) );
+            $subfeatdiv.bind("dblclick",  dojo.hitch( this, 'onFeatureDoubleClick') );
+        }
+        return subfeatdiv;
+    },
+
+    _subfeatSorter: function( a, b ) {
+        var as = a.get('start');
+        var bs = b.get('start');
+        if ( as == bs )  { return 0; }
+        else if ( as > bs ) { return 1; }
+        else if ( as < bs ) { return -1; }
+        else  { return 0; /* shouldn't fall through to here */ }
+    },
+
+
+    /**
+     *  if feature has translated region (CDS, wholeCDS, start_codon, ???), 
+     *  reworks feature's subfeatures for more annotation-editing-friendly selection 
+     *
+     *  Assumes:
+     *      if translated, will either have 
+     *           CDS-ish term for each coding segment
+     *           wholeCDS from start of translation to end of translation (so already pre-processed)
+     *           mutually exclusive (either have CDS, or wholeCDS, but not both)
+     *      if wholeCDS present, then pre-processed (no UTRs)
+     *      if any exon-ish types present, then _all_ exons are present with exon-ish types
+     */
+    _processTranslation: function( feature ) {
+        var track = this;
+
+        var feat_type = feature.get('type');
+
+        // most very dense genomic feature tracks do not have CDS.  Trying to minimize overhead for that case -- 
+        //    keep list of types that NEVER have CDS children (match, alignment, repeat, etc.)
+        //    (WARNING in this case not sorting, but sorting (currently) only needed for features with CDS (for reading frame calcs))
+        if (SeqOnto.neverHasCDS[feat_type])  {
+            feature.normalized = true;
+            return;
+        }
+        var subfeats = feature.get('subfeatures');
+
+        // var cds = subfeats.filter( function(feat) { return feat.get('type') === 'CDS'; } );
+        var cds = subfeats.filter( function(feat) { 
+            return SeqOnto.cdsTerms[feat.get('type')];
+        } );
+        var wholeCDS = subfeats.filter( function(feat) { return feat.get('type') === 'wholeCDS'; } );
+        
+        // most very dense genomic feature tracks do not have CDS.  Trying to minimize overhead for that case -- 
+        //    if no CDS, no wholeCDS, consider normalized 
+        //    (WARNING in this case not sorting, but sorting (currently) only needed for features with CDS (for reading frame calcs))
+        // 
+        if (cds.length === 0 && wholeCDS.length === 0)  {
+            feature.normalized = true;
+            return;
+        }
+
+        var newsubs;
+        // wholeCDS is specific to WebApollo, if seen can assume no CDS, and UTR/exon already normalized
+        if (wholeCDS.length > 0)  {
+            // extract wholecds from subfeats, then sort subfeats
+            feature.wholeCDS = wholeCDS[0];
+            newsubs = subfeats.filter( function(feat) { return feat.get('type') !== 'wholeCDS'; } );
+        }
+        
+        // if has a CDS, remove CDS from subfeats and sort exons
+        else if (cds.length > 0)  {
+            cds.sort(this._subfeatSorter);
+            var cdsmin = cds[0].get('start');
+            var cdsmax = cds[cds.length-1].get('end');
+            feature.wholeCDS = new SimpleFeature({ parent: feature, 
+                                                   data: { start: cdsmin, end: cdsmax, type: 'wholeCDS', 
+                                                           strand: feature.get('strand') } 
+                                                 } );
+            var hasExons = false;
+            for (var i=0; i<subfeats.length; i++)  { 
+                // if (subfeats[i].get('type') === 'exon')  { hasExons = true; break; } 
+                if (SeqOnto.exonTerms[subfeats[i].get('type')])  { hasExons = true; break; } 
+            }
+            if (hasExons)  {
+                // filter out UTR and CDS
+                newsubs = subfeats.filter( function(feat) { 
+                    var ftype = feat.get('type');
+                    return (! (SeqOnto.utrTerms[ftype] || SeqOnto.cdsTerms[ftype]) );
+                } );
+            }
+            else  {  // no exons, but at least one CDS, possibly UTR
+                // create exons by joining abutting UTR/CDS
+                var sortedsubs = subfeats.slice();  // shallow copy subfeats array
+                sortedsubs.sort(this._subfeatSorter);
+                newsubs = [];
+                // since cds.length > 0, guaranteed to have at least one CDS
+                var exonCount = 0;
+                var prevStart, prevEnd;
+                // scan through sorted subfeats, joining abutting UTR/CDS regions
+                for (var i=0; i<sortedsubs.length; i++)  {
+                    var subfeat = sortedsubs[i];
+                    var ftype = subfeat.get('type');
+                    var curStart = subfeat.get('start');
+                    var curEnd = subfeat.get('end');
+
+                    if (SeqOnto.utrTerms[ftype] || SeqOnto.cdsTerms[ftype] ) {  
+                        if (! prevStart)  {  // first UTR/CDS, just initialize first exon
+                            prevStart = subfeat.get('start');
+                            prevEnd = subfeat.get('end');
+                        }
+                        else  {  // compare to previous UTR/CDS
+                            // abutting, extend previous exon
+                            if (curStart == prevEnd)  {
+                                prevEnd = curEnd;
+                            }
+                            // not abutting, create previous exon and start new one
+                            else  {
+                                var exon = new SimpleFeature({ parent: feature, 
+                                                               id: feature.id() + "-exon-" + exonCount++, 
+                                                               data: { start: prevStart, end: prevEnd, type: 'exon', 
+                                                                       strand: feature.get('strand')  } 
+                                                             } );
+                                newsubs.push(exon);
+                                prevStart = curStart;
+                                prevEnd = curEnd;
+                            }
+                        }
+                    }
+                    else  {  // not a CDS or UTR, just add to new subfeats array
+                        newsubs.push(subfeat);
+                    }
+                }
+                // add last exon after exiting loop
+                var exon = new SimpleFeature({ parent: feature, 
+                                               id: feature.id() + "-exon-" + exonCount++, 
+                                               data: { start: prevStart, end: prevEnd, type: 'exon', 
+                                                       strand: feature.get('strand') } 
+                                             } );
+                newsubs.push(exon);
+                
+            }
+        }
+        // ensure that subfeatures are sorted by ascending start (regardless of feature orientation)
+        //    may want to revisit later and sort subfeatures of minus strand in descending order ??
+        //       but if do this must make sure to change reading frame calcs to reflect this
+        newsubs.sort(this._subfeatSorter);  
+        feature.filteredsubs = newsubs;
+        feature.normalized = true;
+    }, 
+
+
+    /**
+     * overriding handleSubFeatures for customized handling of UTR/CDS-segment rendering within exon divs
+     */
+    handleSubFeatures: function( feature, featDiv,
+                                    displayStart, displayEnd, block )  {
+
+        var subfeats = feature.get('subfeatures');  
+        if (! subfeats)  { return; }
+
+        if (! feature.normalized )  {
+            this._processTranslation( feature );
+        }
+        var wholeCDS = feature.wholeCDS;
+        var parentId = this.getId(feature);
+
+        // if processing resulted in filtered subfeats, render with those instead of unfiltered subfeats
+        if (feature.filteredsubs)  { subfeats = feature.filteredsubs; }
+        var slength = subfeats.length;
+        var subfeat;
+        var subtype;
+
+        if (wholeCDS) {
+            var cdsStart = wholeCDS.get('start');
+            var cdsEnd = wholeCDS.get('end');
+            //    current convention is start = min and end = max regardless of strand, but checking just in case
+            var cdsMin = Math.min(cdsStart, cdsEnd);
+            var cdsMax = Math.max(cdsStart, cdsEnd);
+            if (this.verbose_render)  { console.log("wholeCDS:"); console.log(wholeCDS); }
+        }
+
+        var priorCdsLength = 0;
+        if (debugFrame)  { console.log("====================================================="); }
+
+        var strand = feature.get('strand');
+        var reverse = false;
+        if (strand === -1 || strand === '-') {
+            reverse = true;
+        }
+        /* WARNING: currently assuming children are ordered by ascending min
+         * (so if on minus strand then need to calc frame starting with the last exon)
+         */
+        for (var i = 0; i < slength; i++) {
+            if (reverse) {
+                subfeat = subfeats[slength-i-1];
+            }
+            else  {
+                subfeat = subfeats[i];
+            }
+            var uid = this.getId(subfeat);
+            subtype = subfeat.get('type');
+            // don't render "wholeCDS" type
+            // although if subfeatureClases is properly set up, wholeCDS would also be filtered out in renderFeature?
+            // if (subtype == "wholeCDS")  {  continue; }
+            var subDiv = this.renderSubfeature( feature, featDiv, subfeat, displayStart, displayEnd, block);
+            if( subDiv )
+                subDiv.subfeature = subfeat;
+
+            // if subfeat is of type "exon", add CDS/UTR rendering
+            // if (subDiv && wholeCDS && (subtype === "exon")) {
+            // if (wholeCDS && (subtype === "exon")) {   // pass even if subDiv is null (not drawn), in order to correctly calc downstream CDS frame
+
+            // CHANGED to call renderExonSegments even if no wholeCDS --
+            //     non wholeCDS means undefined cdsMin, which will trigger creation of UTR div for entire exon
+            if (subtype === "exon") {   // pass even if subDiv is null (not drawn), in order to correctly calc downstream CDS frame
+                priorCdsLength = this.renderExonSegments(subfeat, subDiv, cdsMin, cdsMax, displayStart, displayEnd, priorCdsLength, reverse);
+            }
+            if (this.verbose_render)  {
+                console.log("in DraggableFeatureTrack.handleSubFeatures, subDiv: ");
+                console.log(subDiv);
+            }
+        }
+   },
+
+    handleReverseStrandOffset: function(inputFrame){
+        var offset = ( 2 - (this.refSeq.length  % 3)  )  ;
+        inputFrame = (inputFrame + offset )% 3;
+        if(inputFrame==2){
+            inputFrame=0
+        }
+        else
+        if(inputFrame==0){
+            inputFrame=2
+        }
+        return inputFrame ;
+    },
+
+   /**
+    *  TODO: still need to factor in truncation based on displayStart and displayEnd???
+
+   From: http://mblab.wustl.edu/GTF22.html
+   Frame is calculated as (3 - ((length-frame) mod 3)) mod 3.
+       (length-frame) is the length of the previous feature starting at the first whole codon (and thus the frame subtracted out).
+       (length-frame) mod 3 is the number of bases on the 3' end beyond the last whole codon of the previous feature.
+       3-((length-frame) mod 3) is the number of bases left in the codon after removing those that are represented at the 3' end of the feature.
+       (3-((length-frame) mod 3)) mod 3 changes a 3 to a 0, since three bases makes a whole codon, and 1 and 2 are left unchanged.
+    */
+    renderExonSegments: function( subfeature, subDiv, cdsMin, cdsMax,
+                                  displayStart, displayEnd, priorCdsLength, reverse, UTRclass)  {
+        var subStart = subfeature.get('start');
+        var subEnd = subfeature.get('end');
+        var subLength = subEnd - subStart;
+        var CDSclass;
+
+        //   if (debugFrame)  { console.log("exon: " + subStart); }
+
+        // if the feature has been truncated to where it doesn't cover
+        // this subfeature anymore, just skip this subfeature
+        // GAH: was OR, but should be AND?? var render = ((subEnd > displayStart) && (subStart < displayEnd));
+        var render = subDiv && (subEnd > displayStart) && (subStart < displayEnd);
+
+        // look for UTR and CDS subfeature class mapping from trackData
+        //    if can't find, then default to parent feature class + "-UTR" or "-CDS"
+        if( render ) {  // subfeatureClases defaults set in this._defaultConfig
+            if (!UTRclass) {
+                UTRclass = this.config.style.subfeatureClasses["UTR"];
+            }
+            CDSclass = this.config.style.subfeatureClasses["CDS"];
+        }
+
+        //    if ((subEnd <= displayStart) || (subStart >= displayEnd))  { return undefined; }
+
+        var segDiv;
+        // console.log("render sub frame");
+        // whole exon is untranslated (falls outside wholeCDS range, or no CDS info found)
+        if( (cdsMin === undefined && cdsMax === undefined) ||
+            (cdsMax <= subStart || cdsMin >= subEnd))  {
+            if( render )  {
+                segDiv = document.createElement("div");
+                // not worrying about appending "plus-"/"minus-" based on strand yet
+                dojo.addClass(segDiv, "subfeature");
+                dojo.addClass(segDiv, UTRclass);
+                if (Util.is_ie6) segDiv.appendChild(document.createComment());
+                segDiv.style.cssText =
+                    "left: " + (100 * ((subStart - subStart) / subLength)) + "%;"
+                    + "width: " + (100 * ((subEnd - subStart) / subLength)) + "%;";
+                subDiv.appendChild(segDiv);
+            }
+        }
+
+        /*
+         Frame is calculated as (3 - ((length-frame) mod 3)) mod 3.
+            (length-frame) is the length of the previous feature starting at the first whole codon (and thus the frame subtracted out).
+            (length-frame) mod 3 is the number of bases on the 3' end beyond the last whole codon of the previous feature.
+            3-((length-frame) mod 3) is the number of bases left in the codon after removing those that are represented at the 3' end of the feature.
+            (3-((length-frame) mod 3)) mod 3 changes a 3 to a 0, since three bases makes a whole codon, and 1 and 2 are left unchanged.
+        */
+        // whole exon is translated
+        else if (cdsMin <= subStart && cdsMax >= subEnd) {
+            var overhang = priorCdsLength % 3;  // number of bases overhanging from previous CDS
+            var absFrame, cdsFrame, initFrame
+            var relFrame = (3 - (priorCdsLength % 3)) % 3;
+            if (reverse)  {
+                initFrame = (cdsMax  ) % 3;
+                absFrame = (subEnd ) % 3;
+                cdsFrame = ( (absFrame - relFrame) + 3 ) % 3;
+                cdsFrame = this.handleReverseStrandOffset(cdsFrame);
+            }
+            else  {
+                initFrame = cdsMin % 3;
+                absFrame = (subStart % 3);
+                cdsFrame = (absFrame + relFrame) % 3;
+            }
+            if (debugFrame)  {
+                    console.log("whole exon: " + subStart + " -- ", subEnd, " initFrame: ", initFrame,
+                                           ", overhang: " + overhang + ", relFrame: ", relFrame, ", absFrame: ", absFrame,
+                                           ", cdsFrame: " + cdsFrame);
+            }
+
+            if (render)  {
+                segDiv = document.createElement("div");
+                // not worrying about appending "plus-"/"minus-" based on strand yet
+                dojo.addClass(segDiv, "subfeature");
+                dojo.addClass(segDiv, CDSclass);
+                if (Util.is_ie6) segDiv.appendChild(document.createComment());
+                segDiv.style.cssText =
+                    "left: " + (100 * ((subStart - subStart) / subLength)) + "%;"
+                    + "width: " + (100 * ((subEnd - subStart) / subLength)) + "%;";
+                dojo.addClass(segDiv, "cds-frame" + cdsFrame);
+                subDiv.appendChild(segDiv);
+            }
+            priorCdsLength += subLength;
+        }
+        // partial translation of exon
+        else  {
+            // calculate 5'UTR, CDS segment, 3'UTR
+            var cdsSegStart = Math.max(cdsMin, subStart);
+            var cdsSegEnd = Math.min(cdsMax, subEnd);
+            var overhang = priorCdsLength % 3;  // number of bases overhanging
+            var absFrame, cdsFrame, initFrame;
+            if (priorCdsLength > 0)  {
+                var relFrame = (3 - (priorCdsLength % 3)) % 3;
+                if (reverse)  {
+                    initFrame = (cdsMax) % 3;
+                    absFrame = (subEnd ) % 3;
+                    cdsFrame = (3 + absFrame - relFrame) % 3;
+                    cdsFrame = this.handleReverseStrandOffset(cdsFrame);
+                }
+                else  {
+                    // cdsFrame = (subStart + ((3 - (priorCdsLength % 3)) % 3)) % 3;
+                    initFrame = cdsMin % 3;
+                    absFrame = (subStart % 3);
+                    cdsFrame = (absFrame + relFrame) % 3;
+                }
+                if (debugFrame)  { console.log("partial exon: " + subStart + ", initFrame: " + (cdsMin % 3) +
+                                               ", overhang: " + overhang + ", relFrame: " + relFrame + ", subFrame: " + (subStart % 3) +
+                                               ", cdsFrame: " + cdsFrame); }
+            }
+            else  {  // actually shouldn't need this? -- if priorCdsLength = 0, then above conditional collapses down to same calc...
+                if (reverse) {
+                    cdsFrame = (cdsMax) % 3;
+                    cdsFrame = this.handleReverseStrandOffset(cdsFrame);
+                }
+                else  {
+                    cdsFrame = cdsMin % 3;
+                }
+            }
+
+            var utrStart;
+            var utrEnd;
+            // make left UTR (if needed)
+            if (cdsMin > subStart) {
+                utrStart = subStart;
+                utrEnd = cdsSegStart;
+                if (render)  {
+                    segDiv = document.createElement("div");
+                    // not worrying about appending "plus-"/"minus-" based on strand yet
+                    dojo.addClass(segDiv, "subfeature");
+                    dojo.addClass(segDiv, UTRclass);
+                    if (Util.is_ie6) segDiv.appendChild(document.createComment());
+                    segDiv.style.cssText =
+                        "left: " + (100 * ((utrStart - subStart) / subLength)) + "%;"
+                        + "width: " + (100 * ((utrEnd - utrStart) / subLength)) + "%;";
+                    subDiv.appendChild(segDiv);
+                }
+            }
+            if (render)  {
+                // make CDS segment
+                segDiv = document.createElement("div");
+                // not worrying about appending "plus-"/"minus-" based on strand yet
+                dojo.addClass(segDiv, "subfeature");
+                dojo.addClass(segDiv, CDSclass);
+                if (Util.is_ie6) segDiv.appendChild(document.createComment());
+                segDiv.style.cssText =
+                    "left: " + (100 * ((cdsSegStart - subStart) / subLength)) + "%;"
+                    + "width: " + (100 * ((cdsSegEnd - cdsSegStart) / subLength)) + "%;";
+                dojo.addClass(segDiv, "cds-frame" + cdsFrame);
+                subDiv.appendChild(segDiv);
+            }
+            priorCdsLength += (cdsSegEnd - cdsSegStart);
+
+            // make right UTR  (if needed)
+            if (cdsMax < subEnd)  {
+                utrStart = cdsSegEnd;
+                utrEnd = subEnd;
+                if (render)  {
+                    segDiv = document.createElement("div");
+                    // not worrying about appending "plus-"/"minus-" based on strand yet
+                    dojo.addClass(segDiv, "subfeature");
+                    dojo.addClass(segDiv, UTRclass);
+                    if (Util.is_ie6) segDiv.appendChild(document.createComment());
+                    segDiv.style.cssText =
+                        "left: " + (100 * ((utrStart - subStart) / subLength)) + "%;"
+                        + "width: " + (100 * ((utrEnd - utrStart) / subLength)) + "%;";
+                    subDiv.appendChild(segDiv);
+                }
+            }
+        }
+        return priorCdsLength;
+    },
+
+
+    /*
+     *  selection occurs on mouse down
+     *  mouse-down on unselected feature -- deselect all & select feature
+     *  mouse-down on selected feature -- no change to selection (but may start drag?)
+     *  mouse-down on "empty" area -- deselect all
+     *        (WARNING: this is preferred behavior, but conflicts with dblclick for zoom -- zoom would also deselect)
+     *         therefore have mouse-click on empty area deselect all (no conflict with dblclick)
+     *  shift-mouse-down on unselected feature -- add feature to selection
+     *  shift-mouse-down on selected feature -- remove feature from selection
+     *  shift-mouse-down on "empty" area -- no change to selection
+     *
+     *   "this" should be a featdiv or subfeatdiv
+     */
+    onFeatureMouseDown: function(event) {
+        // event.stopPropagation();
+        if( this.verbose_selection || this.verbose_drag ) { 
+            console.log("DFT.onFeatureMouseDown called"); 
+        console.log("genome coord: " + this.getGenomeCoord(event));
+        }
+
+        // drag_create conditional needed in older strategy using trigger(event) for feature drag bootstrapping with JQuery 1.5, 
+        //   but not with with JQuery 1.7+ strategy using _mouseDown(event), since _mouseDown call doesn't lead to onFeatureMouseDown() call 
+        // if (this.drag_create)  { this.drag_create = null; return; }
+        this.handleFeatureSelection(event);
+        if (this.drag_enabled)  {
+            this.handleFeatureDragSetup(event);
+        }
+   },
+
+   handleFeatureSelection: function( event )  {
+       var ftrack = this;
+       var selman = ftrack.selectionManager;
+       var featdiv = (event.currentTarget || event.srcElement);
+       var feat = featdiv.feature || featdiv.subfeature;
+
+       if( selman.unselectableTypes[feat.get('type')] ) {
+           return;
+       }
+
+       var already_selected = selman.isSelected( { feature: feat, track: ftrack } );
+       var parent_selected = false;
+       var parent = feat.parent();
+       if (parent)  {
+           parent_selected = selman.isSelected( { feature: parent, track: ftrack } );
+       }
+       if (this.verbose_selection)  {
+           console.log("DFT.handleFeatureSelection() called, actual mouse event");
+           console.log(featdiv);
+           console.log(feat);
+           console.log("already selected: " + already_selected + ",  parent selected: " + parent_selected +
+                       ",  shift: " + (event.shiftKey));
+       }
+       // if parent is selected, allow propagation of event up to parent,
+       //    in order to ensure parent draggable setup and triggering
+       // otherwise stop propagation
+       if (! parent_selected)  {
+           event.stopPropagation();
+       }
+       if (event.shiftKey)  {
+           if (already_selected) {  // if shift-mouse-down and this already selected, deselect this
+               selman.removeFromSelection( { feature: feat, track: this });
+           }
+           else if (parent_selected)  {
+               // if shift-mouse-down and parent selected, do nothing --
+               //   event will get propagated up to parent, where parent will get deselected...
+               // selman.removeFromSelection(parent);
+           }
+           else  {  // if shift-mouse-down and neither this or parent selected, select this
+               // children are auto-deselected by selection manager when parent is selected
+               selman.addToSelection({ feature: feat, track: this }, true);
+           }
+       }
+       else if (event.altKey) {
+           if (already_selected) {
+               // select entire feature
+               selman.addToSelection({ feature: feat.parent(), track: this}, false);
+           }
+           else if (parent_selected) {
+               // do nothing
+           }
+           else {
+               selman.addToSelection({ feature: feat.parent(), track: this}, false);
+           }
+       }
+       else if (event.ctrlKey) {
+           if (already_selected) {
+               // do nothing
+           }
+           else if (parent_selected) {
+               // do nothing
+           }
+           else {
+               selman.addToSelection({ feature: feat, track: this}, false);
+           }
+       }
+       else if (event.metaKey) {
+       }
+       else  {  // no shift modifier
+           if (already_selected)  {  // if this selected, do nothing (this remains selected)
+               if (this.verbose_selection)  { console.log("already selected"); }
+           }
+           else  {
+               if (parent_selected)  {
+                   // if this not selected but parent selected, do nothing (parent remains selected)
+                   //    event will propagate up (since parent_selected), so draggable check
+                   //    will be done in bubbled parent event
+               }
+               else  {  // if this not selected and parent not selected, select this
+                   selman.clearSelection();
+                   selman.addToSelection({ track: this, feature: feat});
+               }
+           }
+       }
+    },
+
+    /* 
+     * WARNING: assumes one level (featdiv has feature) 
+     *                  or two-level (featdiv has feature, subdivs have subfeature) feature hierarchy
+     * attaching ghost to pinned AnnotTrack or SequenceTrack to ensure that stays on top
+     */
+    handleFeatureDragSetup: function(event)  {
+        var ftrack = this;
+        var featdiv = (event.currentTarget || event.srcElement);
+        if (this.verbose_drag)  {  console.log("called handleFeatureDragSetup()"); console.log(featdiv); }
+        var feat = featdiv.feature || featdiv.subfeature;
+        var selected = this.selectionManager.isSelected( { feature: feat, track: ftrack });
+        // set all other tracks to standard track zIndex, 
+        // set this track to > than others to ensure ghost is drawn on top of all other tracks
+  /*     ftrack.div.style.zIndex = 10;
+        $(ftrack.gview.tracks).each( function(index, track)  {
+            if (track.div !== ftrack.div && track.div.style.zIndex !== 7)  {
+                track.div.style.zIndex = 7;
+            }
+        } );
+        */
+
+        /*
+        // simple version for testing
+        // (no multiselect ghosting, no appendTo redirection, no event retriggering for simultaneous select & drag)
+            if (selected)  {  
+                var $featdiv = $(featdiv);
+                $featdiv.draggable(   { 
+                helper: 'clone', 
+                opacity: 0.5,
+                axis: 'y', 
+                } );
+            }
+        */
+        /**
+         *  ideally would only make $.draggable call once for each selected div
+         *  but having problems with draggability disappearing from selected divs
+         *       that $.draggable was already called on
+         *  therefore whenever mousedown on a previously selected div also want to
+         *       check that draggability and redo if missing
+         */
+        if (selected)  {
+            var $featdiv = $(featdiv);
+            if (! $featdiv.hasClass("ui-draggable"))  {
+                if (this.verbose_drag)  {
+                    console.log("setting up dragability");
+                    console.log(featdiv);
+                }
+                var atrack = ftrack.webapollo.getAnnotTrack();
+                if (! atrack) { atrack = ftrack.webapollo.getSequenceTrack();  }
+                var fblock = ftrack.getBlock(featdiv);
+
+                // append drag ghost to featdiv block's equivalent block in annotation track if present, 
+                //     else  append to equivalent block in sequence track if present, 
+                //     else append to featdiv's block 
+                var ablock = ( atrack ? atrack.getEquivalentBlock(fblock) : fblock);
+
+                $featdiv.draggable(   // draggable() adds "ui-draggable" class to div
+                {
+                    zIndex: 200, 
+                    appendTo: ablock.domNode, // would default to featdiv's parent div
+                    // custom helper for pseudo-multi-drag ("pseudo" because multidrag is visual only --
+                    //      handling of draggable when dropped is already done through selection)
+                    //    strategy for custom helper is to make a "holder" div with same dimensionsas featdiv
+                    //       that's (mostly) a clone of the featdiv draggable is being called on
+                    //       (since draggable seems to like that),
+                    //     then add clones of all selected feature divs (including another clone of featdiv)
+                    //        to holder, with dimensions of each clone recalculated as pixels and set relative to
+                    //        featdiv that the drag is actually initiated on (and thus relative to the holder's
+                    //        dimensions)
+
+                    // helper: 'clone',
+                    helper: function() {
+                        // var $featdiv_copy = $featdiv.clone();
+                        var $pfeatdiv;
+                        // get top-level feature (assumes one or two-level feature hierarchy)
+                        if (featdiv.subfeature) {
+                            $pfeatdiv = $(featdiv.parentNode); 
+                        }
+                        else  {
+                             $pfeatdiv = $(featdiv);
+                        }
+                        var $holder = $pfeatdiv.clone();
+                        $holder.removeClass();
+                        // just want the shell of the top-level feature, so remove children 
+                        //      (selected children will be added back in below)
+                        $holder.empty(); 
+                        $holder.addClass("custom-multifeature-draggable-helper");
+                        var holder = $holder[0];
+                        // var featdiv_copy = $featdiv_copy[0];
+
+                        var foffset = $pfeatdiv.offset();
+                        var fheight = $pfeatdiv.height();
+                        var fwidth = $pfeatdiv.width();
+                        var ftop = foffset.top;
+                        var fleft = foffset.left;
+                        if (this.verbose_drag)  {
+                            console.log("featdiv dimensions: ");
+                            console.log(foffset); console.log("height: " + fheight + ", width: " + fwidth);
+                        }
+                        var selection = ftrack.selectionManager.getSelection();
+                        var selength = selection.length;
+                        for (var i=0; i<selength; i++)  {
+                            var srec = selection[i];
+                            var strack = srec.track;
+                            var sfeat = srec.feature;
+                            var sfeatdiv = strack.getFeatDiv( sfeat );
+                            // if (sfeatdiv && (sfeatdiv !== featdiv))  {
+                            if (sfeatdiv)  {
+                                var $sfeatdiv = $(sfeatdiv);
+                                var $divclone = $sfeatdiv.clone();
+                                var soffset = $sfeatdiv.offset();
+                                var sheight = $sfeatdiv.height();
+                                var swidth =$sfeatdiv.width();
+                                var seltop = soffset.top;
+                                var sleft = soffset.left;
+                                $divclone.width(swidth);
+                                $divclone.height(sheight);
+                                var delta_top = seltop - ftop;
+                                var delta_left = sleft - fleft;
+                                if (this.verbose_drag)  {
+                                    console.log(sfeatdiv);
+                                    console.log("delta_left: " + delta_left + ", delta_top: " + delta_top);
+                                }
+                                //  setting left and top by pixel, based on delta relative to moused-on feature
+                                //    tried using $divclone.position( { ...., "offset": delta_left + " " + delta_top } );,
+                                //    but position() not working for negative deltas? (ends up using absolute value)
+                                //    so doing more directly with "left and "top" css calls
+                                $divclone.css("left", delta_left);
+                                $divclone.css("top", delta_top);
+                                var divclone = $divclone[0];
+                                holder.appendChild(divclone);
+                            }
+                        }
+                        if (this.verbose_drag)  { console.log(holder); }
+                        return holder;
+                    },
+                    opacity: 0.5,
+                    axis: 'y'
+                    // drag_create setting in create() needed by older drag bootstrapping strategy with JQuery 1.5, 
+                    //     but not with different JQuery 1.7+ strategy
+                    // , create: function(event, ui)  { ftrack.drag_create = true; }
+                } );
+
+                // Want to be able to both make feature draggable and initiate actual dragging with the same mousedown event 
+                // to do this need to retrigger/simulate the mousedown event again
+                // see http://bugs.jqueryui.com/ticket/3876 regarding switch from previous hacky approach using JQuery 1.5:
+                //       $featdiv.trigger(event) and ftrack.drag_create 
+                // to new hacky approach using JQuery 1.7+:
+                //       data("draggable")._mouseDown(event);
+                // _mouseDown doesn't lead to another call to onFeatMouseDown, but does trigger the drag
+                //
+                // see also http://stackoverflow.com/questions/9634639/why-does-this-break-in-jquery-1-7-x
+                //     for more explanation of event handling changes in JQuery 1.7
+
+                // _mouseDown(event) triggering boostrapping of feature drag 
+                // $featdiv.data("draggable")._mouseDown(event);  
+                $featdiv.draggable().data("draggable")._mouseDown(event);
+                // $featdiv.trigger(event);
+            }
+        }
+    }, 
+
+    /* given a feature or subfeature, return block that rendered it */
+    getBlock: function( featdiv ) {
+        var fdiv = featdiv;
+        while (fdiv.feature || fdiv.subfeature) {
+            if (fdiv.parentNode.block) { return fdiv.parentNode.block; }
+            fdiv = fdiv.parentNode;
+        }
+        return null;  // should never get here...
+    }, 
+
+    getEquivalentBlock: function ( block ) {
+        var startBase = block.startBase;
+        var endBase = block.endBase;
+        for (var i=this.firstAttached; i<=this.lastAttached; i++)  {
+            var testBlock = this.blocks[i];
+            if (testBlock.startBase == startBase && testBlock.endBase == endBase) {
+                return testBlock;
+            }
+        }
+        return null;
+    }, 
+
+    onFeatureDoubleClick: function( event )  {
+        var ftrack = this;
+        var selman = ftrack.selectionManager;
+        // prevent event bubbling up to genome view and triggering zoom
+        event.stopPropagation();
+        var featdiv = (event.currentTarget || event.srcElement);
+        if (this.verbose_selection)  {
+            console.log("DFT.featDoubleClick");
+            console.log(ftrack);
+            console.log(featdiv);
+        }
+
+        // only take action on double-click for subfeatures
+        //  (but stop propagation for both features and subfeatures)
+        // GAH TODO:  make this work for feature hierarchies > 2 levels deep
+        var subfeat = featdiv.subfeature;
+        // if (subfeat && (! unselectableTypes[subfeat.get('type')]))  {  // only allow double-click parent selection for selectable features
+        if( subfeat && selman.isSelected({ feature: subfeat, track: ftrack }) ) {  // only allow double-click of child for parent selection if child is already selected
+            var parent = subfeat.parent();
+            // select parent feature
+            // children (including subfeat double-clicked one) are auto-deselected in FeatureSelectionManager if parent is selected
+            if( parent ) { selman.addToSelection({ feature: parent, track: ftrack }); }
+        }
+    },
+
+
+    /**
+     *  returns first feature or subfeature div (including itself)
+     *  found when crawling towards root from branch in
+     *  feature/subfeature/descendants div hierachy
+     */
+    getLowestFeatureDiv: function(elem)  {
+        while (!elem.feature && !elem.subfeature)  {
+            elem = elem.parentNode;
+            if (elem === document)  {return null;}
+        }
+        return elem;
+    },
+
+
+    /**
+     * Near as I can tell, track.showRange is called every time the
+     * appearance of the track changes in a way that would cause
+     * feature divs to be added or deleted (or moved? -- not sure,
+     * feature moves may also happen elsewhere?)  So overriding
+     * showRange here to try and map selected features to selected
+     * divs and make sure the divs have selection style set
+     */
+    showRange: function( first, last, startBase, bpPerBlock, scale,
+                         containerStart, containerEnd ) {
+        this.inherited( arguments );
+
+        //    console.log("called DraggableFeatureTrack.showRange(), block range: " +
+        //          this.firstAttached +  "--" + this.lastAttached + ",  " + (this.lastAttached - this.firstAttached));
+        // redo selection styles for divs in case any divs for selected features were changed/added/deleted
+        var srecs = this.selectionManager.getSelection();
+        for (var sin in srecs)  {
+            // only look for selected features in this track --
+            // otherwise will be redoing (sfeats.length * tracks.length) times instead of sfeats.length times,
+            // because showRange is getting called for each track
+            var srec = srecs[sin];
+            if (srec.track === this)  {
+                // some or all feature divs are usually recreated in a showRange call
+                //  therefore calling track.selectionAdded() to retrigger setting of selected-feature CSS style, etc. on new feat divs
+                this.selectionAdded(srec);
+            }
+        }
+    },
+
+
+
+/*
+ *  for the input mouse event, returns genome position under mouse IN 1-BASED INTERBASE COORDINATES
+ *  WARNING: returns base position relative to UI coordinate system 
+ *       (which is 1-based interbase)
+ *  But for most elements in genome view (features, graphs, etc.) the underlying data structures are 
+ *       in 0-base interbase coordinate system
+ *  So if you want data structure coordinates, you need to do (getUiGenomeCoord() - 1)
+ *       or use the convenience function getGenomeCoord()
+ *
+ *  event can be on GenomeView.elem or any descendant DOM elements (track, block, feature divs, etc.)
+ *  assumes:
+ *      event is a mouse event (plain Javascript event or JQuery event)
+ *      elem is a DOM element OR JQuery wrapped set (in which case result is based on first elem in result set)
+ *      elem is displayed  (see JQuery.offset() docs)
+ *      no border/margin/padding set on the doc <body> element  (see JQuery.offset() docs)
+ *      if in IE<9, either page is not scrollable (in the HTML page sense) OR event is JQuery event
+ *         (currently JBrowse index.html page is not scrollable (JBrowse internal scrolling is NOT same as HTML page scrolling))
+ */
+
+/*   
+    getUiGenomeCoord: function(mouseEvent)  {
+        return Math.floor(this.gview.absXtoBp(mouseEvent.pageX));
+    }, 
+*/
+
+/**
+ *  for the input mouse event, returns genome position under mouse IN 0-BASED INTERBASE COORDINATES
+ *  WARNING:
+ *  returns genome coord in 0-based interbase (which is how internal data structure represent coords), 
+ *       instead of 1-based interbase (which is how UI displays coordinates)
+ *  if need display coordinates, use getUiGenomeCoord() directly instead
+ *  
+ *  otherwise same capability and assumptions as getUiGenomeCoord(): 
+ *  event can be on GenomeView.elem or any descendant DOM elements (track, block, feature divs, etc.)
+ *  assumes:
+ *      event is a mouse event (plain Javascript event or JQuery event)
+ *      elem is a DOM element OR JQuery wrapped set (in which case result is based on first elem in result set)
+ *      elem is displayed  (see JQuery.offset() docs)
+ *      no border/margin/padding set on the doc <body> element  (see JQuery.offset() docs)
+ *      if in IE<9, either page is not scrollable (in the HTML page sense) OR event is JQuery event
+ *         (currently JBrowse index.html page is not scrollable (JBrowse internal scrolling is NOT same as HTML page scrolling))
+ * 
+ */
+    getGenomeCoord: function(mouseEvent)  {
+        return Math.floor(this.gview.absXtoBp(mouseEvent.pageX));
+        //  return this.getUiGenomeCoord(mouseEvent) - 1;
+    },
+    
+    _makeFeatureContextMenu: function( featDiv, menuTemplate ) {
+        var atrack = this.webapollo.getAnnotTrack();
+
+        var menu = this.inherited(arguments);
+        menu.addChild(new dijitMenuSeparator());
+        
+        this.contextMenuItems = {};
+
+        var createAnnotationMenu = new dijitMenu();
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "gene",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                this.selectionManager.clearSelection();
+                atrack.createAnnotations(selection);
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "pseudogene",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "transcript", null, "pseudogene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "tRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "tRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "snRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "snRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "snoRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "snoRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "ncRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "ncRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "rRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "rRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "miRNA",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericAnnotations(selFeats, "miRNA", null, "gene");
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "repeat_region",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericOneLevelAnnotations(selFeats, "repeat_region", true);
+            })
+        }));
+        createAnnotationMenu.addChild(new dijitMenuItem( {
+            label: "transposable_element",
+            onClick: dojo.hitch(this, function() {
+                var selection = this.selectionManager.getSelection();
+                var selFeats = this.selectionManager.getSelectedFeatures();
+                this.selectionManager.clearSelection();
+                atrack.createGenericOneLevelAnnotations(selFeats, "transposable_element", true);
+            })
+        }));
+        
+        var createAnnotationMenuItem = new dijitPopupMenuItem( {
+            label: "Create new annotation",
+            popup: createAnnotationMenu
+        } );
+        this.contextMenuItems["create_annotation"] = createAnnotationMenuItem;
+        menu.addChild(createAnnotationMenuItem);
+        
+        dojo.connect(menu, "onOpen", dojo.hitch(this, function() {
+            this.updateContextMenu();
+        }));
+        
+    },
+    
+    // override getLayout to access addRect method
+    _getLayout: function () {
+        var thisB = this;
+        var browser = this.browser;
+        var layout = this.inherited(arguments);
+        var clabel = this.name + "-collapsed";
+        return declare.safeMixin(layout, {
+            addRect: function (id, left, right, height, data) {
+                var cm = thisB.collapsedMode || browser.cookie(clabel) == "true";
+                //store height for collapsed mode
+                if (cm) {
+                    var pHeight = Math.ceil(height / this.pitchY);
+                    this.pTotalHeight = Math.max(this.pTotalHeight || 0, pHeight);
+                }
+                var ycoord = (data&&data.get('strand'))==-1?20:0;
+                return cm ? ycoord : this.inherited(arguments);
+            }
+        });
+    },
+    _trackMenuOptions: function () {
+        var thisB = this;
+        var browser = this.browser;
+        var clabel = this.name + "-collapsed";
+        var options = this.inherited(arguments) || [];
+
+        options.push({
+            label: "Collapsed view",
+            title: "Collapsed view",
+            type: 'dijit/CheckedMenuItem',
+            checked: !!('collapsedMode' in thisB ? thisB.collapsedMode : browser.cookie(clabel) == "true"),
+            onClick: function (event) {
+                thisB.collapsedMode = this.get("checked");
+                browser.cookie(clabel, this.get("checked") ? "true" : "false");
+                var temp = thisB.showLabels;
+                if (this.get("checked")) {
+                    thisB.showLabels = false;
+                }
+                else if (thisB.previouslyShowLabels) {
+                    thisB.showLabels = true;
+                }
+                thisB.previouslyShowLabels = temp;
+                delete thisB.trackMenu;
+                thisB.makeTrackMenu();
+                thisB.redraw();
+            }
+        });
+
+        return options;
+    },
+    updateContextMenu: function() {
+        var atrack = this.webapollo.getAnnotTrack();
+        if (!atrack || !atrack.isLoggedIn() || !atrack.hasWritePermission()) {
+            this.contextMenuItems["create_annotation"].set("disabled", true);
+        }
+        else {
+            this.contextMenuItems["create_annotation"].set("disabled", false);
+        }
+
+    },
+    updateFeatureLabelPositions: function( coords ) {
+        var showLabels=this.webapollo._showLabels;
+        if( ! 'x' in coords )
+            return;
+
+        array.forEach( this.blocks, function( block, blockIndex ) {
+
+
+            // calculate the view left coord relative to the
+            // block left coord in units of pct of the block
+            // width
+            if( ! block || ! this.label )
+                return;
+            var viewLeft = 100 * ( (this.label.offsetLeft+(showLabels?this.label.offsetWidth:0)) - block.domNode.offsetLeft ) / block.domNode.offsetWidth + 2;
+
+            // if the view start is unknown, or is to the
+            // left of this block, we don't have to worry
+            // about adjusting the feature labels
+            if( ! viewLeft )
+                return;
+
+            var blockWidth = block.endBase - block.startBase;
+
+            array.forEach( block.domNode.childNodes, function( featDiv ) {
+                              if( ! featDiv.label ) return;
+                              var labelDiv = featDiv.label;
+                              var feature = featDiv.feature;
+
+                              // get the feature start and end in terms of block width pct
+                              var minLeft = parseInt( feature.get('start') );
+                              minLeft = 100 * (minLeft - block.startBase) / blockWidth;
+                              var maxLeft = parseInt( feature.get('end') );
+                              maxLeft = 100 * ( (maxLeft - block.startBase) / blockWidth
+                                                - labelDiv.offsetWidth / block.domNode.offsetWidth
+                                              );
+
+                              // move our label div to the view start if the start is between the feature start and end
+                              labelDiv.style.left = Math.max( minLeft, Math.min( viewLeft, maxLeft ) ) + '%';
+
+                          },this);
+        },this);
+    }
+
+});
+
+        return draggableTrack;
+});
+
+/*  Subclass of FeatureTrack that allows features to be selected,
+    and dragged and dropped into the annotation track to create annotations.
+
+    Note:
+    for selection to work for features that cross block boundaries, z-index of feature style MUST be set, and must be > 0
+    otherwise what happens is:
+          feature div inherits z-order from parent, so same z-order as block
+          so feature div pixels may extend into next block, but next block draws ON TOP OF IT (assuming next block added
+          to parent after current block).  So events over part of feature div that isn't within it's parent block will never
+          reach feature div but instead be triggered on next block
+    This issue will be more obvious if blocks have background color set since then not only will selection not work but
+       part of feature div that extends into next block won't even be visible, since next block background will render over it
+ */
+
+
+
+
+ /*
+   Copyright (c) 2010-2011 Berkeley Bioinformatics Open-source Projects & Lawrence Berkeley National Labs
+
+   This package and its accompanying libraries are free software; you can
+   redistribute it and/or modify it under the terms of the LGPL (either
+   version 2.1, or at your option, any later version) or the Artistic
+   License 2.0.  Refer to LICENSE for the full license text.
+*/
diff --git a/client/apollo/js/View/Track/DraggableResultFeatures.js b/client/apollo/js/View/Track/DraggableResultFeatures.js
new file mode 100644
index 0000000..23fc39d
--- /dev/null
+++ b/client/apollo/js/View/Track/DraggableResultFeatures.js
@@ -0,0 +1,49 @@
+define( [
+         'dojo/_base/declare',
+         'WebApollo/View/Track/DraggableHTMLFeatures',
+         'WebApollo/JSONUtils'
+         ],
+         function( declare, DraggableFeatureTrack, JSONUtils ) {
+
+var DraggableResultFeatures = declare( DraggableFeatureTrack, {   
+    constructor: function(args)  {  },
+
+    makeTrackMenu: function() {
+        var track = this;
+        this.inherited(arguments);
+        var trackMenu = this.trackMenu;
+        var annotTrack;
+        for (var i = 0; i < this.genomeView.tracks.length; ++i) {
+            if (this.genomeView.tracks[i].isWebApolloAnnotTrack) {
+                annotTrack = this.genomeView.tracks[i];
+                break;
+            }
+        }
+        if (trackMenu && annotTrack) {
+            var mitems = this.trackMenu.getChildren();
+            for (var mindex=0; mindex < mitems.length; mindex++) {
+                    if (mitems[mindex].type == "dijit/MenuSeparator")  { break; }
+            }
+            trackMenu.addChild(new dijit.MenuItem({
+                label: "Promote all to annotations",
+                iconClass: 'dijitIconEdit',
+                onClick: function() {
+                    if (confirm("Are you sure you want to promote all annotations?")) {
+                        var featuresToAdd = new Array();
+                        track.store.getFeatures({start: track.store.refSeq.start, end: track.store.refSeq.end}, function(feature) {
+                            var afeat = JSONUtils.createApolloFeature(feature, "transcript");
+                            featuresToAdd.push(afeat);
+                        });
+                        var postData = '{ "track": "' + annotTrack.getUniqueTrackName() + '", "features": ' + JSON.stringify(featuresToAdd) + ', "operation": "add_transcript" }';
+                        annotTrack.executeUpdateOperation(postData);
+                    }
+                }
+            }), mindex);
+        }
+
+    }
+});
+
+return DraggableResultFeatures;
+
+});
diff --git a/client/apollo/js/View/Track/SequenceTrack.js b/client/apollo/js/View/Track/SequenceTrack.js
new file mode 100644
index 0000000..8dc7487
--- /dev/null
+++ b/client/apollo/js/View/Track/SequenceTrack.js
@@ -0,0 +1,1239 @@
+define( [
+    'dojo/_base/declare',
+    'dojo/request/xhr',
+    'JBrowse/Store/Sequence/StaticChunked',
+    'WebApollo/Store/SeqFeature/ScratchPad',
+    'WebApollo/View/Track/DraggableHTMLFeatures',
+    'WebApollo/JSONUtils',
+    'WebApollo/Permission',
+    'dojox/widget/Standby',
+    'jquery/jquery'
+     ],
+function( declare,
+    xhr,
+    StaticChunked,
+    ScratchPad,
+    DraggableFeatureTrack,
+    JSONUtils,
+    Permission,
+    Standby,
+    $
+) {
+
+var SequenceTrack = declare( "SequenceTrack", DraggableFeatureTrack,
+{
+
+/**
+ * Track to display the underlying reference sequence, when zoomed in
+ * far enough.
+ * @class
+ * @constructor
+ */
+    constructor: function( args ) {
+        this.isWebApolloSequenceTrack = true;
+        var track = this;
+
+        /**
+         * DraggableFeatureTrack now has its own context menu for divs,
+         * and adding this flag provides a quick way to short-circuit it's
+         * initialization
+         */
+        this.has_custom_context_menu = true;
+        //        this.use_standard_context_menu = false;
+        this.show_reverse_strand = true;
+        this.show_protein_translation = true;
+        this.context_path = "..";
+        this.verbose_server_notification = false;
+
+        this.residues_context_menu = new dijit.Menu({});  // placeholder till setAnnotTrack() triggers real menu init
+        this.annot_context_menu = new dijit.Menu({});     // placeholder till setAnnotTrack() triggers real menu init
+
+        this.residuesMouseDown = function(event) {
+            track.onResiduesMouseDown(event);
+        };
+
+//        this.charSize = this.webapollo.getSequenceCharacterSize();
+        //        this.charWidth = this.charSize.charWidth;
+        //        this.seqHeight = this.charSize.seqHeight;
+
+        // splitting seqHeight into residuesHeight and translationHeight, so future iteration may be possible
+        //    for DNA residues and protein translation to be different styles
+        //        this.dnaHeight = this.seqHeight;
+        //        this.proteinHeight = this.seqHeight;
+
+        // this.refSeq = refSeq;  already assigned in BlockBased superclass
+
+        if (this.store.name == 'refseqs') {
+            this.sequenceStore = this.store;
+            var annotStoreConfig = dojo.clone(this.config);
+            annotStoreConfig.browser = this.browser;
+            annotStoreConfig.refSeq = this.refSeq;
+            var annotStore = new ScratchPad(annotStoreConfig);
+            this.store = annotStore;
+            annotStoreConfig.name = this.store.name;
+            this.browser._storeCache[this.store.name] = {
+                refCount: 1,
+                store: this.store
+            };
+        }
+        else  {
+            var seqStoreConfig = dojo.clone(this.config);
+            seqStoreConfig.storeClass = "JBrowse/Store/Sequence/StaticChunked";
+            seqStoreConfig.type = "JBrowse/Store/Sequence/StaticChunked";
+            // old style, using residuesUrlTemplate
+            if (this.config.residuesUrlTemplate) {
+                seqStoreConfig.urlTemplate = this.config.residuesUrlTemplate;
+            }
+
+            var inner_config = dojo.clone(seqStoreConfig);
+            // need a seqStoreConfig.config,
+            //   since in StaticChunked constructor seqStoreConfig.baseUrl is ignored,
+            //   and seqStoreConfig.config.baseUrl is used instead (as of JBrowse 1.9.8+)
+            seqStoreConfig.config = inner_config;
+            // must add browser and refseq _after_ cloning, otherwise get Browser errors
+            seqStoreConfig.browser = this.browser;
+            seqStoreConfig.refSeq = this.refSeq;
+
+            this.sequenceStore = new StaticChunked(seqStoreConfig);
+            this.browser._storeCache[ 'refseqs'] = {
+                refCount: 1,
+                store: this.sequenceStore
+            };
+        }
+
+        this.trackPadding = 10;
+        this.SHOW_IF_FEATURES = true;
+        this.ALWAYS_SHOW = false;
+        // this.setLoaded();
+        //        this.initContextMenu();
+
+        /*
+        var atrack = this.getAnnotTrack();
+        if (atrack)  {
+            this.setAnnotTrack(atrack);
+        }
+        */
+
+        this.translationTable = {};
+
+        var initAnnotTrack = dojo.hitch(this, function() {
+            var atrack = this.getAnnotTrack();
+            if (atrack && this.div) {
+                this.setAnnotTrack(atrack);
+            }
+            else {
+                window.setTimeout(initAnnotTrack, 100);
+            }
+        });
+        initAnnotTrack();
+
+    },
+
+// annotSelectionManager is class variable (shared by all AnnotTrack instances)
+// SequenceTrack.seqSelectionManager = new FeatureSelectionManager();
+
+// setting up selection exclusiveOr --
+//    if selection is made in annot track, any selection in other tracks is deselected, and vice versa,
+//    regardless of multi-select mode etc.
+// SequenceTrack.seqSelectionManager.addMutualExclusion(DraggableFeatureTrack.selectionManager);
+// SequenceTrack.seqSelectionManager.addMutualExclusion(AnnotTrack.annotSelectionManager);
+//DraggableFeatureTrack.selectionManager.addMutualExclusion(SequenceTrack.seqSelectionManager);
+
+//    loadSuccess: function(trackInfo)  { }  // loadSuccess no longer called by track initialization/loading
+    _defaultConfig: function() {
+        var thisConfig = this.inherited(arguments);
+        // nulling out menuTemplate to suppress default JBrowse feature contextual menu
+        thisConfig.menuTemplate = null;
+        thisConfig.maxFeatureScreenDensity = 100000; // set rediculously high, ensures will never show "zoomed too far out" placeholder
+        thisConfig.style.renderClassName = null;
+        thisConfig.style.arrowheadClass = null;
+        thisConfig.style.centerChildrenVertically = false;
+        thisConfig.ignoreFeatureFilter = true;
+        thisConfig.style.showLabels = false;
+        thisConfig.pinned = true;
+        return thisConfig;
+    },
+
+    /** removing "Pin to top" menuitem, so SequenceTrack is always pinned
+     *    and "Delete track" menuitem, so can't be deleted
+     *   (very hacky since depends on label property of menuitem config)
+     */
+   _trackMenuOptions: function() {
+       var options = this.inherited( arguments );
+       options = this.webapollo.removeItemWithLabel(options, "Pin to top");
+       options = this.webapollo.removeItemWithLabel(options, "Delete track");
+       return options;
+   },
+
+    loadTranslationTable: function() {
+        var track = this;
+        var query={
+            "track": track.annotTrack.getUniqueTrackName(),
+            "operation": "get_translation_table"
+        };
+        return xhr.post(track.context_path + "/AnnotationEditorService", {
+            data: JSON.stringify(query),
+            handleAs: "json"
+        }).then(function(response) { //
+            track.translationTable = {};
+            var ttable = response.translation_table;
+            for (var codon in ttable) {
+                // looping through codon table, make sure not hitting generic properties...
+                if (ttable.hasOwnProperty(codon)) {
+                    var aa = ttable[codon];
+                    var nucs = [];
+                    for (var i=0; i<3; i++) {
+                        var nuc = codon.charAt(i);
+                        nucs[i] = [];
+                        nucs[i][0] = nuc.toUpperCase();
+                        nucs[i][1] = nuc.toLowerCase();
+                    }
+                    for (var i=0; i<2; i++) {
+                        var n0 = nucs[0][i];
+                        for (var j=0; j<2; j++) {
+                            var n1 = nucs[1][j];
+                            for (var k=0; k<2; k++) {
+                                var n2 = nucs[2][k];
+                                var triplet = n0 + n1 + n2;
+                                track.translationTable[triplet] = aa;
+                                // console.log("triplet: ", triplet, ", aa: ", aa );
+                            }
+                        }
+                    }
+                }
+            }
+            track.changed();
+        },
+        function(response) {
+            console.log("get_translation_table error",response);
+            return response;
+        });
+    },
+
+    /**
+     * called by AnnotTrack to initiate sequence alterations load
+     */
+    loadSequenceAlterations: function() {
+        var track = this;
+
+        var query={
+            "track": track.annotTrack.getUniqueTrackName(),
+            "operation": "get_sequence_alterations",
+            "organism": track.webapollo.organism,
+            "clientToken": track.getAnnotTrack().getClientToken()
+        };
+
+        return dojo.xhrPost( {
+            postData: JSON.stringify(query),
+            url: track.context_path + "/AnnotationEditorService",
+            handleAs: "json",
+            load: function(response, ioArgs) { //
+                var responseFeatures = response.features;
+                if(!responseFeatures) {
+                    alert("Error: "+JSON.stringify(response));
+                    return;
+                }
+                for (var i = 0; i < responseFeatures.length; i++) {
+                    var jfeat = JSONUtils.createJBrowseSequenceAlteration(responseFeatures[i]);
+                    track.store.insert(jfeat);
+                }
+                track.featureCount = track.storedFeatureCount();
+                if (track.ALWAYS_SHOW || (track.SHOW_IF_FEATURES && track.featureCount > 0)) {
+                    track.show();
+                }
+                else {
+                    track.hide();
+                }
+                track.changed();
+            },
+            error: function(response) {
+                return response;
+            }
+        });
+    },
+
+    startZoom: function(destScale, destStart, destEnd) {
+        // would prefer to only try and hide dna residues on zoom if previous scale was at base pair resolution
+        //   (otherwise there are no residues to hide), but by time startZoom is called, pxPerBp is already set to destScale,
+        //    so would require keeping prevScale var around, or passing in prevScale as additional parameter to startZoom()
+        // so for now just always trying to hide residues on a zoom, whether they're present or not
+
+        $(".dna-residues", this.div).css('display', 'none');
+        $(".block-seq-container", this.div).css('height', '20px');
+        this.heightUpdate(20);
+        this.gview.trackHeightUpdate(this.name, Math.max(this.labelHeight, 20));
+    },
+
+    endZoom: function(destScale, destBlockBases) {
+        var charSize = this.webapollo.getSequenceCharacterSize();
+
+        if( ( destScale == charSize.width ) ||
+            this.ALWAYS_SHOW || (this.SHOW_IF_FEATURES && this.featureCount > 0)) {
+            this.show();
+        }
+        else  {
+            this.hide();
+        }
+        this.clear();
+    },
+
+    setViewInfo: function( genomeView, numBlocks,
+                           trackDiv, labelDiv,
+                           widthPct, widthPx, scale ) {
+
+        this.inherited( arguments );
+
+        var charSize = this.webapollo.getSequenceCharacterSize();
+        if ( (scale == charSize.width ) ||
+            this.ALWAYS_SHOW || (this.SHOW_IF_FEATURES && this.featureCount > 0) ) {
+            this.show();
+        } else {
+            this.hide();
+            this.heightUpdate(0);
+        }
+        this.setLabel( this.key );
+    },
+
+    startStandby: function() {
+        if (this.standby == null) {
+            this.standby = new Standby({target: this.div, color: "transparent",image:"plugins/WebApollo/img/loading.gif"});
+            document.body.appendChild(this.standby.domNode);
+            this.standby.startup();
+            this.standby.show();
+        }
+    },
+
+    stopStandby: function() {
+        if (this.standby != null) {
+            this.standby.hide();
+        }
+    },
+
+    fillBlock: function( args ) {
+        var blockIndex = args.blockIndex;
+        var block = args.block;
+        var leftBase = args.leftBase;
+        var rightBase = args.rightBase;
+        var scale = args.scale;
+        var containerStart = args.containerStart;
+        var containerEnd = args.containerEnd;
+
+        var verbose = false;
+        var fillArgs = arguments;
+        var track = this;
+
+        var finishCallback = args.finishCallback;
+            args.finishCallback = function() {
+                finishCallback();
+                track.stopStandby();
+            };
+
+        var charSize = this.webapollo.getSequenceCharacterSize();
+        if ((scale == charSize.width) ||
+                this.ALWAYS_SHOW || (this.SHOW_IF_FEATURES && this.featureCount > 0) ) {
+            this.show();
+        } else {
+            this.hide();
+            this.heightUpdate(0);
+        }
+        var blockHeight = 0;
+
+        if (this.shown) {
+            // make a div to contain the sequences
+            var seqNode = document.createElement("div");
+            seqNode.className = "wa-sequence";
+            // seq_block_container style sets width = 100%, so seqNode fills the block width
+            //    regardless of whether holding residue divs or not
+            $(seqNode).addClass("block-seq-container");
+            block.domNode.appendChild(seqNode);
+
+            var slength = rightBase - leftBase;
+
+            // just always add two base pairs to front and end,
+            //    to make sure can do three-frame translation across for every base position in (leftBase..rightBase),
+            //    both forward (need tw pairs on end) and reverse (need 2 extra bases at start)
+            var leftExtended = leftBase - 2;
+            var rightExtended = rightBase + 2;
+
+            var dnaHeight     = charSize.height;
+            var proteinHeight = charSize.height;
+
+            if ( scale == charSize.width ) {
+                this.sequenceStore.getReferenceSequence(
+                    { ref: this.refSeq.name, start: leftExtended, end: rightExtended },
+                    function( seq ) {
+                        var start = args.leftBase - 2;
+                        var end = args.rightBase + 2;
+
+                        if (start < 0) {
+                            start = 0;
+                        }
+                        if (args.leftBase == -1) {
+                            var idx = seq.lastIndexOf(" ");
+                            seq = seq.substring(0, idx) + SequenceTrack.nbsp + seq.substring(idx + 1);
+                        }
+
+                        var blockStart = start + 2;
+                        var blockEnd = end - 2;
+                        var blockResidues = seq.substring(2, seq.length-2);
+                        var blockLength = blockResidues.length;
+                        var extendedStart = start;
+                        var extendedEnd = end;
+                        var extendedStartResidues = seq.substring(0, seq.length-2);
+                        var extendedEndResidues = seq.substring(2);
+
+                        if (track.show_protein_translation) {
+                            var framedivs = [];
+                            for (var i=0; i<3; i++) {
+                                var tstart = blockStart + i;
+                                var frame = tstart % 3;
+                                var transProtein = track.renderTranslation( extendedEndResidues, i, blockLength);
+                                // if coloring CDS in feature tracks by frame, use same "cds-frame" styling,
+                                //    otherwise use more muted "frame" styling
+                                $(transProtein).addClass("cds-frame" + frame);
+                                framedivs[frame] = transProtein;
+                            }
+                            for (var i=2; i>=0; i--) {
+                                var transProtein = framedivs[i];
+                                seqNode.appendChild(transProtein);
+                                $(transProtein).bind("mousedown", track.residuesMouseDown);
+                                blockHeight += proteinHeight;
+                            }
+                        }
+
+                        // add a div for the forward strand
+                        var forwardDNA = track.renderResidues( blockResidues );
+                        $(forwardDNA).addClass("forward-strand");
+                        seqNode.appendChild( forwardDNA );
+
+
+                        track.residues_context_menu.bindDomNode(forwardDNA);
+                        $(forwardDNA).bind("mousedown", track.residuesMouseDown);
+                        blockHeight += dnaHeight;
+
+                        if (track.show_reverse_strand) {
+                            // and one for the reverse strand
+                            // var reverseDNA = track.renderResidues( start, end, track.complement(seq) );
+                            var reverseDNA = track.renderResidues( track.complement(blockResidues) );
+                            $(reverseDNA).addClass("reverse-strand");
+                            seqNode.appendChild( reverseDNA );
+                            // dnaContainer.appendChild(reverseDNA);
+                            track.residues_context_menu.bindDomNode(reverseDNA);
+                            $(reverseDNA).bind("mousedown", track.residuesMouseDown);
+                            blockHeight += dnaHeight;
+                        }
+
+                        // set up highlighting of base pair underneath mouse
+                        $(forwardDNA).bind("mouseleave", function(event) {
+                            track.removeTextHighlight(forwardDNA);
+                            if (reverseDNA) { track.removeTextHighlight(reverseDNA); }
+                            track.last_dna_coord = undefined;
+                        } );
+                        $(forwardDNA).bind("mousemove", function(event) {
+                            var gcoord = track.getGenomeCoord(event);
+                            if (gcoord >= 0 && ((!track.last_dna_coord) || (gcoord !== track.last_dna_coord))) {
+                                var blockCoord = gcoord - leftBase;
+                                track.last_dna_coord = gcoord;
+                                track.setTextHighlight(forwardDNA, blockCoord, blockCoord, "dna-highlighted");
+                                if (!track.freezeHighlightedBases) {
+                                    track.lastHighlightedForwardDNA = forwardDNA;
+                                }
+                                if (reverseDNA)  {
+                                    track.setTextHighlight(reverseDNA, blockCoord, blockCoord, "dna-highlighted");
+                                    if (!track.freezeHighlightedBases) {
+                                        track.lastHighlightedReverseDNA = reverseDNA;
+                                    }
+                                }
+                            }
+                            else if (gcoord < 0) {
+                                track.clearHighlightedBases();
+                            }
+                        } );
+                        if (reverseDNA) {
+                            $(reverseDNA).bind("mouseleave", function(event) {
+                                track.removeTextHighlight(forwardDNA);
+                                track.removeTextHighlight(reverseDNA);
+                                track.last_dna_coord = undefined;
+                            } );
+                            $(reverseDNA).bind("mousemove", function(event) {
+                                var gcoord = track.getGenomeCoord(event);
+                                if (gcoord >= 0 && ((!track.last_dna_coord) || (gcoord !== track.last_dna_coord))) {
+                                    var blockCoord = gcoord - leftBase;
+                                    track.last_dna_coord = gcoord;
+                                    track.setTextHighlight(forwardDNA, blockCoord, blockCoord, "dna-highlighted");
+                                    track.setTextHighlight(reverseDNA, blockCoord, blockCoord, "dna-highlighted");
+                                    if (!track.freezeHighlightedBases) {
+                                        track.lastHighlightedForwardDNA = forwardDNA;
+                                        track.lastHighlightedReverseDNA = reverseDNA;
+                                    }
+                                }
+                                else if (gcoord < 0) {
+                                    track.clearHighlightedBases();
+                                }
+                            } );
+                        }
+
+                        if (track.show_protein_translation && track.show_reverse_strand) {
+                            var extendedReverseComp = track.reverseComplement(extendedStartResidues);
+                            if (verbose)  { console.log("extendedReverseComp: " + extendedReverseComp); }
+                            var framedivs = [];
+                            var offset = ( 2 - (track.refSeq.length  % 3)  )  ;
+                            for (var i=2; i>=0; i--) {
+                                var transStart = blockStart + 1 - i;
+                                var frame = (transStart % 3 + 3) % 3;
+                                frame = (frame + offset )% 3;
+                                var transProtein = track.renderTranslation( extendedStartResidues, i, blockLength, true);
+                                $(transProtein).addClass("neg-cds-frame" + frame);
+                                framedivs[frame] = transProtein;
+                            }
+                            for (var i=2; i>=0; i--) {
+                            // for (var i=0; i<3; i++) {
+                                var transProtein = framedivs[i];
+                                seqNode.appendChild(transProtein);
+                                $(transProtein).bind("mousedown", track.residuesMouseDown);
+                                blockHeight += proteinHeight;
+                            }
+                        }
+                        track.inherited("fillBlock", fillArgs);
+                        blockHeight += 5;  // a little extra padding below (track.trackPadding used for top padding)
+                        track.heightUpdate(blockHeight, blockIndex);
+                    },
+                    function() {}
+                    );
+                }
+                else  {
+                    blockHeight = 20;  // default dna track height if not zoomed to base level
+                    seqNode.style.height = "20px";
+
+                    track.inherited("fillBlock", arguments);
+                    track.heightUpdate(blockHeight, blockIndex);
+                }
+        } else {
+            this.heightUpdate(0, blockIndex);
+        }
+    },
+
+    addFeatureToBlock: function( feature, uniqueId, block, scale, labelScale, descriptionScale,
+                                 containerStart, containerEnd ) {
+        var featDiv =
+        this.renderFeature(feature, uniqueId, block, scale, labelScale, descriptionScale, containerStart, containerEnd);
+        $(featDiv).addClass("sequence-alteration");
+
+        var charSize = this.webapollo.getSequenceCharacterSize();
+
+        var seqNode = $("div.wa-sequence", block.domNode).get(0);
+        featDiv.style.top = "0px";
+        var ftype = feature.get("type");
+        if (ftype) {
+            if (ftype == "deletion") {
+
+            }
+            else if (ftype == "insertion") {
+                if ( scale == charSize.width ) {
+                    var container  = document.createElement("div");
+                    var residues = feature.get("residues");
+                    $(container).addClass("dna-residues");
+                    container.appendChild( document.createTextNode( residues ) );
+                    container.style.position = "absolute";
+                    container.style.top = "-16px";
+                    container.style.border = "2px solid #00CC00";
+                    container.style.backgroundColor = "#AAFFAA";
+                    featDiv.appendChild(container);
+                }
+            }
+            else if ((ftype == "substitution")) {
+                if ( scale == charSize.width ) {
+                    var container  = document.createElement("div");
+                    var residues = feature.get("residues");
+                    $(container).addClass("dna-residues");
+                    container.appendChild( document.createTextNode( residues ) );
+                    container.style.position = "absolute";
+                    container.style.top = "-16px";
+                    container.style.border = "1px solid black";
+                    container.style.backgroundColor = "#FFF506";
+                    featDiv.appendChild(container);
+                }
+            }
+        }
+        seqNode.appendChild(featDiv);
+        return featDiv;
+    },
+
+    /**
+     *  overriding renderFeature to add event handling right-click context menu
+     */
+    renderFeature: function( feature, uniqueId, block, scale, labelScale, descriptionScale,
+                             containerStart, containerEnd ) {
+        var featDiv = this.inherited( arguments );
+
+        if (featDiv && featDiv != null)  {
+            this.annot_context_menu.bindDomNode(featDiv);
+        }
+        return featDiv;
+    },
+
+    reverseComplement: function(seq) {
+        return this.reverse(this.complement(seq));
+    },
+
+    reverse: function(seq) {
+        return seq.split("").reverse().join("");
+    },
+
+    complement: (function() {
+                     var compl_rx   = /[ACGT]/gi;
+
+                     // from bioperl: tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/
+                     // generated with:
+                     // perl -MJSON -E '@l = split "","acgtrymkswhbvdnxACGTRYMKSWHBVDNX"; print to_json({ map { my $in = $_; tr/acgtrymkswhbvdnxACGTRYMKSWHBVDNX/tgcayrkmswdvbhnxTGCAYRKMSWDVBHNX/; $in => $_ } @l})'
+                     var compl_tbl  = {"S":"S","w":"w","T":"A","r":"y","a":"t","N":"N","K":"M","x":"x","d":"h","Y":"R","V":"B","y":"r","M":"K","h":"d","k":"m","C":"G","g":"c","t":"a","A":"T","n":"n","W":"W","X":"X","m":"k","v":"b","B":"V","s":"s","H":"D","c":"g","D":"H","b":"v","R":"Y","G":"C"};
+
+                     var compl_func = function(m) { return compl_tbl[m] || SequenceTrack.nbsp; };
+                     return function( seq ) {
+                         return seq.replace( compl_rx, compl_func );
+                     };
+                 })(),
+
+    // given the start and end coordinates, and the sequence bases, creates a div containing the sequence
+    renderResidues: function ( seq ) {
+        var container  = document.createElement("div");
+        $(container).addClass("dna-residues");
+        container.appendChild( document.createTextNode( seq ) );
+        return container;
+    },
+
+    // end is ignored, assume all of seq is translated (except any extra bases at end)
+    renderTranslation: function ( input_seq, offset, blockLength, reverse) {
+            var CodonTable = this.translationTable;
+        var verbose = false;
+        // sequence of diagnostic block
+        //    var verbose = (input_seq === "GTATATTTTGTACGTTAAAAATAAAAA" || input_seq === "GCGTATATTTTGTACGTTAAAAATAAA" );
+        var seq;
+        if (reverse) {
+            seq = this.reverseComplement(input_seq);
+            if (verbose) { console.log("revcomped, input: " + input_seq + ", output: " + seq); }
+        }
+        else  {
+            seq = input_seq;
+        }
+        var container  = document.createElement("div");
+        $(container).addClass("dna-residues");
+        $(container).addClass("aa-residues");
+        $(container).addClass("offset" + offset);
+        var prefix = "";
+        var suffix = "";
+        for (var i=0; i<offset; i++) { prefix += SequenceTrack.nbsp; }
+        for (var i=0; i<(2-offset); i++) { suffix += SequenceTrack.nbsp; }
+
+        var extra_bases = (seq.length - offset) % 3;
+        var dnaRes = seq.substring(offset, seq.length - extra_bases);
+        if (verbose)  { console.log("to translate: " + dnaRes + ", length = " + dnaRes.length); }
+        var aaResidues = dnaRes.replace(/(...)/gi,  function(codon) {
+                                            var aa = CodonTable[codon];
+                                            // if no mapping and blank in codon, return blank
+                                            // if no mapping and no blank in codon,  return "?"
+                                            if (!aa) {
+                                                if (codon.indexOf(SequenceTrack.nbsp) >= 0) { aa = SequenceTrack.nbsp; }
+                                                else  { aa = "?"; }
+                                            }
+                                            return prefix + aa + suffix;
+                                            // return aa;
+                                        } );
+        var trimmedAaResidues = aaResidues.substring(0, blockLength);
+        if (verbose)  { console.log("AaLength: " + aaResidues.length + ", trimmedAaLength = " + trimmedAaResidues.length); }
+        aaResidues = trimmedAaResidues;
+        if (reverse) {
+            var revAaResidues = this.reverse(aaResidues);
+            if (verbose) { console.log("reversing aa string, input: \"" + aaResidues + "\", output: \"" + revAaResidues + "\""); }
+            aaResidues = revAaResidues;
+            while (aaResidues.length < blockLength)  {
+                aaResidues = SequenceTrack.nbsp + aaResidues;
+            }
+        }
+        container.appendChild( document.createTextNode( aaResidues ) );
+        return container;
+    },
+
+    onResiduesMouseDown: function(event)  {
+        this.last_mousedown_event = event;
+    },
+
+    onFeatureMouseDown: function(event) {
+        this.last_mousedown_event = event;
+        var ftrack = this;
+        if (ftrack.verbose_selection || ftrack.verbose_drag)  {
+            console.log("SequenceTrack.onFeatureMouseDown called");
+        }
+        this.handleFeatureSelection(event);
+    },
+
+    initContextMenu: function() {
+        var thisObj = this;
+        thisObj.contextMenuItems = new Array();
+        thisObj.annot_context_menu = new dijit.Menu({});
+
+        var index = 0;
+        if (this.annotTrack.permission & Permission.WRITE) {
+            thisObj.annot_context_menu.addChild(new dijit.MenuItem( {
+                label: "Delete",
+                onClick: function() {
+                    thisObj.deleteSelectedFeatures();
+                }
+            } ));
+            thisObj.contextMenuItems["delete"] = index++;
+        }
+        thisObj.annot_context_menu.addChild(new dijit.MenuItem( {
+            label: "Information",
+            onClick: function(event) {
+                thisObj.getInformation();
+            }
+        } ));
+        thisObj.contextMenuItems["information"] = index++;
+
+        thisObj.annot_context_menu.onOpen = function(event) {
+                // keeping track of mousedown event that triggered annot_context_menu popup,
+                //   because need mouse position of that event for some actions
+                thisObj.annot_context_mousedown = thisObj.last_mousedown_event;
+                // if (thisObj.permission & Permission.WRITE) { thisObj.updateMenu(); }
+                dojo.forEach(this.getChildren(), function(item, idx, arr) {
+                    if (item._setSelected)  { item._setSelected(false); }  // test for function existence first
+                    if (item._onUnhover)  { item._onUnhover(); }  // test for function existence first
+                });
+        };
+
+        /**
+         *   context menu for right click on sequence residues
+         */
+        thisObj.residuesMenuItems = new Array();
+        thisObj.residues_context_menu = new dijit.Menu({});
+        index = 0;
+
+        thisObj.residuesMenuItems["toggle_reverse_strand"] = index++;
+        thisObj.residues_context_menu.addChild(new dijit.MenuItem( {
+            label: "Toggle Reverse Strand",
+            onClick: function(event) {
+                thisObj.show_reverse_strand = ! thisObj.show_reverse_strand;
+                thisObj.clearHighlightedBases();
+                thisObj.changed();
+            }
+        } ));
+
+        thisObj.residuesMenuItems["toggle_protein_translation"] = index++;
+        thisObj.residues_context_menu.addChild(new dijit.MenuItem( {
+            label: "Toggle Protein Translation",
+            onClick: function(event) {
+                thisObj.show_protein_translation = ! thisObj.show_protein_translation;
+                thisObj.clearHighlightedBases();
+                thisObj.changed();
+            }
+        } ));
+
+
+        if (this.annotTrack.permission & Permission.WRITE) {
+
+            thisObj.residues_context_menu.addChild(new dijit.MenuSeparator());
+                thisObj.residues_context_menu.addChild(new dijit.MenuItem( {
+                    label: "Create Genomic Insertion",
+                    onClick: function() {
+                        thisObj.freezeHighlightedBases = true;
+                        thisObj.createGenomicInsertion();
+                    }
+                } ));
+                thisObj.residuesMenuItems["create_insertion"] = index++;
+                thisObj.residues_context_menu.addChild(new dijit.MenuItem( {
+                    label: "Create Genomic Deletion",
+                    onClick: function(event) {
+                            thisObj.freezeHighlightedBases = true;
+                            thisObj.createGenomicDeletion();
+                    }
+                } ));
+                thisObj.residuesMenuItems["create_deletion"] = index++;
+
+                thisObj.residues_context_menu.addChild(new dijit.MenuItem( {
+                    label: "Create Genomic Substitution",
+                    onClick: function(event) {
+                            thisObj.freezeHighlightedBases = true;
+                            thisObj.createGenomicSubstitution();
+                    }
+                } ));
+                thisObj.residuesMenuItems["create_substitution"] = index++;
+        }
+
+        thisObj.residues_context_menu.onOpen = function(event) {
+            thisObj.residues_context_mousedown = thisObj.last_mousedown_event;
+            dojo.forEach(this.getChildren(), function(item, idx, arr) {
+                 if (item._setSelected) { item._setSelected(false); }
+                 if (item._onUnhover) { item._onUnhover(); }
+            });
+
+            thisObj.freezeHighlightedBases = true;
+        };
+
+        thisObj.residues_context_menu.onBlur = function() {
+                thisObj.freezeHighlightedBases = false;
+        };
+
+        thisObj.residues_context_menu.onClose = function(event) {
+                if (!thisObj.freezeHighlightedBases) {
+                        thisObj.clearHighlightedBases();
+                }
+        };
+
+        thisObj.annot_context_menu.startup();
+        thisObj.residues_context_menu.startup();
+    },
+
+    getUniqueTrackName: function() {
+        return this.name + "-" + this.refSeq.name;
+    },
+
+    createGenomicInsertion: function()  {
+        var gcoord = this.getGenomeCoord(this.residues_context_mousedown);
+
+        var content = this.createAddSequenceAlterationPanel("insertion", gcoord);
+        this.annotTrack.openDialog("Add Insertion", content);
+    },
+
+    createGenomicDeletion: function()  {
+        var gcoord = this.getGenomeCoord(this.residues_context_mousedown);
+
+        var content = this.createAddSequenceAlterationPanel("deletion", gcoord);
+        this.annotTrack.openDialog("Add Deletion", content);
+
+    },
+
+    createGenomicSubstitution: function()  {
+        var gcoord = this.getGenomeCoord(this.residues_context_mousedown);
+        var content = this.createAddSequenceAlterationPanel("substitution", gcoord);
+        this.annotTrack.openDialog("Add Substitution", content);
+    },
+
+    deleteSelectedFeatures: function()  {
+        var selected = this.selectionManager.getSelection();
+        this.selectionManager.clearSelection();
+        this.requestDeletion(selected);
+    },
+
+    requestDeletion: function(selected)  {
+        var track = this;
+        var features = "[ ";
+        for (var i = 0; i < selected.length; ++i) {
+                var annot = selected[i].feature;
+                if (i > 0) {
+                    features += ", ";
+                }
+                features += '{ "uniquename": "' + annot.id() + '" }';
+        }
+        features += "]";
+        var postData = '{ "track": "' + track.annotTrack.getUniqueTrackName() + '", "features": ' + features + ', "operation": "delete_sequence_alteration" }';
+        track.annotTrack.executeUpdateOperation(postData);
+    },
+
+    getInformation: function()  {
+        var selected = this.selectionManager.getSelection();
+        var annotTrack = this.getAnnotTrack();
+        if (annotTrack) {
+            annotTrack.getInformationForSelectedAnnotations(selected);
+        }
+    },
+
+    /**
+     * sequence alteration annotation ADD command received by a ChangeNotificationListener,
+     *      so telling SequenceTrack to add to it's SeqFeatureStore
+     */
+    annotationsAddedNotification: function(responseFeatures)  {
+        if (this.verbose_server_notification)  { console.log("SequenceTrack.annotationsAddedNotification() called"); }
+        var track = this;
+        // add to store
+        for (var i = 0; i < responseFeatures.length; ++i) {
+            var feat = JSONUtils.createJBrowseSequenceAlteration( responseFeatures[i] );
+            var id = responseFeatures[i].uniquename;
+            if (! this.store.getFeatureById(id))  {
+                this.store.insert(feat);
+            }
+        }
+        track.featureCount = track.storedFeatureCount();
+        if (this.ALWAYS_SHOW || (this.SHOW_IF_FEATURES && this.featureCount > 0)) {
+            this.show();
+        }
+        else {
+            this.hide();
+        }
+        // track.hideAll();   shouldn't need to call hideAll() before changed() anymore
+        track.changed();
+    },
+
+    /**
+     * sequence alteration annotation DELETE command received by a ChangeNotificationListener,
+     *      so telling SequenceTrack to remove from it's SeqFeatureStore
+     */
+    annotationsDeletedNotification: function(annots)  {
+        if (this.verbose_server_notification)  {  console.log("SequenceTrack.removeSequenceAlterations() called"); }
+        var track = this;
+        // remove from SeqFeatureStore
+        for (var i = 0; i < annots.length; ++i) {
+            var id_to_delete = annots[i].uniquename;
+            this.store.deleteFeatureById(id_to_delete);
+        }
+        track.featureCount = track.storedFeatureCount();
+        if (this.ALWAYS_SHOW || (this.SHOW_IF_FEATURES && this.featureCount > 0)) {
+            this.show();
+        }
+        else {
+            this.hide();
+        }
+        // track.hideAll();   shouldn't need to call hideAll() before changed() anymore
+        track.changed();
+    },
+
+    /*
+     *  sequence alteration UPDATE command received by a ChangeNotificationListener
+     *  currently handled as if receiving DELETE followed by ADD command
+     */
+    annotationsUpdatedNotification: function(annots)  {
+        this.annotationsDeletedNotification(annots);
+        this.annotationAddedNotification(annots);
+    },
+
+    storedFeatureCount: function(start, end)  {
+        // get accurate count of features loaded (should do this within the XHR.load() function
+        var track = this;
+        if (start == undefined) {
+            //            start = 0;
+            start = track.refSeq.start;
+        }
+        if (end == undefined) {
+            //            end = track.refSeq.length;
+            end = track.refSeq.end;
+        }
+        var count = 0;
+        track.store.getFeatures({ ref: track.refSeq.name, start: start, end: end}, function() { count++; });
+
+        return count;
+    },
+
+    createAddSequenceAlterationPanel: function(type, gcoord) {
+        var track = this;
+        var content = dojo.create("div");
+        var charWidth = 15;
+        if (type == "deletion") {
+            var deleteDiv = dojo.create("div", { }, content);
+            var deleteLabel = dojo.create("label", { innerHTML: "Length", className: "sequence_alteration_input_label" }, deleteDiv);
+            var deleteField = dojo.create("input", { type: "text", size: 10, className: "sequence_alteration_input_field" }, deleteDiv);
+            var comment = dojo.create("div", { }, content);
+            var comLabel = dojo.create("label", { innerHTML: "Comment", className: "sequence_alteration_comment_label" }, comment);
+            var comField = dojo.create("input", { type: "text", size: charWidth, className: "sequence_alteration_comment_field" }, comment);
+            $(deleteField).keydown(function(e) {
+                var unicode = e.charCode || e.keyCode;
+                var isBackspace = (unicode == 8);  // 8 = BACKSPACE
+                if (unicode == 13) {  // 13 = ENTER/RETURN
+                    addSequenceAlteration();
+                }
+                else {
+                    var newchar = String.fromCharCode(unicode);
+                    // only allow numeric chars and backspace
+                    if (! (newchar.match(/[0-9]/) || isBackspace))  {
+                        return false;
+                    }
+                }
+            });
+        }
+        else {
+            var plusDiv = dojo.create("div", { }, content);
+            var minusDiv = dojo.create("div", { }, content);
+            var plusLabel = dojo.create("label", { innerHTML: "+ strand", className: "sequence_alteration_input_label" }, plusDiv);
+            var plusField = dojo.create("input", { type: "text", size: charWidth, className: "sequence_alteration_input_field" }, plusDiv);
+            var minusLabel = dojo.create("label", { innerHTML: "- strand", className: "sequence_alteration_input_label" }, minusDiv);
+            var minusField = dojo.create("input", { type: "text", size: charWidth, className: "sequence_alteration_input_field" }, minusDiv);
+            var comment = dojo.create("div", { }, content);
+            var comLabel = dojo.create("label", { innerHTML: " Comment", className: "sequence_alteration_comment_label" }, comment);
+            var comField = dojo.create("input", { type: "text", size: charWidth, className: "sequence_alteration_comment_field" }, comment);
+
+            $(plusField).keydown(function(e) {
+                var unicode = e.charCode || e.keyCode;
+                // ignoring delete key, doesn't do anything in input elements?
+                var isBackspace = (unicode == 8);  // 8 = BACKSPACE
+                if (unicode == 13) {  // 13 = ENTER/RETURN
+                    addSequenceAlteration();
+                }
+                else {
+                    var curval = e.srcElement.value;
+                    var newchar = String.fromCharCode(unicode);
+                    // only allow acgtnACGTN and backspace
+                    //    (and acgtn are transformed to uppercase in CSS)
+                    if (newchar.match(/[acgtnACGTN]/) || isBackspace)  {
+                        // can't synchronize scroll position of two input elements,
+                        // see http://stackoverflow.com/questions/10197194/keep-text-input-scrolling-synchronized
+                        // but, if scrolling triggered (or potentially triggered), can hide other strand input element
+                        // scrolling only triggered when length of input text exceeds character size of input element
+                        if (isBackspace)  {
+                            minusField.value = track.complement(curval.substring(0,curval.length-1));
+                        }
+                        else {
+                            minusField.value = track.complement(curval + newchar);
+                        }
+                        if (curval.length > charWidth) {
+                            $(minusDiv).hide();
+                        }
+                        else {
+                            $(minusDiv).show();  // make sure is showing to bring back from a hide
+                        }
+                    }
+                    else { return false; }  // prevents entering any chars other than ACGTN and backspace
+                }
+            });
+
+            $(minusField).keydown(function(e) {
+                var unicode = e.charCode || e.keyCode;
+                // ignoring delete key, doesn't do anything in input elements?
+                var isBackspace = (unicode == 8);  // 8 = BACKSPACE
+                if (unicode == 13) {  // 13 = ENTER
+                    addSequenceAlteration();
+                }
+                else {
+                    var curval = e.srcElement.value;
+                    var newchar = String.fromCharCode(unicode);
+                    // only allow acgtnACGTN and backspace
+                    //    (and acgtn are transformed to uppercase in CSS)
+                    if (newchar.match(/[acgtnACGTN]/) || isBackspace)  {
+                        // can't synchronize scroll position of two input elements,
+                        // see http://stackoverflow.com/questions/10197194/keep-text-input-scrolling-synchronized
+                        // but, if scrolling triggered (or potentially triggered), can hide other strand input element
+                        // scrolling only triggered when length of input text exceeds character size of input element
+                        if (isBackspace)  {
+                            plusField.value = track.complement(curval.substring(0,curval.length-1));
+                        }
+                        else {
+                            plusField.value = track.complement(curval + newchar);
+                        }
+                        if (curval.length > charWidth) {
+                            $(plusDiv).hide();
+                        }
+                        else {
+                            $(plusDiv).show();  // make sure is showing to bring back from a hide
+                        }
+                    }
+                    else { return false; }  // prevents entering any chars other than ACGTN and backspace
+                }
+            });
+
+        }
+        var buttonDiv = dojo.create("div", { className: "sequence_alteration_button_div" }, content);
+        var addButton = dojo.create("button", { innerHTML: "Add", className: "sequence_alteration_button" }, buttonDiv);
+
+        var addSequenceAlteration = function() {
+            var ok = true;
+            var inputField;
+            var commentField = comField;
+            var inputField = ((type == "deletion") ? deleteField : plusField);
+            // if (type == "deletion") { inputField = deleteField; }
+            // else  { inputField = plusField; }
+            var input = inputField.value.toUpperCase();
+            var commentFieldValue = commentField.value;
+            if (input.length == 0) {
+                alert("Input cannot be empty for " + type);
+                ok = false;
+            }
+            if (ok) {
+                var input = inputField.value.toUpperCase();
+                if (type == "deletion") {
+                    if (input.match(/\D/)) {
+                        alert("The length must be a number");
+                        ok = false;
+                    }
+                    else {
+                        input = parseInt(input);
+                        if (input <= 0) {
+                            alert("The length must be a positive number");
+                            ok = false;
+                        }
+                    }
+                }
+                else {
+                    if (input.match(/[^ACGTN]/)) {
+                        alert("The sequence should only containg A, C, G, T, N");
+                        ok = false;
+                    }
+                }
+            }
+            if (ok) {
+                var fmin = gcoord;
+                var fmax;
+                if (type == "insertion") {
+                    fmax = gcoord;
+                }
+                else if (type == "deletion") {
+                    fmax = gcoord + parseInt(input);
+                }
+                else if (type == "substitution") {
+                    fmax = gcoord + input.length;
+                }
+                if (track.storedFeatureCount(fmin, fmax == fmin ? fmin + 1 : fmax) > 0) {
+                    alert("Cannot create overlapping sequence alterations");
+                }
+                else {
+                    var feature = {
+                        location: {
+                            fmin: fmin,
+                            fmax: fmax,
+                            strand: 1
+                        },
+                        type: {
+                            name: type,
+                            cv: {
+                                name: "sequence"
+                            }
+                        }
+                    };
+                    if (type != "deletion") {
+                        feature.residues = input;
+                    }
+                    if (commentFieldValue.length != 0) {
+                        feature.non_reserved_properties = [
+                            {
+                                tag: "justification",
+                                value: commentFieldValue
+                            }
+                        ];
+                    }
+                    var features = [feature];
+                    var postData = {
+                        "track": track.annotTrack.getUniqueTrackName(),
+                        "features": features,
+                        "operation": "add_sequence_alteration",
+                        "clientToken": track.annotTrack.getClientToken()
+                    };
+                    track.annotTrack.executeUpdateOperation(JSON.stringify(postData));
+                    track.annotTrack.closeDialog();
+                }
+            }
+        };
+
+        dojo.connect(addButton, "onclick", null, function() {
+                addSequenceAlteration();
+        });
+
+        return content;
+    },
+
+    handleError: function(response) {
+        console.log("ERROR: ");
+        console.log(response);  // in Firebug, allows retrieval of stack trace, jump to code, etc.
+        console.log(response.stack);
+        var error = eval('(' + response.responseText + ')');
+        if (error && error.error) {
+            alert(error.error);
+            return;
+        }
+    },
+
+    setAnnotTrack: function(annotTrack) {
+        this.startStandby();
+        // if (this.annotTrack)  { console.log("WARNING: SequenceTrack.setAnnotTrack called but annoTrack already set"); }
+        var track = this;
+
+        this.annotTrack = annotTrack;
+        this.initContextMenu();
+
+        this.loadTranslationTable().then(
+                function() {
+                    if(track.webapollo.getAnnotTrack().isLoggedIn()) {
+                        track.loadSequenceAlterations().then(function() {
+                            track.stopStandby();
+                        });
+                    }
+                },
+                function() {
+                    track.stopStandby();
+                });
+    },
+
+    /*
+     * Given an element that contains text, highlights a given range of the text
+     * If called repeatedly, removes highlighting from previous call first
+     *
+     * Assumes there is no additional markup within element, just a text node
+     *    (would like to eventually rewrite to remove this restriction?  Possibly could use HTML Range manipulation,
+     *        i.e. range.surroundContents() etc. )
+     *
+     * optionally specify what class to use to indicate highlighting (defaults to "text-highlight")
+     *
+     * adapted from http://stackoverflow.com/questions/9051369/highlight-substring-in-element
+     */
+    setTextHighlight: function (element, start, end, classname) {
+        if (this.freezeHighlightedBases) {
+            return;
+        }
+        if (! classname) { classname = "text-highlight"; }
+        var item = $(element);
+        var str = item.text();
+        if (!str) {
+            str = item.html();
+            item.data("origHTML", str);
+        }
+        var highlighted_base=str.substr(start, end - start + 1);
+        if(highlighted_base==SequenceTrack.nbsp) {
+            return;
+        }
+        str = str.substr(0, start) +
+            '<span class="' + classname + '">' +
+            str.substr(start, end - start + 1) +
+            '</span>' +
+            str.substr(end + 1);
+        item.html(str);
+    },
+
+    /*
+     *  remove highlighting added with setTextHighlight
+     */
+    removeTextHighlight: function(element) {
+        if (this.freezeHighlightedBases) {
+            return;
+        }
+        var item = $(element);
+        var str = item.text();
+        if (str) {
+            item.html(str);
+        }
+    },
+
+    clearHighlightedBases: function() {
+        this.freezeHighlightedBases = false;
+        this.removeTextHighlight(this.lastHighlightedForwardDNA);
+        if (this.lastHighlightedReverseDNA) {
+            this.removeTextHighlight(this.lastHighlightedReverseDNA);
+        }
+    },
+
+    getAnnotTrack: function()  {
+        if (this.annotTrack)  {
+            return this.annotTrack;
+        }
+        else  {
+            var tracks = this.gview.tracks;
+            for (var i = 0; i < tracks.length; i++)  {
+                // should be doing instanceof here, but class setup is not being cooperative
+                if (tracks[i].isWebApolloAnnotTrack)  {
+                    this.annotTrack = tracks[i];
+                    this.annotTrack.seqTrack = this;
+                    break;
+                }
+            }
+        }
+        return this.annotTrack;
+    },
+
+    hide: function() {
+        this.inherited(arguments);
+        var annotTrack = this.getAnnotTrack();
+        if (annotTrack && !annotTrack.isLoggedIn()) {
+            dojo.style(this.genomeView.pinUnderlay, "display", "none");
+        }
+    }
+
+});
+
+    SequenceTrack.nbsp = String.fromCharCode(160);
+    return SequenceTrack;
+});
+
diff --git a/client/apollo/js/View/Track/WebApolloAlignments2.js b/client/apollo/js/View/Track/WebApolloAlignments2.js
new file mode 100644
index 0000000..1edf009
--- /dev/null
+++ b/client/apollo/js/View/Track/WebApolloAlignments2.js
@@ -0,0 +1,123 @@
+define( [
+        'dojo/_base/declare',
+        'dojo/_base/array',
+        'dojo/promise/all',
+        'dijit/Menu',
+        'dijit/MenuItem',
+        'dijit/CheckedMenuItem',
+        'dijit/MenuSeparator',
+        'dijit/PopupMenuItem',
+        'dijit/Dialog',
+        'JBrowse/Util',
+        'JBrowse/View/Track/Alignments2'
+    ],
+    function(
+        declare,
+        array,
+        all,
+        dijitMenu,
+        dijitMenuItem,
+        dijitCheckedMenuItem,
+        dijitMenuSeparator,
+        dijitPopupMenuItem,
+        dijitDialog,
+        Util,
+        Alignments2
+    ) {
+        return declare(Alignments2, {
+
+            /**
+             * An extension to JBrowse/View/Track/Alignments2 to allow for creating annotations
+             * from read alignments.
+             */
+
+            constructor: function() {
+                this.browser.getPlugin('WebApollo', dojo.hitch(this, function(plugin) {
+                    this.webapollo = plugin;
+                }));
+            },
+
+            _defaultConfig: function() {
+                var thisB = this;
+                var config = this.inherited(arguments);
+
+                config.menuTemplate.push({
+                    "label": "Create new annotation",
+                    "children": [
+                        {
+                            "label": "gene",
+                            "action":  function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createAnnotations({selection: {feature: this.feature}});
+                            }
+                        },
+                        {
+                            "label": "pseudogene",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "transcript", null, "pseudogene");
+                            }
+                        },
+                        {
+                            "label": "tRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "tRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "snRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "snRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "snoRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "snoRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "ncRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "ncRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "rRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "rRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "miRNA",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericAnnotations([this.feature], "miRNA", null, "gene");
+                            }
+                        },
+                        {
+                            "label": "repeat_region",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericOneLevelAnnotations([this.feature], "repeat_region", true);
+                            }
+                        },
+                        {
+                            "label": "transposable_element",
+                            "action": function() {
+                                var atrack = thisB.webapollo.getAnnotTrack();
+                                atrack.createGenericOneLevelAnnotations([this.feature], "transposable_element", true);
+                            }
+                        }
+                    ]
+                });
+                return config;
+            }
+        });
+    }
+);
\ No newline at end of file
diff --git a/client/apollo/js/View/Track/WebApolloCanvasFeatures.js b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js
new file mode 100644
index 0000000..ffab9e3
--- /dev/null
+++ b/client/apollo/js/View/Track/WebApolloCanvasFeatures.js
@@ -0,0 +1,132 @@
+define( [
+            'dojo/_base/declare',
+            'dojo/_base/array',
+            'JBrowse/View/Track/CanvasFeatures',
+            'dijit/Menu',
+            'dijit/MenuItem',
+            'dijit/CheckedMenuItem',
+            'dijit/MenuSeparator',
+            'dijit/PopupMenuItem',
+            'dijit/Dialog',
+            'JBrowse/Util',
+            'JBrowse/Model/SimpleFeature',
+            'WebApollo/SequenceOntologyUtils'
+        ],
+        function( declare,
+            array,
+            CanvasFeaturesTrack,
+            dijitMenu,
+            dijitMenuItem,
+            dijitCheckedMenuItem,
+            dijitMenuSeparator,
+            dijitPopupMenuItem,
+            dijitDialog,
+            Util,
+            SimpleFeature,
+            SeqOnto )
+{
+
+return declare( CanvasFeaturesTrack,
+
+{
+    constructor: function() {
+        this.browser.getPlugin( 'WebApollo', dojo.hitch( this, function(p) {
+            this.webapollo = p;
+        }));
+    },
+    _defaultConfig: function() {
+        console.log("WA config",document.body);
+        var config = Util.deepUpdate(dojo.clone(this.inherited(arguments)),
+            {
+                style: {
+                    textColor: function() { return dojo.hasClass(document.body,'Dark') ?'white': 'black'; },
+                    text2Color: function() { return dojo.hasClass(document.body,'Dark')? 'LightSteelBlue': 'blue'; },
+                    connectorColor: function() { return dojo.hasClass(document.body,'Dark')? 'lightgrey': 'black'; },
+                    color: function() { return dojo.hasClass(document.body,'Dark')? 'orange': 'goldenrod'; }
+                }
+            });
+        var thisB=this;
+        config.menuTemplate.push(            {
+              "label" : "Create new annotation",
+              "children" : [
+                {
+                  "label" : "gene",
+                  "action":  function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createAnnotations({x1:{feature:this.feature}});
+                  }
+                },
+                {
+                  "label" : "pseudogene",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "transcript", null, "pseudogene");
+                  }
+                },
+                {
+                  "label" : "tRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "tRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "snRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "snRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "snoRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "snoRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "ncRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "ncRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "rRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "rRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "miRNA",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericAnnotations([this.feature], "miRNA", null, "gene");
+                   }
+                },
+                {
+                  "label" : "repeat_region",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericOneLevelAnnotations([this.feature], "repeat_region", true);
+                   }
+                },
+                {
+                  "label" : "transposable element",
+                  "action" : function() {
+                     var atrack=thisB.webapollo.getAnnotTrack();
+                     atrack.createGenericOneLevelAnnotations([this.feature], "transposable_element", true);
+                   }
+                }
+              ]
+            }
+        );
+        return config;
+    }
+
+
+});
+
+});
+
diff --git a/client/apollo/js/View/TrackList/Faceted.js b/client/apollo/js/View/TrackList/Faceted.js
new file mode 100644
index 0000000..35968fd
--- /dev/null
+++ b/client/apollo/js/View/TrackList/Faceted.js
@@ -0,0 +1,22 @@
+define([
+    "dojo/_base/declare",
+    'JBrowse/View/TrackList/Faceted'
+],
+function(declare,Faceted) {
+    return declare('WebApollo.View.TrackList.Faceted',Faceted,
+    {
+        // Subclass method for track selector to remove webapollo specific tracks
+        renderGrid:function() {
+            //Remove the tracks from the metadata descriptions
+            for(var index in this.trackDataStore.identIndex) {
+                if(this.trackDataStore.identIndex[index]["track type"]=="WebApollo/View/Track/AnnotTrack" || 
+                   this.trackDataStore.identIndex[index]["track type"]=="WebApollo/View/Track/AnnotSequenceTrack") {
+                    delete this.trackDataStore.identIndex[index];
+                    this.trackDataStore.facetIndexes.itemCount--;
+                }
+            }
+            return this.inherited(arguments);
+        }
+    });
+});
+
diff --git a/client/apollo/js/View/TrackList/Hierarchical.js b/client/apollo/js/View/TrackList/Hierarchical.js
new file mode 100644
index 0000000..6050943
--- /dev/null
+++ b/client/apollo/js/View/TrackList/Hierarchical.js
@@ -0,0 +1,21 @@
+define([
+    "dojo/_base/declare",
+    'JBrowse/View/TrackList/Hierarchical'
+],
+function(declare,Hierarchical) {
+    return declare('WebApollo.View.TrackList.Hierarchical',Hierarchical,
+    {
+        // Subclass method for track selector to remove webapollo specific tracks
+        addTracks: function( tracks, inStartup ) {
+            for(var i=0;i<tracks.length;i++) {
+                if(tracks[i]["track type"]=="WebApollo/View/Track/AnnotTrack" || tracks[i]["track type"]=="WebApollo/View/Track/AnnotSequenceTrack") {
+                    tracks.splice(i,1);
+                    --i;
+                }
+            }
+            //call superclass
+            this.inherited(arguments);
+        }
+    });
+});
+
diff --git a/client/apollo/js/WebApollo.profile.js b/client/apollo/js/WebApollo.profile.js
new file mode 100644
index 0000000..0959f78
--- /dev/null
+++ b/client/apollo/js/WebApollo.profile.js
@@ -0,0 +1,64 @@
+function copyOnly(mid) {
+    return mid in {
+        // There are no modules right now that are copy-only. If you have some, though, just add
+        // them here like this:
+        // 'app/module': 1
+    };
+}
+
+var profile = {
+    action: 'release',
+    cssOptimize: 'comments',
+    mini: true,
+
+    basePath: '../../../src',
+    packages: [
+        {name: 'WebApollo', location: '../plugins/WebApollo/js' },
+        { name: 'jquery', location: '../plugins/WebApollo/jslib/jquery/', main: 'jquery' },
+        { name: 'jqueryui', location: '../plugins/WebApollo/jslib/jqueryui/', main: 'draggable' }
+    ],
+    layers: {
+        'WebApollo/main': {
+            include: [
+                'WebApollo'
+            ],  
+            exclude: [ 'JBrowse' ]
+        }   
+    },
+    
+    layerOptimize: 'closure',
+    stripConsole: 'normal',
+    selectorEngine: 'acme',
+
+
+    staticHasFeatures: {
+        'dojo-trace-api':0,
+        'dojo-log-api':0,
+        'dojo-publish-privates':0,
+        'dojo-sync-loader':0,
+        'dojo-xhr-factory':0,
+        'dojo-test-sniff':0
+    },
+
+    resourceTags: {
+        // Files that contain test code.
+        test: function (filename, mid) {
+            return false;
+        },
+
+        // Files that should be copied as-is without being modified by the build system.
+        copyOnly: function (filename, mid) {
+            return copyOnly(mid);
+        },
+
+        // Files that are AMD modules.
+        amd: function (filename, mid) {
+            return !copyOnly(mid) && /\.js$/.test(filename);
+        },
+
+        // Files that should not be copied when the “mini” compiler flag is set to true.
+        miniExclude: function (filename, mid) {
+            return ! ( /^WebApollo/.test(mid) );
+        }
+    }
+};
diff --git a/client/apollo/js/main.js b/client/apollo/js/main.js
new file mode 100644
index 0000000..598170e
--- /dev/null
+++ b/client/apollo/js/main.js
@@ -0,0 +1,754 @@
+require({
+           packages: [
+               { name: 'jqueryui', location: '../plugins/WebApollo/jslib/jqueryui' },
+               { name: 'jquery', location: '../plugins/WebApollo/jslib/jquery', main: 'jquery' }
+           ]
+       },
+       [],
+       function() {
+
+define.amd.jQuery = true;
+
+define([
+           'dojo/_base/declare',
+           'dojo/_base/lang',
+           'dojo/dom-construct',
+           'dojo/dom-class',
+           'dojo/query',
+           'dojo/_base/window',
+           'dojo/_base/array',
+           'dijit/registry',
+           'dijit/Menu',
+           'dijit/MenuItem',
+           'dijit/MenuSeparator',
+           'dijit/CheckedMenuItem',
+           'dijit/PopupMenuItem',
+           'dijit/form/DropDownButton',
+           'dijit/DropDownMenu',
+           'dijit/form/Button',
+           'JBrowse/Plugin',
+           'WebApollo/FeatureEdgeMatchManager',
+           'WebApollo/FeatureSelectionManager',
+           'WebApollo/TrackConfigTransformer',
+           'WebApollo/View/Track/AnnotTrack',
+           'WebApollo/View/TrackList/Hierarchical',
+           'WebApollo/View/TrackList/Faceted',
+           'WebApollo/InformationEditor',
+           'WebApollo/View/Dialog/Help',
+           'JBrowse/View/FileDialog/TrackList/GFF3Driver',
+           'JBrowse/CodonTable',
+           'dojo/io-query',
+           'jquery/jquery',
+           'lazyload/lazyload'
+       ],
+    function( declare,
+            lang,
+            domConstruct,
+            domClass,
+            query,
+            win,
+            array,
+            dijitRegistry,
+            dijitMenu,
+            dijitMenuItem,
+            dijitMenuSeparator,
+            dijitCheckedMenuItem,
+            dijitPopupMenuItem,
+            dijitDropDownButton,
+            dijitDropDownMenu,
+            dijitButton,
+            JBPlugin,
+            FeatureEdgeMatchManager,
+            FeatureSelectionManager,
+            TrackConfigTransformer,
+            AnnotTrack,
+            Hierarchical,
+            Faceted,
+            InformationEditor,
+            HelpMixin,
+            GFF3Driver,
+            CodonTable,
+            ioQuery,
+            $,
+            LazyLoad ) {
+
+return declare( [JBPlugin, HelpMixin],
+{
+
+    constructor: function( args ) {
+        console.log("loaded WebApollo plugin");
+        var thisB = this;
+        this.searchMenuInitialized = false;
+        var browser = this.browser;  // this.browser set in Plugin superclass constructor
+        [
+          'plugins/WebApollo/jslib/bbop/bbop.js',
+          'plugins/WebApollo/jslib/bbop/golr.js',
+          'plugins/WebApollo/jslib/bbop/jquery.js',
+          'plugins/WebApollo/jslib/bbop/search_box.js',
+          'plugins/WebApollo/jslib/websocket/spring-websocket.js'
+        ].forEach(function(src) {
+          var script = document.createElement('script');
+          script.src = src;
+          script.async = false;
+          document.head.appendChild(script);
+        });
+
+        // Checking for cookie for determining the color scheme of WebApollo
+        if (browser.cookie("Scheme")=="Dark") {
+            domClass.add(win.body(), "Dark");
+        }
+
+        browser.cookie("colorCdsByFrame",browser.cookie("colorCdsByFrame")==null?!browser.config.overrideColorCdsByFrameTrue:browser.cookie("colorCdsByFrame"));
+
+        if (browser.cookie("colorCdsByFrame")=="true") {
+            domClass.add(win.body(), "colorCds");
+        }
+
+        if (browser.cookie("Scheme-Flat")=="Flat") {
+            domClass.add(win.body(), "Flat");
+        }
+
+        if(!browser.config.overrideApolloStyles) {
+            domClass.add(win.body(), "Apollo");
+        }
+        if (browser.config.favicon) {
+            // this.setFavicon("plugins/WebApollo/img/webapollo_favicon.ico");
+            this.setFavicon(browser.config.favicon);
+        }
+        var queryParams = ioQuery.queryToObject( window.location.search.slice(1) );
+
+        if(queryParams.organism) {
+            this.organism=queryParams.organism;
+        }
+
+        args.cssLoaded.then( function() {
+            if (! browser.config.view) { browser.config.view = {}; }
+            browser.config.view.maxPxPerBp = thisB.getSequenceCharacterSize().width;
+        } );
+
+        if (! browser.config.helpUrl)  {
+            browser.config.helpUrl = "http://genomearchitect.org/webapollo/docs/help.html";
+        }
+
+        // hand the browser object to the feature edge match manager
+        FeatureEdgeMatchManager.setBrowser( browser );
+
+        this.featSelectionManager = new FeatureSelectionManager();
+        this.annotSelectionManager = new FeatureSelectionManager();
+        this.trackTransformer = new TrackConfigTransformer({ browser: browser });
+
+        // setting up selection exclusiveOr --
+        //    if selection is made in annot track, any selection in other tracks is deselected, and vice versa,
+        //    regardless of multi-select mode etc.
+        this.annotSelectionManager.addMutualExclusion(this.featSelectionManager);
+        this.featSelectionManager.addMutualExclusion(this.annotSelectionManager);
+
+        FeatureEdgeMatchManager.addSelectionManager(this.featSelectionManager);
+        FeatureEdgeMatchManager.addSelectionManager(this.annotSelectionManager);
+
+        if(!browser.config.quickHelp)
+        {
+            browser.config.quickHelp = {
+                "title": "Apollo Help",
+                "content": this.defaultHelp()
+            }
+        };
+
+        // register the WebApollo track types with the browser, so
+        // that the open-file dialog and other things will have them
+        // as options
+        browser.registerTrackType({
+            type:                 'WebApollo/View/Track/DraggableHTMLFeatures',
+            defaultForStoreTypes: [ 'JBrowse/Store/SeqFeature/NCList',
+                                    'JBrowse/Store/SeqFeature/GFF3',
+                                    'WebApollo/Store/SeqFeature/ApolloGFF3'
+                                  ],
+            label: 'WebApollo Features'
+        });
+        browser.registerTrackType({
+            type:                 'WebApollo/View/Track/DraggableAlignments',
+            defaultForStoreTypes: [
+                                    'JBrowse/Store/SeqFeature/BAM'
+                                  ],
+            label: 'WebApollo Alignments'
+        });
+        browser.registerTrackType({
+            type:                 'WebApollo/View/Track/SequenceTrack',
+            defaultForStoreTypes: [ 'JBrowse/Store/Sequence/StaticChunked' ],
+            label: 'WebApollo Sequence'
+        });
+
+        // transform track configs from vanilla JBrowse to WebApollo:
+        // type: "JBrowse/View/Track/HTMLFeatures" ==> "WebApollo/View/Track/DraggableHTMLFeatures"
+        //
+        array.forEach(browser.config.tracks,function(e) { thisB.trackTransformer.transform(e); });
+
+        // update track selector to WebApollo's if needed
+        // if no track selector set, use WebApollo's Hierarchical selector
+        if (!browser.config.trackSelector) {
+            browser.config.trackSelector = { type: 'WebApollo/View/TrackList/Hierarchical' };
+        }
+        // if using JBrowse's Hierarchical selector, switch to WebApollo's
+        else if (browser.config.trackSelector.type == "Hierarchical") {
+            browser.config.trackSelector.type = 'WebApollo/View/TrackList/Hierarchical';
+        }
+        // if using JBrowse's Hierarchical selector, switch to WebApollo's
+        else if (browser.config.trackSelector.type == "Faceted") {
+            browser.config.trackSelector.type = 'WebApollo/View/TrackList/Faceted';
+        }
+
+
+        if(browser.config.show_nav&&browser.config.show_menu) {
+            this.createMenus();
+        }
+
+
+        // put the WebApollo logo in the powered_by place in the main JBrowse bar
+        browser.afterMilestone( 'initView', function() {
+            if (browser.poweredByLink)  {
+                dojo.disconnect(browser.poweredBy_clickHandle);
+                browser.poweredByLink.innerHTML = '<img src=\"plugins/WebApollo/img/ApolloLogo_100x36.png\" height=\"25\" />';
+                browser.poweredByLink.href = 'http://genomearchitect.org/';
+                browser.poweredByLink.target = "_blank";
+            }
+            if(browser.config.show_nav&&browser.config.show_menu) {
+                // the fileDialog element is only initialized if the navigation bar exists
+                var customGff3Driver = declare(GFF3Driver,   {
+                    constructor: function( args ) {
+                        this.storeType = 'WebApollo/Store/SeqFeature/ApolloGFF3';
+                    }
+                });
+                browser.fileDialog.addFileTypeDriver(new customGff3Driver());
+                thisB.postCreateMenus();
+            }
+
+            // Initialize information editor with similar style to track selector
+                        var view = browser.view;
+                        view.oldOnResize = view.onResize;
+
+                             /* trying to fix residues rendering bug when web browser scaling/zoom (Cmd+, Cmd-) is used
+                               *    bug appears in Chrome, not Firefox, unsure of other browsers
+                               */
+                                view.onResize = function() {
+                                var fullZoom = (view.pxPerBp >= view.maxPxPerBp);
+                                var centerBp = Math.round((view.minVisible() + view.maxVisible())/2);
+                                var oldCharSize = thisB.getSequenceCharacterSize();
+                                var newCharSize = thisB.getSequenceCharacterSize(true);
+                                // detect if something happened to change pixel size of residues font (likely a web browser zoom)
+                                    var charWidthChanged = (newCharSize.width != oldCharSize.width);
+                                var charWidth = newCharSize.width;
+                                if (charWidthChanged) {
+                                        if (! browser.config.view) { browser.config.view = {}; }
+                                        browser.config.view.maxPxPerBp = charWidth;
+                                        view.maxPxPerBp = charWidth;
+                                    }
+                                if (charWidthChanged && fullZoom) {
+                                        view.pxPerBp = view.maxPxPerBp;
+                                        view.oldOnResize();
+                                        thisB.browserZoomFix(centerBp);
+                                    }
+                                else  {
+                                        view.oldOnResize();
+                                    }
+                            };
+
+
+        });
+        this.monkeyPatchRegexPlugin();
+
+
+    },
+    updateLabels: function() {
+        if(!this._showLabels) {
+            query('.track-label').style('visibility','hidden');
+        }
+        else {
+            query('.track-label').style('visibility','visible');
+        }
+        this.browser.view.updateScroll();
+    },
+
+    /**
+     *  Hack to try and fix residues rendering bug when web browser scaling/zoom (Cmd+, Cmd-) is used
+     *    bug appears in Chrome, not Firefox, unsure of other browsers
+     *    based on GenomeView.zoomToBaseLevel(), GenomeView.updateZoom(), then stripping away unneeded
+    */
+    browserZoomFix: function(pos) {
+        var view = this.browser.view;
+        if (view.animation) return;
+        var baseZoomIndex = view.zoomLevels.length - 1;
+        var zoomLoc = 0.5;
+        view.showWait();
+        view.trimVertical();
+        var relativeScale = view.zoomLevels[baseZoomIndex] / view.pxPerBp;
+        var fixedBp = pos;
+        view.curZoom = baseZoomIndex;
+        view.pxPerBp = view.zoomLevels[baseZoomIndex];
+        view.maxLeft = (view.pxPerBp * view.ref.end) - view.getWidth();
+
+        // needed, otherwise Density track can render wrong
+        //    possibly would have problems with other Canvas-based tracks too, though haven't seen in XYPlot yet
+        for (var track = 0; track < view.tracks.length; track++)
+            view.tracks[track].startZoom(view.pxPerBp,
+                                         fixedBp - ((zoomLoc * view.getWidth())
+                                                    / view.pxPerBp),
+                                         fixedBp + (((1 - zoomLoc) * view.getWidth())
+                                                    / view.pxPerBp));
+
+        var eWidth = view.elem.clientWidth;
+        var centerPx = view.bpToPx(fixedBp) - (zoomLoc * eWidth) + (eWidth / 2);
+        // stripeWidth: pixels per block
+        view.stripeWidth = view.stripeWidthForZoom(view.curZoom);
+        view.scrollContainer.style.width =
+            (view.stripeCount * view.stripeWidth) + "px";
+        view.zoomContainer.style.width =
+            (view.stripeCount * view.stripeWidth) + "px";
+        var centerStripe = Math.round(centerPx / view.stripeWidth);
+        var firstStripe = (centerStripe - ((view.stripeCount) / 2)) | 0;
+        view.offset = firstStripe * view.stripeWidth;
+        view.maxOffset = view.bpToPx(view.ref.end+1) - view.stripeCount * view.stripeWidth;
+        view.maxLeft = view.bpToPx(view.ref.end+1) - view.getWidth();
+        view.minLeft = view.bpToPx(view.ref.start);
+        view.zoomContainer.style.left = "0px";
+        view.setX((centerPx - view.offset) - (eWidth / 2));
+        dojo.forEach(view.uiTracks, function(track) { track.clear(); });
+
+        // needed, otherwise Density track can render wrong
+        //    possibly would have problems with other Canvas-based tracks too, though haven't seen in XYPlot yet
+        view.trackIterate( function(track) {
+            track.endZoom( view.pxPerBp,Math.round(view.stripeWidth / view.pxPerBp));
+        });
+
+        view.showVisibleBlocks(true);
+        view.showDone();
+        view.showCoarse();
+    },
+
+    plusStrandFilter: function(feature)  {
+        var strand = feature.get('strand');
+        return !(strand == 1 || strand == '+' || !strand)
+    },
+
+    minusStrandFilter: function(feature)  {
+        var strand = feature.get('strand');
+        return !(strand == -1 || strand == '-' || !strand)
+    },
+
+    addStrandFilterOptions: function()  {
+        var thisB = this;
+        var browser = this.browser;
+        var plus_strand_toggle = new dijitCheckedMenuItem(
+                {
+                    label: "Show plus strand",
+                    checked: true,
+                    onClick: function(event) {
+                        var plus = this.get("checked");
+                        if(!plus) {
+                            thisB.plusFilter=browser.addFeatureFilter(thisB.plusStrandFilter);
+                        } else  {
+                            browser.removeFeatureFilter(thisB.plusFilter);
+                        }
+                        browser.view.redrawTracks();
+                    }
+                });
+        browser.addGlobalMenuItem( 'view', plus_strand_toggle );
+        var minus_strand_toggle = new dijitCheckedMenuItem(
+                {
+                    label: "Show minus strand",
+                    checked: true,
+                    onClick: function(event) {
+                        var minus = this.get('checked');
+                        if (!minus)  {
+                            thisB.minusFilter=browser.addFeatureFilter(thisB.minusStrandFilter);
+                        }
+                        else  {
+                            browser.removeFeatureFilter(thisB.minusFilter);
+                        }
+                        browser.view.redrawTracks();
+                    }
+                });
+        browser.addGlobalMenuItem( 'view', minus_strand_toggle );
+    },
+
+    initSearchMenu: function()  {
+        if (! this.searchMenuInitialized) {
+            var webapollo = this;
+            this.browser.addGlobalMenuItem( 'tools',
+                                            new dijitMenuItem(
+                                                {
+                                                    id: 'menubar_apollo_seqsearch',
+                                                    label: "Search sequence",
+                                                    onClick: function() {
+                                                        webapollo.getAnnotTrack().searchSequence();
+                                                    }
+                                                }) );
+            if(!dijitRegistry.byId("dropdownmenu_tools")){
+                this.browser.renderGlobalMenu( 'tools', {text: 'Tools'}, this.browser.menuBar );
+            }
+
+        }
+
+        // move Tool menu in front of Help menu
+        var toolsMenu = dijit.byId('dropdownbutton_tools');
+        var helpMenu = dijit.byId('dropdownbutton_help');
+        domConstruct.place(toolsMenu.domNode,helpMenu.domNode,'before');
+        this.searchMenuInitialized = true;
+    },
+
+    showAnnotatorPanel: function(){
+        // http://asdfasfasdf/asfsdf/asdfasdf/apollo/<organism ID / client token>/jbrowse/index.html?loc=Group9.10%3A501752..501878&highlight=&tracklist=1&tracks=DNA%2CAnnotations&nav=1&overview=1
+        // to
+        // /apollo/annotator/loadLink?loc=Group9.10:501765..501858&organism=16&tracks=&clientToken=1315746673267340807380563276
+        var hrefString = window.location.href;
+        var hrefTokens = hrefString.split("\/");
+        var organism ;
+        for(var h in hrefTokens){
+            // alert(hrefTokens[h]);
+            if(hrefTokens[h]=="jbrowse"){
+                organism = hrefTokens[h-1] ;
+            }
+        }
+
+        // NOTE: Here is where you customize your view into Apollo, by adding / changing parameters
+
+        var jbrowseString = "/jbrowse/index.html?";
+        var jbrowseIndex = hrefString.indexOf(jbrowseString);
+        var params = hrefString.substring(jbrowseIndex + jbrowseString.length);
+        params = params.replace("tracklist=1","tracklist=0");
+        var finalString =  "../../annotator/loadLink?"+params + "&organism=" + organism ;
+        // if(params.indexOf("&clientToken=")<0){
+        //     finalString += "&clientToken="+this.getClientToken();
+        // }
+        window.location.href = finalString;
+    },
+
+    isEmbedded: function(){
+        return (typeof window.parent.getEmbeddedVersion == 'function' && window.parent.getEmbeddedVersion() == 'ApolloGwt-2.0') ;
+    },
+
+    initLoginMenu: function(username) {
+        var webapollo = this;
+        var loginButton;
+        if (username)  {   // permission only set if permission request succeeded
+            // if we are logged in with JBrowse mode, then disallow and go straight to the annotator
+            if(!webapollo.isEmbedded()){
+                webapollo.showAnnotatorPanel();
+                return ;
+            }
+            this.browser.addGlobalMenuItem( 'user',
+                            new dijitMenuItem(
+                                            {
+                                                    label: 'Logout',
+                                                    onClick: function()  {
+                                                            webapollo.getAnnotTrack().logout();
+                                                    }
+                                            })
+            );
+            var userMenu = this.browser.makeGlobalMenu('user');
+            loginButton = new dijitDropDownButton(
+                            { className: 'user',
+                                    innerHTML: '<span class="usericon"></span>' + username,
+                                    title: 'user logged in: UserName',
+                                    dropDown: userMenu
+                            });
+        }
+        else  {
+            loginButton = new dijitButton(
+                            { className: 'login',
+                                    innerHTML: "Login",
+                                    onClick: function()  {
+                                           webapollo.getAnnotTrack().showAnnotatorPanel();
+                                    }
+                            });
+        }
+
+        // get all toplinks and hide the one that says 'Full-screen view'
+        $('.topLink').each(function(index){
+            var innerHtml = $( this ).text();
+            if(innerHtml.indexOf("Full-screen view")>=0){
+                $( this ).hide();
+            }
+            // console.log( index + ": " + $( this ).text() );
+        });
+
+        this.browser.menuBar.appendChild( loginButton.domNode );
+        this.loginMenuInitialized = true;
+    },
+
+    /**
+     *  get the GenomeView's user annotation track
+     *  WebApollo assumes there is only one AnnotTrack
+     *     if there are multiple AnnotTracks, getAnnotTrack returns first one found
+     *         iterating through tracks list
+     */
+    getAnnotTrack: function()  {
+        if (this.browser && this.browser.view && this.browser.view.tracks)  {
+            var tracks = this.browser.view.tracks;
+            for (var i = 0; i < tracks.length; i++)  {
+                // should be doing instanceof here, but class setup is not being cooperative
+                if (tracks[i].isWebApolloAnnotTrack)  {
+                    return tracks[i];
+                }
+            }
+        }
+        return null;
+    },
+
+    /**
+     *  get the GenomeView's sequence track
+     *  WebApollo assumes there is only one SequenceTrack
+     *     if there are multiple SequenceTracks, getSequenceTrack returns first one found
+     *         iterating through tracks list
+     */
+    getSequenceTrack: function()  {
+        if (this.browser && this.browser.view && this.browser.view.tracks)  {
+            var tracks = this.browser.view.tracks;
+            for (var i = 0; i < tracks.length; i++)  {
+                // should be doing instanceof here, but class setup is not being cooperative
+                if (tracks[i].isWebApolloSequenceTrack)  {
+                    return tracks[i];
+                }
+            }
+        }
+        return null;
+    },
+
+
+    /** ported from berkeleybop/jbrowse GenomeView.js
+      * returns char height/width on GenomeView
+      */
+    getSequenceCharacterSize: function(recalc)  {
+        var container = this.browser.container;
+        if (this.browser.view && this.browser.view.elem)  {
+            container = this.browser.view.elem;
+        }
+        if (recalc || (! this._charSize))  {
+            //            this._charSize = this.calculateSequenceCharacterSize(this.browser.view.elem);
+            this._charSize = this.calculateSequenceCharacterSize(container);
+        }
+        return this._charSize;
+    },
+
+    /**
+     * ported from berkeleybop/jbrowse GenomeView.js
+     * Conducts a test with DOM elements to measure sequence text width
+     * and height.
+     */
+    calculateSequenceCharacterSize: function( containerElement ) {
+        var widthTest = document.createElement("div");
+        widthTest.className = "wa-sequence";
+        widthTest.style.visibility = "hidden";
+        var widthText = "12345678901234567890123456789012345678901234567890";
+        widthTest.appendChild(document.createTextNode(widthText));
+        containerElement.appendChild(widthTest);
+
+        var result = {
+            width:  widthTest.clientWidth / widthText.length,
+            height: widthTest.clientHeight
+        };
+
+        containerElement.removeChild(widthTest);
+        return result;
+    },
+
+    /** utility function, given an array with objects that have label props,
+     *        return array with all objects that don't have label
+     *   D = [ { label: A }, { label: B}, { label: C } ]
+     *   E = D.removeItemWithLabel("B");
+     *   E ==> [ { label: A }, { label: C } ]
+     */
+    removeItemWithLabel: function(inarray, label) {
+        var outarray = [];
+        for (var i=0; i<inarray.length; i++) {
+            var obj = inarray[i];
+            if (! (obj.label && (obj.label === label))) {
+                outarray.push(obj);
+            }
+        }
+        return outarray;
+    },
+
+    setFavicon: function(favurl) {
+        var $head = $('head');
+        // remove any existing favicons
+        var $existing_favs = $("head > link[rel='icon'], head > link[rel='shortcut icon']");
+        $existing_favs.remove();
+
+        // add new favicon (as both rel='icon' and rel='shortcut icon' for better browser compatibility)
+        var favicon1 = document.createElement('link');
+        favicon1.id = "favicon_icon";
+        favicon1.rel = 'icon';
+        favicon1.type="image/x-icon";
+        favicon1.href = favurl;
+
+        var favicon2 = document.createElement('link');
+        favicon2.id = "favicon_shortcut_icon";
+        favicon2.rel = 'shortcut icon';
+        favicon2.type="image/x-icon";
+        favicon2.href = favurl;
+
+        $head.prepend(favicon1);
+        $head.prepend(favicon2);
+    },
+    monkeyPatchRegexPlugin: function() {
+        //use var to avoid optimizer
+        var plugin='RegexSequenceSearch/Store/SeqFeature/RegexSearch';
+        require([plugin], function(RegexSearch) {
+            lang.extend(RegexSearch,{
+                translateSequence:function( sequence, frameOffset ) {
+                    var slicedSeq = sequence.slice( frameOffset );
+                    slicedSeq = slicedSeq.slice( 0, Math.floor( slicedSeq.length / 3 ) * 3);
+
+                    var translated = "";
+                    var codontable=new CodonTable();
+                    var codons=codontable.generateCodonTable(codontable.defaultCodonTable);
+                    for(var i = 0; i < slicedSeq.length; i += 3) {
+                        var nextCodon = slicedSeq.slice(i, i + 3);
+                        translated = translated + codons[nextCodon];
+                    }
+
+                    return translated;
+                }
+            });
+        });
+    },
+    // createMenus adds new menu items and is run before the initView milestone
+    createMenus: function() {
+        var browser=this.browser;
+        var thisB=this;
+
+
+                // add a global menu option for setting CDS color
+        var cds_frame_toggle = new dijitCheckedMenuItem(
+                {
+                    label: "Color by CDS frame",
+                    checked: browser.cookie("colorCdsByFrame")=="true",
+                    onClick: function(event) {
+                        if(this.get("checked")) domClass.add(win.body(), "colorCds");
+                        else domClass.remove(win.body(),"colorCds");
+                        browser.cookie("colorCdsByFrame", this.get("checked")?"true":"false");
+                    }
+                });
+        browser.addGlobalMenuItem( 'view', cds_frame_toggle );
+
+        var css_frame_menu = new dijitMenu();
+
+        css_frame_menu.addChild(
+            new dijitMenuItem({
+                    label: "Light",
+                    onClick: function (event) {
+                        browser.cookie("Scheme","Light");
+                        domClass.remove(win.body(), "Dark");
+                    }
+                }
+            )
+        );
+        css_frame_menu.addChild(
+            new dijitMenuItem({
+                    label: "Dark",
+                    onClick: function (event) {
+                        browser.cookie("Scheme","Dark");
+                        domClass.add(win.body(), "Dark");
+                    }
+                }
+            )
+        );
+
+        css_frame_menu.addChild(new dijitMenuSeparator());
+        css_frame_menu.addChild(
+            new dijitMenuItem({
+                    label: "Grid",
+                    onClick: function (event) {
+                        browser.cookie("Scheme-Flat","");
+                        domClass.remove(win.body(), "Flat");
+                    }
+                }
+            )
+        );
+        css_frame_menu.addChild(
+            new dijitMenuItem({
+                    label: "No Grid",
+                    onClick: function (event) {
+                        browser.cookie("Scheme-Flat","Flat");
+                        domClass.add(win.body(), "Flat");
+                    }
+                }
+            )
+        );
+
+
+        var css_frame_toggle = new dijitPopupMenuItem(
+            {
+                label: "Color Scheme"
+                ,popup: css_frame_menu
+            });
+
+        browser.addGlobalMenuItem('view', css_frame_toggle);
+
+        this.addStrandFilterOptions();
+
+
+        this._showLabels=(browser.cookie("showTrackLabel")||"true")=="true"
+        var hide_track_label_toggle = new dijitCheckedMenuItem(
+            {
+                label: "Show track label",
+                checked: this._showLabels,
+                onClick: function(event) {
+                    thisB._showLabels=this.get("checked");
+                    browser.cookie("showTrackLabel",this.get("checked")?"true":"false");
+                    thisB.updateLabels();
+                }
+            });
+        browser.addGlobalMenuItem( 'view', hide_track_label_toggle);
+        browser.addGlobalMenuItem( 'view', new dijitMenuSeparator());
+        browser.subscribe('/jbrowse/v1/n/tracks/visibleChanged', dojo.hitch(this,"updateLabels"));
+
+
+    },
+    // postCreateMenu is run after initView for convenience of ordering new items
+    postCreateMenus: function() {
+        var browser=this.browser;
+        var help=dijit.byId("menubar_generalhelp");
+
+        help.set("label", "Apollo Help");
+        help.set("iconClass", null);
+        var jbrowseUrl = "http://jbrowse.org";
+
+        browser.addGlobalMenuItem( 'help',
+                                new dijitMenuItem(
+                                    {
+                                        id: 'menubar_powered_by_jbrowse',
+                                        label: 'Powered by JBrowse',
+                                        // iconClass: 'jbrowseIconHelp',
+                                        onClick: function()  { window.open(jbrowseUrl,'help_window').focus(); }
+                                    })
+                              );
+        browser.addGlobalMenuItem( 'help',
+            new dijitMenuItem(
+                {
+                    id: 'menubar_web_service_api',
+                    label: 'Web Service API',
+                    // iconClass: 'jbrowseIconHelp',
+                    onClick: function()  { window.open("web_services/api",'help_window').focus(); }
+                })
+        );
+        browser.addGlobalMenuItem( 'help',
+            new dijitMenuItem(
+                {
+                    id: 'menubar_apollo_version',
+                    label: 'Get Version',
+                    // iconClass: 'jbrowseIconHelp',
+                    onClick: function()  {
+                        window.open("../version.jsp",'help_window').focus();
+                    }
+                })
+        );
+        this.updateLabels();
+    }
+
+
+});
+
+});
+
+});
diff --git a/client/apollo/jslib/bbop/bbop.js b/client/apollo/jslib/bbop/bbop.js
new file mode 100644
index 0000000..5e6b4fd
--- /dev/null
+++ b/client/apollo/jslib/bbop/bbop.js
@@ -0,0 +1,16724 @@
+/* 
+ * Package: core.js
+ * 
+ * Namespace: bbop.core
+ * 
+ * BBOP language extensions to JavaScript.
+ * 
+ * Purpose: Helpful basic utilities and operations to fix common needs in JS.
+ */
+
+// Module and namespace checking.
+
+if ( typeof bbop == "undefined" ){ var bbop = {}; }
+if ( typeof bbop.core == "undefined" ){ bbop.core = {}; }
+if ( typeof amigo == "undefined" ){ var amigo = {}; }
+
+/*
+ * Variable: global
+ * 
+ * Capture the global object for later reference.
+ * 
+ * Used by namespace and require.
+ * 
+ * TODO: There is a temporary workaround for NodeJS here
+ * TODO: Creates loop; problem?
+ * 
+ * Also see:
+ *  <namespace>
+ *  <requires>
+ */
+bbop.core.global = this;
+if( typeof GLOBAL !== 'undefined' ){ // TODO: Better probe of NodeJS-ness.
+    (function(){
+	 var global_context = {};
+	 global_context['bbop'] = GLOBAL['bbop'];
+	 global_context['amigo'] = GLOBAL['amigo'];
+	 bbop.core.global = global_context;
+	 //bbop.core.global = GLOBAL;
+     })();
+}
+
+///
+/// Utility functions can hang as prototypes.
+///
+
+/*
+ * Function: namespace
+ * 
+ * Create a namespace (chained object) in the global environment.
+ * 
+ * Parameters: An arbitrary number of strings.
+ * 
+ * Returns: Nothing. Side-effects: this function extends the global
+ * object for easy namespace creation.
+ * 
+ * Also See: <require>
+ */
+bbop.core.namespace = function(){
+
+    // Go through the arguments and add them to the namespace,
+    // starting at global.
+    var current_object = bbop.core.global;
+    for ( var i = 0; i < arguments.length; i++ ) {
+	var ns = arguments[i];
+	if( ! current_object[ns] ){
+	    current_object[ns] = {};
+	}
+	current_object = current_object[ns];
+    }
+    return current_object;
+};
+
+/*
+ * Function: require
+ * 
+ * Throw an error unless a specified namespace is defined.
+ * 
+ * Parameters: An arbitrary number of strings.
+ * 
+ * Returns: Nothing. Side-effects: throws an error if the namespace
+ * defined by the strings is not currently found.
+ * 
+ * Also See: <namespace>
+ */
+bbop.core.require = function(){
+
+    // Walk through from global namespace, checking.
+    var current_object = bbop.core.global;
+    for ( var i = 0; i < arguments.length; i++ ) {
+	var ns = arguments[i];
+	if( ! current_object[ns] ){
+	    throw new Error("Could not find required NS: " + ns);
+	}
+	current_object = current_object[ns];
+    }
+    return current_object;
+};
+
+/*
+ * Function: crop
+ *
+ * Crop a string nicely.
+ * 
+ * Parameters:
+ *  str - the string to crop
+ *  lim - the final length to crop to (optional, defaults to 10)
+ *  suff - the string to add to the end (optional, defaults to '')
+ * 
+ * Returns: Nothing. Side-effects: throws an error if the namespace
+ * defined by the strings is not currently found.
+ */
+bbop.core.crop = function(str, lim, suff){
+    var ret = str;
+
+    var limit = 10;
+    if( lim ){ limit = lim; }
+
+    var suffix = '';
+    if( suff ){ suffix = suff; }
+    
+    if( str.length > limit ){
+	ret = str.substring(0, (limit - suffix.length)) + suffix;
+    }
+    return ret;
+};
+
+/*
+ * Function: fold
+ *
+ * Fold a pair of hashes together, using the first one as an initial
+ * template--only the keys in the default hash will be defined in the
+ * final hash--and the second hash getting precedence.
+ * 
+ * The can be quite useful when defining functions--essentially
+ * allowing a limited default value system for arguments.
+ * 
+ * Parameters:
+ *  default_hash - Template hash.
+ *  arg_hash - Argument hash to match.
+ * 
+ * Returns: A new hash.
+ * 
+ * Also see: <merge>
+ */
+bbop.core.fold = function(default_hash, arg_hash){
+
+    if( ! default_hash ){ default_hash = {}; }
+    if( ! arg_hash ){ arg_hash = {}; }
+
+    var ret_hash = {};
+    for( var key in default_hash ){
+	if( bbop.core.is_defined(arg_hash[key]) ){
+	    ret_hash[key] = arg_hash[key];
+	}else{
+	    ret_hash[key] = default_hash[key];
+	}
+    }
+    return ret_hash;
+};
+
+/*
+ * Function: merge
+ *
+ * Merge a pair of hashes together, the second hash getting
+ * precedence. This is a superset of the keys both hashes.
+ * 
+ * Parameters:
+ *  older_hash - first pass
+ *  newer_hash - second pass
+ * 
+ * Returns: A new hash.
+ * 
+ * Also see: <fold>
+ */
+bbop.core.merge = function(older_hash, newer_hash){
+
+    if( ! older_hash ){ older_hash = {}; }
+    if( ! newer_hash ){ newer_hash = {}; }
+
+    var ret_hash = {};
+    function _add (key, val){
+	ret_hash[key] = val;
+    }
+    bbop.core.each(older_hash, _add);
+    bbop.core.each(newer_hash, _add);
+    return ret_hash;
+};
+
+/*
+ * Function: get_keys
+ *
+ * Get the hash keys from a hash/object, return as an array.
+ *
+ * Parameters:
+ *  arg_hash - the hash in question
+ *
+ * Returns: an array of keys
+ */
+bbop.core.get_keys = function (arg_hash){
+
+    if( ! arg_hash ){ arg_hash = {}; }
+    var out_keys = [];
+    for (var out_key in arg_hash) {
+	if (arg_hash.hasOwnProperty(out_key)) {
+	    out_keys.push(out_key);
+	}
+    }
+    
+    return out_keys;
+};
+
+/*
+ * Function: hashify
+ *
+ * Returns a hash form of the argument array/list. For example ['a',
+ * 'b'] would become {'a': true, 'b': true} or [['a', '12'], ['b',
+ * '21']] would become {'a': '12', 'b': '21'}. Using mixed sub-lists
+ * is undefined.
+ *
+ * Parameters:
+ *  list - the list to convert
+ *
+ * Returns: a hash
+ */
+bbop.core.hashify = function (list){
+    var rethash = {};
+
+    if( list && list[0] ){
+	if( bbop.core.is_array(list[0]) ){
+	    bbop.core.each(list,
+			   function(item){
+			       var key = item[0];
+			       var val = item[1];
+			       if( bbop.core.is_defined(key) ){
+				   rethash[key] = val;
+			       }
+			   });
+	}else{
+	    bbop.core.each(list,
+			   function(item){
+			       rethash[item] = true;
+			   });
+	}
+    }
+
+    return rethash;
+};
+
+/*
+ * Function: is_same
+ *
+ * Returns true if it things the two incoming arguments are value-wise
+ * the same.
+ * 
+ * Currently only usable for simple (atomic single layer) hashes,
+ * atomic lists, boolean, null, number, and string values. Will return
+ * false otherwise.
+ * 
+ * Parameters:
+ *  thing1 - thing one
+ *  thing2 - thing two
+ *
+ * Returns: boolean
+ */
+bbop.core.is_same = function (thing1, thing2){
+
+    var retval = false;
+
+    // If is hash...steal the code from test.js.
+    if( bbop.core.is_hash(thing1) && bbop.core.is_hash(thing2) ){
+	
+	var same_p = true;
+	
+	// See if the all of the keys in hash1 are defined in hash2
+	// and that they have the same ==.
+	for( var k1 in thing1 ){
+	    if( typeof thing2[k1] === 'undefined' ||
+		thing1[k1] !== thing2[k1] ){
+		    same_p = false;
+		    break;
+		}
+	}
+
+	// If there is still no problem...
+	if( same_p ){
+	    
+	    // Reverse of above.
+	    for( var k2 in thing2 ){
+		if( typeof thing1[k2] === 'undefined' ||
+		    thing2[k2] !== thing1[k2] ){
+			same_p = false;
+			break;
+		    }
+	    }
+	}
+
+	retval = same_p;
+
+    }else if( bbop.core.is_array(thing1) && bbop.core.is_array(thing2) ){
+	// If it's an array convert and pass it off to the hash function.
+	retval = bbop.core.is_same(bbop.core.hashify(thing1),
+				   bbop.core.hashify(thing2));
+    }else{
+	
+	// So, we're hopefully dealing with an atomic type. If they
+	// are the same, let's go ahead and try.
+	var t1_is = bbop.core.what_is(thing1);
+	var t2_is = bbop.core.what_is(thing2);
+	if( t1_is == t2_is ){
+	    if( t1_is == 'null' ||
+		t1_is == 'boolean' ||
+		t1_is == 'null' ||
+		t1_is == 'number' ||
+		t1_is == 'string' ){
+		    if( thing1 == thing2 ){
+			retval = true;
+		    }
+		}
+	}
+    }
+
+    return retval;
+};
+
+/*
+ * Function: what_is
+ *
+ * Return the string best guess for what the input is, null if it
+ * can't be identified. In addition to the _is_a property convention,
+ * current core output strings are: 'null', 'array', 'boolean',
+ * 'number', 'string', 'function', and 'object'.
+ * 
+ * Parameters: 
+ *  in_thing - the thing in question
+ *
+ * Returns: a string
+ */
+bbop.core.what_is = function(in_thing){
+    var retval = null;
+    if( typeof(in_thing) != 'undefined' ){
+
+	// If it's an object, try and guess the 'type', otherwise, let
+	// typeof.
+	if( in_thing == null ){
+	    retval = 'null';
+	}else if( typeof(in_thing) == 'object' ){
+	    
+	    // Look for the 'is_a' property that I should be using.
+	    if( typeof(in_thing._is_a) != 'undefined' ){
+		retval = in_thing._is_a;
+	    }else{
+		if( bbop.core.is_array(in_thing) ){
+		    retval = 'array';
+		}else{
+		    retval = 'object';
+		}		
+	    }
+	}else{
+	    retval = typeof(in_thing);
+	}
+    }
+    return retval;
+};
+
+/*
+ * Function: is_array
+ *
+ * Return the best guess (true/false) for whether or not a given
+ * object is being used as an array.
+ *
+ * Parameters: 
+ *  in_thing - the thing in question
+ *
+ * Returns: boolean
+ */
+bbop.core.is_array = function(in_thing){
+    var retval = false;
+    if( in_thing &&
+	typeof(in_thing) == 'object' &&
+	typeof(in_thing.push) == 'function' &&
+	typeof(in_thing.length) == 'number' ){
+	retval = true;
+    }
+    return retval;
+};
+
+/*
+ * Function: is_hash
+ *
+ * Return the best guess (true/false) for whether or not a given
+ * object is being used as a hash.
+ *
+ * Parameters: 
+ *  in_thing - the thing in question
+ *
+ * Returns: boolean
+ */
+bbop.core.is_hash = function(in_thing){
+    var retval = false;
+    if( in_thing &&
+	typeof(in_thing) == 'object' &&
+	(! bbop.core.is_array(in_thing)) ){
+	retval = true;
+    }
+    return retval;
+};
+
+/*
+ * Function: is_empty
+ *
+ * Return true/false on whether or not the object in question has any
+ * items of interest (iterable?).
+ *
+ * Parameters: 
+ *  in_thing - the thing in question
+ *
+ * Returns: boolean
+ */
+bbop.core.is_empty = function(in_thing){
+    var retval = false;
+    if( bbop.core.is_array(in_thing) ){
+	if( in_thing.length == 0 ){
+	    retval = true;
+	}
+    }else if( bbop.core.is_hash(in_thing) ){
+	var in_hash_keys = bbop.core.get_keys(in_thing);
+	if( in_hash_keys.length == 0 ){
+	    retval = true;
+	}
+    }else{
+	// TODO: don't know about this case yet...
+	//throw new Error('unsupported type in is_empty');	
+	retval = false;
+    }
+    return retval;
+};
+
+/*
+ * Function: is_defined
+ *
+ * Return true/false on whether or not the passed object is defined.
+ *
+ * Parameters: 
+ *  in_thing - the thing in question
+ *
+ * Returns: boolean
+ */
+bbop.core.is_defined = function(in_thing){
+    var retval = true;
+    if( typeof(in_thing) === 'undefined' ){
+	retval = false;
+    }
+    return retval;
+};
+
+/*
+ * Function: each
+ *
+ * Implement a simple iterator so I don't go mad.
+ *  array - function(item, index)
+ *  object - function(key, value)
+ *
+ *  TODO/BUG/WARNING?: This does not seem to work with the local
+ *  function variable "arguments".
+ * 
+ * Parameters: 
+ *  in_thing - hash or array
+ *  in_function - function to apply to elements
+ *
+ * Returns:
+ *  n/a
+ */
+bbop.core.each = function(in_thing, in_function){
+
+    // Probably an not array then.
+    if( typeof(in_thing) == 'undefined' ){
+	// this is a nothing, to nothing....
+    }else if( typeof(in_thing) != 'object' ){
+	throw new Error('Unsupported type in bbop.core.each: ' +
+			typeof(in_thing) );
+    }else if( bbop.core.is_hash(in_thing) ){
+	// Probably a hash...
+	var hkeys = bbop.core.get_keys(in_thing);
+	for( var ihk = 0; ihk < hkeys.length; ihk++ ){
+	    var ikey = hkeys[ihk];
+	    var ival = in_thing[ikey];
+	    in_function(ikey, ival);
+	}
+    }else{
+	// Otherwise likely an array.
+	for( var iai = 0; iai < in_thing.length; iai++ ){
+	    in_function(in_thing[iai], iai);
+	}
+    }
+};
+
+/*
+ * Function: pare
+ *
+ * Take an array or hash and pare it down using a couple of functions
+ * to what we want.
+ * 
+ * Both parameters are optional in the sense that you can set them to
+ * null and they will have no function; i.e. a null filter will let
+ * everything through and a null sort will let things go in whatever
+ * order.
+ *
+ * Parameters: 
+ *  in_thing - hash or array
+ *  filter_function - hash (function(key, val)) or array (function(item, i)).
+ *   This function must return boolean true or false.
+ *  sort_function - function to apply to elements: function(a, b)
+ *   This function must return an integer as the usual sort functions do.
+ *
+ * Returns: An array.
+ */
+bbop.core.pare = function(in_thing, filter_function, sort_function){
+
+    var ret = [];
+    
+    // Probably an not array then.
+    if( typeof(in_thing) == 'undefined' ){
+	// this is a nothing, to nothing....
+    }else if( typeof(in_thing) != 'object' ){
+	throw new Error('Unsupported type in bbop.core.pare: ' +
+			typeof(in_thing) );
+    }else if( bbop.core.is_hash(in_thing) ){
+	// Probably a hash; filter it if filter_function is defined.
+	if( filter_function ){	
+	    bbop.core.each(in_thing,
+			   function(key, val){
+			       if( filter_function(key, val) ){
+				   // Remove matches to the filter.
+			       }else{
+				   ret.push(val);
+			       }
+			   });
+	}else{
+	    bbop.core.each(in_thing, function(key, val){ ret.push(val); });
+	}
+    }else{
+	// Otherwise, probably an array; filter it if filter_function
+	// is defined.
+	if( filter_function ){	
+	    bbop.core.each(in_thing,
+			   function(item, index){
+			       if( filter_function(item, index) ){
+				   // filter out item if true
+			       }else{
+				   ret.push(item);
+			       }
+			   });
+	}else{
+	    bbop.core.each(in_thing, function(item, index){ ret.push(item); });
+	}
+    }
+
+    // For both: sort if there is anything.
+    if( ret.length > 0 && sort_function ){
+	ret.sort(sort_function);	    
+    }
+
+    return ret;
+};
+
+/*
+ * Function: clone
+ *
+ * Clone an object down to its atoms.
+ *
+ * Parameters: 
+ *  thing - whatever
+ *
+ * Returns: a new whatever
+ */
+bbop.core.clone = function(thing){
+
+    var clone = null;
+
+    if( typeof(thing) == 'undefined' ){
+	// Nothin' doin'.
+	//print("looks undefined");
+    }else if( typeof(thing) == 'function' ){
+	// Dunno about this case...
+	//print("looks like a function");
+	clone = thing;
+    }else if( typeof(thing) == 'boolean' ||
+	      typeof(thing) == 'number' ||
+	      typeof(thing) == 'string' ){
+	// Atomic types can be returned as-is (i.e. assignment in
+	// JS is the same as copy for atomic types).
+	//print("cloning atom: " + thing);
+	clone = thing;
+    }else if( typeof(thing) == 'object' ){
+	// Is it a hash or an array?
+	if( typeof(thing.length) == 'undefined' ){
+	    // Looks like a hash!
+	    //print("looks like a hash");
+	    clone = {};
+	    for(var h in thing){
+		clone[h] = bbop.core.clone(thing[h]);
+	    }
+	}else{
+	    // Looks like an array!
+	    //print("looks like an array");
+	    clone = [];
+	    for(var i = 0; i < thing.length; i++){
+		clone[i] = bbop.core.clone(thing[i]);
+	    }
+	}
+    }else{
+	// Then I don't know what it is--might be platform dep.
+	//print("no idea what it is");
+    }
+    return clone;
+};
+
+/*
+ * Function: to_string
+ *
+ * Essentially add standard 'to string' interface to the string class
+ * and as a stringifier interface to other classes. More meant for
+ * output. Only atoms, arrays, and objects with a to_string function
+ * are handled.
+ *
+ * Parameters: 
+ *  in_thing - something
+ *
+ * Returns: string
+ * 
+ * Also See: <dump>
+ */
+bbop.core.to_string = function(in_thing){
+
+    var what = bbop.core.what_is(in_thing);
+    if( what == 'number' ){
+	return in_thing.toString();
+    }else if( what == 'string' ){
+	return in_thing;
+    }else if( what == 'array' ){
+	return bbop.core.dump(in_thing);
+    }else if( in_thing.to_string && typeof(in_thing.to_string) == 'function' ){
+	return in_thing.to_string();
+    }else{
+	throw new Error('to_string interface not defined for this object');
+    }
+};
+
+/*
+ * Function: dump
+ *
+ * Dump an object to a string form as best as possible. More meant for
+ * debugging. For a slightly different take, see to_string.
+ *
+ * Parameters: 
+ *  in_thing - something
+ *
+ * Returns: string
+ * 
+ * Also See: <to_string>
+ */
+bbop.core.dump = function(thing){
+
+    var retval = '';
+
+    var what = bbop.core.what_is(thing);
+    if( what == null ){
+	retval = 'null';
+    }else if( what == 'null' ){
+	retval = 'null';
+    }else if( what == 'string' ){
+	retval = '"' + thing + '"';
+    }else if( what == 'boolean' ){
+	if( thing ){
+	    retval = "true";
+	}else{
+	    retval = "false";
+	}
+    }else if( what == 'array' ){
+
+	var astack = [];
+	bbop.core.each(thing, function(item, i){
+			   astack.push(bbop.core.dump(item));
+		       });
+	retval = '[' + astack.join(', ') + ']';
+
+    }else if( what == 'object' ){
+
+	var hstack = [];
+	bbop.core.each(thing, function(key, val){
+			   hstack.push('"'+ key + '": ' +
+				       bbop.core.dump(val));
+		       });
+	retval = '{' + hstack.join(', ') + '}';
+
+    }else{
+	retval = thing;
+    }
+
+    return retval;
+};
+
+/*
+ * Function: has_interface
+ *
+ * Check to see if all top-level objects in a namespace supply an
+ * "interface".
+ * 
+ * Mostly intended for use during unit testing.
+ *
+ * Parameters: 
+ *  iobj - the object/constructor in question
+ *  interface_list - the list of interfaces (as a strings) we're looking for
+ *
+ * Returns: boolean
+ *
+ * TODO: Unit test this to make sure it catches both prototype (okay I
+ * think) and uninstantiated objects (harder/impossible?).
+ */
+bbop.core.has_interface = function(iobj, interface_list){
+    var retval = true;
+    bbop.core.each(interface_list,
+		   function(iface){
+		       //print('|' + typeof(in_key) + ' || ' + typeof(in_val));
+		       //print('|' + in_key + ' || ' + in_val);
+		       if( typeof(iobj[iface]) == 'undefined' &&
+			   typeof(iobj.prototype[iface]) == 'undefined' ){
+			   retval = false;
+			   throw new Error(bbop.core.what_is(iobj) +
+					   ' breaks interface ' + 
+					   iface);
+                       }
+		   });
+    return retval;
+};
+
+/*
+ * Function: get_assemble
+ *
+ * Assemble an object into a GET-like query. You probably want to see
+ * the tests to get an idea of what this is doing.
+ * 
+ * The last argument of double hashes gets quoted (Solr-esque),
+ * otherwise not. It will try and avoid adding additional sets of
+ * quotes to strings.
+ *
+ * This does nothing to make the produced "URL" in any way safe.
+ * 
+ * WARNING: Not a hugely clean function--there are a lot of special
+ * cases and it could use a good (and safe) clean-up.
+ * 
+ * Parameters: 
+ *  qargs - hash/object
+ *
+ * Returns: string
+ */
+bbop.core.get_assemble = function(qargs){
+
+    var mbuff = [];
+    for( var qname in qargs ){
+	var qval = qargs[qname];
+
+	if( typeof qval == 'string' || typeof qval == 'number' ){
+	    // Is standard name/value pair.
+	    var nano_buffer = [];
+	    nano_buffer.push(qname);
+	    nano_buffer.push('=');
+	    nano_buffer.push(qval);
+	    mbuff.push(nano_buffer.join(''));
+	}else if( typeof qval == 'object' ){
+	    if( typeof qval.length != 'undefined' ){
+		// Is array (probably).
+		// Iterate through and double on.
+		for(var qval_i = 0; qval_i < qval.length ; qval_i++){
+		    var nano_buff = [];
+		    nano_buff.push(qname);
+		    nano_buff.push('=');
+		    nano_buff.push(qval[qval_i]);
+		    mbuff.push(nano_buff.join(''));
+		}
+	    }else{
+		// // TODO: The "and" case is pretty much like
+		// // the array, the "or" case needs to be
+		// // handled carefully. In both cases, care will
+		// // be needed to show which filters are marked.
+		// Is object (probably).
+		// Special "Solr-esque" handling.
+		for( var sub_name in qval ){
+		    var sub_vals = qval[sub_name];
+
+		    // Since there might be an array down there,
+		    // ensure that there is an iterate over it.
+		    if( bbop.core.what_is(sub_vals) != 'array' ){
+			sub_vals = [sub_vals];
+		    }
+
+		    var loop = bbop.core.each;
+		    loop(sub_vals,
+			 function(sub_val){
+			     var nano_buff = [];
+			     nano_buff.push(qname);
+			     nano_buff.push('=');
+			     nano_buff.push(sub_name);
+			     nano_buff.push(':');
+			     if( typeof sub_val !== 'undefined' && sub_val ){
+				 // Do not double quote strings.
+				 // Also, do not requote if we already
+				 // have parens in place--that
+				 // indicates a complicated
+				 // expression. See the unit tests.
+				 var val_is_a = bbop.core.what_is(sub_val);
+				 if( val_is_a == 'string' &&
+				     sub_val.charAt(0) == '"' &&
+				     sub_val.charAt(sub_val.length -1) == '"' ){
+				     nano_buff.push(sub_val);
+				 }else if( val_is_a == 'string' &&
+				     sub_val.charAt(0) == '(' &&
+				     sub_val.charAt(sub_val.length -1) == ')' ){
+				     nano_buff.push(sub_val);
+				 }else{
+				     nano_buff.push('"' + sub_val + '"');
+				 }
+			     }else{
+				 nano_buff.push('""');
+			     }
+			     mbuff.push(nano_buff.join(''));
+			 });
+		}
+	    }
+	}else if( typeof qval == 'undefined' ){
+	    // This happens in some cases where a key is tried, but no
+	    // value is found--likely equivalent to q="", but we'll
+	    // let it drop.
+	    // var nano_buff = [];
+	    // nano_buff.push(qname);
+	    // nano_buff.push('=');
+	    // mbuff.push(nano_buff.join(''));	    
+	}else{
+	    throw new Error("bbop.core.get_assemble: unknown type: " + 
+			    typeof(qval));
+	}
+    }
+    
+    return mbuff.join('&');
+};
+
+/*
+ * Function: 
+ *
+ * Random number generator of fixed length. Return a random number
+ * string of length len.
+ *
+ * Parameters: 
+ *  len - the number of random character to return.
+ *
+ * Returns: string
+ */
+bbop.core.randomness = function(len){
+
+    var random_base =
+	['1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
+	 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+	 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
+    var length = len || 10;
+    var cache = new Array();
+    for( var ii = 0; ii < length; ii++ ){
+	var rbase_index = Math.floor(Math.random() * random_base.length);
+	cache.push(random_base[rbase_index]);
+    }
+    return cache.join('');
+};
+
+/*
+ * Function: first_split
+ *
+ * Attempt to return a two part split on the first occurrence of a
+ * character.
+ *
+ * Returns '' for parts not found.
+ * 
+ * Unit tests make the edge cases clear.
+ * 
+ * Parameters:
+ *  character - the character to split on
+ *  string - the string to split
+ *
+ * Returns:
+ *  list of first and second parts
+ */
+bbop.core.first_split = function(character, string){
+
+    var retlist = null;
+
+    var eq_loc = string.indexOf(character);
+    if( eq_loc == 0 ){
+	retlist = ['', string.substr(eq_loc +1, string.length)];
+    }else if( eq_loc > 0 ){
+	var before = string.substr(0, eq_loc);
+	var after = string.substr(eq_loc +1, string.length);
+	retlist = [before, after];
+    }else{
+	retlist = ['', ''];
+    }
+
+    return retlist;
+};
+
+/*
+ * Function: url_parameters
+ *
+ * Return the parameters part of a URL.
+ *
+ * Unit tests make the edge cases clear.
+ * 
+ * Parameters:
+ *  url - url (or similar string)
+ *
+ * Returns:
+ *  list of part lists
+ */
+bbop.core.url_parameters = function(url){
+
+    var retlist = [];
+
+    // Pull parameters.
+    var tmp = url.split('?');
+    var path = '';
+    var parms = [];
+    if( ! tmp[1] ){ // catch bad url--nothing before '?'
+	parms = tmp[0].split('&');
+    }else{ // normal structure
+	path = tmp[0];
+	parms = tmp[1].split('&');
+    }
+
+    // Decompose parameters.
+    bbop.core.each(parms,
+		  function(p){
+		      var c = bbop.core.first_split('=', p);
+		      if( ! c[0] && ! c[1] ){
+			  retlist.push([p]);
+		      }else{
+			  retlist.push(c);		  
+		      }
+		  });
+    
+    return retlist;
+};
+
+/*
+ * Function: resourcify
+ *
+ * Convert a string into something consistent for urls (getting icons,
+ * etc.). Return a munged/hashed-down version of the resource.
+ * Assembles, converts spaces to underscores, and all lowercases.
+ * 
+ * Parameters:
+ *  base - base url for the resource(s)
+ *  resource - the filename or whatever to be transformed
+ *  extension - *[optional]* the extension of the resource
+ *
+ * Returns:
+ *  string
+ */
+bbop.core.resourcify = function(base, resource, extension){
+
+    var retval = base + '/' + resource;
+
+    // Add the extension if it is there.
+    if( extension ){
+	retval += '.' + extension;	
+    }
+
+    // Spaces to underscores and all lowercase.
+    return retval.replace(" ", "_", "g").toLowerCase();
+};
+
+/*
+ * Function: uuid
+ *
+ * RFC 4122 v4 compliant UUID generator.
+ * From: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns:
+ *  string
+ */
+bbop.core.uuid = function(){
+
+    // Replace x (and y) in string.
+    function replacer(c) {
+	var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+	return v.toString(16);
+    }
+    var target_str = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
+    return target_str.replace(/[xy]/g, replacer);
+};
+
+/*
+ * Function: numeric_sort_ascending
+ *
+ * A sort function to put numbers in ascending order.
+ * 
+ * Useful as the argument to .sort().
+ * 
+ * See: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
+ * 
+ * Parameters:
+ *  a - the first number
+ *  b - the second number
+ *
+ * Returns:
+ *  number of their relative worth
+ */
+bbop.core.numeric_sort_ascending = function(a, b){
+    return a - b;
+};
+
+/*
+ * Function: numeric_sort_descending
+ *
+ * A sort function to put numbers in descending order.
+ * 
+ * Useful as the argument to .sort().
+ * 
+ * See: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
+ * 
+ * Parameters:
+ *  a - the first number
+ *  b - the second number
+ *
+ * Returns:
+ *  number of their relative worth
+ */
+bbop.core.numeric_sort_descending = function(a, b){
+    return b - a;
+};
+
+/*
+ * Function: dequote
+ *
+ * Remove the quotes from a string.
+ * 
+ * Parameters:
+ *  str - the string to dequote
+ *
+ * Returns:
+ *  the dequoted string (or the original string)
+ */
+bbop.core.dequote = function(str){
+    var retstr = str;
+
+    if( bbop.core.is_defined(str) && str.length > 2 ){
+	var end = str.length -1;
+	if( str.charAt(0) == '"' && str.charAt(end) == '"' ){
+	    retstr = str.substr(1, end -1);
+	}
+    }
+
+    return retstr;
+};
+
+/*
+ * Function: ensure
+ *
+ * Make sure that a substring exists at the beginning or end (or both)
+ * of a string.
+ * 
+ * Parameters:
+ *  str - the string to ensure that has the property
+ *  add - the string to check for (and possibly add)
+ *  place - *[optional]* "front"|"back", place to ensure (defaults to both)
+ *
+ * Returns:
+ *  a new string with the property enforced
+ */
+bbop.core.ensure = function(str, add, place){
+
+    // 
+    var do_front = false;
+    var do_back = false;
+    if( ! bbop.core.is_defined(place) ){
+	do_front = true;
+	do_back = true;
+    }else if( place == 'front' ){
+	do_front = true;
+    }else if( place == 'back' ){
+	do_back = true;
+    }else{
+	// Don't know what it is, not doing anything.
+    }
+
+    //
+    var strlen = str.length;
+    var addlen = add.length;
+    var front_substr = str.substr(0, addlen);
+    var back_substr = str.substr((strlen - addlen), (strlen -1));
+
+    //
+    var front_add = '';
+    if( do_front && front_substr != add ){
+	front_add = add;
+    }
+    var back_add = '';
+    if( do_back && back_substr != add ){
+	back_add = add;
+    }
+
+    // print('do_front: ' + do_front);
+    // print('do_back: ' + do_back);
+    // print('str.length: ' + strlen);
+    // print('add.length: ' + addlen);
+    // print('front_substr: ' + front_substr);
+    // print('back_substr: ' + back_substr);
+    // print('front_add: ' + front_add);
+    // print('back_add: ' + back_add);
+
+    return front_add + str + back_add;
+};
+
+/*
+ * Function: chomp
+ *
+ * Trim the leading and trailing whitespace from a string.
+ * Named differently so as not to confuse with JS 1.8.1's trim().
+ * 
+ * Parameters:
+ *  str - the string to ensure that has the property
+ *
+ * Returns:
+ *  the trimmed string
+ */
+bbop.core.chomp = function(str){
+
+    var retstr = '';
+
+    retstr = str.replace(/^\s+/,'');
+    retstr = retstr.replace(/\s+$/,'');
+
+    return retstr;
+};
+
+/*
+ * Function: splode
+ *
+ * Break apart a string on certain delimiter.
+ * 
+ * Parameters:
+ *  str - the string to ensure that has the property
+ *  delimiter - *[optional]* either a string or a simple regexp; defaults to ws
+ *
+ * Returns:
+ *  a list of separated substrings
+ */
+bbop.core.splode = function(str, delimiter){
+
+    var retlist = null;
+
+    if( bbop.core.is_defined(str) ){
+	if( ! bbop.core.is_defined(delimiter) ){
+	    delimiter = /\s+/;
+	}
+	
+	retlist = str.split(delimiter);
+    }
+
+    return retlist;
+};
+
+// // Giving up on this for now: the general case seems too hard to work with 
+// // in so many different, contradictory, and changing environments.
+// /*
+//  * Function: evaluate
+//  * 
+//  * Getting a cross-platform that can evaluate to the global namespace
+//  * seems a little bit problematic. This is an attempt to wrap that all
+//  * away.
+//  * 
+//  * This is not an easy problem--just within browsers there are a lot
+//  * of issues:
+//  * http://perfectionkills.com/global-eval-what-are-the-options/ After
+//  * that, the server side stuff tries various ways to keep you from
+//  * affecting the global namespace in certain circumstances.
+//  * 
+//  * Parameters:
+//  *  to_eval - the string to evaluate
+//  * 
+//  * Returns:
+//  *  A list with the following fields: retval, retval_str, okay_p, env_type.
+//  */
+// bbop.core.evaluate = function(to_eval){
+
+//     var retval = null;
+//     var retval_str = '';
+//     var okay_p = true;
+//     var env_type = 'server';
+
+//     // Try and detect our environment.
+//     try{
+// 	if( bbop.core.is_defined(window) &&
+// 	    bbop.core.is_defined(window.eval) &&
+// 	    bbop.core.what_is(window.eval) == 'function' ){
+// 		env_type = 'browser';
+// 	    }
+//     } catch (x) {
+// 	// Probably not a browser then, right? Hopefully all the
+// 	// servers that we'll run into are the same (TODO: check
+// 	// nodejs).
+//     }
+//     print('et: ' + env_type);
+
+//     // Now try for the execution.
+//     try{
+// 	// Try and generically evaluate.
+// 	if( env_type == 'browser' ){
+// 	    print('eval as if (browser)');
+// 	    retval = window.eval(to_eval);
+// 	}else{
+// 	    // TODO: Does this work?
+// 	    print('eval as else (server)');
+// 	    //retval = this.eval(to_eval);		
+// 	    retval = bbop.core.global.eval(to_eval);
+// 	}
+//     }catch (x){
+// 	// Bad things happened.
+// 	print('fail on: (' + retval +'): ' + to_eval);
+// 	retval_str = '[n/a]';
+// 	okay_p = false;
+//     }
+	
+//     // Make whatever the tmp_ret is prettier for the return string.
+//     if( bbop.core.is_defined(retval) ){
+// 	if( bbop.core.what_is(retval) == 'string' ){
+// 	    retval_str = '"' + retval + '"';
+// 	}else{
+// 	    retval_str = retval;
+// 	}
+//     }else{
+// 	// Return as-is.
+//     }
+
+//     return [retval, retval_str, okay_p, env_type];
+// };
+
+/*
+ * Function: extend
+ * 
+ * What seems to be a typical idiom for subclassing in JavaScript.
+ * 
+ * This attempt has been scraped together from bits here and there and
+ * lucid explanations from Mozilla:
+ * 
+ * https://developer.mozilla.org/en-US/docs/JavaScript/Introduction_to_Object-Oriented_JavaScript
+ * https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Details_of_the_Object_Model
+ * https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Inheritance_Revisited
+ * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/new
+ * 
+ * Parameters:
+ *  subclass - the subclass object
+ *  superclass - the superclass object
+ * 
+ * Returns:
+ *  n/a
+ */
+bbop.core.extend = function(subclass, baseclass){
+
+    // Create a temporary nothing so that we don't fiddle the
+    // baseclass's(?) with what we do to subclass later on.
+    function tmp_object(){}
+
+    // This nothings prototype gets the base class's.
+    tmp_object.prototype = baseclass.prototype;
+
+    // We instantiate the tmp_object, whose prototype is the
+    // baseclass's; we make subclass's prototype this object, giving
+    // us something that is very much like baseclass.
+    subclass.prototype = new tmp_object; // same as: "new tmp_object();"
+
+    // Now we go back and make the constructor of subclass actually
+    // subclass again--we blew it away in the last step. Now we have a
+    // subclass constructor with the protoype of baseclass.
+    subclass.prototype.constructor = subclass;
+
+    // // Create a property to allow access to the constructor of
+    // // baseclass. This is useful when subclass needs access to
+    // // baseclass's constructor for property setting.
+    // subclass.base_constructor = baseclass;
+
+    // // Create a property to
+    // subclass.parent_class = baseclass.prototype;
+};
+
+// /*
+//  * Function: coder
+//  *
+//  * An object that functions to encode and decode data that we might be
+//  * hiding in element ids.
+//  * 
+//  * This constructor takes hash parameters.
+//  *
+//  * Parameters: 
+//  *  string - the base "namespace" string to use; has a default
+//  *  size - 
+//  *
+//  * Returns: 
+//  *  bbop.core.coder object
+//  */
+// bbop.core.coder = function(args){
+
+//     var mangle_base_string = "bbop_core_coder_mangle_";
+//     var mangle_base_space_size = 10;
+
+//     var defs = {string: mangle_base_string, size: mangle_base_space_size};
+//     var final_args = bbop.core.fold(defs, args);
+//     var mangle_str = final_args['string'];
+//     var space_size = final_args['size'];
+
+//     // TODO/BUG: apparently, html ids can only be of a limited
+//     // character set.
+//     //var en_re = new RegExp("/:/", "gi");
+//     //var de_re = new RegExp("/-_-/", "gi");
+//     this.encode = function(str){
+// 	// Mangle and encode.
+// 	var new_str = mangle_str + bbop.core.randomness(space_size) +'_'+ str;
+// 	// TODO:
+// 	// str.replace(en_re, "-_-");
+// 	return new_str;
+//     };
+//     this.decode = function(str){	    
+// 	// Decode and demangle.
+// 	var new_str = str.substring(mangle_str.length + space_size + 1);
+// 	// TODO:
+// 	// str.replace(de_re, ":");
+// 	return new_str;
+//     };
+// };
+/* 
+ * Package: version.js
+ * 
+ * Namespace: bbop.version
+ * 
+ * This package was automatically created during the release process
+ * and contains its version information--this is the release of the 
+ * API that you have.
+ */
+
+bbop.core.namespace('bbop', 'version');
+bbop.version = {};
+
+/*
+ * Variable: revision
+ *
+ * Partial version for this library; revision (major/minor version numbers)
+ * information.
+ */
+bbop.version.revision = "2.0b1";
+
+/*
+ * Variable: release
+ *
+ * Partial version for this library: release (date-like) information.
+ */
+bbop.version.release = "20130627";
+/* 
+ * Package: json.js
+ * 
+ * Namespace: bbop.json
+ * 
+ * JSON stringifying and parsing capabilities.  This package is a
+ * small modification of json2.js (in the Public Domain from
+ * https://raw.github.com/douglascrockford/JSON-js/master/json2.js and
+ * http://json.org) to fit in a little more with the style of BBOP
+ * JS. As well, the Date prototypes were removed. See json2.js in the
+ * source directory for this package for the original.
+ * 
+ * As much of the original documentation and structure was kept as
+ * possible while converting to Naturaldocs and the bbop namespace.
+ * 
+ * Purpose: Ensure that JSON parsing capabilites exist on all
+ * platforms that BBOP JS runs on.
+ */
+
+/*
+ * Function: stringify
+ * 
+ * This method produces a JSON text from a JavaScript value.
+ * 
+ * When an object value is found, if the object contains a toJSON
+ * method, its toJSON method will be called and the result will be
+ * stringified. A toJSON method does not serialize: it returns the
+ * value represented by the name/value pair that should be serialized,
+ * or undefined if nothing should be serialized. The toJSON method
+ * will be passed the key associated with the value, and this will be
+ * bound to the value.
+
+ * For example, this would serialize Dates as ISO strings.
+ * 
+ * : Date.prototype.toJSON = function (key) {
+ * :         function f(n) {
+ * :               // Format integers to have at least two digits.
+ * :                    return n < 10 ? '0' + n : n;
+ * :                }
+ * :
+ * :                return this.getUTCFullYear()   + '-' +
+ * :                  f(this.getUTCMonth() + 1) + '-' +
+ * :                     f(this.getUTCDate())      + 'T' +
+ * :                     f(this.getUTCHours())     + ':' +
+ * :                     f(this.getUTCMinutes())   + ':' +
+ * :                     f(this.getUTCSeconds())   + 'Z';
+ * :            };
+ * 
+ * You can provide an optional replacer method. It will be passed the
+ * key and value of each member, with this bound to the containing
+ * object. The value that is returned from your method will be
+ * serialized. If your method returns undefined, then the member will
+ * be excluded from the serialization.
+ * 
+ * If the replacer parameter is an array of strings, then it will be
+ * used to select the members to be serialized. It filters the results
+ * such that only members with keys listed in the replacer array are
+ * stringified.
+ * 
+ * Values that do not have JSON representations, such as undefined or
+ * functions, will not be serialized. Such values in objects will be
+ * dropped; in arrays they will be replaced with null. You can use
+ * a replacer function to replace those with JSON values.
+ * JSON.stringify(undefined) returns undefined.
+ * 
+ * The optional space parameter produces a stringification of the
+ * value that is filled with line breaks and indentation to make it
+ * easier to read.
+ * 
+ * If the space parameter is a non-empty string, then that string will
+ * be used for indentation. If the space parameter is a number, then
+ * the indentation will be that many spaces. For example:
+ * 
+ * : text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ * : // text is '["e",{"pluribus":"unum"}]'
+ * : 
+ * : text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ * : // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+ * :
+ * : text = JSON.stringify([new Date()], function (key, value) {
+ * :          return this[key] instanceof Date ?
+ * :                 'Date(' + this[key] + ')' : value;
+ * :  });
+ * :  // text is '["Date(---current time---)"]'
+ *
+ * Parameters:
+ *  value - any JavaScript value, usually an object or array.
+ *  replacer - an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings.
+ *  space - an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level.
+ * 
+ * Returns: string
+ */
+
+/*
+ * Function: parse
+ * (text, reviver)
+ * 
+ * This method parses a JSON text to produce an object or array.
+ * It can throw a SyntaxError exception.
+ * 
+ * The optional reviver parameter is a function that can filter and
+ * transform the results. It receives each of the keys and values,
+ * and its return value is used instead of the original value.
+ * If it returns what it received, then the structure is not modified.
+ * If it returns undefined then the member is deleted. For example:
+ * 
+ * : // Parse the text. Values that look like ISO date strings will
+ * : // be converted to Date objects.
+ * :
+ * : myData = JSON.parse(text, function (key, value) {
+ * :     var a;
+ * :     if (typeof value === 'string') {
+ * :         a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ * :         if (a) {
+ * :             return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ * :                 +a[5], +a[6]));
+ * :         }
+ * :     }
+ * :     return value;
+ * : });
+ * :
+ * : myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ * :     var d;
+ * :     if (typeof value === 'string' &&
+ * :             value.slice(0, 5) === 'Date(' &&
+ * :             value.slice(-1) === ')') {
+ * :         d = new Date(value.slice(5, -1));
+ * :                   if (d) {
+ * :             return d;
+ * :         }
+ * :     }
+ * :     return value;
+ * : });
+ * 
+ * Parameters:
+ *  text - the string to parse to a JavaScript entity.
+ *  reviver - *[optional]* optional transforming function for modifying results; see the documentation above for more details.
+ * 
+ * Returns: well, pretty much anything you put in...
+ */
+
+/*jslint evil: true, regexp: true */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'json', 'stringify');
+bbop.core.namespace('bbop', 'json', 'parse');
+
+(function () {
+    //'use strict';
+
+    // function f(n) {
+    //     // Format integers to have at least two digits.
+    //     return n < 10 ? '0' + n : n;
+    // }
+
+    // if (typeof Date.prototype.toJSON !== 'function') {
+
+    //     Date.prototype.toJSON = function (key) {
+
+    //         return isFinite(this.valueOf())
+    //             ? this.getUTCFullYear()     + '-' +
+    //                 f(this.getUTCMonth() + 1) + '-' +
+    //                 f(this.getUTCDate())      + 'T' +
+    //                 f(this.getUTCHours())     + ':' +
+    //                 f(this.getUTCMinutes())   + ':' +
+    //                 f(this.getUTCSeconds())   + 'Z'
+    //             : null;
+    //     };
+
+    //     String.prototype.toJSON      =
+    //         Number.prototype.toJSON  =
+    //         Boolean.prototype.toJSON = function (key) {
+    //             return this.valueOf();
+    //         };
+    // }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
+            var c = meta[a];
+            return typeof c === 'string'
+                ? c
+                : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+        }) + '"' : '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0
+                    ? '[]'
+                    : gap
+                    ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
+                    : '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    if (typeof rep[i] === 'string') {
+                        k = rep[i];
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.prototype.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0
+                ? '{}'
+                : gap
+                ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
+                : '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+//    if (typeof bbop.json.stringify !== 'function') {
+        bbop.json.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                    typeof replacer.length !== 'number')) {
+                throw new Error('bbop.json.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+//    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+//    if (typeof bbop.json.parse !== 'function') {
+        bbop.json.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.prototype.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            text = String(text);
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/
+                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
+                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function'
+                    ? walk({'': j}, '')
+                    : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('bbop.json.parse');
+        };
+//    }
+}());
+/*
+ * Package: logger.js
+ * 
+ * Namespace: bbop.logger
+ * 
+ * BBOP JS logger object. Using .kvetch(), you can automatically log a
+ * message in almost any environment you find yourself in--browser,
+ * server wherever. Also, if you have jQuery available and an element
+ * with the id "bbop-logger-console-textarea",
+ * "bbop-logger-console-text", or "bbop-logger-console-html", the
+ * logger will append to that element (with a "\n" (autoscroll), "\n",
+ * or "<br />" terminator respectively) instead.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'logger');
+
+/*
+ * Constructor: logger
+ * 
+ * Arguments: (optional) initial context.
+ */
+bbop.logger = function(initial_context){
+
+    /*
+     * Variable: DEBUG 
+     * 
+     * Different debugging available per object. Externally toggle
+     * between true and false to switch on and off the logging.
+     */
+    this.DEBUG = false;
+
+    var anchor = this;
+
+    // Define an optional context to tag onto the front of messages.
+    this._context = [];
+    if( initial_context ){
+	this._context = [initial_context];
+    }
+
+    /*
+     * Function: reset_context
+     * 
+     * Define the ability to reset the contex.
+     * 
+     * Arguments:
+     *  new_initial_context - (optional) New context to start with.
+     */
+    this.reset_context = function(new_initial_context){
+	if( new_initial_context ){
+	    this._context = [new_initial_context];
+	}else{
+	    this._context = [];	    
+	}
+    };
+
+    /*
+     * Function: push_context
+     * 
+     * Add an additional logging context to the stack.
+     * 
+     * Arguments:
+     *  new_context - New context to add to the context stack.
+     */
+    this.push_context = function(new_context){
+	this._context.push(new_context);
+    };
+
+    /*
+     * Function: pop_context
+     * 
+     * Remove the last context if it's there.
+     */
+    this.pop_context = function(){
+	var popped_context = null;
+	if( this._context.length > 0 ){
+	    popped_context = this._context.pop();
+	}
+	return popped_context;
+    };
+
+    // Generalizer console (or whatever) printing.
+    this._console_sayer = function(){};
+
+    // // Check for: Opera, FF, Safari, Chrome, console, etc.
+    // if( typeof(jQuery) != 'undefined' &&
+    // 	jQuery('#' + 'bbop-logger-console-textarea') != 'undefined' ){
+    // 	    // Our own logging console takes precedence. 
+    // 	    this._console_sayer = function(msg){
+    // 		var area = jQuery('#'+ 'bbop-logger-console-textarea');
+    // 		area.append(msg + "\n");
+    // 		try{
+    // 		    area.scrollTop(area[0].scrollHeight);
+    // 		} catch (x) {
+    // 		    // could scroll
+    // 		}
+    // 	    };
+    // }else if( typeof(jQuery) != 'undefined' &&
+    // 	jQuery('#' + 'bbop-logger-console-text') != 'undefined' &&
+    // 	jQuery('#' + 'bbop-logger-console-text').length != 0 ){
+    // 	    // Our own logging console takes precedence. 
+    // 	    this._console_sayer = function(msg){
+    // 		jQuery('#' + 'bbop-logger-console-text').append(msg + "\n");
+    // 	    };
+    // }else
+    if( typeof(jQuery) != 'undefined' &&
+	jQuery('#' + 'bbop-logger-console-html') != 'undefined' &&
+	jQuery('#' + 'bbop-logger-console-html').length ){
+	    // Our own logging console takes precedence. 
+	    this._console_sayer = function(msg){
+		var area = jQuery('#'+ 'bbop-logger-console-html');
+		area.append(msg + "<br />");
+		try{
+    		    area.scrollTop(area[0].scrollHeight);
+		} catch (x) {
+		    // could scroll
+		}
+		//jQuery('#' + 'bbop-logger-console-html').append(msg + "<br />");
+	    };
+    }else if( typeof(console) != 'undefined' &&
+	      typeof(console.log) == 'function' ){
+	// This may be okay for Chrome and a subset of various console
+	// loggers. This should now include FF's Web Console.
+	this._console_sayer = function(msg){ console.log(msg + "\n"); };
+    }else if( typeof(opera) != 'undefined' &&
+	typeof(opera.postError) == 'function' ){
+	// If Opera is in there, probably Opera.
+	this._console_sayer = function(msg){ opera.postError(msg + "\n"); };
+    }else if( typeof(window) != 'undefined' &&
+	      typeof(window.dump) == 'function' ){
+	// From developer.mozilla.org: To see the dump output you have
+	// to enable it by setting the preference
+	// browser.dom.window.dump.enabled to true. You can set the
+	// preference in about:config or in a user.js file. Note: this
+	// preference is not listed in about:config by default, you
+	// may need to create it (right-click the content area -> New
+	// -> Boolean).
+	this._console_sayer = function(msg){ dump( msg + "\n"); };
+    }else if( typeof(window) != 'undefined' &&
+	      typeof(window.console) != 'undefined' &&
+	      typeof(window.console.log) == 'function' ){
+	// From developer.apple.com: Safari's "Debug" menu allows you
+	// to turn on the logging of JavaScript errors. To display the
+	// debug menu in Mac OS X, open a Terminal window and type:
+	// "defaults write com.apple.Safari IncludeDebugMenu 1" Need
+	// the wrapper function because safari has personality
+	// problems.
+	this._console_sayer = function(msg){ console.log(msg + "\n"); };
+    }else if( typeof(build) == 'function' &&
+	      typeof(getpda) == 'function' &&
+	      typeof(pc2line) == 'function' &&
+	      typeof(print) == 'function' ){
+	// This may detect SpiderMonkey on the comand line.
+	this._console_sayer = function(msg){ print(msg); };
+    }else if( typeof(org) != 'undefined' &&
+	      typeof(org.rhino) != 'undefined' &&
+	      typeof(print) == 'function' ){
+	// This may detect Rhino on the comand line.
+	this._console_sayer = function(msg){ print(msg); };
+    }
+    
+    /*
+     * Function: kvetch
+     * 
+     * Log a string to somewhere. Also return a string to (mostly for
+     * the unit tests).
+     * 
+     * Arguments:
+     *  string - The string to print out to wherever we found.
+     */
+    this.kvetch = function(string){
+	var ret_str = null;
+	if( anchor.DEBUG == true ){
+
+	    // Make sure there is something there no matter what.
+	    if( typeof(string) == 'undefined' ){ string = ''; }
+
+	    // Redefined the string a little if we have contexts.
+	    if( anchor._context.length > 0 ){
+		var cstr = anchor._context.join(':');
+		string = cstr + ': '+ string;
+	    }
+
+	    // Actually log to the console.
+	    anchor._console_sayer(string);
+
+	    // Bind for output.
+	    ret_str = string;
+	}
+	return ret_str;
+    };
+};
+/*
+ * Package: template.js
+ * 
+ * Namespace: bbop.template
+ * 
+ * BBOP JS template object/enginette.
+ * 
+ * Some (nonsensical) usage is like:
+ * 
+ * : var tt = new bbop.template("{{foo}} {{bar}} {{foo}}");
+ * : 'A B A' == tt.fill({'foo': 'A', 'bar': 'B'});
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'template');
+
+/*
+ * Constructor: template
+ * 
+ * Arguments:
+ *  template_string - the string template to use for future fill calls
+ * 
+ * Returns:
+ *  self
+ */
+bbop.template = function(template_string){
+    this.is_a = 'bbop.template';
+
+    var anchor = this;
+
+    anchor._template_string = template_string;
+
+    // First break the template string into ordered sections which we
+    // will interleve later.
+    var split_re = /\{\{[A-Za-z0-9_-]+\}\}/;
+    anchor._template_split_strings =
+	template_string.split(split_re);
+
+    // Now map out which variables are at which locations.
+    var var_id_re = /\{\{[A-Za-z0-9_-]+\}\}/g;
+    anchor._var_id_matches =
+	template_string.match(var_id_re);
+    // Trim off the '{{' and '}}' from the matches.
+    bbop.core.each(anchor._var_id_matches,
+		  function(item, index){
+		      var new_item = item.substring(2, item.length -2);
+		      anchor._var_id_matches[index] = new_item;
+		  });
+
+    /*
+     * Function: fill
+     * 
+     * Fill the template with the corresponding hash items. Undefined
+     * variables are replaced with ''.
+     * 
+     * Arguments:
+     *  fill_hash - the template with the hashed values
+     * 
+     * Returns:
+     *  string
+     */
+    this.fill = function(fill_hash){
+	var ret_str = '';
+
+	bbop.core.each(anchor._template_split_strings,
+		       function(str, index){
+
+			   // Add the next bit.
+			   ret_str += str;
+
+			   // Add the replacement value if we can make
+			   // sense of it.
+			   if( index < anchor._var_id_matches.length ){
+			       var use_str = '';
+			       var varname = anchor._var_id_matches[index];
+			       if( varname &&
+				   bbop.core.is_defined(fill_hash[varname]) ){
+				   use_str = fill_hash[varname];
+			       }
+			       ret_str += use_str;
+			   }
+		       });
+
+	return ret_str;
+    };
+
+    /*
+     * Function: variables
+     * 
+     * Return a hash of the variables used in the template.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  a hash like: {'foo': true, 'bar': true, ...}
+     */
+    this.variables = function(){
+	return bbop.core.hashify(anchor._var_id_matches);
+    };
+
+};
+/*
+ * Package: logic.js
+ * 
+ * Namespace: bbop.logic
+ * 
+ * BBOP object to try and take some of the pain out of managing the
+ * boolean logic that seems to show up periodically. Right now mostly
+ * aimed at dealing with Solr/GOlr.
+ * 
+ * Anatomy of a core data bundle.
+ * 
+ * : data_bundle => {op: arg}
+ * : op => '__AND__', '__OR__', '__NOT__'
+ * : arg => <string>, array, data_bundle
+ * : array => [array_item*]
+ * : array_item => <string>, data
+ * 
+ * Example:
+ * 
+ * : {and: [{or: ...}, {or: ...}, {and: ...} ]}
+ * : var filters = {'and': []};
+ *
+ * TODO: parens between levels
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.namespace('bbop', 'logic');
+
+/*
+ * Constructor: logic
+ * 
+ * Contructor for the bbop.logic object. NOTE: during processing,
+ * binary operators with a single argument cease to exist as they will
+ * never make it to output.
+ * 
+ * Arguments:
+ *  default_conjuntion - *[optional]* "and" or "or"; defaults to "and"
+ * 
+ * Returns:
+ *  bbop logic object
+ */
+bbop.logic = function(default_conjunction){
+    this._is_a = 'bbop.logic';
+
+    // Add logging.
+    var logger = new bbop.logger();
+    //logger.DEBUG = true;
+    logger.DEBUG = false;
+    function ll(str){ logger.kvetch(str); }
+
+    var logic_anchor = this;
+
+    // // Handling conjunctions.
+    // this._and = '__AND__';
+    // this._or = '__OR__';
+    // this._not = '__NOT__';
+    // function _is_token(possible_token){
+    // 	var retval = false;
+    // 	if( possible_token == this._and ||
+    // 	    possible_token == this._or ||
+    // 	    possible_token == this._not ){
+    // 	   retval = true; 
+    // 	}
+    // 	return retval;
+    // }
+    // // Convert the internal
+    // function _usable
+
+    // // Set the internal default conjunction. Default to "and".
+    // if( ! default_conjunction ){
+    // 	default_conjunction = this._and;
+    // }else if( default_conjunction == this._or ){
+    // 	default_conjunction = this._or;
+    // }else{
+    // 	default_conjunction = this._and;
+    // }
+    if( ! default_conjunction ){
+    	default_conjunction = 'and';
+    }
+    this.default_conjunction = default_conjunction;
+
+    // Set initial state.
+    // ie: this._bundle = {'__AND__': []};
+    //this._bundle = {};
+    //this._bundle[this.default_conjunction] = [];
+    // See documentation for empty().
+    var _empty = function(){
+	logic_anchor._bundle = {};
+	logic_anchor._bundle[logic_anchor.default_conjunction] = [];
+    };
+    _empty();
+
+    /*
+     * Function: add
+     * 
+     * Add to the current stored logic bundle.
+     * 
+     * Parameters:
+     *  item - string or bbop.logic object
+     * 
+     * Returns:
+     *  n/a
+     */
+    //this.and = function(){
+    //this.or = function(){
+    //this.not = function(){
+    this.add = function(item){
+
+	// Add things a little differently if it looks like a bit of
+	// logic.
+	if(  bbop.core.what_is(item) == 'bbop.logic' ){
+	    this._bundle[this.default_conjunction].push(item._bundle);
+	}else{
+	    this._bundle[this.default_conjunction].push(item);
+	}
+    };
+
+    /*
+     * Function: negate
+     * 
+     * Negate the current stored logic.
+     * 
+     * TODO/BUG: I think this might cause an unreleasable circular
+     * reference.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.negate = function(){
+	var nega = {};
+	nega['not'] = this._bundle;
+	this._bundle = nega;
+    };
+    
+    // Walk the data structure...
+    this._read_walk = function(data_bundle, in_encoder, lvl){
+	
+	// The encoder defaults to whatever--no transformations
+	var encoder = in_encoder || function(in_out){ return in_out; };
+
+	ll("LRW: with: " + bbop.core.dump(data_bundle));
+
+	// If level is not defined, we just started and we're on level
+	// one, the first level.
+	var l_enc = '(';
+	var r_enc = ')';
+	if( typeof(lvl) == 'undefined' ){
+	    lvl = 1;
+	    l_enc = '';
+	    r_enc = '';
+	}	
+
+	var read = '';
+	
+	// The task of walking is broken into the terminal case (a
+	// string) or things that we need to operate on (arrays or
+	// sub-data_bundles).
+	if( bbop.core.what_is(data_bundle) == 'string' ){
+	    ll("LRW: trigger string");
+	    read = data_bundle;
+	}else{
+	    ll("LRW: trigger non-string");
+
+	    // Always single op.
+	    var op = bbop.core.get_keys(data_bundle)[0];
+	    var arg = data_bundle[op];
+
+	    // We can treat the single data_bundle/string case like a
+	    // degenerate array case.
+	    if( ! bbop.core.is_array(arg) ){
+		arg = [arg];
+	    }
+
+	    // Recure through the array and join the results with the
+	    // current op.
+	    //ll('L: arg: ' + bbop.core.what_is(arg));
+	    var stack = [];
+	    bbop.core.each(arg, function(item, i){
+			       stack.push(logic_anchor._read_walk(item,
+								  encoder,
+								  lvl + 1));
+			   });
+
+	    // Slightly different things depending on if it's a unary
+	    // or binary op.
+	    if( op == 'not' ){
+		// TODO: I believe that it should no be possible
+		// (i.e. policy by code) to have a 'not' with more
+		// that a single argument.
+		read = op + ' ' + stack.join('');
+	    }else{
+		read = l_enc + stack.join(' ' + op + ' ') + r_enc;
+	    }
+	}
+
+	
+	ll("LRW: returns: " + read);
+	return read;
+    };
+
+    /*
+     * Function: to_string
+     * 
+     * Dump the current data out to a string.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.to_string = function(){
+	return logic_anchor._read_walk(logic_anchor._bundle);
+    };
+
+    /*
+     * Function: url
+     * 
+     * TODO
+     * 
+     * Dump the current data out to a URL.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.url = function(){
+	return logic_anchor._read_walk(logic_anchor._bundle);
+    };
+
+    /*
+     * Function: empty
+     * 
+     * Empty/reset self.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    // Staggered declaration so I can use it above during initialization.
+    this.empty = _empty;
+
+    /*
+     * Function: parse
+     * 
+     * TODO: I think I can grab the shunting yard algorithm for a
+     * similar problem in the old AmiGO 1.x codebase.
+     * 
+     * Parse an incoming string into the internal data structure.
+     * 
+     * Parameters:
+     *  in_str - the incoming string to parse
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.parse = function(in_str){
+	return null;
+    };
+
+};
+/* 
+ * Package: test.js
+ * 
+ * Namespace: bbop.test
+ * 
+ * A trivial testing framework for JS. See test.tests.js for usage.
+ * 
+ *  Note: this cannot depend on core.js (it tests that), so some stuff
+ *  may be duped. On the other hand, we can test ourselves--see
+ *  test.js.tests.
+ */
+
+// Module and namespace checking.
+if ( typeof bbop == "undefined" ){ var bbop = {}; }
+
+/*
+ * Constructor: test
+ * 
+ * Contructor for the BBOP JS unit test system.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  BBOP test suite object
+ */
+bbop.test = function(){
+
+    ///
+    /// Accounting and reporting.
+    ///
+
+    var test_number = 1;
+    var tests_passed = 0;
+    var tests_failed = 0;
+    function _incr_tests(){ test_number = test_number + 1; }
+    function _incr_passed(){ tests_passed = tests_passed + 1; }
+    function _incr_failed(){ tests_failed = tests_failed + 1; }
+    function _incr_failed(){ tests_failed = tests_failed + 1; }
+    function _complete(bool, msg){
+	if( bool ){
+	    if( msg ){
+		print('Test ' + test_number + ' passed: ' + msg + '.');
+	    }else{
+		print('Test ' + test_number + ' passed.');
+	    }
+	    _incr_passed();
+	}else{
+	    if( msg ){
+		print('FAIL: Test ' + test_number + ' failed: ' + msg + '.');
+	    }else{
+		print('FAIL: Test ' + test_number + ' failed.');
+	    }
+	    _incr_failed();
+	}
+	test_number++;	
+    }
+
+    /*
+     * Function: report
+     *
+     * Print a report about what happened during the tests.
+     *
+     * Parameters: 
+     *  n/a
+     *
+     * Returns: 
+     *  n/a; but prints the report as a side-effect
+     */
+    this.report = function(){
+	if( tests_passed + 1 == test_number ){
+	    print('* All tests passed.');
+	}else{
+	    print('* Tests passed: ' + tests_passed);
+	    print('* Tests failed: ' + tests_failed);
+	}
+    };
+
+    ///
+    /// Internal helper functions--different kinds of comparisions.
+    ///
+
+    //
+    function _same_array(one, two){
+	var retval = true;
+	if( one.length != two.length ){
+	    retval = false;
+	}else{
+	    for( var i = 0; i < one.length; i++ ){
+		if( one[i] != two[i] ){
+		    retval = false;
+		    break;
+		}
+	    }
+	}
+	return retval;
+    }
+
+    // Looking at array as sets of...something.
+    function _same_set(set1, set2){
+	var h1 = {};
+	var h2 = {};
+	for( var h1i = 0; h1i < set1.length; h1i++ ){ h1[set1[h1i]] = 1; }
+	for( var h2i = 0; h2i < set2.length; h2i++ ){ h2[set2[h2i]] = 1; }
+	return _same_hash(h1, h2);
+    }
+
+    // NOTE/WARNING: This is a very shallow comparison function.
+    function _same_hash(hash1, hash2){
+
+	var same_p = true;
+	
+	// See if the all of the keys in hash1 are defined in hash2
+	// and that they have the same ==.
+	for( var k1 in hash1 ){
+	    if( typeof hash2[k1] === 'undefined' ||
+		hash1[k1] !== hash2[k1] ){
+		same_p = false;
+		break;
+	    }
+	}
+
+	// If there is still no problem...
+	if( same_p ){
+
+	    // Reverse of above.
+	    for( var k2 in hash2 ){
+		if( typeof hash1[k2] === 'undefined' ||
+		    hash2[k2] !== hash1[k2] ){
+		    same_p = false;
+		    break;
+		}
+	    }
+	}
+	
+	return same_p;
+    }
+
+    // TODO: This could probably be done better.
+    function _link_comp(str1, str2){
+
+	// Decompose links and arguments.
+	var tmp1 = str1.split('?');
+	var head1 = '';
+	var args1 = [];
+	if( ! tmp1[1] ){ // nothing before '?'
+	    args1 = tmp1[0].split('&');
+	}else{ // normal structure
+	    head1 = tmp1[0];
+	    args1 = tmp1[1].split('&');
+	}
+	var sorted_args1 = args1.sort();
+
+	var tmp2 = str2.split('?');
+	var head2 = '';
+	var args2 = [];
+	if( ! tmp2[1] ){ // nothing before '?'
+	    args2 = tmp2[0].split('&');
+	}else{ // normal structure
+	    head2 = tmp2[0];
+	    args2 = tmp2[1].split('&');
+	}
+	var sorted_args2 = args2.sort();
+
+	// var tmp2 = str2.split('?');
+	// var head2 = tmp2[0];
+	// var args2 = tmp2[1].split('&');
+	// var sorted_args2 = args2.sort();
+
+	// Compare heads and arguments.
+	var retval = false;
+	if( head1 == head2 &&
+	    _same_array(sorted_args1, sorted_args2) ){
+	    retval = true;
+	}
+	return retval;
+    }
+
+    // Walk through the list and see if it's there.
+    // If compareator is not defined, just to atom comparison.
+    function _in_list(in_item, list, comparator){
+
+	var retval = false;
+	for(var li = 0; li < list.length; li++ ){
+	    var list_item = list[li];
+
+	    if( comparator ){
+		var comp_op = comparator(in_item, list_item);
+		if( comp_op && comp_op == true ){
+		    retval = true;
+		}
+	    }else{
+		if( in_item == list_item ){
+		    retval = true;
+		}
+	    }
+	}
+
+	return retval;
+    }
+
+    // Basically asking if you can make the target string from the
+    // base string with the add_str added into it somewhere. Strange,
+    // but another way of looking at URL creation in some cases.
+    function _is_string_embedded(target_str, base_str, add_str){
+
+	// Walk through all of ways of splitting base_str and add
+	// add_str in there to see if we get the target_str.
+	var retval = false;
+	for(var si = 0; si <= base_str.length; si++ ){
+	    
+	    var car = base_str.substr(0, si);
+	    var cdr = base_str.substr(si, base_str.length);
+	    //print(car + "|" + add_str + "|" + cdr);
+	    if( car + add_str + cdr == target_str){
+		retval = true;
+		break;
+	    }
+	}
+	return retval;
+    }
+
+    ///
+    /// End-user comparisions and assertions.
+    ///
+
+    /*
+     * Function: is_same_atom
+     *
+     * Test whether two atoms are the same.
+     *
+     * Parameters: 
+     *  question - the atom to test
+     *  answer - the expected atom
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    function _is_simple_same(question, answer, msg){
+	_complete(question == answer, msg);
+    }
+    this.is_same_atom = _is_simple_same;
+
+    /*
+     * Function: is_different_atom
+     *
+     * A negative version of <is_same_atom>.
+     *
+     * Parameters: 
+     *  question - the atom to test
+     *  answer - the unexpected atom
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_different_atom = function(question, answer, msg){
+	_complete(question != answer, msg);
+    };
+
+    /*
+     * Function: is_defined
+     *
+     * Test whether a value is defined.
+     *
+     * Parameters: 
+     *  thing - the value to test for being defined
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_defined = function(thing, msg){
+	if( thing ){
+	    _complete(true, msg);
+	}else{
+	    _complete(false, msg);
+	}
+    };
+
+    /*
+     * Function: is_not_defined
+     *
+     * A negative version of <is_defined>.
+     *
+     * Parameters: 
+     *  thing - the value to test for being undefined
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_not_defined = function(thing, msg){
+	if( thing ){
+	    _complete(false, msg);
+	}else{
+	    _complete(true, msg);
+	}
+    };
+
+    /*
+     * Function: is_true
+     *
+     * Test whether a value is true.
+     *
+     * Parameters: 
+     *  bool - the variable to test
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_true = function(bool, msg){
+	if( bool == true ){
+	    _complete(true, msg);
+	}else{
+	    _complete(false, msg);
+	}
+    };
+
+    /*
+     * Function: is_false
+     *
+     * A negative version of <is_true>.
+     *
+     * Parameters: 
+     *  bool - the variable to test
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_false = function(bool, msg){
+	if( bool == false ){
+	    _complete(true, msg);
+	}else{
+	    _complete(false, msg);
+	}
+    };
+
+    /*
+     * Function: is_x_greater_than_y
+     *
+     * Test whether one value is greate than another. Uses the
+     * standard ">" operator.
+     *
+     * Parameters: 
+     *  x_thing - the expected greater value
+     *  y_thing - the expected lesser value
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_x_greater_than_y = function(x_thing, y_thing, msg){
+	if( x_thing > y_thing ){
+	    _complete(true, msg);
+	}else{
+	    _complete(false, msg);
+	}
+    };
+
+    /*
+     * Function: is_same_url
+     *
+     * Test whether two links are functionally equivalent.
+     *
+     * Parameters: 
+     *  link1 - url
+     *  link2 - url
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_same_url = function(link1, link2, msg){
+	_complete(_link_comp(link1, link2), msg);
+    };    
+
+    /*
+     * Function: is_different_url
+     *
+     * A negative version of <is_same_url>.
+     *
+     * Parameters: 
+     *  link1 - url
+     *  link2 - url
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_different_url = function(link1, link2, msg){
+	_complete(! _link_comp(link1, link2), msg);
+    };    
+
+    // /*
+    //  * Function: is_same_url_by_assembly
+    //  *
+    //  * Test whether two URLs are functionally equivalent.
+    //  *
+    //  * Parameters: 
+    //  *  link - url
+    //  *  base - string
+    //  *  psuedo_assembly - hash 
+    //  *  msg - *[optional]* informational message about test
+    //  *
+    //  * Returns: 
+    //  *  n/a
+    //  */
+    // function _psuedo_assmble(assembly){
+    // 	var retval = '';
+    // 	for( var k2 in hash2 ){
+    // 	return retval;
+    // }
+    // this.is_same_url_by_assembly = function(link, base,
+    // 					    psuedo_assembly, msg){
+    // 	_complete(_link_comp(link,
+    // 			     base + bbop.core.get_assemble(assembly)),
+    // 		  msg);
+    // };    
+
+    /*
+     * Function: is_same_set
+     *
+     * Test whether two sets (as atomic arrays) are the same.
+     *
+     * Parameters: 
+     *  set1 - set (as array)
+     *  set2 - set (as array)
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_same_set = function(set1, set2, msg){
+	_complete(_same_set(set1, set2), msg);
+    };
+
+    /*
+     * Function: is_different_set
+     *
+     * A negative version of <is_same_set>.
+     *
+     * Parameters: 
+     *  set1 - set (as array)
+     *  set2 - set (as array)
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_different_set = function(set1, set2, msg){
+	_complete(! _same_set(set1, set2), msg);
+    };
+
+    /*
+     * Function: is_same_hash
+     *
+     * Test whether two simple atomic hashes are the same.
+     *
+     * Parameters: 
+     *  hash1 - hash
+     *  hash2 - hash
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_same_hash = function(hash1, hash2, msg){
+	_complete(_same_hash(hash1, hash2), msg);
+    };
+
+    /*
+     * Function: is_different_hash
+     *
+     * A negative version of <is_same_hash>.
+     *
+     * Parameters: 
+     *  hash1 - hash
+     *  hash2 - hash
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_different_hash = function(hash1, hash2, msg){
+	_complete(! _same_hash(hash1, hash2), msg);
+    };
+
+    /*
+     * Function: is_in_list
+     *
+     * Test whether an item is in a list (array).
+     *
+     * Parameters: 
+     *  item - the value to test
+     *  list - the array to test in
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_in_list = function(item, list, msg){
+	_complete(_in_list(item, list), msg);
+    };
+
+    /*
+     * Function: is_not_in_list
+     *
+     * A negative version of <is_in_list>.
+     *
+     * Parameters: 
+     *  item - the value to test
+     *  list - the array to test in
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_not_in_list = function(item, list, msg){
+	_complete(! _in_list(item, list), msg);
+    };
+
+    /*
+     * Function: is_in_list_diy
+     *
+     * A DIY version of is_in_list. In this case, you can pass your
+     * own comparison function to check the item against the list.
+     *
+     * Parameters: 
+     *  item - the value to test
+     *  list - the array to test in
+     *  comp - the comparison function; like: function(in_item, list_item){...}
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_in_list_diy = function(item, list, comp, msg){
+	_complete(_in_list(item, list, comp), msg);
+    };
+
+    /*
+     * Function: is_not_in_list_diy
+     *
+     * A negative version of <is_in_list_diy>.
+     *
+     * Parameters: 
+     *  item - the value to test
+     *  list - the array to test in
+     *  comp - the comparison function; like: function(in_item, list_item){...}
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_not_in_list_diy = function(item, list, comp, msg){
+	_complete(! _in_list(item, list, comp), msg);
+    };
+
+    /*
+     * Function: is_string_embedded
+     *
+     * Test whether a target string (target_str) can be made by
+     * embedding a string (added_str) into a base string (base_str).
+     * 
+     * Useful in certain cases when checking URLs.
+     *
+     * Parameters: 
+     *  target_str - the value to test
+     *  base_str - the expected value
+     *  added_str - the expected value
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_string_embedded = function(target_str, base_str, added_str, msg){
+	_complete(_is_string_embedded(target_str, base_str, added_str), msg);
+    };
+
+    /*
+     * Function: is_string_not_embedded
+     *
+     * A negative version of <is_string_embedded>.
+     *
+     * Parameters: 
+     *  target_str - the value to test
+     *  base_str - the expected value
+     *  added_str - the expected value
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.is_string_not_embedded =
+	function(target_str, base_str, added_str, msg){
+	    _complete(! _is_string_embedded(target_str, base_str, added_str),
+		      msg);
+	};
+
+    /*
+     * Function: pass
+     *
+     * Always return test as true--useful when testing using control
+     * structures and the like.
+     *
+     * Parameters: 
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.pass = function(msg){
+	_complete(true, msg);
+    };
+
+    /*
+     * Function: fail
+     *
+     * Always return test as false--useful when testing using control
+     * structures and the like.
+     *
+     * Parameters: 
+     *  msg - *[optional]* informational message about test
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.fail = function(msg){
+	_complete(false, msg);
+    };
+};
+/* 
+ * Package: registry.js
+ * 
+ * Namespace: bbop.registry
+ * 
+ * BBOP generic lightweight listener/callback registry system.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'registry');
+
+/*
+ * Constructor: registry
+ * 
+ * Contructor for BBOP registry. Takes a list of event categories as
+ * strings.
+ * 
+ * Arguments:
+ *  evt_list - a list of strings that identify the events to be used
+ * 
+ * Returns:
+ *  bbop registry object
+ */
+bbop.registry = function(evt_list){
+    this._is_a = 'bbop.registry';
+
+    var registry_anchor = this;
+
+    // Handle the registration of call functions to get activated
+    // after certain events.
+    this.callback_registry = {};
+    bbop.core.each(evt_list, function(item, i){
+		       registry_anchor.callback_registry[item] = {};
+		   });
+    
+    /*
+     * Function: register
+     *
+     * Add the specified function from the registry, with an optional
+     * relative priority against other callback functions.
+     *
+     * The in_priority value is relative to others in the category,
+     * with a higher priority...getting priority.
+     * 
+     * Parameters: 
+     *  category - string; one of the pre-defined categories
+     *  function_id - string; a unique string to identify a function
+     *  in_function - function
+     *  in_priority - *[optional]* number
+     *
+     * Returns: 
+     *  n/a
+     * 
+     * See also:
+     *  <apply>
+     */
+    this.register = function(category, function_id, in_function, in_priority){
+
+	// Only these categories.
+	if( typeof(registry_anchor.callback_registry[category]) == 'undefined'){
+	    throw new Error('cannot register, unknown category');
+	}
+
+	// The default priority is 0.
+	var priority = 0;
+	if( in_priority ){ priority = in_priority; }
+
+	registry_anchor.callback_registry[category][function_id] =
+	    {
+		runner: in_function,
+		priority: priority
+	    };
+    };
+
+    /*
+     * Function: is_registered
+     *
+     * Returns whether or not an id has already been registered to a
+     * category. Will return null if the category does not exist.
+     * 
+     * Parameters: 
+     *  category - string; one of the pre-defined categories
+     *  function_id - string; a unique string to identify a function
+     *
+     * Returns: 
+     *  true, false, or null
+     */
+    this.is_registered = function(category, function_id){
+
+	var retval = null;
+
+	var anc = registry_anchor.callback_registry;
+
+	//
+	if( typeof(anc[category]) != 'undefined'){
+	    
+	    retval = false;
+
+	    if( typeof(anc[category][function_id]) != 'undefined'){
+		retval = true;
+	    }
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: unregister
+     *
+     * Remove the specified function from the registry. Must specify a
+     * legitimate category and the function id of the function in it.
+     *
+     * Parameters: 
+     *  category - string
+     *  function_id - string
+     *
+     * Returns: 
+     *  boolean on whether something was unregistered
+     */
+    this.unregister = function(category, function_id){
+	var retval = false;
+	if( registry_anchor.callback_registry[category] &&
+	    registry_anchor.callback_registry[category][function_id] ){
+		delete registry_anchor.callback_registry[category][function_id];
+		retval = true;
+            }
+	return retval;
+    };
+    
+    /*
+     * Function: get_callbacks
+     *
+     * Generic getter for callback functions, returns by priority.
+     *
+     * Parameters: 
+     *  category - string
+     *
+     * Returns: 
+     *  an ordered (by priority) list of function_id strings
+     */
+    this.get_callbacks = function(category){
+
+	var cb_id_list =
+	    bbop.core.get_keys(registry_anchor.callback_registry[category]);
+	// Sort callback list according to priority.
+	var ptype_registry_anchor = this;
+	cb_id_list.sort(
+	    function(a, b){  
+		var pkg_a =
+		    ptype_registry_anchor.callback_registry[category][a];
+		var pkg_b =
+		    ptype_registry_anchor.callback_registry[category][b];
+		return pkg_b['priority'] - pkg_a['priority'];
+	    });
+	
+	// Collect the actual stored functions by priority.
+	var cb_fun_list = [];
+	for( var cbi = 0; cbi < cb_id_list.length; cbi++ ){
+	    var cb_id = cb_id_list[cbi];
+	    var to_run =
+		registry_anchor.callback_registry[category][cb_id]['runner'];
+	    cb_fun_list.push(to_run);
+	    // ll('callback: ' + category + ', ' + cb_id + ', ' +
+	    //    this.callback_registry[category][cb_id]['priority']);
+	}
+	
+	return cb_fun_list;
+    };
+
+    /*
+     * Function: apply_callbacks
+     *
+     * Generic runner for prioritized callbacks with various arguments
+     * and an optional change in context..
+     *
+     * Parameters: 
+     *  category - string
+     *  arg_list - a list of arguments to pass to the function in the category
+     *  context - *[optional]* the context to apply the arguments in
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.apply_callbacks = function(category, arg_list, context){
+
+	// Run all against registered functions.
+	var callbacks = registry_anchor.get_callbacks(category);
+	for( var ci = 0; ci < callbacks.length; ci++ ){
+	    var run_fun = callbacks[ci];
+	    //run_fun(arg_list);
+	    run_fun.apply(context, arg_list);
+	}
+    };
+};
+/* 
+ * Package: html.js
+ * 
+ * Namespace: bbop.html
+ * 
+ * Right now contains bbop.html.tag, but all html producing functions
+ * should go in here somewhere.
+ * 
+ * All bbop.html implement the interface:
+ *  .to_string(): returns a string of you and below
+ *  .add_to(): add things between the tags
+ *  .empty(): empties all things between the tags
+ *  .get_id(): return the id or null if not defined
+ * These are enforced during the tests.
+ * 
+ * For functions that take attribute hashes, there is a special
+ * attribute {'generate_id': true} that will generate a somewhat
+ * random id if an incoming id was not already specified. This id can
+ * be retrieved using get_id().
+ * 
+ * This package takes all of the bbop.html.* namespace.
+ */
+
+bbop.core.require('bbop', 'core');
+//bbop.core.require('bbop', 'logger');
+//bbop.core.require('bbop', 'amigo');
+bbop.core.namespace('bbop', 'html');
+bbop.core.namespace('bbop', 'html', 'tag');
+bbop.core.namespace('bbop', 'html', 'accordion');
+bbop.core.namespace('bbop', 'html', 'list');
+bbop.core.namespace('bbop', 'html', 'input');
+bbop.core.namespace('bbop', 'html', 'img');
+
+/*
+ * Namespace: bbop.html.tag
+ * 
+ * Constructor: tag
+ * 
+ * Create the fundamental tag object to work with and extend.
+ * 
+ * Parameters:
+ *  tag - the tag name to be created
+ *  attrs - *[serially optional]* the typical attributes to add
+ *  below - *[optional]* a list/array of other html objects that exists "between" the tags
+ * 
+ * Returns:
+ *  bbop.html.tag object
+ */
+bbop.html.tag = function(tag, attrs, below){
+    this._is_a = 'bbop.html.tag';
+
+    // Arg check--attrs should be defined as something.
+    if( ! attrs ){ attrs = {}; }
+
+    // Generate (or not) id if it was requested.
+    if( ! bbop.core.is_defined(attrs['id']) &&
+	bbop.core.is_defined(attrs['generate_id']) &&
+	bbop.core.is_defined(attrs['generate_id']) == true ){
+	    // Add a real id.
+	    attrs['id'] = 'gen_id-bbop-html-'+ bbop.core.randomness(20);
+	    // Remove the 'generated_id' property.
+	    delete attrs['generate_id'];
+	}
+    this._attrs = attrs;
+    
+    // Arg check--below should be some kind of an array.
+    if( ! below ){
+	below = [];
+    }else if( bbop.core.is_array(below) ){
+	// do nothing
+    }else{
+	// hopefully a bbop.html.tag then
+	below = [below];
+    }
+
+    // Accumulate the incoming attributes if there are any.
+    var additional_attrs = '';
+    bbop.core.each(this._attrs, function(in_key, in_val){
+		       additional_attrs = additional_attrs + ' ' +
+			   in_key + '="' + in_val + '"';
+		   });
+
+    this._car = '<' + tag + additional_attrs + '>';
+    this._cdr = '</' + tag + '>';
+    this._contents = below;
+    this._singleton = '<' + tag + additional_attrs + ' />';
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert a tag object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.tag.prototype.to_string = function(){
+    var acc = '';
+    bbop.core.each(this._contents,
+		   function(item, i){
+		       // if( typeof(item) == 'string' ){
+		       // 	   acc = acc + item;
+		       // }else if( typeof(item['to_string']) == 'function' ){
+		       // 	   acc = acc + item.to_string();
+		       // }else{
+		       // 	   throw new Error('No to_string for (' +
+		       // 			   bbop.core.what_is(item) +
+		       // 			   ') ' + item);
+		       // }
+		       acc = acc + bbop.core.to_string(item);
+		   });
+    
+    // Special return case if there are no children (to prevent
+    // weirdness for things like br and input).
+    var output = this._singleton;
+    if( acc != '' ){ output = this._car + acc + this._cdr; }
+
+    return output;
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the tags. Order of addition is order of output.
+ * 
+ * Parameters:
+ *  bbop_html_tag_or_string - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.tag.prototype.add_to = function(bbop_html_tag_or_string){
+    this._contents.push(bbop_html_tag_or_string);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content between the tags.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.tag.prototype.empty = function(){
+    this._contents = [];
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.tag.prototype.get_id = function(){
+    var retval = null;
+    if( bbop.core.is_defined(this._attrs['id']) ){
+	retval = this._attrs['id'];
+    }
+    return retval;
+};
+
+/*
+ * Namespace: bbop.html.accordion
+ * 
+ * Constructor: accordion
+ * 
+ * Create the a frame for the functional part of a jQuery accordion
+ * structure.
+ * 
+ * :Input:
+ * : [[title, string/*.to_string()], ...]
+ * :
+ * :Output:
+ * : <div id="accordion">
+ * :  <h3><a href="#">Section 1</a></h3>
+ * :  <div>
+ * :   <p>
+ * :    foo
+ * :   </p>
+ * :  </div>
+ * :  ...
+ * : </div>
+ * 
+ * Parameters:
+ *  in_list - accordion frame headers: [[title, string/*.to_string()], ...]
+ *  attrs - *[serially optional]* attributes to apply to the new top-level div
+ *  add_id_p - *[optional]* true or false; add a random id to each section
+ * 
+ * Returns:
+ *  bbop.html.accordion object
+ * 
+ * Also see: <tag>
+ */
+bbop.html.accordion = function(in_list, attrs, add_id_p){
+    this._is_a = 'bbop.html.accordion';
+
+    //
+    if( typeof(add_id_p) == 'undefined' ){ add_id_p = false; }
+
+    // Arg check--attrs should be defined as something.
+    this._attrs = attrs || {};
+
+    // Internal stack always starts with a div.
+    this._div_stack = new bbop.html.tag('div', this._attrs);
+
+    this._section_id_to_content_id = {};
+
+    // Iterate over the incoming argument list.
+    var accordion_this = this;
+    bbop.core.each(in_list, function(item){
+		       var sect_title = item[0];
+		       var content = item[1];
+		       accordion_this.add_to(sect_title, content, add_id_p);
+		   });
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert the accordion object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.accordion.prototype.to_string = function(){
+    return this._div_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add a contect section to the accordion.
+ * 
+ * Parameters:
+ *  section_info - a string or a hash with 'id', 'label', and 'description'
+ *  content_blob - string or bbop.html object to put in a section
+ *  add_id_p - *[optional]* true or false; add a random id to the section
+ * 
+ * Returns: n/a
+ */
+bbop.html.accordion.prototype.add_to =
+    function(section_info, content_blob, add_id_p){
+
+    // If section_info isn't an object, assume it is a string and use
+    // it for everything.
+    var section_id = null;
+    var section_label = null;
+    var section_desc = null;
+    if(typeof section_info != 'object' ){
+	section_id = section_info;
+	section_label = section_info;
+    }else{
+	if( section_info['id'] ){ section_id = section_info['id']; }
+	if( section_info['label'] ){ section_label = section_info['label']; }
+	if( section_info['description'] ){
+	    section_desc = section_info['description'];
+	}
+    }
+
+    // Add header section.
+    //var h3 = new bbop.html.tag('h3', {title: section_desc});
+    var h3 = new bbop.html.tag('h3');
+    var anc = null;
+    if( section_desc ){
+	// anc = new bbop.html.tag('a', {href: '#'}, section_label);
+	anc = new bbop.html.tag('a', {href: '#', title: section_desc},
+				section_label);
+    }else{
+	anc = new bbop.html.tag('a', {href: '#'}, section_label);
+    }
+    h3.add_to(anc);
+    this._div_stack.add_to(h3);
+
+    var div = null;
+
+    // Generate random id for the div.
+    if( typeof(add_id_p) == 'undefined' ){ add_id_p = false; }
+    if( add_id_p ){
+	var rid = 'accordion-' + section_id + '-' + bbop.core.randomness(20);
+	this._section_id_to_content_id[section_id] = rid;    
+	div = new bbop.html.tag('div', {'id': rid});	
+    }else{
+	div = new bbop.html.tag('div');	
+    }
+
+    // Add add content stub to section.
+   var p = new bbop.html.tag('p', {}, bbop.core.to_string(content_blob));
+    div.add_to(p);
+    this._div_stack.add_to(div);
+};
+
+// // Add a section to the accordion.
+// bbop.html.accordion.prototype.add_to_section = function(sect_id, content){
+//     var cdiv = this._section_id_to_content_div[sect_id];
+//     if( ! cdiv ){
+// 	throw new Error('Cannot add to undefined section.');
+//     }
+// };
+
+/*
+ * Function: empty
+ * 
+ * Empty all sections from the accordion.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.accordion.prototype.empty = function(){
+    this._div_stack = new bbop.html.tag('div', this._attrs);
+    this._section_id_to_content_id = {};
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.accordion.prototype.get_id = function(){
+    return this._div_stack.get_id();
+};
+
+/*
+ * Function: get_section_id
+ * 
+ * Get the "real" section id by way of the "convenience" section id?
+ * 
+ * Parameters:
+ *  sect_id - TODO ???
+ * 
+ * Returns: TODO ???
+ */
+bbop.html.accordion.prototype.get_section_id = function(sect_id){
+	return this._section_id_to_content_id[sect_id];    
+};
+
+
+// // TODO: just empty the contents from an ided section.
+// bbop.html.accordion.prototype.empty_section = function(sect_id){
+//     var div = this._section_id_to_content_div[sect_id];
+//     div.empty();
+// };
+
+/*
+ * Namespace: bbop.html.list
+ * 
+ * Constructor: list
+ * 
+ * Create the a frame for an unordered list object.
+ * 
+ * :Input:
+ * : [string/*.to_string(), ...]
+ * :
+ * :Output:
+ * : <ul id="list">
+ * :  <li>foo</li>
+ * :   ...
+ * : </ul>
+ * 
+ * Parameters:
+ *  in_list - list of strings/bbop.html objects to be li separated
+ *  attrs - *[optional]* attributes to apply to the new top-level ul
+ * 
+ * Returns:
+ *  bbop.html.list object
+ * 
+ * Also see: <tag>
+ */
+bbop.html.list = function(in_list, attrs){
+    this._is_a = 'bbop.html.list';
+    
+    // Arg check--attrs should be defined as something.
+    if( ! attrs ){ attrs = {}; }
+    this._attrs = attrs;
+
+    // Internal stack always starts with a ul.
+    this._ul_stack = new bbop.html.tag('ul', this._attrs);
+
+    var list_this = this;
+    bbop.core.each(in_list, function(item){ list_this.add_to(item); });
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert a list object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.list.prototype.to_string = function(){
+    return this._ul_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add a new li section to a list.
+ * 
+ * Optionally, it can take multiple arguments and will add each of
+ * them to the new li tag in turn.
+ * 
+ * Parameters:
+ *  item1 - another tag object or a string (html or otherwise)
+ *  item2 - *[optional]* ...on forever
+ * 
+ * Returns: n/a
+ */
+bbop.html.list.prototype.add_to = function(){
+
+    // Convert anonymous arguments into an Array.
+    var args = Array.prototype.slice.call(arguments); 
+
+    // Cycle through and add them to the accumulator for the new li.
+    var li_acc = [];
+    bbop.core.each(args,
+		   function(arg){
+		       li_acc.push(bbop.core.to_string(arg));
+		   });
+
+    // Join them and add them to the stack of the encompassing ul.
+    var li = new bbop.html.tag('li', {}, li_acc.join(" "));
+    this._ul_stack.add_to(li);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content (li's) from the list.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.list.prototype.empty = function(){
+    this._ul_stack = new bbop.html.tag('ul', this._attrs);
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.list.prototype.get_id = function(){
+    return this._ul_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.input
+ * 
+ * Constructor: input
+ * 
+ * Create a form input.
+ * 
+ * Parameters:
+ *  attrs - *[optional]* the typical attributes to add
+ * 
+ * Returns:
+ *  bbop.html.input object
+ */
+bbop.html.input = function(attrs){
+    this._is_a = 'bbop.html.input';
+    
+    // Arg check--attrs should be defined as something.
+    if( ! attrs ){ attrs = {}; }
+    this._attrs = attrs;
+
+    // Internal stack always starts with a ul.
+    this._input_stack = new bbop.html.tag('input', this._attrs);
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert an input into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.input.prototype.to_string = function(){
+    return this._input_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the input tags.
+ * 
+ * Parameters:
+ *  item - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.input.prototype.add_to = function(item){
+    this._input_stack.add_to(bbop.core.to_string(item));
+};
+
+/*
+ * Function: empty
+ * 
+ * Reset/remove all children.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.input.prototype.empty = function(){
+    this._input_stack = new bbop.html.tag('input', this._attrs);
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.input.prototype.get_id = function(){
+    return this._input_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.anchor
+ * 
+ * Constructor: anchor
+ * 
+ * Create an anchor object. Note: href, title, etc. go through
+ * in_attrs.
+ * 
+ * Parameters:
+ *  in_cont - the contents between the "a" tags
+ *  in_attrs - *[optional]* the typical attributes to add
+ * 
+ * Returns:
+ *  bbop.html.anchor object
+ */
+bbop.html.anchor = function(in_cont, in_attrs){
+    this._is_a = 'bbop.html.anchor';
+    
+    // Arg check--attrs should be defined as something.
+    this._attrs = in_attrs || {};
+
+    // Internal stack always starts with a ul.
+    this._anchor_stack = new bbop.html.tag('a', this._attrs, in_cont);
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert an anchor object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.anchor.prototype.to_string = function(){
+    return this._anchor_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the tags. Order of addition is order of output.
+ * 
+ * Parameters:
+ *  item - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.anchor.prototype.add_to = function(item){
+    this._anchor_stack.add_to(item);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content between the tags.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.anchor.prototype.empty = function(){
+    this._anchor_stack.empty();
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.anchor.prototype.get_id = function(){
+    return this._anchor_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.image
+ * 
+ * Constructor: image
+ * 
+ * Create an image (img) object. Note: alt, title, etc. go through
+ * in_attrs.
+ * 
+ * Parameters:
+ *  in_attrs - *[optional]* the typical attributes to add
+ * 
+ * Returns:
+ *  bbop.html.image object
+ */
+bbop.html.image = function(in_attrs){
+    this._is_a = 'bbop.html.image';
+    
+    // Arg check--attrs should be defined as something.
+    this._attrs = in_attrs || {};
+
+    // Internal stack always starts with a ul.
+    this._image_stack = new bbop.html.tag('img', this._attrs);
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert an image object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.image.prototype.to_string = function(){
+    return this._image_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the tags. Order of addition is order of output.
+ * 
+ * Parameters:
+ *  item - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.image.prototype.add_to = function(item){
+    this._image_stack.add_to(item);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content between the tags.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.image.prototype.empty = function(){
+    this._image_stack.empty();
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.image.prototype.get_id = function(){
+    return this._image_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.table
+ * 
+ * Constructor: table
+ * 
+ * Create a simple table structure.
+ * in_headers is necessary, but can be empty.
+ * in_entries is necessary, but can be empty.
+ * 
+ * Parameters:
+ *  in_headers - ordered list of headers
+ *  in_entries - lists of lists of entry items
+ *  in_attrs - *[optional]* the typical attributes to add to the table
+ * 
+ * Returns:
+ *  bbop.html.table object
+ */
+bbop.html.table = function(in_headers, in_entries, in_attrs){
+    this._is_a = 'bbop.html.table';
+    
+    // Arg check--attrs should be defined as something.
+    var headers = in_headers || [];
+    var entries = in_entries || [];
+    this._attrs = in_attrs || {};
+
+    // Row class count.
+    this._count = 0;
+
+    // Internal stack always starts with a table.
+    this._table_stack = new bbop.html.tag('table', this._attrs);
+
+    // Only add headers if they exist.
+    if( ! bbop.core.is_empty(headers) ){
+	var head_row = new bbop.html.tag('tr');
+	bbop.core.each(headers,
+		       function(header){
+			   var th = new bbop.html.tag('th');
+			   th.add_to(header);
+			   head_row.add_to(th);
+		       });
+	var head_stack = new bbop.html.tag('thead');
+	head_stack.add_to(head_row);
+	this._table_stack.add_to(head_stack);
+    }
+
+    // Add incoming rows to the body. Keep the body stack around for
+    // bookkeeping.
+    this._body_stack = new bbop.html.tag('tbody');
+    this._table_stack.add_to(this._body_stack);
+
+    var this_table = this;
+    bbop.core.each(entries, function(item){ this_table.add_to(item); });
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert a table object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.table.prototype.to_string = function(){
+    return this._table_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add data row. The entries argument is coerced into an array of tds.
+ * 
+ * Parameters:
+ *  entries - lists of lists of entry items
+ * 
+ * Returns: n/a
+ */
+bbop.html.table.prototype.add_to = function(entries){
+    
+    //this._body_stack = new bbop.html.tag('tbody');
+
+    // Get the class for the row.
+    var row_class = 'odd_row';
+    if( this._count % 2 == 0 ){ row_class = 'even_row'; }
+    this._count = this._count + 1;
+
+    var tr = new bbop.html.tag('tr', {'class': row_class});
+
+    // Array or not, add everything as tds.
+    if( ! bbop.core.is_array(entries) ){ entries = [entries]; }
+    bbop.core.each(entries,
+		   function(entry){
+		       var td = new bbop.html.tag('td');
+		       td.add_to(entry);
+		       tr.add_to(td);
+		   });
+    this._body_stack.add_to(tr);
+};
+
+/*
+ * Function: empty
+ * 
+ * Headers do not get wiped, just the data rows in the tbody.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.table.prototype.empty = function(){
+    this._count = 0;
+    this._body_stack.empty();
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.table.prototype.get_id = function(){
+    return this._table_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.button
+ * 
+ * Constructor: button
+ * 
+ * Create a button object.
+ * For after-the-fact decoration, take a look at:
+ * <https://jquery-ui.googlecode.com/svn/tags/1.6rc5/tests/static/icons.html>
+ * 
+ * Parameters:
+ *  in_label - label
+ *  in_attrs - *[optional]* the typical attributes to add
+ * 
+ * Returns:
+ *  bbop.html.button object
+ */
+bbop.html.button = function(in_label, in_attrs){
+    this._is_a = 'bbop.html.button';
+    
+    // Arg check--attrs should be defined as something.
+    this._attrs = in_attrs || {};
+
+    // Internal stack is just the top-level button.
+    this._button_stack = new bbop.html.tag('button', this._attrs, in_label);
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert a button object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.button.prototype.to_string = function(){
+    return this._button_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the tags. Order of addition is order of output.
+ * Not really worth much as it just equates to changing the label.
+ * 
+ * Parameters:
+ *  item - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.button.prototype.add_to = function(item){
+    this._button_stack.add_to(item);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content between the tags. This equates to removing the
+ * label.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.button.prototype.empty = function(){
+    this._button_stack.empty();
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.button.prototype.get_id = function(){
+    return this._button_stack.get_id();
+};
+
+/*
+ * Namespace: bbop.html.span
+ * 
+ * Constructor: span
+ * 
+ * Create a span object.
+ * Fun for calling live bits after the fact.
+ * 
+ * Parameters:
+ *  in_label - label
+ *  in_attrs - *[optional]* the typical attributes to add
+ * 
+ * Returns:
+ *  bbop.html.span object
+ */
+bbop.html.span = function(in_label, in_attrs){
+    this._is_a = 'bbop.html.span';
+    
+    // Arg check--attrs should be defined as something.
+    this._attrs = in_attrs || {};
+
+    // Internal stack is just the top-level span.
+    this._span_stack = new bbop.html.tag('span', this._attrs, in_label);
+};
+
+/*
+ * Function: to_string
+ * 
+ * Convert a span object into a html-ized string.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns:
+ *  string
+ */
+bbop.html.span.prototype.to_string = function(){
+    return this._span_stack.to_string();
+};
+
+/*
+ * Function: add_to
+ * 
+ * Add content between the tags. Order of addition is order of output.
+ * Not really worth much as it just equates to changing the label.
+ * 
+ * Parameters:
+ *  item - another tag object or a string (html or otherwise)
+ * 
+ * Returns: n/a
+ */
+bbop.html.span.prototype.add_to = function(item){
+    this._span_stack.add_to(item);
+};
+
+/*
+ * Function: empty
+ * 
+ * Remove all content between the tags. This equates to removing the
+ * label.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: n/a
+ */
+bbop.html.span.prototype.empty = function(){
+    this._span_stack.empty();
+};
+
+/*
+ * Function: get_id
+ * 
+ * Return the id if extant, null otherwise.
+ * 
+ * Parameters: n/a
+ * 
+ * Returns: string or null
+ */
+bbop.html.span.prototype.get_id = function(){
+    return this._span_stack.get_id();
+};
+/* 
+ * Package: linker.js
+ * 
+ * Namespace: bbop.linker
+ * 
+ * This package contains a "useable", but utterly worthless reference
+ * implementation of a linker.
+ */
+
+bbop.core.namespace('bbop', 'linker');
+
+/*
+ * Constructor: linker
+ *
+ * Partial version for this library; revision (major/minor version numbers)
+ * information.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  n/a
+ */
+bbop.linker = function(){
+    this._is_a = 'bbop.linker';
+};
+
+/*
+ * Function: url
+ * 
+ * Return a url string.
+ * 
+ * Arguments:
+ *  args - id
+ *  xid - *[optional]* an internal transformation id (context)
+ * 
+ * Returns:
+ *  null -- always "fails""
+ */
+bbop.linker.prototype.url = function(id, xid){
+    return null;
+};
+
+/*
+ * Function: anchor
+ * 
+ * Return an html anchor string.
+ * 
+ * Arguments:
+ *  args - id
+ *  xid - *[optional]* an internal transformation id (context)
+ * 
+ * Returns:
+ *  null -- always "fails""
+ */
+bbop.linker.prototype.anchor = function(id, xid){
+    return null;
+};
+/* 
+ * Package: handler.js
+ * 
+ * Namespace: bbop.handler
+ * 
+ * This package contains a "useable", but utterly worthless reference
+ * implementation of a handler.
+ */
+
+bbop.core.namespace('bbop', 'handler');
+
+/*
+ * Constructor: handler
+ *
+ * Partial version for this library; revision (major/minor version numbers)
+ * information.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  n/a
+ */
+bbop.handler = function(){
+    this._is_a = 'bbop.handler';
+};
+
+/*
+ * Function: url
+ * 
+ * Return a url string.
+ * 
+ * Arguments:
+ *  data - the incoming thing to be handled
+ *  name - the field name to be processed
+ *  context - *[optional]* a string to add extra context to the call
+ *  fallback - *[optional]* a fallback function to call in case nothing is found
+ * 
+ * Returns:
+ *  null
+ */
+bbop.handler.prototype.dispatch = function(data, name, context, fallback){
+    return null;
+};
+/* 
+ * Package: model.js
+ * 
+ * Namespace: bbop.model
+ * 
+ * Purpose: Basic edged graph and operations.
+ * 
+ * NOTE: A model instance may not be the whole graph, just a
+ * subgraph--this is the difference between nodes and
+ * named_nodes. nodes are real things, while named_nodes are things
+ * referenced by edges.
+ * 
+ * Check TODOs, we would like everything as linear as possible.
+ * 
+ * TODO: memoize everything but add_*. Functional enough that it
+ * should work if we just empty the caches after every add_* op.
+ * 
+ * Required: bbop.core (<core.js>)
+ */
+
+// Module and namespace checking.
+bbop.core.require('bbop', 'core'); // not needed, but want the habit
+bbop.core.require('bbop', 'logger');
+bbop.core.namespace('bbop', 'model');
+
+
+/*
+ * Variable: default_predicate
+ * 
+ * The predicate we'll use when none other is defined. You can
+ * probably safely ignore this if all of the edges in your graph are
+ * the same.
+ */
+bbop.model.default_predicate = 'points_at';
+
+///
+///  Node sub-object.
+///
+
+/*
+ * Namespace: bbop.model.node
+ * 
+ * Constructor: node
+ * 
+ * Contructor for a BBOP graph model node.
+ * 
+ * Arguments:
+ *  new_id - a unique id for the node
+ *  new_label - *[optional]* a user-friendly description of the node
+ * 
+ * Returns:
+ *  bbop model node
+ */
+bbop.model.node = function(new_id, new_label){
+    this._is_a = 'bbop.model.node';
+    this._id = new_id || undefined;
+    this._label = new_label || undefined;
+
+    // Only have a real type if the constructor went properly.
+    this._type = 'node';
+    if( ! new_id ){
+	this._type = undefined;	
+    }
+
+    this._metadata = undefined;
+};
+
+/*
+ * Function: id
+ *
+ * Getter/setter for node id.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.node.prototype.id = function(value){
+    if(value) this._id = value; return this._id; };
+
+/*
+ * Function: type
+ *
+ * Getter/setter for node type.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.node.prototype.type = function(value){
+    if(value) this._type = value; return this._type; };
+
+/*
+ * Function: label
+ *
+ * Getter/setter for node label.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.node.prototype.label = function(value){
+    if(value) this._label = value; return this._label; };
+
+/*
+ * Function: metadata
+ *
+ * Getter/setter for node metadata.
+ * 
+ * The metadata value does not necessarily have to be an atomic type.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  value
+ */
+bbop.model.node.prototype.metadata = function(value){
+    if(value) this._metadata = value; return this._metadata; };
+
+/*
+ * Function: clone
+ *
+ * Get a fresh new copy of the current node (using bbop.core.clone for
+ * metadata object).
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.node.prototype.clone = function(){
+    var tmp_clone = new bbop.model.node(this.id());
+    tmp_clone.type(this.type());
+    tmp_clone.label(this.label());
+    tmp_clone.metadata(bbop.core.clone(this.metadata()));
+    return tmp_clone;
+};
+
+
+///
+///  Edge sub-object.
+///
+
+/*
+ * Namespace: bbop.model.edge
+ * 
+ * Constructor: edge
+ * 
+ * Contructor for a BBOP graph model edge.
+ * 
+ * If no predicate is given, <default_predicate> is used.
+ * Predicates are currently treated as raw strings.
+ * 
+ * Arguments:
+ *  subject - node id string or node
+ *  object - node id string or node
+ *  predicate - *[optional]* a user-friendly description of the node
+ * 
+ * Returns:
+ *  bbop model edge
+ */
+bbop.model.edge = function(subject, object, predicate){
+    this._is_a = 'bbop.model.edge';
+
+    // Either a string or a node.
+    if( ! subject ){
+	this._subject_id = undefined;
+    }else if( typeof subject == 'string' ){
+	this._subject_id = subject;	
+    }else{
+	this._subject_id = subject.id();
+    }
+    // Either a string or a node.
+    if( ! object ){
+	this._object_id = undefined;
+    }else if( typeof object == 'string' ){
+	this._object_id = object;	
+    }else{
+	this._object_id = object.id();
+    }
+    // Predicate default or incoming.
+    this._predicate_id = bbop.model.default_predicate;
+    if( predicate ){
+	this._predicate_id = predicate;
+    }
+
+    // Only have a real type if the constructor went properly.
+    this._type = 'edge';
+    if( ! subject || ! object ){
+	this._type = undefined;	
+    }
+
+    //
+    this._metadata = undefined;
+};
+
+/*
+ * Function: subject_id
+ *
+ * Getter/setter for edge subject id.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.edge.prototype.subject_id = function(){
+    return this._subject_id; };
+
+/*
+ * Function: object_id
+ *
+ * Getter/setter for edge object id.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.edge.prototype.object_id = function(){
+    return this._object_id; };
+
+/*
+ * Function: predicate_id
+ *
+ * Getter/setter for edge predicate id.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.edge.prototype.predicate_id = function(){
+    return this._predicate_id; };
+
+/*
+ * Function: type
+ *
+ * Getter/setter for edge type.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.edge.prototype.type = function(value){
+    if(value) this._type = value; return this._type; };
+
+/*
+ * Function: metadata
+ *
+ * Getter/setter for edge metadata.
+ *
+ * The metadata value does not necessarily have to be an atomic type.
+ * 
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  value
+ */
+bbop.model.edge.prototype.metadata = function(value){
+    if(value) this._metadata = value; return this._metadata; };
+
+/*
+ * Function: clone
+ *
+ * Get a fresh new copy of the current edge (using bbop.core.clone for
+ * metadata object).
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.edge.prototype.clone = function(){
+    var tmp_clone = new bbop.model.edge(this.subject_id(),
+					this.object_id(),
+					this.predicate_id());
+    // Metadata kind of needs to be duped separately.
+    tmp_clone.metadata(bbop.core.clone(this.metadata()));
+    return tmp_clone;
+};
+
+///
+///  Graph sub-object.
+///
+
+/*
+ * Namespace: bbop.model.graph
+ * 
+ * Constructor: graph
+ * 
+ * Contructor for a BBOP graph model graph.
+ * 
+ * TODO: make compilation piecewise with every added node and edge.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  bbop model node
+ */
+//
+bbop.model.graph = function(){
+    this._is_a = 'bbop.model.graph';
+
+    this._id = undefined;
+
+    // A per-graph logger.
+    this._logger = new bbop.logger(this._is_a);
+    this._logger.DEBUG = true;
+    //this._logger.DEBUG = false;
+    //function ll(str){ anchor._logger.kvetch(str); }
+
+    // For bbop.model.node and bbop.model.edge (hash not possible for
+    // edges--only relation, not "real").
+    this._nodes = { array: new Array, hash: {} };
+    this._edges = { array: new Array };
+    this._predicates = { array: new Array, hash: {} };
+
+    // All things that are referenced by edges (which may not include
+    // actual node ids--dangling links).
+    this._named_nodes = { array: new Array, hash: {} };
+
+    // Useful forthings like leaves, roots, and singletons.
+    this._subjects = { array: new Array, hash: {} };
+    this._objects = { array: new Array, hash: {} };     
+
+    // Table structures for quick lookups of relations.
+    //this._predicate_subject_table = {};    // [pred][sub] -> bbop.model.edge.
+    //this._subject_predicate_table = {};    // [sub][pred] -> bbop.model.edge.
+    //this._predicate_object_table = {};     // [pred][obj] -> sub data struct.
+    //this._object_predicate_table = {};     // [obj][pred] -> sub data struct.
+
+    // New parallel structures to for our simplified graph.
+    this._so_table = {}; // true/undef
+    this._os_table = {}; // true/undef
+    this._sop_table = {}; // {'rel1': true, 'rel2': true}
+
+    // Table structures for quick lookups of node properties.
+    this._is_a_singleton_lookup = {}; // [nid] -> bbop.model.node.    
+};
+
+
+/*
+ * Function: id
+ *
+ * Getter/setter for the graph id.
+ *
+ * Parameters: 
+ *  value - *[optional]* new value for this property to take
+ *
+ * Returns: 
+ *  string
+ */
+bbop.model.graph.prototype.id = function(value){
+    if( value ) this._id = value; return this._id;
+};
+
+/*
+ * Function: add_node
+ *
+ * Add a node to the graph.
+ *
+ * Parameters: 
+ *  node - <node> to add to the graph
+ *
+ * Returns: 
+ *  n/a
+ */
+bbop.model.graph.prototype.add_node = function(node){
+
+    // Check for for anything funny.
+    if( ! node.id() ||
+	this._nodes.hash[ node.id() ] ||
+	this._nodes.hash[ node.id() ] ){
+	    //alert("tried to add same node: " + node.id());
+	    //throw new Error("tried to add same node: " + node.id());
+	}else{
+
+	    var nid = node.id();
+	    
+	    // Add it to all the concerned recall data structures.
+	    this._nodes.hash[ nid ] = node;
+	    this._nodes.array.push(node);
+	    this._named_nodes.hash[ nid ] = node;
+	    this._named_nodes.array.push(node);
+
+	    // If this does not belong to any relation so far, then it is a
+	    // singleton.
+	    if( ! this._subjects.hash[ nid ] && ! this._objects.hash[ nid ] ){
+		this._is_a_singleton_lookup[ nid ] = true;
+	    }
+	}
+};
+
+
+/*
+ * Function: add_edge
+ *
+ * Add an edge to the graph.
+ *
+ * Parameters: 
+ *  edge - <edge> to add to the graph
+ *
+ * Returns: 
+ *  n/a
+ */
+bbop.model.graph.prototype.add_edge = function(edge){
+
+    //
+    var sub_id = edge.subject_id();
+    var obj_id = edge.object_id();
+    var pred_id = edge.predicate_id();
+
+    // Subject -> object.
+    if( ! this._so_table[ sub_id ] ){ this._so_table[ sub_id ] = {}; }
+    this._so_table[ sub_id ][ obj_id ] = true;
+    // Object -> subject.
+    if( ! this._os_table[ obj_id ] ){ this._os_table[ obj_id ] = {}; }
+    this._os_table[ obj_id ][ sub_id ] = true;
+    // Their relationships (defined by SOP).
+    if( ! this._sop_table[ sub_id ] ){
+	this._sop_table[ sub_id ] = {}; }
+    if( ! this._sop_table[ sub_id ][ obj_id ] ){
+	this._sop_table[ sub_id ][obj_id] = {}; }
+    //this._sop_table[ sub_id ][ obj_id ][ pred_id ] = true;
+    this._sop_table[ sub_id ][ obj_id ][ pred_id ] = edge;
+
+    // If this is a new predicate add it to all of the necessary data
+    // structures.
+    if( ! this._predicates.hash[ pred_id ] ){
+	this._predicates.hash[ pred_id ] = true; 
+	this._predicates.array.push(pred_id); 
+    }
+
+    // 
+    if( ! this._subjects.hash[ sub_id ] ){
+	this._subjects.hash[ sub_id ] = true; 
+	this._subjects.array.push(sub_id); 
+	//this._subject_predicate_table[ sub_id ] = {};
+    }
+    if( ! this._objects.hash[ obj_id ] ){
+	this._objects.hash[ obj_id ] = true; 
+	this._objects.array.push(obj_id); 
+	//this._object_predicate_table[ obj_id ] = {};
+    }
+
+    // Remove the edge's subject and object from the singleton table.
+    if( this._is_a_singleton_lookup[ sub_id ] ){
+	delete this._is_a_singleton_lookup[ sub_id ]; }
+    if( this._is_a_singleton_lookup[ obj_id ] ){
+	delete this._is_a_singleton_lookup[ obj_id ]; }
+
+    // Onto the array and subject and object into named bodies.
+    this._edges.array.push(edge);
+    if( ! this._named_nodes.hash[ sub_id ] ){
+	this._named_nodes.array.push(sub_id); }
+    this._named_nodes.hash[ sub_id ] = edge;
+    if( ! this._named_nodes.hash[ obj_id ] ){
+	this._named_nodes.array.push(obj_id); }
+    this._named_nodes.hash[ obj_id ] = edge;
+};
+
+/*
+ * Function: all_nodes
+ *
+ * Returns an /original/ list of all added nodes.
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  array of nodes
+ */
+bbop.model.graph.prototype.all_nodes = function(){
+    return this._nodes.array;
+};
+
+/*
+ * Function: all_edges
+ *
+ * Returns an /original/ list of all added edges.
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  array of edges
+ */
+bbop.model.graph.prototype.all_edges = function(){
+    return this._edges.array;
+};
+
+/*
+ * Function: all_predicates
+ *
+ * Returns an /original/ list of all added predicates.
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  array of predicates (strings)
+ */
+bbop.model.graph.prototype.all_predicates = function(){
+    return this._predicates.array;
+};
+
+/*
+ * Function: all_dangling
+ *
+ * List all external nodes by referenced id.
+ *
+ * Parameters:
+ *  n/a
+ *
+ * Returns: 
+ *  array of extrnal nodes by id
+ */
+bbop.model.graph.prototype.all_dangling = function(){
+    // Disjoint of named and extant.
+    var unnamed = new Array();
+    for( var named_id in this._named_nodes.hash ){
+	if( ! this._nodes.hash[named_id] ){
+	    unnamed.push(named_id);
+	}
+    }
+    return unnamed;
+};
+
+/*
+ * Function: is_complete
+ *
+ * Any bad parts in graph? Essentially, make sure that there are no
+ * weird references and nothing is dangling.
+ *
+ * Parameters:
+ * n/a
+ *
+ * Returns: 
+ *  boolean
+ */
+bbop.model.graph.prototype.is_complete = function(){
+    var retval = true;
+    if( this.all_dangling().length > 0 ){
+	retval = false;
+    }
+    return retval;
+};
+
+/*
+ * Function: get_node
+ *
+ * Return a /copy/ of a node by id (not the original) if extant.
+ *
+ * Parameters:
+ *  nid - the id of the node we're looking for
+ *
+ * Returns: 
+ *  <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_node = function(nid){
+    var retnode = null;
+    if( this._nodes.hash[ nid ] ){
+	var tmp_node = this._nodes.hash[ nid ];
+	retnode = tmp_node.clone();
+    }
+    return retnode;
+};
+
+/*
+ * Function: get_edge
+ *
+ * Return a /copy/ of an edge by ids (not the original) if extant.
+ *
+ * Parameters:
+ *  sub_id - the subject_id of the edge we're looking for
+ *  obj_id - the object_id of the edge we're looking for
+ *  pred - *[optional]* the predicate of the edge we're looking for
+ *
+ * Returns: 
+ *  <bbop.model.edge>
+ */
+bbop.model.graph.prototype.get_edge = function(sub_id, obj_id, pred){	
+
+    if( ! pred ){ pred = bbop.model.default_predicate; }
+
+    var ret_edge = null;
+    if( this._sop_table[sub_id] &&
+	this._sop_table[sub_id][obj_id] &&
+	this._sop_table[sub_id][obj_id][pred] ){
+	    var tmp_edge = this._sop_table[sub_id][obj_id][pred];
+	    ret_edge = tmp_edge.clone();
+	}
+    return ret_edge; 
+};
+
+/*
+ * Function: get_edges
+ *
+ * Return all edges (copies) of given subject and object ids. Returns
+ * entirely new edges.
+ *
+ * Parameters:
+ *  sub_id - the subject_id of the edge we're looking for
+ *  obj_id - the object_id of the edge we're looking for
+ *
+ * Returns: 
+ *  list of <bbop.model.edge>
+ */
+bbop.model.graph.prototype.get_edges = function(sub_id, obj_id){
+    var retlist = new Array();
+    if( this._sop_table[sub_id] &&
+	this._sop_table[sub_id][obj_id] ){
+	    for( var pred in this._sop_table[sub_id][obj_id] ){
+		var found_edge = this._sop_table[sub_id][obj_id][pred];
+		var tmp_edge = found_edge.clone();
+		retlist.push(tmp_edge);
+	    }
+	}		
+    return retlist;
+};
+
+
+/*
+ * Function: get_predicates
+ *
+ * Return all predicates of given subject and object ids.
+ *
+ * Parameters:
+ *  sub_id - the subject_id of the edge we're looking for
+ *  obj_id - the object_id of the edge we're looking for
+ *
+ * Returns: 
+ *  list of predicate ids (as strings)
+ */
+bbop.model.graph.prototype.get_predicates = function(sub_id, obj_id){
+    var retlist = [];
+    if( this._sop_table[sub_id] &&
+	this._sop_table[sub_id][obj_id] ){
+	    for( var pred in this._sop_table[sub_id][obj_id] ){
+		retlist.push(pred);
+	    }
+	}
+    return retlist;
+};
+
+
+/*
+ * Function: edges_to_nodes
+ *
+ * Translate an edge array into extant (node) bodies, switching on
+ * either 'subject' or 'object'.
+ * 
+ * This will return the /original/ nodes.
+ *
+ * This will throw an error on any world issues that crop up.
+ * 
+ * Parameters: 
+ *  in_edges - the edges we want the subjects or objects of
+ *  target - 'subject' or 'object'
+ *
+ * Returns: 
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.edges_to_nodes = function(in_edges, target){
+    
+    // Double check.
+    if( target != 'subject' && target != 'object'){
+	throw new Error('Bad target for edges to bodies.');
+    }
+
+    // 
+    var results = new Array();
+    for( var i = 0; i < in_edges.length; i++ ){ 
+	var in_e = in_edges[i];
+
+	// Switch between subject and object.
+	var target_id = null;
+	if( target == 'subject' ){
+	    target_id = in_e.subject_id();
+	}else{
+	    target_id = in_e.object_id();
+	}
+	
+	//
+	if( target_id && this._nodes.hash[ target_id ] ){
+	    results.push(this._nodes.hash[ target_id ]);
+	}else{
+	    throw new Error(target + ' world issue');
+	}
+    }
+    return results;
+};
+
+/*
+ * Function: is_root_node
+ *
+ * Roots are defined as nodes who are the subject of nothing,
+ * independent of predicate.
+ *
+ * Parameters: 
+ *  nb_id - id of the node to check
+ *
+ * Returns: 
+ *  boolean
+ */
+bbop.model.graph.prototype.is_root_node = function(nb_id){
+    var result = false;	
+    if( this._nodes.hash[ nb_id ] &&
+	! this._subjects.hash[ nb_id ] ){	    
+	    result = true;
+	}
+    return result;
+};
+
+
+/*
+ * Function: get_root_nodes
+ *
+ * Return a list of /copies/ of the root nodes.
+ * 
+ * BUG/TODO: Could I speed this up by my moving some of the
+ * calculation into the add_node and add_edge methods? O(|#nodes|)
+ * 
+ * Parameters:
+ *  n/a 
+ *
+ * Returns:
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_root_nodes = function(){
+    var results = new Array();
+    for( var nb_id in this._nodes.hash ){
+	if( this.is_root_node(nb_id) ){
+	    results.push( this.get_node(nb_id).clone() );
+	}
+    }
+    return results;
+};
+
+
+/*
+ * Function: is_leaf_node
+ *
+ * Leaves are defined as nodes who are the object of nothing,
+ * independent of predicate.
+ * 
+ * Parameters: 
+ *  nb_id - id of the node to check
+ *
+ * Returns: 
+ *  boolean
+ */
+bbop.model.graph.prototype.is_leaf_node = function(nb_id){
+
+    var result = false;
+    if( this._nodes.hash[ nb_id ] &&
+	! this._objects.hash[ nb_id ] ){	    
+	    result = true;
+	}
+    return result;
+};
+
+/*
+ * Function: get_leaf_nodes
+ *
+ * Return a list of /copies/ of the leaf nodes.
+ * 
+ * BUG/TODO: Could I speed this up by my moving some of the
+ * calculation into the add_node and add_edge methods? O(|#nodes|)
+ * 
+ * Parameters:
+ *  n/a 
+ *
+ * Returns:
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_leaf_nodes = function(){
+    var results = new Array();
+    for( var nb_id in this._nodes.hash ){
+	if( this.is_leaf_node(nb_id) ){
+	    results.push( this.get_node(nb_id).clone() );
+	}
+    }
+    return results;
+};
+
+/*
+ * Function: get_singleton_nodes
+ *
+ * Find nodes that are roots and leaves over all relations. This
+ * returns the /original/ node.
+ * 
+ * Throws an error if there is a world issue.
+ *
+ * Parameters:
+ *  n/a 
+ *
+ * Returns: 
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_singleton_nodes = function(){
+    // Translate array into array extant bodies.
+    var singleton_array = new Array();
+    for( var singleton_id in this._is_a_singleton_lookup ){
+	if( this._nodes.hash[ singleton_id ] ){
+	    singleton_array.push( this._nodes.hash[ singleton_id ] );
+	}else{
+	    throw new Error("world issue in get_singletons: "+singleton_id);
+	}
+    }
+    return singleton_array;
+};
+
+/*
+ * Function: get_parent_edges
+ *
+ * Return all parent edges; the /originals/. If no predicate is given,
+ * use the default one.
+ * 
+ * TODO: it might be nice to memoize this since others depend on it.
+ *
+ * Parameters: 
+ *  nb_id - the node to consider
+ *  in_pred - *[optional]* over this predicate
+ *
+ * Returns: 
+ *  array of <bbop.model.edge>
+ */
+bbop.model.graph.prototype.get_parent_edges = function(nb_id, in_pred){
+
+    var results = new Array();
+
+    // Get all parents, or just parents from a specific relation.
+    var preds_to_use = new Array();
+    if( in_pred ){
+	preds_to_use.push(in_pred);
+    }else{
+	preds_to_use = this._predicates.array;
+    }
+
+    // Try all of our desired predicates.
+    for( var j = 0; j < preds_to_use.length; j++ ){
+	var pred = preds_to_use[j];
+
+	// Scan the table for goodies; there really shouldn't be a
+	// lot here.
+	if( this._so_table[ nb_id ] ){		
+	    for( var obj_id in this._so_table[nb_id] ){
+		// If it looks like something is there, try to see
+		// if there is an edge for our current pred.
+		var tmp_edge = this.get_edge(nb_id, obj_id, pred);
+		if( tmp_edge ){
+		    results.push( tmp_edge );
+		}
+	    }
+	}
+    }
+    return results;
+};
+
+/*
+ * Function: get_parent_nodes
+ *
+ * Return all parent nodes; the /originals/. If no predicate is given,
+ * use the default one.
+ * 
+ * Parameters: 
+ *  nb_id - the node to consider
+ *  in_pred - *[optional]* over this predicate
+ *
+ * Returns: 
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_parent_nodes = function(nb_id, in_pred){
+
+    var results = new Array();
+    var edges = this.get_parent_edges(nb_id, in_pred);
+    for( var i = 0; i < edges.length; i++ ){
+	// Make sure that any found edges are in our
+	// world.
+	var obj_id = edges[i].object_id();
+	var tmp_node = this.get_node(obj_id);
+	if( tmp_node ){
+	    results.push( this.get_node(obj_id) );
+	}
+    }
+    return results;
+};
+
+/*
+ * Function: get_child_nodes
+ *
+ * Return all child nodes; the /originals/. If no predicate is given,
+ * use the default one.
+ * 
+ * Parameters: 
+ *  nb_id - the node to consider
+ *  in_pred - *[optional]* over this predicate
+ *
+ * Returns: 
+ *  array of <bbop.model.node>
+ */
+bbop.model.graph.prototype.get_child_nodes = function(nb_id, in_pred){
+
+    var results = new Array();
+
+    // Get all children, or just children from a specific relation.
+    var preds_to_use = new Array();
+    if( in_pred ){
+	preds_to_use.push(in_pred);
+    }else{
+	preds_to_use = this._predicates.array;
+    }
+
+    // Try all of our desired predicates.
+    for( var j = 0; j < preds_to_use.length; j++ ){
+	var pred = preds_to_use[j];
+
+	// Scan the table for goodies; there really shouldn't be a
+	// lot here.
+	if( this._os_table[ nb_id ] ){		
+	    for( var sub_id in this._os_table[nb_id] ){
+		// If it looks like something is there, try to see
+		// if there is an edge for our current pred.
+		if( this.get_edge(sub_id, nb_id, pred) ){
+		    // Make sure that any found edges are in our
+		    // world.
+		    var tmp_node = this.get_node(sub_id);
+		    if( tmp_node ){
+			results.push( this.get_node(sub_id) );
+		    }
+		}
+	    }
+	}
+    }
+    return results;
+};
+
+/*
+ * Function: get_ancestor_subgraph
+ *
+ * Return new ancestors subgraph. Single id or id list as first
+ * argument. Predicate string/id as optional second.
+ *
+ * Parameters: 
+ *  nb_id_or_list - the node id(s) to consider
+ *  pid - *[optional]* over this predicate
+ *
+ * Returns: 
+ *  <bbop.model.graph>
+ */
+bbop.model.graph.prototype.get_ancestor_subgraph = function(nb_id_or_list, pid){
+
+    // Shared data structure to trim multiple paths.
+    // Nodes: color to get through the graph quickly and w/o cycles.
+    var seen_node_hash = {};
+    // Edges: just listed--hashing would be essentially the same
+    // as a call to graph.add_edge (I think--benchmark?).
+    var seen_edge_list = [];
+    var anchor = this;
+
+    // Define recursive ascent.
+    function rec_up(nid){
+
+	//print('rec_up on: ' + nid);
+
+    	var results = new Array();
+    	var new_parent_edges = anchor.get_parent_edges(nid, pid);
+
+	// Capture edge list for later adding.
+	for( var e = 0; e < new_parent_edges.length; e++ ){
+	    seen_edge_list.push(new_parent_edges[e]);
+	}
+
+	// Pull extant nodes from edges. NOTE: This is a retread
+	// of what happens in get_parent_nodes to avoid another
+	// call to get_parent_edges (as all this is now
+	// implemented).
+	var new_parents = new Array();
+	for( var n = 0; n < new_parent_edges.length; n++ ){
+	    // Make sure that any found edges are in our
+	    // world.
+	    var obj_id = new_parent_edges[n].object_id();
+	    var temp_node = anchor.get_node(obj_id);
+	    if( temp_node ){
+		new_parents.push( temp_node );
+	    }
+	}
+
+	// Make sure we're in there too.
+	var tmp_node = anchor.get_node(nid);
+	if( tmp_node ){
+	    new_parents.push( tmp_node );
+	}
+
+	// Recur on unseen things and mark the current as seen.
+    	if( new_parents.length != 0 ){
+    	    for( var i = 0; i < new_parents.length; i++ ){
+    		// Only do things we haven't ever seen before.
+    		var new_anc = new_parents[i];
+    		var new_anc_id = new_anc.id();
+    		if( ! seen_node_hash[ new_anc_id ] ){
+    		    seen_node_hash[ new_anc_id ] = new_anc;
+    		    rec_up(new_anc_id);	
+    		}
+    	    }
+    	}
+    	return results;
+    }
+    
+    // Recursive call and collect data from search. Make multiple
+    // ids possible.
+    //if( nb_id_or_list.length && nb_id_or_list.index ){
+    if( bbop.core.is_array(nb_id_or_list) ){ // verify listy-ness
+	for( var l = 0; l < nb_id_or_list.length; l++ ){	    
+	    rec_up(nb_id_or_list[l]);
+	}
+    }else{
+    	rec_up(nb_id_or_list);
+    }
+    
+    // Build new graph using data.
+    var new_graph = new bbop.model.graph();
+    for( var k in seen_node_hash ){
+	new_graph.add_node(seen_node_hash[k]);
+    }
+    for( var x = 0; x < seen_edge_list.length; x++ ){	    
+	new_graph.add_edge(seen_edge_list[x]);
+    }
+
+    return new_graph;
+};
+
+/*
+ * Function: load_json
+ * 
+ * Load the graph from the specified JSON object (not string).
+ * 
+ * TODO: a work in progress
+ * 
+ * Parameters:
+ *  JSON object
+ * 
+ * Returns:
+ *  true; side-effects: creates the graph internally
+ */
+bbop.model.graph.prototype.load_json = function(json_object){
+
+    var anchor = this;
+
+    // First, load nodes; scrape out what we can.
+    if( json_object.nodes ){
+	bbop.core.each(json_object.nodes,
+		       function(node_raw){
+			   var nid = node_raw.id;
+			   var nlabel = node_raw.lbl;
+			   var n = new bbop.model.node(nid, nlabel);
+			   if(node_raw.meta){ n.metadata(node_raw.meta); }
+			   anchor.add_node(n);
+		       });
+    }
+
+    // Now try to load edges; scrape out what we can.
+    if( json_object.edges ){
+	bbop.core.each(json_object.edges,
+		       function(edge_raw){
+			   var e = new bbop.model.edge(edge_raw.sub,
+						       edge_raw.obj,
+						       edge_raw.pred);
+			   // Copy out meta.
+			   if(edge_raw.meta){ e.metadata(edge_raw.meta); } 
+			   
+			   anchor.add_edge(e);
+		      });
+    }
+
+    return true;
+};
+/* 
+ * Package: tree.js
+ * 
+ * Namespace: bbop.model.tree
+ * 
+ * Purpose: Extend <bbop.model> in <model.js> to be handy for a (phylo)tree.
+ * 
+ * TODO: /Much/ better documentation. I have no idea what's going on
+ * in there anymore...
+ * 
+ * TODO: See: http://raphaeljs.com/graffle.html
+ * 
+ * TODO: Subtree calculation during bracket_down.
+ */
+
+// Module and namespace checking.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'model');
+bbop.core.namespace('bbop', 'model', 'tree');
+
+
+// // BUG/TODO: remove later...or something...
+// bbop.model.tree.logger = new bbop.logger();
+// bbop.model.tree.logger.DEBUG = true;
+// var _kvetch = bbop.model.tree.logger.kvetch;
+
+/*
+ * Namespace: bbop.model.tree.node
+ * 
+ * Constructor: node
+ * 
+ * Same as parent, but just takes id in constructor.
+ * 
+ * Arguments:
+ *  new_id - a unique id for the node
+ */
+bbop.model.tree.node = function(new_id){
+    bbop.model.node.call(this, new_id);
+    this._is_a = 'bbop.model.tree.node';
+};
+bbop.core.extend(bbop.model.tree.node, bbop.model.node);
+
+/*
+ * Namespace: bbop.model.tree.edge
+ * 
+ * Constructor: edge
+ * 
+ * Same as parent class, but optionally adds distance as an argument.
+ */
+bbop.model.tree.edge = function(parent, child, distance){
+    bbop.model.edge.call(this, child, parent, '');
+    this._is_a = 'bbop.model.tree.edge';
+    this._distance = distance || 0.0;
+};
+bbop.core.extend(bbop.model.tree.edge, bbop.model.edge);
+
+/*
+ * Function: distance
+ *
+ * Return an edge's "distance".
+ *
+ * Parameters:
+ *  value - *[optional]* new number for this property to take
+ *
+ * Returns: 
+ *  number
+ */
+bbop.model.tree.edge.prototype.distance = function(d){
+    if(d){ this._distance = d; }
+    return this._distance;
+};
+
+/*
+ * Function: clone
+ *
+ * Make sure that clone gets distance as well.
+ *
+ * Parameters: 
+ *  n/a
+ *
+ * Returns: 
+ *  <bbop.model.tree.edge>
+ */
+bbop.model.tree.edge.prototype.clone = function(){
+    var tmp_clone = new bbop.model.tree.edge(this.object_id(),
+					     this.subject_id(),
+					     this.distance());
+    tmp_clone.metadata(bbop.core.clone(this.metadata()));
+    return tmp_clone;
+};
+
+/*
+ * Namespace: bbop.model.tree.graph
+ * 
+ * Constructor: graph
+ * 
+ * Same as parent.
+ * Needs some more functionality...
+ */
+bbop.model.tree.graph = function(){
+    bbop.model.graph.call(this);
+    this._is_a = 'bbop.model.tree.graph';
+
+    // Useful for making sure that certain recursive functions keep
+    // the desired notion of "this"ness.
+    var anchor = this;
+
+    /*
+     * Function: default_sort
+     *
+     * The default comparator function for ordering the
+     * brackets. Alphabetical down.
+     * 
+     * Parameters: 
+     *  a - a bracket item
+     *  b - a bracket item
+     *
+     * Returns: 
+     *  string
+     */
+    this.default_sort = function(a, b){
+	var sort_val = 0;
+	if( a.id < b.id ){
+	    sort_val = - 1;
+	}else if( a.id > b.id ){
+	    sort_val = 1;
+	}
+	//_kvetch('sort: ' + a.id + ' <?> ' + b.id + ' = ' + sort_val);
+	return sort_val;
+    };
+
+    // Get information on kids, relations, and distances working our
+    // way up from the leaves.
+    var max_dist = 0.0;
+    var all_dists_parent = {};
+    var all_dists_child = {};
+    var node_list = new Array();
+    var node_hash = {};
+    var edge_list = new Array();
+    var edge_hash = {};
+    function info_up(node_id){
+	
+	var nid = node_id;
+	//_kvetch("info_up: working on: " + nid);
+
+	// Node bookkeeping.
+	if( ! node_hash[nid] ){
+	    node_hash[nid] = true;
+	    node_list.push(nid);
+	}
+
+	// Can only have at most one parent.
+	var node_parent = anchor.get_parent_nodes(nid);
+	if( node_parent && node_parent.length ){
+	    node_parent = node_parent[0];
+	    var pid = node_parent.id();
+
+	    // Edge bookkeeping.
+	    var edge_uid = pid + '_!muNge!_' + node_id;
+	    if( ! edge_hash[edge_uid] ){
+		edge_hash[edge_uid] = true;
+		edge_list.push([pid, node_id]);
+		//_kvetch('info_up: indexing: ' + edge_uid);
+	    }
+
+	    // Add new data to globals.
+	    //_kvetch(" info_up: seems to have parent: " + pid);
+	    if( ! all_dists_parent[pid]){
+		all_dists_parent[pid] = {};
+	    }
+	    if( ! all_dists_child[nid]){
+		all_dists_child[nid] = {};
+	    }
+
+	    if( ! all_dists_parent[pid][nid] ){
+		// 
+		var dist = anchor.get_edge(nid,pid).distance();
+		all_dists_parent[pid][nid] = dist;
+		all_dists_child[nid][pid] = dist;
+		// Look a little for max.
+		if( dist > max_dist ){
+		    max_dist = dist;
+		}
+	    }
+
+	    // Get any data you can from your kids.
+	    for( var k_id in all_dists_parent[nid] ){
+
+		var increment = all_dists_parent[pid][nid] +
+		    all_dists_parent[nid][k_id];
+		all_dists_parent[pid][k_id] = increment;
+		all_dists_child[k_id][pid] = increment;
+
+		// Look a little for max.
+		if( increment > max_dist ){
+		    max_dist = increment;
+		}
+	    }
+
+	    // Recur on parent.
+	    info_up(pid);
+	}
+    }
+
+    // Recursive comb down (give partitioned ordering).
+    // A bracket looks like: "[{id:a, brackets: [...]}, ...]".
+    // TODO: subtree calculation during.
+    var brackets = new Array();
+    var max_depth = 0;
+    function bracket_down(in_node, lvl, parent_node_id){
+	    
+	// Bootstrap lvl to 1.
+	if( ! lvl ){ lvl = 1; }
+	if( ! parent_node_id ){ parent_node_id = null; }
+
+	var in_node_id = in_node.id();
+	//_kvetch(' bracket_down: ' + in_node_id);
+
+	// 
+	var child_bracket = new Array();
+	var child_nodes = anchor.get_child_nodes(in_node_id);
+	for( var cb = 0; cb < child_nodes.length; cb++ ){
+	    var child_node = child_nodes[cb];
+	    var child_node_id = child_node.id();
+	    //_kvetch('  bracket_down: pushing: ' + child_node_id);
+	    child_bracket.push(bracket_down(child_node, lvl + 1, in_node_id));
+	}
+
+	// Sort the children.
+	child_bracket.sort(anchor.default_sort);
+
+	// Grab max depth.
+	if( lvl > max_depth ){ max_depth = lvl;	}
+
+	//
+	//_kvetch(' bracket_down: found kids: ' + child_bracket.length);
+	return {
+	    id: in_node_id,
+	    routing_node: false,
+	    level: lvl,
+	    parent_id: parent_node_id,
+	    brackets: child_bracket
+	};
+    }
+
+    // Return a layout that can be trivially rendered
+    // by...something...
+    var max_width = 0;
+    var cohort_list = new Array(); // will reinit
+
+    /*
+     * Function: layout
+     *
+     * With the current graph, produce a usable layout object.
+     * 
+     * TODO: layout should take bracket ordering func
+     *
+     * Parameters:
+     *  n/a
+     *
+     * Returns: 
+     *  a rather complicated layout object
+     */
+    this.layout = function (){
+
+	// Refresh scope on new layout call.
+	brackets = new Array();
+	node_list = new Array();
+	node_hash = {};
+	edge_list = new Array();
+	edge_hash = {};
+	cohort_list = new Array(); // token--now also reset and sized below
+
+	// Pass one:
+	// Collect all of our bracketing information, also order the
+	// brackets to some function.
+	var base_nodes = anchor.get_root_nodes();
+	for( var bb = 0; bb < base_nodes.length; bb++ ){
+	    //_kvetch('bracket_down: start: ' + base_nodes[bb].id());
+	    brackets.push(bracket_down(base_nodes[bb]));
+	}
+	// The children are ordered--make the top-level one ordered as
+	// well.
+	brackets.sort(anchor.default_sort);
+
+	// Pass one:
+	// Essentially walk the brackets, find brackets that end early
+	// (above max_depth) and add routing nodes down.
+	function dangle_routing(in_item){
+	    if( in_item.level < max_depth ){
+		in_item.brackets.push({id: in_item.id,
+				       routing_node: true,
+				       level: in_item.level + 1,
+				       parent_id: in_item.id,
+				       brackets: []
+				      });
+		dangle_routing(in_item.brackets[0]);
+	    }
+	    return in_item;
+	}
+	function add_routing(in_brackets){
+
+	    //
+	    for( var i = 0; i < in_brackets.length; i++ ){
+		var item = in_brackets[i];
+
+		//
+		if( item.brackets.length == 0 && item.level < max_depth ){
+		    //_kvetch(' add_routing: dangle: ' + item.id);
+		    dangle_routing(item);
+		}else if( item.brackets.length != 0 ){
+		    //_kvetch(' add_routing: descend: ' + item.id);
+		    add_routing(item.brackets);
+		}
+	    }
+	}
+	add_routing(brackets);
+
+	// Pass three:
+	// Collect global cohort information into a matrix (cohort_list).
+	cohort_list = new Array(max_depth);
+	for( var cli = 0; cli < cohort_list.length; cli++ ){
+	    cohort_list[cli] = new Array();
+	}
+	// Walk down and stack up.
+	function order_cohort(in_brackets){	    
+	    // Push into global cohort list list.
+	    for( var i = 0; i < in_brackets.length; i++ ){
+		var bracket_item = in_brackets[i];
+		//
+		//_kvetch(' order_cohort: i: ' + i);
+		//_kvetch(' order_cohort: lvl: ' + bracket_item.level);
+		cohort_list[bracket_item.level - 1].push(bracket_item);
+		// Drill down.
+		if( bracket_item.brackets.length > 0 ){
+		    //_kvetch(' order_cohort: down: ' +
+		    //        bracket_item.brackets.length);
+		    order_cohort(bracket_item.brackets);
+		}
+	    }
+	}
+	order_cohort(brackets);
+
+	// Gather distance info up from leaves.
+	var base_info_nodes = anchor.get_leaf_nodes();
+	max_width = base_info_nodes.length; // by def, leaves are widest
+	for( var bi = 0; bi < base_info_nodes.length; bi++ ){
+	    info_up(base_info_nodes[bi].id());
+	}
+
+	///
+	/// Decide relative y positions by walking backwards through
+	/// the cohorts.
+	///
+
+
+	// Walk backwards through the cohorts to find a base Y position. for
+	// the final cohort.
+	var position_y = {};
+	var final_cohort = cohort_list[(max_depth - 1)];
+	//_kvetch('look at final cohort: ' + (max_depth - 1));
+	for( var j = 0; j < final_cohort.length; j++ ){
+	    var f_item = final_cohort[j];
+	    //var local_shift = j + 1.0; // correct, but shifts too far down
+	    var local_shift = j + 0.0;
+	    position_y[f_item.id] = local_shift;
+	    //_kvetch('position_y: ' + f_item.id + ', ' + local_shift);
+	}
+	// Walk backwards through the remaining cohorts to find the best Y
+	// positions.
+	for( var i = cohort_list.length - 1; i > 0; i-- ){
+	    //
+	    var cohort = cohort_list[i - 1];
+	    //_kvetch('look at cohort: ' + (i - 1));
+	    for( var k = 0; k < cohort.length; k++ ){
+		var item = cohort[k];
+
+		// Deeper placements always take precedence.
+		if( position_y[item.id] != undefined ){
+		    //_kvetch('position_y (old): '+ item.id);
+		}else{
+
+		    // If you have one parent, they have the same Y as you.
+		    // This generalizes to: the parent has the average Y of
+		    // it's children. This is easy then, once we
+		    // start, but how to get the initial leaf
+		    // placement? Get item's children and take their
+		    // average (by definition, they must already be in
+		    // the placed list (even if it was just a routing
+		    // node)).
+		    var c_nodes = anchor.get_child_nodes(item.id);
+		    var position_acc = 0.0;
+		    for( var ci = 0; ci < c_nodes.length; ci++ ){
+			var node = c_nodes[ci];
+			position_acc = position_acc + position_y[node.id()];
+		    }
+		    // _kvetch(' position_acc: ' + position_acc);
+		    // _kvetch(' c_nodes: ' + c_nodes);
+		    // _kvetch(' c_nodes.length: ' + c_nodes.length);
+		    var avg = position_acc / (c_nodes.length * 1.0);
+		    position_y[item.id] = avg;
+		    //_kvetch('position_y (new): '+ item.id +', '+ avg);
+		}
+	    }
+	}
+ 
+	//
+	var x_offset = 0.0;
+	var position_x = {};
+	var roots = anchor.get_root_nodes();
+	for( var r = 0; r < roots.length; r++ ){
+
+	    var root_id = roots[r].id();
+	    position_x[root_id] = x_offset;
+	    //_kvetch('position_x:: ' + root_id + ', ' + position_x[root_id]);
+    
+	    if( item.routing_node == false ){
+		// Get kids and their x distance (for placement).
+		for( var nid in all_dists_parent[root_id] ){
+		    var dist = all_dists_parent[root_id][nid] + x_offset;
+		    position_x[nid] = dist;
+		    //_kvetch('position_x:: ' + nid + ', ' + dist);
+		}
+	    }
+	}
+
+	//
+	return {
+	    parent_distances: all_dists_parent,
+	    child_distances: all_dists_child,
+	    max_distance: max_dist,
+	    max_depth: max_depth,
+	    max_width: max_width,
+	    cohorts: cohort_list,
+	    //routing: routing_list,
+	    brackets: brackets,
+	    node_list: node_list,
+	    edge_list: edge_list,
+	    position_x: position_x,
+	    position_y: position_y
+	};
+    };
+
+    /*
+     * Function: dump_cohorts
+     *
+     * Dump the cohorts; for debugging?
+     *
+     * Parameters:
+     *  n/a
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.dump_cohorts = function(){
+    	for( var i = 0; i < cohort_list.length; i++ ){
+    	    for( var j = 0; j < cohort_list[i].length; j++ ){
+    		var item = cohort_list[i][j];
+    		//_kvetch(item.id + ' ' + i +':'+ j + ', ' + item.routing_node);
+    	    }
+    	}
+    };
+
+    /*
+     * Function: dump_dist
+     *
+     * Dump distances; for debugging?
+     *
+     * Parameters: 
+     *  in_arg - string; 'child'/'parent'?
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.dump_dist = function(in_arg){
+
+	//_kvetch(' in ');
+
+	// Argument selection.
+	var dists = all_dists_parent;
+	if( in_arg == "child" ){
+	    dists = all_dists_child;
+	}
+
+	// Dump selected dist.
+	for( var n_id in dists ){
+	    for( var k_id in dists[n_id] ){
+		//_kvetch(n_id +' : '+ k_id +' => '+ dists[n_id][k_id]);
+	    }
+	}
+    };
+
+    /*
+     * Function: dump_brackets
+     *
+     * Dump brackets; for debugging?
+     *
+     * Parameters: 
+     *  brack - *[optional]* ???
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.dump_brackets = function(brack){
+
+	// Bootstrap if just starting.
+	if( ! brack ){ brack = brackets; }
+	//if( ! lvl ){ lvl = 1; }
+
+	// Printer.
+	for( var i = 0; i < brack.length; i++ ){
+
+	    var pid = '(null)';
+	    if( brack[i].parent_id ){ pid = brack[i].parent_id; }
+
+	    // _kvetch('id: ' + brack[i].id +
+	    // 		     ', parent: ' + pid +
+	    // 		     ', level: ' + brack[i].level);
+	    this.dump_brackets(brack[i].brackets);
+	}
+    };
+
+};
+bbop.core.extend(bbop.model.tree.graph, bbop.model.graph);
+// Overload add_node to add label information to new object.
+/* 
+ * Package: bracket.js
+ * 
+ * Namespace: bbop.model.bracket.graph
+ * 
+ * Purpose: An extension of <bbop.model.graph> to produce a bracketed
+ * layout (like the neighborhood view in AmiGO 1.8).
+ * 
+ * TODO: A work in progress...
+ */
+
+// Module and namespace checking.
+bbop.core.require('bbop', 'core'); // not needed, but want the habit
+bbop.core.require('bbop', 'model');
+bbop.core.namespace('bbop', 'model', 'bracket', 'graph');
+
+/*
+ * Namespace: bbop.model.bracket.graph
+ * 
+ * Constructor: bracket
+ * 
+ * Extension of <bbop.model.graph>
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  this
+ */
+bbop.model.bracket.graph = function(){
+    bbop.model.graph.call(this);
+    this._is_a = 'bbop.model.bracket.graph';
+
+    var anchor = this;
+    var loop = bbop.core.each;
+    anchor._logger.DEBUG = true;
+    function ll(str){ anchor._logger.kvetch(str); }
+
+    /*
+     * Function: bracket_layout
+     *
+     * Largely borrowed from ChewableGraph.pm from the perl section on
+     * AmiGO 2.
+     * 
+     * Produces a simple bracketed layout based on the maximum
+     * distance from the node-of-interest to all other nodes. It also
+     * includes direct children as the last row. Useful in some layout
+     * contexts.
+     *
+     * Any node in a properly made graph should be fine, but for the
+     * usual end case, see <rich_bracket_layout>.
+     * 
+     * Parameters: 
+     *  term_acc - node of interest
+     *
+     * Returns: 
+     *  list of lists or id strings [[id1, id2], ...]
+     */
+    this.bracket_layout = function(term_acc){
+	
+	// This is the actual path climbing agent.
+	function max_info_climber(in_curr_term, in_curr_term_dist,
+				  in_max_hist, in_enc_hist){
+
+	    // We either bootstrap (first run) or pull them in.
+	    var curr_term = in_curr_term || term_acc;
+	    var curr_term_distance = in_curr_term_dist || 0;
+	    var max_hist = in_max_hist || {};
+	    var encounter_hist = in_enc_hist || {};
+
+	    // ll('looking at: ' + curr_term + ' at ' + curr_term_distance);
+
+	    // Only recur if our encounter history sez that either
+	    // this node is new or if we have a higher distance count
+	    // (in which case we add it and continue on our merry
+	    // way).
+	    if( ! bbop.core.is_defined(encounter_hist[curr_term]) ){
+		// ll(curr_term + ' is a new encounter at distance ' +
+		//    curr_term_distance);
+
+		// Note that we have encountered this node before.
+		encounter_hist[curr_term] = 1;
+
+		// Our first distance is the current one!
+		max_hist[curr_term] = curr_term_distance;
+
+		// Increment our distance.
+		curr_term_distance++;
+
+		// Take a look at all the parents of our current term.
+		loop(anchor.get_parent_nodes(curr_term),
+		     function(p){
+			 // Since this is a new node encounter, let's
+			 // see what else is out there to discover.
+			 max_info_climber(p.id(), curr_term_distance,
+					  max_hist, encounter_hist);
+		     });
+
+	    }else if( encounter_hist[curr_term] ){
+		// ll(curr_term + ' has been seen before');
+
+		// If we're seeing this node again, but with a
+		// separate history, we'll add the length or our
+		// history to the current, but will not recur in any
+		// case (we've been here before).
+		if( max_hist[curr_term] < curr_term_distance ){
+		    // ll(curr_term +' has a new max of '+ curr_term_distance);
+		    max_hist[curr_term] = curr_term_distance;
+		}
+	    }
+
+	    // Return the collected histories.
+	    return max_hist;
+	}
+	// A hash of the maximum distance from the node-in-question to
+	// the roots.
+	var max_node_dist_from_root = max_info_climber();
+	// ll('max_node_dist_from_root: ' +
+	//    bbop.core.dump(max_node_dist_from_root));
+
+	///
+	/// Convert this into something like brackets.
+	///
+
+	// First, invert hash.
+	// E.g. from {x: 1, y: 1, z: 2} to {1: [x, y], 2: [z]} 
+	var lvl_lists = {};
+	loop(max_node_dist_from_root,
+	    function(node_id, lvl){
+		// Make sure that level is defined before we push.
+		if( ! bbop.core.is_defined(lvl_lists[lvl]) ){
+		    lvl_lists[lvl] = [];
+		}
+
+		lvl_lists[lvl].push(node_id);
+	    });
+	// ll('lvl_lists: ' + bbop.core.dump(lvl_lists));
+
+	// Now convert the level-keyed hash into an array of arrays.
+	// E.g. from {1: [x, y], 2: [z]} to [[x, y], [z]]
+	var bracket_list = [];
+	var levels = bbop.core.get_keys(lvl_lists);
+	levels.sort(bbop.core.numeric_sort_ascending);
+	// ll('levels: ' + bbop.core.dump(levels));
+	loop(levels,
+	    function(level){
+		var bracket = [];
+		loop(lvl_lists[level],
+		     function(item){
+			 bracket.push(item);
+		     });
+		bracket_list.push(bracket);
+	    });
+	bracket_list.reverse(); // ...but I want the opposite
+	// ll('bracket_list: ' + bbop.core.dump(bracket_list));
+
+	// Well, that takes care of the parents, now lets do the
+	// trivial task of adding all of the kids (if any).
+	var c_nodes = anchor.get_child_nodes(term_acc);
+	// Only add another level when there are actually kids.
+	if( c_nodes && ! bbop.core.is_empty(c_nodes) ){ 
+	    var kid_bracket = [];
+	    loop(c_nodes,
+		 function(c){
+		     kid_bracket.push(c.id());
+		 });
+	    bracket_list.push(kid_bracket);
+	}
+
+	return bracket_list;
+    };
+
+    /*
+     * Function: relation_weight
+     *
+     * A GO-specific take on the relative importance of relations in a
+     * graph.
+     * 
+     * Parameters: 
+     *  predicate_acc - as string
+     *  default_weight - *[optional]* as numbrt
+     *
+     * Returns: 
+     *  relative weight of predicate as number; defaults to 0
+     */
+    this.relation_weight = function(predicate_acc, default_weight){
+
+	var rel = predicate_acc || '';
+	var dflt = default_weight || 0;
+	var order =
+	    {
+		is_a: 1,
+		has_part: 2,
+		part_of: 3,
+		regulates: 4,
+		negatively_regulates: 5,
+		positively_regulates: 6
+	    };
+
+	var ret_weight = dflt;
+	if( bbop.core.is_defined(rel) &&
+	    rel &&
+	    bbop.core.is_defined(order[rel]) ){
+	    ret_weight = order[rel];
+	}
+
+	return ret_weight;
+    };
+
+    /*
+     * Function: dominant_relationship
+     *
+     * Given a bunch of relationships, return the one that is more
+     * "dominant".
+     * 
+     * A GO-specific take on the relative importance of relations in a
+     * graph.
+     * 
+     * Parameters: 
+     *  whatever - predicate acc, or lists of lists them...whatever
+     *
+     * Returns: 
+     *  string acc of the dominant relationship or null
+     * 
+     * See also:
+     *  <relationship_weight>
+     */
+    this.dominant_relationship = function(){
+
+	// Collect all of the relations, recursively unwinding as
+	// necessary to get to the end of the arguments/lists of
+	// predicate accs.
+	// WARNING: Do /not/ try to refactor this for loop--see the
+	// documentation for each for the reason.
+	var all_rels = [];
+	for( var dri = 0; dri < arguments.length; dri++ ){
+	    var arg = arguments[dri];
+	    //ll('ARG: ' + arg);
+	    if( bbop.core.what_is(arg) === 'array' ){
+		// This funny thing is actually "dereferencing" the
+		// array one step for the recursion.
+		all_rels.push(this.dominant_relationship.apply(this, arg));
+	    }else{
+		all_rels.push(arg);
+	    }
+	}
+	
+	// Sort all of the remaining predicate accs according to
+	// relation_weight.
+	all_rels.sort(function(a, b){
+			  return anchor.relation_weight(b) -
+			      anchor.relation_weight(a);
+		      });
+
+	// Choose the top if it's there, null otherwise.
+	var retval = null;
+	if( all_rels.length ){
+	    retval = all_rels[0];
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: rich_bracket_layout
+     *
+     * Very similar to <bracket_layout>, except that instead of the
+     * node id, there is a list of [node_id, node_label, predicate].
+     * 
+     * This is only reliably producable if the following two condition
+     * is met: the transitivity graph is the one made for the node of
+     * interest by the GOlr loading engine. This is easy to meet if
+     * using GOlr, but probably better to roll your own if you're not.
+     * 
+     * Also, the relative weight of the relations used is very
+     * GO-specific--see <relation_weight>.
+     * 
+     * Again, heavy borrowing from ChewableGraph.pm from the perl
+     * section in AmiGO 2.
+     * 
+     * Parameters: 
+     *  term_acc - node of interest
+     *  transitivity_graph - the <bbop.model.graph> for the relations
+     *
+     * Returns: 
+     *  list of lists of lists: [[[id, label, predicate], ...], ...]
+     */
+    this.rich_bracket_layout = function(term_acc, transitivity_graph){
+	
+	// First, lets just get our base bracket layout.
+	var layout = anchor.bracket_layout(term_acc);
+
+	// So, let's go through all the rows, looking on the
+	// transitivity graph to see if we can find the predicates.
+	var bracket_list = [];
+	loop(layout,
+	    function(layout_level){
+		var bracket = [];
+		loop(layout_level,
+		     function(layout_item){
+
+			 // The defaults for what we'll pass back out.
+			 var curr_acc = layout_item;
+			 var pred_id = 'is_a';			 
+			 var curr_node = anchor.get_node(curr_acc);
+			 var label = curr_node.label() || layout_item;
+
+			 // 
+
+			 // Now we just have to determine
+			 // predicates. If we're the one, we'll just
+			 // use the defaults.
+			 if( curr_acc == term_acc ){
+			     // Default.
+			 }else{
+			     // Since the transitivity graph only
+			     // stores ancestors, we can also use it
+			     // to passively test if these are
+			     // children we should be looking for.
+			     var trels =
+				 transitivity_graph.get_predicates(term_acc,
+								   curr_acc);
+			     if( ! bbop.core.is_empty(trels) ){
+				 // Not children, so decide which of
+				 // the returned edges is the best.
+				 pred_id = anchor.dominant_relationship(trels);
+			     }else{
+				 // Probably children, so go ahead and
+				 // try and pull the direct
+				 // parent/child relation.
+				 var drels = anchor.get_predicates(curr_acc,
+								   term_acc);
+				 pred_id = anchor.dominant_relationship(drels);
+			     }
+			 }
+
+			 // Turn our old layout item into a new-info
+			 // rich list.
+			 bracket.push([curr_acc, label, pred_id]);
+		     });
+		// Sort alphanum and then re-add to list.
+		bracket.sort(
+		    function(a, b){
+			if( a[1] < b[1] ){
+			    return -1;
+			}else if( a[1] > b[1] ){
+			    return 1;
+			}else{
+			    return 0;
+			}
+		    });
+		bracket_list.push(bracket);
+	    });
+
+	return bracket_list;
+    };
+};
+bbop.core.extend(bbop.model.bracket.graph, bbop.model.graph);
+/* 
+ * Package: conf.js
+ * 
+ * Generic BBOP manager for dealing with gross GOlr configuration
+ * and management.
+ * 
+ * Contains <bbop.golr.conf_field>, <bbop.golr.conf_class>, and
+ * <bbop.golr.conf>.
+ * 
+ * TODO: better document all of this. Essentially, this is all for
+ * getting data out of a JSONized version of the YAML files used to
+ * drive the OWLTools-Solr parts of GOlr.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'golr');
+
+/*
+ * Namespace: bbop.golr.conf_field
+ * 
+ * Constructor: conf_field
+ * 
+ * Contructor for a GOlr search field.
+ * 
+ * Arguments:
+ *  field_conf_struct - JSONized config
+ * 
+ * Returns:
+ *  conf_field object
+ */
+bbop.golr.conf_field = function (field_conf_struct){
+    this._is_a = 'bbop.golr.conf_field';
+
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger.
+    var logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    // Capture search fields.
+    this._field = field_conf_struct;
+
+    /*
+     * Function: display_name
+     * 
+     * The user-facing display name. Suitable for label or title
+     * somewhere.
+     * 
+     * Returns:
+     *  Display name string.
+     */
+    this.display_name = function(){
+	return this._field['display_name'];
+    };
+
+    /*
+     * Function: description
+     * 
+     * A longer description. Suitable for tooltips.
+     * 
+     * Returns:
+     *  Description string.
+     */
+    this.description = function(){
+	return this._field['description'];
+    };
+
+    /*
+     * Function: id
+     * 
+     * The unique ID of this profile.
+     * 
+     * Returns:
+     *  String.
+     */
+    this.id = function(){
+	return this._field['id'];
+    };
+
+    /*
+     * Function: searchable
+     * 
+     * Returns whether or not a string field has a shadow
+     * "*_searchable" field defined that is suitable for dismax
+     * searches. Defaults to false.
+     * 
+     * Returns:
+     *  boolean
+     */
+    this.searchable = function(){
+	var retval = false;
+	if( this._field['searchable'] == 'true' ||
+	    this._field['searchable'] == true ){
+		retval = true;	
+	    }
+	return retval;
+    };
+
+    /*
+     * Function: required
+     * 
+     * Returns whether or not this field is required. Defaults to
+     * false.
+     * 
+     * Not of particular use.
+     * 
+     * Returns:
+     *  Boolean.
+     */
+    this.required = function(){
+	var retval = false;
+	if( this._field['required'] == 'true' ||
+	    this._field['required'] == true ){
+		retval = true;	
+	    }
+	return retval;
+    };
+
+    /*
+     * Function: is_multi
+     * 
+     * Using the "cardinality" entry, returns whether or not this
+     * field is "single" (false) or "multi" (true). Defaults to false.
+     * 
+     * Returns:
+     *  Boolean.
+     */
+    this.is_multi = function(){
+	var retval = false;
+	if( this._field['cardinality'] == 'multi' ){
+	    retval = true;	
+	}
+	return retval;
+    };
+
+    /*
+     * Function: is_fixed
+     * 
+     * Using the "property_type" entry, returns whether or not this
+     * field is "dynamic" (false) or "fixed" (true). Defaults to false.
+     * 
+     * Not of particular use.
+     * 
+     * Returns:
+     *  Boolean.
+     */
+    this.is_fixed = function(){
+	var retval = false;
+	if( this._field['property_type'] == 'fixed' ){
+	    retval = true;	
+	}
+	return retval;
+    };
+
+    /*
+     * Function: property
+     * 
+     * Returns the method of this field's generation in the loader.
+     * 
+     * Not of particular use.
+     * 
+     * Returns:
+     *  String.
+     */
+    this.property = function(){
+	var retval = '???';
+	if( this._field['property'] ){
+	    retval = this._field['property'];
+	}
+	return retval;
+    };
+
+    // TODO: ...
+};
+
+/*
+ * Namespace: bbop.golr.conf_class
+ *
+ * Constructor: conf_class
+ * 
+ * Contructor for a GOlr search class.
+ * 
+ * Arguments:
+ *  class_conf_struct - JSONized config
+ * 
+ * Returns:
+ *  conf_class object
+ */
+bbop.golr.conf_class = function (class_conf_struct){
+    this._is_a = 'bbop.golr.conf_class';
+
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger.
+    var logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    // Capture class and the component fields into variables.
+    this._class = class_conf_struct;
+    // this._fields = {};
+    // bbop.core.each(this._class['fields'],
+    // 		   function(item, index){
+    // 		       var sf = new bbop.golr.conf_field(item);
+    // 		       anchor._fields[sf.id()] = sf;
+    // 		  });
+
+    /*
+     * Function: display_name
+     * 
+     * The user-facing display name. Suitable for label or title
+     * somewhere.
+     * 
+     * Returns:
+     *  Display name string.
+     */
+    this.display_name = function(){
+	return this._class['display_name'];
+    };
+
+    /*
+     * Function: description
+     * 
+     * A longer description. Suitable for tooltips.
+     * 
+     * Returns:
+     *  Description string.
+     */
+    this.description = function(){
+	return this._class['description'];
+    };
+
+    /*
+     * Function: weight
+     * 
+     * The relative weight of this search class.
+     * 
+     * Returns:
+     *  Integer.
+     */
+    this.weight = function(){
+    	return parseInt(this._class['weight']) || 0;
+    };
+
+    /*
+     * Function: id
+     * 
+     * The unique ID of this profile.
+     * 
+     * Returns:
+     *  String.
+     */
+    this.id = function(){
+	return this._class['id'];
+    };
+
+    /*
+     * Function: searchable_extension
+     * 
+     * This returns the searchable extension used for this
+     * class. There is a typical default, but it might be change in
+     * namespace collisions, so it's better to just use this.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     * string
+     */
+    this.searchable_extension = function(){
+    	//return this._class['searchable_extension'] || '_searchable';
+    	return '_searchable';
+    };
+
+    /*
+     * Function: get_field
+     * 
+     * Returns a search field by id string. Null otherwise.
+     * 
+     * Parameters:
+     *  fid - a string id for the field
+     * 
+     * Returns:
+     *  <bbop.golr.conf_field>
+     */
+    this.get_field = function(fid){
+	var retval = null;
+	if( this._class.fields_hash &&
+	    this._class.fields_hash[fid] ){
+		retval = new bbop.golr.conf_field(this._class.fields_hash[fid]);
+	    }
+	return retval;
+    };
+
+    /*
+     * Function: get_fields
+     * 
+     * Return all of the fields in this search class.
+     * 
+     * Returns:
+     *  Array of <bbop.golr.conf_field> (unordered).
+     */
+    this.get_fields = function(){
+	var retval = [];
+	if( this._class.fields_hash ){
+	    bbop.core.each(this._class.fields_hash,
+			   function(fid, struct){
+			       var cf = new bbop.golr.conf_field(struct);
+			       retval.push(cf);
+			   });
+	}
+	return retval;
+    };
+
+    // Internal function to determine if the weight category that's
+    // used by several functions is okay.
+    this._munge_weight_category = function(weight_category){
+
+	// Not defined or only the defined few.
+	if( ! weight_category ){
+	    throw new Error("Missing weight category");	
+	}else if( weight_category != 'boost' &&
+	    weight_category != 'result' &&
+	    weight_category != 'filter' ){
+	    throw new Error("Unknown weight category: " + weight_category);
+	}
+
+	return weight_category + '_weights';
+    };
+
+    /*
+     * Function: get_weights
+     * 
+     * Get the various weights we need to run.
+     * 
+     * The weight category can be 'boost', 'result', or 'filter'.
+     * 
+     * Arguments:
+     *  weight_category - string identifying the legal weight category
+     * 
+     * Returns:
+     *  object of {field => weight, ...}
+     */
+    this.get_weights = function(weight_category){
+	
+	var rethash = {};
+
+	// Only the defined few.
+	weight_category = this._munge_weight_category(weight_category);
+
+	// Collect the good bits.
+	if( ! bbop.core.is_defined(this._class[weight_category]) ){
+	    throw new Error("Missing weight category: " + weight_category);
+	}else{
+	    // Only work it if there is something there more than "".
+	    var wcs = this._class[weight_category];
+	    if( wcs && wcs != "" && wcs != " " ){
+		var dfab = wcs;
+		var fields = dfab.split(/\s+/);
+		bbop.core.each(fields,
+			       function(item, i){
+				   var field_val = item.split(/\^/);
+				   rethash[field_val[0]] =
+				       parseFloat(field_val[1]);
+			       });
+	    }
+	}
+
+	return rethash;
+    };
+
+    /*
+     * Function: field_order_by_weight
+     * 
+     * Returns an array of field ids ordered by weight.
+     * 
+     * The weight category can be 'boost', 'result', or 'filter'.
+     * 
+     * Arguments:
+     * weight_category - string identifying the legal weight category
+     * cutoff - *[optional]* if not defined, all listed fields in set returned
+     * 
+     * Returns:
+     *  array like [field5, field4, ...]
+     */
+    this.field_order_by_weight = function(weight_category, cutoff){
+
+    	var retset = [];
+
+	var weights = this.get_weights(weight_category);
+
+	// Add the ones that meet threshold (if there is one) to the
+	// set.
+	bbop.core.each(weights,
+		       function(key, val){
+			   if( cutoff ){
+			       if( val >= cutoff ){
+				   retset.push(key);			       
+			       }
+			   }else{
+			       retset.push(key);			       
+			   }
+		      });
+
+	// Order the set.
+	retset.sort(function(a, b){
+			return weights[b] - weights[a];
+		    });
+
+    	return retset;
+    };
+};
+
+/*
+ * Namespace: bbop.golr.conf
+ *
+ * Constructor: conf
+ * 
+ * Contructor for the GOlr query manager.
+ * Why don't we just take bbop.golr.golr_meta as read? We want to
+ * leave the door open to having multiple GOlrs running in the same area.
+ * 
+ * Arguments:
+ *  golr_conf_var - JSized GOlr config
+ * 
+ * Returns:
+ *  golr conf object
+ * 
+ */
+bbop.golr.conf = function (golr_conf_var){
+    this._is_a = 'bbop.golr.conf';
+
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger.
+    var logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    // Lightly check incoming arguments.
+    // There could be a hash of pinned filters argument.
+    if( ! golr_conf_var || typeof golr_conf_var != 'object' ){
+	ll('ERROR: no proper golr conf var argument');
+    }
+    
+    // Settle in the conf.
+    this._golr_conf = golr_conf_var;
+
+    // Process the conf classes into one spot.
+    this._classes = {};
+    bbop.core.each(anchor._golr_conf,
+		  function(key, val){
+		      var new_asp = new bbop.golr.conf_class(val);
+		      anchor._classes[new_asp.id()] = new_asp;
+		  });
+
+    /*
+     * Function: get_class
+     * 
+     * Returns a class info object by id string. Null otherwise.
+     * 
+     * Arguments:
+     *  fid - TODO
+     * 
+     * Returns:
+     *  bbop.golr.conf_class.
+     */
+    this.get_class = function(fid){
+	var retval = null;
+	if( this._classes &&
+	    this._classes[fid] ){
+		retval = this._classes[fid];
+	    }
+	return retval;
+    };
+
+    /*
+     * Function: get_classes
+     * 
+     * Returns an array of all search classes.
+     * 
+     * Returns:
+     *  Array of <bbop.golr.conf_class> (unordered).
+     */
+    this.get_classes = function(){
+	var ret = [];
+	bbop.core.each(anchor._classes,
+		       function(key, val){
+			   ret.push(val);
+		       });
+	return ret;
+    };
+
+    /*
+     * Function: get_classes_by_weight
+     * 
+     * Returns an array of all search classes. Ordered by weight.
+     * 
+     * Returns:
+     *  Array of <bbop.golr.conf_class>.
+     */
+    this.get_classes_by_weight = function(){
+	var ret = this.get_classes();
+
+	ret.sort(
+	    function(cc1, cc2){
+		var w1 = cc1.weight() || 0;
+		var w2 = cc2.weight() || 0;
+		return w2 - w1;
+	    });
+
+	return ret;
+    };
+};
+/* 
+ * Package: response.js
+ * 
+ * Namespace: bbop.golr.response
+ * 
+ * Generic BBOP handler for dealing with the gross parsing of
+ * responses from a GOlr server (whereas <golr_conf> deals with the
+ * reported configuration). This is not intended to do anything like
+ * modeling the data in the store (<golr_manager>), but rather to deal
+ * with things like checking for success, what paging would look like,
+ * what parameters were passed, etc.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'golr', 'response');
+
+/*
+ * Constructor: response
+ * 
+ * Contructor for a GOlr query response object.
+ * 
+ * The constructor argument is an object, not a string.
+ * 
+ * Arguments:
+ *  json_data - the JSON data (as object) returned from a request
+ * 
+ * Returns:
+ *  golr response object
+ */
+bbop.golr.response = function(json_data){
+    this._is_a = 'bbop.golr.response';
+
+    // The raw incoming document.
+    this._raw = json_data;
+
+    // Cache for repeated calls to success().
+    this._success = null;
+
+    // Cache for repeated calls to get_doc* functions.
+    // These are non-incremental indices--they are either full formed
+    // (the first time they are hit) or they are null.
+    this._doc_id2index = null;
+    this._doc_index2_id = null;
+
+    // Cache for repeated calls to resolve labels.
+    // This cache is incremental--the more it's used the larger it gets.
+    this._doc_label_maps = {}; // {<field_1>: <parsed_json_map_1>, ...}
+
+    // For highlight stripping, I just want to compile this once.
+    this._hl_regexp = new RegExp("\<\[\^\>\]\*\>", "g");
+
+};
+
+/*
+ * Function: raw
+ * 
+ * returns a pointer to the initial response object
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  object
+ */
+bbop.golr.response.prototype.raw = function(){
+    return this._raw;
+};
+
+/*
+ * Function: success
+ * 
+ * Simple return verification of sane response from server.
+ * 
+ * Success caches its return value.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  boolean
+ */
+bbop.golr.response.prototype.success = function(){
+
+    if( this._success == null ){
+
+	var robj = this._raw;
+	if( robj &&
+	    robj.responseHeader &&
+	    typeof robj.responseHeader.status != 'undefined' &&
+	    robj.responseHeader.status == 0 &&
+	    robj.responseHeader.params &&
+	    robj.response &&
+	    typeof robj.response.numFound != 'undefined' &&
+	    typeof robj.response.start != 'undefined' &&
+	    typeof robj.response.maxScore != 'undefined' &&
+	    robj.response.docs &&
+	    robj.facet_counts &&
+	    robj.facet_counts.facet_fields ){
+		this._success = true;
+	    }else{
+		this._success = false;
+	    }
+    }
+
+    return this._success;
+};
+
+/*
+ * Function: callback_type
+ * 
+ * Return the callback type if it was specified in the query,
+ * otherwise return null. For example "reset" and "response".
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  string (or null)
+ */
+bbop.golr.response.prototype.callback_type = function(){
+    var robj = this._raw;
+    var retval = null;
+    if( robj.responseHeader.params.callback_type &&
+	typeof robj.responseHeader.params.callback_type != 'undefined' ){
+	    retval = robj.responseHeader.params.callback_type;
+	}
+    return retval;
+};
+
+/*
+ * Function: parameters
+ * 
+ * Get the parameter chunk--variable stuff we put in.
+ * 
+ * Pretty general, specialized functions are better.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  hash
+ */
+bbop.golr.response.prototype.parameters = function(){
+    var robj = this._raw;
+    return robj.responseHeader.params;
+};
+
+/*
+ * Function: parameter
+ * 
+ * Get the parameter chunk--variable stuff we put in.
+ * 
+ * Pretty general, specialized functions are better.
+ * 
+ * Arguments:
+ *  n/a
+ *  key - string id for the wanted parameter
+ * 
+ * Returns:
+ *  hash, string, whatever is there at that key (otherwise null)
+ */
+bbop.golr.response.prototype.parameter = function(key){
+    var robj = this._raw;
+    var retval = null;
+    if( robj.responseHeader.params[key] && robj.responseHeader.params[key] ){
+	retval = robj.responseHeader.params[key];
+    }
+    return retval;
+};
+
+/*
+ * Function: row_step
+ * 
+ * Returns the number of rows requested (integer).
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  integer
+ */
+bbop.golr.response.prototype.row_step = function(){	
+    var robj = this._raw;
+    return parseInt(robj.responseHeader.params.rows);
+};
+
+/*
+ * Function: total_documents
+ * 
+ * Return the total number of documents found.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  integer
+ */
+bbop.golr.response.prototype.total_documents = function(){
+    var robj = this._raw;
+    return parseInt(robj.response.numFound);
+};
+
+/*
+ * Function: start_document
+ * 
+ * Returns the start document for this response as an integer.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  integer
+ */
+bbop.golr.response.prototype.start_document = function(){
+    var robj = this._raw;
+    return parseInt(robj.response.start) + 1;
+};
+
+/*
+ * Function: end_document
+ * 
+ * Returns the end document for this response as an integer.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  integer
+ */
+bbop.golr.response.prototype.end_document = function(){
+    var robj = this._raw;
+    return this.start_document() +
+	parseInt(robj.response.docs.length) - 1;
+};
+
+/*
+ * Function: packet
+ * 
+ * Return the packet number of the current response.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  integer or null (no packet defined)
+ */
+bbop.golr.response.prototype.packet = function(){
+    var robj = this._raw;
+    var retval = null;
+    var pval = robj.responseHeader.params.packet;
+    if( pval ){
+	retval = parseInt(pval);
+    }
+    return retval;
+};
+
+/*
+ * Function: paging_p
+ * 
+ * Whether or not paging is necessary with the given results set.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  boolean
+ */
+bbop.golr.response.prototype.paging_p = function(){
+    var robj = this._raw;
+    var retval = false;
+    if( this.total_documents() > this.row_step() ){
+	retval = true;
+    }
+    return retval;
+};
+
+/*
+ * Function: paging_previous_p
+ * 
+ * Whether or paging backwards is an option right now.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  boolean
+ */
+bbop.golr.response.prototype.paging_previous_p = function(){
+    // We'll take this as a proxy that a step was taken.
+    // Remember: we offset the start_document by one for readability.
+    var robj = this._raw;
+    var retval = false;
+    if( this.start_document() > 1 ){
+	retval = true;
+    }
+    return retval;
+};
+
+/*
+ * Function: paging_next_p
+ * 
+ * Whether or paging forwards is an option right now.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  boolean
+ */
+bbop.golr.response.prototype.paging_next_p = function(){
+    // We'll take this as a proxy that a step was taken.
+    var robj = this._raw;
+    var retval = false;
+    if( this.total_documents() > this.end_document() ){
+	retval = true;	
+    }
+    return retval;
+};
+
+/*
+ * Function: documents
+ * 
+ * Returns an array of raw and unprocessed document hashes.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  hash
+ */
+bbop.golr.response.prototype.documents = function(){
+    var robj = this._raw;
+    return robj.response.docs;
+};
+
+/*
+ * Function: get_doc
+ * 
+ * Returns a specified document, in its raw hash form.
+ * 
+ * Arguments:
+ *  doc_id - document identifier either an id (first) or place in the array
+ * 
+ * Returns:
+ *  document hash or null
+ */
+bbop.golr.response.prototype.get_doc = function(doc_id){
+
+    var doc = null;
+    var robj = this._raw;
+
+    // First check if the document is available by position.
+    var docs = robj.response.docs;
+    if( docs && docs[doc_id] ){
+	doc = docs[doc_id];
+    }else{ // Not available by position, so lets see if we can get it by id.
+	
+	//print('in: ' + doc_id + ' _' + this._doc_id2index);
+
+	// Build the doc index if it isn't there.
+	var local_anchor = this;
+	if( ! this._doc_id2index ){
+	    //print('BUILD triggered on: ' + doc_id);
+	    this._doc_id2index = {};
+	    this._doc_index2id = {};
+	    bbop.core.each(docs,
+			   function(doc_item, doc_index){
+			       var did = doc_item['id'];
+			       //print('BUILD: ' + did + ' => ' + doc_index);
+			       local_anchor._doc_id2index[did] = doc_index;
+			       local_anchor._doc_index2id[doc_index] = did;
+			   });
+	}
+	
+	//print('pre-probe: ' + doc_id + ' _' + this._doc_id2index);
+
+	// Try and probe it out.
+	if( this._doc_id2index &&
+	    bbop.core.is_defined(this._doc_id2index[doc_id]) ){
+	    //print('PROBE: ' + doc_id);
+	    var doc_i = this._doc_id2index[doc_id];
+	    doc = docs[doc_i];
+	}
+    }
+
+    return doc;
+};
+
+/*
+ * Function: get_doc_field
+ * 
+ * Returns the value(s) of the requested fields.
+ * 
+ * Remember that determining whether the returned value is a string or
+ * a list is left as an exercise for the reader when using this
+ * function.
+ * 
+ * Arguments:
+ *  doc_id - document identifier either an id (first) or place in the array
+ *  field_id - the identifier of the field we're trying to pull
+ * 
+ * Returns:
+ *  value or list of values
+ */
+bbop.golr.response.prototype.get_doc_field = function(doc_id, field_id){
+
+    var ret = null;
+
+    // If we found our doc, go ahead and start looking for the field.
+    var doc = this.get_doc(doc_id);
+    if( doc && bbop.core.is_defined(doc[field_id]) ){
+	
+	// We have an answer with this.
+	ret = doc[field_id];
+    }
+
+    return ret;
+};
+
+/*
+ * Function: get_doc_label
+ * 
+ * Tries to return a label for a document, field, and id combination.
+ * 
+ * WARNING: This function could be potentially slow on large datasets.
+ * 
+ * Arguments:
+ *  doc_id - document identifier either an id (first) or place in the array
+ *  field_id - the identifier of the field we're trying to pull
+ *  item_id - *[optional]* the item identifier that we're trying to resolve; if the field in question is a string or a single-valued list (as opposed to a multi-values list), this argument is not necessary, but it wouldn't hurt either
+ * 
+ * Returns:
+ *  null (not found) or string
+ */
+bbop.golr.response.prototype.get_doc_label = function(doc_id,field_id,item_id){
+
+    var retval = null;
+
+    var anchor = this;
+
+    // If we found our doc, and confirmed that the field in question
+    // exists in the doc, go ahead and start digging to resolve the id.
+    var doc = this.get_doc(doc_id);
+    if( doc && bbop.core.is_defined(doc[field_id]) ){
+	
+	// First try the '_label' extension.
+	var ilabel = this.get_doc_field(doc_id, field_id + '_label');
+
+	if( ilabel && bbop.core.what_is(ilabel) == 'string' ){
+	    // It looks like the simple solution.
+	    //print('trivial hit');
+	    retval = ilabel; // Hit!
+	}else if( ilabel && bbop.core.what_is(ilabel) == 'array' ){
+	    
+	    // Well, it's multi-valued, but id might just be the one.
+	    var iid = this.get_doc_field(doc_id, field_id);
+	    if( ilabel.length == 1 && iid &&
+		bbop.core.what_is(iid) == 'array' &&
+		iid.length == 1 ){
+		    // Case of a single id trivially mapping to a
+		    // single label.
+		    //print('forced hit');
+		    retval = ilabel[0]; // Hit!
+	    }else{
+
+		//print('need to probe');
+
+		// Since we'll do this twice with different map
+		// fields, a generic function to try and probe a JSON
+		// string map (caching it along the way) for a label.
+		function _map_to_try(doc_key, map_field, item_key){
+
+		    var retlbl = null;
+
+		    var map_str = anchor.get_doc_field(doc_key, map_field);
+
+		    if( map_str && bbop.core.what_is(map_str) == 'string' ){
+
+			// First, check the cache. If it's not there
+			// add it.
+			if( ! bbop.core.is_defined(anchor._doc_label_maps[doc_key]) ){
+			    anchor._doc_label_maps[doc_key] = {};
+			}
+			if( ! bbop.core.is_defined(anchor._doc_label_maps[doc_key][map_field]) ){
+			    // It looks like a map wasn't defined, so let's
+			    // convert it into JSON now.
+			    anchor._doc_label_maps[doc_key][map_field] =
+				bbop.json.parse(map_str);
+			}
+
+			// Pull our map out of the cache.
+			var map = anchor._doc_label_maps[doc_key][map_field];
+
+			// Probe to see if we have anything in the map.
+			if( map && map[item_key] ){
+			    retlbl = map[item_key];
+			}
+		    }
+
+		    return retlbl;
+		}
+
+		// Well, now we know that either we have to find a map
+		// or the information isn't there. First try the
+		// standard "_map".
+		var mlabel = _map_to_try(doc_id, field_id + '_map', item_id);
+		if( mlabel ){
+		    //print('map hit');
+		    retval = mlabel; // Hit!
+		}else{
+		    // If that didn't work, try again with
+		    // "_closure_map".
+		    var cmlabel =
+			_map_to_try(doc_id, field_id + '_closure_map', item_id);
+		    if( cmlabel ){
+			//print('closure map hit');
+			retval = cmlabel; // Hit!
+		    }else{
+			// If that didn't work, try again with
+			// "_list_map".
+			var lmlabel =
+			    _map_to_try(doc_id, field_id +'_list_map', item_id);
+			if( lmlabel ){
+			    //print('list map hit');
+			    retval = lmlabel; // Hit!
+			}
+		    }
+		}
+	    }
+	}
+    }
+
+    return retval;
+};
+
+/*
+ * Function: get_doc_highlight
+ * 
+ * Returns the highlighted value(s) of the requested fields.
+ * 
+ * WARNING: This function is a work in progress and will not return
+ * multi-valued fields, just the first match it finds.
+ * 
+ * WARNING: This function could be potentially slow on large datasets.
+ * 
+ * Arguments:
+ *  doc_id - document id
+ *  field_id - the identifier of the field we're trying to pull
+ *  item - the item that we're looking for the highlighted HTML for
+ * 
+ * Returns:
+ *  string of highlight or null if nothing was found
+ */
+bbop.golr.response.prototype.get_doc_highlight = function(doc_id,field_id,item){
+
+    var ret = null;
+    var robj = this._raw;
+    var hlre = this._hl_regexp;
+
+    // See if we can find a highlighted version in the raw
+    // response. First, see if the document is in the hilight section;
+    // otherwise try and pull the id out first, then head for the
+    // highlight section.
+    var hilite_obj = null;
+    if( robj.highlighting && robj.highlighting[doc_id] ){
+	hilite_obj = robj.highlighting[doc_id];
+    }else{
+	var iid = this._doc_index2id[doc_id];
+	if( iid ){
+	    var new_doc = this.get_doc(iid);
+	    var new_doc_id = new_doc['id'];
+	    if( robj.highlighting && robj.highlighting[new_doc_id] ){
+		hilite_obj = robj.highlighting[new_doc_id];
+	    }
+	}
+    }
+
+    // If we got a highlight object, see if the highlighted field is
+    // there--search the different possibilities for what a highlight
+    // field may be called.
+    if( hilite_obj ){
+	
+	//print('here (field_id): ' + field_id);
+
+	var ans = null;
+
+	if( hilite_obj[field_id + '_label_searchable'] ){
+	    ans = hilite_obj[field_id + '_label_searchable'];
+	}
+
+	if( ! ans ){
+	    if( hilite_obj[field_id + '_label'] ){
+		ans = hilite_obj[field_id + '_label'];
+	    }	    
+	}
+
+	if( ! ans ){
+	    if( hilite_obj[field_id + '_searchable'] ){
+		ans = hilite_obj[field_id + '_searchable'];
+	    }
+	}
+
+	if( ! ans ){
+	    if( hilite_obj[field_id] ){
+		//print('here (field_id): ' + field_id);
+		ans = hilite_obj[field_id];
+	    }
+	}
+
+	if( ans ){ // looks like I found a list of something
+
+	    // Use only the first match.
+	    var matches_p = false;
+	    bbop.core.each(ans,
+			   function(an){
+			       if( ! matches_p ){
+				   var stripped = an.replace(hlre, '');
+				   //print('stripped: ' + stripped);
+				   //print('item: ' + item);
+				   if( item == stripped ){
+				       matches_p = true;
+				       ret = an;
+				   }
+			       }
+			   });
+	}
+    }
+
+    return ret;
+};
+
+// /*
+//  * Function: facet_fields
+//  * 
+//  * Return a count sorted array of the response's facet fields and counts.
+//  * 
+//  * Arguments:
+//  *  n/a
+//  * 
+//  * Returns:
+//  *  list of string/integer doublets
+//  */
+// bbop.golr.response.prototype.facet_fields = function(){
+//     var robj = this._raw;
+//     return robj.facet_counts.facet_fields;
+// };
+
+/*
+ * Function: facet_field_list
+ * 
+ * Return a count sorted array of the response's facet fields.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  list of strings
+ */
+bbop.golr.response.prototype.facet_field_list = function(){
+    var robj = this._raw;
+    return bbop.core.get_keys(robj.facet_counts.facet_fields).sort();
+};
+
+/*
+ * Function: facet_field
+ * 
+ * Return a count-sorted array of a facet field's response.
+ * 
+ * : [["foo", 60], ...]
+ * 
+ * Arguments:
+ *  facet_name - name of the facet to examine
+ * 
+ * Returns:
+ *  list of nested lists
+ */
+bbop.golr.response.prototype.facet_field = function(facet_name){
+    var robj = this._raw;
+    return robj.facet_counts.facet_fields[facet_name];
+};
+
+/*
+ * Function: facet_counts
+ * 
+ * For a given facet field, return a hash of that field's items and
+ * their counts.
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  hash of facets to their integer counts
+ */
+bbop.golr.response.prototype.facet_counts = function(){
+
+    var robj = this._raw;
+    var ret_hash = {};
+
+    var anchor = this;
+    
+    var each = bbop.core.each;
+    var facet_field_list = this.facet_field_list();
+    each(facet_field_list,
+	 function(ffield){
+	     
+	     // Make sure the top field is present,
+	     if( ! ret_hash[ffield] ){
+		 ret_hash[ffield] = {};		
+	     }
+
+	     var facet_field_items = anchor.facet_field(ffield);
+	     each(facet_field_items,
+		  function(item, index){
+		      var name = item[0];
+		      var count = item[1];
+		      ret_hash[ffield][name] = count;
+		  });
+	 });
+    
+    return ret_hash;
+};
+
+/*
+ * Function: query
+ * 
+ * Return the raw query parameter "q".
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  string or null
+ */
+bbop.golr.response.prototype.query = function(){
+    var robj = this._raw;    
+    var retval = null;
+    
+    if( robj.responseHeader.params && robj.responseHeader.params.q ){
+	retval = robj.responseHeader.params.q;
+    }
+    
+    return retval;
+};
+
+/*
+ * Function: query_filters
+ *
+ * A sensible handling of the not-so-great format of "fq" returned by
+ * Solr (fq can be irritating single value or irritating array, along
+ * with things like "-" in front of values). Since plus and minus
+ * filters are mutually exclusive, we have a return format like:
+ * 
+ * : {field1: {filter1: (true|false), ...}, ...}
+ * 
+ * Where the true|false value represents a positive (true) or negative
+ * (false) filter.
+ * 
+ * Parameters:
+ *  n/a
+ *
+ * Returns:
+ *  a hash of keyed hashes
+ */
+bbop.golr.response.prototype.query_filters = function(){
+    var robj = this._raw;    
+    var ret_hash = {};
+    var fq_list = this.parameter('fq');
+    if( fq_list ){
+	
+	// Ensure that it's a list and not just a naked string (as can
+	// sometimes happen).
+	if( bbop.core.what_is(fq_list) == 'string'){
+	    fq_list = [fq_list];
+	}
+	
+	// Make the return fq more tolerable.
+	var each = bbop.core.each;
+	each(fq_list,
+	     function(fq_item){
+		 
+		 // Split everything on colons. Field is the first
+		 // one, and everything else joined back together is
+		 // the value of the filter. Best if you think about
+		 // the GO id and non-GO id cases.
+		 var splits = fq_item.split(":");
+		 var field = splits.shift();
+		 var value = splits.join(":"); // GO 0022008 -> GO:0022008
+
+		 // First let's just assume that we have a positive
+		 // filter.
+		 var polarity = true;
+		 
+		 // Check and see if the first value in our
+		 // field is '-' or '+'. If so, edit it out, but
+		 // change the polarity in the '-' case.
+		 if( field.charAt(0) == '-' ){
+		     polarity = false;
+		     field = field.substring(1, field.length);
+		 }else if( field.charAt(0) == '+' ){
+		     field = field.substring(1, field.length);
+		 }
+
+		 // Ensure that there is a place in the return hash
+		 // for us.
+		 if( ! ret_hash[field] ){
+		     ret_hash[field] = {};
+		 }
+		 
+		 // I want just the first quote and the final quote
+		 // gone from the value if they are matching quotes.
+		 if( value.charAt(0) == '"' &&
+		     value.charAt(value.length -1) == '"' ){
+			 value = value.substring(1, value.length -1);
+		     }
+		 
+		 // The final filter note.
+		 ret_hash[field][value] = polarity;
+		 
+	     });
+    }
+    
+    return ret_hash;
+};
+/* 
+ * Package: manager.js
+ * 
+ * Namespace: bbop.golr.manager
+ * 
+ * Generic BBOP manager for dealing with gross GOlr configuration and
+ * management. Remember, this is actually a "subclass" of
+ * <bbop.registry>. The defined events for this registry are: "reset",
+ * "search", and "error".
+ * 
+ *  reset - functions for initializing and resetting
+ *  search - functions for receiving standard search results
+ *  error - functions to call when something goes very wrong
+ * 
+ * Both <bbop.golr.response> (or clean error data) and the manager
+ * itself (this as anchor) should be passed to the callbacks.
+ * 
+ * TODO/BUG: <set_query> and <set_default_query> should both take
+ * strings or <bbop.logic> as arguments. Those, as well as <get_query>
+ * and <get_query> should only return <bbop.logic>.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.namespace('bbop', 'golr', 'manager');
+
+/*
+ * Constructor: manager
+ * 
+ * Contructor for the GOlr query manager
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.registry>
+ */
+bbop.golr.manager = function (golr_loc, golr_conf_obj){
+    //bbop.registry.call(this, ['reset', 'search', 'error']);
+    bbop.registry.call(this, ['reset', 'search', 'error']);
+    this._is_a = 'bbop.golr.manager';
+
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger.
+    this._logger = new bbop.logger(this._is_a);
+    //this._logger.DEBUG = true;
+    this._logger.DEBUG = false;
+    function ll(str){ anchor._logger.kvetch(str); }
+
+    // To help keep requests from the past haunting us. Actually doing
+    // something with this number is up to the UI.
+    this.last_sent_packet = 0;
+    //this.last_received_packet = 0;
+
+    // Lightly check incoming arguments.
+    // There should be a string url argument.
+    // There could be a hash of pinned filters argument.
+    if( ! golr_loc || ! golr_conf_obj ){
+	ll('ERROR: no proper arguments');
+    }
+    if( typeof golr_loc != 'string' ){
+	ll('ERROR: no proper golr url string argument');
+    }
+    if(	! golr_conf_obj._is_a || ! golr_conf_obj._is_a == 'bbop.golr.conf' ){
+	    ll('ERROR: no proper bbop.golr.conf object argument');
+	    throw new Error('boink! ' + bbop.core.what_is(golr_conf_obj) );
+	}
+    
+    // Whether or not to prevent ajax events from going.
+    // This may not be usable, or applicable, to all backends.
+    this._safety = false;
+
+    // Our default target url.
+    this._solr_url = golr_loc;
+
+    // Settle in the configurations.
+    // this._golr_conf = new bbop.golr.conf(golr_conf_var);
+    this._golr_conf = golr_conf_obj;
+
+    // The current data batches that we are storing.
+    this._batch_urls = [];
+    this._batch_accumulator_func = function(){};
+    this._batch_final_func = function(){};
+
+    // The current state stack.
+    this._excursions = [];
+
+    // The current class/personality that we're using. It may be none.
+    this._current_class = null;
+
+    // Our (default) query and the real deal.
+    this.fundamental_query = '*:*'; // cannot be changed
+    this.default_query = '*:*'; // changable
+    this.query = this.default_query; // current
+
+    // Our (default) fl and whatever we have now.
+    //this.default_fl = '*%2Cscore';
+    this.default_fl = '*,score';
+    this.current_fl = this.default_fl;
+
+    // We remember defaults in the case of rows and start since they
+    // are the core to any paging mechanisms and may change often.
+    //this.default_rows = 25;
+    //this.default_rows = 100;
+    this.default_rows = 10;
+    this.default_start = 0;
+    this.current_rows = this.default_rows;
+    this.current_start = this.default_start;
+
+    // There is a reason for this...TODO: later (25+)
+    this.default_facet_limit = 25;
+    this.current_facet_limit = 25;
+    // {facet_field_name: value, ...}
+    this.current_facet_field_limits = {};
+    // TODO: paging for facets;
+    this.current_facet_offset = 25;
+    this.current_facet_field_offsets = {};
+
+    // Our default query args, with facet fields plugged in.
+    this.query_variants =
+	{
+	    // Our default standard search type. This means we don't
+	    // have to explicitly add fields to the search (although
+	    // the query fields ('qf') are still necessary to make
+	    // anything real happen).
+	    defType: 'edismax',
+
+	    // Things unlikely to be touched.
+	    // There are unlikely to be messed with too much.
+	    qt: 'standard',
+	    indent: 'on',
+	    wt: 'json',
+	    //version: '2.2',
+	    rows: anchor.current_rows,
+	    start: anchor.current_start, // Solr is offset indexing
+	    //fl: '*%2Cscore',
+	    fl: anchor.default_fl,
+    
+	    // Deprecated: see query_filters
+	    //fq: {},
+	    
+	    // Deprecated: see query_fields
+	    //qf: {},
+	    
+	    // Deprecated: see query
+	    //q: '*:*'
+
+	    // Control of facets.
+	    facet: 'true',
+	    'facet.mincount': 1,
+	    'facet.sort': 'count',
+	    'json.nl': 'arrarr', // only in facets right now
+	    'facet.limit': anchor.default_facet_limit
+	    // TODO?: 'f.???.facet.limit': 50,
+	    // TODO: 'json.nl': [flat|map|arrarr]
+
+	    // Deprecated: see facet_fields
+	    //'facet.field': []
+	};
+
+    // This is the 'qf' parameter. Although we keep it, it only needs
+    // to be exposed when the query ('q') field is set.
+    //this.query_fields = [];
+    this.query_fields = {};
+
+    // A richer way to handle the 'fq' query variant.
+    // It should look like:
+    // {<filter>: {<value>:{'sticky_p':(t|f), 'negative_p':(t|f)}, ...}}
+    this.query_filters = {};
+
+    // The engine for the facet.field list.
+    this.facet_fields = {};
+
+    /*
+     * Function: debug
+     * 
+     * Turn on or off the verbose messages. Uses <bbop.logger>, so
+     * they should come out everywhere.
+     * 
+     * Parameters: 
+     *  p - *[optional]* true or false for debugging
+     *
+     * Returns: 
+     *  boolean; the current state of debugging
+     */
+    this.debug = function(p){
+	if( p == true || p == false ){
+	    this._logger.DEBUG = p;
+	    // TODO: add debug parameter a la include_highlighting
+	}
+	return this._logger.DEBUG;
+    };
+
+    /*
+     * Function: lite
+     * 
+     * Limit the returns fields (the parameter "fl") to the ones
+     * defined in the set of fields defined in results, label fields
+     * if available (i.e. "_label", "_map" when "_label" is
+     * multi=valued), and "score" and "id".
+     * 
+     * The default is "false".
+     * 
+     * Parameters: 
+     *  use_lite_p - *[optional]* true or false, none just returns current
+     *
+     * Returns: 
+     *  boolean; the current state of lite-ness
+     */
+    this.lite = function(use_lite_p){
+
+	// Adjust the current state accordingly.
+	if( use_lite_p == true || use_lite_p == false ){
+	    if( use_lite_p == true ){
+		
+		// The actual collections and adjustment.
+		// First, this only works if we have a personality, so
+		// check to see if we have one.
+		var per = anchor.get_personality();
+		if( per ){
+		    // Since we have a personality, collect all of the
+		    // mentioned fields.
+		    var field_collection = {};
+		    var loop = bbop.core.each;
+		    var union = bbop.core.merge;
+		    var ccl = anchor._current_class;
+
+		    // Fill field_collection with the fields
+		    // in the given category.
+		    //loop(['boost', 'result', 'filter'],
+		    //loop(['result', 'filter'],
+		    loop(['result'],
+			 function(cat){
+			     field_collection = 
+				 union(field_collection, ccl.get_weights(cat));
+			 });
+		    
+		    // Next, flatten into a list.
+		    var flist = bbop.core.get_keys(field_collection);
+
+		    // Now for all the fields in these categories, see
+		    // if we can find additional "special" labels to
+		    // go with them.
+		    loop(flist,
+		    	 function(flist_item){
+			     loop(['_label'],
+			     //loop(['_label', '_label_searchable'],
+		    		   function(field_suffix){
+				       var new_field = 
+					   flist_item + field_suffix;
+				       var nf_obj = ccl.get_field(new_field);
+				       if( nf_obj ){
+					   flist.push(new_field);
+
+					   // There appears to be the
+					   // thing label. If they are
+					   // both multi-valued, then
+					   // there will be a map as
+					   // well.
+					   if( nf_obj.is_multi() ){
+					       flist.push(flist_item + '_map');
+					   }
+				       }
+				   });
+			 });
+
+
+		    // Finally, set these fields (plus score) as the
+		    // new return fields.
+		    flist.push('score');
+		    flist.push('id');
+		    //anchor.current_fl = flist.join('%2C');
+		    anchor.current_fl = flist.join(',');
+		    anchor.set('fl', anchor.current_fl);
+		}
+
+	    }else{ // else false
+		// Reset.
+		anchor.current_fl = anchor.default_fl;
+		anchor.set('fl', anchor.current_fl);
+	    }
+	}
+
+	// Return the current state.
+	var retval = false;
+	if( anchor.default_fl != anchor.current_fl ){
+	    retval = true;
+	}
+	return retval;
+    };
+
+    // An internal helper function to munge the name of a field into
+    // the name of its corresponding facet field.
+    function _field_to_facet_field(field){
+	return 'f.' + field + '.facet.limit';
+    }
+    
+    /*
+     * Function: get_facet_limit
+     * 
+     * Get the limit for a specified facet or the global limit.
+     * 
+     * Parameters: 
+     *  field - *[optional]* limit for a specific field; otherwise global value
+     *
+     * Returns: 
+     *  integer or null
+     */
+    this.get_facet_limit = function(field){
+	var retval = null;
+
+	if( ! field ){
+	    retval = anchor.current_facet_limit;
+	}else{
+	    var f = _field_to_facet_field(field);
+	    var try_val = anchor.current_facet_field_limits[f];
+	    if( bbop.core.is_defined(try_val) ){
+		retval = try_val;
+	    }
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: set_facet_limit
+     * 
+     * Change the number of facet values returned per call.
+     * The default is likely 25.
+     * 
+     * Just as in Solr, a -1 argument is how to indicate unlimted
+     * facet returns.
+     * 
+     * This setting does not survive things like <resets_facet_limit>.
+     * 
+     * Parameters: 
+     *  arg1 - (integer) set the global limit
+     *
+     * Parameters: 
+     *  arg1 - (string) the name of the field to check
+     *  arg2 - (integer) set the limit for this field
+     *
+     * Returns: 
+     *  boolean on whether something was set
+     */
+    this.set_facet_limit = function(arg1, arg2){
+	var retval = false;
+
+	// Decide which form of the function we're using.
+	if( ! bbop.core.is_defined(arg2) && 
+	    bbop.core.what_is(arg1) == 'number' ){ // form one
+		
+		// Set
+		var nlimit = arg1;
+		anchor.current_facet_limit = nlimit;
+		anchor.set('facet.limit', anchor.current_facet_limit);
+		
+		retval = true;
+	
+	}else if( bbop.core.is_defined(arg1) && 
+		  bbop.core.is_defined(arg2) &&
+		  bbop.core.what_is(arg1) == 'string' &&
+		  bbop.core.what_is(arg2) == 'number' ){
+		      
+		      var field = _field_to_facet_field(arg1);
+		      var limit = arg2;
+		      anchor.current_facet_field_limits[field] = limit;
+		      
+		      retval = true;
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: set_default_facet_limit
+     * 
+     * Permanently change the default number of facet values returned
+     * per call. The default's default is likely 25.
+     * 
+     * Just as in Solr, a -1 argument is how to indicate unlimted
+     * facet returns.
+     * 
+     * Parameters: 
+     *  lim - (integer) set the global default limit
+     *
+     * Returns: 
+     *  old default
+     */
+    this.set_default_facet_limit = function(lim){
+
+	// Capture ret.
+	var retval = anchor.default_facet_limit;
+
+	// Set
+	anchor.default_facet_limit = lim;
+	//anchor.set('facet.limit', anchor.default_facet_limit);
+		
+	return retval;
+    };
+
+    /*
+     * Function: reset_facet_limit
+     * 
+     * Either reset the global limit to the original (likely 25)
+     * and/or remove the specified filter. Sets everything back to the
+     * original values or whatever was set by
+     * <set_default_facet_limit>.
+     * 
+     * Parameters: 
+     *  field - *[optional]* remove limit for a field; otherwise all and global
+     *
+     * Returns: 
+     *  boolean on whether something was reset
+     */
+    this.reset_facet_limit = function(field){
+	var retval = false;
+
+	if( ! bbop.core.is_defined(field) ){
+	    // Eliminate all fields by blowing them away.
+	    anchor.current_facet_limit = anchor.default_facet_limit;
+	    anchor.set('facet.limit', anchor.current_facet_limit);
+	    anchor.current_facet_field_limits = {};
+	    retval = true;
+	}else{ // eliminate just the one field
+	    var f = _field_to_facet_field(field);
+	    if( bbop.core.is_defined(anchor.current_facet_field_limits[f]) ){
+		delete anchor.current_facet_field_limits[f];
+		retval = true;
+	    }
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: get_results_count
+     * 
+     * Get the current number of results that will be returned.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns: 
+     *  integer
+     */
+    this.get_results_count = function(field){
+	return anchor.get('rows');
+    };
+
+    /*
+     * Function: set_results_count
+     * 
+     * Change the number of result documents returned per call.
+     * The default is likely 10.
+     * 
+     * Parameters: 
+     *  count - (integer) set the global results count
+     *
+     * Returns:
+     *  the count set
+     */
+    this.set_results_count = function(count){
+	anchor.set('rows', count);
+	anchor.current_rows = count;
+	return anchor.current_rows;
+    };
+
+    /*
+     * Function: reset_results_count
+     * 
+     * Reset the number of documents to their original setting, likely
+     * 10.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the new count
+     */
+    this.reset_results_count = function(){
+	anchor.set('rows', anchor.default_rows);
+	anchor.current_rows = anchor.default_rows;
+	return anchor.current_rows;
+    };
+
+    /*
+     * Function: plist_to_property_hash
+     *
+     * Turn a plist to a hash containing the different properties that
+     * can be defined for a query filter. Possible values are: '+'
+     * (positive filter), '-' (negative filter), '*' (sticky filter),
+     * '$' (transient). If mutually exclusive properties are defined
+     * (e.g. both '+' and '-'), the last one will be used. Or, since
+     * that is a call to silliness, let's say the behavior is
+     * undefined.
+     *
+     * Parameters: 
+     *  plist - *[optional]* a list of properties to apply to the filter
+     *
+     * Returns: 
+     *  A hash version of the plist; otherwise, the default property hash
+     */
+    this.plist_to_property_hash = function(plist){
+
+	// Let's start with the default values.
+	var phash = {
+	    //'positive_p': true,
+	    'negative_p': false,
+	    //'transient_p': true
+	    'sticky_p': false
+	};
+
+	// If not defined, just return the default list.
+	if( plist ){	    
+	    bbop.core.each(plist,
+			   function(item){
+			       if( item == '+' ){
+				   phash['negative_p'] = false;
+				   //phash['positive_p'] = true;
+			       }else if( item == '-' ){
+				   phash['negative_p'] = true;
+				   //phash['positive_p'] = false;
+			       }else if( item == '*' ){
+				   phash['sticky_p'] = true;
+				   //phash['transient_p'] = false;
+			       }else if( item == '$' ){
+				   phash['sticky_p'] = false;
+				   //phash['transient_p'] = true;
+			       }
+			   });
+	}
+
+	return phash;
+    };
+
+    /*
+     * Function: add_query_filter
+     *
+     * Setter for query filters ('fq').
+     *
+     * Parameters: 
+     *  filter - filter (type) string
+     *  value - filter value string (or TODO: defined logic hash)
+     *  plist - *[optional]* list of properties of the filter
+     *
+     * Returns: 
+     *  (TODO) The current query filter hash.
+     * 
+     * See also:
+     *  <plist_to_property_hash>
+     */
+    this.add_query_filter = function(filter, value, plist){
+	
+	// Make sure we've defined the group.
+	if( ! bbop.core.is_defined(this.query_filters[filter]) ){
+	    this.query_filters[filter] = {};
+	}
+
+	this.query_filters[filter][value] = this.plist_to_property_hash(plist);
+	
+	//ll("Current state: " + bbop.core.dump(this.query_filters));
+
+	return {}; // TODO
+    };
+
+    /*
+     * Function: remove_query_filter
+     *
+     * Remover for query filters ('fq'), is a plist is specified, it
+     * will only remove if all of the listed criteria are met.
+     *
+     * Parameters: 
+     *  filter - filter (type) string
+     *  value - filter value string (TODO: or defined logic hash)
+     *  plist - *[optional]* list of properties of the filter
+     *
+     * Returns: 
+     *  boolean (on success)
+     */
+    this.remove_query_filter = function(filter, value, plist){
+
+	// Default return value.
+	var retval = false;
+
+	// Internal helper to delete a low level key, and then if the
+	// top-level is empty, get that one too.
+	function _full_delete(hash, key1, key2){
+	    if( key1 && key2 && hash &&
+		hash[key1] && hash[key1][key2] ){
+		    delete hash[key1][key2];
+		}
+	    if( bbop.core.is_empty(hash[key1]) ){
+		delete hash[key1];
+	    }
+	}
+
+	// If we have a filter, a value, and it's there...
+	if( filter && value &&
+	    anchor.query_filters[filter] &&
+	    anchor.query_filters[filter][value] ){
+
+		// If no real plist hash been defined, just go ahead
+		// and get rid of that. Otherwise, make sure that the
+		// defined plist and the stored properties are the
+		// same before deleting.
+		if( ! plist || bbop.core.is_empty(plist) ){
+		    _full_delete(anchor.query_filters, filter, value);
+		    retval = true;
+		}else{
+		    
+		    var filter_phash = anchor.query_filters[filter][value];
+		    var in_phash = anchor.plist_to_property_hash(plist);
+		    
+		    if( bbop.core.is_same(filter_phash, in_phash) ){		
+			_full_delete(anchor.query_filters, filter, value);
+			retval = true;
+		    }
+		}
+	    }
+
+	return retval;
+    };
+
+    /*
+     * Function: reset_query_filters
+     *
+     * Reset the query filters ('fq'); but leave sticky filters alone.
+     *
+     * Parameters: 
+     *  n/a
+     * 
+     * Returns: 
+     *  (TODO) The current query filter hash.
+     */
+    this.reset_query_filters = function(){
+
+	// Drill down and delete all non-stickies.
+	var loop = bbop.core.each;
+	loop(anchor.query_filters,
+	     function(filter, values){
+		 //ll('filter: ' + filter);
+		 loop(values,
+		      function(value, props){
+			  //ll('  value: ' + value);
+			  var sticky_p = props['sticky_p'];
+			  if( ! sticky_p ){
+			      //ll('hit: ' + filter + ', ' + value);
+			      anchor.remove_query_filter(filter, value);
+			  }
+		      });
+	     });
+
+	return {}; // TODO
+    };
+
+    /*
+     * Function: get_query_filter_properties
+     *
+     * Get a hash representing a query filter ('fq').
+     *
+     * Parameters: 
+     *  key - filter string (TODO: or defined logic hash)
+     *
+     * Returns: 
+     *  The current query filter hash for key.
+     */
+    this.get_query_filter_properties = function(filter, value){
+
+	// Default return value.
+	var retobj = null;
+	
+	// If we have a key and it's there...
+	var aqf = anchor.query_filters;
+	if( filter && value && aqf[filter] && aqf[filter][value] ){
+	    retobj =
+		{
+		    'filter' : filter,
+		    'value' : value,
+		    //'polarity': aqf[filter][value]['negative_p'],
+		    'negative_p': aqf[filter][value]['negative_p'],
+		    'sticky_p': aqf[filter][value]['sticky_p']
+		};
+	}
+
+	return retobj;
+    };
+
+    /*
+     * Function: get_query_filters
+     *
+     * Get a list of hashes representing the query filters ('fq'). The
+     * return lists look like:
+     *
+     * : [{'filter': A, 'value': B, 'negative_p': C, 'sticky_p': D}, ...]
+     *
+     * Where A and B are strings and C and D are booleans.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns: 
+     *  A list of the current query filter hashs.
+     */
+    this.get_query_filters = function(){
+
+	var retlist = [];	
+	var loop = bbop.core.each;
+	loop(anchor.query_filters,
+	     function(f, values){
+		 loop(values,
+		      function(v, props){
+			  retlist.push(anchor.get_query_filter_properties(f,v));
+		      });
+	     });
+
+	return retlist;
+    };
+
+    /*
+     * Function: get_sticky_query_filters
+     *
+     * Get a list of hashes representing the current stucky query
+     * filters ('fq'). See <get_query_filters> for a specification of
+     * what the return type looks like.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns: 
+     *  A list of the current sticky query filter hashs.
+     * 
+     * See also:
+     *  <get_query_filters>
+     */
+    this.get_sticky_query_filters = function(){
+
+	var retlist = [];	
+	var loop = bbop.core.each;
+	loop(anchor.query_filters,
+	     function(f, values){
+		 loop(values,
+		      function(v, props){
+			  var qfp = anchor.get_query_filter_properties(f,v);
+			  if( qfp['sticky_p'] == true ){
+			      retlist.push(qfp);			      
+			  }
+		      });
+	     });
+
+	return retlist;
+    };
+
+    // A little extra thing that we might need sometimes.
+    this.query_extra = null;
+
+    // The callback function called after a successful AJAX
+    // intialization/reset call. First it runs some template code,
+    // then it does all of the callbacks.
+    this._run_reset_callbacks = function(json_data){
+	ll('run reset callbacks...');
+	var response = new bbop.golr.response(json_data);
+	anchor.apply_callbacks('reset', [response, anchor]);
+    };
+
+    // The main callback function called after a successful AJAX call in
+    // the update function.
+    this._run_search_callbacks = function(json_data){
+	ll('run search callbacks...');
+	var response = new bbop.golr.response(json_data);
+	anchor.apply_callbacks('search', [response, anchor]);
+    };
+
+    // This set is called when we run into a problem.
+    this._run_error_callbacks = function(json_data){
+	ll('run error callbacks...');
+	var response = new bbop.golr.response(json_data);
+	anchor.apply_callbacks('error', [response, anchor]);
+    };
+
+    /*
+     * Function: sensible_query_p
+     * 
+     * Simply ask the manager if a free text query ('q') makes sense
+     * at this point.
+     * 
+     * This currently means that the query text ('q') is three (3) or
+     * longer and that query fields ('qf') are defined.
+     * 
+     * This is an overridable opinion of the manager.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  boolean
+     */
+    this.sensible_query_p = function(qfs){
+	var retval = false;
+	var q = anchor.get_query();
+	var qf = anchor.query_field_set();
+	if( q && q.length >= 3 && qf && ! bbop.core.is_empty(qf) ){
+	    retval = true;
+	}
+	return retval;
+    };
+
+    /*
+     * Function: last_packet_sent
+     *
+     * It is up to the UI to do something interesting with this number.
+     * 
+     * Also remember that this number only rises through calls to
+     * <update> or one of its wrappers. Calls to <get_query_url> and
+     * the like will not affect this number.
+     * 
+     * Parameters:
+     *  n/a 
+     *
+     * Returns:
+     *  integer
+     * 
+     * See also:
+     *  <update>
+     */
+    this.last_packet_sent = function(){
+    	return anchor.last_sent_packet;
+    };
+
+    /*
+     * Function: clear
+     *
+     * Clear all non-sticky query parameters to get back to a more
+     * "original" state.
+     * 
+     * Not to be confused with <reset>.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  n/a
+     */
+    this.clear = function(){
+
+	// Reset 'q'.
+	anchor.query = anchor.default_query;
+
+	// Reset 'fq', all but sticky.
+	anchor.reset_query_filters();
+    };
+
+    /*
+     * Function: reset
+     *
+     * Manually trigger the "reset" chain of events.
+     *
+     * This is a curried wrapper for <update> and should be preferred
+     * over a direct call to update.
+     *
+     * Note to be confused with <clear>.
+     *
+     * Returns:
+     *  the query url (with the jQuery callback specific parameters)
+     * 
+     * See also:
+     *  <update>
+     */
+    this.reset = function(){
+	return anchor.update('reset');
+    };
+
+    /*
+     * Function: search
+     *
+     * Trigger the "search" chain of events.
+     * Takes a field-keyed hash of bbop.logics as an argument.
+     * 
+     * This is a curried wrapper for <update> and should be preferred
+     * over a direct call to update.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  the query url (with the jQuery callback specific parameters)
+     * 
+     * See also:
+     *  <update>
+     */
+    this.search = function(){
+	return anchor.update('search');
+    };
+
+    /*
+     * Function: page
+     *
+     * Re-trigger the "search" chain of events, but with the variables
+     * set for a different section of the results.
+     * 
+     * Note that this operates independently of any impossibilites in
+     * the results--just how such paging would look and
+     * triggering. Ths UI should handle impossibilities and the like.
+     * 
+     * This is a wrapper for <update> and should be preferred over a
+     * direct call to update.
+     * 
+     * Parameters: 
+     *  rows - the number of rows to return
+     *  start - the offset of the rows to return
+     *
+     * Returns:
+     *  the query url (with the jQuery callback specific parameters)
+     * 
+     * See also:
+     *  <update>
+     */
+    this.page = function(rows, start){
+	anchor.set('rows', rows);
+	anchor.set('start', start);
+	return anchor.update('search', rows, start);
+    };
+
+    /*
+     * Function: page_first
+     *
+     * Currently a convenience alias for <search>. Think about it--it
+     * makes sense.
+     * 
+     * This is a wrapper for <page> and should be preferred over a
+     * direct call to page.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  n/a
+     * 
+     * See also:
+     *  <page>
+     */
+    this.page_first = anchor.search;
+    
+    /*
+     * Function: page_previous
+     * 
+     * This is a wrapper for <page> and should be preferred over a
+     * direct call to page.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the query url (with the jQuery callback specific parameters)
+     * 
+     * See also:
+     *  <page>
+     */
+    this.page_previous = function(){
+	var do_rows = anchor.get_page_rows();
+	var do_offset = anchor.get_page_start() - do_rows;
+	return anchor.page(do_rows, do_offset);
+    };
+    
+    /*
+     * Function: page_next
+     * 
+     * This is a wrapper for <page> and should be preferred over a
+     * direct call to page.
+     * 
+     * Parameters: 
+     *  the query url (with the jQuery callback specific parameters)
+     *
+     * Returns:
+     *  n/a
+     * 
+     * See also:
+     *  <page>
+     */
+    this.page_next = function(){
+	var do_rows = anchor.get_page_rows();
+	var do_offset = anchor.get_page_start() + do_rows;
+	return anchor.page(do_rows, do_offset);
+    };
+    
+    /*
+     * Function: page_last
+     * 
+     * Trigger search on last page parameters.
+     * 
+     * Since the manager has no idea about what is actually being
+     * returned, the real world number of total documents needs to be
+     * added as an argument.
+     * 
+     * This is a wrapper for <page> and should be preferred over a
+     * direct call to page.
+     * 
+     * Parameters: 
+     *  total_document_count - integer for the total number of docs found
+     *
+     * Returns:
+     *  the query url (with the jQuery callback specific parameters)
+     * 
+     * See also:
+     *  <page>
+     */
+    this.page_last = function(total_document_count){
+	var do_rows = anchor.get_page_rows();
+	var mod = total_document_count % do_rows;
+	var do_offset = total_document_count - mod;
+	// ll("page_last: " + total_document_count + " " +
+	//    do_rows + " " + mod + " " + do_offset);
+	var ret = null;
+	if( mod == 0 ){
+	    ret = anchor.page(do_rows, do_offset - do_rows);
+	}else{
+	    ret = anchor.page(do_rows, do_offset);
+	}
+	return ret;
+    };
+
+    /*
+     * Function: get_page_rows
+     *
+     * Return the number of rows the manager is currently set
+     * to. Useful as an argument to <page>.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  integer; the number of rows the manager is currently set to
+     * 
+     * See also:
+     *  <page>
+     */
+    this.get_page_rows = function(){
+	return anchor.get('rows');
+    };
+
+    /*
+     * Function: get_page_start
+     *
+     * Return the rows offset the manager is currently set to. Useful
+     * as an argument to <page>.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  integer; the offset the manager is currently set to
+     * 
+     * See also:
+     *  <page>
+     */
+    this.get_page_start = function(){
+	return anchor.get('start');
+    };
+
+    /*
+     * Function: add_query_field
+     * 
+     * Add a new query field to the query. 
+     * 
+     * This does not go through and expand into searchable fields, for
+     * that see: <query_field_set>.
+     *
+     * Parameters: 
+     *  qf - the query field to add
+     *  boost - *[optional]* defaults to 1.0
+     *
+     * Returns:
+     *  true or false on whether or not it is a new field
+     * 
+     * See also:
+     *  <query_field_set>
+     */
+    this.add_query_field = function(qf, boost){
+	
+	var retval = false;
+
+	// Make sure that some boost is there.
+	if( ! bbop.core.is_defined(boost) ){
+	    boost = 1.0;
+	}
+
+	// Check.
+	if( ! bbop.core.is_defined(anchor.query_fields[qf]) ){
+	    retval = true;
+	}
+
+	// Add.
+	anchor.query_fields[qf] = boost;
+
+	return retval;
+    };
+
+    /*
+     * Function: query_field_set
+     *
+     * Bulk getter/setter for the query fields--the fields that are
+     * searched (and by what weight) when using a query ('q' or
+     * set_query(), i.e. the 'qf' field).
+     * 
+     * This will always use searchable fields if possible,
+     * automatically replacing the non-searchable versions (I can't
+     * think of any reason to use non-searchable versions unless you
+     * want your searches to not work) if a personality is set. If no
+     * personality is set, it will just use the arguments as-is.
+     * 
+     * The argument replaces the current set.
+     *
+     * The qfs argument should be a hash like:
+     * 
+     *  {'field01': value01, ...}
+     * 
+     * Parameters: 
+     *  qfs - *[optional]* query fields to set
+     *
+     * Returns:
+     *  the current query_fields array (e.g. ["field01^value01", ...])
+     */
+    this.query_field_set = function(qfs){
+
+	// Covenience.
+	var loop = bbop.core.each;
+	var cclass = anchor._current_class;
+
+	// Only do something if we have a query field set.
+	if( qfs ){
+	    
+	    // Only do the probing if a personality has been set.
+	    if( cclass ){
+
+		// Get the current searchable extension string from
+		// the personality class.
+		//var s_ext = cclass.searchable_extension();
+		// Actually, we're going to make this non-variable.
+		var s_ext = '_searchable';
+
+		// Probe the input to see if there are any searchable
+		// alternatives to try, use those instead.
+		var searchable_qfs = {};
+		loop(qfs,
+	    	     function(filter, value){
+			 // If the probe fails, just put in
+			 // whatever is there.
+			 var cfield = cclass.get_field(filter);
+			 if( cfield && cfield.searchable() ){
+			     //ll('filter/value:');
+			     var new_f = filter + s_ext;
+			     searchable_qfs[new_f] = value;
+			 }else{
+			     searchable_qfs[filter] = value;
+			 }
+	    	     });
+		qfs = searchable_qfs;
+	    }	    
+
+	    // Overwrite the current.
+	    anchor.query_fields = qfs;
+	}
+	
+	// Using the original information, convert them to the
+	// proper output format.
+	var output_format = [];
+	loop(anchor.query_fields,
+	     function(filter, value){
+		 output_format.push(filter + '^' + value);
+	     });
+	return output_format;
+    };
+
+    /*
+     * Function: facets
+     *
+     * Bulk getter/setter for facets (technically 'facet.field').
+     *
+     * Parameters: 
+     *  key - *[optional]* facet to add to the facet list
+     *
+     * Parameters: 
+     *  list - *[optional]* list to replace the current list with
+     *
+     * Returns:
+     *  the current facets hash.
+     */
+    this.facets = function(list_or_key){
+	if( list_or_key ){
+	    if( bbop.core.what_is(list_or_key) != 'array' ){
+		// Arrayify it.
+		list_or_key = [list_or_key];
+	    }else{
+		// When there is a list, we are replacing the whole
+		// thing, so let's just poof it out of existance.
+		anchor.facet_fields = {};
+	    }
+	    bbop.core.each(list_or_key,
+			   function(item){
+			       anchor.facet_fields[item] = true;
+			   });
+	}
+	return bbop.core.get_keys(anchor.facet_fields);
+    };
+
+    /*
+     * Function: set_default_query
+     *
+     * Setter for the default query for the query variable ('q').
+     * 
+     * Call <reset_query> if you want to affect query immediately.
+     * 
+     * Parameters: 
+     *  new_default_query - new default query string (or TODO: <bbop.logic>)
+     *
+     * Returns:
+     *  the current setting of default query for ('q')
+     */
+    this.set_default_query = function(new_default_query){
+	anchor.default_query = new_default_query;
+	return anchor.default_query;
+    };
+
+    // /*
+    //  * Function: set_first_run_query
+    //  *
+    //  * Setter for a first run query.  Normally, when <reset_query>, or
+    //  * related method, is executed, we reset back to the default
+    //  * query. This method sets a one time variable so a non empty
+    //  * value can be used for the first reset.
+    //  * 
+    //  * Call <reset_query> if you want to affect query immediately.
+    //  * 
+    //  * Parameters: 
+    //  *  first_run_query - query_string (or TODO: <bbop.logic>)
+    //  *
+    //  * Returns:
+    //  *  the current setting of default query for ('q')
+    //  */
+    // this.set_first_run_query = function(first_run_query){
+    // 	anchor.default_query = new_default_query;
+    // 	return anchor.default_query;
+    // };
+
+    /*
+     * Function: reset_default_query
+     *
+     * Reset the default query back to "*:*".
+     * 
+     * Call <reset_query> if you want to affect query immediately.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of default query ('q')
+     */
+    this.reset_default_query = function(){
+	anchor.default_query = anchor.fundamental_query;
+	return anchor.default_query;
+    };
+
+    /*
+     * Function: set_query
+     *
+     * Setter for the query variable ('q').
+     * 
+     * Parameters: 
+     *  new_query - new value for the query string (or TODO: <bbop.logic>)
+     *
+     * Returns:
+     *  the current setting of query ('q')
+     * 
+     * Also see:
+     *  <set_comfy_query>
+     */
+    this.set_query = function(new_query){
+	anchor.query = new_query;
+	return anchor.query;
+    };
+
+    /*
+     * Function: set_comfy_query
+     *
+     * A specialized setter for the query variable ('q'), as follows:
+     *
+     * If the input is all alphanum or space, the input is
+     * tokenized. The last token, if it is at least three characters,
+     * gets a wildcard '*'.
+     * 
+     * This might be a more comfortable way to search for most naive
+     * (non-power user) interfaces.
+     * 
+     * Parameters: 
+     *  new_query - new value for the query string (or TODO: <bbop.logic>)
+     *
+     * Returns:
+     *  the current setting of query ('q')
+     * 
+     * Also see:
+     *  <set_query>
+     */
+    this.set_comfy_query = function(new_query){
+
+	var comfy_query = new_query;
+
+	// Check that there is something there.
+	if( new_query && new_query.length && new_query.length > 0 ){
+
+	    // That it is alphanum+space-ish
+	    var alphanum = new RegExp(/^[a-zA-Z0-9 ]+$/);
+	    if( alphanum.test(new_query) ){
+	    
+		// Break it into tokens and get the last.
+		var tokens = new_query.split(new RegExp('\\s+'));
+		var last_token = tokens[tokens.length -1];
+		//ll('last: ' + last_token);
+		
+		// If it is three or more, add the wildcard.
+		if( last_token.length >= 3 ){
+		    tokens[tokens.length -1] = last_token + '*';
+
+		    // And join it all back into our comfy query.
+		    comfy_query = tokens.join(' ');
+		}
+	    }
+	}
+
+	// Kick it back to the normal set_query.
+	return anchor.set_query(comfy_query);
+    };
+
+    /*
+     * Function: set_id
+     *
+     * A limited setter, removing whatever else is on query. This is
+     * for when you want to lock into one (unique) document by id
+     * (essentially 'q=id:"foo"'). All other query operations behave
+     * as they should around it.
+     * 
+     * Parameters: 
+     *  new_id - string id
+     *
+     * Returns:
+     *  the current setting of query ('q')
+     * 
+     * Also see:
+     *  <set_ids>
+     */
+    this.set_id = function(new_id){
+	anchor.query = 'id:' + bbop.core.ensure(new_id, '"');
+	return anchor.query;
+    };
+
+    /*
+     * Function: set_ids
+     *
+     * Like <set_id>, a limited setter. It removes whatever else is on
+     * query and replaces it with something like:
+     * 
+     * : gm.get_download_url(['id', 'score'], {'entity_list':['GO:1', 'GO:2']})
+     * : http://golr.berkeleybop.org/select?defType=edismax&qt=standard&indent=on&wt=csv&rows=1000&start=0&fl=id,score&facet=true&facet.mincount=1&facet.sort=count&json.nl=arrarr&facet.limit=25&csv.encapsulator=&csv.separator=%09&csv.header=false&csv.mv.separator=%7C&q=id:(%22GO:1%22%20OR%20%22GO:2%22)
+     * 
+     * This is for when you want to lock into a set of documents. All
+     * other query operations behave as they should around it.
+     * 
+     * Parameters: 
+     *  id_list - a list of ids to search for
+     *
+     * Returns:
+     *  the current setting of query ('q')
+     * 
+     * Also see:
+     *  <set_ids>
+     */
+    this.set_ids = function(id_list){
+
+	var fixed_list = [];
+	bbop.core.each(id_list,
+		       function(item){
+			   fixed_list.push(bbop.core.ensure(item, '"'));
+		       });
+
+	var base_id_list = '(' + fixed_list.join(' OR ') + ')';
+
+	anchor.query = 'id:' + base_id_list;
+	return anchor.query;
+    };
+
+    /*
+     * Function: get_query
+     *
+     * Getter for the query variable ('q').
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of extra
+     */
+    this.get_query = function(){
+	return anchor.query;
+    };
+
+    /*
+     * Function: get_default_query
+     *
+     * Getter for what the query variable 'q' will be set to on a
+     * <reset_query>.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of the default query
+     */
+    this.get_default_query = function(){
+	return anchor.default_query;
+    };
+
+    /*
+     * Function: get_fundamental_query
+     *
+     * Getter for what the query variable 'q' will be set to on a
+     * <reset_default_query>.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of the fundamental default query
+     */
+    this.get_fundamental_query = function(){
+	return anchor.fundamental_query;
+    };
+
+    /*
+     * Function: get_query
+     *
+     * Getter for the query variable ('q').
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of extra
+     */
+    this.get_query = function(){
+	return anchor.query;
+    };
+
+    /*
+     * Function: reset_query
+     *
+     * Remove/reset the query variable ('q'); this set it back to the
+     * default query.
+     *
+     * Parameters:
+     *  none
+     *
+     * Returns:
+     *  the current value of query
+     * 
+     * Also see:
+     *  <set_default_query>
+     *  <reset_default_query>
+     */
+    this.reset_query = function(){
+	anchor.query = anchor.default_query;
+	ll('reset query to default: ' + anchor.query);
+	return anchor.query;
+    };
+
+    /*
+     * Function: set_extra
+     *
+     * Setter for the internal string variable to be appended to the
+     * end of a query. For special use cases only (e.g. extend
+     * functionality of the API safely).
+     * 
+     * Parameters: 
+     *  new_extra - *[optional]* new value for the extras string
+     *
+     * Returns:
+     *  the current setting of extra
+     */
+    this.set_extra = function(new_extra){
+	anchor.query_extra = new_extra;
+	return anchor.query_extra;
+    };
+
+    /*
+     * Function: get_extra
+     *
+     * Getter for the internal string variable to be appended
+     * to the end of a query.
+     *
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  the current setting of extra
+     */
+    this.get_extra = anchor.set_extra;
+
+    /*
+     * Function: remove_extra
+     *
+     * Remove/reset the extra bit.
+     *
+     * Parameters:
+     *  none
+     *
+     * Returns:
+     *  ""
+     */
+    this.remove_extra = function(){
+	anchor.query_extra = "";
+	return anchor.query_extra;
+    };
+
+    /*
+     * Function: set
+     *
+     * Set an internal variable for the query. The internal variables
+     * are typically things like 'qt', 'indent', etc.--things that you
+     * might set and forget a while. It does /not/ include highly
+     * dynamic variables (like callback and packet) or querying
+     * variables like 'q' and 'fq'; for those you need to use the API.
+     *
+     * Parameters: 
+     *  key - the name of the parameter to change
+     *  new_val - what you want the new value to be
+     *
+     * Returns:
+     *  n/a
+     */
+    this.set = function(key, new_val){
+	anchor.query_variants[key] = new_val;
+    };
+
+    /*
+     * Function: get
+     *
+     * Get an internal variable for the query.
+     *
+     * See <set> for the kinds of parameters that can be read.
+     * 
+     * Parameters: 
+     *  key - the name of the parameter to get
+     *
+     * Returns:
+     *  The found value of the key.
+     */
+    this.get = function(key){
+	return anchor.query_variants[key];
+    };
+
+    /*
+     * Function: unset
+     *
+     * Unset (remove) an internal variable for the query. Only usable on certain types of 
+     * 
+     * Only use is you really know what you're doing.
+     *
+     * Parameters: 
+     *  key - the name of the parameter to unset/remove
+     *
+     * Returns:
+     *  boolean; true false on whether the key was found
+     */
+    this.unset = function(key){
+	var retval = false;
+
+	if( bbop.core.is_defined(anchor.query_variants[key]) ){
+	    retval = true;
+	    delete anchor.query_variants[key];
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: include_highlighting
+     *
+     * Turn hilighting on or off (with true or false).
+     * 
+     * This essentially adds the parameters to the query string to
+     * make sure that basic highlighting on the search is returned.
+     * 
+     * It starts off as false. The optional html_elt_str argument
+     * defaults to:
+     *  : <em class="hilite">
+     *
+     * Parameters: 
+     *  hilite_p - *[optional]* boolean
+     *  html_elt_str - *[serially optional]* the HTML element string to use
+     *
+     * Returns:
+     *  either false or the current string being used for the return element
+     */
+    this.include_highlighting = function(hilite_p, html_elt_str){
+	var retval = false;
+
+	if( bbop.core.is_defined(hilite_p) &&
+	    (hilite_p == true || hilite_p == false) ){
+	    if( hilite_p == true ){
+
+		// Set the default string if necessary.
+		if( ! html_elt_str ){ html_elt_str = '<em class="hilite">'; }
+
+		// Set the parameters.
+		anchor.set('hl', 'true');
+		anchor.set('hl.simple.pre', html_elt_str);
+
+		// And the retval is not longer false.
+		retval = html_elt_str;
+
+	    }else{
+		
+		// Unset the parameters.
+		anchor.unset('hl');
+		anchor.unset('hl.simple.pre');
+	    }
+
+	}else{
+	    // Otherwise, just discover the current state and return
+	    // it.
+	    var cl_tmp = anchor.get('hl.simple.pre');
+	    if( bbop.core.is_defined(cl_tmp) ){
+		retval = cl_tmp;
+	    }
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: set_personality
+     *
+     * While we are always contacting the same Solr instance, we
+     * sometimes want to have different weights, facets, etc. This
+     * function allows us to use the pre-set ones defined in the
+     * constructor configuration argument.
+     * 
+     * Currently, this only sets the 'facet.field' internal variable.
+     *
+     * Parameters: 
+     *  personality_id - string
+     *
+     * Returns:
+     *  Will return false if personality doesn't exist
+     */
+    this.set_personality = function(personality_id){
+	var retval = false;
+
+	// This sets the facet.field internal variable.
+	var cclass = anchor._golr_conf.get_class(personality_id);
+	if( cclass ){
+
+	    // Remember what our personality is.
+	    // WARNING: this line must go before the query_field_set
+	    // line below, or else we won't get the "smart" search.
+	    this._current_class = cclass;
+
+	    // Set the facets for our class.
+	    anchor.facets(cclass.field_order_by_weight('filter'));
+
+	    // Set the query field weights ('qf') necessary to make
+	    // queries run properly.
+	    anchor.query_field_set(cclass.get_weights('boost'));
+	    
+	    // Show that we did indeed set a personality.
+	    retval = true;
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: get_personality
+     *
+     * Returns the current personality, null if none.
+     * 
+     * Parameters: 
+     *  n/a
+     *
+     * Returns:
+     *  Returns the current personality as a string, null if none is set
+     */
+    this.get_personality = function(){
+	var retval = null;
+
+	if( bbop.core.is_defined(anchor._current_class) &&
+	    bbop.core.what_is(anchor._current_class) == 'bbop.golr.conf_class'){
+	    retval = anchor._current_class.id();
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: get_query_url
+     *
+     * Get the current invariant state of the manager returned as a
+     * encoded URL string (using encodeURI()).
+     * 
+     * This means the URL for the current query to the GOlr store, but
+     * without extra information about packets, callbacks, and the
+     * like.
+     * 
+     * This is generally appropriate for getting data, but maybe not
+     * for things like high-speed autocomplete where races can
+     * occur. For those, you might want to consider <update> or
+     * <search>.
+     *
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  URL string
+     * 
+     * Also see:
+     *  <update>, <search>
+     */
+    this.get_query_url = function(){
+
+	// Structure of the necessary invariant parts.	
+	var qurl = anchor._solr_url + 'select?';
+
+	// Get all of our query filter variables and try and
+	// make something of them that get_assemble can
+	// understand. Sticky doesn't matter here, but negativity
+	// does. However, we can be pretty naive since the hashing
+	// should have already taken out mutually exclusive dupes.
+	var fq = {};
+	var loop = bbop.core.each;
+	loop(anchor.get_query_filters(),
+	     function(filter_property){
+
+		 // Grab only the properties that affect the
+		 // URL.
+		 var filter = filter_property['filter'];
+		 var value = filter_property['value'];
+		 var negative_p = filter_property['negative_p'];
+
+		 // We need to alter at the filter level.
+		 if( negative_p ){
+		     filter = '-' + filter;
+		 }
+
+		 // Make sure it is defined.
+		 if( ! bbop.core.is_defined(fq[filter]) ){
+		     fq[filter] = [];
+		 }
+		 fq[filter].push(value);
+	     });
+
+	// Add all of our different specialized hashes.
+	var things_to_add = [
+	    //bbop.core.get_assemble(anchor.query_invariants),
+	    //bbop.core.get_assemble(anchor.query_facets),
+	    bbop.core.get_assemble(anchor.query_variants),
+	    bbop.core.get_assemble(anchor.current_facet_field_limits),
+	    //bbop.core.get_assemble({'fq': anchor.query_sticky_filters}),
+	    bbop.core.get_assemble({'fq': fq}),
+	    bbop.core.get_assemble({'facet.field':
+				    bbop.core.get_keys(anchor.facet_fields)}),
+	    bbop.core.get_assemble({'q': anchor.query}),
+	    anchor.query_extra
+	];
+	// Add query_fields ('qf') iff query ('q') is set and it is
+	// not length 0.
+	if( anchor.query &&
+	    anchor.query.length &&
+	    anchor.query.length != 0 &&
+	    anchor.query != anchor.fundamental_query ){
+		var in_qf =
+		    bbop.core.get_assemble({'qf': anchor.query_field_set()});
+		things_to_add.push(in_qf);
+	    }
+	
+	// Assemble the assemblies into a single URL, throw out
+	// everything that seems like it isn't real to keep the URL as
+	// clean a possible.
+	var filtered_things = 
+	    bbop.core.pare(things_to_add,
+			   function(item, index){
+			       var retval = true;
+			       if( item && item != '' ){
+				   retval = false;
+			       }
+			       return retval;
+			   });
+
+	var final_qurl = qurl + filtered_things.join('&');
+	// Spaces can cause problems in URLs in some environments.
+	//final_qurl = final_qurl.replace(/ /g, '%20');
+	// Convert the URL into something more usable.
+	// Because we internally use %09 as a special case, make sure
+	// we don't double-up on it.
+	var fs1 = encodeURI(final_qurl);
+	var fs2 = fs1.replace(/\%2509/g, '%09');
+	final_qurl = fs2;
+
+	ll('qurl: ' + final_qurl);
+    	return final_qurl;
+    };
+
+    /*
+     * Function: push_excursion
+     *
+     * Save the current state of the manager--data and sticky filter
+     * information--onto an internal stack. Batch information is not
+     * stored.
+     * 
+     * Useful for gettinginto a state, doing something else, then
+     * returning to the original state.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  the number of items on the excursion stack
+     * 
+     * Also see:
+     *  <get_query_url>
+     *  <pop_excursion>
+     */
+    this.push_excursion = function(){
+	
+	var now = {
+	    // Save current state (data).
+	    data_url: anchor.get_query_url(),
+	    // Save current state (session).
+	    session: {
+		// Get the sticky filters.
+		sticky_filters: anchor.get_sticky_query_filters()
+	    }
+	};
+
+	// Save.
+	anchor._excursions.push(now);
+
+	// ...
+    	return anchor._excursions.length;
+    };
+
+    /*
+     * Function: pop_excursion
+     *
+     * Return to a previously pushed state. Batch items are not
+     * recovered.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  boolean on whether a state was recovered
+     * 
+     * Also see:
+     *  <get_query_url>
+     *  <gpush_excursion>
+     */
+    this.pop_excursion = function(){
+	
+	var retval = false;
+
+	var then = anchor._excursions.pop();
+	if( then ){
+	    retval = true;
+
+	    // Recover data state.
+	    var then_data_url = then['data_url'];
+	    anchor.load_url(then_data_url);
+
+	    // Recover the session state.
+	    var then_session_stickies = then['session']['sticky_filters'];
+	    // Add the sticky filters.
+	    bbop.core.each(then_session_stickies,
+			   function(sticky){
+			       var flt = sticky['filter'];
+			       var fvl = sticky['value'];
+			       var fpl = [];
+			       if( sticky['negative_p'] == true ){
+				   fpl.push('-');
+			       }
+			       if( sticky['sticky_p'] == true ){
+				   fpl.push('*');
+			       }
+			       anchor.add_query_filter(flt, fvl, fpl);
+			   });	    
+	}
+
+    	return retval;
+    };
+
+    /*
+     * Function: get_download_url
+     *
+     * Get the current invariant state of the manager returned as a
+     * URL string.
+     * 
+     * This differs from <get_query_url> in that the generated string
+     * is intended for text-processing uses rather than computerized
+     * searching uses. The idea where is to create a TSV file for
+     * downloading and consumption.
+     * 
+     * Instead of downloading all of the results, a limited listed set
+     * can be downloaded using entity_list, which identifies documents by id.
+     * 
+     * The optional argument hash looks like:
+     *  rows - the number of rows to return; defaults to: 1000
+     *  encapsulator - how to enclose whitespace fields; defaults to: ""
+     *  separator - separator between fields; defaults to: "%09" (tab)
+     *  header - whether or not to show headers; defaults to: "false"
+     *  mv_separator - separator for multi-valued fields; defaults to: "|"
+     *  entity_list - list of specific download items in results; default null
+     * 
+     * With the entity list, keep in mind that null and an empty list
+     * are handled in pretty much the same way--they are an indication
+     * that we are going after nothing specific, and so all results
+     * are game.
+     * 
+     * Parameters:
+     *  field_list - a list of fields to return
+     *  in_arg_hash - *[optional]* additional optional arguments
+     * 
+     * Returns:
+     *  URL string
+     * 
+     * Also see:
+     *  <get_query_url>
+     */
+    this.get_download_url = function(field_list, in_arg_hash){
+	
+	// Save current state.
+	anchor.push_excursion();
+
+	// Deal with getting arguments in properly.
+	var default_hash =
+	    {
+		rows : 1000,
+		encapsulator : '',
+		separator : "%09",
+		header : 'false',
+		mv_separator : "|",
+		entity_list : []
+	    };
+	var arg_hash = bbop.core.fold(default_hash, in_arg_hash);
+
+	// Make the changes we want.
+	anchor.set('wt', 'csv');
+	anchor.set('start', 0);
+	anchor.set('fl', field_list.join(','));
+	anchor.set('rows', arg_hash['rows']);
+	anchor.set('csv.encapsulator', arg_hash['encapsulator']);
+	anchor.set('csv.separator', arg_hash['separator']);
+	anchor.set('csv.header', arg_hash['header']);
+	anchor.set('csv.mv.separator', arg_hash['mv_separator']);
+
+	// A little more tricky, jimmy the entity list into the query
+	// if it's viable.
+	var entity_list = arg_hash['entity_list'];
+	if( bbop.core.is_defined(entity_list) &&
+	    bbop.core.is_array(entity_list) &&
+	    entity_list.length > 0 ){
+		anchor.set_ids(entity_list);
+	}
+
+	// Get url.
+	var returl = anchor.get_query_url();
+
+	// Reset the old state.
+	anchor.pop_excursion();
+
+    	return returl;
+    };
+
+    /*
+     * Function: get_state_url
+     *
+     * Get the current invariant state of the manager, plus the
+     * current personality as a paramater, returned as a URL string.
+     * 
+     * This differs from <get_query_url> in that the generated string
+     * is intended for applications that may want a little more
+     * information and hinting over just what the current search
+     * is. This method essentially parameterizes some of the "hidden
+     * state" of the manager.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  URL string
+     * 
+     * Also see:
+     *  <get_query_url>
+     */
+    this.get_state_url = function(){
+	
+	// Save current state.
+	anchor.push_excursion();
+
+	// Make the changes we want. First, physically set the
+	// "personality", then set pins for jump-in recovery.
+	anchor.set('personality', anchor.get_personality());
+
+	// Explicitly set sticky pins for later recovery.
+	// Do this pretty much exactly like we do for get_query_url().
+	var sticky_filters = anchor.get_sticky_query_filters();
+	var sfq = {};
+	bbop.core.each(sticky_filters,
+		       function(sticky_filter){
+
+			   var filter = sticky_filter['filter'];
+			   var value = sticky_filter['value'];
+			   var negative_p = sticky_filter['negative_p'];
+
+			   if( negative_p ){
+			       filter = '-' + filter;
+			   }
+
+			   // Make sure it is defined.
+			   if( ! bbop.core.is_defined(sfq[filter]) ){
+			       sfq[filter] = [];
+			   }
+			   sfq[filter].push(value);
+		       });
+	anchor.set('sfq', sfq);
+	
+	// Get url.
+	var returl = anchor.get_query_url();
+
+	// Reset the old state.
+	anchor.pop_excursion();
+
+    	return returl;
+    };
+
+    /*
+     * Function: load_url
+     *
+     * Makes a a best attempt to recover the state of a manager from
+     * the clues left in a data url. This can also (and probably
+     * should) be thought of as a "load bookmark"
+     * function. Theoretically, you should even be able to use
+     * "bookmarks" from alien installations.
+     * 
+     * Note that while this recovers enough to get the same data,
+     * certain "session"/"preference" type things that are not encoded
+     * in the url (e.g. filter stickiness, the contents of batch
+     * queues, non-default base queries, etc.) will not be replayed
+     * and must be recovered or guessed on an app by app basis..
+     * 
+     * Warning: this currently only replays a small subset of possible
+     * parameters. Currently: personality, q, fq, ???. In the future,
+     * this should no all non-session information.
+     * 
+     * Warning: Because there is more to bookmarks than just the major
+     * stuff, variants not supplied in the bookmark will be removed.
+     * 
+     * This returns true if the parameter portions of the new and
+     * bookmark urls match. However, this is often not the case--think
+     * shifting personalities, etc.
+     * 
+     * Parameters:
+     *  url - A URL string generated by a manager's get_query_url (or similar)
+     * 
+     * Returns:
+     *  boolean
+     */
+    this.load_url = function(url){
+
+	var loop = bbop.core.each;
+
+	// // Some Regexps that would be nice to just compile once.
+	// var regexp_url_space = /\%20/g; // '%20' == ' '
+	// var regexp_url_quote = /\%22/g; // '%22' == '"'
+	// var regexp_url_left_paren = /\%28/g; // '%28' == '('
+	// var regexp_url_right_paren = /\%29/g; // '%29' == ')'
+
+	// We are assuming that we are consuming our own URLs from
+	// get_query_url(), so we start by attempting to decode it
+	// (TODO: need a tab watch here)?
+	var decoded_url = decodeURI(url);
+
+	// Break down url.
+	var in_params = bbop.core.url_parameters(decoded_url);
+
+	// First, look for the personality setting and invoke it if
+	// it's there--it will dominate unless we take care of it first.
+	// Also note the all the keys that we see (for later erasure
+	// of excess).
+	var seen_params = {};
+	loop(in_params,
+	     function(ip){
+		 var key = ip[0];
+		 var val = ip[1];
+		 if( key == 'personality' && val && val != '' ){
+		     anchor.set_personality(val);
+		 }
+		 seen_params[key] = true;
+	     });
+	
+	// Now cycle through the the parameters again and invoke the
+	// appropriate functions to bring them in line.
+	var sticky_cache = {};
+	loop(in_params,
+	     function(ip){
+		 var key = ip[0];
+		 var val = ip[1];
+		 if( bbop.core.is_defined(val) && val != '' ){
+		     if( key == 'personality' ){
+			 // Already did it, skip.
+		     }else if( key == 'q' ){
+			 anchor.set_query(val);
+		     }else if( key == 'fq' || key == 'sfq' ){
+			 // Split the fq (or sfq) parameter.
+			 var fnv = bbop.core.first_split(':', val);
+			 var fname = fnv[0];
+			 var fval = fnv[1];
+			 //ll('HERE: fname: ' + fname);
+			 //ll('HERE: fval: ' + fval);
+			 if( fname && fval ){
+
+			     var plist = [];
+
+			     // Remove leading sign on a filter and
+			     // add it to the plist.
+			     var lead_char = fname.charAt(0);
+			     if( lead_char == '-' || lead_char == '+' ){
+				 plist.push(lead_char);
+				 fname = fname.substr(1, fname.length -1);
+			     }
+
+			     // // TODO: 
+			     // // If the fval looks like it has not been
+			     // // decoded (like from a URL-safe
+			     // // bookmark), go ahead and do so.
+			     // fval = fval.replace(regexp_url_space, ' ');
+			     // fval = fval.replace(regexp_url_quote, '"');
+			     // fval = fval.replace(regexp_url_left_paren, '(');
+			     // fval = fval.replace(regexp_url_right_paren,')');
+
+			     // Do not allow quotes in--they will be
+			     // added by the assembler.
+			     fval = bbop.core.dequote(fval);
+
+			     // Make it sticky it it came in on "sfq".
+			     // Note if this is the sticky form.
+			     var skey = fname + '^' + fval;
+			     if( key == 'sfq' ){
+				 sticky_cache[skey] = true;
+				 plist.push('*');
+			     }
+
+			     // Add the query filter properly, but
+			     // only if we have not already added the
+			     // sticky form (prevent clobbering).
+			     if( ! bbop.core.is_defined(sticky_cache[skey]) ||
+				 key == 'sfq'){
+				 anchor.add_query_filter(fname, fval, plist);
+				 
+			     }
+			 }
+		     }else if( key == 'qf' ){
+			 // qf is handles a little strangely...
+			 var foo = bbop.core.first_split('^', val);
+			 //ll('qf: key: '+ key +', val: '+ val +', foo: '+ foo);
+			 anchor.add_query_field(foo[0], foo[1]);
+		     }else if( key == 'facet.field' ){
+		      	 anchor.facets(val);
+		     }else if( key == 'start' || key == 'rows' ){
+			 // Numbers need to be handled carefully.
+			 if( bbop.core.what_is(val) == 'string' ){
+			     val = parseFloat(val);
+			 }
+		      	 anchor.set(key, val);
+		     }else{
+			 // This one catches all of the non-special
+			 // parameters and resets them using .set().
+			 anchor.set(key, val);
+			 // if( key == 'fq' ){
+			 //     throw new Error("OI");			     
+			 // }
+		     }
+		 }
+	     });
+
+	// Now go through and remove all of the query variant
+	// parameters that were not seen in the bookmark.
+	loop(anchor.query_variants,
+	     function(key, val){
+		 if( ! bbop.core.is_defined(seen_params[key]) ){
+		     anchor.unset(key);
+		 }
+	     });
+
+	// Produce our own url from what we've done. If the parameters
+	// match with the incoming argument's return true.
+	var curr_url = anchor.get_query_url();
+	var curr_params = bbop.core.url_parameters(curr_url);
+	var differences = 0;
+	if( in_params.length == curr_params.length ){
+	    loop(in_params,
+		 function(in_p, i){
+		     var curr_p = curr_params[i];
+		     if( in_p.length == curr_p.length ){
+			 if( in_p.length == 1 ){
+			     if( in_p[0] == curr_p[0] ){
+				 // match!
+			     }else{
+				 differences++;
+			     }
+			 }else if( in_p.length == 2 ){
+			     if( in_p[0] == curr_p[0] && in_p[1] == curr_p[1] ){
+				 // match!
+			     }else{
+				 differences++;
+			     }
+			 }
+		     }else{
+			 differences++;
+		     }
+		 });
+	}else{
+	    differences++;
+	}
+
+	// Tally the differences and decides if they're the same.
+	var retval = false;
+	if( differences == 0 ){
+	    retval = true;
+	}
+    	return retval;
+    };
+
+    /*
+     * Function: add_to_batch
+     *
+     * "Save" the current manager state to run later in serial batch
+     * mode.
+     * 
+     * The actual job of running these batches is left to the
+     * implementation of the sub-managers; probably in "run_batch".
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  state url
+     */
+    this.add_to_batch = function(){
+	var qurl = anchor.get_query_url();
+	anchor._batch_urls.push(qurl);
+    	return qurl;
+    };
+
+    /*
+     * Function: batch_urls
+     *
+     * Return a pointer to the current batch urls.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  array
+     */
+    this.batch_urls = function(){
+    	return anchor._batch_urls;
+    };
+
+    /*
+     * Function: next_batch_url
+     *
+     * Return the next data to be processed, removing it from the
+     * batch queue in the process.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  state url or null
+     */
+    this.next_batch_url = function(){
+    	return anchor._batch_urls.shift() || null;
+    };
+
+    /*
+     * Function: reset_batch
+     *
+     * Clear the currently queued data batch.
+     * 
+     * The actual job of running these batches is left to the
+     * implementation of the sub-managers; probably in "run_batch".
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns:
+     *  the number of items cleared
+     */
+    this.reset_batch = function(){
+	var num = anchor._batch_urls.length;
+	anchor._batch_urls = [];
+    	return num;
+    };
+};
+bbop.core.extend(bbop.golr.manager, bbop.registry);
+
+/*
+ * Function: update
+ *
+ * The user code to select the type of update (and thus the type
+ * of callbacks to be called on data return).
+ * 
+ * This mechanism adds a couple of variables over other methods
+ * for bookkeeping: packet (incremented every time) and callback_type.
+ * 
+ * The currently recognized callback types are "reset" (for when you
+ * are starting or starting over) and "search" (what you typically
+ * want when you get new data) and "error" for when something went
+ * wrong. But only "search" and "reset" manipulate the system.
+ * 
+ * If rows or start are not set, they will both be reset to their
+ * initial values--this is to allow for paging on "current"
+ * results and then getting back to the business of searching with
+ * as little fuss as possible. Because of things like this, one
+ * should avoid calling this directly whenever possible and prefer
+ * simpler functionality of the wrapper methods: <search>,
+ * <reset>, and <page>.
+ * 
+ * Parameters: 
+ *  callback_type - callback type string; 'search', 'reset' and 'error'
+ *  rows - *[optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url (with the jQuery callback specific parameters)
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.prototype.update = function(callback_type, rows, start){
+
+    //function ll(s){ this._logger.kvetch(s); }
+
+    // Handle paging in this main section by resetting to
+    // the defaults if rows and offset are not explicitly
+    // defined.
+    if( ! bbop.core.is_defined(rows) || ! bbop.core.is_defined(start) ){
+    	this.set('rows', this.current_rows);
+    	this.set('start', this.current_start);
+    }
+    
+    // Our bookkeeping--increment packet.
+    this.last_sent_packet = this.last_sent_packet + 1;
+    
+    // Necessary updated query variants.
+    var update_query_variants = {
+    	packet: this.last_sent_packet,
+    	callback_type: callback_type
+    };
+    var update_qv = bbop.core.get_assemble(update_query_variants);
+    
+    // Structure of the necessary invariant parts.	
+    //var qurl = this.get_query_url();
+    var qurl = null;
+    
+    // Conditional merging of the remaining variant parts.
+    if( callback_type == 'reset' ){
+	
+    	// Take everything back to the initial state--this means
+    	// resetting the query and removing all non-sticky
+    	// filters.
+	
+    	// Reset and do completely open query.
+    	//ll('reset assembly');
+	
+    	// Save the q vals, do a fundamental get, then reset to
+    	// what we had.
+    	//var tmp_save = this.get_query();
+    	//this.reset_default_query();
+    	this.reset_query();
+    	this.reset_query_filters();
+    	qurl = this.get_query_url();
+    	qurl = qurl + '&' + update_qv;
+    	//this.set_query(tmp_save);
+	
+    }else if( callback_type == 'search' ){
+	
+    	//ll('search assembly');
+    	qurl = this.get_query_url();
+    	qurl = qurl + '&' + update_qv;
+	
+    }else{
+    	throw new Error("Unknown callback_type: " + callback_type);
+    }
+    
+    //ll('qurl: ' + qurl);
+    return qurl;
+};
+/* 
+ * Package: preload.js
+ * 
+ * Namespace: bbop.golr.manager.preload
+ * 
+ * Preload BBOP manager for dealing with remote calls. Remember,
+ * this is actually a "subclass" of <bbop.golr.manager>.
+ * 
+ * This is synchronous.
+ * 
+ * This is mostly for testing purposes.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.require('bbop', 'golr', 'manager');
+bbop.core.namespace('bbop', 'golr', 'manager', 'preload');
+
+/*
+ * Constructor: preload
+ * 
+ * Contructor for the GOlr query manager.
+ * 
+ * Allows preloading of the returned document.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.golr.manager>
+ */
+bbop.golr.manager.preload = function (golr_loc, golr_conf_obj){
+    bbop.golr.manager.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.golr.manager.preload';
+
+    // The only property to add.
+    this._bgm_load = null;
+};
+bbop.core.extend(bbop.golr.manager.preload, bbop.golr.manager);
+
+/*
+ * Function: load
+ *
+ * Parameters: 
+ *  thing - what to send to the callbacks
+ *
+ * Returns:
+ *  n/a
+ */
+bbop.golr.manager.preload.prototype.load = function(thing){
+    this._bgm_load = thing;    
+};
+
+/*
+ * Function: update
+ *
+ *  See the documentation in <golr_manager.js> on update to get more
+ *  of the story. This override function adds a trigger that can be
+ *  preloaded with results. Really only for testing.
+ *
+ * Parameters: 
+ *  callback_type - callback type string
+ *  rows - *[serially optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.preload.prototype.update = function(callback_type,
+						      rows, start){
+    // Get "parents" url first.
+    var parent_update = bbop.golr.manager.prototype.update;
+    var qurl = parent_update.call(this, callback_type, rows, start);
+
+    // 
+    var logger = new bbop.logger(this._is_a);
+    //this._logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    // Grab the data from the server and pick the right callback group
+    // accordingly.
+    var json_data = this._bgm_load;    
+    if( bbop.core.is_defined(json_data) ){
+	var response = new bbop.golr.response(json_data);
+	this.apply_callbacks(callback_type, [response, this]);
+    }else{
+	this.apply_callbacks('error', ['unparsable json data', this]);
+    }
+
+    return qurl;
+};
+/* 
+ * Package: nodejs.js
+ * 
+ * Namespace: bbop.golr.manager.nodejs
+ * 
+ * NodeJS BBOP manager for dealing with remote calls. Remember,
+ * this is actually a "subclass" of <bbop.golr.manager>.
+ * 
+ * This may be madness.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.require('bbop', 'golr', 'manager');
+bbop.core.namespace('bbop', 'golr', 'manager', 'nodejs');
+
+/*
+ * Constructor: nodejs
+ * 
+ * Contructor for the GOlr query manager; NodeJS flavor. YMMV.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.golr.manager>
+ */
+bbop.golr.manager.nodejs = function (golr_loc, golr_conf_obj){
+//function GOlrManager(in_args){
+    // We are a registry like this:
+    bbop.golr.manager.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.golr.manager.nodejs';
+
+    // Get a good self-reference point.
+    //var anchor = this;
+
+    // Per-manager logger.
+    //this._ll = ll;
+
+    // //
+    // ll('Alive.');
+
+};
+bbop.core.extend(bbop.golr.manager.nodejs, bbop.golr.manager);
+
+/*
+ * Function: update
+ *
+ *  See the documentation in <golr_manager.js> on update to get more
+ *  of the story. This override function adds functionality to NodeJS.
+ *
+ * Parameters: 
+ *  callback_type - callback type string
+ *  rows - *[serially optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url (with any NodeJS specific paramteters)
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.nodejs.prototype.update = function(callback_type,
+						     rows, start){
+    // Get "parents" url first.
+    var parent_update = bbop.golr.manager.prototype.update;
+    var qurl = parent_update.call(this, callback_type, rows, start);
+
+    // 
+    var logger = new bbop.logger(this._is_a);
+    //this._logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    var anchor = this;
+    this.last = null;
+    
+    //
+    function on_error(e) {
+	console.log('problem with request: ' + e.message);
+    }
+    function on_connect(res){
+	//console.log('STATUS: ' + res.statusCode);
+	//console.log('HEADERS: ' + JSON.stringify(res.headers));
+	res.setEncoding('utf8');
+	var raw_data = '';
+	res.on('data', function (chunk) {
+		   //console.log('BODY: ' + chunk);
+		   raw_data = raw_data + chunk;
+	       });
+	// Parse JS and call callback_type callbacks.
+	res.on('end', function () {
+		   var json_data = JSON.parse(raw_data);
+		   anchor.last = json_data;
+		   var response = new bbop.golr.response(json_data);
+		   anchor.apply_callbacks(callback_type, [response, anchor]);
+	       });
+    }
+    //debugger;
+    // WARNING: This should actually be passed in by the context.
+    //var http = require('http');
+    var req = http.request(qurl, on_connect);
+    req.on('error', on_error);
+    
+    // write data to request body
+    //req.write('data\n');
+    //req.write('data\n');
+    req.end();
+    
+    return qurl;
+};
+/* 
+ * Package: rhino.js
+ * 
+ * Namespace: bbop.golr.manager.rhino
+ * 
+ * Rhino BBOP manager for dealing with remote calls. Remember,
+ * this is actually a "subclass" of <bbop.golr.manager>.
+ * 
+ * This may be madness.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.require('bbop', 'golr', 'manager');
+bbop.core.namespace('bbop', 'golr', 'manager', 'rhino');
+
+/*
+ * Constructor: rhino
+ * 
+ * Contructor for the GOlr query manager; Rhino-style.
+ * 
+ * Beware that this version is a synchronous call.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.golr.manager>
+ */
+bbop.golr.manager.rhino = function (golr_loc, golr_conf_obj){
+    bbop.golr.manager.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.golr.manager.rhino';
+};
+bbop.core.extend(bbop.golr.manager.rhino, bbop.golr.manager);
+
+/*
+ * Function: update
+ *
+ *  See the documentation in <golr_manager.js> on update to get more
+ *  of the story. This override function adds functionality to Rhino.
+ *
+ * Parameters: 
+ *  callback_type - callback type string
+ *  rows - *[serially optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url (with any Rhino specific paramteters)
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.rhino.prototype.update = function(callback_type,
+						    rows, start){
+    // Get "parents" url first.
+    var parent_update = bbop.golr.manager.prototype.update;
+    var qurl = parent_update.call(this, callback_type, rows, start);
+
+    // 
+    var logger = new bbop.logger(this._is_a);
+    //this._logger = new bbop.logger(this._is_a);
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    // Grab the data from the server and pick the right callback group
+    // accordingly.
+    var raw = readUrl(qurl); // in Rhino
+    var json_data = null;
+    if( raw && raw != '' ){
+	json_data = JSON.parse(raw);
+	if( json_data ){
+	    var response = new bbop.golr.response(json_data);
+	    this.apply_callbacks(callback_type, [response, this]);
+	}else{
+	    this.apply_callbacks('error', ['unparsable data', this]);
+	}
+    }else{
+	this.apply_callbacks('error', ['no data', this]);
+    }
+
+    return qurl;
+};
+
+/*
+ * Function: fetch
+ *
+ * This is the synchronous data getter for Rhino--probably your best
+ * bet right now for scripting.
+ * 
+ * Parameters:
+ *  n/a 
+ *
+ * Returns:
+ *  <bbop.golr.response> or null
+ * 
+ * Also see:
+ *  <update>
+ */
+bbop.golr.manager.rhino.prototype.fetch = function(){
+    
+    var qurl = this.get_query_url();
+
+    // Grab the data from the server and pick the right callback group
+    // accordingly.
+    var raw = readUrl(qurl); // in Rhino
+    var json_data = null;
+    var retval = null;
+    if( raw && raw != '' ){
+	json_data = JSON.parse(raw);
+	if( json_data ){
+	    var response = new bbop.golr.response(json_data);
+	    retval = response;
+	}
+    }
+
+    return retval;
+};
+
+/* 
+ * Package: jquery.js
+ * 
+ * Namespace: bbop.golr.manager.jquery
+ * 
+ * jQuery BBOP manager for dealing with actual ajax calls. Remember,
+ * this is actually a "subclass" of <bbop.golr.manager>.
+ * 
+ * This should still be able to limp along (no ajax and no error
+ * parsing) even outside of a jQuery environment.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.require('bbop', 'golr', 'manager');
+bbop.core.namespace('bbop', 'golr', 'manager', 'jquery');
+
+/*
+ * Constructor: jquery
+ * 
+ * Contructor for the GOlr query manager
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.golr.manager>
+ */
+bbop.golr.manager.jquery = function (golr_loc, golr_conf_obj){
+    bbop.golr.manager.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.golr.manager.jquery';
+    
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger helper.
+    function ll(str){ anchor._logger.kvetch(str); }
+
+    // Before anything else, if we cannot find a viable jQuery library
+    // for use, we're going to create a fake one so we can still test
+    // and work in a non-browser/networked environment.
+    anchor.JQ = new bbop.golr.faux_ajax();
+    try{ // some interpreters might not like this kind of probing
+    	if( typeof(jQuery) !== 'undefined' ){
+    	    //JQ = jQuery;
+    	    anchor.JQ = jQuery.noConflict();
+    	}
+    }catch (x){
+    }finally{
+    	var got = bbop.core.what_is(anchor.JQ);
+    	if( got && got == 'bbop.golr.faux_ajax'){
+    	}else{
+    	    got = 'jQuery';
+    	}
+    	ll('Using ' + got + ' for Ajax calls.');
+    }
+
+    // The base jQuery Ajax args we need with the setup we have.
+    anchor.jq_vars = {
+	//url: qurl,
+	type: "GET",
+	dataType: 'jsonp',
+	jsonp: 'json.wrf'
+    };
+
+    // We'll override the original with something that actually speaks
+    // jQuery. This is the function that runs where there is an AJAX
+    // error during an update. First it has to run some template code,
+    // then it does all of the callbacks.
+    this._run_error_callbacks = function(result, status, error) {
+
+    	ll('Failed server request: '+ result +', '+ status +', '+ error);
+    	//ll('Failed (a): '+ bbop.core.what_is(status));
+    	//ll('Failed (b): '+ bbop.core.dump(status));
+		
+    	var clean_error = "unknown error";
+
+    	// Get the error out (clean it) if possible.
+    	var jreq = result.responseText;
+    	var req = anchor.JQ.parseJSON(jreq); // TODO/BUG: this must be removed
+    	if( req && req['errors'] && req['errors'].length > 0 ){
+    	    var in_error = req['errors'][0];
+    	    ll('ERROR:' + in_error);
+    	    // Split on newline if possible to get
+    	    // at the nice part before the perl
+    	    // error.
+    	    var reg = new RegExp("\n+", "g");
+    	    var clean_error_split =
+    		in_error.split(reg);
+    	    clean_error = clean_error_split[0];
+    	}else if( bbop.core.what_is(error) == 'string' &&
+    		  error.length > 0){
+    	    clean_error = error;
+    	}else if( bbop.core.what_is(status) == 'string' &&
+    		  status.length > 0){
+    	    clean_error = status;
+    	}
+	
+    	// Run all against registered functions.
+    	ll('run error callbacks...');
+    	anchor.apply_callbacks('error', [clean_error, anchor]);
+    };
+
+    // Try and decide between a reset callback and a search callback.
+    // This is useful since jQuery doesn't have a natural way to do
+    // that within the callbacks.
+    this._callback_type_decider = function(json_data){
+    	ll('in callback type decider...');
+
+	var response = new bbop.golr.response(json_data);
+
+    	// 
+    	if( ! response.success() ){
+    	    throw new Error("Unsuccessful response from golr server!");
+    	}else{
+    	    var cb_type = response.callback_type();
+    	    ll('okay response from server, will probe type...: ' + cb_type);
+    	    if( cb_type == 'reset' ){
+    		anchor._run_reset_callbacks(json_data);
+    	    }else if( cb_type == 'search' ){
+    		anchor._run_search_callbacks(json_data);
+    	    }else{
+    		throw new Error("Unknown callback type!");
+    	    }
+    	}
+    };
+
+    /*
+     * Function: safety
+     *
+     * Getter/setter for the trigger safety.
+     * 
+     * If the safety is on, ajax events controlled by the manager will
+     * not occur. The default if off (false).
+     * 
+     * Parameters: 
+     *  safety_on_p - boolean
+     *
+     * Returns:
+     *  boolean
+     */
+    this.safety = function(safety_on_p){
+	if( bbop.core.is_defined(safety_on_p) ){
+	    anchor._safety = safety_on_p;
+	}
+	return anchor._safety;
+    };
+};
+bbop.core.extend(bbop.golr.manager.jquery, bbop.golr.manager);
+
+/*
+ * Function: update
+ *
+ *  See the documentation in <golr_manager.js> on update to get more
+ *  of the story. This override function adds functionality for
+ *  jQuery.
+ * 
+ * You can prevent the triggering of ajax with the <safety>
+ * method.
+ *
+ * Parameters: 
+ *  callback_type - callback type string
+ *  rows - *[serially optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url (with the jQuery callback specific parameters)
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.jquery.prototype.update = function(callback_type,
+						     rows, start){
+    
+    // Get "parents" url first.
+    var parent_update = bbop.golr.manager.prototype.update;
+    var qurl = parent_update.call(this, callback_type, rows, start);
+    
+    // Only actually trigger if the safety is off (default).
+    if( ! this.safety() ){
+	
+	//ll('try: ' + qurl);
+	//widgets.start_wait('Updating...');
+	
+	// Setup JSONP for Solr and jQuery ajax-specific parameters.
+	this.jq_vars['success'] = this._callback_type_decider; // decide & run
+	this.jq_vars['error'] = this._run_error_callbacks; // run error cbs
+	//done: _callback_type_decider, // decide & run search or reset
+	//fail: _run_error_callbacks, // run error callbacks
+	//always: function(){} // do I need this?
+	this.JQ.ajax(qurl, this.jq_vars);
+    }
+    
+    return qurl;
+};
+
+/*
+ * Function: run_batch
+ *
+ * A distant cousin of <update>.
+ * Designed to "serially" get data from a server for
+ * certain types of data crunching routines.
+ * 
+ * Why would you want this? Lets say there are ten distinct things
+ * that you want from the server. Coordinating and collating them all
+ * without annoying the server or going insane is hard in an
+ * asynchronous environment.
+ *
+ * Parameters: 
+ *  accumulator_func - the function that collects
+ *  final_func - the function to run on completion
+ *
+ * Returns:
+ *  the number of batch items run
+ */
+bbop.golr.manager.jquery.prototype.run_batch = function(accumulator_func,
+							final_func){
+
+    var anchor = this;
+
+    // Set the various callbacks internally so we can get back at them
+    // when we lose our stack during the ajax.
+    if( accumulator_func ){ this._batch_accumulator_func = accumulator_func; }
+    if( final_func ){ this._batch_final_func = final_func; }
+
+    // Look at how many states are left.
+    var qurl = anchor.next_batch_url();
+    if( qurl ){
+	    
+	// Generate a custom callback function that will start
+	// this process (next_generator) again--continue the cycle.
+	var next_cycle = function(json_data){
+	    var response = new bbop.golr.response(json_data);
+	    anchor._batch_accumulator_func.apply(anchor, [response, anchor]);
+	    anchor.run_batch();
+	};
+	
+	// Put this custom callback on success.
+	anchor.jq_vars['success'] = next_cycle;
+	anchor.jq_vars['error'] = anchor._run_error_callbacks;
+	anchor.JQ.ajax(qurl, anchor.jq_vars);
+    }else{
+	anchor._batch_final_func.apply(anchor);
+    }
+};
+
+/*
+ * Function: fetch
+ *
+ * A cousin of <update>, but is made to avoid all of the usual
+ * callback functions (except error) and just run the single function
+ * from the argument.
+ * 
+ * Why would you want this? Sometimes you need just a little data
+ * without updating the whole interface or whatever.
+ *
+ * Parameters: 
+ *  run_func - the function to run on completion
+ *
+ * Returns:
+ *  n/a
+ */
+bbop.golr.manager.jquery.prototype.fetch = function(run_func){
+
+    // ...
+    var anchor = this;
+    var qurl = anchor.get_query_url();
+    anchor._run_func = run_func;
+    anchor.jq_vars['success'] =
+	function(json_data){
+	    var response = new bbop.golr.response(json_data);
+	    anchor._run_func(response);   
+	};
+    anchor.jq_vars['error'] = anchor._run_error_callbacks;
+    anchor.JQ.ajax(qurl, anchor.jq_vars);
+};
+
+/*
+ * Namespace: bbop.golr.faux_ajax
+ *
+ * Constructor: faux_ajax
+ * 
+ * Contructor for a fake and inactive Ajax. Used by bbop.golr.manager.jquery
+ * in (testing) environments where jQuery is not available.
+ * 
+ * Returns:
+ *  faux_ajax object
+ */
+bbop.golr.faux_ajax = function (){
+    this._is_a = 'bbop.golr.faux_ajax';
+
+    /*
+     * Function: ajax
+     *
+     * Fake call to jQuery's ajax.
+     *
+     * Parameters: 
+     *  args - whatever
+     *
+     * Returns:
+     *  null
+     */
+    this.ajax = function(args){
+	return null;
+    };
+    /*
+     * Function: parseJSON
+     *
+     * Fake call to jQuery's parseJSON.
+     *
+     * Parameters: 
+     *  args - whatever--they are ignored
+     *
+     * Returns:
+     *  ""
+     */
+    this.parseJSON = function(args){
+	return "";
+    };
+};
+/*
+ * Package: clickable_object.js
+ * 
+ * Namespace: bbop.widget.display.clickable_object
+ * 
+ * BBOP object to produce a clickable image or a clickable text span,
+ * both producing something that can give its id for later clickable
+ * actions.
+ * 
+ * This is a method, not a constructor.
+ */
+
+bbop.core.require('bbop', 'core');
+//bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'display', 'clickable_object');
+
+/*
+ * Method: clickable_object
+ * 
+ * Generator for a clickable object.
+ * 
+ * TODO: May eventually expand it to include making a jQuery button.
+ * 
+ * Arguments:
+ *  label - *[optional]* the text to use for the span or label (defaults to '')
+ *  source - *[optional]* the URL source of the image (defaults to '')
+ *  id - *[optional]* the id for the object (defaults to generate_id: true)
+ * 
+ * Returns:
+ *  bbop.html.span or bbop.html.image
+ */
+bbop.widget.display.clickable_object = function(label, source, id){
+    //this._is_a = 'bbop.widget.display.clickable_object';
+    //var anchor = this;
+    // // Per-UI logger.
+    // var logger = new bbop.logger();
+    // logger.DEBUG = true;
+    // function ll(str){ logger.kvetch('W (clickable_object): ' + str); }
+
+    // Default args.
+    if( ! label ){ label = ''; }
+    if( ! source ){ source = ''; }
+
+    // Decide whether we'll use an incoming id or generate our own.
+    var args = {};
+    if( id ){
+	args['id'] = id;
+    }else{
+	args['generate_id'] = true;
+    }
+
+    // Figure out an icon or a label.
+    var obj = null;
+    if( source == '' ){
+	obj = new bbop.html.span(label, args);
+    }else{
+	args['src'] = source;
+	args['title'] = label;
+	obj = new bbop.html.image(args);
+    }
+
+    return obj;
+};
+/*
+ * Package: text_buttom_sim.js
+ * 
+ * Namespace: bbop.widget.display.text_button_sim
+ * 
+ * BBOP object to produce a clickable text span, that in conjunction with the local CSS, should make an awfully button looking creature.
+ * 
+ * It uses the class: "bbop-js-text-button-sim".
+ * 
+ * Note: this is a method, not a constructor.
+ */
+
+bbop.core.require('bbop', 'core');
+//bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'display', 'text_button_sim');
+
+/*
+ * Method: text_button_sim
+ * 
+ * Generator for a text span for use for buttons.
+ * 
+ * Arguments:
+ *  label - *[optional]* the text to use for the span or (defaults to 'X')
+ *  title - *[optional]* the hover text (defaults to 'X')
+ *  id - *[optional]* the id for the object (defaults to generate_id: true)
+ *  add_attrs - *[optional]* more attributes to be folded in to the span as hash
+ * 
+ * Returns:
+ *  bbop.html.span
+ */
+bbop.widget.display.text_button_sim = function(label, title, id, add_attrs){
+    
+    // Default args.
+    if( ! label ){ label = 'X'; }
+    if( ! title ){ title = 'X'; }
+    if( ! add_attrs ){ add_attrs = {}; }
+    
+    // Decide whether we'll use an incoming id or generate our own.
+    var args = {
+	'class': "bbop-js-text-button-sim",
+	'title': title
+    };
+    if( id ){
+	args['id'] = id;
+    }else{
+	args['generate_id'] = true;
+    }
+
+    // Addtional optional atrributes and overrides.    
+    args = bbop.core.merge(args, add_attrs);
+
+    var obj = new bbop.html.span(label, args);    
+    return obj;
+};
+/*
+ * Package: results_table_by_class_conf.js
+ * 
+ * Namespace: bbop.widget.display.results_table_by_class_conf
+ * 
+ * Subclass of <bbop.html.tag>.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'display', 'results_table_by_class_conf');
+
+/*
+ * Function: results_table_by_class_conf
+ *
+ * Using a conf class and a set of data, automatically populate and
+ * return a results table.
+ *  
+ * Parameters:
+ *  class_conf - a <bbop.golr.conf_class>
+ *  golr_resp - a <bbop.golr.response>
+ *  linker - a linker object; see <amigo.linker> for more details
+ *  handler - a handler object; see <amigo.handler> for more details
+ *  elt_id - the element id to attach it to
+ *  selectable_p - *[optional]* whether to create checkboxes (default true)
+ *
+ * Returns:
+ *  <bbop.html.table> filled with results
+ */
+bbop.widget.display.results_table_by_class = function(cclass,
+						      golr_resp,
+						      linker,
+						      handler,
+						      elt_id,
+						      selectable_p){
+    //bbop.html.tag.call(this, 'div');
+    //var amigo = new bbop.amigo();
+
+    // Temp logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    //logger.DEBUG = false;
+    function ll(str){ logger.kvetch('TT: ' + str); }
+
+    // Conveience aliases.
+    var each = bbop.core.each;
+    var is_defined = bbop.core.is_defined;
+
+    // The context we'll deliver to
+    var display_context = 'bbop.widgets.search_pane';
+
+    // Only want to compile once.
+    var ea_regexp = new RegExp("\<\/a\>", "i"); // detect an <a>
+    var br_regexp = new RegExp("\<br\ \/\>", "i"); // detect a <br />
+
+    // Sort out whether we want to display checkboxes. Also, give life
+    // to the necessary variables if they will be called upon.
+    var add_selectable_p = false;
+    var select_column_id = null;
+    var select_item_name = null;
+    if( is_defined(selectable_p) && selectable_p == true ){
+	add_selectable_p = true;
+
+	// Special id and names for optional select column.
+	var local_mangle = bbop.core.uuid();
+	select_column_id = 'rtbcc_select_' + local_mangle;
+	select_item_name = 'rtbcc_select_name_' + local_mangle;
+    }
+
+    /*
+     * Function: item_name
+     *
+     * Return a string of the name attribute used by the checkboxes if
+     * we selected for checkboxes to be displayed.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  string or null if displaying checkboxes was false
+     */
+    this.item_name = function(){	
+	return select_item_name;
+    };
+
+    /*
+     * Function: toggle_id
+     *
+     * Return a string of the id of the checkbox in the header if we
+     * selected for checkboxes to be displayed.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  string or null if displaying checkboxes was false
+     */
+    this.toggle_id = function(){	
+	return select_column_id;
+    };
+
+    // Now take what we have, and wrap around some expansion code
+    // if it looks like it is too long.
+    var trim_hash = {};
+    var trimit = 100;
+    function _trim_and_store( in_str ){
+
+	var retval = in_str;
+
+	//ll("T&S: " + in_str);
+
+	// Skip if it is too short.
+	//if( ! ea_regexp.test(retval) && retval.length > (trimit + 50) ){
+	if( retval.length > (trimit + 50) ){
+	    //ll("T&S: too long: " + retval);
+
+	    // Let there be tests.
+	    var list_p = br_regexp.test(retval);
+	    var anchors_p = ea_regexp.test(retval);
+
+	    var tease = null;
+	    if( ! anchors_p && ! list_p ){
+		// A normal string then...trim it!
+		//ll("\tT&S: easy normal text, go nuts!");
+		tease = new bbop.html.span(bbop.core.crop(retval, trimit, ''),
+					   {'generate_id': true});
+	    }else if( anchors_p && ! list_p ){
+		// It looks like it is a link without a break, so not
+		// a list. We cannot trim this safely.
+		//ll("\tT&S: single link so cannot work on!");
+	    }else{
+		//ll("\tT&S: we have a list to deal with");
+		
+		var new_str_list = retval.split(br_regexp);
+		if( new_str_list.length <= 3 ){
+		    // Let's just ignore lists that are only three
+		    // items.
+		    //ll("\tT&S: pass thru list length <= 3");
+		}else{
+		    //ll("\tT&S: contruct into 2 plus tag");
+		    var new_str = '';
+		    new_str = new_str + new_str_list.shift();
+		    new_str = new_str + '<br />';
+		    new_str = new_str + new_str_list.shift();
+		    tease = new bbop.html.span(new_str, {'generate_id': true});
+		}
+	    }
+
+	    // If we have a tease, assemble the rest of the packet
+	    // to create the UI.
+	    if( tease ){
+		//ll("T&S: tease: " + tease.to_string());
+		
+		// Setup the text for tease and full versions.
+		// var more_b = new bbop.html.span('<b>[more...]</b>',
+		// 				{'generate_id': true});
+		// var full = new bbop.html.span(retval,
+		// 			      {'generate_id': true});
+		// var less_b = new bbop.html.span('<b>[less]</b>',
+		// 				{'generate_id': true});
+		var bgen = bbop.widget.display.text_button_sim;
+		var more_b = new bgen('more...', 'Display the complete list');
+		var full = new bbop.html.span(retval,
+					      {'generate_id': true});
+		var less_b = new bgen('less', 'Display the truncated list');
+		
+		// Store the different parts for later activation.
+		var tease_id = tease.get_id();
+		var more_b_id = more_b.get_id();
+		var full_id = full.get_id();
+		var less_b_id = less_b.get_id();
+		trim_hash[tease_id] = 
+		    [tease_id, more_b_id, full_id, less_b_id];
+		
+		// New final string.
+		retval = tease.to_string() + " " +
+		    more_b.to_string() + " " +
+		    full.to_string() + " " +
+		    less_b.to_string();
+	    }
+	}
+
+	return retval;
+    }
+
+    // Create a locally mangled checkbox.
+    function _create_select_box(val, id, name){
+	if( ! is_defined(name) ){
+	    name = select_item_name;	    
+	}
+	
+	var input_attrs = {
+	    'value': val,
+	    'name': name,
+	    'type': 'checkbox'
+	};
+	if( is_defined(id) ){
+	    input_attrs['id'] = id;
+	}
+	var input = new bbop.html.input(input_attrs);
+	return input;
+    }
+
+    ///
+    /// Render the headers.
+    ///
+
+    // Start with score, and add the others by order of the class
+    // results_weights field.
+    // var headers = ['score'];
+    // var headers_display = ['Score'];
+    var headers = [];
+    var headers_display = [];
+    if( add_selectable_p ){
+	// Hint for later.
+	headers.push(select_column_id);
+
+	// Header select for selecting all.
+	var hinp = _create_select_box('', select_column_id, '');
+	//headers_display.push('All ' + hinp.to_string());
+	headers_display.push(hinp.to_string());
+    }
+    var results_order = cclass.field_order_by_weight('result');
+    each(results_order,
+	 function(fid){
+	     // Store the raw headers/fid for future use.
+	     headers.push(fid);
+	     // Get the headers into a presentable state.
+	     var field = cclass.get_field(fid);
+	     if( ! field ){ throw new Error('conf error: not found:' + fid); }
+	     //headers_display.push(field.display_name());
+	     var fdname = field.display_name();
+	     var fdesc = field.description() || '???';
+	     var head_span_attrs = {
+		 // TODO/NOTE: to make the tooltip work properly, since the
+		 // table headers are being created each time,
+		 // the tooltop initiator would have to be called after
+		 // each pass...I don't know that I want to do that.
+		 //'class': 'bbop-js-ui-hoverable bbop-js-ui-tooltip',
+		 'class': 'bbop-js-ui-hoverable',
+		 'title': fdesc
+	     };
+	     // More aggressive link version.
+	     //var head_span = new bbop.html.anchor(fdname, head_span_attrs);
+	     var head_span = new bbop.html.span(fdname, head_span_attrs);
+	     headers_display.push(head_span.to_string());
+	 });
+
+    ///
+    /// Render the documents.
+    ///
+
+    // Some of what we'll do for each field in each doc (see below).
+    // var ext = cclass.searchable_extension();
+    function _process_entry(fid, iid, doc){
+
+	var retval = '';
+	var did = doc['id'];
+
+	// BUG/TODO: First see if the filed will be multi or not.
+	// If not multi, follow the first path. If multi, break it
+	// down and try again.
+
+	// Get a label instead if we can.
+	var ilabel = golr_resp.get_doc_label(did, fid, iid);
+	if( ! ilabel ){
+	    ilabel = iid;
+	}
+
+	// Extract highlighting if we can from whatever our "label"
+	// was.
+	var hl = golr_resp.get_doc_highlight(did, fid, ilabel);
+
+	// See what kind of link we can create from what we got.
+	var ilink = linker.anchor({id: iid, label: ilabel, hilite: hl}, fid);
+	
+	ll('processing: ' + [fid, ilabel, iid].join(', '));
+	//ll('ilink: ' + ilink);
+
+	// See what we got, in order of how much we'd like to have it.
+	if( ilink ){
+	    retval = ilink;
+	}else if( ilabel ){
+	    retval = ilabel;
+	}else{
+	    retval = iid;
+	}
+
+	return retval;
+    }
+
+    // Cycle through and render each document.
+    // For each doc, deal with it as best we can using a little
+    // probing. Score is a special case as it is not an explicit
+    // field.
+    var table_buff = [];
+    var docs = golr_resp.documents();
+    each(docs,
+	 function(doc){
+	     
+	     // Well, they had better be in here, so we're
+	     // just gunna cycle through all the headers/fids.
+	     var entry_buff = [];
+	     each(headers,
+		  function(fid){
+		      // Detect out use of the special selectable
+		      // column and add a special checkbox there.
+		      if( fid == select_column_id ){
+			  // Also
+			  var did = doc['id'];
+			  var dinp = _create_select_box(did);
+			  entry_buff.push(dinp.to_string());
+		      }else if( fid == 'score' ){
+			  // Remember: score is also
+			  // special--non-explicit--case.
+			  var score = doc['score'] || 0.0;
+			  score = bbop.core.to_string(100.0 * score);
+			  entry_buff.push(bbop.core.crop(score, 4) + '%');
+		      }else{
+
+			  // Not "score", so let's figure out what we
+			  // can automatically.
+			  var field = cclass.get_field(fid);
+
+			  // Make sure that something is there and
+			  // that we can iterate over whatever it
+			  // is.
+			  var bits = [];
+			  if( doc[fid] ){
+			      if( field.is_multi() ){
+				  //ll("Is multi: " + fid);
+				  bits = doc[fid];
+			      }else{
+				  //ll("Is single: " + fid);
+				  bits = [doc[fid]];
+			      }
+			  }
+
+			  // Render each of the bits.
+			  var tmp_buff = [];
+			  each(bits,
+			       function(bit){
+
+				   // The major difference that we'll have here
+				   // is between standard fields and special
+				   // handler fields. If the handler
+				   // resolves to null, fall back onto
+				   // standard.
+				   ll('! '+ bit +' '+ fid +' '+ display_context);
+				   var out = handler.dispatch(bit, fid,
+							      display_context);
+				   if( is_defined(out) && out != null ){
+				       // Handler success.
+				       tmp_buff.push(out);
+				   }else{
+				       // Standard output.   
+				       out = _process_entry(fid, bit, doc);
+				       //ll('out: ' + out);
+				       tmp_buff.push(out);
+				   }
+			       });
+			  // Join it, trim/store it, push to to output.
+			  var joined = tmp_buff.join('<br />');
+			  entry_buff.push(_trim_and_store(joined));
+		      }
+		  });
+	     table_buff.push(entry_buff);
+	 });
+	
+	// Add the table to the DOM.
+	var final_table = new bbop.html.table(headers_display, table_buff);
+	jQuery('#' + elt_id).append(bbop.core.to_string(final_table));
+
+	// Add the roll-up/down events to the doc.
+	each(trim_hash,
+	    function(key, val){
+		var tease_id = val[0];
+		var more_b_id = val[1];
+		var full_id = val[2];
+		var less_b_id = val[3];
+
+		// Initial state.
+		jQuery('#' + full_id ).hide();
+		jQuery('#' + less_b_id ).hide();
+
+		// Click actions to go back and forth.
+		jQuery('#' + more_b_id ).click(
+		    function(){
+			jQuery('#' + tease_id ).hide();
+			jQuery('#' + more_b_id ).hide();
+			jQuery('#' + full_id ).show('fast');
+			jQuery('#' + less_b_id ).show('fast');
+		    });
+		jQuery('#' + less_b_id ).click(
+		    function(){
+			jQuery('#' + full_id ).hide();
+			jQuery('#' + less_b_id ).hide();
+			jQuery('#' + tease_id ).show('fast');
+			jQuery('#' + more_b_id ).show('fast');
+		    });
+	    });
+
+	//return final_table;
+};
+//bbop.widget.display.results_table_by_class.prototype = new bbop.html.tag;
+/*
+ * Package: two_column_layout.js
+ * 
+ * Namespace: bbop.widget.display.two_column_layout
+ * 
+ * Reusable object to create a two-column layout.
+ * 
+ * Subclass of <bbop.html.tag>.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'display', 'two_column_layout');
+
+/*
+ * Constructor: two_column_layout
+ *
+ * Produce a div containing a CSS hardwired two-column layout.
+ * These are currently hardwired to:
+ * 
+ * : 'class': 'twocol-leftcolumn', 'style': 'margin-top: -15px;'
+ * : 'class': 'twocol-content', 'style': 'margin-left: 26em; margin-top: -15px;'
+ * 
+ * Parameters:
+ *  col1 - the string or <bbop.html> object for the left column
+ *  col2 - the string or <bbop.html> object for the right column
+ *
+ * Returns:
+ *  <bbop.html.tag> subclass
+ */
+bbop.widget.display.two_column_layout = function (col1, col2){
+    bbop.html.tag.call(this, 'div', {'class': 'twocol-wrapper'});
+
+    // Left (control) side.
+    this._two_column_stack_left =
+	new bbop.html.tag('div',
+			  {'class': 'twocol-leftcolumn',
+			   'style': 'margin-top: -15px;'},
+			  col1);
+    this.add_to(this._two_column_stack_left);
+
+    // Right (display) side.
+    this._two_column_stack_right =
+	new bbop.html.tag('div',
+			  {'class': 'twocol-content',
+			   'style': 'margin-left: 26em; margin-top: -15px;'},
+			  col2);
+    this.add_to(this._two_column_stack_right);
+};
+bbop.widget.display.two_column_layout.prototype = new bbop.html.tag;
+
+/*
+ * Package: filter_shield.js
+ * 
+ * Namespace: bbop.widget.display.filter_shield
+ * 
+ * BBOP object to produce a self-constructing/self-destructing shield
+ * to support very large filter selection in the live search/search
+ * pane genre.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+//bbop.core.require('bbop', 'model');
+//bbop.core.require('bbop', 'model', 'graph', 'bracket');
+bbop.core.require('bbop', 'html');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'display', 'filter_shield');
+
+/*
+ * Constructor: filter_shield
+ * 
+ * Contructor for the bbop.widget.display.filter_shield object.
+ * 
+ * Support for <bbop.widget.search_pane> by way of
+ * <bbop.widget.display.live_search>
+ * 
+ * Arguments:
+ *  spinner_img_src - *[optional]* optional source of a spinner image to use
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.display.filter_shield = function(spinner_img_src){
+
+    this._is_a = 'bbop.widget.display.filter_shield';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (filter_shield): ' + str); }
+
+    // Variables that we'll need to keep.
+    var is_open_p = false;
+    var parea = new bbop.html.tag('div', {'generate_id': true});
+    var pmsg = new bbop.html.tag('div', {'generate_id': true}, "Waiting...");
+    parea.add_to(pmsg);
+
+    var div = new bbop.html.tag('div', {'generate_id': true}, parea);
+    var pmsg_id = pmsg.get_id();
+    //var pbar_id = pbar.get_id();
+    var div_id = div.get_id();
+    var diargs = {
+	modal: true,
+	draggable: false,
+	width: 800,
+	height: 600,
+	close:
+	function(){
+	    // TODO: Could maybe use .dialog('destroy') instead?
+	    jQuery('#' + div_id).remove();
+	}	    
+    };
+
+    /*
+     * Function: start_wait
+     * 
+     * Render an unpopulated modal shield with some kind of waiting
+     * element. This is to act as a block for the IO if
+     * desired--calling this before .draw() is not required (as
+     * .draw() will call it anyways if you haven't).
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.start_wait = function(){
+
+	// Mark that we've finally opened it.
+	is_open_p = true;
+
+	// Append div to body.
+	jQuery('body').append(div.to_string());	
+
+	// If we have an image source specified, go ahead and add it to
+	// the waiting display before popping it open.
+	if( spinner_img_src && spinner_img_src != '' ){
+	    var s = new bbop.widget.spinner(parea.get_id(), spinner_img_src);
+	}
+
+	// Pop open the dialog.
+	var dia = jQuery('#' + div_id).dialog(diargs);
+    };
+
+    /*
+     * Function: draw
+     * 
+     * Render a temporary modal filter shield.
+     * 
+     * Arguments:
+     *  field_name - the name (id) of the filter field to display
+     *  filter_list - a list of [[filter_id, filter_count], ...]
+     *  manager - the manager that we'll use for the callbacks
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.draw = function(field_name, filter_list, manager){
+	//ll(doc['id']);
+
+	// Open the shield if it is not already open.
+	if( ! is_open_p ){
+	    anchor.open();
+	}
+
+	var txt = 'No filters...';
+	var tbl = new bbop.html.table(null, null, {'generate_id': true});
+	var button_hash = {};
+	var each = bbop.core.each; // conveience
+	var bgen = bbop.widget.display.text_button_sim;
+	each(filter_list,
+ 	     function(field){
+		 var fname = field[0];
+		 var fcount = field[1];
+
+		 var b_plus = new bgen('+', 'Add positive filter');
+		 var b_minus = new bgen('-', 'Add negative filter');
+		 button_hash[b_plus.get_id()] =
+		     [field_name, fname, fcount, '+'];
+		 button_hash[b_minus.get_id()] =
+		     [field_name, fname, fcount, '-'];
+
+		 tbl.add_to([fname, '(' + fcount + ')',
+			     b_plus.to_string(),
+			     b_minus.to_string()]);
+	     });
+	txt = tbl.to_string();
+
+	// Create a filter slot div.
+	
+	// Add filter slot and table text to div.
+	jQuery('#' + div_id).empty();
+	var fdiv = new bbop.html.tag('div', {'generate_id': true});
+	jQuery('#' + div_id).append(fdiv.to_string());	
+	jQuery('#' + div_id).append(txt);
+
+	// Apply the filter to the table.
+	var ft = null;
+	if( spinner_img_src && spinner_img_src != '' ){
+	    ft = bbop.widget.filter_table(fdiv.get_id(), tbl.get_id(),
+					  spinner_img_src, null);
+	}else{
+	    ft = bbop.widget.filter_table(fdiv.get_id(), tbl.get_id(), null);
+	}
+
+	// Okay, now introducing a function that we'll be using a
+	// couple of times in our callbacks. Given a button id (from
+	// a button hash) and the [field, filter, count, polarity]
+	// values from the props, make a button-y thing an active
+	// filter.
+	function filter_select_live(button_id, create_time_button_props){
+	    var in_polarity = create_time_button_props[3];
+
+	    // Decide on the button graphical elements.
+	    var b_ui_icon = 'ui-icon-plus';
+	    if( in_polarity == '-' ){
+		b_ui_icon = 'ui-icon-minus';
+	    }
+	    var b_ui_props = {
+		icons: { primary: b_ui_icon},
+		text: false
+	    };
+
+	    // Create the button and immediately add the event.
+	    jQuery('#' + button_id).click(
+		function(){
+		    var tid = jQuery(this).attr('id');
+		    var call_time_button_props = button_hash[tid];
+		    var call_field = call_time_button_props[0];	 
+		    var call_filter = call_time_button_props[1];
+		    //var in_count = button_props[2];
+		    var call_polarity = call_time_button_props[3];
+		    
+		    // Change manager, fire, and close the dialog.
+		    manager.add_query_filter(call_field, call_filter,
+			  		     [call_polarity]);
+		    manager.search();
+		    jQuery('#' + div_id).remove();
+		});
+	}
+
+	// Now let's go back and add the buttons, styles,
+	// events, etc. in the main accordion section.
+	each(button_hash, filter_select_live);
+
+    };
+
+};
+/*
+ * Package: live_search.js
+ * 
+ * Namespace: bbop.widget.display.live_search
+ * 
+ * AmiGO object to draw various UI elements that have to do with things
+ * dealing with a fully faceted searcher/browser.
+ * 
+ * It is probably not particularly useful directly, but rather used as
+ * the framework for more specialized interfaces.
+ * 
+ * See Also:
+ *  <search_pane.js>
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'widget', 'display', 'clickable_object');
+bbop.core.require('bbop', 'widget', 'display', 'results_table_by_class_conf');
+bbop.core.require('bbop', 'widget', 'display', 'two_column_layout');
+bbop.core.namespace('bbop', 'widget', 'live_search');
+
+/*
+ * Constructor: live_search
+ * 
+ * Contructor for the bbop.widget.display.live_search object.
+ * 
+ * Arguments:
+ *  interface_id - string id of the div to build on
+ *  conf_class - <bbop.golr.conf_class> for hints and other settings
+ * 
+ * Returns:
+ *  BBOP GOlr UI object
+ */
+bbop.widget.display.live_search = function(interface_id, conf_class){
+
+    var anchor = this;
+    var each = bbop.core.each;
+    
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('UI (search): ' + str); }
+
+    // There should be a string interface_id argument.
+    // The class configuration we'll be using to hint and build.
+    this.interface_id = interface_id;
+    this.class_conf = conf_class;
+
+    // Somebody will probably set these externally at some point.
+    this.linker = new bbop.linker();
+    this.handler = new bbop.handler();
+   
+    // We need strong control of the displayed buttons since we're
+    // going to make them a dynamic (post-setup) resource.
+    //this.button_definitions = button_defs;
+    this.button_definitions = [];
+
+    // Get the user interface hook and remove anything that was there.
+    var ui_div_id = this.interface_id;
+    jQuery('#' + ui_div_id).empty();
+
+    // Mangle everything around this unique id so we don't collide
+    // with other instances on the same page.
+    var mangle = ui_div_id + '_ui_element_' + bbop.core.uuid() + '_';
+
+    // Render a control section into HTML. This includes the accordion
+    // and current filter sections.
+    var ui_controls_section_id = mangle + 'ui-controls-wrapper';
+    var controls_div = new bbop.html.tag('div', {'id': ui_controls_section_id});
+    //jQuery('#' + ui_div_id).append(controls_div.to_string());
+
+    // Render a results section into HTML. The includes the results
+    // table and the results meta-info sections.
+    var ui_results_section_id = mangle + 'ui-results-wrapper';
+    var results_div = new bbop.html.tag('div', {'id': ui_results_section_id});
+    //jQuery('#' + ui_div_id).append(results_div.to_string());
+
+    // A dynamic handle (set when rendering results) of the select
+    // column control id and item group name.
+    var ui_results_selection_control_id = null;
+    var ui_results_selection_item_name = null;
+    var show_checkboxes_p = false;
+
+    // Add the sections to a two column layout and add that into the
+    // main ui div.
+    var two_col_div =
+	new bbop.widget.display.two_column_layout(controls_div, results_div);
+    jQuery('#' + ui_div_id).append(two_col_div.to_string());
+
+    // Main div id hooks to the easily changable areas of the two
+    // column display.
+    var ui_meta_div_id = mangle + 'meta-id';
+    var ui_user_button_div_id = mangle + 'user-button-id';
+    var ui_results_table_div_id = mangle + 'results-table-id';
+    var ui_count_control_div_id = mangle + 'count_control-id';
+    var ui_sticky_filters_div_id = mangle + 'sticky_filters-id';
+    var ui_current_filters_div_id = mangle + 'current_filters-id';
+    var ui_query_input_id = mangle + 'query-id';
+    var ui_clear_query_span_id = mangle + 'clear-query-id';
+    var ui_clear_user_filter_span_id = mangle + 'clear-user-filter-id';
+
+    // Globally declared (or not) icons.
+    var ui_spinner_search_source = '';
+    var ui_spinner_shield_source = '';
+    var ui_icon_positive_label = '';
+    var ui_icon_positive_source = '';
+    var ui_icon_negative_label = '';
+    var ui_icon_negative_source = '';
+    var ui_icon_remove_label = '';
+    var ui_icon_remove_source = '';
+
+    // The spinner, if it exists, needs to be accessible by everybody
+    // and safe to use.
+    var spinner = null;
+    function _spinner_gen(elt_id){
+	var spinner_args = {
+	    //timeout: 5,
+	    //timeout: 500,
+	    timeout: 10,
+	    //classes: 'bbop-widget-search_pane-spinner',
+	    visible_p: false
+	};
+	spinner = new bbop.widget.spinner(elt_id,
+					  ui_spinner_search_source,
+					  spinner_args);
+    }
+    function _spin_up(){
+	if( spinner ){
+	    spinner.start_wait();
+	}
+    }
+    function _spin_down(){
+	if( spinner ){
+	    spinner.finish_wait();
+	}
+    }
+
+    // Additional id hooks for easy callbacks. While these are not as
+    // easily changable as the above, we use them often enough and
+    // across functions to have a hook.
+    var accordion_div_id = mangle + 'filter-accordion-id';
+    
+    // These pointers are used in multiple functions (e.g. both
+    // *_setup and *_draw).
+    var filter_accordion_widget = null;
+    //var current_filters_div = null;
+
+    /*
+     * Function: show_checkboxes_p
+     *
+     * External function to show the item checkboxes in the use interface.
+     * 
+     * Parameters:
+     *  new_setting - *[optional]* show or not; defaults to false
+     *
+     * Returns:
+     *  true/false--the current state of showing the select boxes
+     */
+    this.show_checkboxes_p = function(new_setting){
+	if( bbop.core.is_defined(new_setting) ){
+	    if( new_setting ){
+		show_checkboxes_p = true;
+	    }else{
+		show_checkboxes_p = false;		
+	    }
+	}
+
+	return show_checkboxes_p;
+    };
+
+    /*
+     * Function: set_linker
+     *
+     * Set the linker to be used when creating links.
+     * If not set, a null function is used.
+     * 
+     * Parameters:
+     *  linker - the linker function to be used
+     *
+     * Returns:
+     *  true/false on whether it was properly set
+     */
+    this.set_linker = function(linker){
+
+	var retval = false;
+
+	if( bbop.core.is_defined(linker) ){
+		anchor.linker = linker;
+		retval = true;
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: set_handler
+     *
+     * Set the handler to be used when dealing with displaying special fields.
+     * If not set, a null function is used.
+     * 
+     * Parameters:
+     *  handler - the handler function to be used
+     *
+     * Returns:
+     *  true/false on whether it was properly set
+     */
+    this.set_handler = function(handler){
+
+	var retval = false;
+
+	if( bbop.core.is_defined(handler) ){
+		anchor.handler = handler;
+		retval = true;
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: selected_name
+     *
+     * External function to show give the name of the input name group
+     * for the selectable items in the checkboxes (if they are being
+     * used). Null otherwise.
+     * 
+     * Keep in mind that this variable changes every times that the
+     * results table refreshes.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  string or null
+     */
+    this.selected_name = function(){
+	return ui_results_selection_item_name;
+    };
+
+    /*
+     * Function: setup_query
+     *
+     * Setup the free text query display under contructed tags for
+     * later population.
+     * 
+     * If no icon_clear_source is defined, icon_clear_label will be
+     * used as the defining text.
+     * 
+     * Parameters:
+     *  label_str - *[optional]* string or bbop.html for input label
+     *  icon_clear_label - *[optional]* string or bbop.html for clear icon
+     *  icon_clear_source - *[optional]* string to define the src of img 
+     *
+     * Returns:
+     *  n/a
+     */
+    this.setup_query = function(label_str, icon_clear_label, icon_clear_source){
+	ll('setup_query for: ' + ui_query_input_id);
+
+	// Some defaults.
+	if( ! label_str ){ label_str = ''; }
+	if( ! icon_clear_label ){ icon_clear_label = ''; }
+	if( ! icon_clear_source ){ icon_clear_source = ''; }
+	
+	// The incoming label.
+	var query_label_attrs = {
+	    'class': 'bbop-js-search-pane-query-label'
+	};
+	var query_label_div =
+	    new bbop.html.tag('div', query_label_attrs, label_str);
+
+	// The text area.
+	var ta_args = {
+	    'id': ui_query_input_id,
+	    'class': 'bbop-js-search-pane-textarea'
+	};
+	var query_area = new bbop.html.tag('textarea', ta_args);
+
+	// Figure out an icon or a label.
+	var clear_query_obj =
+	    bbop.widget.display.clickable_object(icon_clear_label,
+						 icon_clear_source,
+						 ui_clear_query_span_id);
+	// And a div to put it in.
+	var clear_div_attrs = {
+	    'class': 'bbop-js-search-pane-clear-button',
+	    'generate_id': true
+	};
+	var clear_div =
+	    new bbop.html.tag('div', clear_div_attrs, clear_query_obj);	
+
+	// General container div.
+	var gen_div_attrs = {
+	    'generate_id': true
+	};
+	var gen_div = new bbop.html.tag('div', gen_div_attrs);
+
+	// Add to display.
+	query_label_div.add_to(clear_div.to_string());
+	gen_div.add_to(query_label_div.to_string());
+	gen_div.add_to(query_area.to_string());
+	jQuery('#' + ui_controls_section_id).append(gen_div.to_string());
+    };
+
+    // /*
+    //  * Function: setup_count_control
+    //  *
+    //  * Setup the results count control for later use. This is a kind
+    //  * of semi-permanent structure like the accordion.
+    //  * 
+    //  * Parameters:
+    //  *  n/a
+    //  *
+    //  * Returns:
+    //  *  n/a
+    //  */
+    // this.setup_count_control = function(manager){
+    // 	ll('setup_count_control for: ' + ui_query_input_id);
+	
+    // 	// Create inputs (the current order is important for proper
+    // 	// for/id creation).
+    // 	var cinputs = [];
+    // 	each([10, 25, 50, 100],
+    // 	     function(num, cindex){
+    // 		 // Create and store the option.
+    // 		 var sel_input_attrs = {
+    // 		     'generate_id': true,
+    // 		     'value': num
+    // 		 };
+    // 		 var sel_input =
+    // 		     new bbop.html.tag('option', sel_input_attrs, num);
+    // 		 var sel_input_id = sel_input.get_id();
+    // 		 cinputs.push(sel_input);
+    // 	     });
+    // 	// Option container div.
+    // 	var sel_attrs = {
+    // 	    'id': ui_count_control_div_id
+    // 	};
+    // 	var sel = new bbop.html.tag('select', sel_attrs, cinputs);
+
+    // 	// Create a text label.
+    // 	var sel_label = new bbop.html.tag('label', {},
+    // 					  'Results count  ');
+
+    // 	// Container div.
+    // 	var sel_div_attrs = {
+    // 	    'generate_id': true,
+    // 	    'class': 'bbop-js-search-pane-results-count'
+    // 	};
+    // 	var sel_div = new bbop.html.tag('div', sel_div_attrs);
+
+    // 	// Assemble these elements into the UI.
+    // 	sel_div.add_to(sel_label);
+    // 	sel_div.add_to(sel);
+    // 	jQuery('#' + ui_controls_section_id).append(sel_div.to_string());
+    // };
+
+    /*
+     * Function: setup_sticky_filters
+     *
+     * Setup sticky filters display under contructed tags for later
+     * population. The seeding information is coming in through the
+     * GOlr conf class.
+     * 
+     * Add in the filter state up here.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns:
+     *  n/a
+     */
+    this.setup_sticky_filters = function(){
+    
+	ll('setup_sticky_filters UI for class configuration: ' +
+	   this.class_conf.id());
+
+	var sticky_filters_attrs = {
+	    'id': ui_sticky_filters_div_id,
+	    'class': 'bbop-js-search-pane-sticky-filters'
+	};
+	var sticky_filters_div =
+	    new bbop.html.tag('div', sticky_filters_attrs,
+			      "No applied sticky filters.");
+
+	// Add the output to the page.
+	var sticky_filters_str = sticky_filters_div.to_string();
+	jQuery('#' + ui_controls_section_id).append(sticky_filters_str);
+    };
+
+    /*
+     * Function: setup_current_filters
+     *
+     * Setup current filters display under contructed tags for later
+     * population. The seeding information is coming in through the
+     * GOlr conf class.
+     * 
+     * Add in the filter state up here.
+     * 
+     * If no icon_reset_source is defined, icon_reset_label will be
+     * used as the defining text.
+     * 
+     * Parameters:
+     *  icon_remove_label - *[optional]* string or bbop.html for remove icon
+     *  icon_remove_source - *[optional]* string to define the src of img 
+     *
+     * Returns:
+     *  n/a
+     */
+    this.setup_current_filters = function(icon_remove_label,icon_remove_source){
+	ll('setup_current_filters UI for class configuration: ' +
+	   this.class_conf.id());
+
+	// Set the class variables for use when we do the redraws.
+	if( icon_remove_label ){ ui_icon_remove_label = icon_remove_label; }
+	if( icon_remove_source ){ ui_icon_remove_source = icon_remove_source; }
+
+	// Create the placeholder.
+	var current_filters_div =
+	    new bbop.html.tag('div', {'id': ui_current_filters_div_id},
+			      "No applied user filters.");
+
+	// Add the output to the page.
+	var curr_filters_str = current_filters_div.to_string();
+	jQuery('#' + ui_controls_section_id).append(curr_filters_str);
+    };
+
+    /*
+     * Function: setup_accordion
+     *
+     * Setup the accordion skeleton under contructed tags for later
+     * population. The seeding information is coming in through the
+     * GOlr conf class.
+     * Start building the accordion here. Not an updatable part.
+     * 
+     * If no icon_*_source is defined, icon_*_label will be
+     * used as the defining text.
+     * 
+     * Parameters:
+     *  icon_positive_label - *[optional]* string or bbop.html for positive icon
+     *  icon_positive_source - *[optional]* string to define the src of img 
+     *  icon_negative_label - *[optional]* string or bbop.html for positive icon
+     *  icon_negative_source - *[optional]* string to define the src of img 
+     *  spinner_shield_source - *[optional]* string to define the src of img 
+     *
+     * Returns: 
+     *  n/a
+     */
+    this.setup_accordion = function(icon_positive_label, icon_positive_source,
+				    icon_negative_label, icon_negative_source,
+				    spinner_shield_source){
+	
+	ll('setup_accordion UI for class configuration: ' +
+	   this.class_conf.id());
+
+	// Set the class variables for use when we do the redraws.
+	if( spinner_shield_source ){
+	    ui_spinner_shield_source = spinner_shield_source; }
+	if( icon_positive_label ){
+	    ui_icon_positive_label = icon_positive_label; }
+	if( icon_positive_source ){
+	    ui_icon_positive_source = icon_positive_source; }
+	if( icon_negative_label ){
+	    ui_icon_negative_label = icon_negative_label; }
+	if( icon_negative_source ){
+	    ui_icon_negative_source = icon_negative_source; }
+
+	var filter_accordion_attrs = {
+	    id: accordion_div_id
+	};
+	filter_accordion_widget = // heavy lifting by special widget
+	    new bbop.html.accordion([], filter_accordion_attrs, true);
+
+	// Add the sections with no contents as a skeleton to be
+	// filled by draw_accordion.
+	var field_list = this.class_conf.field_order_by_weight('filter');
+	each(field_list,
+	     function (in_field){
+		 ll('saw field: ' + in_field);
+		 var ifield = anchor.class_conf.get_field(in_field);
+		 var in_attrs = {
+		     id: in_field,
+		     label: ifield.display_name(),
+		     description: ifield.description()
+		 };
+		 filter_accordion_widget.add_to(in_attrs, '', true);
+	     });
+	
+	// Add the output from the accordion to the page.
+	var accordion_str = filter_accordion_widget.to_string();
+	jQuery('#' + ui_controls_section_id).append(accordion_str);
+
+	// Add the jQuery accordioning.
+	var jqacc_attrs = {
+	    clearStyle: true,
+	    heightStyle: 'content',
+	    collapsible: true,
+	    active: false
+	};
+	jQuery("#" + accordion_div_id).accordion(jqacc_attrs);
+    };
+
+    /*
+     * Function: setup_results
+     *
+     * Setup basic results table using the class conf. For actual
+     * results rendering, see .draw_results. While there is a meta
+     * block supplied, its use is optional.
+     * 
+     * Argument hash entries:
+     *  meta - draw the meta-results; defaults to false
+     *  spinner_source - the source of the image to use for the activity spinner
+     * 
+     * Parameters:
+     *  hash; see above for details
+     *
+     * Returns:
+     *  n/a
+     */
+    this.setup_results = function(args){
+
+	ll('setup_results UI for class configuration: ' + this.class_conf.id());
+	
+	// Decide whether or not to add the meta div.
+	var add_meta_p = false;
+	if( args && args['meta'] && args['meta'] == true ){
+	    add_meta_p = true;
+	}
+	// Get the spinner source and set it globally, if there is
+	// one.
+	var add_spinner_p = false;
+	if( args && args['spinner_source'] && args['spinner_source'] != '' ){
+	    ui_spinner_search_source = args['spinner_source'];
+	    add_spinner_p = true;
+	}
+
+	// <div id="results_block" class="block">
+	// <h2>Found entities</h2>
+	// <div id="load_float"></div>
+	// <div id="meta_results">
+	// <div id="results_div">
+	var block = new bbop.html.tag('div', {'class': 'block'});
+
+	// Add header section.
+	var hargs = {
+	    generate_id: true,
+	    'classes': 'bbop-widget-search_pane-spinner-element'
+	};
+	var header = new bbop.html.tag('h2', hargs, 'Found entities ');
+	block.add_to(header);
+
+	// If wanted, add meta to display queue.
+	if( add_meta_p ){	    
+	    var meta = new bbop.html.tag('div', {'id': ui_meta_div_id});
+	    block.add_to(meta);
+	}
+
+	// Add results section.
+	var results = new bbop.html.tag('div', {'id': ui_results_table_div_id});
+	block.add_to(results);
+
+	jQuery('#' + ui_results_section_id).append(block.to_string());
+
+	// Now that the block is added, we can add the spinner to our
+	// larger context. Safe access functions defined elsewhere.
+	if( add_spinner_p ){
+	    _spinner_gen(header.get_id());
+	}
+
+	// If wanted, add initial render of meta.
+	if( add_meta_p ){	    
+	    ll('Add meta UI div');
+	    jQuery('#' + ui_meta_div_id).empty();
+	    var init_str = 'Performing initial search, please wait...';
+	    jQuery('#' + ui_meta_div_id).append(init_str);
+
+	    // Optionally, if we have defined the image source, add
+	    // the image to the initial waiting.
+	    if( ui_spinner_search_source && ui_spinner_search_source != '' ){
+		var init_spin_str = ' <img src="' +
+		    ui_spinner_search_source + '" alt="[waiting]" ' +
+		    'class="bbop-js-spinner"/>';
+		jQuery('#' + ui_meta_div_id).append(init_spin_str);
+	    }
+	}
+    };
+
+    /*
+     * Function: draw_user_buttons
+     *
+     * (Re)draw the user-defined buttons in the meta information area.
+     * Will naturally fail if there is no meta div that has been
+     * nested with the user button element.
+     * 
+     * Parameters:
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_user_buttons = function(manager){
+	function _button_rollout(button_def_hash){
+	    var default_hash =
+    		{
+		    label : 'n/a',
+		    disabled_p : false,
+		    text_p : false,
+		    icon : 'ui-icon-help',
+		    click_function_generator :
+		    function(){
+			return function(){
+			    alert('No callback defined for this button--' +
+				  'the generator may have been empty!');
+			};
+		    }
+    		};
+	    var folding_hash = button_def_hash || {};
+	    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+	    
+	    var label = arg_hash['label'];
+	    var disabled_p = arg_hash['disabled_p'];
+	    var text_p = arg_hash['text_p'];
+	    var icon = arg_hash['icon'];
+	    var click_function_generator =
+		arg_hash['click_function_generator'];
+	    
+	    var b = new bbop.html.button(label, {'generate_id': true});
+	    jQuery('#' + ui_user_button_div_id).append(b.to_string());
+	    var b_props = {
+		icons: { primary: icon},
+		disabled: disabled_p,
+		text: text_p
+	    };
+	    var click_fun = click_function_generator(manager);
+	    jQuery('#' + b.get_id()).button(b_props).click(click_fun);
+	}
+
+	// Check that we're not about to do the impossible.
+	if( ! jQuery('#' + ui_user_button_div_id) ){
+	    alert('cannot refresh buttons without a place to draw them');
+	}else{
+	    jQuery('#' + ui_user_button_div_id).empty();
+	    jQuery('#' + ui_user_button_div_id).empty();
+	    bbop.core.each(anchor.button_definitions, _button_rollout);
+	}
+    };
+
+    /*
+     * Function: draw_meta
+     *
+     * Draw meta results. Includes selector for drop down.
+     * 
+     * (Re)draw the count control with the current information in the
+     * manager. This also tries to set the selector to the response
+     * number (to keep things in sync), unbinds any current "change"
+     * event, and adds a new change event.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_meta = function(response, manager){
+	
+	ll('draw_meta for: ' + ui_meta_div_id);
+
+	///
+	/// Section 1: the numbers display.
+	///
+
+	// Collect numbers for display.
+	var total_c = response.total_documents();
+	var first_d = response.start_document();
+	var last_d = response.end_document();
+
+	// Draw meta; the current numbers and page--the same for
+	// every type of return.
+	jQuery('#' + ui_meta_div_id).empty();
+	if( total_c == 0 ){
+	    jQuery('#' + ui_meta_div_id).append('No results found.');
+	}else{
+
+	    // A div for the literal meta results and the count
+	    // selector next to them.
+	    var mdiv_attrs = {
+	    };
+	    var mdiv = new bbop.html.tag('div', mdiv_attrs);
+
+	    // The literal return metadata.
+	    var dmeta_attrs = {
+		'class': 'bbop-js-search-pane-meta'
+	    };
+	    var dmeta = new bbop.html.tag('div', dmeta_attrs);
+	    dmeta.add_to('Total: ' + total_c +
+			 '; showing ' + first_d +
+			 '-' + last_d);
+	    mdiv.add_to(dmeta);
+
+	    ///
+	    /// Section 2: results count.
+	    ///
+
+	    // Create inputs (the current order is important for proper
+	    // for/id creation).
+	    var cinputs = [];
+	    each([10, 25, 50, 100],
+		 function(num, cindex){
+		     // Create and store the option.
+		     var sel_input_attrs = {
+			 'generate_id': true,
+			 'value': num
+		     };
+		     var sel_input =
+			 new bbop.html.tag('option', sel_input_attrs, num);
+		     var sel_input_id = sel_input.get_id();
+		     cinputs.push(sel_input);
+		 });
+	    // Option container div.
+	    var sel_attrs = {
+		'id': ui_count_control_div_id
+	    };
+	    var sel = new bbop.html.tag('select', sel_attrs, cinputs);
+	    
+	    // Create a text label.
+	    var sel_label = new bbop.html.tag('label', {},
+					      'Results count  ');
+	    
+	    // Container div.
+	    var sel_div_attrs = {
+		'generate_id': true,
+		'class': 'bbop-js-search-pane-results-count'
+	    };
+	    var sel_div = new bbop.html.tag('div', sel_div_attrs);
+	    
+	    // Assemble these elements into the UI.
+	    sel_div.add_to(sel_label);
+	    sel_div.add_to(sel);
+	    mdiv.add_to(sel_div);
+
+	    // Render out the last two sections.
+	    jQuery('#' + ui_meta_div_id).append(mdiv.to_string());
+	    
+	    ///
+	    /// Section 3: results count activity, setting.
+	    ///
+
+	    // First, unbind so we don't accidentally trigger with any
+	    // changes and don't pile up event handlers.
+	    jQuery('#' + ui_count_control_div_id).unbind('change');
+
+	    // Next, pull out the number of rows requested.
+	    var step = response.row_step();
+	    
+	    // Set the value to the number.
+	    jQuery('#' + ui_count_control_div_id).val(step);
+	    
+	    // Finally, reactivate the event handler on the select.
+	    jQuery('#' + ui_count_control_div_id).change(
+		function(event, ui){
+		    var sv = jQuery('#' + ui_count_control_div_id).val();
+		    if( bbop.core.is_defined(sv) ){
+			// Convert to a number.
+			var si = parseInt(sv);
+			
+			// Set manager and to the search.
+			manager.set_results_count(si);
+			manager.search();
+			// We are now searching--show it.
+			_spin_up();
+		    }
+		});
+
+	    ///
+	    /// Section 4: the paging buttons.
+	    ///
+	    
+	    var bdiv_attrs = {
+		'generate_id': true
+	    };
+	    var bdiv = new bbop.html.tag('div', bdiv_attrs);
+	    jQuery('#' + ui_meta_div_id).append(bdiv.to_string());
+	    var bdiv_id = bdiv.get_id();
+
+	    // Now add the raw buttons to the interface, and after this,
+	    // activation and adding events.
+	    var b_first = new bbop.html.button('First', {'generate_id': true});
+	    //jQuery('#' + ui_meta_div_id).append(b_first.to_string());
+	    jQuery('#' + bdiv_id).append(b_first.to_string());
+	    var b_back = new bbop.html.button('Prev', {'generate_id': true});
+	    //jQuery('#' + ui_meta_div_id).append(b_back.to_string());
+	    jQuery('#' + bdiv_id).append(b_back.to_string());
+	    var b_forward = new bbop.html.button('Next', {'generate_id': true});
+	    //jQuery('#' + ui_meta_div_id).append(b_forward.to_string());
+	    jQuery('#' + bdiv_id).append(b_forward.to_string());
+	    var b_last = new bbop.html.button('Last', {'generate_id': true});
+	    //jQuery('#' + ui_meta_div_id).append(b_last.to_string());
+	    jQuery('#' + bdiv_id).append(b_last.to_string());
+
+	    // Do the math about what buttons to activate.
+	    var b_first_disabled_p = false;
+	    var b_back_disabled_p = false;
+	    var b_forward_disabled_p = false;
+	    var b_last_disabled_p = false;
+	    
+	    // Only activate paging if it is necessary to the returns.
+	    if( ! response.paging_p() ){
+		b_first_disabled_p = true;
+		b_back_disabled_p = true;
+		b_forward_disabled_p = true;
+		b_last_disabled_p = true;
+	    }
+	    
+	    // Don't activate back on the first page.
+	    if( ! response.paging_previous_p() ){
+		b_first_disabled_p = true;
+		b_back_disabled_p = true;
+	    }
+	    
+	    // Don't activate next on the last page.
+	    if( ! response.paging_next_p() ){
+		b_forward_disabled_p = true;
+		b_last_disabled_p = true;
+	    }
+	    
+	    // First page button.
+	    var b_first_props = {
+		icons: { primary: "ui-icon-seek-first"},
+		disabled: b_first_disabled_p,
+		text: false
+	    };
+	    jQuery('#' + b_first.get_id()).button(b_first_props).click(
+		function(){
+		    // Cheat and trust reset by proxy to work.
+		    manager.page_first(); 
+		    // We are now searching--show it.
+		    _spin_up();
+		});
+	    
+	    // Previous page button.
+	    var b_back_props = {
+		icons: { primary: "ui-icon-seek-prev"},
+		disabled: b_back_disabled_p,
+		text: false
+	    };
+	    jQuery('#' + b_back.get_id()).button(b_back_props).click(
+		function(){
+		    manager.page_previous();
+		    // We are now searching--show it.
+		    _spin_up();
+		});
+	    
+	    // Next page button.
+	    var b_forward_props = {
+		icons: { primary: "ui-icon-seek-next"},
+		disabled: b_forward_disabled_p,
+		text: false
+	    };
+	    jQuery('#' + b_forward.get_id()).button(b_forward_props).click(
+		function(){
+		    manager.page_next();
+		    // We are now searching--show it.
+		    _spin_up();
+		});
+	    
+	    // Last page button.
+	    var b_last_props = {
+		icons: { primary: "ui-icon-seek-end"},
+		disabled: b_last_disabled_p,
+		text: false
+	    };
+	    jQuery('#' + b_last.get_id()).button(b_last_props).click(
+		function(){
+		    // A little trickier.
+		    manager.page_last(total_c);
+		    // We are now searching--show it.
+		    _spin_up();
+		});
+	    
+	    ///
+	    /// Section 5: the button_definition buttons.
+	    ///
+
+	    // Spacer.	    
+	    // jQuery('#' + ui_meta_div_id).append('   ' +
+	    // 					'   ');
+	    jQuery('#'+ bdiv_id).append('      ');
+
+	    // (R)establish the user button div to the end of the meta
+	    // retults.
+	    var ubuttons = new bbop.html.tag('span',
+					     {'id': ui_user_button_div_id});
+	    //jQuery('#' + ui_meta_div_id).append(ubuttons.to_string());
+	    jQuery('#' + bdiv_id).append(ubuttons.to_string());
+
+	    // Add all of the defined buttons after the spacing.
+	    anchor.draw_user_buttons(manager);
+	}
+    };
+
+    // Detect whether or not a keyboard event is ignorable.
+    function _ignorable_event(event){
+
+	var retval = false;
+
+	if( event ){
+	    var kc = event.keyCode;
+	    if( kc ){
+		if( kc == 39 || // right
+                    kc == 37 || // left
+                    kc == 32 || // space
+                    kc == 20 || // ctl?
+                    kc == 17 || // ctl?
+                    kc == 16 || // shift
+                    //kc ==  8 || // delete
+                    kc ==  0 ){ // super
+			ll('ignorable key event: ' + kc);
+			retval = true;
+		    }
+            }
+	}
+	return retval;
+    }
+
+    /*
+     * Function: draw_query
+     *
+     * Draw the query widget. This function makes it active
+     * as well.
+     * 
+     * Clicking the reset button will reset the query to ''.
+     * 
+     * NOTE: Since this is part of the "persistant" interface (i.e. it
+     * does not get wiped after every call), we make sure to clear the
+     * event listeners when we redraw the function to prevent them from
+     * building up.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_query = function(response, manager){
+
+    	ll('draw_query for: ' + ui_query_input_id);
+
+	// Add a smartish listener.
+	jQuery('#' + ui_query_input_id).unbind('keyup');
+	jQuery('#' + ui_query_input_id).keyup(
+	    function(event){
+
+		// If we're left with a legitimate event, handle it.
+		if( ! _ignorable_event(event) ){
+
+		    // Can't ignore it anymore, so it goes into the
+		    // manager for testing.
+		    var tmp_q = manager.get_query();
+		    var input_text = jQuery(this).val();
+		    manager.set_query(input_text);
+
+		    // If the manager feels like it's right, trigger.
+		    if( manager.sensible_query_p() ){
+			ll('keeping set query: ' + input_text);
+			// Set the query to be more "usable" just
+			// before triggering (so the tests can't be
+			// confused by our switch).
+			manager.set_comfy_query(input_text);
+			manager.search();
+
+			// We are now searching--show it.
+			_spin_up();
+		    }else{
+			ll('rolling back query: ' + tmp_q);		    
+			manager.set_query(tmp_q);
+		    }
+		}
+	    });
+
+	// Now reset the clear button and immediately set the event.
+	jQuery('#' + ui_clear_query_span_id).unbind('click');
+	jQuery('#' + ui_clear_query_span_id).click(
+	    function(){
+		manager.reset_query();
+		//anchor.set_query_field(manager.get_query());
+		anchor.set_query_field('');
+		manager.search();
+		// We are now searching--show it.
+		_spin_up();
+	    });
+    };
+
+    /*
+     * Function: reset_query
+     *
+     * Simply reset the query and then redraw (rebind) the query.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     * 
+     * See:
+     *  <draw_query>
+     */
+    this.reset_query = function(response, manager){
+
+    	ll('reset_query for: ' + ui_query_input_id);
+
+	// Reset manager back to the default.
+	manager.reset_query();
+
+	anchor.draw_query(response, manager);
+    };
+
+    /*
+     * Function: draw_sticky_filters
+     *
+     * (Re)draw the information on the sticky filter set.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_sticky_filters = function(response, manager){
+    
+	ll('draw_sticky_filters for: ' + ui_div_id);
+
+	// Add in the actual HTML for the pinned filters and buttons.
+	var sticky_query_filters = manager.get_sticky_query_filters();
+	ll('sticky filters: ' + bbop.core.dump(sticky_query_filters));
+	var fq_list_tbl =
+	    new bbop.html.table(['',
+				 'Your search is pinned to these filters'], []);
+	// [{'filter': A, 'value': B, 'negative_p': C, 'sticky_p': D}, ...]
+	each(sticky_query_filters,
+	     function(fset){
+
+		 //
+		 var sfield = fset['filter'];
+		 var sfield_val = fset['value'];
+
+		 // Boolean value to a character.
+		 var polarity = fset['negative_p'];
+		 var polstr = '+';
+		 if( polarity ){ polstr = '-'; }
+
+		 // Generate a button with a unique id.
+		 var label_str = polstr + ' ' + sfield + ':' + sfield_val;
+		 fq_list_tbl.add_to(['<b>'+ polstr +'</b>',
+				     sfield + ': ' + sfield_val]);
+	     });
+	
+	// Either add to the display, or display the "empty" message.
+	var sfid = '#' + ui_sticky_filters_div_id;
+	jQuery(sfid).empty();
+	if( sticky_query_filters.length == 0 ){
+	    jQuery(sfid).append("No sticky filters.");
+	}else{
+	    // Attach to the DOM...
+	    jQuery(sfid).append(fq_list_tbl.to_string());
+	}
+    };
+
+    /*
+     * Function: draw_current_filters
+     *
+     * (Re)draw the information on the current filter set.
+     * This function makes them active as well.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_current_filters = function(response, manager){
+    
+	ll('draw_current_filters for: ' + ui_div_id);
+
+	///
+	/// Add in the actual HTML for the filters and buttons. While
+	/// doing so, tie a unique id to the filter--we'll use that
+	/// later on to add buttons and events to them.
+	///
+
+	// First, we need to make the filter clear button for the top
+	// of the table.
+	var b_cf =
+	    new bbop.widget.display.text_button_sim('X', 
+						    'Clear all user filters',
+						    ui_clear_user_filter_span_id);
+
+	var in_query_filters = response.query_filters();
+	//var sticky_query_filters = manager.get_sticky_query_filters();
+	ll('filters: ' + bbop.core.dump(in_query_filters));
+	var fq_list_tbl = new bbop.html.table(['', 'User filters',
+					       b_cf.to_string()], []);
+	var has_fq_p = false; // assume there are no filters to begin with
+	var button_hash = {};
+	each(in_query_filters,
+	     function(field, field_vals){
+		 each(field_vals,
+		      function(field_val, polarity){
+
+			  // Make note of stickiness, skip adding if sticky.
+			  var qfp =
+			      manager.get_query_filter_properties(field,
+								  field_val);
+			  if( ! qfp || qfp['sticky_p'] == false ){
+			  
+			      // Note the fact that we actually have a
+			      // query filter to work with and display.
+			      has_fq_p = true;
+
+			      // Boolean value to a character.
+			      var polstr = '-';
+			      if( polarity ){ polstr = '+'; }
+
+			      // Generate a button with a unique id.
+			      var label_str = polstr+' '+ field +':'+field_val;
+
+			      // Argh! Real jQuery buttons are way too slow!
+			      // var b = new bbop.html.button('remove filter',
+			      // 		  {'generate_id': true});
+
+			      // Is the "button" a span or an image?
+			      var b = bbop.widget.display.clickable_object(
+				  ui_icon_remove_label,
+				  ui_icon_remove_source,
+				  null); // generate_id
+
+			      // Tie the button it to the filter for
+			      // jQuery and events attachment later on.
+			      var bid = b.get_id();
+			      button_hash[bid] = [polstr, field, field_val];
+			  
+			      //ll(label_str +' '+ bid);
+			      //fq_list_tbl.add_to(label_str +' '+ b.to_string());
+			      fq_list_tbl.add_to(['<b>'+ polstr +'</b>',
+						  field + ': ' + field_val,
+						  b.to_string()]);
+			      //label_str +' '+ b.to_string());
+			  }
+		      });
+	     });
+
+	// Either add to the display, or display the "empty" message.
+	var cfid = '#' + ui_current_filters_div_id;
+	jQuery(cfid).empty();
+	if( ! has_fq_p ){
+	    jQuery(cfid).append("No current user filters.");
+	}else{
+
+	    // With this, the buttons will be attached to the
+	    // DOM...
+	    jQuery(cfid).append(fq_list_tbl.to_string());
+	    
+	    // First, lets add the reset for all of the filters.
+	    jQuery('#' + b_cf.get_id()).click(
+		function(){
+       		    manager.reset_query_filters();
+       		    manager.search();
+		    // We are now searching--show it.
+		    _spin_up();
+		}		
+	    );
+
+	    // Now let's go back and add the buttons, styles,
+	    // events, etc. to the filters.
+	    each(button_hash,
+		 function(button_id){
+		     var bid = button_id;
+
+		     // // Get the button.
+		     // var bprops = {
+		     // 	 icons: { primary: "ui-icon-close"},
+		     // 	 text: false
+		     // };
+		     // Create the button and immediately add the event.
+		     //jQuery('#' + bid).button(bprops).click(
+		     jQuery('#' + bid).click(
+			 function(){
+			     var tid = jQuery(this).attr('id');
+			     var button_props = button_hash[tid];
+			     var polstr = button_props[0];
+			     var field = button_props[1];
+			     var value = button_props[2];
+
+			     // Change manager and fire.
+			     // var lstr = polstr +' '+ field +' '+ value;
+			     // alert(lstr);
+			     // manager.remove_query_filter(field,value,
+			     // 				 [polstr, '*']);
+			     manager.remove_query_filter(field, value);
+			     manager.search();
+			     // We are now searching--show it.
+			     _spin_up();
+			 });
+		 });
+	}
+    };
+
+    /*
+     * Function: draw_accordion
+     *
+     * (Re)draw the information in the accordion controls/filters.
+     * This function makes them active as well.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_accordion = function(response, manager){
+    
+	ll('draw_accordion for: ' + ui_div_id);
+
+	// Make sure that accordion has already been inited.
+	if( typeof(filter_accordion_widget) == 'undefined' ){
+	    throw new Error('Need to init accordion to use it.');
+	}
+
+	// We'll need this in a little bit for calculating when to
+	// display the "more" option for the field filters.
+	var real_facet_limit = manager.get_facet_limit();
+	var curr_facet_limit = real_facet_limit -1; // the facets we'll show
+
+	// We want this so we can filter out any facets that have the
+	// same count as the current response total--these facets are
+	// pretty much information free.
+	var total_docs = response.total_documents();
+
+	// A helper function for when no filters are
+	// displayed.
+	function _nothing_to_see_here(in_field){
+	    var section_id = filter_accordion_widget.get_section_id(in_field);
+	    jQuery('#' + section_id).empty();
+	    jQuery('#' + section_id).append('Nothing to filter.');
+	}
+
+	// Hash where we collect our button information.
+	// button_id -> [source, filter, count, polarity];
+	var button_hash = {};
+
+	// And a hash to store information to be able to generate the
+	// complete filter shields.
+	// span_id -> filter_id
+	var overflow_hash = {};
+
+	// Cycle through each facet field; all the items in each,
+	// create the lists and buttons (while collectong data useful
+	// in creating the callbacks) and put them into the accordion.
+	each(response.facet_field_list(),
+	     function(in_field){
+
+		 var facet_bd = response.facet_field(in_field);
+		 if( bbop.core.is_empty(facet_bd) ){
+		     
+		     // No filters means nothing in the box.
+		     _nothing_to_see_here(in_field);
+
+		 }else{
+		     
+		     // Create ul lists of the facet contents.
+		     var tbl_id = mangle + 'filter-list-' + in_field;
+		     var facet_list_tbl_attrs = {
+			 id: tbl_id
+		     };
+
+		     var facet_list_tbl =
+			 new bbop.html.table([], [], facet_list_tbl_attrs);
+		     
+		     ll("consider:" + in_field + ": " +
+			response.facet_field(in_field).length);
+
+		     // BUG/TODO:
+		     // Count the number of redundant (not shown)
+		     // facets so we can at least give a face to this
+		     // bug/problem.
+		     var redundant_count = 0;
+		     // Now go through and get filters and counts.
+		     var good_count = 0; // only count when good
+		     var overflow_p = false; // true when at 24 -> 25
+		     each(response.facet_field(in_field),
+			  function(ff_field, ff_index){
+				  
+			      // Pull out info early so we can test it
+			      // for information content.
+			      var f_name = ff_field[0];
+			      var f_count = ff_field[1];
+			      
+			      // ll(in_field + ": " + f_name + ": " +
+			      // 	 [f_count,
+			      // 	  total_docs,
+			      // 	  ff_index,
+			      // 	  good_count,
+			      // 	  redundant_count,
+			      // 	  real_facet_limit].join(', '));
+			      
+			      // TODO: The field is likely redundant
+			      // (BUG: not always true in closures),
+			      // so eliminate it.
+			      if( f_count == total_docs ){
+				  //ll("\tnothing here");
+				  redundant_count++;
+			      }else if( ff_index < real_facet_limit -1 ){
+				  //ll("\tgood row");
+				  good_count++;
+
+				  // Create buttons and store them for later
+				  // activation with callbacks to
+				  // the manager.
+				  var b_plus =
+				      bbop.widget.display.clickable_object(
+					  ui_icon_positive_label,
+					  ui_icon_positive_source,
+					  null); // generate_id
+				  var b_minus =
+				      bbop.widget.display.clickable_object(
+					  ui_icon_negative_label,
+					  ui_icon_negative_source,
+					  null); // generate_id
+				  
+				  // Store in hash for later keying to
+				  // event.
+				  button_hash[b_plus.get_id()] =
+				      [in_field, f_name, f_count, '+'];
+				  button_hash[b_minus.get_id()] =
+				      [in_field, f_name, f_count, '-'];
+				  
+				  // // Add the label and buttons to the
+				  // // appropriate ul list.
+				  //facet_list_ul.add_to(
+				  // fstr,b_plus.to_string(),
+				  //   b_minus.to_string());
+				  // Add the label and buttons to the table.
+				  facet_list_tbl.add_to([f_name,
+							 '('+ f_count+ ')',
+							 b_plus.to_string(),
+							 b_minus.to_string()
+							]);
+			      }
+			
+			      // This must be logically separated from
+			      // the above since we still want to show
+			      // more even if all of the top 25 are
+			      // redundant.
+			      if( ff_index == real_facet_limit -1 ){
+				  // Add the more button if we get up to
+				  // this many facet rows. This should
+				  // only happen on the last possible
+				  // iteration.
+				  
+				  overflow_p = true;
+				  //ll( "\tadd [more]");
+				  
+				  // Since this is the overflow item,
+				  // add a span that can be clicked on
+				  // to get the full filter list.
+				  //ll("Overflow for " + in_field);
+				  var bgn = bbop.widget.display.text_button_sim;
+				  var b_over =
+				      new bgn('more...',
+					      'Display the complete list');
+				  facet_list_tbl.add_to([b_over.to_string(),
+				  			 '', '']);
+				  overflow_hash[b_over.get_id()] = in_field;
+			      }
+			  });
+
+		     // There is a case when we have filtered out all
+		     // avilable filters (think db source).
+		     if( good_count == 0 && ! overflow_p ){
+			 _nothing_to_see_here(in_field);
+		     }else{
+			 // Otherwise, now add the ul to the
+			 // appropriate section of the accordion in
+			 // the DOM.
+			 var sect_id =
+			     filter_accordion_widget.get_section_id(in_field);
+			 jQuery('#' + sect_id).empty();
+
+			 // TODO/BUG:
+			 // Give warning to the redundant facets.
+			 var warn_txt = null;
+			 if( redundant_count == 1 ){
+			     warn_txt = "field is";
+			 }else if( redundant_count > 1 ){
+			     warn_txt = "fields are";
+			 }
+			 if( warn_txt ){
+			     jQuery('#' + sect_id).append(
+				 "<small> The top (" + redundant_count +
+				     ") redundant " + warn_txt + " not shown" +
+				     "</small>");
+							  
+			 }
+
+			 // Add facet table.
+			 var final_tbl_str = facet_list_tbl.to_string();
+			 jQuery('#' + sect_id).append(final_tbl_str);
+		     }
+		 }
+	     });
+
+	// Okay, now introducing a function that we'll be using a
+	// couple of times in our callbacks. Given a button id (from
+	// a button hash) and the [field, filter, count, polarity]
+	// values from the props, make a button-y thing an active
+	// filter.
+	function filter_select_live(button_id, create_time_button_props){
+	    //var bid = button_id;
+	    //var in_field = create_time_button_props[0];	 
+	    //var in_filter = create_time_button_props[1];
+	    //var in_count = create_time_button_props[2];
+	    var in_polarity = create_time_button_props[3];
+
+	    // Decide on the button graphical elements.
+	    var b_ui_icon = 'ui-icon-plus';
+	    if( in_polarity == '-' ){
+		b_ui_icon = 'ui-icon-minus';
+	    }
+	    var b_ui_props = {
+		icons: { primary: b_ui_icon},
+		text: false
+	    };
+
+	    // Create the button and immediately add the event.
+	    //jQuery('#' + button_id).button(b_ui_props).click(
+	    jQuery('#' + button_id).click(
+		function(){
+		    var tid = jQuery(this).attr('id');
+		    var call_time_button_props = button_hash[tid];
+		    var call_field = call_time_button_props[0];	 
+		    var call_filter = call_time_button_props[1];
+		    //var in_count = button_props[2];
+		    var call_polarity = call_time_button_props[3];
+		    
+		    // Change manager and fire.
+		    // var bstr =call_field+' '+call_filter+' '+call_polarity;
+		    // alert(bstr);
+		    manager.add_query_filter(call_field, call_filter,
+			  		     [call_polarity]);
+		    manager.search();
+		    // We are now searching--show it.
+		    _spin_up();
+		});
+	}
+
+	// Now let's go back and add the buttons, styles,
+	// events, etc. in the main accordion section.
+	each(button_hash, filter_select_live);
+
+	// Next, tie the events to the "more" spans.
+	each(overflow_hash,
+	     function(button_id, filter_name){
+		 jQuery('#' + button_id).click(
+
+		     // On click, set that one field to limitless in
+		     // the manager, setup a shield, and wait for the
+		     // callback.
+		     function(){
+
+			 // Recover the field name.
+			 var tid = jQuery(this).attr('id');
+			 var call_time_field_name = overflow_hash[tid];
+			 //alert(call_time_field_name);
+
+			 // Set the manager to no limit on that field and
+			 // only rturn the information that we want.
+			 manager.set_facet_limit(0);
+			 manager.set_facet_limit(call_time_field_name, -1);
+			 var curr_row = manager.get('rows');
+			 manager.set('rows', 0);
+
+			 // Create the shield and pop-up the
+			 // placeholder.
+			 var fs = bbop.widget.display.filter_shield;
+			 var filter_shield = new fs(ui_spinner_shield_source); 
+			 filter_shield.start_wait();
+
+			 // Open the populated shield.
+			 function draw_shield(resp){
+
+			    // ll("shield what: " + bbop.core.what_is(resp));
+			    // ll("shield resp: " + bbop.core.dump(resp));
+
+			     // First, extract the fields from the
+			     // minimal response.
+			     var fina = call_time_field_name;
+			     var flist = resp.facet_field(call_time_field_name);
+
+			     // Draw the proper contents of the shield.
+			     filter_shield.draw(fina, flist, manager);
+			 }
+			 manager.fetch(draw_shield);
+
+			 // Reset the manager to more sane settings.
+			 manager.reset_facet_limit();
+			 manager.set('rows', curr_row);
+		     });
+	     });
+
+	ll('Done current accordion for: ' + ui_div_id);
+    };
+
+    /*
+     * Function: draw_results
+     *
+     * Draw results using hints from the golr class configuration.
+     * 
+     * Parameters:
+     *  response - the <bbop.golr.response> returned from the server
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_results = function(response, manager){
+	
+	ll('draw_results for: ' + ui_results_table_div_id);
+
+	//ll('final_table a: ' + final_table._is_a);
+	//ll('final_table b: ' + final_table.to_string);
+	//ll('final_table c: ' + final_table.to_string());
+
+	// Clear whatever is there.
+	var urtdi = ui_results_table_div_id;
+	jQuery('#' + urtdi).empty();
+
+	// Display product when not empty.
+	var docs = response.documents();
+	if( ! bbop.core.is_empty(docs) ){
+	    var final_table = new bbop.widget.display.results_table_by_class(
+		anchor.class_conf,
+		response,
+		anchor.linker,
+		anchor.handler,
+		urtdi,
+		show_checkboxes_p);
+
+	    // Capture the current name state of the control and
+	    // group.
+	    ui_results_selection_control_id = final_table.toggle_id();
+	    ui_results_selection_item_name = final_table.item_name();
+
+	    // Since we already added to the DOM in the final_table
+	    // instantiation above, go ahead and locally add the group
+	    // toggle if the checkboxes are defined.
+	    if( ui_results_selection_control_id &&
+		ui_results_selection_item_name ){
+		    jQuery('#' + ui_results_selection_control_id).click(
+			function(){
+			    var cstr = 'input[id=' +
+				ui_results_selection_control_id +
+				']';
+			    var nstr = 'input[name=' +
+				ui_results_selection_item_name +
+				']';
+			    if( jQuery(cstr).prop('checked') ){
+				jQuery(nstr).prop('checked', true);
+			    }else{
+				jQuery(nstr).prop('checked', false);
+			    }
+			});
+	    }
+	}
+
+	// Our search obviously came back.
+	_spin_down();
+
+	// If it looks like we enabled the checkboxes, go ahead and
+	// activate the group toggle for them.
+	
+    };
+
+    /*
+     * Function: draw_error
+     *
+     * Somehow report an error to the user.
+     * 
+     * Parameters:
+     *  error_message - a string(?) describing the error
+     *  manager - <bbop.golr.manager> that we initially registered with
+     *
+     * Returns:
+     *  n/a
+     */
+    this.draw_error = function(error_message, manager){
+	ll("draw_error: " + error_message);
+	alert("Runtime error: " + error_message);
+	_spin_down();
+    };
+
+    /*
+     * Function: set_buttons
+     *
+     * Set the list of buttons for display by changing the button
+     * definition hash list.
+     * 
+     * If no buttons are set, the list is cleared.
+     * 
+     * Parameters:
+     *  button_def_list - *[optional]*
+     *
+     * Returns:
+     *  n/a
+     */
+    this.set_buttons = function(button_def_list){
+	if( ! button_def_list ){
+	    button_def_list = [];
+	}
+	ll("changing buttons: to " + button_def_list.length +
+	   " from " + anchor.button_definitions.length);
+	anchor.button_definitions = button_def_list;
+    };
+
+    /*
+     * Function: set_query_field
+     *
+     * Set the text in the search query field box.
+     * 
+     * If no query is set, the field is cleared.
+     * 
+     * Parameters:
+     *  query - *[optional]* string
+     *
+     * Returns:
+     *  true or false on whether the task was accomplished
+     */
+    this.set_query_field = function(query){
+	var retval = false;
+	if( ! query ){
+	    query = '';
+	}
+	if( jQuery('#' + ui_query_input_id) ){
+	    ll("changing query search field: to " + query);
+	    jQuery('#' + ui_query_input_id).val(query);
+	    //jQuery('#' + ui_query_input_id).keyup();
+	    retval = true;
+	}
+	return retval;
+    };
+};
+/*
+ * Package: spinner.js
+ * 
+ * Namespace: bbop.widget.spinner
+ * 
+ * BBOP object to produce a self-constructing/self-destructing
+ * spinner. It can display various spinner/throbber images and can
+ * have a set timeout to deal with annoying servers and exotic race
+ * conditions.
+ * 
+ * The class of the spinner image is "bbop-widget-spinner".
+ * 
+ * Visibility is controlled by the application and removal of
+ * "bbop-js-spinner-hidden".
+ * 
+ * This is a completely self-contained UI.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'spinner');
+
+/*
+ * Constructor: spinner
+ * 
+ * Contructor for the bbop.widget.spinner object.
+ * 
+ * A trivial invocation might be something like:
+ * : var s = new bbop.widget.spinner("inf01", "http://localhost/amigo2/images/waiting_ajax.gif");
+ * : s.hide();
+ * : s.show();
+ * 
+ * Or, in a slightly different use case:
+ * 
+ * : var s = new bbop.widget.spinner("inf01", "http://localhost/amigo2/images/waiting_ajax.gif", {'timout': 5});
+ * : s.start_wait();
+ * 
+ * The optional hash arguments look like:
+ *  timeout - the number of seconds to wait before invoking <clear_waits>; 0 indicates waiting forever; defaults to 5
+ *  visible_p - whether or not the spinner is visible on initialization; true|false; defaults to true
+ *  classes - a string of space-separated classes that you want added to the spinner image
+ * 
+ * Arguments:
+ *  host_elt_id - string id of the place to place the widget
+ *  img_src - the URL for the image to use in the spinner
+ *  argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.spinner = function(host_elt_id, img_src, argument_hash){
+    
+    this._is_a = 'bbop.widget.spinner';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (spinner): ' + str); }
+
+    // Our argument default hash.
+    var default_hash = {
+	'timeout': 5,
+	'visible_p': true,
+	'classes': ''
+    };
+    var folding_hash = argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // Spin out arguments.
+    var timeout = arg_hash['timeout'];
+    var visible_p = arg_hash['visible_p'];
+    var classes = arg_hash['classes'];
+
+    ///
+    /// Part 1: Append the image into the given element id.
+    ///
+
+    // Use the incoming arguments to help determine the default
+    // classes on the element.'
+    var spinner_classes = ['bbop-js-spinner'];
+    if( ! visible_p ){
+	spinner_classes.push('bbop-js-spinner-hidden');
+    }
+    if( classes && classes != '' ){
+	spinner_classes.push(classes);
+    }
+
+    // Create new element.
+    var spinner_elt =
+	new bbop.html.image({'generate_id': true,
+			     'src': img_src,
+			     'title': "Please wait...",
+			     'class': spinner_classes.join(' '),
+			     'alt': "(waiting...)"});
+    var spinner_elt_id = spinner_elt.get_id();
+
+    // Append img to end of given element.
+    jQuery('#' + host_elt_id).append(spinner_elt.to_string());
+    
+    ///
+    /// Part 2: Dynamic display management.
+    ///
+
+    // Counts and accounting.
+    var current_waits = 0;
+    var timeout_queue = [];
+
+    /*
+     * Function: show
+     * 
+     * Show the spinner if it is hidden (regardless of current waits).
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.show = function(){
+	ll("show");
+	jQuery('#' + spinner_elt_id).removeClass('bbop-js-spinner-hidden');	
+
+	// If the timeout is defined, push a timer onto
+	// the queue.
+	function _on_timeout(){
+	    anchor.finish_wait();
+	}
+	if( timeout > 0 ){
+	    setTimeout(_on_timeout, (timeout * 1000));
+	}
+	// foo=setTimeout(function(){}, 1000);
+	// clearTimeout(foo);
+    };
+
+    /*
+     * Function: hide
+     * 
+     * Hide the spinner if it is showing (regardless of current waits).
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.hide = function(){
+	ll("hide");
+	jQuery('#' + spinner_elt_id).addClass('bbop-js-spinner-hidden');	
+    };
+
+    /*
+     * Function: start_wait
+     * 
+     * Displays the initial spinner if it is not already displayed and
+     * adds one to the wait count.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.start_wait = function(){
+
+	ll("Start outstanding waits: " + current_waits);
+
+	// 
+	if( current_waits == 0 ){
+	    anchor.show();
+	}
+
+	current_waits++;
+    };
+
+    /*
+     * Function: finish_wait
+     * 
+     * Removes one from the wait count and hides the spinner if the
+     * number of outstanding waits has reached zero.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.finish_wait = function(){
+
+	ll("Finish outstanding waits: " + current_waits);
+
+	// Stay at least at 0--we might have stragglers or incoming
+	// after a reset.
+	if( current_waits > 0 ){
+	    current_waits--;	    
+	}
+
+	// Gone if we are not waiting for anything.
+	if( current_waits == 0 ){
+	    anchor.hide();
+	}
+    };
+
+    /*
+     * Function: clear_waits
+     * 
+     * Hides the spinner and resets all the waiting counters. Can be
+     * used during things like server errors or collisions.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.clear_waits = function(){
+	current_waits = 0;
+	anchor.hide();
+    };
+};
+/*
+ * Package: filter_table.js
+ * 
+ * Namespace: bbop.widget.filter_table
+ * 
+ * Create a dynamic filter for removing rows from a table (where the
+ * rows are inside of a tbody).
+ * 
+ * The repaint_func argument takes the table id as its argument. If a
+ * function is not specified, the default function will do nothing.
+ */
+
+// YANKED: ...apply the classes "even_row" and "odd_row" to the table.
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'widget', 'display', 'text_button_sim');
+bbop.core.require('bbop', 'widget', 'spinner');
+bbop.core.namespace('bbop', 'widget', 'filter_table');
+
+/*
+ * Method: filter_table
+ * 
+ * The table needs to keep the row information in a tbody, not just at
+ * the top level.
+ * 
+ * Arguments:
+ *  elt_id - the element to inject the filter into
+ *  table_id - the table that we will operate on
+ *  img_src - *[optional]* img source URL for the spinner image (defaults to no spinner)
+ *  repaint_func - the repaint function to run after filtering (see above)
+ *  label - *[optional]* the label to use for the filter
+ * 
+ * Returns:
+ *  n/a
+ */
+bbop.widget.filter_table = function(elt_id, table_id, img_src,
+				    repaint_func, label){
+    this._is_a = 'bbop.widget.filter_table';
+
+    var anchor = this;
+    
+    var logger = new bbop.logger();
+    //logger.DEBUG = true;
+    logger.DEBUG = false;
+    function ll(str){ logger.kvetch(str); }
+
+    ll('init filter_table in ' + elt_id + ' for ' + table_id);
+
+    // Sort out spinner image source.
+    anchor.img_src = null;
+    if( img_src ){
+	anchor.img_src = img_src;
+    }
+
+    // Sort out repaint function.
+    anchor.repaint_func = 
+    	function (tid){};	
+    // function (tid){
+    //     jQuery('table#' + tid + ' tr:even').attr('class', 'even_row');
+    //     jQuery('table#' + tid + ' tr:odd').attr('class', 'odd_row');
+    // };
+    if( repaint_func ){
+    	anchor.repaint_func = repaint_func;
+    }
+
+    // Sort out label.
+    anchor.label = 'Filter:';
+    if( label ){
+	anchor.label = label;
+    }
+
+    ll('finished args');
+
+    // Create a label, input field, and a clear button.
+    var input_attrs = {
+	'type': 'text',
+	'class': 'textBox',
+	'value': "",
+	'generate_id': true
+    };
+    var input = new bbop.html.input(input_attrs);
+    var lbl_attrs = {
+	'for': input.get_id(),
+	'generate_id': true
+    };
+    var lbl = new bbop.html.tag('label', lbl_attrs);
+    lbl.add_to(anchor.label);
+    var clear_button =
+	new bbop.widget.display.text_button_sim('X', 'Clear filter');
+
+    ll('widget gen done');
+
+    // And add them to the DOM at the location.
+    jQuery('#' + elt_id).empty();
+    jQuery('#' + elt_id).append(lbl.to_string());
+    jQuery('#' + elt_id).append(input.to_string());
+    jQuery('#' + elt_id).append(clear_button.to_string());
+
+    // Also, attach a spinner.
+    var spinner = null;
+    if( anchor.img_src ){
+	jQuery('#' + elt_id).append('  ');
+	spinner = new bbop.widget.spinner(elt_id, anchor.img_src,
+					 {
+					     visible_p: false
+					 });
+    }
+    
+    ll('widget addition done');
+
+    // Make the clear button active.
+    jQuery('#' + clear_button.get_id()).click(
+	function(){
+	    ll('click call');
+	    if( spinner ){ spinner.show(); }
+            jQuery('#' + input.get_id()).val('');
+	    trs.show();
+	    // Recolor after filtering.
+	    anchor.repaint_func(table_id);
+	    if( spinner ){ spinner.hide(); }
+	});
+
+    // Cache information about the table.
+    var trs = jQuery('#' + table_id + ' tbody > tr');
+    var tds = trs.children();
+
+    // Make the table filter active.
+    jQuery('#' + input.get_id()).keyup(
+	function(){
+
+	    if( spinner ){ spinner.show(); }
+
+            var stext = jQuery(this).val();
+
+	    ll('keyup call: (' + stext + '), ' + trs);
+
+	    if( ! bbop.core.is_defined(stext) || stext == "" ){
+		// Restore when nothing found.
+		trs.show();
+	    }else{
+		// Want this to be insensitive.
+		stext = stext.toLowerCase();
+
+		// All rows (the whole table) gets hidden.
+		trs.hide();
+
+		// jQuery filter to match element contents against
+		// stext.
+		function _match_filter(){
+		    var retval = false;
+		    var lc = jQuery(this).text().toLowerCase();
+		    if( lc.indexOf(stext) >= 0 ){
+			retval = true;
+		    }
+		    return retval;
+		}
+
+		// If a td has a match, the parent (tr) gets shown.
+		// Or: show only matching rows.
+		tds.filter(_match_filter).parent("tr").show();
+            }
+
+	    // Recolor after filtering.
+	    anchor.repaint_func(table_id);
+
+	    if( spinner ){ spinner.hide(); }
+	});
+};
+/*
+ * Package: browse.js
+ * 
+ * Namespace: bbop.widget.browse
+ * 
+ * BBOP object to draw various UI elements that have to do with
+ * autocompletion.
+ * 
+ * This is a completely self-contained UI and manager.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'model');
+bbop.core.require('bbop', 'model', 'bracket', 'graph');
+bbop.core.require('bbop', 'html');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'browse');
+
+/*
+ * Constructor: browse
+ * 
+ * Contructor for the bbop.widget.browse object.
+ * 
+ * This is a specialized (and widgetized) subclass of
+ * <bbop.golr.manager.jquery>.
+ * 
+ * While everything in the argument hash is technically optional,
+ * there are probably some fields that you'll want to fill out to make
+ * things work decently. The options for the argument hash are:
+ * 
+ *  topology_graph_field -  the field for the topology graph
+ *  transitivity_graph_field - the field for the transitivity graph
+ *  info_button_callback - functio to call when info clicked, gets doc
+ *  base_icon_url - the url base that the fragments will be added to
+ *  image_type - 'gif', 'png', etc.
+ *  current_icon - the icon fragment for the current term
+ *  info_icon - the icon fragment for the information icon
+ *  info_alt - the alt text and title for the information icon
+ * 
+ * The basic formula for the icons is: base_icon_url + '/' + icon +
+ * '.' + image_type; then all spaces are turned to underscores and all
+ * uppercase letters are converted into lowercase letters.
+ * 
+ * The functions for the callbacks look like function(<term acc>,
+ * <json data for the specific document>){}. If no function is given,
+ * an empty function is used.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ *  interface_id - string id of the element to build on
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  this object
+ */
+bbop.widget.browse = function(golr_loc, golr_conf_obj, interface_id,
+			      in_argument_hash){
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('B (widget): ' + str); }
+
+    bbop.golr.manager.jquery.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.widget.browse';
+    // ll("what_is (post: this.update): " + bbop.core.what_is(this.update));
+
+    // 
+    var anchor = this;
+    var loop = bbop.core.each;
+    
+    // Our argument default hash.
+    var default_hash =
+	{
+	    'topology_graph_field' : 'topology_graph_json',
+	    'transitivity_graph_field' : 'transitivity_graph_json',
+	    //'transitivity_graph_field' : 'regulates_transitivity_graph_json',
+	    'info_button_callback' : function(){},
+	    'base_icon_url' : null,
+	    'image_type' : 'gif',
+	    'current_icon' : 'this',
+	    'info_icon' : 'info',
+	    'info_alt' : 'Click for more information.'
+	};
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // There should be a string interface_id argument.
+    this._interface_id = interface_id;
+    this._info_button_callback = arg_hash['info_button_callback'];
+    var topo_graph_field = arg_hash['topology_graph_field'];
+    var trans_graph_field = arg_hash['transitivity_graph_field'];
+    var base_icon_url = arg_hash['base_icon_url'];
+    var image_type = arg_hash['image_type'];
+    var current_icon = arg_hash['current_icon'];
+    var info_icon = arg_hash['info_icon'];
+    var info_alt = arg_hash['info_alt'];
+   
+    // The current acc that we are interested in.
+    this._current_acc = null;
+
+    // Successful callbacks call draw_rich_layout.
+    anchor.register('search', 'do', draw_rich_layout);
+
+    // Recursively draw a rich layout using nested uls.
+    function draw_rich_layout(resp){
+	
+	///
+	/// Get the rich layout from the returned document if
+	/// possible. Note the use of JSON, supplied by jQuery,
+	/// instead of out internal method bbop.json.parse.
+	///
+	var doc = resp.documents()[0];
+
+	var topo_graph = new bbop.model.bracket.graph();
+	topo_graph.load_json(JSON.parse(doc[topo_graph_field]));
+
+	var trans_graph = new bbop.model.graph();
+	trans_graph.load_json(JSON.parse(doc[trans_graph_field]));
+
+	//ll('to: ' + doc['topology_graph']);
+	//ll('tr: ' + doc['transitivity_graph']);
+	//ll('ro: ' + anchor._current_acc);
+	//ll('g: ' + topo_graph.get_parent_nodes(anchor._current_acc));
+	var rich_layout = topo_graph.rich_bracket_layout(anchor._current_acc,
+							 trans_graph);
+	//ll("rl: " + bbop.core.dump(rich_layout));
+
+	///
+	/// Next, produce the raw HTML skeleton.
+	/// TODO: Keep a cache of the interesting ids for adding
+	/// events later.
+	///
+
+	// I guess we'll just start by making the list.
+	var top_level = new bbop.html.list();
+
+	// Store the navigation anf info buttons.
+	var nav_button_hash = {};
+	var info_button_hash = {};
+
+	// Cycle down through the brackets, adding spaces every time
+	// we go down another level.
+	var spacing = '    ';
+	var spaces = spacing;
+	loop(rich_layout, // for every level
+	     function(layout_level){
+		 loop(layout_level, // for every item at this level
+		      function(level_item){			  
+
+			  var nid = level_item[0];
+			  var lbl = level_item[1];
+			  var rel = level_item[2];
+			  
+			  // For various sections, decide to run image
+			  // (img) or text code depending on whether
+			  // or not it looks like we have a real URL.
+			  var use_img_p = true;
+			  if( base_icon_url == null || base_icon_url == '' ){
+			      use_img_p = false;
+			  }
+
+			  // Clickable acc span.
+			  // No images, so the same either way. Ignore
+			  // it if we're current.
+			  var nav_b = null;
+			  if(anchor._current_acc == nid){
+			      var inact_attrs = {
+				  'class': 'bbop-js-text-button-sim-inactive',
+				  'title': 'Current term.'
+			      };
+			      nav_b = new bbop.html.span(nid, inact_attrs);
+			  }else{
+			      var tbs = bbop.widget.display.text_button_sim;
+			      var bttn_title =
+				  'Reorient neighborhood onto this node (' +
+				  nid + ').';
+			      nav_b = new tbs(nid, bttn_title);
+			      nav_button_hash[nav_b.get_id()] = nid;
+			  }
+
+			  // Clickable info span. A little difference
+			  // if we have images.
+			  var info_b = null;
+			  if( use_img_p ){
+			      // Do the icon version.
+			      var imgsrc = bbop.core.resourcify(base_icon_url,
+								info_icon,
+								image_type);
+			      info_b =
+				  new bbop.html.image({'alt': info_alt,
+						       'title': info_alt,
+				  		       'src': imgsrc,
+				  		       'generate_id': true});
+			  }else{
+			      // Do a text-only version.
+			      info_b =
+				  new bbop.html.span('<b>[i]</b>',
+						     {'generate_id': true});
+			  }
+			  info_button_hash[info_b.get_id()] = nid;
+
+			  // "Icon". If base_icon_url is defined as
+			  // something try for images, otherwise fall
+			  // back to this text ick.
+			  var icon = null;
+			  if( use_img_p ){
+			      // Do the icon version.
+			      var ialt = '[' + rel + ']';
+			      var isrc = null;
+			      if(anchor._current_acc == nid){
+				  isrc = bbop.core.resourcify(base_icon_url,
+			      				      current_icon,
+							      image_type);
+			      }else{
+				  isrc = bbop.core.resourcify(base_icon_url,
+			      				      rel, image_type);
+			      }
+			      icon =
+				  new bbop.html.image({'alt': ialt,
+						       'title': rel,
+				  		       'src': isrc,
+				  		       'generate_id': true});
+			  }else{
+			      // Do a text-only version.
+			      if(anchor._current_acc == nid){
+				  icon = '[[->]]';
+			      }else if( rel && rel.length && rel.length > 0 ){
+				  icon = '[' + rel + ']';
+			      }else{
+				  icon = '[???]';
+			      }
+			  }
+
+			  // Stack the info, with the additional
+			  // spaces, into the div.
+			  top_level.add_to(spaces,
+					   icon,
+					   nav_b.to_string(),
+					   lbl,
+					   info_b.to_string());
+		      }); 
+		 spaces = spaces + spacing;
+	     }); 
+
+	// Add the skeleton to the doc.
+	jQuery('#' + anchor._interface_id).empty();
+	jQuery('#' + anchor._interface_id).append(top_level.to_string());
+
+	///
+	/// Finally, attach any events to the browser HTML doc.
+	///
+
+	// Navigation.
+	loop(nav_button_hash,
+	     function(button_id, node_id){
+
+		 jQuery('#' + button_id).click(
+		     function(){
+			 var tid = jQuery(this).attr('id');
+			 var call_time_node_id = nav_button_hash[tid];
+			 //alert(call_time_node_id);
+			 anchor.draw_browser(call_time_node_id);
+		     });
+	     });
+
+	// Information.
+	loop(info_button_hash,
+	     function(button_id, node_id){
+
+		 jQuery('#' + button_id).click(
+		     function(){
+			 var tid = jQuery(this).attr('id');
+			 var call_time_node_id = info_button_hash[tid];
+			 var call_time_doc = resp.get_doc(call_time_node_id);
+			 anchor._info_button_callback(call_time_node_id,
+						      call_time_doc);
+		     });
+	     });
+    }
+	
+    /*
+     * Function: draw_browser
+     * 
+     * Bootstraps the process.
+     * 
+     * Parameters:
+     *  term_acc - acc of term we want to have as the term of interest
+     * 
+     * Returns
+     *  n/a
+     */
+    //bbop.widget.browse.prototype.draw_browser = function(term_acc){
+    // this._current_acc = term_acc;
+    // this.set_id(term_acc);
+    // this.update('search');
+    this.draw_browser = function(term_acc){
+	anchor._current_acc = term_acc;
+	anchor.set_id(term_acc);
+	anchor.update('search');
+    };
+    
+};
+bbop.core.extend(bbop.widget.browse, bbop.golr.manager.jquery);
+/*
+ * Package: search_box.js
+ * 
+ * Namespace: bbop.widget.search_box
+ * 
+ * BBOP object to draw various UI elements that have to do with
+ * autocompletion.
+ * 
+ * This is a completely self-contained UI and manager.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'template');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'search_box');
+
+/*
+ * Constructor: search_box
+ * 
+ * Contructor for the bbop.widget.search_box object.
+ * 
+ * This is a specialized (and widgetized) subclass of
+ * <bbop.golr.manager.jquery>.
+ * 
+ * The function for the callback argument should either accept a
+ * JSONized solr document representing the selected item or null
+ * (nothing found).
+ * 
+ * While everything in the argument hash is technically optional,
+ * there are probably some fields that you'll want to fill out to make
+ * things work decently. The options for the argument hash are:
+ * 
+ *  label_template - string template for dropdown, can use any document field
+ *  value_template - string template for selected, can use any document field
+ *  minimum_length - wait for this many characters to start (default 3)
+ *  list_select_callback - function takes a json solr doc on dropdown selection
+ * 
+ * To get a better idea on how to use the templates, see the demo page
+ * at http://cdn.berkeleybop.org/jsapi/bbop-js/demo/index.html and
+ * read the documentation for <bbop.template>.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ *  interface_id - string id of the element to build on
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  this object
+ */
+bbop.widget.search_box = function(golr_loc,
+				  golr_conf_obj,
+				  interface_id,
+				  in_argument_hash){
+    bbop.golr.manager.jquery.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.widget.search_box';
+
+    // Aliases.
+    var anchor = this;
+    var loop = bbop.core.each;
+    
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (auto): ' + str); }
+
+    // Our argument default hash.
+    var default_hash =
+	{
+	    'label_template': '{{id}}',
+	    'value_template': '{{id}}',
+	    'minimum_length': 3, // wait for three characters or more
+	    'list_select_callback': function(){}
+	};
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // There should be a string interface_id argument.
+    this._interface_id = interface_id;
+    this._list_select_callback = arg_hash['list_select_callback'];
+    var label_tt = new bbop.template(arg_hash['label_template']);
+    var value_tt = new bbop.template(arg_hash['value_template']);
+    var minlen = arg_hash['minimum_length'];
+
+    // The all-important argument hash. See:
+    // http://jqueryui.com/demos/autocomplete/#method-widget
+    var auto_args = {
+	minLength: minlen,
+	// Function for a successful data hit.
+	// The data getter, which is making it all more complicated
+	// than it needs to be...we need to close around those
+	// callback hooks so we have to do it inplace here.
+	source: function(request_data, response_hook) {
+	    anchor.jq_vars['success'] = function(json_data){
+		var retlist = [];
+		var resp = new bbop.golr.response(json_data);
+		if( resp.success() ){
+		    loop(resp.documents(),
+			 function(doc){
+
+			     // First, try and pull what we can out of our
+			     var lbl = label_tt.fill(doc);
+
+			     // Now the same thing for the return/value.
+			     var val = value_tt.fill(doc);
+
+			     // Add the discovered items to the return
+			     // save.
+			     var item = {
+				 'label': lbl,
+				 'value': val,
+				 'document': doc
+			     };
+			     retlist.push(item);
+			 });
+		}
+		response_hook(retlist);
+	    };
+
+	    // Get the selected term into the manager and fire.
+	    //anchor.set_query(request_data.term);
+	    anchor.set_comfy_query(request_data.term);
+	    anchor.JQ.ajax(anchor.get_query_url(), anchor.jq_vars);
+	},
+	// What to do when an element is selected.
+	select: function(event, ui){
+	    var doc_to_apply = null;
+	    if( ui.item ){
+		doc_to_apply = ui.item.document;
+	    }
+
+	    // Only do the callback if it is defined.
+	    if( bbop.core.is_defined(anchor._list_select_callback) ){
+		anchor._list_select_callback(doc_to_apply);
+	    }
+	}
+    };
+
+    // Set the ball rolling (attach jQuery autocomplete to doc).
+    jQuery('#' + anchor._interface_id).autocomplete(auto_args);
+
+
+    /*
+     * Function: destroy
+     * 
+     * Remove the autocomplete and functionality from the DOM.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.destroy = function(){
+	jQuery('#' + anchor._interface_id).autocomplete('destroy');
+    };
+
+    /*
+     * Function: content
+     * 
+     * Get the current text contents of the search box.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  string
+     */
+    this.content = function(){
+	return jQuery('#' + anchor._interface_id).val();
+    };
+
+};
+bbop.core.extend(bbop.widget.search_box, bbop.golr.manager.jquery);
+/*
+ * Package: dialog.js
+ * 
+ * Namespace: bbop.widget.dialog
+ * 
+ * BBOP object to produce a self-constructing/self-destructing
+ * jQuery popup dialog.
+ * 
+ * This is a completely self-contained UI.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'dialog');
+
+/*
+ * Constructor: dialog
+ * 
+ * Contructor for the bbop.widget.dialog object.
+ * 
+ * The optional hash arguments look like:
+ * 
+ * Arguments:
+ *  item - string or bbop.html to display.
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.dialog = function(item, in_argument_hash){
+    
+    this._is_a = 'bbop.widget.dialog';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (dialog): ' + str); }
+
+    // Our argument default hash.
+    var default_hash = {
+	//modal: true,
+	//draggable: false,
+	width: 300, // the jQuery default anyways
+	title: '', 
+	close:
+	function(){
+	    // TODO: Could maybe use .dialog('destroy') instead?
+	    jQuery('#' + div_id).remove();
+	}	    
+    };
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // Not an argument for the dialog, so remove it.
+    var title = arg_hash['title'];
+    delete arg_hash['title'];
+
+    ///
+    /// Actually draw.
+    ///
+
+    // Coerce our argument into a string.
+    var str = item || 'Nothing here...';
+    if( bbop.core.what_is(item) != 'string' ){
+	str = item.to_string();
+    }
+
+    // Create new div.
+    var div = new bbop.html.tag('div', {'generate_id': true, title: title});
+    var div_id = div.get_id();
+
+    // Append div to end of body.
+    jQuery('body').append(div.to_string());
+    
+    // Add text to div.
+    jQuery('#' + div_id).append(str);
+    
+    // Boink!
+    var dia = jQuery('#' + div_id).dialog(arg_hash);
+};
+/*
+ * Package: term_shield.js
+ * 
+ * Namespace: bbop.widget.term_shield
+ * 
+ * BBOP object to produce a self-constructing/self-destructing term
+ * information shield.
+ * 
+ * This is a completely self-contained UI and manager.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+//bbop.core.require('bbop', 'model');
+//bbop.core.require('bbop', 'model', 'graph', 'bracket');
+bbop.core.require('bbop', 'html');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'term_shield');
+
+/*
+ * Constructor: term_shield
+ * 
+ * Contructor for the bbop.widget.term_shield object.
+ * 
+ * This is (sometimes) a specialized (and widgetized) subclass of
+ * <bbop.golr.manager.jquery>.
+ * 
+ * To actually do much useful, you should set the personality of the
+ * widget.
+ * 
+ * The optional hash arguments look like:
+ * 
+ *  linker - a "linker" object
+ *  width - defaults to 700
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server; not needed if local
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.term_shield = function(golr_loc, golr_conf_obj, in_argument_hash){
+    
+    bbop.golr.manager.jquery.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.widget.term_shield';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (term_shield): ' + str); }
+
+    // Our argument default hash.
+    var default_hash = {
+	'linker_function': function(){},
+	'width': 700
+    };
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+    var width = arg_hash['width'];
+    var linker = arg_hash['linker_function'];
+
+    // Draw a locally help Solr response doc.
+    function _draw_local_doc(doc){
+	
+	//ll(doc['id']);
+
+	var personality = anchor.get_personality();
+	var cclass = golr_conf_obj.get_class(personality);
+
+	var txt = 'Nothing here...';
+	if( doc && cclass ){
+
+	    var tbl = new bbop.html.table();
+	    var results_order = cclass.field_order_by_weight('result');
+	    var each = bbop.core.each; // convenience
+	    each(results_order,
+		 function(fid){
+		     // 
+		     var field = cclass.get_field(fid);
+		     var val = doc[fid];
+
+		     // Determine if we have a list that we're working
+		     // with or not.
+		     if( field.is_multi() ){
+
+			 if( val ){
+			     val = val.join(', ');
+			 }else{
+			     val = 'n/a';
+			 }
+
+		     }else{
+
+			 // When handling just the single value, see
+			 // if we can link out the value.
+			 var link = null;
+			 if( val ){
+			     //link = linker.anchor({id: val});
+			     //link = linker.anchor({id: val}, 'term');
+			     link = linker.anchor({id: val}, fid);
+			     if( link ){ val = link; }
+			 }else{
+			     val = 'n/a';
+			 }
+		     }
+
+		     tbl.add_to([field.display_name(), val]);
+		 });
+	    txt = tbl.to_string();
+	}
+
+	// Create div.
+	var div = new bbop.html.tag('div', {'generate_id': true});
+	var div_id = div.get_id();
+
+	// Append div to body.
+	jQuery('body').append(div.to_string());
+
+	// Add text to div.
+	jQuery('#' + div_id).append(txt);
+
+	// Modal dialogify div; include self-destruct.
+	var diargs = {
+	    modal: true,
+	    draggable: false,
+	    width: width,
+	    close:
+	    function(){
+		// TODO: Could maybe use .dialog('destroy') instead?
+		jQuery('#' + div_id).remove();
+	    }	    
+	};
+	var dia = jQuery('#' + div_id).dialog(diargs);
+    }
+
+    // Get a doc by id from a remote server then display it when it
+    // gets local.
+    // TODO: spinner?
+    function _draw_remote_id(id_string){
+	function _process_resp(resp){
+	    var doc = resp.get_doc(0);
+	    _draw_local_doc(doc);
+	}
+	anchor.register('search', 'do', _process_resp);
+	anchor.set_id(id_string);
+	//ll('FOO: ' + id_string);
+	anchor.search();
+    }
+
+    /*
+     * Function: draw
+     * 
+     * Render a temporary modal information shield. 
+     * 
+     * Arguments:
+     *  item - either a document id or a Solr-returned document
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.draw = function(item){
+    // Call the render directly if we already have a document,
+    // otherwise, if it seems like a string (potential id), do a
+    // callback on it and pull the doc out.
+	if( bbop.core.what_is(item) == 'string' ){
+	    _draw_remote_id(item);
+	}else{
+	    _draw_local_doc(item);
+	}
+    };
+    
+};
+bbop.core.extend(bbop.widget.term_shield, bbop.golr.manager.jquery);
+/*
+ * Package: list_select_shield.js
+ * 
+ * Namespace: bbop.widget.list_select_shield
+ * 
+ * BBOP object to produce a self-constructing/self-destructing term
+ * information shield.
+ * 
+ * A simple invocation could be:
+ * 
+ * : new bbop.widget.list_select_shield({title: 'foo', blurb: 'explanation', list_of_lists: [[['a', 'b'], ['c', 'd', true]], [[1, 2], [3, 4]]], title_list: ['title 1', 'title 2'], action: function(selected_args){ alert(selected_args.join(', '));}})
+ * 
+ * This is a completely self-contained UI and manager.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+//bbop.core.require('bbop', 'model');
+//bbop.core.require('bbop', 'model', 'graph', 'bracket');
+bbop.core.require('bbop', 'html');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'list_select_shield');
+
+/*
+ * Constructor: list_select_shield
+ * 
+ * Contructor for the bbop.widget.list_select_shield object.
+ * 
+ * The purpose of this object to to create a popup that 1) display
+ * multiple lists for the user to select from and 2) triggers an
+ * action (by function argument) to act on the list selections.
+ * 
+ * The "list_of_lists" argument is a list of lists structured like:
+ * : [[[label, value, nil|true|false], ...], ...]
+ * 
+ * Items that are true will appear as pre-checked when the lists come
+ * up.
+ * 
+ * The "action" argument is a function that takes a list of selected
+ * values.
+ * 
+ * The argument hash looks like:
+ *  title - *[optional]* the title to be displayed 
+ *  blurb - *[optional]* a text chunk to explain the point of the action
+ *  title_list - a list of titles/explanations for the lists
+ *  list_of_lists - a list of lists (see above)
+ *  action - *[optional] * the action function to be triggered (see above, defaults to no-op)
+ *  width - *[optional]* width as px integer (defaults to 800)
+ * 
+ * Arguments:
+ *  in_argument_hash - hash of arguments (see above)
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.list_select_shield = function(in_argument_hash){    
+    this._is_a = 'bbop.widget.list_select_shield';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (list_select_shield): ' + str); }
+
+    // Aliases.
+    var each = bbop.core.each;
+    var uuid = bbop.core.uuid;
+    
+    // Our argument default hash.
+    var default_hash = {
+	'title': '',
+	'blurb': '',
+	'title_list': [],
+	'list_of_lists': [],
+	'action': function(){},
+	'width': 800
+    };
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+    var title = arg_hash['title'];
+    var blurb = arg_hash['blurb'];
+    var title_list = arg_hash['title_list'];
+    var list_of_lists = arg_hash['list_of_lists'];
+    var action = arg_hash['action'];
+    var width = arg_hash['width'];
+
+    // Cache the group names as we go so we can pull them out later
+    // when we scan for checked items.
+    var group_cache = [];
+    function _draw_radio_list(list){
+
+	var list_cache = [];
+	var rdo_grp = 'bbop_js_lss_' + uuid();
+	group_cache.push(rdo_grp);
+
+	each(list,
+	     function(item){
+
+		 var lbl = item[0];
+		 var val = item[1];
+		 var ckt = item[2] || false;
+
+		 //ll('lbl: ' + lbl);
+		 //ll('val: ' + val);
+		 //ll('ckt: ' + ckt);
+
+		 // Radio button.	 
+		 var rdo_attrs = {
+		     'generate_id': true,
+		     'name': rdo_grp,
+		     'type': 'radio',
+		     'value': val
+		 };
+		 if( ckt ){
+		     rdo_attrs['checked'] = 'checked';
+		 }
+		 var rdo = new bbop.html.input(rdo_attrs);
+		 //ll('rdo: ' + rdo.to_string());
+
+		 // Label for it.
+		 var rdo_lbl_attrs = {
+		     'for': rdo.get_id()
+		 };
+		 var rdo_lbl = new bbop.html.tag('label', rdo_lbl_attrs,
+						 ' ' + lbl);
+		 //ll('rdo_lbl: ' + rdo_lbl.to_string());
+
+		 // And a span to capture both.
+		 var rdo_span_attrs = {
+		 };
+		 var rdo_span = new bbop.html.span('', rdo_span_attrs);
+		 //ll('rdo_span (1): ' + rdo_span.to_string());
+		 rdo_span.add_to(rdo);
+		 //ll('rdo_span (2): ' + rdo_span.to_string());
+		 rdo_span.add_to(rdo_lbl);
+		 //ll('rdo_span (3): ' + rdo_span.to_string());
+
+		 // Now /this/ goes into the list.
+		 list_cache.push(rdo_span);
+	     });
+
+	// Now we have a list of all the items, put them into a UL
+	// element.
+	var ul_list_attrs = {
+	    'generate_id': true
+	};
+	var ul_list = new bbop.html.list(list_cache, ul_list_attrs);
+
+	// ...and send it back.
+	return ul_list;
+    }
+
+    // Append super container div to body.
+    var div = new bbop.html.tag('div', {'generate_id': true});
+    var div_id = div.get_id();
+    jQuery('body').append(div.to_string());
+
+    // Add title and blurb to div.
+    jQuery('#' + div_id).append('<p>' + blurb + '</p>');
+
+    // Add the table of lists to div.
+    var tbl = new bbop.html.table(title_list);
+    var lol_cache = [];
+    each(list_of_lists,
+	 function(sub_list){
+	     lol_cache.push(_draw_radio_list(sub_list));
+	 });
+    tbl.add_to(lol_cache);
+    jQuery('#' + div_id).append(tbl.to_string());
+
+    // Finally, add a clickable button to that calls the action
+    // function. (Itself embedded in a container div to help move it
+    // around.)
+    var cont_div_attrs = {
+	'class': 'bbop-js-ui-dialog-button-right',
+	'generate_id': true
+    };
+    var cont_div = new bbop.html.tag('div', cont_div_attrs);
+    var cont_btn_attrs = {
+	//'class': 'bbop-js-ui-dialog-button-right'
+    };
+    var cont_btn = new bbop.widget.display.text_button_sim('Continue',
+							   'Click to continue',
+							   null,
+							   cont_btn_attrs);
+    cont_div.add_to(cont_btn);
+    jQuery('#' + div_id).append(cont_div.to_string());
+
+    // Since we've technically added the button, back it clickable
+    // Note that this is very much radio button specific.
+    jQuery('#' + cont_btn.get_id()).click(
+	function(){
+	    // Jimmy values out from above by cycling over the
+	    // collected groups.
+	    var selected = [];
+	    each(group_cache,
+		 function(gname){
+		     var find_str = 'input[name=' + gname + ']';
+		     var val = null;
+		     jQuery(find_str).each(
+			 function(){
+			     if( this.checked ){
+				 val = jQuery(this).val();
+			     }
+			     // }else{
+			     // 	 selected.push(null);
+			     //}
+			 });
+		     selected.push(val);
+		 });
+
+	    // Calls those values with our action function.
+	    action(selected);
+
+	    // And destroy ourself.
+	    jQuery('#' + div_id).remove();
+	});
+
+    // Modal dialogify div; include self-destruct.
+    var diargs = {
+	'title': title,
+	'modal': true,
+	'draggable': false,
+	'width': width,
+	'close':
+	function(){
+	    // TODO: Could maybe use .dialog('destroy') instead?
+	    jQuery('#' + div_id).remove();
+	}	    
+    };
+    var dia = jQuery('#' + div_id).dialog(diargs);
+};
+/*
+ * Package: search_pane.js
+ * 
+ * Namespace: bbop.widget.search_pane
+ * 
+ * BBOP object to produce a self-constructing/self-destructing term
+ * general filtering search tool for an index. This is a completely
+ * self-contained UI and manager.
+ * 
+ * The function ".establish_display()" must be run *after* an initial
+ * personality is set. Also, in many use cases, you'll want to have a
+ * line like the following before running ".establish_display()":
+ * sp_widget.add_query_filter('document_category', 'annotation',
+ * ['*']);
+ * 
+ * Also, establish_display() literally just establishes the physical
+ * presence of the display. To actually populate it with data once you
+ * start, a seeding call to the .reset() or .search() is necessary.
+ * 
+ * The search pane will display one less filter row than is set with
+ * .set_facet_limit(), it will use this runover to decide whether or
+ * not to display the "more" option for the filters.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'search_pane');
+
+/*
+ * Constructor: search_pane
+ * 
+ * Contructor for the bbop.widget.search_pane object.
+ * 
+ * This is a specialized (and widgetized) subclass of
+ * <bbop.golr.manager.jquery>.
+ * 
+ * Sticky filters (see manager documentation) are "hidden" from the
+ * user in all displays.
+ * 
+ * The optional hash arguments look like:
+ * 
+ *  linker - the linker to be used; null function otherwise
+ *  handler - special field handler to be used; null function otherwise
+ *  show_filterbox_p - show currents filters and accordion (default true)
+ *  show_pager_p - show the results pager (default true)
+ *  show_checkboxes_p - show/enable the item select checkboxes (default true)
+ *  spinner_search_source - source for the spinner used during typical searching
+ *  spinner_shield_source - source for the spinner used shield waiting
+ *  icon_clear_label - (default: text button based on 'X')
+ *  icon_clear_source - (default: '')
+ *  icon_reset_label - (default: text button based on 'X')
+ *  icon_reset_source - (default: '')
+ *  icon_positive_label - (default: text button based on '+')
+ *  icon_positive_source - (default: '')
+ *  icon_negative_label - (default: text button based on '-')
+ *  icon_negative_source - (default: '')
+ *  icon_remove_label - (default: text button based on 'X')
+ *  icon_remove_source - (default: '')
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server; not needed if local
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ *  interface_id - string id of the element to build on
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.search_pane = function(golr_loc, golr_conf_obj, interface_id,
+				   in_argument_hash){
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('SP (widget): ' + str); }    
+
+    bbop.golr.manager.jquery.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.widget.search_pane';
+    // ll("what_is (post: this.update): " + bbop.core.what_is(this.update));
+
+    // ...
+    var anchor = this;
+
+    // We need to keep a handle on the live_search ui component so we
+    // can manipulate the buttons after the fact.
+    this.ui = null;
+    this.user_buttons = [];
+
+    // It's also good to know if the display has actually been
+    // established yet (e.g. the user-defined buttons being added
+    // before can have the redraw not happen, since there is nothing
+    // there yet and they will be draw naturally when the display
+    // finally is.
+    this.established_p = false;
+
+    // A special set for a single run after the first reset.
+    this.initial_reset_p = true;
+    this.initial_reset_callback =
+	function(response, manager){ ll('empty first run'); };
+
+    // Our argument default hash.
+    function _button_wrapper(str, title){
+	var b = new bbop.widget.display.text_button_sim(str, title, '');
+	return b.to_string();
+    }
+    var default_hash =
+    	{
+    	    //'layout_type' : 'two-column',
+	    'linker': new bbop.linker(),
+	    'handler': new bbop.handler(),
+    	    'show_searchbox_p' : true,
+    	    'show_filterbox_p' : true,
+    	    'show_pager_p' : true,
+    	    'show_checkboxes_p' : true,
+    	    'spinner_search_source' : '',
+    	    'spinner_shield_source' : '',
+	    'icon_clear_label': _button_wrapper('X', 'Clear text from query'),
+	    'icon_clear_source': '',
+	    'icon_reset_label': _button_wrapper('!','Reset user query filters'),
+	    'icon_reset_source': '',
+	    'icon_positive_label': _button_wrapper('+', 'Add positive filter'),
+	    'icon_positive_source': '',
+	    'icon_negative_label': _button_wrapper('-', 'Add negative filter'),
+	    'icon_negative_source': '',
+	    'icon_remove_label':_button_wrapper('X','Remove filter from query'),
+	    'icon_remove_source': ''
+    	};
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // Pull args into variables.
+    //var base_icon_url = arg_hash['base_icon_url'];
+    //var image_type = arg_hash['image_type'];
+    //var layout_type = arg_hash['layout_type'];
+    var linker = arg_hash['linker'];
+    var handler = arg_hash['handler'];
+    var show_searchbox_p = arg_hash['show_searchbox_p'];
+    var show_filterbox_p = arg_hash['show_filterbox_p'];
+    var show_pager_p = arg_hash['show_pager_p'];
+    var show_checkboxes_p = arg_hash['show_checkboxes_p'];
+    var spinner_search_source = arg_hash['spinner_search_source'];
+    var spinner_shield_source = arg_hash['spinner_shield_source'];
+    var icon_clear_label = arg_hash['icon_clear_label'];
+    var icon_clear_source = arg_hash['icon_clear_source'];
+    var icon_reset_label = arg_hash['icon_reset_label'];
+    var icon_reset_source = arg_hash['icon_reset_source'];
+    var icon_positive_label = arg_hash['icon_positive_label'];
+    var icon_positive_source = arg_hash['icon_positive_source'];
+    var icon_negative_label = arg_hash['icon_negative_label'];
+    var icon_negative_source = arg_hash['icon_negative_source'];
+    var icon_remove_label = arg_hash['icon_remove_label'];
+    var icon_remove_source = arg_hash['icon_remove_source'];
+
+    /*
+     * Function: establish_display
+     * 
+     * Completely redraw the display.
+     * 
+     * Required to display after setting up the manager.
+     * 
+     * Also may be useful after a major change to the manager to reset
+     * it.
+     * 
+     * Parameters:
+     *  n/a
+     * 
+     * Returns
+     *  n/a
+     */
+    this.establish_display = function(){
+	
+    	// Blow away whatever was there completely.
+    	jQuery('#' + interface_id).empty();
+
+    	// Can only make a display if there is a set
+    	// personality--there is no general default and it is an
+    	// error.
+    	var personality = anchor.get_personality();
+    	var cclass = golr_conf_obj.get_class(personality);
+    	if( ! personality || ! cclass ){
+    	    ll('ERROR: no useable personality set');
+    	    throw new Error('ERROR: no useable personality set');
+    	}
+
+    	///
+    	/// Setup UI and bind it to events.
+    	///
+	
+	anchor.ui = new bbop.widget.display.live_search(interface_id, cclass);
+	// And add the correct handlers.
+	anchor.ui.set_linker(linker);
+	anchor.ui.set_handler(handler);
+
+	// Try to add any buttons that we have loafing around into the
+	// initial setup.
+	anchor.ui.set_buttons(anchor.user_buttons);
+
+	// IF want to show the checkboxes, get them in now.
+	if( show_checkboxes_p ){
+	    anchor.ui.show_checkboxes_p(true);
+	}
+
+	///
+    	/// Things to do on every reset event. Essentially re-draw
+    	/// everything.
+	///
+
+    	if( show_searchbox_p ){ // conditionally display search box stuff
+    	    anchor.register('reset', 'reset_query', anchor.ui.reset_query, -1);
+	}
+    	if( show_filterbox_p ){ // conditionally display filter stuff
+    	    anchor.register('reset', 'sticky_first',
+    			    anchor.ui.draw_sticky_filters, -1);
+    	    anchor.register('reset', 'curr_first',
+    			    anchor.ui.draw_current_filters, -1);
+    	    anchor.register('reset', 'accordion_first',
+    			    anchor.ui.draw_accordion, -1);
+    	}
+    	// We're always showing meta and results.
+    	anchor.register('reset', 'meta_first', anchor.ui.draw_meta, -1);
+    	anchor.register('reset', 'results_first', anchor.ui.draw_results, -1);
+	
+	// Finally, we're going to add a first run behavior here.
+	// We'll wrap the user-defined function into a 
+	function _initial_runner(response, manager){
+	    // I can't just remove the callback from the register
+	    // after the first run because it would be reconstituted
+	    // every time it was reset (established).
+	    if( anchor.initial_reset_p ){
+		anchor.initial_reset_p = false;
+		anchor.initial_reset_callback(response, manager);
+		//ll('unregister: ' + anchor.unregister('reset', 'first_run'));
+	    }
+	}
+    	anchor.register('reset', 'initial_reset', _initial_runner, -100);
+
+	///
+    	/// Things to do on every search event.
+	///
+
+    	if( show_searchbox_p ){ // conditionally display search box stuff
+	    // TODO: I worry a little about this being rebound after
+	    // every keyboard event, but rationally, considering the
+	    // rebinds and redraws that are happening down in the
+	    // accordion, that seems a little silly.
+    	    anchor.register('search', 'draw_query', anchor.ui.draw_query, -1);
+	}
+    	if( show_filterbox_p ){ // conditionally display filter stuff
+    	    anchor.register('search','sticky_filters_std',
+    			    anchor.ui.draw_sticky_filters);
+    	    anchor.register('search','curr_filters_std',
+    			    anchor.ui.draw_current_filters);
+    	    anchor.register('search', 'accordion_std',
+			    anchor.ui.draw_accordion);
+    	}
+    	// These will always be updated after a search.
+    	anchor.register('search', 'meta_usual', anchor.ui.draw_meta);
+    	anchor.register('search', 'results_usual', anchor.ui.draw_results);
+	
+    	// Things to do on an error.
+    	anchor.register('error', 'results_unusual', anchor.ui.draw_error);	
+	
+    	// Setup the gross frames for the filters and results.
+    	if( show_searchbox_p ){ // conditionally display search box stuff
+    	    anchor.ui.setup_query('Free-text filtering',
+				  icon_clear_label,
+				  icon_clear_source);
+	}
+    	if( show_filterbox_p ){ // conditionally display filter stuff
+    	    anchor.ui.setup_sticky_filters();
+    	    anchor.ui.setup_current_filters(icon_remove_label,
+					    icon_remove_source);
+    	    anchor.ui.setup_accordion(icon_positive_label,
+				      icon_positive_source,
+				      icon_negative_label,
+				      icon_negative_source,
+				      spinner_shield_source);
+	}
+    	anchor.ui.setup_results({'meta': show_pager_p,
+				 'spinner_source': spinner_search_source});
+	
+    	// // Start the ball with a reset event.
+    	//anchor.reset();
+
+	// The display has been established.
+	anchor.established_p = true;
+    };
+
+    /*
+     * Function: get_selected_items
+     * 
+     * The idea is to return a list of the items selected (with
+     * checkboxes) in the display. This means that there are three
+     * possibilities. 1) We are not using checkboxes or the display
+     * has not been established, so we return null; 2) no or all items
+     * have been selected, so we get back an empty list (all == none
+     * in our view); 3) a subset list of strings (ids).
+     * 
+     * NOTE: Naturally, does not function until the display is established.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns
+     *  string list or null
+     */
+    this.get_selected_items = function(){
+	var retval = null;
+
+	// 
+	var gname = anchor.ui.selected_name();
+	if( gname ){
+	    retval = [];
+
+	    // Cycle through and pull out the values of the checked
+	    // ones.
+	    var total_count = 0;
+	    var nstr = 'input[name=' + gname + ']';
+	    jQuery(nstr).each(
+		function(){
+		    if( this.checked ){
+			var val = jQuery(this).val();
+			retval.push(val);
+		    }
+		    total_count++;
+		});
+
+	    // If we are selecting all of the items on this page, that
+	    // is the same as not selecting any in our world, so reset
+	    // and warn.
+	    if( total_count > 0 && total_count == retval.length ){
+		alert('You can "select" all of the items on a results page by not selecting any (all being the default). This will also get your results processed faster and cause significantly less overhead on the servers.');
+		retval = [];
+	    }	    
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: add_button
+     * 
+     * Add a user-defined button to the display.
+     * 
+     * NOTE: Does not function until the display is established.
+     * 
+     * Parameters:
+     *  button_definition_hash - ""
+     *
+     * Returns
+     *  n/a
+     */
+    this.add_button = function(button_definition_hash){
+	// Add to our locally stored buttons.
+	anchor.user_buttons.push(button_definition_hash);
+
+	if( anchor.established_p && anchor.ui ){
+	    anchor.ui.set_buttons(anchor.user_buttons);
+	    anchor.ui.draw_user_buttons(anchor);	    
+	}
+    };
+
+     /*
+     * Function: clear_buttons
+     * 
+     * Remove all user-defined buttons from the display.
+     * 
+     * NOTE: Does not function until the display is established.
+     * 
+     * Parameters:
+     *  n/a
+     *
+     * Returns
+     *  n/a
+     */
+    this.clear_buttons = function(){
+	// Clear our locally stored buttons.
+	anchor.user_buttons = [];
+
+	if( anchor.established_p && anchor.ui ){
+	    anchor.ui.set_buttons(anchor.user_buttons);
+	    anchor.ui.draw_user_buttons(anchor);	    
+	}
+    };
+
+    /*
+     * Function: set_query_field_text
+     * 
+     * Push text into the search box. Does not affect the state of the
+     * manager in any way.
+     * 
+     * NOTE: Does not function until the display is established.
+     * 
+     * Parameters:
+     *  query - the text to put into the search box
+     *
+     * Returns
+     *  true or false on whether the task was accomplished
+     */
+    this.set_query_field_text = function(query){
+	var retval = false;	
+	if( anchor.established_p && anchor.ui ){
+	    retval = anchor.ui.set_query_field(query);
+	}
+	return retval;
+    };
+
+    /*
+     * Function: set_initial_reset_callback
+     * 
+     * Add a callback to be run after the initial reset is finished.
+     * 
+     * Parameters:
+     *  response - the usual
+     *  manager - the usual
+     *
+     * Returns
+     *  n/a
+     */
+    this.set_initial_reset_callback = function(callback){
+	anchor.initial_reset_callback = callback;
+    };
+
+    // // Now let's run the above function as the initializer.
+    // anchor.establish_display();
+};
+bbop.core.extend(bbop.widget.search_pane, bbop.golr.manager.jquery);
+/*
+ * Package: repl.js
+ * 
+ * Namespace: bbop.widget.repl
+ * 
+ * A self-contained flexible REPL to use as a base to explore the BBOP
+ * environment that you setup.
+ * 
+ * This is a completely self-contained UI and manager.
+ * 
+ * WARNING: This widget cannot display any kind of HTML tags in the
+ * log.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+//bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'repl');
+
+/*
+ * Constructor: repl
+ * 
+ * Contructor for the bbop.widget.repl object.
+ * 
+ * The in_argument_hash has the following options.
+ * 
+ *  buffer_id - the id of the evaluation buffer textarea (default: null/random)
+ *  cli_id - the id of the CLI textarea (default: null/random)
+ *  display_initial_commands_p - (default true)
+ * 
+ * If you do not specify ids for the inputs, random ones will be
+ * generated.
+ * 
+ * Arguments:
+ *  interface_id - string id of the element to build on
+ *  initial_commands - a list of initial commands to feed the interpreter
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  this object
+ */
+bbop.widget.repl = function(interface_id, initial_commands, in_argument_hash){
+    this._is_a = 'bbop.widget.repl';
+
+    // Aliases.
+    var anchor = this;
+    var loop = bbop.core.each;
+    
+    // Our argument default hash.
+    var default_hash =
+	{
+	    'buffer_id': null,
+	    'cli_id': null,
+	    'display_initial_commands_p': true
+	};
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+    var in_buffer_id = arg_hash['buffer_id'];
+    var in_cli_id = arg_hash['cli_id'];
+    var display_initial_commands_p = arg_hash['display_initial_commands_p'];
+
+    // Get no commands if nothing else.
+    var init_buffer = initial_commands || [];
+    
+    // The main div we'll work with.
+    var repl_id = interface_id;
+    jQuery('#' + repl_id).empty();
+
+    // Save our CLI history as we go.
+    var history_pointer = 0;
+    var history_list = [''];
+
+    ///
+    /// Setup the HTML and layout on the page.
+    ///
+
+    // Env into work buffer.
+    var command_buffer_args = {'rows': '12', cols:'80'};
+    if( in_buffer_id ){
+	command_buffer_args['id'] = in_buffer_id;
+    }else{
+	command_buffer_args['generate_id'] = true;
+    }
+    var command_buffer = new bbop.html.tag('textarea', command_buffer_args,
+					   init_buffer.join("\n"));	
+    jQuery('#' + repl_id).append(command_buffer.to_string());
+    
+    jQuery('#' + repl_id).append('<br />');
+
+    // Command buffer eval button.
+    var command_buffer_button = new bbop.html.button('Evaluate buffer',
+	    				   {'generate_id': true});
+    jQuery('#' + repl_id).append(command_buffer_button.to_string());
+
+    // Clear buffer button.
+    var clear_buffer_button = new bbop.html.button('Clear buffer',
+	    					   {'generate_id': true});
+    jQuery('#' + repl_id).append(clear_buffer_button.to_string());
+
+    // Clear log button.
+    var clear_log_button = new bbop.html.button('Clear log',
+	    					{'generate_id': true});
+    jQuery('#' + repl_id).append(clear_log_button.to_string());
+
+    jQuery('#' + repl_id).append('<br />');
+
+    // Log (+ clear botton).
+    // //var logging_console_id = 'bbop-logger-console-text';
+    // var logging_console_id = 'bbop-logger-console-textarea';
+    // var logging_console = new bbop.html.tag('textarea',
+    // 					    {'rows': '7', cols:'80',
+    // 					     'readonly': 'readonly',
+    // 					     'id': logging_console_id});
+    var logging_console_id = 'bbop-logger-console-html';
+    var logging_console = new bbop.html.tag('div',
+    					    {'id': logging_console_id,
+					     'class': 'nowrap',
+    					     'style': 'height: 7em; width: 40em; border: 1px solid #888888; overflow: auto;'});
+    jQuery('#' + repl_id).append(logging_console.to_string());
+
+    //jQuery('#' + repl_id).append('<br />');
+
+    // A usage message.
+    var cli_msg = new bbop.html.tag('span', {},
+				    "[eval: return; ctrl+up/down: history]:");
+    jQuery('#' + repl_id).append(cli_msg.to_string());
+    jQuery('#' + repl_id).append('<br />');
+
+    // Command line.
+    var command_line_args = {'rows': '1', cols:'80'};
+    if( in_cli_id ){
+	command_line_args['id'] = in_cli_id;
+    }else{
+	command_line_args['generate_id'] = true;
+    }
+    var command_line = new bbop.html.tag('textarea', command_line_args);
+    jQuery('#' + repl_id).append(command_line.to_string());
+
+    ///
+    /// Core helper functions.
+    ///
+
+    // Per-UI logger. Notice that we waited until after the log div
+    // was added to run this to make sure we bind to the right spot.
+    var rlogger = new bbop.logger();
+    rlogger.DEBUG = true;
+    //function log(str){ rlogger.kvetch('repl (pre): ' + str); }
+    function log(str){ rlogger.kvetch(str); }
+
+    // Advance the log to the bottom.
+    function _advance_log_to_bottom(){
+    	// var cons = jQuery('#' + logging_console_id);
+    	// var foo = cons.scrollTop(cons[0].scrollHeight);	
+    };
+
+    // Eval!
+    function _evaluate(to_eval){
+
+	var retval = null;
+	var retval_str = '';
+	var okay_p = true;
+
+	try{
+	    // If we get through this, things have gone well.
+	    // Global eval actually kind of tricky:
+	    //  http://perfectionkills.com/global-eval-what-are-the-options/
+	    //var ret = eval(to_eval);
+	    //var ret = jQuery.globalEval(to_eval);
+	    retval = window.eval(to_eval);
+	    if( bbop.core.is_defined(retval) ){
+		//log('// in def if');
+		if( bbop.core.what_is(retval) == 'string' ){
+		    // // log('// in str if');
+		    // retval_str = retval;
+		    // // var gt_re = new RegExp("/\>/", "gi");
+		    // // var lt_re = new RegExp("/\</", "gi");
+		    // retval_str.replace(">", ">");
+		    // retval_str.replace("<", "<");
+		    // //retval_str = '<pre>' + retval_str + '</pre>';
+		    // // log('// end: (' + retval_str + ')');
+		    // retval_str = '<code>' + retval_str + '</code>';
+		    // retval_str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
+		    retval_str = '"' + retval + '"';
+		}else{
+		    retval_str = retval; // worth a try at least
+		}
+	    }else{
+		// Maybe undefined, but probably just no return value.
+		//retval_str = '[undefined]';
+		retval_str = '';
+	    }
+	}catch (x){
+	    // Bad things happened.
+	    retval = null;
+	    retval_str = '[n/a]';
+	    okay_p = false;
+	}
+
+	return [retval, retval_str, okay_p];
+    }
+
+    // Update the CLI to the current point in the history.
+    function _update_cli(){
+
+	var item = history_list[history_pointer];
+	jQuery('#' + command_line.get_id()).val(item);
+	//log('// [history]: ' + item);
+	//log('// history: '+history_pointer+', '+history_list.length);
+	//_advance_log_to_bottom();
+    }
+
+    ///
+    /// Build callbacks.
+    ///
+    
+    // A lot of cases for button presses when reading from the command
+    // line.
+    function read_cli(event){
+
+	var which = event.which;
+	var ctrl_p = event.ctrlKey;
+	//log('cli: ' + which + ', ' + ctrl_p);
+
+	if ( which == 13 ) { // return
+	    
+	    // Stop events.
+	    event.preventDefault();
+	    
+	    // Get and ensure nice JS, wipe CLI clean.
+	    var to_eval = jQuery('#' + command_line.get_id()).val();
+	    if( to_eval != '' ){
+		jQuery('#' + command_line.get_id()).val('');
+		
+		// Enter the new command into our history and bump the
+		// index to the last thing pushed on.
+		history_list.pop(); // pop the empty ''
+		history_list.push(to_eval);
+		history_list.push(''); // push new empty ''
+		history_pointer = history_list.length -1;
+		//log('// history: '+history_pointer+', '+history_list.length);
+		
+		// Log, eval, log.
+		to_eval = bbop.core.ensure(to_eval, ';', 'back');
+		log(to_eval);
+		var evals = _evaluate(to_eval);
+		log('// ' + evals[1]);
+		_advance_log_to_bottom();
+
+		return false;
+	    }
+	}else if( ctrl_p && which == 38 ){ // ctrl + up
+
+	    // Stop stuff?
+	    event.preventDefault();
+
+	    if( history_pointer == 0 ){
+		_update_cli();
+	    }else if( history_pointer > 0 ){
+		history_pointer--;
+		_update_cli();
+	    }
+
+	    return false;
+
+	}else if( ctrl_p && which == 40 ){ // ctrl + down
+
+	    // Stop stuff?
+	    event.preventDefault();
+
+	    if( history_pointer < history_list.length -1 ){
+		history_pointer++;
+		_update_cli();
+	    }
+
+	    return false;
+	}
+
+	return true;
+    }
+    jQuery('#' + command_line.get_id()).keydown(read_cli);
+
+    // Bind buffer eval.
+    function read_buffer(){
+	var to_eval = jQuery('#' + command_buffer.get_id()).val();
+	if( to_eval != '' ){
+	    log('// Evaluating buffer...');
+	    var evals = _evaluate(to_eval);
+	    log('// ' + evals[1]);
+	    _advance_log_to_bottom();
+	}
+    }
+    var cbbid = '#' + command_buffer_button.get_id();
+    var command_buffer_button_props = {
+	icons: { primary: "ui-icon-play"},
+	disabled: false,
+	text: true
+    };    
+    jQuery(cbbid).button(command_buffer_button_props).click(read_buffer);
+
+    // Bind buffer clear.
+    function clear_buffer(){
+	//jQuery('#' + logging_console_id).val('');
+	//alert('to clear: ' + command_buffer.get_id());
+	// FF, at least, does something weird here and empty() does
+	// not always work--doubling seems to be file.
+	jQuery('#' + command_buffer.get_id()).val('');
+	//jQuery('#' + command_buffer.get_id()).empty();
+    }
+    var cbuid = '#' + clear_buffer_button.get_id();
+    var clear_buffer_button_props = {
+	icons: { primary: "ui-icon-trash"},
+	disabled: false,
+	text: true
+    };
+    jQuery(cbuid).button(clear_buffer_button_props).click(clear_buffer);
+
+    // Bind log clear.
+    function clear_log(){
+	//jQuery('#' + logging_console_id).val('');
+	jQuery('#' + logging_console_id).empty();
+    }
+    var clbid = '#' + clear_log_button.get_id();
+    var clear_log_button_props = {
+	icons: { primary: "ui-icon-trash"},
+	disabled: false,
+	text: true
+    };
+    jQuery(clbid).button(clear_log_button_props).click(clear_log);
+
+    ///
+    /// Bootstrap session.
+    ///
+
+    // Evaluate what we initially put in the command buffer.
+    jQuery(cbbid).click(); // run the stuff in the buffer
+    if( display_initial_commands_p == false ){ // maybe make it disappear
+	clear_buffer();
+	clear_log();
+    }
+    log('// [Session start.]');
+
+    ///
+    /// External use methods.
+    ///
+
+    /*
+     * Function: get_id
+     * 
+     * Get the id of different components in the REPL.
+     * 
+     * Currently supported arguments are:
+     *  - 'buffer'
+     * 
+     * Arguments:
+     *  str - the item you want to check
+     * 
+     * Returns:
+     *  string or null
+     */
+    this.get_id = function(str){
+
+	var retval = null;
+
+	if( str ){
+	    if( str == 'buffer' ){
+		retval = command_buffer.get_id();
+	    }
+	}
+
+	return retval;
+    };
+
+    /*
+     * Function: replace_buffer_text
+     * 
+     * Replace the buffer text with new text.
+     * 
+     * Arguments:
+     *  str - the new text for the command buffer
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.replace_buffer_text = function(str){
+	clear_buffer();
+	//jQuery('#' + command_buffer.get_id()).append(str);
+	jQuery('#' + command_buffer.get_id()).val(str);
+    };
+
+    /*
+     * Function: advance_log_to_bottom
+     * 
+     * Can't be bothered to check now, but this needs to be done
+     * separately from the log because of an initial race condition.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.advance_log_to_bottom = function(){
+	_advance_log_to_bottom();
+    };
+
+    /*
+     * Function: destroy
+     * 
+     * Remove the autocomplete and functionality from the DOM.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.destroy = function(){
+	jQuery('#' + anchor._interface_id).val('');
+    };
+
+};
+////////////
+////
+//// bbop.widget.phylo_old
+////
+//// Purpose: Extend the model to be handy for a (phylo)tree.
+//// 
+//// WARNING: This functionality is deprecated.
+////
+//// Width is determined by used div width (style).
+//// 
+//// Taken name spaces:
+////    bbop.widget.phylo_old.*
+////
+//// TODO: better selection of displayable text
+//// TODO: get parser so we can start really checking/use.
+//// TODO: make things non-interactive during visible == false?
+//// TODO: font and text placement
+//// TODO: better text alignment
+//// TODO: floating right-hand text (see PAINT)
+//// TODO: some "speed-up" refactoring?
+////
+//// OKAY: FF, Safari, Chrome, Opera
+//// TODO: IE a little wonky, but not too bad--easy fix?
+////
+//// Required:
+////    Rafael
+////    bbop.core
+////    bbop.model
+////    bbop.model.tree
+////
+//////////
+
+
+// Module and namespace checking.
+//bbop.core.require('Raphael');
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'model');
+bbop.core.require('bbop', 'model', 'tree');
+bbop.core.namespace('bbop', 'widget', 'phylo_old');
+//bbop.core.namespace('bbop', 'widget', 'phylo_old', 'renderer');
+
+///
+/// PNodes (phylonode) object.
+///
+
+// Render out.
+// Actually, use this to wrap graph abstraction.
+bbop.widget.phylo_old.renderer = function (element_id, info_box_p){
+
+    // Logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch(str); }
+
+    var elt_id = element_id;
+
+    var renderer_context = this;
+
+    // Properties that can be manipulated externally.
+    //this.animation_time = 100;
+    this.animation_time = 200;
+    //this.animation_time = 1000; // for debug
+    //this.use_animation = true;
+    this.use_animation = false;
+
+    // These first two defaults will be overwritten on display.
+    this.box_width = 60;
+    this.box_height = 30;
+
+    // Internal-only variables.
+    this._render_frame_width = 800;
+    this._render_interal_width = this._render_frame_width;
+    this._render_frame_height = 600;
+    this._render_internal_height = this._render_height;
+    //this._node_labels = {};
+    //this._node_hover_labels = {};
+    //this._edge_labels = {};
+    //this._floating_labels = {};
+
+    ///
+    /// Functions to handle internal graph management.
+    ///
+    
+    var node_cache_hash = {};
+    this._graph = new bbop.model.tree.graph();
+
+    //
+    this.add_node = function(node){
+	var nid = node.id();
+	node_cache_hash[nid] = node;
+	this._graph.add_node(node);
+    };	
+
+    //
+    //this.add_edge = function(nid1, nid2, dist){
+    this.add_edge = function(edge){
+
+	var retval = false;
+
+	var n1 = node_cache_hash[edge.subject_id()];
+	var n2 = node_cache_hash[edge.object_id()];
+	if( n1 && n2 ){
+	    this._graph.add_edge(edge);
+	    retval = true;	    
+	}
+
+	return retval;
+    };
+
+    ///
+    /// ...
+    ///
+
+    // Init: context, label, x-coord, y-coord.
+    graph_pnode = function(context, label, px, py, internal_p){
+
+	var pnode_box_width = renderer_context.box_width;
+	var pnode_box_height = renderer_context.box_height;
+
+	// Color and size definitions.
+	var text_offset_x = pnode_box_width / 2.0;
+	var text_offset_y = pnode_box_height / 2.0;
+	this.base_node_color = "#00f";
+
+	// Variations if an internal node.
+	if( internal_p ){
+	    pnode_box_width = pnode_box_width / 2.0;
+	    pnode_box_width = 2.0;
+	    //pnode_box_height = 2.0;
+	    text_offset_x = (pnode_box_width / 2.0);
+	}
+
+	// Future visibility.    
+	this.visible = true;
+	
+	// For advanced tree use.
+	this.open = true;
+	
+	// Coloration and style attributes.
+	this.shape_base_attr = {
+	    "fill": this.base_node_color,
+	    "fill-opacity": 0.05,
+	    "opacity": 1.0,
+	    "stroke": this.base_node_color,
+	    "stroke-width": 2,
+	    "title": "This is " + label,
+	    "cursor": "move"
+	};
+	this.shape_highlight_attr = {
+	    "fill": this.base_node_color,
+	    "fill-opacity": 0.5,
+	    "opacity": 1.0,
+	    "stroke": this.base_node_color,
+	    "stroke-width": 3
+	};
+	this.shape_dim_attr = {
+	    "fill": this.base_node_color,
+	    "fill-opacity": 0.0,
+	    "opacity": 0.5,
+	    "stroke": this.base_node_color,
+	    "stroke-width": 1
+	};
+	this.shape_invisible_attr = {
+	    "fill": "#000",
+	    "fill-opacity": 0.0,
+	    "opacity": 0.0,
+	    "stroke": "#000",
+	    "stroke-width": 0
+	};
+	// Text in node.
+	this.text_base_attr = {
+	    //"fill-opacity": 1.0,
+	    "opacity" : 1.0,
+	    "font-size" : 10
+	};
+	//this.text_highlight_attr = {"fill-opacity": 1.0, "font-size" : 12};
+	this.text_highlight_attr = {
+	    //"fill-opacity": 1.0,
+	    "opacity" : 1.0,
+	    "font-size" : 10
+	};
+	this.text_dim_attr = {
+	    //"fill-opacity": 0.2,
+	    "opacity" : 0.2,
+	    "font-size" : 10
+	};
+	this.text_invisible_attr = {
+	    //"fill-opacity": 0.0,
+	    "opacity" : 0.0,
+	    "font-size" : 10
+	};
+
+	// Draw out initial node.
+	this._context = context;
+
+	this._text = // NOTE: text is *centered* at this point.
+	this._context.text(px + text_offset_x, py + text_offset_y, label);
+	this._text.toBack(); // make sure it's behind the boxes
+	this._shape = this._context.rect(px, py,
+					 pnode_box_width, pnode_box_height,
+					 2);
+
+	// Proxy properties and functions.
+	// This is so wrong, but feels so good...proxy most things through
+	// shape.
+	this.id = this._shape.id; // Use the shape's UID as our UID.
+	this.getBBox = function(){
+	    return this._shape.getBBox.call(this._shape);
+	};
+	// Semi-proxy.
+	this.shape_attr = function(arg){
+	    return this._shape.attr.call(this._shape, arg);
+	};
+
+	// Add to the object the initial position.
+	this._start_shape_y = this._shape.attr("y");
+	this._start_text_y = this._text.attr("y");
+
+	// Setup shape attributes.
+	this._shape.attr(this.shape_base_attr);
+    };
+    // Call first when you want to move.
+    graph_pnode.prototype.update_position = function(){
+	this._start_shape_y = this._shape.attr("y");
+	this._start_text_y = this._text.attr("y");
+    };
+    // Move.
+    graph_pnode.prototype.move_y = function(arg){
+	var d_shape = this._start_shape_y + arg;
+	var d_text = this._start_text_y + arg;
+	this._shape.attr.call(this._shape, {"y": d_shape});
+	this._text.attr.call(this._text, {"y": d_text});
+    };
+    // Event handler proxies for underlying shapes (text ignored).
+    graph_pnode.prototype.drag = function(mv_func,start_func,end_func){
+	this._shape.drag(mv_func, start_func, end_func);
+    };
+    graph_pnode.prototype.dblclick = function(handler){
+	this._shape.dblclick.call(this._shape, handler);
+    };
+    graph_pnode.prototype.mouseover = function(handler){
+	this._shape.mouseover.call(this._shape, handler);
+    };
+    graph_pnode.prototype.mouseout = function(handler){
+	this._shape.mouseout.call(this._shape, handler);
+    };
+
+    graph_pnode.prototype.update = function(message){
+
+	//
+	var shape_attr_to_apply = this.shape_base_attr;
+	var text_attr_to_apply = this.text_base_attr;
+
+	// 
+	if( this.visible == false ){
+	    shape_attr_to_apply = this.shape_invisible_attr;
+	    text_attr_to_apply = this.text_invisible_attr;
+	}else if( message == 'highlight' ){
+	    shape_attr_to_apply = this.shape_highlight_attr;
+	    text_attr_to_apply = this.text_highlight_attr;
+	}else if( message == 'dim' ){
+	    shape_attr_to_apply = this.shape_dim_attr;
+	    text_attr_to_apply = this.text_dim_attr;
+	}
+
+	// Change border on whether or not it's "opened".
+	if( this.open == false ){
+	    shape_attr_to_apply['stroke'] = "#070";
+	}else{
+    	    shape_attr_to_apply['stroke'] = this.base_node_color;	
+	}
+
+	// Render with whatever filtered through.
+	if( renderer_context.use_animation ){
+	    this._shape.animate.call(this._shape,
+				     shape_attr_to_apply,
+				     renderer_context.animation_time);
+	    this._shape.animate.call(this._text,
+				     text_attr_to_apply,
+				     renderer_context.animation_time);	
+	}else{
+	    this._shape.attr(shape_attr_to_apply);
+	    this._text.attr(text_attr_to_apply);
+	}
+    };
+
+
+    ///
+    /// Define the edges (connections) to be used for drawing.
+    /// Connection (between two pnodes) object.
+    ///
+
+    // Init: context, shape, shape, and "distance" representation
+    // (optional).
+    graph_connection = function(context, obj1, obj2, dist_rep){
+
+	//this.context = context;
+
+	// These need to be set right away for path calculation.    
+	this.from = obj1;
+	this.to = obj2;
+
+	this.id = this.from.id + '_id_invariant_' + this.to.id;
+
+	// Get path.
+	var path_info = this.get_path_between_info();
+	var path = path_info['path'];
+	var cp = path_info['center_point'];
+
+	// ll("conn: cp: (" + cp[0] + ", " + cp[1] + ")");
+
+	// Future visibility.
+	this.visible = true;
+
+	var base_edge_color = "#030";
+	var base_edge_width = "3";
+	var highlight_edge_color = "#00f";
+	var highlight_edge_width = "5";
+	var invisible_edge_color = "#000";
+	var invisible_edge_width = "0";
+
+	this.edge_base_attr = {
+	    "stroke": base_edge_color,
+     	    "stroke-width": base_edge_width,
+	    "fill": "none",
+	    "opacity": 1.0,
+	    "fill-opacity": 0.0
+	};
+	this.edge_highlight_attr = {
+	    "stroke": highlight_edge_color,
+     	    "stroke-width": highlight_edge_width,
+	    "fill": "none",
+	    "opacity": 1.0,
+	    "fill-opacity": 0.0
+	};
+	this.edge_dim_attr = {
+	    "stroke": base_edge_color,
+     	    "stroke-width": 1,
+	    "fill": "none",
+	    "opacity": 0.5,
+	    "fill-opacity": 0.0
+	};
+	this.edge_invisible_attr = {
+	    "stroke": invisible_edge_color,
+     	    "stroke-width": invisible_edge_width,
+	    "fill": "none",
+	    "opacity": 0.0,
+	    "fill-opacity": 0.0
+	};
+	// // As connections.
+	// this.text_base_attr = {"fill-opacity": 1.0, "font-size" : 10};
+	// this.text_highlight_attr = {"fill-opacity": 1.0, "font-size" : 10};
+	// this.text_dim_attr = {"fill-opacity": 0.2, "font-size" : 10};
+	// this.text_invisible_attr = {"fill-opacity": 0.0, "font-size" : 10};
+	// Highlight-only.
+	this.text_base_attr = {
+	    "opacity": 0.0,
+	    "font-size" : 10
+	};
+	//this.text_highlight_attr = {"fill-opacity": 1.0, "font-size" : 12};
+	this.text_highlight_attr = {
+	    "opacity": 1.0,
+	    "font-size" : 10
+	};
+	this.text_dim_attr = {
+	    "opacity": 0.0,
+	    "font-size" : 10
+	};
+	this.text_invisible_attr = {
+	    "opacity": 0.0,
+	    "font-size" : 10
+	};
+
+	// Build up text at path centerpoint.
+	this.text = null;
+	if( dist_rep ){
+	    this.text = context.text(cp[0], (cp[1] + 10), dist_rep);
+	    this.text.toBack(); // make sure it's behind the boxes
+	    this.text.attr(this.text_base_attr);	
+	}
+
+	// Colors and lines.
+	this.line = context.path(path);
+	this.line.attr(this.edge_base_attr);
+    };
+    // Update line graphic.
+    graph_connection.prototype.update = function(message){
+
+	// Get path.
+	var path_info = this.get_path_between_info();
+	var path = path_info['path'];
+
+	// Update line position.
+	this.line.attr({path: path});
+
+	// Update line graphics on message.
+	var line_attr_to_apply = null;
+	if( this.visible == false ){
+	    line_attr_to_apply = this.edge_invisible_attr;
+	}else if( message == 'highlight' ){
+	    line_attr_to_apply = this.edge_highlight_attr;
+	}else if( message == 'dim' ){
+	    line_attr_to_apply = this.edge_dim_attr;
+	}else{
+	    line_attr_to_apply = this.edge_base_attr;
+	}
+
+	// Render with whatever filtered through.
+	if( renderer_context.use_animation ){	
+	    this.line.animate.call(this.line,
+				   line_attr_to_apply,
+				   renderer_context.animation_time);
+	}else{
+	    this.line.attr(line_attr_to_apply);
+	}
+
+	// Update text position.
+	var text_attr_to_apply = null;
+	if( this.text ){
+	    var cp = path_info['center_point'];
+	    this.text.attr({"x": cp[0], "y": (cp[1] + 10)});
+
+	    // Update graphics graphics on message.
+	    if( this.visible == false ){
+		text_attr_to_apply = this.text_invisible_attr;
+	    }else if( message == 'highlight' ){
+		text_attr_to_apply = this.text_highlight_attr;
+	    }else if( message == 'dim' ){
+		text_attr_to_apply = this.text_dim_attr;
+	    }else{
+		text_attr_to_apply = this.text_base_attr;
+	    }
+
+	    // Render with whatever filtered through.
+	    if( renderer_context.use_animation ){	
+		this.text.animate.call(this.text,
+				       text_attr_to_apply,
+				       renderer_context.animation_time);
+	    }else{
+		this.text.attr(text_attr_to_apply);
+	    }
+	}
+    };
+    // // Generate path from between the two internally stored objects.
+    // graph_connection.prototype.get_path_between_info = function(){
+
+    // 	var bb1 = this.from.getBBox();
+    // 	var bb2 = this.to.getBBox();
+
+    // 	//ll("bb1.width: " + bb1.width);
+    // 	//ll("bb1.x: " + bb1.x + ", bb1.y: " + bb1.y);
+    // 	//ll("bb1.width: "+ bb1.width +", bb1.height: "+ bb1.height);
+
+    // 	var p = [{x: bb1.x + bb1.width / 2, y: bb1.y - 1},
+    // 		 {x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1},
+    // 		 {x: bb1.x - 1, y: bb1.y + bb1.height / 2},
+    // 		 {x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2},
+    // 		 {x: bb2.x + bb2.width / 2, y: bb2.y - 1},
+    // 		 {x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1},
+    // 		 {x: bb2.x - 1, y: bb2.y + bb2.height / 2},
+    // 		 {x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2}];
+    // 	var d = {};
+    // 	var dis = [];
+    // 	for (var i = 0; i < 4; i++) {
+    //         for (var j = 4; j < 8; j++) {
+    // 		var dx = Math.abs(p[i].x - p[j].x);
+    // 		var dy = Math.abs(p[i].y - p[j].y);
+    // 		if ((i == j - 4) ||
+    // 		    (((i != 3 && j != 6) || p[i].x < p[j].x) &&
+    // 		     ((i != 2 && j != 7) || p[i].x > p[j].x) &&
+    // 		     ((i != 0 && j != 5) || p[i].y > p[j].y) &&
+    // 		     ((i != 1 && j != 4) || p[i].y < p[j].y))) {
+    //                 dis.push(dx + dy);
+    //                 d[dis[dis.length - 1]] = [i, j];
+    // 		}
+    //         }
+    // 	}
+    // 	var res = null;
+    // 	if (dis.length == 0) {
+    //         res = [0, 4];
+    // 	}else{
+    //         res = d[Math.min.apply(Math, dis)];
+    // 	}
+    // 	var x1 = p[res[0]].x;
+    // 	var y1 = p[res[0]].y;
+    // 	var x2 = p[res[1]].x;
+    // 	var y2 = p[res[1]].y;
+    // 	var dx = Math.max(Math.abs(x1 - x2) / 2, 10);
+    // 	var dy = Math.max(Math.abs(y1 - y2) / 2, 10);
+    // 	return {"path": [
+    // 		    "M", x1.toFixed(3), y1.toFixed(3),
+    // 		    "L", x1.toFixed(3), y2.toFixed(3),
+    // 		    "L", x2.toFixed(3), y2.toFixed(3)
+    // 		].join(","),
+    // 		// "center_point": [(x1.toFixed(3) + x1.toFixed(3)),
+    // 		// 		     (y1.toFixed(3) + y2.toFixed(3))]
+    // 		"center_point": [(x1 + x2) / 2.0, (y2)]
+    // 	       };
+    // };
+
+    // Generate path from between the two internally stored objects.
+    graph_connection.prototype.get_path_between_info = function(){
+
+	var bb1 = this.from.getBBox();
+	var bb2 = this.to.getBBox();
+
+	//ll("bb1.width: " + bb1.width);
+	//ll("bb1.x: " + bb1.x + ", bb1.y: " + bb1.y);
+	//ll("bb1.width: "+ bb1.width +", bb1.height: "+ bb1.height);
+
+	var p =
+	    [
+		// bb1: middle-top
+		{x: bb1.x + bb1.width / 2, y: bb1.y - 1},
+		// bb1: middle-bottom
+		{x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1},
+		// bb1: left-middle
+		{x: bb1.x - 1, y: bb1.y + bb1.height / 2},
+		// bb1: right-middle
+		{x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2},
+		// bb2: middle-top
+		//{x: bb2.x + bb2.width / 2, y: bb2.y - 1},
+		{x: bb2.x - 1, y: bb2.y + bb2.height / 2},
+		// bb2: middle-bottom
+		//{x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1},
+		{x: bb2.x - 1, y: bb2.y + bb2.height / 2},
+		// bb2: left-middle
+		{x: bb2.x - 1, y: bb2.y + bb2.height / 2},
+		// bb2: right-middle
+		//{x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2}
+		{x: bb2.x - 1, y: bb2.y + bb2.height / 2}
+	    ];
+	var d = {};
+	var dis = [];
+	for (var i = 0; i < 4; i++) { // for bb1
+            for (var j = 4; j < 8; j++) { // for bb2
+		var dx = Math.abs(p[i].x - p[j].x);
+		var dy = Math.abs(p[i].y - p[j].y);
+		if ((i == j - 4) ||
+    		    (((i != 3 && j != 6) || p[i].x < p[j].x) &&
+    		     ((i != 2 && j != 7) || p[i].x > p[j].x) &&
+    		     ((i != 0 && j != 5) || p[i].y > p[j].y) &&
+    		     ((i != 1 && j != 4) || p[i].y < p[j].y))) {
+                    dis.push(dx + dy);
+                    d[dis[dis.length - 1]] = [i, j];
+		}
+            }
+	}
+	var res = null;
+	if (dis.length == 0) {
+            res = [0, 4];
+	}else{
+            res = d[Math.min.apply(Math, dis)];
+	}
+	var x1 = p[res[0]].x;
+	var y1 = p[res[0]].y;
+	var x2 = p[res[1]].x;
+	var y2 = p[res[1]].y;
+	//var dx = Math.max(Math.abs(x1 - x2) / 2, 10);
+	//var dy = Math.max(Math.abs(y1 - y2) / 2, 10);
+	return {"path": [
+    		    "M", x1.toFixed(3), y1.toFixed(3),
+    		    "L", x1.toFixed(3), y2.toFixed(3),
+    		    "L", x2.toFixed(3), y2.toFixed(3)
+		].join(","),
+		// "center_point": [(x1.toFixed(3) + x1.toFixed(3)),
+		// 		     (y1.toFixed(3) + y2.toFixed(3))]
+		"center_point": [(x1 + x2) / 2.0, (y2)]
+	       };
+    };
+
+    ///
+    /// Functions and sub-functions for display.
+    ///
+
+    // TODO: later, allow display to take args to force size.
+    this.display = function () {
+
+	var layout = this._graph.layout();
+	var elt = document.getElementById(elt_id);
+
+	// Fudge variables.
+	var edge_shift = 1.0; // fudge to allow the last little bit on screen
+	var absolute_pull = 15.0; // there seems to be a misjudgement
+				  // in width by about this much
+
+	// Adjust vertical scales and display.
+	var y_scale = renderer_context.box_height * 2.0; // fixed y-scale
+	this._render_frame_height = (layout.max_width * y_scale);
+	this._render_internal_height = this._render_frame_height - edge_shift;
+
+	// Adjust for render width based on platform.
+	// TODO: later, allow display to take args to force size.
+	var x_scale = 1.0;
+	//if( window && window.innerWidth && 1 == 2){
+	    //this._render_frame_width = window.innerWidth;
+	//}else 
+	if( elt.clientWidth ){
+	    this._render_frame_width = elt.clientWidth;
+	}else{
+	    ll("UFP: Unidentified Failing Platform.");
+	}
+	// Now adjust the drawing width to make sure that the boxes
+	// fit.
+	//this._render_internal_width = this._render_frame_width;
+	this._render_internal_width =
+	    this._render_frame_width
+	    - (1.0 * renderer_context.box_width)
+	    - absolute_pull;
+	// If we're using the info box, adjust inwards by some amount.
+	if( info_box_p ){
+	    this._render_internal_width = this._render_internal_width * 0.8;
+	}
+	// Recalculate x-scale.
+	x_scale = this._render_internal_width / layout.max_distance;
+	// Get that last pixel column on board.
+	ll('internal width: ' + this._render_internal_width);
+	ll('frame width: ' + this._render_frame_width);
+
+	// Create context.
+	var paper = Raphael(elt_id,
+			    this._render_frame_width,
+			    this._render_frame_height);
+	ll('display: made paper');
+
+	///
+	/// Graph helper function definitions.
+	/// 
+
+	function get_pnode_from_phynode_id(phynode_id){
+	    var ret = null;
+	    if( phynode_id_to_index[phynode_id] ){
+		ret = phynodes[phynode_id_to_index[phynode_id]];
+	    }
+	    return ret;
+	}
+
+	// Subtree list, including self.
+	function gather_list_from_hash(nid, hash){
+    	    var retlist = new Array();
+    	    retlist.push(nid);
+    	    // Get all nodes cribbing from distances.
+    	    for( vt in hash[nid] ){
+    		//ll("id: " + id + ", v: " + ct);
+    		retlist.push(vt);
+    	    }
+    	    return retlist;	
+	}
+
+	// Subtree list, including self.
+	function get_descendant_node_list(nid){
+	    return gather_list_from_hash(nid, layout.parent_distances);
+	}
+
+	// Ancestor list, including self.
+	function get_ancestor_node_list(nid){
+	    return gather_list_from_hash(nid, layout.child_distances);
+	}
+
+	//
+	function get_associated(phynode_id, index_kept, getter){
+
+    	    var retlist = new Array();
+	    
+    	    var node_id = phynode_id_to_node_id[phynode_id];
+    	    var subtree_node_list = getter(node_id);
+    	    for( var si = 0; si < subtree_node_list.length; si++ ){
+
+    		var subnode_id = subtree_node_list[si];
+    		var sindex = node_id_to_index[subnode_id];
+
+    		var thing = index_kept[sindex];
+    		retlist.push(thing);
+    	    }
+
+    	    return retlist;
+	}
+
+	function get_descendant_phynodes(phynode_id){
+    	    return get_associated(phynode_id, phynodes, get_descendant_node_list);
+	}
+
+	function get_descendant_texts(phynode_id){
+    	    return get_associated(phynode_id, texts, get_descendant_node_list);
+	}
+
+	function get_ancestor_phynodes(phynode_id){
+    	    return get_associated(phynode_id, phynodes, get_ancestor_node_list);
+	}
+
+	// General func.
+	function get_connections(phynode_id, phynode_getter, conn_hash){
+
+	    var retlist = new Array();
+
+	    // Fish in the connection ancestor hash for edges.
+	    var tmp_phynodes = phynode_getter(phynode_id);
+	    for( var si = 0; si < tmp_phynodes.length; si++ ){
+		var tshp = tmp_phynodes[si];
+		var tnid = phynode_id_to_node_id[tshp.id];
+		if( tnid && conn_hash[tnid] ){
+		    for( var anid in conn_hash[tnid] ){
+			var conn_index = conn_hash[tnid][anid];
+			var conn = connections[conn_index];
+			ll('get_conn: found: [' + conn_index +
+					 '] ' + anid + ' <=> ' + tnid +
+					 ' ... ' + conn);
+			retlist.push(conn);
+		    }
+		}
+	    }
+	    return retlist;
+	};
+
+	//
+	function get_ancestor_connections(phynode_id){
+	    return get_connections(phynode_id,
+				   get_ancestor_phynodes,
+				   conn_hash_ancestor);
+	}
+
+	//
+	function get_descendant_connections(phynode_id){
+	    return get_connections(phynode_id,
+				   get_descendant_phynodes,
+				   conn_hash_descendant);
+	}
+
+	///
+	/// Phynode manipulation function definitions.
+	/// 
+
+	// Dragging animation (color dimming).
+	var start = function () {
+
+    	    var phynode_id = this.id;
+
+	    // Darken boxes and update current position before dragging.
+    	    var assoc_phynodes = get_descendant_phynodes(phynode_id);
+    	    for( var si = 0; si < assoc_phynodes.length; si++ ){
+		var phynode = assoc_phynodes[si];
+		phynode.update_position();
+		phynode.update("dim");
+    	    }
+
+	    // "Dim" edges.
+	    var subtree_edges = get_descendant_connections(phynode_id);
+	    for( var se = 0; se < subtree_edges.length; se++ ){
+		var ste = subtree_edges[se];
+		ste.update("dim");
+	    }
+	};
+
+	// Movement animation (don't allow movement on the x-axis) and
+	// redo lines.
+	var move = function (dx, dy) {
+
+    	    var phynode_id = this.id;
+
+	    // Move box positions.
+    	    var assoc_phynodes = get_descendant_phynodes(phynode_id);
+    	    for( var si = 0; si < assoc_phynodes.length; si++ ){
+		var mshp = assoc_phynodes[si];
+		mshp.move_y(dy);
+		//ll('mshp['+si+']:'+' oy: '+mshp.start_y+', dy:'+dy);
+    	    }
+
+	    // Collect subtree edges for next bit.
+	    var dimmable_subtree = {};
+	    var subtree_edges = get_descendant_connections(phynode_id);
+	    for( var se = 0; se < subtree_edges.length; se++ ){
+		var ste = subtree_edges[se];
+		dimmable_subtree[ste.id] = true;
+	    }
+
+	    // Update connections; keep subtree dimmed while in transit.
+            for (var i = connections.length; i--;) {
+		var conn = connections[i];
+		if( dimmable_subtree[conn.id] ){
+		    conn.update('dim');		
+		}else{
+		    conn.update();		
+		}
+            }
+            paper.safari();
+	};
+
+	// Undrag animation.
+	var stop = function () {
+
+    	    var phynode_id = this.id;
+
+	    // Fade boxes.
+    	    var assoc_phynodes = get_descendant_phynodes(phynode_id);
+    	    for( var si = 0; si < assoc_phynodes.length; si++ ){
+		var mshp = assoc_phynodes[si];
+		mshp.update();
+    	    }
+
+	    // Update connections; bring them all back to normal.
+            for (var i = connections.length; i--;) {
+		connections[i].update();		
+            }
+            paper.safari();
+	};
+
+	// Experiment with double click.
+	function dblclick_event_handler(event){
+
+	    var phynode_id = this.id;
+
+	    // If this is the first double click here...
+	    var pn = get_pnode_from_phynode_id(phynode_id);
+	    if( pn.open == true ){
+		
+		// "Vanish" edges.
+		var subtree_edges = get_descendant_connections(phynode_id);
+		for( var se = 0; se < subtree_edges.length; se++ ){
+		    var ste = subtree_edges[se];
+		    ste.visible = false;
+		    ste.update();
+		}
+
+		// "Vanish" nodes and text; not this node though...
+		var subtree_nodes = get_descendant_phynodes(phynode_id);
+		for( var sn = 0; sn < subtree_nodes.length; sn++ ){
+		    var stn = subtree_nodes[sn];
+		    if( stn.id != phynode_id ){
+			// Turn of visibilty for children.
+			stn.visible = false;
+		    }else{
+			// Mark self as closed.
+			stn.open = false;
+		    }
+		    stn.update();
+		}
+	    }else{ //Otherwise...
+		
+		// Reestablish edges.
+		var subtree_edges = get_descendant_connections(phynode_id);
+		for( var se = 0; se < subtree_edges.length; se++ ){
+		    var ste = subtree_edges[se];
+		    ste.visible = true;
+		    ste.update();
+		}
+
+		// Restablish pnodes; clear all history.
+		var subtree_nodes = get_descendant_phynodes(phynode_id);
+		for( var sn = 0; sn < subtree_nodes.length; sn++ ){
+		    var stn = subtree_nodes[sn];
+		    stn.open = true;
+		    stn.visible = true;
+		    stn.update();
+		}
+	    }
+	}
+
+	// Experiment with hover.
+	function mouseover_event_handler(event){
+
+    	    var phynode_id = this.id;
+
+	    // Cycle through ancestor phynodes.
+    	    var anc_phynodes = get_ancestor_phynodes(phynode_id);
+    	    for( var ai = 0; ai < anc_phynodes.length; ai++ ){
+		// Change boxes opacity (darken).
+		var ashp = anc_phynodes[ai];
+		ashp.update("highlight");
+	    }
+	    // Cycle through descendant phynodes.
+    	    var desc_phynodes = get_descendant_phynodes(phynode_id);
+    	    for( var di = 0; di < desc_phynodes.length; di++ ){
+		// Change boxes opacity (darken).
+		var dshp = desc_phynodes[di];
+		dshp.update("highlight");
+	    }
+
+	    // See if we can fish any edges out and highlight them.
+    	    var anc_edges = get_ancestor_connections(phynode_id);
+    	    for( var ac = 0; ac < anc_edges.length; ac++ ){
+		var aconn = anc_edges[ac];
+		aconn.update("highlight");
+	    }
+    	    var desc_edges = get_descendant_connections(phynode_id);
+    	    for( var dc = 0; dc < desc_edges.length; dc++ ){
+		var dconn = desc_edges[dc];
+		dconn.update("highlight");
+	    }
+	    paper.safari();
+	}
+	function mouseout_event_handler(event){
+
+    	    var phynode_id = this.id;
+
+	    // Cycle through ancestor phynodes.
+    	    var anc_phynodes = get_ancestor_phynodes(phynode_id);
+    	    for( var ai = 0; ai < anc_phynodes.length; ai++ ){
+		// Change boxes opacity (lighten).
+		var ashp = anc_phynodes[ai];
+		ashp.update();
+    	    }
+	    // Cycle through descendant phynodes.
+    	    var desc_phynodes = get_descendant_phynodes(phynode_id);
+    	    for( var di = 0; di < desc_phynodes.length; di++ ){
+		// Change boxes opacity (lighten).
+		var dshp = desc_phynodes[di];
+		dshp.update();
+    	    }
+
+	    // See if we can fish any edges out and unhighlight them.
+    	    var anc_edges = get_ancestor_connections(phynode_id);
+    	    for( var ac = 0; ac < anc_edges.length; ac++ ){
+		var aconn = anc_edges[ac];
+		aconn.update();
+	    }
+    	    var desc_edges = get_descendant_connections(phynode_id);
+    	    for( var dc = 0; dc < desc_edges.length; dc++ ){
+		var dconn = desc_edges[dc];
+		dconn.update();
+	    }
+	    paper.safari();
+	}
+
+	///
+	///  Render info box if wanted.
+	///
+
+	if( info_box_p ){
+	    
+	    //var lnodes = this._graph.get_leaf_nodes();
+	    // Get the last ordered cohort and build table from that.
+	    var lnodes = layout.cohorts[layout.cohorts.length - 1];
+	    for( var ln = 0; ln < lnodes.length; ln++ ){	    
+		var lnode = lnodes[ln];
+
+		var pr_xa = paper.width - (paper.width * 0.2) + 20; // x-axis
+		var pr_ya = 1.0 + (y_scale * ln); // y-axis
+		var bw = (paper.width * 0.2) - 30.0; // width
+		var bh = y_scale - 1.0; // height
+		var pr = paper.rect(pr_xa, pr_ya,
+				    bw, bh,
+				    1); // roundness
+		pr.attr({
+			    "fill": "#eeee99",
+			    "fill-opacity": 0.5,
+			    "opacity": 1.0,
+			    "stroke": "#333388",
+			    "stroke-width": 1,
+			    "title": "This is " + lnode.id
+			    //"cursor": "move"
+			});
+
+		var pt = paper.text(pr_xa + (bw / 2.0), pr_ya + (bh / 2.0),
+				    "Data for " + lnode.id);
+	    }
+	}
+
+	///
+	/// Phynode creation and placement.
+	/// 
+
+	// Add phynodes and create lookup (hash) for use with connections.
+	var phynodes = new Array();
+	var phynode_hash = {};
+	var texts = new Array();
+	var phynode_id_to_index = {};
+	var phynode_id_to_node_id = {};
+	var node_id_to_index = {};
+	for( var nidi = 0; nidi < layout.node_list.length; nidi++ ){
+
+	    // Calculate position.
+	    var node_id = layout.node_list[nidi];
+	    var lpx = (layout.position_x[node_id] * x_scale) + edge_shift;
+	    var lpy = (layout.position_y[node_id] * y_scale) + edge_shift;
+
+	    // Create node at place. 
+	    var phynode = null;
+	    if( ! this._graph.is_leaf_node(node_id) && info_box_p ){
+		ll('display: internal node: ' + node_id);
+		phynode = new graph_pnode(paper, node_id, lpx, lpy, true);
+		//phynode.attr("width") = 10;
+		//phynode.attr("height") = 10;
+	    }else{
+		phynode = new graph_pnode(paper, node_id, lpx, lpy);
+	    }
+
+            phynodes.push(phynode);
+
+	    // Indexing for later (edge) use.
+	    phynode_hash[node_id] = nidi;
+
+	    // More indexing.
+	    var ref_index = phynodes.length -1;
+	    var phynode_id = phynode.id;
+	    phynode_id_to_index[phynode_id] = ref_index;
+	    phynode_id_to_node_id[phynode_id] = node_id;
+	    node_id_to_index[node_id] = ref_index;
+
+	    ll('display: indexed (node): node_id: ' + node_id +
+			     ', phynode_id: ' + phynode_id +
+			     ', ref_index: ' + ref_index);
+	}
+
+	// Add listeners.
+	for (var i = 0, ii = phynodes.length; i < ii; i++) {
+	    phynodes[i].dblclick(dblclick_event_handler);
+            phynodes[i].drag(move, start, stop);
+	    phynodes[i].mouseover(mouseover_event_handler);
+	    phynodes[i].mouseout(mouseout_event_handler);
+	}
+
+	// Add stored connections.
+	var connections = new Array();
+	var conn_hash_ancestor = {};
+	var conn_hash_descendant = {};
+	for( var ei = 0; ei < layout.edge_list.length; ei++ ){
+
+	    //
+	    var edge = layout.edge_list[ei];
+	    var e0 = edge[0];
+	    var e1 = edge[1];
+
+	    // Push edge onto array.
+	    var n0_pnode = phynodes[phynode_hash[e0]];
+	    var n1_pnode = phynodes[phynode_hash[e1]];
+	    var d_label = layout.parent_distances[e0][e1] + '';
+	    var nconn = new graph_connection(paper, n0_pnode, n1_pnode,
+					     d_label);
+	    connections.push(nconn);
+
+	    // Index edge index for later recall.
+	    if( ! conn_hash_descendant[e0] ){ conn_hash_descendant[e0] = {}; }
+	    conn_hash_descendant[e0][e1] = ei;
+	    if( ! conn_hash_ancestor[e1] ){ conn_hash_ancestor[e1] = {}; }
+	    conn_hash_ancestor[e1][e0] = ei;
+
+	    ll('display: indexed (edge): e0: ' + e0 +
+	       ', e1: ' + e1 +
+	       ', ei: ' + ei);
+	}
+	
+	// See: https://github.com/sorccu/cufon/wiki/about
+	// See: http://raphaeljs.com/reference.html#getFont
+	// var txt = paper.print(100, 100, "print",
+	//  paper.getFont("Museo"), 30).attr({fill: "#00f"});
+	//paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
+	//txt[0].attr({fill: "#f00"});
+    };
+
+};
+/*
+ * Package: message.js
+ * 
+ * Namespace: bbop.widget.message
+ * 
+ * TODO: Code needs to be cleaned with <bbop.html>.
+ * 
+ * BBOP object to produce a self-constructing/self-destructing
+ * sliding message/announcments/warnings.
+ * 
+ * Note that this is a steal of some older code. We'll probably have
+ * to clean this up a bit at some point.
+ * 
+ * These messages make use of the classes "bbop-js-message" and
+ * "bbop-js-message-CTYPE", where CTYPE is one of "error",
+ * "warning", or "notice".
+ * 
+ * Initial placement and the likes should be manipulated through
+ * "bbop-js-message"--the created divs are append to the end of
+ * the body and will not be particularly useful unless styled.
+ * 
+ * This is a completely self-contained UI.
+ */
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'html');
+bbop.core.namespace('bbop', 'widget', 'message');
+
+/*
+ * Constructor: message
+ * 
+ * Contructor for the bbop.widget.message object.
+ *
+ * A trivial invocation might be something like:
+ * : var m = new bbop.widget.message();
+ * : m.notice("Hello, World!");
+ * 
+ * Arguments:
+ *  n/a
+ * 
+ * Returns:
+ *  self
+ */
+bbop.widget.message = function(){
+    
+    this._is_a = 'bbop.widget.message';
+
+    var anchor = this;
+
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (message): ' + str); }
+
+    // Generate tags.
+    function _generate_element(ctype, str){
+
+	var message_classes = ['bbop-js-message',
+			       'bbop-js-message-' + ctype];
+
+	var message_elt =
+	    new bbop.html.tag('div',
+			      {'generate_id': true,
+			       'class': message_classes.join(' ')},
+			      '<h2>' + str + '</h2>');
+
+    	jQuery("body").append(jQuery(message_elt.to_string()).hide());
+
+	// jQuery-ify the element.
+    	var elt = jQuery('#' + message_elt.get_id());
+    	return elt;
+    }
+
+    // Destroy tags.
+    function _destroy_element(){
+    	jQuery(this).remove();
+    }
+
+    ///
+    /// Notice and error handling.
+    ///
+    // elt.show().fadeIn('slow').fadeOut('slow', _destroy_element);
+
+    /*
+     * Function: notice
+     * 
+     * Temporarily display a messsage styled for notices.
+     * 
+     * Parameters:
+     *  msg - the message
+     * 
+     * Returns
+     *  n/a
+     */
+    this.notice = function(msg){
+    	var elt = _generate_element('notice', msg);
+    	elt.show().slideDown('slow').slideUp('slow', _destroy_element);
+    };
+
+    /*
+     * Function: warning
+     * 
+     * Temporarily display a messsage styled for warnings.
+     * 
+     * Parameters:
+     *  msg - the message
+     * 
+     * Returns
+     *  n/a
+     */
+    this.warning = function(msg){
+    	var elt = _generate_element('warning', msg);
+    	elt.show().slideDown('slow').slideUp('slow', _destroy_element);
+    };
+
+    /*
+     * Function: error
+     * 
+     * Temporarily display a messsage styled for errors.
+     * 
+     * Parameters:
+     *  msg - the message
+     * 
+     * Returns
+     *  n/a
+     */
+    this.error = function(msg){
+    	var elt = _generate_element('error', msg);
+    	elt.show().fadeTo(2500, 0.9).fadeOut(1000, _destroy_element);
+    };
+
+};
+/*
+ * Package: overlay.js
+ * 
+ * Namespace: bbop.contrib.go.overlay
+ * 
+ * This package contributes some very high-level functions to make
+ * using things like the web REPL easier to use with GOlr data
+ * sources.
+ * 
+ * It is suggested that you *[not]* use this if you are seriously
+ * programming for BBOP JS since it plays fast and loose with the
+ * dynamic environment, as well as polluting the global namespace.
+ * 
+ * NOTE: Again, this overlay is only usable in a (jQuery) browser
+ * environment--the JS environments are too varied for this to work
+ * arbitrarily, but similar approaches might work in other
+ * envorinments.
+ */
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('bbop', 'contrib', 'go', 'overlay');
+
+/*
+ * Function: overlay
+ * 
+ * Put a set of useful functions into the global namespace for use
+ * with REPLs and the like.
+ * 
+ * Arguments:
+ *  manager_type - the manager type to use, or null (no sublcass)
+ * 
+ * Returns:
+ *  boolean on whether any errors were thrown
+ */
+bbop.contrib.go.overlay = function(manager_type){
+
+    //var anchor = this;
+    var global_ret = true;
+
+    // Either the base manager, or a manager subclass.
+    var mtype = '';
+    if( manager_type ){
+	mtype = '.' + manager_type;
+    }
+
+    // Well, for now, this is what we will do--see bbop.core.evaluate
+    // for a start on a more general ability. I could likely remove
+    // the "var" and have everything out in the global, but it looks
+    // like that might case errors too.
+    if( manager_type != 'jquery' ){
+	throw new Error('Cannot create non-jquery overlays at this time!');
+    }
+
+    var env = [
+	'var loop = bbop.core.each;',
+	'var dump = bbop.core.dump;',
+	'var what_is = bbop.core.what_is;',
+
+	// Defined a global logger.
+	'var logger = new bbop.logger();',
+	'logger.DEBUG = true;',
+	'function ll(str){ return logger.kvetch(str); }',
+	
+	// Get our data env right.
+	'var server_meta = new amigo.data.server();',
+	'var gloc = server_meta.golr_base();',
+	'var gconf = new bbop.golr.conf(amigo.data.golr);',
+
+	// Support a call back to data.
+	'var data = null;',
+	"function callback(json){ ll('// Returned with \"data\".'); data = new bbop.golr.response(json); }",
+
+	// Get a global manager.
+	'var go = new bbop.golr.manager' + mtype + '(gloc, gconf);',
+	"go.register('search', 's', callback);",
+
+	// Add GO-specific methods to our manager.
+	"bbop.golr.manager.prototype.gaf_url = function(){ return this.get_download_url(['source', 'bioentity_label', 'annotation_class', 'reference', 'evidence_type', 'evidence_with', 'taxon', 'date', 'annotation_extension_class', 'bioentity']); };",
+	"bbop.golr.manager.prototype.doc_type = function(t){ return this.add_query_filter('document_type', t); };",
+	"bbop.golr.manager.prototype.filter = function(f, t, p){ var pol = p || \'+'; return this.add_query_filter(f, t, [p]); };"
+    ];
+
+    var jquery_env = [
+	"var empty = function(did){ jQuery('#' + did).empty(); };",
+	"var append = function(did, str){ jQuery('#' + did).append(str); };"
+    ];
+
+    function _b_eval(to_eval){
+	return window.eval(to_eval);
+    }
+    function _s_eval(to_eval){
+	return eval(to_eval);
+    }
+
+    // The main evaluation function.
+    function _eval(to_eval){
+	
+	var retval = '';
+
+	// Try and detect our environment.
+	var env_type = 'server';
+	try{
+	    if( bbop.core.is_defined(window) &&
+		bbop.core.is_defined(window.eval) &&
+		bbop.core.what_is(window.eval) == 'function' ){
+		    env_type = 'browser';
+		}
+	} catch (x) {
+	    // Probably not a browser then, right?
+	}
+
+	// Now try for the execution.
+	try{
+	    // Try and generically evaluate.
+	    var tmp_ret = null;
+	    if( env_type == 'browser' ){
+		tmp_ret = _b_eval(to_eval);
+	    }else{
+		// TODO: Does this work?
+		tmp_ret = _s_eval(to_eval);		
+	    }
+
+	    // Make whatever the tmp_ret is prettier for the return
+	    // val.
+	    if( bbop.core.is_defined(tmp_ret) ){
+		if( bbop.core.what_is(tmp_ret) == 'string' ){
+		    retval = '"' + tmp_ret + '"';
+		}else{
+		    retval = tmp_ret;
+		}
+	    }else{
+		// ...
+	    }
+	}catch (x){
+	    // Bad things happened.
+	    //print('fail on: (' + tmp_ret +'): ' + to_eval);
+	    retval = '[n/a]';
+	    global_ret = false;
+	}
+	
+	return retval;
+    }
+
+    // Now cycle through the command list.
+    bbop.core.each(env,
+		   function(line){
+		       _eval(line);
+		   });
+    
+    // Add a few specific things if we're in a jQuery REPL
+    // environment.
+    if( manager_type && manager_type == 'jquery' ){	
+	bbop.core.each(jquery_env,
+		       function(line){
+			   _eval(line);
+		       });
+    }
+    
+    return global_ret;
+};
+
diff --git a/client/apollo/jslib/bbop/golr.js b/client/apollo/jslib/bbop/golr.js
new file mode 100644
index 0000000..296fef4
--- /dev/null
+++ b/client/apollo/jslib/bbop/golr.js
@@ -0,0 +1,3782 @@
+/* 
+ * Package: golr.js
+ * 
+ * Namespace: amigo.data.golr
+ * 
+ * This package was automatically created during an AmiGO 2 installation
+ * from the YAML configuration files that AmiGO pulls in.
+ *
+ * Useful information about GOlr. See the package <golr_conf.js>
+ * for the API to interact with this data file.
+ *
+ * NOTE: This file is generated dynamically at installation time.
+ * Hard to work with unit tests--hope it's not too bad. You have to
+ * occasionally copy back to keep the unit tests sane.
+ *
+ * NOTE: This file has a slightly different latout from the YAML
+ * configurations files--in addition instead of the fields
+ * being in lists (fields), they are in hashes keyed by the
+ * field id (fields_hash).
+ */
+
+
+// All of the server/instance-specific meta-data.
+bbop.core.require('bbop', 'core');
+bbop.core.namespace('amigo', 'data', 'golr');
+
+/*
+ * Variable: golr
+ * 
+ * The configuration for the data.
+ * Essentially a JSONification of the OWLTools YAML files.
+ * This should be consumed directly by <bbop.golr.conf>.
+ */
+amigo.data.golr = {
+   "general" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "entity^3.0 category^1.0",
+      "filter_weights" : "category^4.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//general-config.yaml",
+      "display_name" : "General",
+      "description" : "A generic search document to get a general overview of everything.",
+      "schema_generating" : "true",
+      "boost_weights" : "entity^3.0 entity_label^3.0 general_blob^3.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "The mangled internal ID for this entity.",
+            "display_name" : "Internal ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The ID/label for this entity.",
+            "display_name" : "Entity",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "entity",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The label for this entity.",
+            "display_name" : "Enity label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "entity_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The document category that this enitity belongs to.",
+            "display_name" : "Document category",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "category",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "A hidden searchable blob document to access this item. It should contain all the goodies that we want to search for, like species(?), synonyms, etc.",
+            "display_name" : "Generic blob",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "general_blob",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "entity_label" : {
+            "transform" : [],
+            "description" : "The label for this entity.",
+            "display_name" : "Enity label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "entity_label",
+            "property" : []
+         },
+         "entity" : {
+            "transform" : [],
+            "description" : "The ID/label for this entity.",
+            "display_name" : "Entity",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "entity",
+            "property" : []
+         },
+         "general_blob" : {
+            "transform" : [],
+            "description" : "A hidden searchable blob document to access this item. It should contain all the goodies that we want to search for, like species(?), synonyms, etc.",
+            "display_name" : "Generic blob",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "general_blob",
+            "property" : []
+         },
+         "category" : {
+            "transform" : [],
+            "description" : "The document category that this enitity belongs to.",
+            "display_name" : "Document category",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "category",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "The mangled internal ID for this entity.",
+            "display_name" : "Internal ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         }
+      },
+      "document_category" : "general",
+      "weight" : "0",
+      "_strict" : 0,
+      "id" : "general",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//general-config.yaml"
+   },
+   "complex_annotation" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "function_class^5.0 enabled_by^4.0 location_list^3.0 process_class^2.0 annotation_group^1.0",
+      "filter_weights" : "annotation_group_label^5.0 enabled_by_label^4.5 location_list_closure_label^4.0 process_class_closure_label^3.0 function_class_closure_label^2.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//complex-ann-config.yaml",
+      "display_name" : "Complex annotations (ALPHA)",
+      "description" : "An individual unit within LEGO. This is <strong>ALPHA</strong> software.",
+      "schema_generating" : "true",
+      "boost_weights" : "annotation_group_label^1.0 annotation_unit_label^1.0 enabled_by^1.0 enabled_by_label^1.0 location_list_closure^1.0 location_list_closure_label^1.0 process_class_closure_label^1.0 function_class_closure_label^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "A unique (and internal) thing.",
+            "display_name" : "ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation unit",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_unit",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation unit",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_unit_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation group",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_group",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation group",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_group_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Enabled by",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "enabled_by",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Enabled by",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "enabled_by_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Function acc/ID.",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "function_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Common function name.",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "function_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "function_class_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "function_class_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Process acc/ID.",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "process_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Common process name.",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "process_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "process_class_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "process_class_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "???",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "owl_blob_json",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "JSON blob form of the local stepwise topology graph.",
+            "display_name" : "Topology graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "topology_graph_json",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "annotation_group" : {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation group",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_group",
+            "property" : []
+         },
+         "process_class_closure_label" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "process_class_closure_label",
+            "property" : []
+         },
+         "function_class_label" : {
+            "transform" : [],
+            "description" : "Common function name.",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "function_class_label",
+            "property" : []
+         },
+         "panther_family_label" : {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         "panther_family" : {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         "process_class_closure" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "process_class_closure",
+            "property" : []
+         },
+         "taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         "enabled_by" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Enabled by",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "enabled_by",
+            "property" : []
+         },
+         "function_class_closure" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "function_class_closure",
+            "property" : []
+         },
+         "location_list_label" : {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_label",
+            "property" : []
+         },
+         "annotation_unit" : {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation unit",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_unit",
+            "property" : []
+         },
+         "location_list" : {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list",
+            "property" : []
+         },
+         "topology_graph_json" : {
+            "transform" : [],
+            "description" : "JSON blob form of the local stepwise topology graph.",
+            "display_name" : "Topology graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "topology_graph_json",
+            "property" : []
+         },
+         "process_class_label" : {
+            "transform" : [],
+            "description" : "Common process name.",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "process_class_label",
+            "property" : []
+         },
+         "function_class_closure_label" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "function_class_closure_label",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "A unique (and internal) thing.",
+            "display_name" : "ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         "function_class" : {
+            "transform" : [],
+            "description" : "Function acc/ID.",
+            "display_name" : "Function",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "function_class",
+            "property" : []
+         },
+         "taxon_closure" : {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         "annotation_group_label" : {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation group",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_group_label",
+            "property" : []
+         },
+         "location_list_closure_label" : {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_closure_label",
+            "property" : []
+         },
+         "location_list_closure" : {
+            "transform" : [],
+            "description" : "",
+            "display_name" : "Location",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "location_list_closure",
+            "property" : []
+         },
+         "annotation_unit_label" : {
+            "transform" : [],
+            "description" : "???.",
+            "display_name" : "Annotation unit",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_unit_label",
+            "property" : []
+         },
+         "enabled_by_label" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "Enabled by",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "enabled_by_label",
+            "property" : []
+         },
+         "taxon" : {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         "taxon_label" : {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         "owl_blob_json" : {
+            "transform" : [],
+            "description" : "???",
+            "display_name" : "???",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "owl_blob_json",
+            "property" : []
+         },
+         "process_class" : {
+            "transform" : [],
+            "description" : "Process acc/ID.",
+            "display_name" : "Process",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "process_class",
+            "property" : []
+         }
+      },
+      "document_category" : "complex_annotation",
+      "weight" : "-5",
+      "_strict" : 0,
+      "id" : "complex_annotation",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//complex-ann-config.yaml"
+   },
+   "ontology" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "annotation_class^8.0 description^6.0 source^4.0 synonym^3.0 alternate_id^2.0",
+      "filter_weights" : "source^4.0 subset^3.0 regulates_closure_label^1.0 is_obsolete^0.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//ont-config.yaml",
+      "display_name" : "Ontology",
+      "description" : "Ontology classes for GO.",
+      "schema_generating" : "true",
+      "boost_weights" : "annotation_class^3.0 annotation_class_label^5.5 description^1.0 comment^0.5 synonym^1.0 alternate_id^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Common term name.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : [
+               "getLabel"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term definition.",
+            "display_name" : "Definition",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "description",
+            "property" : [
+               "getDef"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term namespace.",
+            "display_name" : "Ontology source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : [
+               "getNamespace"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Is the term obsolete?",
+            "display_name" : "Obsoletion",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "boolean",
+            "id" : "is_obsolete",
+            "property" : [
+               "getIsObsoleteBinaryString"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term comment.",
+            "display_name" : "Comment",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "comment",
+            "property" : [
+               "getComment"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : [
+               "getOBOSynonymStrings"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Alternate term id.",
+            "display_name" : "Alt ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "alternate_id",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "alt_id"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term that replaces this term.",
+            "display_name" : "Replaced By",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "replaced_by",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "replaced_by"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Others terms you might want to look at.",
+            "display_name" : "Consider",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "consider",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "consider"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Term subset.",
+            "display_name" : "Subset",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "subset",
+            "property" : [
+               "getSubsets"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Definition cross-reference.",
+            "display_name" : "Def xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "definition_xref",
+            "property" : [
+               "getDefXref"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Database cross-reference.",
+            "display_name" : "DB xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "database_xref",
+            "property" : [
+               "getXref"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Is-a/part-of",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : [
+               "getRelationIDClosure",
+               "BFO:0000050"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Is-a/part-of",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "BFO:0000050"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : [
+               "getRelationIDClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : [
+               "getRelationIDClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "JSON blob form of the local stepwise topology graph.",
+            "display_name" : "Topology graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "topology_graph_json",
+            "property" : [
+               "getSegmentShuntGraphJSON"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "JSON blob form of the local relation transitivity graph.",
+            "display_name" : "Regulates transitivity graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "regulates_transitivity_graph_json",
+            "property" : [
+               "getLineageShuntGraphJSON"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Only in taxon.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "only_in_taxon",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Only in taxon label.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "only_in_taxon_label",
+            "property" : [
+               "getLabel"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Only in taxon closure.",
+            "display_name" : "Only in taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "only_in_taxon_closure",
+            "property" : [
+               "getRelationLabelClosure",
+               "RO:0002160"
+            ]
+         },
+         {
+            "transform" : [],
+            "description" : "Only in taxon label closure.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "only_in_taxon_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "RO:0002160"
+            ]
+         }
+      ],
+      "fields_hash" : {
+         "only_in_taxon_closure" : {
+            "transform" : [],
+            "description" : "Only in taxon closure.",
+            "display_name" : "Only in taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "only_in_taxon_closure",
+            "property" : [
+               "getRelationLabelClosure",
+               "RO:0002160"
+            ]
+         },
+         "source" : {
+            "transform" : [],
+            "description" : "Term namespace.",
+            "display_name" : "Ontology source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : [
+               "getNamespace"
+            ]
+         },
+         "definition_xref" : {
+            "transform" : [],
+            "description" : "Definition cross-reference.",
+            "display_name" : "Def xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "definition_xref",
+            "property" : [
+               "getDefXref"
+            ]
+         },
+         "regulates_transitivity_graph_json" : {
+            "transform" : [],
+            "description" : "JSON blob form of the local relation transitivity graph.",
+            "display_name" : "Regulates transitivity graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "regulates_transitivity_graph_json",
+            "property" : [
+               "getLineageShuntGraphJSON"
+            ]
+         },
+         "database_xref" : {
+            "transform" : [],
+            "description" : "Database cross-reference.",
+            "display_name" : "DB xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "database_xref",
+            "property" : [
+               "getXref"
+            ]
+         },
+         "alternate_id" : {
+            "transform" : [],
+            "description" : "Alternate term id.",
+            "display_name" : "Alt ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "alternate_id",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "alt_id"
+            ]
+         },
+         "only_in_taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Only in taxon label closure.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "only_in_taxon_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "RO:0002160"
+            ]
+         },
+         "consider" : {
+            "transform" : [],
+            "description" : "Others terms you might want to look at.",
+            "display_name" : "Consider",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "consider",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "consider"
+            ]
+         },
+         "topology_graph_json" : {
+            "transform" : [],
+            "description" : "JSON blob form of the local stepwise topology graph.",
+            "display_name" : "Topology graph (JSON)",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "topology_graph_json",
+            "property" : [
+               "getSegmentShuntGraphJSON"
+            ]
+         },
+         "subset" : {
+            "transform" : [],
+            "description" : "Term subset.",
+            "display_name" : "Subset",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "subset",
+            "property" : [
+               "getSubsets"
+            ]
+         },
+         "only_in_taxon_label" : {
+            "transform" : [],
+            "description" : "Only in taxon label.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "only_in_taxon_label",
+            "property" : [
+               "getLabel"
+            ]
+         },
+         "only_in_taxon" : {
+            "transform" : [],
+            "description" : "Only in taxon.",
+            "display_name" : "Only in taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "only_in_taxon",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         "is_obsolete" : {
+            "transform" : [],
+            "description" : "Is the term obsolete?",
+            "display_name" : "Obsoletion",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "boolean",
+            "id" : "is_obsolete",
+            "property" : [
+               "getIsObsoleteBinaryString"
+            ]
+         },
+         "isa_partof_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Is-a/part-of",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "BFO:0000050"
+            ]
+         },
+         "replaced_by" : {
+            "transform" : [],
+            "description" : "Term that replaces this term.",
+            "display_name" : "Replaced By",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "replaced_by",
+            "property" : [
+               "getAnnotationPropertyValues",
+               "replaced_by"
+            ]
+         },
+         "annotation_class" : {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : [
+               "getIdentifier"
+            ]
+         },
+         "regulates_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : [
+               "getRelationLabelClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         "description" : {
+            "transform" : [],
+            "description" : "Term definition.",
+            "display_name" : "Definition",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "description",
+            "property" : [
+               "getDef"
+            ]
+         },
+         "regulates_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Ancestor",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : [
+               "getRelationIDClosure",
+               "BFO:0000050",
+               "RO:0002211",
+               "RO:0002212",
+               "RO:0002213"
+            ]
+         },
+         "isa_partof_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Is-a/part-of",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : [
+               "getRelationIDClosure",
+               "BFO:0000050"
+            ]
+         },
+         "synonym" : {
+            "transform" : [],
+            "description" : "Term synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : [
+               "getOBOSynonymStrings"
+            ]
+         },
+         "comment" : {
+            "transform" : [],
+            "description" : "Term comment.",
+            "display_name" : "Comment",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "comment",
+            "property" : [
+               "getComment"
+            ]
+         },
+         "annotation_class_label" : {
+            "transform" : [],
+            "description" : "Common term name.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : [
+               "getLabel"
+            ]
+         }
+      },
+      "document_category" : "ontology_class",
+      "weight" : "40",
+      "_strict" : 0,
+      "id" : "ontology",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//ont-config.yaml"
+   },
+   "bbop_ann_ev_agg" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "bioentity^4.0 annotation_class^3.0 taxon^2.0",
+      "filter_weights" : "evidence_type_closure^4.0 evidence_with^3.0 taxon_closure_label^2.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//ann_ev_agg-config.yaml",
+      "display_name" : "Advanced",
+      "description" : "A description of annotation evidence aggregate for GOlr and AmiGO.",
+      "schema_generating" : "true",
+      "boost_weights" : "annotation_class^2.0 annotation_class_label^1.0 bioentity^2.0 bioentity_label^1.0 panther_family^1.0 panther_family_label^1.0 taxon_closure_label^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Column 1 + columns 2.",
+            "display_name" : "Gene/product ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Column 3.",
+            "display_name" : "Gene/product label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Column 5.",
+            "display_name" : "Annotation class",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Column 5 + ontology.",
+            "display_name" : "Annotation class label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "All evidence for this term/gene product pair",
+            "display_name" : "Evidence type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_type_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "All column 8s for this term/gene product pair",
+            "display_name" : "Evidence with",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_with",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Column 13: taxon.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "IDs derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Labels derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Family IDs that are associated with this entity.",
+            "display_name" : "Protein family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Families that are associated with this entity.",
+            "display_name" : "Family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "panther_family_label" : {
+            "transform" : [],
+            "description" : "Families that are associated with this entity.",
+            "display_name" : "Family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         "panther_family" : {
+            "transform" : [],
+            "description" : "Family IDs that are associated with this entity.",
+            "display_name" : "Protein family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         "bioentity_label" : {
+            "transform" : [],
+            "description" : "Column 3.",
+            "display_name" : "Gene/product label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         "taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Labels derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         "annotation_class" : {
+            "transform" : [],
+            "description" : "Column 5.",
+            "display_name" : "Annotation class",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         "taxon" : {
+            "transform" : [],
+            "description" : "Column 13: taxon.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         "bioentity" : {
+            "transform" : [],
+            "description" : "Column 1 + columns 2.",
+            "display_name" : "Gene/product ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         "taxon_label" : {
+            "transform" : [],
+            "description" : "Derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         "annotation_class_label" : {
+            "transform" : [],
+            "description" : "Column 5 + ontology.",
+            "display_name" : "Annotation class label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         "evidence_type_closure" : {
+            "transform" : [],
+            "description" : "All evidence for this term/gene product pair",
+            "display_name" : "Evidence type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_type_closure",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         "evidence_with" : {
+            "transform" : [],
+            "description" : "All column 8s for this term/gene product pair",
+            "display_name" : "Evidence with",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_with",
+            "property" : []
+         },
+         "taxon_closure" : {
+            "transform" : [],
+            "description" : "IDs derived from C13 + ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         }
+      },
+      "document_category" : "annotation_evidence_aggregate",
+      "weight" : "-10",
+      "_strict" : 0,
+      "id" : "bbop_ann_ev_agg",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//ann_ev_agg-config.yaml"
+   },
+   "bioentity" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "bioentity^8.0 bioentity_name^7.0 taxon^6.0 panther_family^5.0 type^4.0 source^3.0 annotation_class_list^2.0",
+      "filter_weights" : "source^7.0 type^6.0 panther_family_label^5.0 annotation_class_list_label^3.5 taxon_closure_label^4.0 regulates_closure_label^2.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//bio-config.yaml",
+      "display_name" : "Gene/products",
+      "description" : "A description of bioentities file for GOlr.",
+      "schema_generating" : "true",
+      "boost_weights" : "bioentity^2.0 bioentity_label^2.0 bioentity_name^1.0 bioentity_internal_id^1.0 isa_partof_closure_label^1.0 regulates_closure_label^1.0 panther_family^1.0 panther_family_label^1.0 taxon_closure_label^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Symbol or name.",
+            "display_name" : "Label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The full name of the gene product.",
+            "display_name" : "Name",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_name",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The bioentity ID used at the database of origin.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_internal_id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Type class (GAF column 12).",
+            "display_name" : "Type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "type",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Inferred annotation (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Inferred annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 1 (database source).",
+            "display_name" : "Source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_class_list",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_class_list_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Gene product synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "JSON blob form of the phylogenic tree.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "phylo_graph_json",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Database cross-reference.",
+            "display_name" : "DB xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "database_xref",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "source" : {
+            "transform" : [],
+            "description" : "GAF column 1 (database source).",
+            "display_name" : "Source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : []
+         },
+         "phylo_graph_json" : {
+            "transform" : [],
+            "description" : "JSON blob form of the phylogenic tree.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "phylo_graph_json",
+            "property" : []
+         },
+         "panther_family_label" : {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         "panther_family" : {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         "bioentity_label" : {
+            "transform" : [],
+            "description" : "Symbol or name.",
+            "display_name" : "Label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         "database_xref" : {
+            "transform" : [],
+            "description" : "Database cross-reference.",
+            "display_name" : "DB xref",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "database_xref",
+            "property" : []
+         },
+         "taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         "annotation_class_list_label" : {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_class_list_label",
+            "property" : []
+         },
+         "bioentity_name" : {
+            "transform" : [],
+            "description" : "The full name of the gene product.",
+            "display_name" : "Name",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_name",
+            "property" : []
+         },
+         "bioentity_internal_id" : {
+            "transform" : [],
+            "description" : "The bioentity ID used at the database of origin.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_internal_id",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         "taxon_closure" : {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         "isa_partof_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : []
+         },
+         "annotation_class_list" : {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_class_list",
+            "property" : []
+         },
+         "taxon" : {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         "regulates_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Inferred annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : []
+         },
+         "regulates_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Inferred annotation (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : []
+         },
+         "bioentity" : {
+            "transform" : [],
+            "description" : "Gene/product ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         "isa_partof_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : []
+         },
+         "synonym" : {
+            "transform" : [],
+            "description" : "Gene product synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         "taxon_label" : {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         "type" : {
+            "transform" : [],
+            "description" : "Type class (GAF column 12).",
+            "display_name" : "Type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "type",
+            "property" : []
+         }
+      },
+      "document_category" : "bioentity",
+      "weight" : "30",
+      "_strict" : 0,
+      "id" : "bioentity",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//bio-config.yaml"
+   },
+   "bbop_term_ac" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "annotation_class^8.0 synonym^3.0 alternate_id^2.0",
+      "filter_weights" : "annotation_class^8.0 synonym^3.0 alternate_id^2.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//term-autocomplete-config.yaml",
+      "display_name" : "Term autocomplete",
+      "description" : "Easily find ontology classes in GO. For personality only--not a schema configuration.",
+      "schema_generating" : "false",
+      "boost_weights" : "annotation_class^5.0 annotation_class_label^5.0 synonym^1.0 alternate_id^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Common term name.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Term synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Alternate term id.",
+            "display_name" : "Alt ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "alternate_id",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "synonym" : {
+            "transform" : [],
+            "description" : "Term synonyms.",
+            "display_name" : "Synonyms",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         "annotation_class_label" : {
+            "transform" : [],
+            "description" : "Common term name.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         "alternate_id" : {
+            "transform" : [],
+            "description" : "Alternate term id.",
+            "display_name" : "Alt ID",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "alternate_id",
+            "property" : []
+         },
+         "annotation_class" : {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Term",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "Term acc/ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         }
+      },
+      "document_category" : "ontology_class",
+      "weight" : "-20",
+      "_strict" : 0,
+      "id" : "bbop_term_ac",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//term-autocomplete-config.yaml"
+   },
+   "annotation" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "bioentity^7.0 bioentity_name^6.0 qualifier^5.0 annotation_class^4.7 annotation_extension_json^4.5 source^4.0 taxon^3.0 evidence_type^2.5 evidence_with^2.0 panther_family^1.5 bioentity_isoform^0.5 reference^0.25",
+      "filter_weights" : "source^7.0 assigned_by^6.5 aspect^6.25 evidence_type_closure^6.0 panther_family_label^5.5 qualifier^5.25 taxon_closure_label^5.0 annotation_class_label^4.5 regulates_closure_label^3.0 annotation_extension_class_closure_label^2.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//ann-config.yaml",
+      "display_name" : "Annotations",
+      "description" : "A description of annotations for GOlr and AmiGO.",
+      "schema_generating" : "true",
+      "boost_weights" : "annotation_class^2.0 annotation_class_label^1.0 bioentity^2.0 bioentity_label^1.0 bioentity_name^1.0 annotation_extension_class^2.0 annotation_extension_class_label^1.0 reference^1.0 panther_family^1.0 panther_family_label^1.0 bioentity_isoform^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "A unique (and internal) combination of bioentity and ontology class.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 1 (database source).",
+            "display_name" : "Source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 12 (type class id).",
+            "display_name" : "Type class id",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "type",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 14 (date of assignment).",
+            "display_name" : "Date",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "date",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 15 (assigned by).",
+            "display_name" : "Assigned by",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "assigned_by",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Rational for redundancy of annotation.",
+            "display_name" : "Redundant for",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "is_redundant_for",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Secondary taxon.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "secondary_taxon",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Secondary taxon.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "secondary_taxon_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Secondary taxon closure.",
+            "display_name" : "Secondary taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "secondary_taxon_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Secondary taxon closure.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "secondary_taxon_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Involved in (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Inferred annotation (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Inferred annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of ids/accs over has_participant.",
+            "display_name" : "Has participant (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "has_participant_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Closure of labels over has_participant.",
+            "display_name" : "Has participant",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "has_participant_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 11: gene product synonyms.",
+            "display_name" : "Synonym",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 1 + columns 2.",
+            "display_name" : "Gene/Product",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 3: bioentity label.",
+            "display_name" : "Gene/product label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The full name of the gene product.",
+            "display_name" : "Gene/Product name",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_name",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "The bioentity ID used at the database of origin.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_internal_id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Annotation qualifier (GAF column 4).",
+            "display_name" : "Qualifier",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "qualifier",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 9: Ontology aspect.",
+            "display_name" : "Ontology (aspect)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "aspect",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 17: Bioentity isoform.",
+            "display_name" : "Isoform",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_isoform",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 7: evidence type.",
+            "display_name" : "Evidence",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "evidence_type",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "All evidence (evidence closure) for this annotation",
+            "display_name" : "Evidence type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_type_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 8: with/from.",
+            "display_name" : "Evidence with",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_with",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 6: database reference.",
+            "display_name" : "Reference",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "reference",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "GAF column 16: extension class for the annotation.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension (labels)",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_closure",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_closure_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "A special JSON blob for GAF column 16.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_json",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "panther_family_label" : {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         "annotation_extension_class_closure_label" : {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_closure_label",
+            "property" : []
+         },
+         "bioentity_label" : {
+            "transform" : [],
+            "description" : "GAF column 3: bioentity label.",
+            "display_name" : "Gene/product label",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_label",
+            "property" : []
+         },
+         "date" : {
+            "transform" : [],
+            "description" : "GAF column 14 (date of assignment).",
+            "display_name" : "Date",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "date",
+            "property" : []
+         },
+         "has_participant_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over has_participant.",
+            "display_name" : "Has participant (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "has_participant_closure",
+            "property" : []
+         },
+         "secondary_taxon_label" : {
+            "transform" : [],
+            "description" : "Secondary taxon.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "secondary_taxon_label",
+            "property" : []
+         },
+         "bioentity_internal_id" : {
+            "transform" : [],
+            "description" : "The bioentity ID used at the database of origin.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_internal_id",
+            "property" : []
+         },
+         "bioentity_name" : {
+            "transform" : [],
+            "description" : "The full name of the gene product.",
+            "display_name" : "Gene/Product name",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_name",
+            "property" : []
+         },
+         "evidence_type" : {
+            "transform" : [],
+            "description" : "GAF column 7: evidence type.",
+            "display_name" : "Evidence",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "evidence_type",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "A unique (and internal) combination of bioentity and ontology class.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         "annotation_extension_class_label" : {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension (labels)",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_label",
+            "property" : []
+         },
+         "isa_partof_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over isa and partof.",
+            "display_name" : "Involved in",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure_label",
+            "property" : []
+         },
+         "annotation_class" : {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class",
+            "property" : []
+         },
+         "annotation_extension_json" : {
+            "transform" : [],
+            "description" : "A special JSON blob for GAF column 16.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_json",
+            "property" : []
+         },
+         "has_participant_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over has_participant.",
+            "display_name" : "Has participant",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "has_participant_closure_label",
+            "property" : []
+         },
+         "synonym" : {
+            "transform" : [],
+            "description" : "GAF column 11: gene product synonyms.",
+            "display_name" : "Synonym",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "synonym",
+            "property" : []
+         },
+         "assigned_by" : {
+            "transform" : [],
+            "description" : "GAF column 15 (assigned by).",
+            "display_name" : "Assigned by",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "assigned_by",
+            "property" : []
+         },
+         "type" : {
+            "transform" : [],
+            "description" : "GAF column 12 (type class id).",
+            "display_name" : "Type class id",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "type",
+            "property" : []
+         },
+         "annotation_extension_class" : {
+            "transform" : [],
+            "description" : "GAF column 16: extension class for the annotation.",
+            "display_name" : "Annotation extension",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class",
+            "property" : []
+         },
+         "source" : {
+            "transform" : [],
+            "description" : "GAF column 1 (database source).",
+            "display_name" : "Source",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "source",
+            "property" : []
+         },
+         "panther_family" : {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         "taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Taxon label closure derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure_label",
+            "property" : []
+         },
+         "qualifier" : {
+            "transform" : [],
+            "description" : "Annotation qualifier (GAF column 4).",
+            "display_name" : "Qualifier",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "qualifier",
+            "property" : []
+         },
+         "reference" : {
+            "transform" : [],
+            "description" : "GAF column 6: database reference.",
+            "display_name" : "Reference",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "reference",
+            "property" : []
+         },
+         "secondary_taxon_closure_label" : {
+            "transform" : [],
+            "description" : "Secondary taxon closure.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "secondary_taxon_closure_label",
+            "property" : []
+         },
+         "taxon_closure" : {
+            "transform" : [],
+            "description" : "Taxon IDs derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "taxon_closure",
+            "property" : []
+         },
+         "bioentity_isoform" : {
+            "transform" : [],
+            "description" : "GAF column 17: Bioentity isoform.",
+            "display_name" : "Isoform",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity_isoform",
+            "property" : []
+         },
+         "secondary_taxon_closure" : {
+            "transform" : [],
+            "description" : "Secondary taxon closure.",
+            "display_name" : "Secondary taxon (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "secondary_taxon_closure",
+            "property" : []
+         },
+         "aspect" : {
+            "transform" : [],
+            "description" : "GAF column 9: Ontology aspect.",
+            "display_name" : "Ontology (aspect)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "aspect",
+            "property" : []
+         },
+         "taxon" : {
+            "transform" : [],
+            "description" : "GAF column 13 (taxon).",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon",
+            "property" : []
+         },
+         "regulates_closure_label" : {
+            "transform" : [],
+            "description" : "Closure of labels over regulates.",
+            "display_name" : "Inferred annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure_label",
+            "property" : []
+         },
+         "regulates_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over regulates.",
+            "display_name" : "Inferred annotation (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "regulates_closure",
+            "property" : []
+         },
+         "secondary_taxon" : {
+            "transform" : [],
+            "description" : "Secondary taxon.",
+            "display_name" : "Secondary taxon",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "secondary_taxon",
+            "property" : []
+         },
+         "isa_partof_closure" : {
+            "transform" : [],
+            "description" : "Closure of ids/accs over isa and partof.",
+            "display_name" : "Involved in (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "isa_partof_closure",
+            "property" : []
+         },
+         "bioentity" : {
+            "transform" : [],
+            "description" : "GAF column 1 + columns 2.",
+            "display_name" : "Gene/Product",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "bioentity",
+            "property" : []
+         },
+         "taxon_label" : {
+            "transform" : [],
+            "description" : "Taxon derived from GAF column 13 and ncbi_taxonomy.obo.",
+            "display_name" : "Taxon",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "taxon_label",
+            "property" : []
+         },
+         "annotation_class_label" : {
+            "transform" : [],
+            "description" : "Direct annotations (GAF column 5).",
+            "display_name" : "Direct annotation",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "annotation_class_label",
+            "property" : []
+         },
+         "evidence_type_closure" : {
+            "transform" : [],
+            "description" : "All evidence (evidence closure) for this annotation",
+            "display_name" : "Evidence type",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_type_closure",
+            "property" : []
+         },
+         "evidence_with" : {
+            "transform" : [],
+            "description" : "GAF column 8: with/from.",
+            "display_name" : "Evidence with",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "evidence_with",
+            "property" : []
+         },
+         "is_redundant_for" : {
+            "transform" : [],
+            "description" : "Rational for redundancy of annotation.",
+            "display_name" : "Redundant for",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "is_redundant_for",
+            "property" : []
+         },
+         "annotation_extension_class_closure" : {
+            "transform" : [],
+            "description" : "Annotation extension.",
+            "display_name" : "Annotation extension (IDs)",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "annotation_extension_class_closure",
+            "property" : []
+         }
+      },
+      "document_category" : "annotation",
+      "weight" : "20",
+      "_strict" : 0,
+      "id" : "annotation",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//ann-config.yaml"
+   },
+   "family" : {
+      "searchable_extension" : "_searchable",
+      "result_weights" : "panther_family^5.0 bioentity_list^4.0",
+      "filter_weights" : "bioentity_list_label^1.0",
+      "_infile" : "/home/sjcarbon/local/src/git/amigo/metadata//protein-family-config.yaml",
+      "display_name" : "Protein families",
+      "description" : "Information about protein (PANTHER) families.",
+      "schema_generating" : "true",
+      "boost_weights" : "panther_family^2.0 panther_family_label^2.0 bioentity_list^1.0 bioentity_list_label^1.0",
+      "fields" : [
+         {
+            "transform" : [],
+            "description" : "Family ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "JSON blob form of the phylogenic tree.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "phylo_graph_json",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Gene/products annotated with this protein family.",
+            "display_name" : "Gene/products",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "bioentity_list",
+            "property" : []
+         },
+         {
+            "transform" : [],
+            "description" : "Gene/products annotated with this protein family.",
+            "display_name" : "Gene/products",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "bioentity_list_label",
+            "property" : []
+         }
+      ],
+      "fields_hash" : {
+         "phylo_graph_json" : {
+            "transform" : [],
+            "description" : "JSON blob form of the phylogenic tree.",
+            "display_name" : "This should not be displayed",
+            "indexed" : "false",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "phylo_graph_json",
+            "property" : []
+         },
+         "panther_family_label" : {
+            "transform" : [],
+            "description" : "PANTHER families that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family_label",
+            "property" : []
+         },
+         "panther_family" : {
+            "transform" : [],
+            "description" : "PANTHER family IDs that are associated with this entity.",
+            "display_name" : "PANTHER family",
+            "indexed" : "true",
+            "searchable" : "true",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "panther_family",
+            "property" : []
+         },
+         "bioentity_list" : {
+            "transform" : [],
+            "description" : "Gene/products annotated with this protein family.",
+            "display_name" : "Gene/products",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "bioentity_list",
+            "property" : []
+         },
+         "bioentity_list_label" : {
+            "transform" : [],
+            "description" : "Gene/products annotated with this protein family.",
+            "display_name" : "Gene/products",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "multi",
+            "type" : "string",
+            "id" : "bioentity_list_label",
+            "property" : []
+         },
+         "id" : {
+            "transform" : [],
+            "description" : "Family ID.",
+            "display_name" : "Acc",
+            "indexed" : "true",
+            "searchable" : "false",
+            "required" : "false",
+            "cardinality" : "single",
+            "type" : "string",
+            "id" : "id",
+            "property" : []
+         }
+      },
+      "document_category" : "family",
+      "weight" : "5",
+      "_strict" : 0,
+      "id" : "family",
+      "_outfile" : "/home/sjcarbon/local/src/git/amigo/metadata//protein-family-config.yaml"
+   }
+};
+
diff --git a/client/apollo/jslib/bbop/jquery.js b/client/apollo/jslib/bbop/jquery.js
new file mode 100644
index 0000000..49512bc
--- /dev/null
+++ b/client/apollo/jslib/bbop/jquery.js
@@ -0,0 +1,331 @@
+/* 
+ * Package: jquery.js
+
+ * 
+ * Namespace: bbop.golr.manager.jquery
+ * 
+ * jQuery BBOP manager for dealing with actual ajax calls. Remember,
+ * this is actually a "subclass" of <bbop.golr.manager>.
+ * 
+ * This should still be able to limp along (no ajax and no error
+ * parsing) even outside of a jQuery environment.
+ */
+
+
+// Setup the internal requirements.
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'registry');
+bbop.core.require('bbop', 'golr', 'conf');
+bbop.core.require('bbop', 'golr', 'response');
+bbop.core.require('bbop', 'golr', 'manager');
+bbop.core.namespace('bbop', 'golr', 'manager', 'jquery');
+
+/*
+ * Constructor: jquery
+ * 
+ * Contructor for the GOlr query manager
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ * 
+ * Returns:
+ *  golr manager object
+ * 
+ * See also:
+ *  <bbop.golr.manager>
+ */
+bbop.golr.manager.jquery = function (golr_loc, golr_conf_obj){
+    bbop.golr.manager.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.golr.manager.jquery';
+    
+    // Get a good self-reference point.
+    var anchor = this;
+
+    // Per-manager logger helper.
+    function ll(str){ anchor._logger.kvetch(str); }
+
+    // Before anything else, if we cannot find a viable jQuery library
+    // for use, we're going to create a fake one so we can still test
+    // and work in a non-browser/networked environment.
+    anchor.JQ = new bbop.golr.faux_ajax();
+    try{ // some interpreters might not like this kind of probing
+    	if( typeof(jQuery) !== 'undefined' ){
+    	    //JQ = jQuery;
+//    	    anchor.JQ = jQuery.noConflict();
+    		anchor.JQ = jQuery;
+    	}
+    }catch (x){
+    }finally{
+    	var got = bbop.core.what_is(anchor.JQ);
+    	if( got && got == 'bbop.golr.faux_ajax'){
+    	}else{
+    	    got = 'jQuery';
+    	}
+    	ll('Using ' + got + ' for Ajax calls.');
+    }
+
+    // The base jQuery Ajax args we need with the setup we have.
+    anchor.jq_vars = {
+	//url: qurl,
+	type: "GET",
+	dataType: 'jsonp',
+	jsonp: 'json.wrf'
+    };
+
+    // We'll override the original with something that actually speaks
+    // jQuery. This is the function that runs where there is an AJAX
+    // error during an update. First it has to run some template code,
+    // then it does all of the callbacks.
+    this._run_error_callbacks = function(result, status, error) {
+
+    	ll('Failed server request: '+ result +', '+ status +', '+ error);
+    	//ll('Failed (a): '+ bbop.core.what_is(status));
+    	//ll('Failed (b): '+ bbop.core.dump(status));
+		
+    	var clean_error = "unknown error";
+
+    	// Get the error out (clean it) if possible.
+    	var jreq = result.responseText;
+    	var req = anchor.JQ.parseJSON(jreq); // TODO/BUG: this must be removed
+    	if( req && req['errors'] && req['errors'].length > 0 ){
+    	    var in_error = req['errors'][0];
+    	    ll('ERROR:' + in_error);
+    	    // Split on newline if possible to get
+    	    // at the nice part before the perl
+    	    // error.
+    	    var reg = new RegExp("\n+", "g");
+    	    var clean_error_split =
+    		in_error.split(reg);
+    	    clean_error = clean_error_split[0];
+    	}else if( bbop.core.what_is(error) == 'string' &&
+    		  error.length > 0){
+    	    clean_error = error;
+    	}else if( bbop.core.what_is(status) == 'string' &&
+    		  status.length > 0){
+    	    clean_error = status;
+    	}
+	
+    	// Run all against registered functions.
+    	ll('run error callbacks...');
+    	anchor.apply_callbacks('error', [clean_error, anchor]);
+    };
+
+    // Try and decide between a reset callback and a search callback.
+    // This is useful since jQuery doesn't have a natural way to do
+    // that within the callbacks.
+    this._callback_type_decider = function(json_data){
+    	ll('in callback type decider...');
+
+	var response = new bbop.golr.response(json_data);
+
+    	// 
+    	if( ! response.success() ){
+    	    throw new Error("Unsuccessful response from golr server!");
+    	}else{
+    	    var cb_type = response.callback_type();
+    	    ll('okay response from server, will probe type...: ' + cb_type);
+    	    if( cb_type == 'reset' ){
+    		anchor._run_reset_callbacks(json_data);
+    	    }else if( cb_type == 'search' ){
+    		anchor._run_search_callbacks(json_data);
+    	    }else{
+    		throw new Error("Unknown callback type!");
+    	    }
+    	}
+    };
+
+    /*
+     * Function: safety
+     *
+     * Getter/setter for the trigger safety.
+     * 
+     * If the safety is on, ajax events controlled by the manager will
+     * not occur. The default if off (false).
+     * 
+     * Parameters: 
+     *  safety_on_p - boolean
+     *
+     * Returns:
+     *  boolean
+     */
+    this.safety = function(safety_on_p){
+	if( bbop.core.is_defined(safety_on_p) ){
+	    anchor._safety = safety_on_p;
+	}
+	return anchor._safety;
+    };
+};
+bbop.core.extend(bbop.golr.manager.jquery, bbop.golr.manager);
+
+/*
+ * Function: update
+ *
+ *  See the documentation in <golr_manager.js> on update to get more
+ *  of the story. This override function adds functionality for
+ *  jQuery.
+ * 
+ * You can prevent the triggering of ajax with the <safety>
+ * method.
+ *
+ * Parameters: 
+ *  callback_type - callback type string
+ *  rows - *[serially optional]* integer; the number of rows to return
+ *  start - *[serially optional]* integer; the offset of the returned rows
+ *
+ * Returns:
+ *  the query url (with the jQuery callback specific parameters)
+ * 
+ * Also see:
+ *  <get_query_url>
+ */
+bbop.golr.manager.jquery.prototype.update = function(callback_type,
+						     rows, start){
+    
+    // Get "parents" url first.
+    var parent_update = bbop.golr.manager.prototype.update;
+    var qurl = parent_update.call(this, callback_type, rows, start);
+    
+    // Only actually trigger if the safety is off (default).
+    if( ! this.safety() ){
+	
+	//ll('try: ' + qurl);
+	//widgets.start_wait('Updating...');
+	
+	// Setup JSONP for Solr and jQuery ajax-specific parameters.
+	this.jq_vars['success'] = this._callback_type_decider; // decide & run
+	this.jq_vars['error'] = this._run_error_callbacks; // run error cbs
+	//done: _callback_type_decider, // decide & run search or reset
+	//fail: _run_error_callbacks, // run error callbacks
+	//always: function(){} // do I need this?
+	this.JQ.ajax(qurl, this.jq_vars);
+    }
+    
+    return qurl;
+};
+
+/*
+ * Function: run_batch
+ *
+ * A distant cousin of <update>.
+ * Designed to "serially" get data from a server for
+ * certain types of data crunching routines.
+ * 
+ * Why would you want this? Lets say there are ten distinct things
+ * that you want from the server. Coordinating and collating them all
+ * without annoying the server or going insane is hard in an
+ * asynchronous environment.
+ *
+ * Parameters: 
+ *  accumulator_func - the function that collects
+ *  final_func - the function to run on completion
+ *
+ * Returns:
+ *  the number of batch items run
+ */
+bbop.golr.manager.jquery.prototype.run_batch = function(accumulator_func,
+							final_func){
+
+    var anchor = this;
+
+    // Set the various callbacks internally so we can get back at them
+    // when we lose our stack during the ajax.
+    if( accumulator_func ){ this._batch_accumulator_func = accumulator_func; }
+    if( final_func ){ this._batch_final_func = final_func; }
+
+    // Look at how many states are left.
+    var qurl = anchor.next_batch_url();
+    if( qurl ){
+	    
+	// Generate a custom callback function that will start
+	// this process (next_generator) again--continue the cycle.
+	var next_cycle = function(json_data){
+	    var response = new bbop.golr.response(json_data);
+	    anchor._batch_accumulator_func.apply(anchor, [response, anchor]);
+	    anchor.run_batch();
+	};
+	
+	// Put this custom callback on success.
+	anchor.jq_vars['success'] = next_cycle;
+	anchor.jq_vars['error'] = anchor._run_error_callbacks;
+	anchor.JQ.ajax(qurl, anchor.jq_vars);
+    }else{
+	anchor._batch_final_func.apply(anchor);
+    }
+};
+
+/*
+ * Function: fetch
+ *
+ * A cousin of <update>, but is made to avoid all of the usual
+ * callback functions (except error) and just run the single function
+ * from the argument.
+ * 
+ * Why would you want this? Sometimes you need just a little data
+ * without updating the whole interface or whatever.
+ *
+ * Parameters: 
+ *  run_func - the function to run on completion
+ *
+ * Returns:
+ *  n/a
+ */
+bbop.golr.manager.jquery.prototype.fetch = function(run_func){
+
+    // ...
+    var anchor = this;
+    var qurl = anchor.get_query_url();
+    anchor._run_func = run_func;
+    anchor.jq_vars['success'] =
+	function(json_data){
+	    var response = new bbop.golr.response(json_data);
+	    anchor._run_func(response);   
+	};
+    anchor.jq_vars['error'] = anchor._run_error_callbacks;
+    anchor.JQ.ajax(qurl, anchor.jq_vars);
+};
+
+/*
+ * Namespace: bbop.golr.faux_ajax
+ *
+ * Constructor: faux_ajax
+ * 
+ * Contructor for a fake and inactive Ajax. Used by bbop.golr.manager.jquery
+ * in (testing) environments where jQuery is not available.
+ * 
+ * Returns:
+ *  faux_ajax object
+ */
+bbop.golr.faux_ajax = function (){
+    this._is_a = 'bbop.golr.faux_ajax';
+
+    /*
+     * Function: ajax
+     *
+     * Fake call to jQuery's ajax.
+     *
+     * Parameters: 
+     *  args - whatever
+     *
+     * Returns:
+     *  null
+     */
+    this.ajax = function(args){
+	return null;
+    };
+    /*
+     * Function: parseJSON
+     *
+     * Fake call to jQuery's parseJSON.
+     *
+     * Parameters: 
+     *  args - whatever--they are ignored
+     *
+     * Returns:
+     *  ""
+     */
+    this.parseJSON = function(args){
+	return "";
+    };
+};
+
diff --git a/client/apollo/jslib/bbop/search_box.js b/client/apollo/jslib/bbop/search_box.js
new file mode 100644
index 0000000..317e9ac
--- /dev/null
+++ b/client/apollo/jslib/bbop/search_box.js
@@ -0,0 +1,178 @@
+/*
+ * Package: search_box.js
+ * 
+ * Namespace: bbop.widget.search_box
+ * 
+ * BBOP object to draw various UI elements that have to do with
+ * autocompletion.
+ * 
+ * This is a completely self-contained UI and manager.
+ */
+
+
+bbop.core.require('bbop', 'core');
+bbop.core.require('bbop', 'logger');
+bbop.core.require('bbop', 'template');
+bbop.core.require('bbop', 'golr', 'manager', 'jquery');
+bbop.core.namespace('bbop', 'widget', 'search_box');
+
+/*
+ * Constructor: search_box
+ * 
+ * Contructor for the bbop.widget.search_box object.
+ * 
+ * This is a specialized (and widgetized) subclass of
+ * <bbop.golr.manager.jquery>.
+ * 
+ * The function for the callback argument should either accept a
+ * JSONized solr document representing the selected item or null
+ * (nothing found).
+ * 
+ * While everything in the argument hash is technically optional,
+ * there are probably some fields that you'll want to fill out to make
+ * things work decently. The options for the argument hash are:
+ * 
+ *  label_template - string template for dropdown, can use any document field
+ *  value_template - string template for selected, can use any document field
+ *  minimum_length - wait for this many characters to start (default 3)
+ *  list_select_callback - function takes a json solr doc on dropdown selection
+ * 
+ * To get a better idea on how to use the templates, see the demo page
+ * at http://cdn.berkeleybop.org/jsapi/bbop-js/demo/index.html and
+ * read the documentation for <bbop.template>.
+ * 
+ * Arguments:
+ *  golr_loc - string url to GOlr server;
+ *  golr_conf_obj - a <bbop.golr.conf> object
+ *  interface_id - string id of the element to build on
+ *  in_argument_hash - *[optional]* optional hash of optional arguments
+ * 
+ * Returns:
+ *  this object
+ */
+bbop.widget.search_box = function(golr_loc,
+				  golr_conf_obj,
+				  node,
+				  in_argument_hash){
+    bbop.golr.manager.jquery.call(this, golr_loc, golr_conf_obj);
+    this._is_a = 'bbop.widget.search_box';
+
+    // Aliases.
+    var anchor = this;
+    var loop = bbop.core.each;
+    
+    // Per-UI logger.
+    var logger = new bbop.logger();
+    logger.DEBUG = true;
+    function ll(str){ logger.kvetch('W (auto): ' + str); }
+
+    // Our argument default hash.
+    var default_hash =
+	{
+	    'label_template': '{{id}}',
+	    'value_template': '{{id}}',
+	    'minimum_length': 3, // wait for three characters or more
+	    'list_select_callback': function(){}
+	};
+    var folding_hash = in_argument_hash || {};
+    var arg_hash = bbop.core.fold(default_hash, folding_hash);
+
+    // There should be a string interface_id argument.
+//    this._interface_id = interface_id;
+    this._$node = jQuery(node);
+    this._list_select_callback = arg_hash['list_select_callback'];
+    var label_tt = new bbop.template(arg_hash['label_template']);
+    var value_tt = new bbop.template(arg_hash['value_template']);
+    var minlen = arg_hash['minimum_length'];
+
+    // The all-important argument hash. See:
+    // http://jqueryui.com/demos/autocomplete/#method-widget
+    var auto_args = {
+	minLength: minlen,
+	// Function for a successful data hit.
+	// The data getter, which is making it all more complicated
+	// than it needs to be...we need to close around those
+	// callback hooks so we have to do it inplace here.
+	source: function(request_data, response_hook) {
+	    anchor.jq_vars['success'] = function(json_data){
+		var retlist = [];
+		var resp = new bbop.golr.response(json_data);
+		if( resp.success() ){
+		    loop(resp.documents(),
+			 function(doc){
+
+			     // First, try and pull what we can out of our
+			     var lbl = label_tt.fill(doc);
+
+			     // Now the same thing for the return/value.
+			     var val = value_tt.fill(doc);
+
+			     // Add the discovered items to the return
+			     // save.
+			     var item = {
+				 'label': lbl,
+				 'value': val,
+				 'document': doc
+			     };
+			     retlist.push(item);
+			 });
+		}
+		response_hook(retlist);
+	    };
+
+	    // Get the selected term into the manager and fire.
+	    //anchor.set_query(request_data.term);
+	    anchor.set_comfy_query(request_data.term);
+	    anchor.JQ.ajax(anchor.get_query_url(), anchor.jq_vars);
+	},
+	// What to do when an element is selected.
+	select: function(event, ui){
+	    var doc_to_apply = null;
+	    if( ui.item ){
+		doc_to_apply = ui.item.document;
+	    }
+
+	    // Only do the callback if it is defined.
+	    if( bbop.core.is_defined(anchor._list_select_callback) ){
+		anchor._list_select_callback(doc_to_apply);
+	    }
+	}
+    };
+
+    // Set the ball rolling (attach jQuery autocomplete to doc).
+//    jQuery('.' + anchor._interface_id).autocomplete(auto_args);
+    this._$node.autocomplete(auto_args);
+
+    /*
+     * Function: destroy
+     * 
+     * Remove the autocomplete and functionality from the DOM.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  n/a
+     */
+    this.destroy = function(){
+    	this._$node.autocomplete('destroy');
+    };
+
+    /*
+     * Function: content
+     * 
+     * Get the current text contents of the search box.
+     * 
+     * Arguments:
+     *  n/a
+     * 
+     * Returns:
+     *  string
+     */
+    this.content = function(){
+	return this._$node.val();
+    };
+
+};
+bbop.core.extend(bbop.widget.search_box, bbop.golr.manager.jquery);
+
diff --git a/client/apollo/jslib/jquery/jquery.js b/client/apollo/jslib/jquery/jquery.js
new file mode 100644
index 0000000..8ccd0ea
--- /dev/null
+++ b/client/apollo/jslib/jquery/jquery.js
@@ -0,0 +1,9266 @@
+/*!
+ * jQuery JavaScript Library v1.7.1
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Nov 21 21:11:03 2011 -0500
+ */
+(function( window, undefined ) {
+
+// Use the correct document accordingly with window argument (sandbox)
+var document = window.document,
+	navigator = window.navigator,
+	location = window.location;
+var jQuery = (function() {
+
+// Define a local copy of jQuery
+var jQuery = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context, rootjQuery );
+	},
+
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	// A central reference to the root jQuery(document)
+	rootjQuery,
+
+	// A simple way to check for HTML strings or ID strings
+	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+	quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
+
+	// Check if a string has a non-whitespace character in it
+	rnotwhite = /\S/,
+
+	// Used for trimming whitespace
+	trimLeft = /^\s+/,
+	trimRight = /\s+$/,
+
+	// Match a standalone tag
+	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
+
+	// JSON RegExp
+	rvalidchars = /^[\],:{}\s]*$/,
+	rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
+	rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
+	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+
+	// Useragent RegExp
+	rwebkit = /(webkit)[ \/]([\w.]+)/,
+	ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
+	rmsie = /(msie) ([\w.]+)/,
+	rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
+
+	// Matches dashed string for camelizing
+	rdashAlpha = /-([a-z]|[0-9])/ig,
+	rmsPrefix = /^-ms-/,
+
+	// Used by jQuery.camelCase as callback to replace()
+	fcamelCase = function( all, letter ) {
+		return ( letter + "" ).toUpperCase();
+	},
+
+	// Keep a UserAgent string for use with jQuery.browser
+	userAgent = navigator.userAgent,
+
+	// For matching the engine and version of the browser
+	browserMatch,
+
+	// The deferred used on DOM ready
+	readyList,
+
+	// The ready event handler
+	DOMContentLoaded,
+
+	// Save a reference to some core methods
+	toString = Object.prototype.toString,
+	hasOwn = Object.prototype.hasOwnProperty,
+	push = Array.prototype.push,
+	slice = Array.prototype.slice,
+	trim = String.prototype.trim,
+	indexOf = Array.prototype.indexOf,
+
+	// [[Class]] -> type pairs
+	class2type = {};
+
+jQuery.fn = jQuery.prototype = {
+	constructor: jQuery,
+	init: function( selector, context, rootjQuery ) {
+		var match, elem, ret, doc;
+
+		// Handle $(""), $(null), or $(undefined)
+		if ( !selector ) {
+			return this;
+		}
+
+		// Handle $(DOMElement)
+		if ( selector.nodeType ) {
+			this.context = this[0] = selector;
+			this.length = 1;
+			return this;
+		}
+
+		// The body element only exists once, optimize finding it
+		if ( selector === "body" && !context && document.body ) {
+			this.context = document;
+			this[0] = document.body;
+			this.selector = selector;
+			this.length = 1;
+			return this;
+		}
+
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			// Are we dealing with HTML string or an ID?
+			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+				// Assume that strings that start and end with <> are HTML and skip the regex check
+				match = [ null, selector, null ];
+
+			} else {
+				match = quickExpr.exec( selector );
+			}
+
+			// Verify a match, and that no context was specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] ) {
+					context = context instanceof jQuery ? context[0] : context;
+					doc = ( context ? context.ownerDocument || context : document );
+
+					// If a single string is passed in and it's a single tag
+					// just do a createElement and skip the rest
+					ret = rsingleTag.exec( selector );
+
+					if ( ret ) {
+						if ( jQuery.isPlainObject( context ) ) {
+							selector = [ document.createElement( ret[1] ) ];
+							jQuery.fn.attr.call( selector, context, true );
+
+						} else {
+							selector = [ doc.createElement( ret[1] ) ];
+						}
+
+					} else {
+						ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
+						selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;
+					}
+
+					return jQuery.merge( this, selector );
+
+				// HANDLE: $("#id")
+				} else {
+					elem = document.getElementById( match[2] );
+
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE and Opera return items
+						// by name instead of ID
+						if ( elem.id !== match[2] ) {
+							return rootjQuery.find( selector );
+						}
+
+						// Otherwise, we inject the element directly into the jQuery object
+						this.length = 1;
+						this[0] = elem;
+					}
+
+					this.context = document;
+					this.selector = selector;
+					return this;
+				}
+
+			// HANDLE: $(expr, $(...))
+			} else if ( !context || context.jquery ) {
+				return ( context || rootjQuery ).find( selector );
+
+			// HANDLE: $(expr, context)
+			// (which is just equivalent to: $(context).find(expr)
+			} else {
+				return this.constructor( context ).find( selector );
+			}
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) ) {
+			return rootjQuery.ready( selector );
+		}
+
+		if ( selector.selector !== undefined ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return jQuery.makeArray( selector, this );
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The current version of jQuery being used
+	jquery: "1.7.1",
+
+	// The default length of a jQuery object is 0
+	length: 0,
+
+	// The number of elements contained in the matched element set
+	size: function() {
+		return this.length;
+	},
+
+	toArray: function() {
+		return slice.call( this, 0 );
+	},
+
+	// Get the Nth element in the matched element set OR
+	// Get the whole matched element set as a clean array
+	get: function( num ) {
+		return num == null ?
+
+			// Return a 'clean' array
+			this.toArray() :
+
+			// Return just the object
+			( num < 0 ? this[ this.length + num ] : this[ num ] );
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems, name, selector ) {
+		// Build a new jQuery matched element set
+		var ret = this.constructor();
+
+		if ( jQuery.isArray( elems ) ) {
+			push.apply( ret, elems );
+
+		} else {
+			jQuery.merge( ret, elems );
+		}
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+
+		ret.context = this.context;
+
+		if ( name === "find" ) {
+			ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
+		} else if ( name ) {
+			ret.selector = this.selector + "." + name + "(" + selector + ")";
+		}
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Execute a callback for every element in the matched set.
+	// (You can seed the arguments with an array of args, but this is
+	// only used internally.)
+	each: function( callback, args ) {
+		return jQuery.each( this, callback, args );
+	},
+
+	ready: function( fn ) {
+		// Attach the listeners
+		jQuery.bindReady();
+
+		// Add the callback
+		readyList.add( fn );
+
+		return this;
+	},
+
+	eq: function( i ) {
+		i = +i;
+		return i === -1 ?
+			this.slice( i ) :
+			this.slice( i, i + 1 );
+	},
+
+	first: function() {
+		return this.eq( 0 );
+	},
+
+	last: function() {
+		return this.eq( -1 );
+	},
+
+	slice: function() {
+		return this.pushStack( slice.apply( this, arguments ),
+			"slice", slice.call(arguments).join(",") );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function( elem, i ) {
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	end: function() {
+		return this.prevObject || this.constructor(null);
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: push,
+	sort: [].sort,
+	splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+	var options, name, src, copy, copyIsArray, clone,
+		target = arguments[0] || {},
+		i = 1,
+		length = arguments.length,
+		deep = false;
+
+	// Handle a deep copy situation
+	if ( typeof target === "boolean" ) {
+		deep = target;
+		target = arguments[1] || {};
+		// skip the boolean and the target
+		i = 2;
+	}
+
+	// Handle case when target is a string or something (possible in deep copy)
+	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+		target = {};
+	}
+
+	// extend jQuery itself if only one argument is passed
+	if ( length === i ) {
+		target = this;
+		--i;
+	}
+
+	for ( ; i < length; i++ ) {
+		// Only deal with non-null/undefined values
+		if ( (options = arguments[ i ]) != null ) {
+			// Extend the base object
+			for ( name in options ) {
+				src = target[ name ];
+				copy = options[ name ];
+
+				// Prevent never-ending loop
+				if ( target === copy ) {
+					continue;
+				}
+
+				// Recurse if we're merging plain objects or arrays
+				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+					if ( copyIsArray ) {
+						copyIsArray = false;
+						clone = src && jQuery.isArray(src) ? src : [];
+
+					} else {
+						clone = src && jQuery.isPlainObject(src) ? src : {};
+					}
+
+					// Never move original objects, clone them
+					target[ name ] = jQuery.extend( deep, clone, copy );
+
+				// Don't bring in undefined values
+				} else if ( copy !== undefined ) {
+					target[ name ] = copy;
+				}
+			}
+		}
+	}
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	noConflict: function( deep ) {
+		if ( window.$ === jQuery ) {
+			window.$ = _$;
+		}
+
+		if ( deep && window.jQuery === jQuery ) {
+			window.jQuery = _jQuery;
+		}
+
+		return jQuery;
+	},
+
+	// Is the DOM ready to be used? Set to true once it occurs.
+	isReady: false,
+
+	// A counter to track how many items to wait for before
+	// the ready event fires. See #6781
+	readyWait: 1,
+
+	// Hold (or release) the ready event
+	holdReady: function( hold ) {
+		if ( hold ) {
+			jQuery.readyWait++;
+		} else {
+			jQuery.ready( true );
+		}
+	},
+
+	// Handle when the DOM is ready
+	ready: function( wait ) {
+		// Either a released hold or an DOMready/load event and not yet ready
+		if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
+			// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+			if ( !document.body ) {
+				return setTimeout( jQuery.ready, 1 );
+			}
+
+			// Remember that the DOM is ready
+			jQuery.isReady = true;
+
+			// If a normal DOM Ready event fired, decrement, and wait if need be
+			if ( wait !== true && --jQuery.readyWait > 0 ) {
+				return;
+			}
+
+			// If there are functions bound, to execute
+			readyList.fireWith( document, [ jQuery ] );
+
+			// Trigger any bound ready events
+			if ( jQuery.fn.trigger ) {
+				jQuery( document ).trigger( "ready" ).off( "ready" );
+			}
+		}
+	},
+
+	bindReady: function() {
+		if ( readyList ) {
+			return;
+		}
+
+		readyList = jQuery.Callbacks( "once memory" );
+
+		// Catch cases where $(document).ready() is called after the
+		// browser event has already occurred.
+		if ( document.readyState === "complete" ) {
+			// Handle it asynchronously to allow scripts the opportunity to delay ready
+			return setTimeout( jQuery.ready, 1 );
+		}
+
+		// Mozilla, Opera and webkit nightlies currently support this event
+		if ( document.addEventListener ) {
+			// Use the handy event callback
+			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+			// A fallback to window.onload, that will always work
+			window.addEventListener( "load", jQuery.ready, false );
+
+		// If IE event model is used
+		} else if ( document.attachEvent ) {
+			// ensure firing before onload,
+			// maybe late but safe also for iframes
+			document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+			// A fallback to window.onload, that will always work
+			window.attachEvent( "onload", jQuery.ready );
+
+			// If IE and not a frame
+			// continually check to see if the document is ready
+			var toplevel = false;
+
+			try {
+				toplevel = window.frameElement == null;
+			} catch(e) {}
+
+			if ( document.documentElement.doScroll && toplevel ) {
+				doScrollCheck();
+			}
+		}
+	},
+
+	// See test/unit/core.js for details concerning isFunction.
+	// Since version 1.3, DOM methods and functions like alert
+	// aren't supported. They return false on IE (#2968).
+	isFunction: function( obj ) {
+		return jQuery.type(obj) === "function";
+	},
+
+	isArray: Array.isArray || function( obj ) {
+		return jQuery.type(obj) === "array";
+	},
+
+	// A crude way of determining if an object is a window
+	isWindow: function( obj ) {
+		return obj && typeof obj === "object" && "setInterval" in obj;
+	},
+
+	isNumeric: function( obj ) {
+		return !isNaN( parseFloat(obj) ) && isFinite( obj );
+	},
+
+	type: function( obj ) {
+		return obj == null ?
+			String( obj ) :
+			class2type[ toString.call(obj) ] || "object";
+	},
+
+	isPlainObject: function( obj ) {
+		// Must be an Object.
+		// Because of IE, we also have to check the presence of the constructor property.
+		// Make sure that DOM nodes and window objects don't pass through, as well
+		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+			return false;
+		}
+
+		try {
+			// Not own constructor property must be Object
+			if ( obj.constructor &&
+				!hasOwn.call(obj, "constructor") &&
+				!hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+				return false;
+			}
+		} catch ( e ) {
+			// IE8,9 Will throw exceptions on certain host objects #9897
+			return false;
+		}
+
+		// Own properties are enumerated firstly, so to speed up,
+		// if last one is own, then all properties are own.
+
+		var key;
+		for ( key in obj ) {}
+
+		return key === undefined || hasOwn.call( obj, key );
+	},
+
+	isEmptyObject: function( obj ) {
+		for ( var name in obj ) {
+			return false;
+		}
+		return true;
+	},
+
+	error: function( msg ) {
+		throw new Error( msg );
+	},
+
+	parseJSON: function( data ) {
+		if ( typeof data !== "string" || !data ) {
+			return null;
+		}
+
+		// Make sure leading/trailing whitespace is removed (IE can't handle it)
+		data = jQuery.trim( data );
+
+		// Attempt to parse using the native JSON parser first
+		if ( window.JSON && window.JSON.parse ) {
+			return window.JSON.parse( data );
+		}
+
+		// Make sure the incoming data is actual JSON
+		// Logic borrowed from http://json.org/json2.js
+		if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+			.replace( rvalidtokens, "]" )
+			.replace( rvalidbraces, "")) ) {
+
+			return ( new Function( "return " + data ) )();
+
+		}
+		jQuery.error( "Invalid JSON: " + data );
+	},
+
+	// Cross-browser xml parsing
+	parseXML: function( data ) {
+		var xml, tmp;
+		try {
+			if ( window.DOMParser ) { // Standard
+				tmp = new DOMParser();
+				xml = tmp.parseFromString( data , "text/xml" );
+			} else { // IE
+				xml = new ActiveXObject( "Microsoft.XMLDOM" );
+				xml.async = "false";
+				xml.loadXML( data );
+			}
+		} catch( e ) {
+			xml = undefined;
+		}
+		if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+			jQuery.error( "Invalid XML: " + data );
+		}
+		return xml;
+	},
+
+	noop: function() {},
+
+	// Evaluates a script in a global context
+	// Workarounds based on findings by Jim Driscoll
+	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+	globalEval: function( data ) {
+		if ( data && rnotwhite.test( data ) ) {
+			// We use execScript on Internet Explorer
+			// We use an anonymous function so that context is window
+			// rather than jQuery in Firefox
+			( window.execScript || function( data ) {
+				window[ "eval" ].call( window, data );
+			} )( data );
+		}
+	},
+
+	// Convert dashed to camelCase; used by the css and data modules
+	// Microsoft forgot to hump their vendor prefix (#9572)
+	camelCase: function( string ) {
+		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
+	},
+
+	// args is for internal usage only
+	each: function( object, callback, args ) {
+		var name, i = 0,
+			length = object.length,
+			isObj = length === undefined || jQuery.isFunction( object );
+
+		if ( args ) {
+			if ( isObj ) {
+				for ( name in object ) {
+					if ( callback.apply( object[ name ], args ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.apply( object[ i++ ], args ) === false ) {
+						break;
+					}
+				}
+			}
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( isObj ) {
+				for ( name in object ) {
+					if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {
+						break;
+					}
+				}
+			}
+		}
+
+		return object;
+	},
+
+	// Use native String.trim function wherever possible
+	trim: trim ?
+		function( text ) {
+			return text == null ?
+				"" :
+				trim.call( text );
+		} :
+
+		// Otherwise use our own trimming functionality
+		function( text ) {
+			return text == null ?
+				"" :
+				text.toString().replace( trimLeft, "" ).replace( trimRight, "" );
+		},
+
+	// results is for internal usage only
+	makeArray: function( array, results ) {
+		var ret = results || [];
+
+		if ( array != null ) {
+			// The window, strings (and functions) also have 'length'
+			// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+			var type = jQuery.type( array );
+
+			if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
+				push.call( ret, array );
+			} else {
+				jQuery.merge( ret, array );
+			}
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, array, i ) {
+		var len;
+
+		if ( array ) {
+			if ( indexOf ) {
+				return indexOf.call( array, elem, i );
+			}
+
+			len = array.length;
+			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+			for ( ; i < len; i++ ) {
+				// Skip accessing in sparse arrays
+				if ( i in array && array[ i ] === elem ) {
+					return i;
+				}
+			}
+		}
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		var i = first.length,
+			j = 0;
+
+		if ( typeof second.length === "number" ) {
+			for ( var l = second.length; j < l; j++ ) {
+				first[ i++ ] = second[ j ];
+			}
+
+		} else {
+			while ( second[j] !== undefined ) {
+				first[ i++ ] = second[ j++ ];
+			}
+		}
+
+		first.length = i;
+
+		return first;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var ret = [], retVal;
+		inv = !!inv;
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( var i = 0, length = elems.length; i < length; i++ ) {
+			retVal = !!callback( elems[ i ], i );
+			if ( inv !== retVal ) {
+				ret.push( elems[ i ] );
+			}
+		}
+
+		return ret;
+	},
+
+	// arg is for internal usage only
+	map: function( elems, callback, arg ) {
+		var value, key, ret = [],
+			i = 0,
+			length = elems.length,
+			// jquery objects are treated as arrays
+			isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
+
+		// Go through the array, translating each of the items to their
+		if ( isArray ) {
+			for ( ; i < length; i++ ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+
+		// Go through every key on the object,
+		} else {
+			for ( key in elems ) {
+				value = callback( elems[ key ], key, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+		}
+
+		// Flatten any nested arrays
+		return ret.concat.apply( [], ret );
+	},
+
+	// A global GUID counter for objects
+	guid: 1,
+
+	// Bind a function to a context, optionally partially applying any
+	// arguments.
+	proxy: function( fn, context ) {
+		if ( typeof context === "string" ) {
+			var tmp = fn[ context ];
+			context = fn;
+			fn = tmp;
+		}
+
+		// Quick check to determine if target is callable, in the spec
+		// this throws a TypeError, but we will just return undefined.
+		if ( !jQuery.isFunction( fn ) ) {
+			return undefined;
+		}
+
+		// Simulated bind
+		var args = slice.call( arguments, 2 ),
+			proxy = function() {
+				return fn.apply( context, args.concat( slice.call( arguments ) ) );
+			};
+
+		// Set the guid of unique handler to the same of original handler, so it can be removed
+		proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
+
+		return proxy;
+	},
+
+	// Mutifunctional method to get and set values to a collection
+	// The value/s can optionally be executed if it's a function
+	access: function( elems, key, value, exec, fn, pass ) {
+		var length = elems.length;
+
+		// Setting many attributes
+		if ( typeof key === "object" ) {
+			for ( var k in key ) {
+				jQuery.access( elems, k, key[k], exec, fn, value );
+			}
+			return elems;
+		}
+
+		// Setting one attribute
+		if ( value !== undefined ) {
+			// Optionally, function values get executed if exec is true
+			exec = !pass && exec && jQuery.isFunction(value);
+
+			for ( var i = 0; i < length; i++ ) {
+				fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+			}
+
+			return elems;
+		}
+
+		// Getting an attribute
+		return length ? fn( elems[0], key ) : undefined;
+	},
+
+	now: function() {
+		return ( new Date() ).getTime();
+	},
+
+	// Use of jQuery.browser is frowned upon.
+	// More details: http://docs.jquery.com/Utilities/jQuery.browser
+	uaMatch: function( ua ) {
+		ua = ua.toLowerCase();
+
+		var match = rwebkit.exec( ua ) ||
+			ropera.exec( ua ) ||
+			rmsie.exec( ua ) ||
+			ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
+			[];
+
+		return { browser: match[1] || "", version: match[2] || "0" };
+	},
+
+	sub: function() {
+		function jQuerySub( selector, context ) {
+			return new jQuerySub.fn.init( selector, context );
+		}
+		jQuery.extend( true, jQuerySub, this );
+		jQuerySub.superclass = this;
+		jQuerySub.fn = jQuerySub.prototype = this();
+		jQuerySub.fn.constructor = jQuerySub;
+		jQuerySub.sub = this.sub;
+		jQuerySub.fn.init = function init( selector, context ) {
+			if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
+				context = jQuerySub( context );
+			}
+
+			return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
+		};
+		jQuerySub.fn.init.prototype = jQuerySub.fn;
+		var rootjQuerySub = jQuerySub(document);
+		return jQuerySub;
+	},
+
+	browser: {}
+});
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+	class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+browserMatch = jQuery.uaMatch( userAgent );
+if ( browserMatch.browser ) {
+	jQuery.browser[ browserMatch.browser ] = true;
+	jQuery.browser.version = browserMatch.version;
+}
+
+// Deprecated, use jQuery.browser.webkit instead
+if ( jQuery.browser.webkit ) {
+	jQuery.browser.safari = true;
+}
+
+// IE doesn't match non-breaking spaces with \s
+if ( rnotwhite.test( "\xA0" ) ) {
+	trimLeft = /^[\s\xA0]+/;
+	trimRight = /[\s\xA0]+$/;
+}
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+
+// Cleanup functions for the document ready method
+if ( document.addEventListener ) {
+	DOMContentLoaded = function() {
+		document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+		jQuery.ready();
+	};
+
+} else if ( document.attachEvent ) {
+	DOMContentLoaded = function() {
+		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+		if ( document.readyState === "complete" ) {
+			document.detachEvent( "onreadystatechange", DOMContentLoaded );
+			jQuery.ready();
+		}
+	};
+}
+
+// The DOM ready check for Internet Explorer
+function doScrollCheck() {
+	if ( jQuery.isReady ) {
+		return;
+	}
+
+	try {
+		// If IE is used, use the trick by Diego Perini
+		// http://javascript.nwbox.com/IEContentLoaded/
+		document.documentElement.doScroll("left");
+	} catch(e) {
+		setTimeout( doScrollCheck, 1 );
+		return;
+	}
+
+	// and execute any waiting functions
+	jQuery.ready();
+}
+
+return jQuery;
+
+})();
+
+
+// String to Object flags format cache
+var flagsCache = {};
+
+// Convert String-formatted flags into Object-formatted ones and store in cache
+function createFlags( flags ) {
+	var object = flagsCache[ flags ] = {},
+		i, length;
+	flags = flags.split( /\s+/ );
+	for ( i = 0, length = flags.length; i < length; i++ ) {
+		object[ flags[i] ] = true;
+	}
+	return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *	flags:	an optional list of space-separated flags that will change how
+ *			the callback list behaves
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible flags:
+ *
+ *	once:			will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *	memory:			will keep track of previous values and will call any callback added
+ *					after the list has been fired right away with the latest "memorized"
+ *					values (like a Deferred)
+ *
+ *	unique:			will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *	stopOnFalse:	interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( flags ) {
+
+	// Convert flags from String-formatted to Object-formatted
+	// (we check in cache first)
+	flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
+
+	var // Actual callback list
+		list = [],
+		// Stack of fire calls for repeatable lists
+		stack = [],
+		// Last fire value (for non-forgettable lists)
+		memory,
+		// Flag to know if list is currently firing
+		firing,
+		// First callback to fire (used internally by add and fireWith)
+		firingStart,
+		// End of the loop when firing
+		firingLength,
+		// Index of currently firing callback (modified by remove if needed)
+		firingIndex,
+		// Add one or several callbacks to the list
+		add = function( args ) {
+			var i,
+				length,
+				elem,
+				type,
+				actual;
+			for ( i = 0, length = args.length; i < length; i++ ) {
+				elem = args[ i ];
+				type = jQuery.type( elem );
+				if ( type === "array" ) {
+					// Inspect recursively
+					add( elem );
+				} else if ( type === "function" ) {
+					// Add if not in unique mode and callback is not in
+					if ( !flags.unique || !self.has( elem ) ) {
+						list.push( elem );
+					}
+				}
+			}
+		},
+		// Fire callbacks
+		fire = function( context, args ) {
+			args = args || [];
+			memory = !flags.memory || [ context, args ];
+			firing = true;
+			firingIndex = firingStart || 0;
+			firingStart = 0;
+			firingLength = list.length;
+			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+				if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) {
+					memory = true; // Mark as halted
+					break;
+				}
+			}
+			firing = false;
+			if ( list ) {
+				if ( !flags.once ) {
+					if ( stack && stack.length ) {
+						memory = stack.shift();
+						self.fireWith( memory[ 0 ], memory[ 1 ] );
+					}
+				} else if ( memory === true ) {
+					self.disable();
+				} else {
+					list = [];
+				}
+			}
+		},
+		// Actual Callbacks object
+		self = {
+			// Add a callback or a collection of callbacks to the list
+			add: function() {
+				if ( list ) {
+					var length = list.length;
+					add( arguments );
+					// Do we need to add the callbacks to the
+					// current firing batch?
+					if ( firing ) {
+						firingLength = list.length;
+					// With memory, if we're not firing then
+					// we should call right away, unless previous
+					// firing was halted (stopOnFalse)
+					} else if ( memory && memory !== true ) {
+						firingStart = length;
+						fire( memory[ 0 ], memory[ 1 ] );
+					}
+				}
+				return this;
+			},
+			// Remove a callback from the list
+			remove: function() {
+				if ( list ) {
+					var args = arguments,
+						argIndex = 0,
+						argLength = args.length;
+					for ( ; argIndex < argLength ; argIndex++ ) {
+						for ( var i = 0; i < list.length; i++ ) {
+							if ( args[ argIndex ] === list[ i ] ) {
+								// Handle firingIndex and firingLength
+								if ( firing ) {
+									if ( i <= firingLength ) {
+										firingLength--;
+										if ( i <= firingIndex ) {
+											firingIndex--;
+										}
+									}
+								}
+								// Remove the element
+								list.splice( i--, 1 );
+								// If we have some unicity property then
+								// we only need to do this once
+								if ( flags.unique ) {
+									break;
+								}
+							}
+						}
+					}
+				}
+				return this;
+			},
+			// Control if a given callback is in the list
+			has: function( fn ) {
+				if ( list ) {
+					var i = 0,
+						length = list.length;
+					for ( ; i < length; i++ ) {
+						if ( fn === list[ i ] ) {
+							return true;
+						}
+					}
+				}
+				return false;
+			},
+			// Remove all callbacks from the list
+			empty: function() {
+				list = [];
+				return this;
+			},
+			// Have the list do nothing anymore
+			disable: function() {
+				list = stack = memory = undefined;
+				return this;
+			},
+			// Is it disabled?
+			disabled: function() {
+				return !list;
+			},
+			// Lock the list in its current state
+			lock: function() {
+				stack = undefined;
+				if ( !memory || memory === true ) {
+					self.disable();
+				}
+				return this;
+			},
+			// Is it locked?
+			locked: function() {
+				return !stack;
+			},
+			// Call all callbacks with the given context and arguments
+			fireWith: function( context, args ) {
+				if ( stack ) {
+					if ( firing ) {
+						if ( !flags.once ) {
+							stack.push( [ context, args ] );
+						}
+					} else if ( !( flags.once && memory ) ) {
+						fire( context, args );
+					}
+				}
+				return this;
+			},
+			// Call all the callbacks with the given arguments
+			fire: function() {
+				self.fireWith( this, arguments );
+				return this;
+			},
+			// To know if the callbacks have already been called at least once
+			fired: function() {
+				return !!memory;
+			}
+		};
+
+	return self;
+};
+
+
+
+
+var // Static reference to slice
+	sliceDeferred = [].slice;
+
+jQuery.extend({
+
+	Deferred: function( func ) {
+		var doneList = jQuery.Callbacks( "once memory" ),
+			failList = jQuery.Callbacks( "once memory" ),
+			progressList = jQuery.Callbacks( "memory" ),
+			state = "pending",
+			lists = {
+				resolve: doneList,
+				reject: failList,
+				notify: progressList
+			},
+			promise = {
+				done: doneList.add,
+				fail: failList.add,
+				progress: progressList.add,
+
+				state: function() {
+					return state;
+				},
+
+				// Deprecated
+				isResolved: doneList.fired,
+				isRejected: failList.fired,
+
+				then: function( doneCallbacks, failCallbacks, progressCallbacks ) {
+					deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );
+					return this;
+				},
+				always: function() {
+					deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments );
+					return this;
+				},
+				pipe: function( fnDone, fnFail, fnProgress ) {
+					return jQuery.Deferred(function( newDefer ) {
+						jQuery.each( {
+							done: [ fnDone, "resolve" ],
+							fail: [ fnFail, "reject" ],
+							progress: [ fnProgress, "notify" ]
+						}, function( handler, data ) {
+							var fn = data[ 0 ],
+								action = data[ 1 ],
+								returned;
+							if ( jQuery.isFunction( fn ) ) {
+								deferred[ handler ](function() {
+									returned = fn.apply( this, arguments );
+									if ( returned && jQuery.isFunction( returned.promise ) ) {
+										returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify );
+									} else {
+										newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
+									}
+								});
+							} else {
+								deferred[ handler ]( newDefer[ action ] );
+							}
+						});
+					}).promise();
+				},
+				// Get a promise for this deferred
+				// If obj is provided, the promise aspect is added to the object
+				promise: function( obj ) {
+					if ( obj == null ) {
+						obj = promise;
+					} else {
+						for ( var key in promise ) {
+							obj[ key ] = promise[ key ];
+						}
+					}
+					return obj;
+				}
+			},
+			deferred = promise.promise({}),
+			key;
+
+		for ( key in lists ) {
+			deferred[ key ] = lists[ key ].fire;
+			deferred[ key + "With" ] = lists[ key ].fireWith;
+		}
+
+		// Handle state
+		deferred.done( function() {
+			state = "resolved";
+		}, failList.disable, progressList.lock ).fail( function() {
+			state = "rejected";
+		}, doneList.disable, progressList.lock );
+
+		// Call given func if any
+		if ( func ) {
+			func.call( deferred, deferred );
+		}
+
+		// All done!
+		return deferred;
+	},
+
+	// Deferred helper
+	when: function( firstParam ) {
+		var args = sliceDeferred.call( arguments, 0 ),
+			i = 0,
+			length = args.length,
+			pValues = new Array( length ),
+			count = length,
+			pCount = length,
+			deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
+				firstParam :
+				jQuery.Deferred(),
+			promise = deferred.promise();
+		function resolveFunc( i ) {
+			return function( value ) {
+				args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+				if ( !( --count ) ) {
+					deferred.resolveWith( deferred, args );
+				}
+			};
+		}
+		function progressFunc( i ) {
+			return function( value ) {
+				pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+				deferred.notifyWith( promise, pValues );
+			};
+		}
+		if ( length > 1 ) {
+			for ( ; i < length; i++ ) {
+				if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
+					args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
+				} else {
+					--count;
+				}
+			}
+			if ( !count ) {
+				deferred.resolveWith( deferred, args );
+			}
+		} else if ( deferred !== firstParam ) {
+			deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
+		}
+		return promise;
+	}
+});
+
+
+
+
+jQuery.support = (function() {
+
+	var support,
+		all,
+		a,
+		select,
+		opt,
+		input,
+		marginDiv,
+		fragment,
+		tds,
+		events,
+		eventName,
+		i,
+		isSupported,
+		div = document.createElement( "div" ),
+		documentElement = document.documentElement;
+
+	// Preliminary tests
+	div.setAttribute("className", "t");
+	div.innerHTML = "   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+
+	all = div.getElementsByTagName( "*" );
+	a = div.getElementsByTagName( "a" )[ 0 ];
+
+	// Can't get basic test support
+	if ( !all || !all.length || !a ) {
+		return {};
+	}
+
+	// First batch of supports tests
+	select = document.createElement( "select" );
+	opt = select.appendChild( document.createElement("option") );
+	input = div.getElementsByTagName( "input" )[ 0 ];
+
+	support = {
+		// IE strips leading whitespace when .innerHTML is used
+		leadingWhitespace: ( div.firstChild.nodeType === 3 ),
+
+		// Make sure that tbody elements aren't automatically inserted
+		// IE will insert them into empty tables
+		tbody: !div.getElementsByTagName("tbody").length,
+
+		// Make sure that link elements get serialized correctly by innerHTML
+		// This requires a wrapper element in IE
+		htmlSerialize: !!div.getElementsByTagName("link").length,
+
+		// Get the style information from getAttribute
+		// (IE uses .cssText instead)
+		style: /top/.test( a.getAttribute("style") ),
+
+		// Make sure that URLs aren't manipulated
+		// (IE normalizes it by default)
+		hrefNormalized: ( a.getAttribute("href") === "/a" ),
+
+		// Make sure that element opacity exists
+		// (IE uses filter instead)
+		// Use a regex to work around a WebKit issue. See #5145
+		opacity: /^0.55/.test( a.style.opacity ),
+
+		// Verify style float existence
+		// (IE uses styleFloat instead of cssFloat)
+		cssFloat: !!a.style.cssFloat,
+
+		// Make sure that if no value is specified for a checkbox
+		// that it defaults to "on".
+		// (WebKit defaults to "" instead)
+		checkOn: ( input.value === "on" ),
+
+		// Make sure that a selected-by-default option has a working selected property.
+		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+		optSelected: opt.selected,
+
+		// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+		getSetAttribute: div.className !== "t",
+
+		// Tests for enctype support on a form(#6743)
+		enctype: !!document.createElement("form").enctype,
+
+		// Makes sure cloning an html5 element does not cause problems
+		// Where outerHTML is undefined, this still works
+		html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
+
+		// Will be defined later
+		submitBubbles: true,
+		changeBubbles: true,
+		focusinBubbles: false,
+		deleteExpando: true,
+		noCloneEvent: true,
+		inlineBlockNeedsLayout: false,
+		shrinkWrapBlocks: false,
+		reliableMarginRight: true
+	};
+
+	// Make sure checked status is properly cloned
+	input.checked = true;
+	support.noCloneChecked = input.cloneNode( true ).checked;
+
+	// Make sure that the options inside disabled selects aren't marked as disabled
+	// (WebKit marks them as disabled)
+	select.disabled = true;
+	support.optDisabled = !opt.disabled;
+
+	// Test to see if it's possible to delete an expando from an element
+	// Fails in Internet Explorer
+	try {
+		delete div.test;
+	} catch( e ) {
+		support.deleteExpando = false;
+	}
+
+	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
+		div.attachEvent( "onclick", function() {
+			// Cloning a node shouldn't copy over any
+			// bound event handlers (IE does this)
+			support.noCloneEvent = false;
+		});
+		div.cloneNode( true ).fireEvent( "onclick" );
+	}
+
+	// Check if a radio maintains its value
+	// after being appended to the DOM
+	input = document.createElement("input");
+	input.value = "t";
+	input.setAttribute("type", "radio");
+	support.radioValue = input.value === "t";
+
+	input.setAttribute("checked", "checked");
+	div.appendChild( input );
+	fragment = document.createDocumentFragment();
+	fragment.appendChild( div.lastChild );
+
+	// WebKit doesn't clone checked state correctly in fragments
+	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+	// Check if a disconnected checkbox will retain its checked
+	// value of true after appended to the DOM (IE6/7)
+	support.appendChecked = input.checked;
+
+	fragment.removeChild( input );
+	fragment.appendChild( div );
+
+	div.innerHTML = "";
+
+	// Check if div with explicit width and no margin-right incorrectly
+	// gets computed margin-right based on width of container. For more
+	// info see bug #3333
+	// Fails in WebKit before Feb 2011 nightlies
+	// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+	if ( window.getComputedStyle ) {
+		marginDiv = document.createElement( "div" );
+		marginDiv.style.width = "0";
+		marginDiv.style.marginRight = "0";
+		div.style.width = "2px";
+		div.appendChild( marginDiv );
+		support.reliableMarginRight =
+			( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0;
+	}
+
+	// Technique from Juriy Zaytsev
+	// http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
+	// We only care about the case where non-standard event systems
+	// are used, namely in IE. Short-circuiting here helps us to
+	// avoid an eval call (in setAttribute) which can cause CSP
+	// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
+	if ( div.attachEvent ) {
+		for( i in {
+			submit: 1,
+			change: 1,
+			focusin: 1
+		}) {
+			eventName = "on" + i;
+			isSupported = ( eventName in div );
+			if ( !isSupported ) {
+				div.setAttribute( eventName, "return;" );
+				isSupported = ( typeof div[ eventName ] === "function" );
+			}
+			support[ i + "Bubbles" ] = isSupported;
+		}
+	}
+
+	fragment.removeChild( div );
+
+	// Null elements to avoid leaks in IE
+	fragment = select = opt = marginDiv = div = input = null;
+
+	// Run tests that need a body at doc ready
+	jQuery(function() {
+		var container, outer, inner, table, td, offsetSupport,
+			conMarginTop, ptlm, vb, style, html,
+			body = document.getElementsByTagName("body")[0];
+
+		if ( !body ) {
+			// Return for frameset docs that don't have a body
+			return;
+		}
+
+		conMarginTop = 1;
+		ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;";
+		vb = "visibility:hidden;border:0;";
+		style = "style='" + ptlm + "border:5px solid #000;padding:0;'";
+		html = "<div " + style + "><div></div></div>" +
+			"<table " + style + " cellpadding='0' cellspacing='0'>" +
+			"<tr><td></td></tr></table>";
+
+		container = document.createElement("div");
+		container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px";
+		body.insertBefore( container, body.firstChild );
+
+		// Construct the test element
+		div = document.createElement("div");
+		container.appendChild( div );
+
+		// Check if table cells still have offsetWidth/Height when they are set
+		// to display:none and there are still other visible table cells in a
+		// table row; if so, offsetWidth/Height are not reliable for use when
+		// determining if an element has been hidden directly using
+		// display:none (it is still safe to use offsets if a parent element is
+		// hidden; don safety goggles and see bug #4512 for more information).
+		// (only IE 8 fails this test)
+		div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";
+		tds = div.getElementsByTagName( "td" );
+		isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+		tds[ 0 ].style.display = "";
+		tds[ 1 ].style.display = "none";
+
+		// Check if empty table cells still have offsetWidth/Height
+		// (IE <= 8 fail this test)
+		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+		// Figure out if the W3C box model works as expected
+		div.innerHTML = "";
+		div.style.width = div.style.paddingLeft = "1px";
+		jQuery.boxModel = support.boxModel = div.offsetWidth === 2;
+
+		if ( typeof div.style.zoom !== "undefined" ) {
+			// Check if natively block-level elements act like inline-block
+			// elements when setting their display to 'inline' and giving
+			// them layout
+			// (IE < 8 does this)
+			div.style.display = "inline";
+			div.style.zoom = 1;
+			support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );
+
+			// Check if elements with layout shrink-wrap their children
+			// (IE 6 does this)
+			div.style.display = "";
+			div.innerHTML = "<div style='width:4px;'></div>";
+			support.shrinkWrapBlocks = ( div.offsetWidth !== 2 );
+		}
+
+		div.style.cssText = ptlm + vb;
+		div.innerHTML = html;
+
+		outer = div.firstChild;
+		inner = outer.firstChild;
+		td = outer.nextSibling.firstChild.firstChild;
+
+		offsetSupport = {
+			doesNotAddBorder: ( inner.offsetTop !== 5 ),
+			doesAddBorderForTableAndCells: ( td.offsetTop === 5 )
+		};
+
+		inner.style.position = "fixed";
+		inner.style.top = "20px";
+
+		// safari subtracts parent border width here which is 5px
+		offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 );
+		inner.style.position = inner.style.top = "";
+
+		outer.style.overflow = "hidden";
+		outer.style.position = "relative";
+
+		offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 );
+		offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop );
+
+		body.removeChild( container );
+		div  = container = null;
+
+		jQuery.extend( support, offsetSupport );
+	});
+
+	return support;
+})();
+
+
+
+
+var rbrace = /^(?:\{.*\}|\[.*\])$/,
+	rmultiDash = /([A-Z])/g;
+
+jQuery.extend({
+	cache: {},
+
+	// Please use with caution
+	uuid: 0,
+
+	// Unique for each copy of jQuery on the page
+	// Non-digits removed to match rinlinejQuery
+	expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
+
+	// The following elements throw uncatchable exceptions if you
+	// attempt to add expando properties to them.
+	noData: {
+		"embed": true,
+		// Ban all objects except for Flash (which handle expandos)
+		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+		"applet": true
+	},
+
+	hasData: function( elem ) {
+		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+		return !!elem && !isEmptyDataObject( elem );
+	},
+
+	data: function( elem, name, data, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var privateCache, thisCache, ret,
+			internalKey = jQuery.expando,
+			getByName = typeof name === "string",
+
+			// We have to handle DOM nodes and JS objects differently because IE6-7
+			// can't GC object references properly across the DOM-JS boundary
+			isNode = elem.nodeType,
+
+			// Only DOM nodes need the global jQuery cache; JS object data is
+			// attached directly to the object so GC can occur automatically
+			cache = isNode ? jQuery.cache : elem,
+
+			// Only defining an ID for JS objects if its cache already exists allows
+			// the code to shortcut on the same path as a DOM node with no cache
+			id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
+			isEvents = name === "events";
+
+		// Avoid doing any more work than we need to when trying to get data on an
+		// object that has no data at all
+		if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
+			return;
+		}
+
+		if ( !id ) {
+			// Only DOM nodes need a new unique ID for each element since their data
+			// ends up in the global cache
+			if ( isNode ) {
+				elem[ internalKey ] = id = ++jQuery.uuid;
+			} else {
+				id = internalKey;
+			}
+		}
+
+		if ( !cache[ id ] ) {
+			cache[ id ] = {};
+
+			// Avoids exposing jQuery metadata on plain JS objects when the object
+			// is serialized using JSON.stringify
+			if ( !isNode ) {
+				cache[ id ].toJSON = jQuery.noop;
+			}
+		}
+
+		// An object can be passed to jQuery.data instead of a key/value pair; this gets
+		// shallow copied over onto the existing cache
+		if ( typeof name === "object" || typeof name === "function" ) {
+			if ( pvt ) {
+				cache[ id ] = jQuery.extend( cache[ id ], name );
+			} else {
+				cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+			}
+		}
+
+		privateCache = thisCache = cache[ id ];
+
+		// jQuery data() is stored in a separate object inside the object's internal data
+		// cache in order to avoid key collisions between internal data and user-defined
+		// data.
+		if ( !pvt ) {
+			if ( !thisCache.data ) {
+				thisCache.data = {};
+			}
+
+			thisCache = thisCache.data;
+		}
+
+		if ( data !== undefined ) {
+			thisCache[ jQuery.camelCase( name ) ] = data;
+		}
+
+		// Users should not attempt to inspect the internal events object using jQuery.data,
+		// it is undocumented and subject to change. But does anyone listen? No.
+		if ( isEvents && !thisCache[ name ] ) {
+			return privateCache.events;
+		}
+
+		// Check for both converted-to-camel and non-converted data property names
+		// If a data property was specified
+		if ( getByName ) {
+
+			// First Try to find as-is property data
+			ret = thisCache[ name ];
+
+			// Test for null|undefined property data
+			if ( ret == null ) {
+
+				// Try to find the camelCased property
+				ret = thisCache[ jQuery.camelCase( name ) ];
+			}
+		} else {
+			ret = thisCache;
+		}
+
+		return ret;
+	},
+
+	removeData: function( elem, name, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var thisCache, i, l,
+
+			// Reference to internal data cache key
+			internalKey = jQuery.expando,
+
+			isNode = elem.nodeType,
+
+			// See jQuery.data for more information
+			cache = isNode ? jQuery.cache : elem,
+
+			// See jQuery.data for more information
+			id = isNode ? elem[ internalKey ] : internalKey;
+
+		// If there is already no cache entry for this object, there is no
+		// purpose in continuing
+		if ( !cache[ id ] ) {
+			return;
+		}
+
+		if ( name ) {
+
+			thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+			if ( thisCache ) {
+
+				// Support array or space separated string names for data keys
+				if ( !jQuery.isArray( name ) ) {
+
+					// try the string as a key before any manipulation
+					if ( name in thisCache ) {
+						name = [ name ];
+					} else {
+
+						// split the camel cased version by spaces unless a key with the spaces exists
+						name = jQuery.camelCase( name );
+						if ( name in thisCache ) {
+							name = [ name ];
+						} else {
+							name = name.split( " " );
+						}
+					}
+				}
+
+				for ( i = 0, l = name.length; i < l; i++ ) {
+					delete thisCache[ name[i] ];
+				}
+
+				// If there is no data left in the cache, we want to continue
+				// and let the cache object itself get destroyed
+				if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
+					return;
+				}
+			}
+		}
+
+		// See jQuery.data for more information
+		if ( !pvt ) {
+			delete cache[ id ].data;
+
+			// Don't destroy the parent cache unless the internal data object
+			// had been the only thing left in it
+			if ( !isEmptyDataObject(cache[ id ]) ) {
+				return;
+			}
+		}
+
+		// Browsers that fail expando deletion also refuse to delete expandos on
+		// the window, but it will allow it on all other JS objects; other browsers
+		// don't care
+		// Ensure that `cache` is not a window object #10080
+		if ( jQuery.support.deleteExpando || !cache.setInterval ) {
+			delete cache[ id ];
+		} else {
+			cache[ id ] = null;
+		}
+
+		// We destroyed the cache and need to eliminate the expando on the node to avoid
+		// false lookups in the cache for entries that no longer exist
+		if ( isNode ) {
+			// IE does not allow us to delete expando properties from nodes,
+			// nor does it have a removeAttribute function on Document nodes;
+			// we must handle all of these cases
+			if ( jQuery.support.deleteExpando ) {
+				delete elem[ internalKey ];
+			} else if ( elem.removeAttribute ) {
+				elem.removeAttribute( internalKey );
+			} else {
+				elem[ internalKey ] = null;
+			}
+		}
+	},
+
+	// For internal use only.
+	_data: function( elem, name, data ) {
+		return jQuery.data( elem, name, data, true );
+	},
+
+	// A method for determining if a DOM node can handle the data expando
+	acceptData: function( elem ) {
+		if ( elem.nodeName ) {
+			var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+			if ( match ) {
+				return !(match === true || elem.getAttribute("classid") !== match);
+			}
+		}
+
+		return true;
+	}
+});
+
+jQuery.fn.extend({
+	data: function( key, value ) {
+		var parts, attr, name,
+			data = null;
+
+		if ( typeof key === "undefined" ) {
+			if ( this.length ) {
+				data = jQuery.data( this[0] );
+
+				if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) {
+					attr = this[0].attributes;
+					for ( var i = 0, l = attr.length; i < l; i++ ) {
+						name = attr[i].name;
+
+						if ( name.indexOf( "data-" ) === 0 ) {
+							name = jQuery.camelCase( name.substring(5) );
+
+							dataAttr( this[0], name, data[ name ] );
+						}
+					}
+					jQuery._data( this[0], "parsedAttrs", true );
+				}
+			}
+
+			return data;
+
+		} else if ( typeof key === "object" ) {
+			return this.each(function() {
+				jQuery.data( this, key );
+			});
+		}
+
+		parts = key.split(".");
+		parts[1] = parts[1] ? "." + parts[1] : "";
+
+		if ( value === undefined ) {
+			data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+			// Try to fetch any internally stored data first
+			if ( data === undefined && this.length ) {
+				data = jQuery.data( this[0], key );
+				data = dataAttr( this[0], key, data );
+			}
+
+			return data === undefined && parts[1] ?
+				this.data( parts[0] ) :
+				data;
+
+		} else {
+			return this.each(function() {
+				var self = jQuery( this ),
+					args = [ parts[0], value ];
+
+				self.triggerHandler( "setData" + parts[1] + "!", args );
+				jQuery.data( this, key, value );
+				self.triggerHandler( "changeData" + parts[1] + "!", args );
+			});
+		}
+	},
+
+	removeData: function( key ) {
+		return this.each(function() {
+			jQuery.removeData( this, key );
+		});
+	}
+});
+
+function dataAttr( elem, key, data ) {
+	// If nothing was found internally, try to fetch any
+	// data from the HTML5 data-* attribute
+	if ( data === undefined && elem.nodeType === 1 ) {
+
+		var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+		data = elem.getAttribute( name );
+
+		if ( typeof data === "string" ) {
+			try {
+				data = data === "true" ? true :
+				data === "false" ? false :
+				data === "null" ? null :
+				jQuery.isNumeric( data ) ? parseFloat( data ) :
+					rbrace.test( data ) ? jQuery.parseJSON( data ) :
+					data;
+			} catch( e ) {}
+
+			// Make sure we set the data so it isn't changed later
+			jQuery.data( elem, key, data );
+
+		} else {
+			data = undefined;
+		}
+	}
+
+	return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+	for ( var name in obj ) {
+
+		// if the public data object is empty, the private is still empty
+		if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+			continue;
+		}
+		if ( name !== "toJSON" ) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+
+
+
+function handleQueueMarkDefer( elem, type, src ) {
+	var deferDataKey = type + "defer",
+		queueDataKey = type + "queue",
+		markDataKey = type + "mark",
+		defer = jQuery._data( elem, deferDataKey );
+	if ( defer &&
+		( src === "queue" || !jQuery._data(elem, queueDataKey) ) &&
+		( src === "mark" || !jQuery._data(elem, markDataKey) ) ) {
+		// Give room for hard-coded callbacks to fire first
+		// and eventually mark/queue something else on the element
+		setTimeout( function() {
+			if ( !jQuery._data( elem, queueDataKey ) &&
+				!jQuery._data( elem, markDataKey ) ) {
+				jQuery.removeData( elem, deferDataKey, true );
+				defer.fire();
+			}
+		}, 0 );
+	}
+}
+
+jQuery.extend({
+
+	_mark: function( elem, type ) {
+		if ( elem ) {
+			type = ( type || "fx" ) + "mark";
+			jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 );
+		}
+	},
+
+	_unmark: function( force, elem, type ) {
+		if ( force !== true ) {
+			type = elem;
+			elem = force;
+			force = false;
+		}
+		if ( elem ) {
+			type = type || "fx";
+			var key = type + "mark",
+				count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 );
+			if ( count ) {
+				jQuery._data( elem, key, count );
+			} else {
+				jQuery.removeData( elem, key, true );
+				handleQueueMarkDefer( elem, type, "mark" );
+			}
+		}
+	},
+
+	queue: function( elem, type, data ) {
+		var q;
+		if ( elem ) {
+			type = ( type || "fx" ) + "queue";
+			q = jQuery._data( elem, type );
+
+			// Speed up dequeue by getting out quickly if this is just a lookup
+			if ( data ) {
+				if ( !q || jQuery.isArray(data) ) {
+					q = jQuery._data( elem, type, jQuery.makeArray(data) );
+				} else {
+					q.push( data );
+				}
+			}
+			return q || [];
+		}
+	},
+
+	dequeue: function( elem, type ) {
+		type = type || "fx";
+
+		var queue = jQuery.queue( elem, type ),
+			fn = queue.shift(),
+			hooks = {};
+
+		// If the fx queue is dequeued, always remove the progress sentinel
+		if ( fn === "inprogress" ) {
+			fn = queue.shift();
+		}
+
+		if ( fn ) {
+			// Add a progress sentinel to prevent the fx queue from being
+			// automatically dequeued
+			if ( type === "fx" ) {
+				queue.unshift( "inprogress" );
+			}
+
+			jQuery._data( elem, type + ".run", hooks );
+			fn.call( elem, function() {
+				jQuery.dequeue( elem, type );
+			}, hooks );
+		}
+
+		if ( !queue.length ) {
+			jQuery.removeData( elem, type + "queue " + type + ".run", true );
+			handleQueueMarkDefer( elem, type, "queue" );
+		}
+	}
+});
+
+jQuery.fn.extend({
+	queue: function( type, data ) {
+		if ( typeof type !== "string" ) {
+			data = type;
+			type = "fx";
+		}
+
+		if ( data === undefined ) {
+			return jQuery.queue( this[0], type );
+		}
+		return this.each(function() {
+			var queue = jQuery.queue( this, type, data );
+
+			if ( type === "fx" && queue[0] !== "inprogress" ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	},
+	dequeue: function( type ) {
+		return this.each(function() {
+			jQuery.dequeue( this, type );
+		});
+	},
+	// Based off of the plugin by Clint Helfers, with permission.
+	// http://blindsignals.com/index.php/2009/07/jquery-delay/
+	delay: function( time, type ) {
+		time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+		type = type || "fx";
+
+		return this.queue( type, function( next, hooks ) {
+			var timeout = setTimeout( next, time );
+			hooks.stop = function() {
+				clearTimeout( timeout );
+			};
+		});
+	},
+	clearQueue: function( type ) {
+		return this.queue( type || "fx", [] );
+	},
+	// Get a promise resolved when queues of a certain type
+	// are emptied (fx is the type by default)
+	promise: function( type, object ) {
+		if ( typeof type !== "string" ) {
+			object = type;
+			type = undefined;
+		}
+		type = type || "fx";
+		var defer = jQuery.Deferred(),
+			elements = this,
+			i = elements.length,
+			count = 1,
+			deferDataKey = type + "defer",
+			queueDataKey = type + "queue",
+			markDataKey = type + "mark",
+			tmp;
+		function resolve() {
+			if ( !( --count ) ) {
+				defer.resolveWith( elements, [ elements ] );
+			}
+		}
+		while( i-- ) {
+			if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
+					( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
+						jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
+					jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
+				count++;
+				tmp.add( resolve );
+			}
+		}
+		resolve();
+		return defer.promise();
+	}
+});
+
+
+
+
+var rclass = /[\n\t\r]/g,
+	rspace = /\s+/,
+	rreturn = /\r/g,
+	rtype = /^(?:button|input)$/i,
+	rfocusable = /^(?:button|input|object|select|textarea)$/i,
+	rclickable = /^a(?:rea)?$/i,
+	rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
+	getSetAttribute = jQuery.support.getSetAttribute,
+	nodeHook, boolHook, fixSpecified;
+
+jQuery.fn.extend({
+	attr: function( name, value ) {
+		return jQuery.access( this, name, value, true, jQuery.attr );
+	},
+
+	removeAttr: function( name ) {
+		return this.each(function() {
+			jQuery.removeAttr( this, name );
+		});
+	},
+
+	prop: function( name, value ) {
+		return jQuery.access( this, name, value, true, jQuery.prop );
+	},
+
+	removeProp: function( name ) {
+		name = jQuery.propFix[ name ] || name;
+		return this.each(function() {
+			// try/catch handles cases where IE balks (such as removing a property on window)
+			try {
+				this[ name ] = undefined;
+				delete this[ name ];
+			} catch( e ) {}
+		});
+	},
+
+	addClass: function( value ) {
+		var classNames, i, l, elem,
+			setClass, c, cl;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).addClass( value.call(this, j, this.className) );
+			});
+		}
+
+		if ( value && typeof value === "string" ) {
+			classNames = value.split( rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.nodeType === 1 ) {
+					if ( !elem.className && classNames.length === 1 ) {
+						elem.className = value;
+
+					} else {
+						setClass = " " + elem.className + " ";
+
+						for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+							if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) {
+								setClass += classNames[ c ] + " ";
+							}
+						}
+						elem.className = jQuery.trim( setClass );
+					}
+				}
+			}
+		}
+
+		return this;
+	},
+
+	removeClass: function( value ) {
+		var classNames, i, l, elem, className, c, cl;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).removeClass( value.call(this, j, this.className) );
+			});
+		}
+
+		if ( (value && typeof value === "string") || value === undefined ) {
+			classNames = ( value || "" ).split( rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.nodeType === 1 && elem.className ) {
+					if ( value ) {
+						className = (" " + elem.className + " ").replace( rclass, " " );
+						for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+							className = className.replace(" " + classNames[ c ] + " ", " ");
+						}
+						elem.className = jQuery.trim( className );
+
+					} else {
+						elem.className = "";
+					}
+				}
+			}
+		}
+
+		return this;
+	},
+
+	toggleClass: function( value, stateVal ) {
+		var type = typeof value,
+			isBool = typeof stateVal === "boolean";
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( i ) {
+				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+			});
+		}
+
+		return this.each(function() {
+			if ( type === "string" ) {
+				// toggle individual class names
+				var className,
+					i = 0,
+					self = jQuery( this ),
+					state = stateVal,
+					classNames = value.split( rspace );
+
+				while ( (className = classNames[ i++ ]) ) {
+					// check each className given, space seperated list
+					state = isBool ? state : !self.hasClass( className );
+					self[ state ? "addClass" : "removeClass" ]( className );
+				}
+
+			} else if ( type === "undefined" || type === "boolean" ) {
+				if ( this.className ) {
+					// store className if set
+					jQuery._data( this, "__className__", this.className );
+				}
+
+				// toggle whole className
+				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+			}
+		});
+	},
+
+	hasClass: function( selector ) {
+		var className = " " + selector + " ",
+			i = 0,
+			l = this.length;
+		for ( ; i < l; i++ ) {
+			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
+				return true;
+			}
+		}
+
+		return false;
+	},
+
+	val: function( value ) {
+		var hooks, ret, isFunction,
+			elem = this[0];
+
+		if ( !arguments.length ) {
+			if ( elem ) {
+				hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ];
+
+				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+					return ret;
+				}
+
+				ret = elem.value;
+
+				return typeof ret === "string" ?
+					// handle most common string cases
+					ret.replace(rreturn, "") :
+					// handle cases where value is null/undef or number
+					ret == null ? "" : ret;
+			}
+
+			return;
+		}
+
+		isFunction = jQuery.isFunction( value );
+
+		return this.each(function( i ) {
+			var self = jQuery(this), val;
+
+			if ( this.nodeType !== 1 ) {
+				return;
+			}
+
+			if ( isFunction ) {
+				val = value.call( this, i, self.val() );
+			} else {
+				val = value;
+			}
+
+			// Treat null/undefined as ""; convert numbers to string
+			if ( val == null ) {
+				val = "";
+			} else if ( typeof val === "number" ) {
+				val += "";
+			} else if ( jQuery.isArray( val ) ) {
+				val = jQuery.map(val, function ( value ) {
+					return value == null ? "" : value + "";
+				});
+			}
+
+			hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ];
+
+			// If set returns undefined, fall back to normal setting
+			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+				this.value = val;
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	valHooks: {
+		option: {
+			get: function( elem ) {
+				// attributes.value is undefined in Blackberry 4.7 but
+				// uses .value. See #6932
+				var val = elem.attributes.value;
+				return !val || val.specified ? elem.value : elem.text;
+			}
+		},
+		select: {
+			get: function( elem ) {
+				var value, i, max, option,
+					index = elem.selectedIndex,
+					values = [],
+					options = elem.options,
+					one = elem.type === "select-one";
+
+				// Nothing was selected
+				if ( index < 0 ) {
+					return null;
+				}
+
+				// Loop through all the selected options
+				i = one ? index : 0;
+				max = one ? index + 1 : options.length;
+				for ( ; i < max; i++ ) {
+					option = options[ i ];
+
+					// Don't return options that are disabled or in a disabled optgroup
+					if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
+							(!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+
+						// Get the specific value for the option
+						value = jQuery( option ).val();
+
+						// We don't need an array for one selects
+						if ( one ) {
+							return value;
+						}
+
+						// Multi-Selects return an array
+						values.push( value );
+					}
+				}
+
+				// Fixes Bug #2551 -- select.val() broken in IE after form.reset()
+				if ( one && !values.length && options.length ) {
+					return jQuery( options[ index ] ).val();
+				}
+
+				return values;
+			},
+
+			set: function( elem, value ) {
+				var values = jQuery.makeArray( value );
+
+				jQuery(elem).find("option").each(function() {
+					this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+				});
+
+				if ( !values.length ) {
+					elem.selectedIndex = -1;
+				}
+				return values;
+			}
+		}
+	},
+
+	attrFn: {
+		val: true,
+		css: true,
+		html: true,
+		text: true,
+		data: true,
+		width: true,
+		height: true,
+		offset: true
+	},
+
+	attr: function( elem, name, value, pass ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set attributes on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		if ( pass && name in jQuery.attrFn ) {
+			return jQuery( elem )[ name ]( value );
+		}
+
+		// Fallback to prop when attributes are not supported
+		if ( typeof elem.getAttribute === "undefined" ) {
+			return jQuery.prop( elem, name, value );
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		// All attributes are lowercase
+		// Grab necessary hook if one is defined
+		if ( notxml ) {
+			name = name.toLowerCase();
+			hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
+		}
+
+		if ( value !== undefined ) {
+
+			if ( value === null ) {
+				jQuery.removeAttr( elem, name );
+				return;
+
+			} else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				elem.setAttribute( name, "" + value );
+				return value;
+			}
+
+		} else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
+			return ret;
+
+		} else {
+
+			ret = elem.getAttribute( name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return ret === null ?
+				undefined :
+				ret;
+		}
+	},
+
+	removeAttr: function( elem, value ) {
+		var propName, attrNames, name, l,
+			i = 0;
+
+		if ( value && elem.nodeType === 1 ) {
+			attrNames = value.toLowerCase().split( rspace );
+			l = attrNames.length;
+
+			for ( ; i < l; i++ ) {
+				name = attrNames[ i ];
+
+				if ( name ) {
+					propName = jQuery.propFix[ name ] || name;
+
+					// See #9699 for explanation of this approach (setting first, then removal)
+					jQuery.attr( elem, name, "" );
+					elem.removeAttribute( getSetAttribute ? name : propName );
+
+					// Set corresponding property to false for boolean attributes
+					if ( rboolean.test( name ) && propName in elem ) {
+						elem[ propName ] = false;
+					}
+				}
+			}
+		}
+	},
+
+	attrHooks: {
+		type: {
+			set: function( elem, value ) {
+				// We can't allow the type property to be changed (since it causes problems in IE)
+				if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
+					jQuery.error( "type property can't be changed" );
+				} else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+					// Setting the type on a radio button after the value resets the value in IE6-9
+					// Reset value to it's default in case type is set after value
+					// This is for element creation
+					var val = elem.value;
+					elem.setAttribute( "type", value );
+					if ( val ) {
+						elem.value = val;
+					}
+					return value;
+				}
+			}
+		},
+		// Use the value property for back compat
+		// Use the nodeHook for button elements in IE6/7 (#1954)
+		value: {
+			get: function( elem, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.get( elem, name );
+				}
+				return name in elem ?
+					elem.value :
+					null;
+			},
+			set: function( elem, value, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.set( elem, value, name );
+				}
+				// Does not return so that setAttribute is also used
+				elem.value = value;
+			}
+		}
+	},
+
+	propFix: {
+		tabindex: "tabIndex",
+		readonly: "readOnly",
+		"for": "htmlFor",
+		"class": "className",
+		maxlength: "maxLength",
+		cellspacing: "cellSpacing",
+		cellpadding: "cellPadding",
+		rowspan: "rowSpan",
+		colspan: "colSpan",
+		usemap: "useMap",
+		frameborder: "frameBorder",
+		contenteditable: "contentEditable"
+	},
+
+	prop: function( elem, name, value ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set properties on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		if ( notxml ) {
+			// Fix name and attach hooks
+			name = jQuery.propFix[ name ] || name;
+			hooks = jQuery.propHooks[ name ];
+		}
+
+		if ( value !== undefined ) {
+			if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				return ( elem[ name ] = value );
+			}
+
+		} else {
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+				return ret;
+
+			} else {
+				return elem[ name ];
+			}
+		}
+	},
+
+	propHooks: {
+		tabIndex: {
+			get: function( elem ) {
+				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+				var attributeNode = elem.getAttributeNode("tabindex");
+
+				return attributeNode && attributeNode.specified ?
+					parseInt( attributeNode.value, 10 ) :
+					rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+						0 :
+						undefined;
+			}
+		}
+	}
+});
+
+// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional)
+jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex;
+
+// Hook for boolean attributes
+boolHook = {
+	get: function( elem, name ) {
+		// Align boolean attributes with corresponding properties
+		// Fall back to attribute presence where some booleans are not supported
+		var attrNode,
+			property = jQuery.prop( elem, name );
+		return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
+			name.toLowerCase() :
+			undefined;
+	},
+	set: function( elem, value, name ) {
+		var propName;
+		if ( value === false ) {
+			// Remove boolean attributes when set to false
+			jQuery.removeAttr( elem, name );
+		} else {
+			// value is true since we know at this point it's type boolean and not false
+			// Set boolean attributes to the same name and set the DOM property
+			propName = jQuery.propFix[ name ] || name;
+			if ( propName in elem ) {
+				// Only set the IDL specifically if it already exists on the element
+				elem[ propName ] = true;
+			}
+
+			elem.setAttribute( name, name.toLowerCase() );
+		}
+		return name;
+	}
+};
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+	fixSpecified = {
+		name: true,
+		id: true
+	};
+
+	// Use this for any attribute in IE6/7
+	// This fixes almost every IE6/7 issue
+	nodeHook = jQuery.valHooks.button = {
+		get: function( elem, name ) {
+			var ret;
+			ret = elem.getAttributeNode( name );
+			return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ?
+				ret.nodeValue :
+				undefined;
+		},
+		set: function( elem, value, name ) {
+			// Set the existing or create a new attribute node
+			var ret = elem.getAttributeNode( name );
+			if ( !ret ) {
+				ret = document.createAttribute( name );
+				elem.setAttributeNode( ret );
+			}
+			return ( ret.nodeValue = value + "" );
+		}
+	};
+
+	// Apply the nodeHook to tabindex
+	jQuery.attrHooks.tabindex.set = nodeHook.set;
+
+	// Set width and height to auto instead of 0 on empty string( Bug #8150 )
+	// This is for removals
+	jQuery.each([ "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			set: function( elem, value ) {
+				if ( value === "" ) {
+					elem.setAttribute( name, "auto" );
+					return value;
+				}
+			}
+		});
+	});
+
+	// Set contenteditable to false on removals(#10429)
+	// Setting to empty string throws an error as an invalid value
+	jQuery.attrHooks.contenteditable = {
+		get: nodeHook.get,
+		set: function( elem, value, name ) {
+			if ( value === "" ) {
+				value = "false";
+			}
+			nodeHook.set( elem, value, name );
+		}
+	};
+}
+
+
+// Some attributes require a special call on IE
+if ( !jQuery.support.hrefNormalized ) {
+	jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			get: function( elem ) {
+				var ret = elem.getAttribute( name, 2 );
+				return ret === null ? undefined : ret;
+			}
+		});
+	});
+}
+
+if ( !jQuery.support.style ) {
+	jQuery.attrHooks.style = {
+		get: function( elem ) {
+			// Return undefined in the case of empty string
+			// Normalize to lowercase since IE uppercases css property names
+			return elem.style.cssText.toLowerCase() || undefined;
+		},
+		set: function( elem, value ) {
+			return ( elem.style.cssText = "" + value );
+		}
+	};
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+	jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+		get: function( elem ) {
+			var parent = elem.parentNode;
+
+			if ( parent ) {
+				parent.selectedIndex;
+
+				// Make sure that it also works with optgroups, see #5701
+				if ( parent.parentNode ) {
+					parent.parentNode.selectedIndex;
+				}
+			}
+			return null;
+		}
+	});
+}
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+	jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+	jQuery.each([ "radio", "checkbox" ], function() {
+		jQuery.valHooks[ this ] = {
+			get: function( elem ) {
+				// Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+				return elem.getAttribute("value") === null ? "on" : elem.value;
+			}
+		};
+	});
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+	jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+		set: function( elem, value ) {
+			if ( jQuery.isArray( value ) ) {
+				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+			}
+		}
+	});
+});
+
+
+
+
+var rformElems = /^(?:textarea|input|select)$/i,
+	rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/,
+	rhoverHack = /\bhover(\.\S+)?\b/,
+	rkeyEvent = /^key/,
+	rmouseEvent = /^(?:mouse|contextmenu)|click/,
+	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+	rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,
+	quickParse = function( selector ) {
+		var quick = rquickIs.exec( selector );
+		if ( quick ) {
+			//   0  1    2   3
+			// [ _, tag, id, class ]
+			quick[1] = ( quick[1] || "" ).toLowerCase();
+			quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" );
+		}
+		return quick;
+	},
+	quickIs = function( elem, m ) {
+		var attrs = elem.attributes || {};
+		return (
+			(!m[1] || elem.nodeName.toLowerCase() === m[1]) &&
+			(!m[2] || (attrs.id || {}).value === m[2]) &&
+			(!m[3] || m[3].test( (attrs[ "class" ] || {}).value ))
+		);
+	},
+	hoverHack = function( events ) {
+		return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
+	};
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+	add: function( elem, types, handler, data, selector ) {
+
+		var elemData, eventHandle, events,
+			t, tns, type, namespaces, handleObj,
+			handleObjIn, quick, handlers, special;
+
+		// Don't attach events to noData or text/comment nodes (allow plain objects tho)
+		if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
+			return;
+		}
+
+		// Caller can pass in an object of custom data in lieu of the handler
+		if ( handler.handler ) {
+			handleObjIn = handler;
+			handler = handleObjIn.handler;
+		}
+
+		// Make sure that the handler has a unique ID, used to find/remove it later
+		if ( !handler.guid ) {
+			handler.guid = jQuery.guid++;
+		}
+
+		// Init the element's event structure and main handler, if this is the first
+		events = elemData.events;
+		if ( !events ) {
+			elemData.events = events = {};
+		}
+		eventHandle = elemData.handle;
+		if ( !eventHandle ) {
+			elemData.handle = eventHandle = function( e ) {
+				// Discard the second event of a jQuery.event.trigger() and
+				// when an event is called after a page has unloaded
+				return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
+					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+					undefined;
+			};
+			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+			eventHandle.elem = elem;
+		}
+
+		// Handle multiple events separated by a space
+		// jQuery(...).bind("mouseover mouseout", fn);
+		types = jQuery.trim( hoverHack(types) ).split( " " );
+		for ( t = 0; t < types.length; t++ ) {
+
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = tns[1];
+			namespaces = ( tns[2] || "" ).split( "." ).sort();
+
+			// If event changes its type, use the special event handlers for the changed type
+			special = jQuery.event.special[ type ] || {};
+
+			// If selector defined, determine special event api type, otherwise given type
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+
+			// Update special based on newly reset type
+			special = jQuery.event.special[ type ] || {};
+
+			// handleObj is passed to all event handlers
+			handleObj = jQuery.extend({
+				type: type,
+				origType: tns[1],
+				data: data,
+				handler: handler,
+				guid: handler.guid,
+				selector: selector,
+				quick: quickParse( selector ),
+				namespace: namespaces.join(".")
+			}, handleObjIn );
+
+			// Init the event handler queue if we're the first
+			handlers = events[ type ];
+			if ( !handlers ) {
+				handlers = events[ type ] = [];
+				handlers.delegateCount = 0;
+
+				// Only use addEventListener/attachEvent if the special events handler returns false
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					if ( elem.addEventListener ) {
+						elem.addEventListener( type, eventHandle, false );
+
+					} else if ( elem.attachEvent ) {
+						elem.attachEvent( "on" + type, eventHandle );
+					}
+				}
+			}
+
+			if ( special.add ) {
+				special.add.call( elem, handleObj );
+
+				if ( !handleObj.handler.guid ) {
+					handleObj.handler.guid = handler.guid;
+				}
+			}
+
+			// Add to the element's handler list, delegates in front
+			if ( selector ) {
+				handlers.splice( handlers.delegateCount++, 0, handleObj );
+			} else {
+				handlers.push( handleObj );
+			}
+
+			// Keep track of which events have ever been used, for event optimization
+			jQuery.event.global[ type ] = true;
+		}
+
+		// Nullify elem to prevent memory leaks in IE
+		elem = null;
+	},
+
+	global: {},
+
+	// Detach an event or set of events from an element
+	remove: function( elem, types, handler, selector, mappedTypes ) {
+
+		var elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
+			t, tns, type, origType, namespaces, origCount,
+			j, events, special, handle, eventType, handleObj;
+
+		if ( !elemData || !(events = elemData.events) ) {
+			return;
+		}
+
+		// Once for each type.namespace in types; type may be omitted
+		types = jQuery.trim( hoverHack( types || "" ) ).split(" ");
+		for ( t = 0; t < types.length; t++ ) {
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tns[1];
+			namespaces = tns[2];
+
+			// Unbind all events (on this namespace, if provided) for the element
+			if ( !type ) {
+				for ( type in events ) {
+					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+				}
+				continue;
+			}
+
+			special = jQuery.event.special[ type ] || {};
+			type = ( selector? special.delegateType : special.bindType ) || type;
+			eventType = events[ type ] || [];
+			origCount = eventType.length;
+			namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null;
+
+			// Remove matching events
+			for ( j = 0; j < eventType.length; j++ ) {
+				handleObj = eventType[ j ];
+
+				if ( ( mappedTypes || origType === handleObj.origType ) &&
+					 ( !handler || handler.guid === handleObj.guid ) &&
+					 ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
+					 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+					eventType.splice( j--, 1 );
+
+					if ( handleObj.selector ) {
+						eventType.delegateCount--;
+					}
+					if ( special.remove ) {
+						special.remove.call( elem, handleObj );
+					}
+				}
+			}
+
+			// Remove generic event handler if we removed something and no more handlers exist
+			// (avoids potential for endless recursion during removal of special event handlers)
+			if ( eventType.length === 0 && origCount !== eventType.length ) {
+				if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+					jQuery.removeEvent( elem, type, elemData.handle );
+				}
+
+				delete events[ type ];
+			}
+		}
+
+		// Remove the expando if it's no longer used
+		if ( jQuery.isEmptyObject( events ) ) {
+			handle = elemData.handle;
+			if ( handle ) {
+				handle.elem = null;
+			}
+
+			// removeData also checks for emptiness and clears the expando if empty
+			// so use it instead of delete
+			jQuery.removeData( elem, [ "events", "handle" ], true );
+		}
+	},
+
+	// Events that are safe to short-circuit if no handlers are attached.
+	// Native DOM events should not be added, they may have inline handlers.
+	customEvent: {
+		"getData": true,
+		"setData": true,
+		"changeData": true
+	},
+
+	trigger: function( event, data, elem, onlyHandlers ) {
+		// Don't do events on text and comment nodes
+		if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
+			return;
+		}
+
+		// Event object or event type
+		var type = event.type || event,
+			namespaces = [],
+			cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType;
+
+		// focus/blur morphs to focusin/out; ensure we're not firing them right now
+		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+			return;
+		}
+
+		if ( type.indexOf( "!" ) >= 0 ) {
+			// Exclusive events trigger only for the exact event (no namespaces)
+			type = type.slice(0, -1);
+			exclusive = true;
+		}
+
+		if ( type.indexOf( "." ) >= 0 ) {
+			// Namespaced trigger; create a regexp to match event type in handle()
+			namespaces = type.split(".");
+			type = namespaces.shift();
+			namespaces.sort();
+		}
+
+		if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
+			// No jQuery handlers for this event type, and it can't have inline handlers
+			return;
+		}
+
+		// Caller can pass in an Event, Object, or just an event type string
+		event = typeof event === "object" ?
+			// jQuery.Event object
+			event[ jQuery.expando ] ? event :
+			// Object literal
+			new jQuery.Event( type, event ) :
+			// Just the event type (string)
+			new jQuery.Event( type );
+
+		event.type = type;
+		event.isTrigger = true;
+		event.exclusive = exclusive;
+		event.namespace = namespaces.join( "." );
+		event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null;
+		ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
+
+		// Handle a global trigger
+		if ( !elem ) {
+
+			// TODO: Stop taunting the data cache; remove global events and always attach to document
+			cache = jQuery.cache;
+			for ( i in cache ) {
+				if ( cache[ i ].events && cache[ i ].events[ type ] ) {
+					jQuery.event.trigger( event, data, cache[ i ].handle.elem, true );
+				}
+			}
+			return;
+		}
+
+		// Clean up the event in case it is being reused
+		event.result = undefined;
+		if ( !event.target ) {
+			event.target = elem;
+		}
+
+		// Clone any incoming data and prepend the event, creating the handler arg list
+		data = data != null ? jQuery.makeArray( data ) : [];
+		data.unshift( event );
+
+		// Allow special events to draw outside the lines
+		special = jQuery.event.special[ type ] || {};
+		if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
+			return;
+		}
+
+		// Determine event propagation path in advance, per W3C events spec (#9951)
+		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+		eventPath = [[ elem, special.bindType || type ]];
+		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+			bubbleType = special.delegateType || type;
+			cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;
+			old = null;
+			for ( ; cur; cur = cur.parentNode ) {
+				eventPath.push([ cur, bubbleType ]);
+				old = cur;
+			}
+
+			// Only add window if we got to document (e.g., not plain obj or detached DOM)
+			if ( old && old === elem.ownerDocument ) {
+				eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);
+			}
+		}
+
+		// Fire handlers on the event path
+		for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {
+
+			cur = eventPath[i][0];
+			event.type = eventPath[i][1];
+
+			handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+			if ( handle ) {
+				handle.apply( cur, data );
+			}
+			// Note that this is a bare JS function and not a jQuery handler
+			handle = ontype && cur[ ontype ];
+			if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) {
+				event.preventDefault();
+			}
+		}
+		event.type = type;
+
+		// If nobody prevented the default action, do it now
+		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+			if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
+				!(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+				// Call a native DOM method on the target with the same name name as the event.
+				// Can't use an .isFunction() check here because IE6/7 fails that test.
+				// Don't do default actions on window, that's where global variables be (#6170)
+				// IE<9 dies on focus/blur to hidden element (#1486)
+				if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {
+
+					// Don't re-trigger an onFOO event when we call its FOO() method
+					old = elem[ ontype ];
+
+					if ( old ) {
+						elem[ ontype ] = null;
+					}
+
+					// Prevent re-triggering of the same event, since we already bubbled it above
+					jQuery.event.triggered = type;
+					elem[ type ]();
+					jQuery.event.triggered = undefined;
+
+					if ( old ) {
+						elem[ ontype ] = old;
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	dispatch: function( event ) {
+
+		// Make a writable jQuery.Event from the native event object
+		event = jQuery.event.fix( event || window.event );
+
+		var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
+			delegateCount = handlers.delegateCount,
+			args = [].slice.call( arguments, 0 ),
+			run_all = !event.exclusive && !event.namespace,
+			handlerQueue = [],
+			i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related;
+
+		// Use the fix-ed jQuery.Event rather than the (read-only) native event
+		args[0] = event;
+		event.delegateTarget = this;
+
+		// Determine handlers that should run if there are delegated events
+		// Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861)
+		if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) {
+
+			// Pregenerate a single jQuery object for reuse with .is()
+			jqcur = jQuery(this);
+			jqcur.context = this.ownerDocument || this;
+
+			for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
+				selMatch = {};
+				matches = [];
+				jqcur[0] = cur;
+				for ( i = 0; i < delegateCount; i++ ) {
+					handleObj = handlers[ i ];
+					sel = handleObj.selector;
+
+					if ( selMatch[ sel ] === undefined ) {
+						selMatch[ sel ] = (
+							handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel )
+						);
+					}
+					if ( selMatch[ sel ] ) {
+						matches.push( handleObj );
+					}
+				}
+				if ( matches.length ) {
+					handlerQueue.push({ elem: cur, matches: matches });
+				}
+			}
+		}
+
+		// Add the remaining (directly-bound) handlers
+		if ( handlers.length > delegateCount ) {
+			handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
+		}
+
+		// Run delegates first; they may want to stop propagation beneath us
+		for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
+			matched = handlerQueue[ i ];
+			event.currentTarget = matched.elem;
+
+			for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
+				handleObj = matched.matches[ j ];
+
+				// Triggered event must either 1) be non-exclusive and have no namespace, or
+				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+				if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
+
+					event.data = handleObj.data;
+					event.handleObj = handleObj;
+
+					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+							.apply( matched.elem, args );
+
+					if ( ret !== undefined ) {
+						event.result = ret;
+						if ( ret === false ) {
+							event.preventDefault();
+							event.stopPropagation();
+						}
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	// Includes some event props shared by KeyEvent and MouseEvent
+	// *** attrChange attrName relatedNode srcElement  are not normalized, non-W3C, deprecated, will be removed in 1.8 ***
+	props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+	fixHooks: {},
+
+	keyHooks: {
+		props: "char charCode key keyCode".split(" "),
+		filter: function( event, original ) {
+
+			// Add which for key events
+			if ( event.which == null ) {
+				event.which = original.charCode != null ? original.charCode : original.keyCode;
+			}
+
+			return event;
+		}
+	},
+
+	mouseHooks: {
+		props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+		filter: function( event, original ) {
+			var eventDoc, doc, body,
+				button = original.button,
+				fromElement = original.fromElement;
+
+			// Calculate pageX/Y if missing and clientX/Y available
+			if ( event.pageX == null && original.clientX != null ) {
+				eventDoc = event.target.ownerDocument || document;
+				doc = eventDoc.documentElement;
+				body = eventDoc.body;
+
+				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+			}
+
+			// Add relatedTarget, if necessary
+			if ( !event.relatedTarget && fromElement ) {
+				event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+			}
+
+			// Add which for click: 1 === left; 2 === middle; 3 === right
+			// Note: button is not normalized, so don't use it
+			if ( !event.which && button !== undefined ) {
+				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+			}
+
+			return event;
+		}
+	},
+
+	fix: function( event ) {
+		if ( event[ jQuery.expando ] ) {
+			return event;
+		}
+
+		// Create a writable copy of the event object and normalize some properties
+		var i, prop,
+			originalEvent = event,
+			fixHook = jQuery.event.fixHooks[ event.type ] || {},
+			copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+		event = jQuery.Event( originalEvent );
+
+		for ( i = copy.length; i; ) {
+			prop = copy[ --i ];
+			event[ prop ] = originalEvent[ prop ];
+		}
+
+		// Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
+		if ( !event.target ) {
+			event.target = originalEvent.srcElement || document;
+		}
+
+		// Target should not be a text node (#504, Safari)
+		if ( event.target.nodeType === 3 ) {
+			event.target = event.target.parentNode;
+		}
+
+		// For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8)
+		if ( event.metaKey === undefined ) {
+			event.metaKey = event.ctrlKey;
+		}
+
+		return fixHook.filter? fixHook.filter( event, originalEvent ) : event;
+	},
+
+	special: {
+		ready: {
+			// Make sure the ready event is setup
+			setup: jQuery.bindReady
+		},
+
+		load: {
+			// Prevent triggered image.load events from bubbling to window.load
+			noBubble: true
+		},
+
+		focus: {
+			delegateType: "focusin"
+		},
+		blur: {
+			delegateType: "focusout"
+		},
+
+		beforeunload: {
+			setup: function( data, namespaces, eventHandle ) {
+				// We only want to do this special case on windows
+				if ( jQuery.isWindow( this ) ) {
+					this.onbeforeunload = eventHandle;
+				}
+			},
+
+			teardown: function( namespaces, eventHandle ) {
+				if ( this.onbeforeunload === eventHandle ) {
+					this.onbeforeunload = null;
+				}
+			}
+		}
+	},
+
+	simulate: function( type, elem, event, bubble ) {
+		// Piggyback on a donor event to simulate a different one.
+		// Fake originalEvent to avoid donor's stopPropagation, but if the
+		// simulated event prevents default then we do the same on the donor.
+		var e = jQuery.extend(
+			new jQuery.Event(),
+			event,
+			{ type: type,
+				isSimulated: true,
+				originalEvent: {}
+			}
+		);
+		if ( bubble ) {
+			jQuery.event.trigger( e, null, elem );
+		} else {
+			jQuery.event.dispatch.call( elem, e );
+		}
+		if ( e.isDefaultPrevented() ) {
+			event.preventDefault();
+		}
+	}
+};
+
+// Some plugins are using, but it's undocumented/deprecated and will be removed.
+// The 1.7 special event interface should provide all the hooks needed now.
+jQuery.event.handle = jQuery.event.dispatch;
+
+jQuery.removeEvent = document.removeEventListener ?
+	function( elem, type, handle ) {
+		if ( elem.removeEventListener ) {
+			elem.removeEventListener( type, handle, false );
+		}
+	} :
+	function( elem, type, handle ) {
+		if ( elem.detachEvent ) {
+			elem.detachEvent( "on" + type, handle );
+		}
+	};
+
+jQuery.Event = function( src, props ) {
+	// Allow instantiation without the 'new' keyword
+	if ( !(this instanceof jQuery.Event) ) {
+		return new jQuery.Event( src, props );
+	}
+
+	// Event object
+	if ( src && src.type ) {
+		this.originalEvent = src;
+		this.type = src.type;
+
+		// Events bubbling up the document may have been marked as prevented
+		// by a handler lower down the tree; reflect the correct value.
+		this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+			src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+	// Event type
+	} else {
+		this.type = src;
+	}
+
+	// Put explicitly provided properties onto the event object
+	if ( props ) {
+		jQuery.extend( this, props );
+	}
+
+	// Create a timestamp if incoming event doesn't have one
+	this.timeStamp = src && src.timeStamp || jQuery.now();
+
+	// Mark it as fixed
+	this[ jQuery.expando ] = true;
+};
+
+function returnFalse() {
+	return false;
+}
+function returnTrue() {
+	return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+	preventDefault: function() {
+		this.isDefaultPrevented = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+
+		// if preventDefault exists run it on the original event
+		if ( e.preventDefault ) {
+			e.preventDefault();
+
+		// otherwise set the returnValue property of the original event to false (IE)
+		} else {
+			e.returnValue = false;
+		}
+	},
+	stopPropagation: function() {
+		this.isPropagationStopped = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+		// if stopPropagation exists run it on the original event
+		if ( e.stopPropagation ) {
+			e.stopPropagation();
+		}
+		// otherwise set the cancelBubble property of the original event to true (IE)
+		e.cancelBubble = true;
+	},
+	stopImmediatePropagation: function() {
+		this.isImmediatePropagationStopped = returnTrue;
+		this.stopPropagation();
+	},
+	isDefaultPrevented: returnFalse,
+	isPropagationStopped: returnFalse,
+	isImmediatePropagationStopped: returnFalse
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+	mouseenter: "mouseover",
+	mouseleave: "mouseout"
+}, function( orig, fix ) {
+	jQuery.event.special[ orig ] = {
+		delegateType: fix,
+		bindType: fix,
+
+		handle: function( event ) {
+			var target = this,
+				related = event.relatedTarget,
+				handleObj = event.handleObj,
+				selector = handleObj.selector,
+				ret;
+
+			// For mousenter/leave call the handler if related is outside the target.
+			// NB: No relatedTarget if the mouse left/entered the browser window
+			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+				event.type = handleObj.origType;
+				ret = handleObj.handler.apply( this, arguments );
+				event.type = fix;
+			}
+			return ret;
+		}
+	};
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+	jQuery.event.special.submit = {
+		setup: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Lazy-add a submit handler when a descendant form may potentially be submitted
+			jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+				// Node name check avoids a VML-related crash in IE (#9807)
+				var elem = e.target,
+					form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+				if ( form && !form._submit_attached ) {
+					jQuery.event.add( form, "submit._submit", function( event ) {
+						// If form was submitted by the user, bubble the event up the tree
+						if ( this.parentNode && !event.isTrigger ) {
+							jQuery.event.simulate( "submit", this.parentNode, event, true );
+						}
+					});
+					form._submit_attached = true;
+				}
+			});
+			// return undefined since we don't need an event listener
+		},
+
+		teardown: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+			jQuery.event.remove( this, "._submit" );
+		}
+	};
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+	jQuery.event.special.change = {
+
+		setup: function() {
+
+			if ( rformElems.test( this.nodeName ) ) {
+				// IE doesn't fire change on a check/radio until blur; trigger it on click
+				// after a propertychange. Eat the blur-change in special.change.handle.
+				// This still fires onchange a second time for check/radio after blur.
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					jQuery.event.add( this, "propertychange._change", function( event ) {
+						if ( event.originalEvent.propertyName === "checked" ) {
+							this._just_changed = true;
+						}
+					});
+					jQuery.event.add( this, "click._change", function( event ) {
+						if ( this._just_changed && !event.isTrigger ) {
+							this._just_changed = false;
+							jQuery.event.simulate( "change", this, event, true );
+						}
+					});
+				}
+				return false;
+			}
+			// Delegated event; lazy-add a change handler on descendant inputs
+			jQuery.event.add( this, "beforeactivate._change", function( e ) {
+				var elem = e.target;
+
+				if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) {
+					jQuery.event.add( elem, "change._change", function( event ) {
+						if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+							jQuery.event.simulate( "change", this.parentNode, event, true );
+						}
+					});
+					elem._change_attached = true;
+				}
+			});
+		},
+
+		handle: function( event ) {
+			var elem = event.target;
+
+			// Swallow native change events from checkbox/radio, we already triggered them above
+			if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+				return event.handleObj.handler.apply( this, arguments );
+			}
+		},
+
+		teardown: function() {
+			jQuery.event.remove( this, "._change" );
+
+			return rformElems.test( this.nodeName );
+		}
+	};
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+		// Attach a single capturing handler while someone wants focusin/focusout
+		var attaches = 0,
+			handler = function( event ) {
+				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+			};
+
+		jQuery.event.special[ fix ] = {
+			setup: function() {
+				if ( attaches++ === 0 ) {
+					document.addEventListener( orig, handler, true );
+				}
+			},
+			teardown: function() {
+				if ( --attaches === 0 ) {
+					document.removeEventListener( orig, handler, true );
+				}
+			}
+		};
+	});
+}
+
+jQuery.fn.extend({
+
+	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+		var origFn, type;
+
+		// Types can be a map of types/handlers
+		if ( typeof types === "object" ) {
+			// ( types-Object, selector, data )
+			if ( typeof selector !== "string" ) {
+				// ( types-Object, data )
+				data = selector;
+				selector = undefined;
+			}
+			for ( type in types ) {
+				this.on( type, selector, data, types[ type ], one );
+			}
+			return this;
+		}
+
+		if ( data == null && fn == null ) {
+			// ( types, fn )
+			fn = selector;
+			data = selector = undefined;
+		} else if ( fn == null ) {
+			if ( typeof selector === "string" ) {
+				// ( types, selector, fn )
+				fn = data;
+				data = undefined;
+			} else {
+				// ( types, data, fn )
+				fn = data;
+				data = selector;
+				selector = undefined;
+			}
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		} else if ( !fn ) {
+			return this;
+		}
+
+		if ( one === 1 ) {
+			origFn = fn;
+			fn = function( event ) {
+				// Can use an empty set, since event contains the info
+				jQuery().off( event );
+				return origFn.apply( this, arguments );
+			};
+			// Use same guid so caller can remove using origFn
+			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+		}
+		return this.each( function() {
+			jQuery.event.add( this, types, fn, data, selector );
+		});
+	},
+	one: function( types, selector, data, fn ) {
+		return this.on.call( this, types, selector, data, fn, 1 );
+	},
+	off: function( types, selector, fn ) {
+		if ( types && types.preventDefault && types.handleObj ) {
+			// ( event )  dispatched jQuery.Event
+			var handleObj = types.handleObj;
+			jQuery( types.delegateTarget ).off(
+				handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type,
+				handleObj.selector,
+				handleObj.handler
+			);
+			return this;
+		}
+		if ( typeof types === "object" ) {
+			// ( types-object [, selector] )
+			for ( var type in types ) {
+				this.off( type, selector, types[ type ] );
+			}
+			return this;
+		}
+		if ( selector === false || typeof selector === "function" ) {
+			// ( types [, fn] )
+			fn = selector;
+			selector = undefined;
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		}
+		return this.each(function() {
+			jQuery.event.remove( this, types, fn, selector );
+		});
+	},
+
+	bind: function( types, data, fn ) {
+		return this.on( types, null, data, fn );
+	},
+	unbind: function( types, fn ) {
+		return this.off( types, null, fn );
+	},
+
+	live: function( types, data, fn ) {
+		jQuery( this.context ).on( types, this.selector, data, fn );
+		return this;
+	},
+	die: function( types, fn ) {
+		jQuery( this.context ).off( types, this.selector || "**", fn );
+		return this;
+	},
+
+	delegate: function( selector, types, data, fn ) {
+		return this.on( types, selector, data, fn );
+	},
+	undelegate: function( selector, types, fn ) {
+		// ( namespace ) or ( selector, types [, fn] )
+		return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn );
+	},
+
+	trigger: function( type, data ) {
+		return this.each(function() {
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	triggerHandler: function( type, data ) {
+		if ( this[0] ) {
+			return jQuery.event.trigger( type, data, this[0], true );
+		}
+	},
+
+	toggle: function( fn ) {
+		// Save reference to arguments for access in closure
+		var args = arguments,
+			guid = fn.guid || jQuery.guid++,
+			i = 0,
+			toggler = function( event ) {
+				// Figure out which function to execute
+				var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+				jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+				// Make sure that clicks stop
+				event.preventDefault();
+
+				// and execute the function
+				return args[ lastToggle ].apply( this, arguments ) || false;
+			};
+
+		// link all the functions, so any of them can unbind this click handler
+		toggler.guid = guid;
+		while ( i < args.length ) {
+			args[ i++ ].guid = guid;
+		}
+
+		return this.click( toggler );
+	},
+
+	hover: function( fnOver, fnOut ) {
+		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+	}
+});
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+	// Handle event binding
+	jQuery.fn[ name ] = function( data, fn ) {
+		if ( fn == null ) {
+			fn = data;
+			data = null;
+		}
+
+		return arguments.length > 0 ?
+			this.on( name, null, data, fn ) :
+			this.trigger( name );
+	};
+
+	if ( jQuery.attrFn ) {
+		jQuery.attrFn[ name ] = true;
+	}
+
+	if ( rkeyEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;
+	}
+
+	if ( rmouseEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;
+	}
+});
+
+
+
+/*!
+ * Sizzle CSS Selector Engine
+ *  Copyright 2011, The Dojo Foundation
+ *  Released under the MIT, BSD, and GPL Licenses.
+ *  More information: http://sizzlejs.com/
+ */
+(function(){
+
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+	expando = "sizcache" + (Math.random() + '').replace('.', ''),
+	done = 0,
+	toString = Object.prototype.toString,
+	hasDuplicate = false,
+	baseHasDuplicate = true,
+	rBackslash = /\\/g,
+	rReturn = /\r\n/g,
+	rNonWord = /\W/;
+
+// Here we check if the JavaScript engine is using some sort of
+// optimization where it does not always call our comparision
+// function. If that is the case, discard the hasDuplicate value.
+//   Thus far that includes Google Chrome.
+[0, 0].sort(function() {
+	baseHasDuplicate = false;
+	return 0;
+});
+
+var Sizzle = function( selector, context, results, seed ) {
+	results = results || [];
+	context = context || document;
+
+	var origContext = context;
+
+	if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
+		return [];
+	}
+	
+	if ( !selector || typeof selector !== "string" ) {
+		return results;
+	}
+
+	var m, set, checkSet, extra, ret, cur, pop, i,
+		prune = true,
+		contextXML = Sizzle.isXML( context ),
+		parts = [],
+		soFar = selector;
+	
+	// Reset the position of the chunker regexp (start from head)
+	do {
+		chunker.exec( "" );
+		m = chunker.exec( soFar );
+
+		if ( m ) {
+			soFar = m[3];
+		
+			parts.push( m[1] );
+		
+			if ( m[2] ) {
+				extra = m[3];
+				break;
+			}
+		}
+	} while ( m );
+
+	if ( parts.length > 1 && origPOS.exec( selector ) ) {
+
+		if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
+			set = posProcess( parts[0] + parts[1], context, seed );
+
+		} else {
+			set = Expr.relative[ parts[0] ] ?
+				[ context ] :
+				Sizzle( parts.shift(), context );
+
+			while ( parts.length ) {
+				selector = parts.shift();
+
+				if ( Expr.relative[ selector ] ) {
+					selector += parts.shift();
+				}
+				
+				set = posProcess( selector, set, seed );
+			}
+		}
+
+	} else {
+		// Take a shortcut and set the context if the root selector is an ID
+		// (but not if it'll be faster if the inner selector is an ID)
+		if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
+				Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
+
+			ret = Sizzle.find( parts.shift(), context, contextXML );
+			context = ret.expr ?
+				Sizzle.filter( ret.expr, ret.set )[0] :
+				ret.set[0];
+		}
+
+		if ( context ) {
+			ret = seed ?
+				{ expr: parts.pop(), set: makeArray(seed) } :
+				Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
+
+			set = ret.expr ?
+				Sizzle.filter( ret.expr, ret.set ) :
+				ret.set;
+
+			if ( parts.length > 0 ) {
+				checkSet = makeArray( set );
+
+			} else {
+				prune = false;
+			}
+
+			while ( parts.length ) {
+				cur = parts.pop();
+				pop = cur;
+
+				if ( !Expr.relative[ cur ] ) {
+					cur = "";
+				} else {
+					pop = parts.pop();
+				}
+
+				if ( pop == null ) {
+					pop = context;
+				}
+
+				Expr.relative[ cur ]( checkSet, pop, contextXML );
+			}
+
+		} else {
+			checkSet = parts = [];
+		}
+	}
+
+	if ( !checkSet ) {
+		checkSet = set;
+	}
+
+	if ( !checkSet ) {
+		Sizzle.error( cur || selector );
+	}
+
+	if ( toString.call(checkSet) === "[object Array]" ) {
+		if ( !prune ) {
+			results.push.apply( results, checkSet );
+
+		} else if ( context && context.nodeType === 1 ) {
+			for ( i = 0; checkSet[i] != null; i++ ) {
+				if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
+					results.push( set[i] );
+				}
+			}
+
+		} else {
+			for ( i = 0; checkSet[i] != null; i++ ) {
+				if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+					results.push( set[i] );
+				}
+			}
+		}
+
+	} else {
+		makeArray( checkSet, results );
+	}
+
+	if ( extra ) {
+		Sizzle( extra, origContext, results, seed );
+		Sizzle.uniqueSort( results );
+	}
+
+	return results;
+};
+
+Sizzle.uniqueSort = function( results ) {
+	if ( sortOrder ) {
+		hasDuplicate = baseHasDuplicate;
+		results.sort( sortOrder );
+
+		if ( hasDuplicate ) {
+			for ( var i = 1; i < results.length; i++ ) {
+				if ( results[i] === results[ i - 1 ] ) {
+					results.splice( i--, 1 );
+				}
+			}
+		}
+	}
+
+	return results;
+};
+
+Sizzle.matches = function( expr, set ) {
+	return Sizzle( expr, null, null, set );
+};
+
+Sizzle.matchesSelector = function( node, expr ) {
+	return Sizzle( expr, null, null, [node] ).length > 0;
+};
+
+Sizzle.find = function( expr, context, isXML ) {
+	var set, i, len, match, type, left;
+
+	if ( !expr ) {
+		return [];
+	}
+
+	for ( i = 0, len = Expr.order.length; i < len; i++ ) {
+		type = Expr.order[i];
+		
+		if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
+			left = match[1];
+			match.splice( 1, 1 );
+
+			if ( left.substr( left.length - 1 ) !== "\\" ) {
+				match[1] = (match[1] || "").replace( rBackslash, "" );
+				set = Expr.find[ type ]( match, context, isXML );
+
+				if ( set != null ) {
+					expr = expr.replace( Expr.match[ type ], "" );
+					break;
+				}
+			}
+		}
+	}
+
+	if ( !set ) {
+		set = typeof context.getElementsByTagName !== "undefined" ?
+			context.getElementsByTagName( "*" ) :
+			[];
+	}
+
+	return { set: set, expr: expr };
+};
+
+Sizzle.filter = function( expr, set, inplace, not ) {
+	var match, anyFound,
+		type, found, item, filter, left,
+		i, pass,
+		old = expr,
+		result = [],
+		curLoop = set,
+		isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
+
+	while ( expr && set.length ) {
+		for ( type in Expr.filter ) {
+			if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
+				filter = Expr.filter[ type ];
+				left = match[1];
+
+				anyFound = false;
+
+				match.splice(1,1);
+
+				if ( left.substr( left.length - 1 ) === "\\" ) {
+					continue;
+				}
+
+				if ( curLoop === result ) {
+					result = [];
+				}
+
+				if ( Expr.preFilter[ type ] ) {
+					match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+
+					if ( !match ) {
+						anyFound = found = true;
+
+					} else if ( match === true ) {
+						continue;
+					}
+				}
+
+				if ( match ) {
+					for ( i = 0; (item = curLoop[i]) != null; i++ ) {
+						if ( item ) {
+							found = filter( item, match, i, curLoop );
+							pass = not ^ found;
+
+							if ( inplace && found != null ) {
+								if ( pass ) {
+									anyFound = true;
+
+								} else {
+									curLoop[i] = false;
+								}
+
+							} else if ( pass ) {
+								result.push( item );
+								anyFound = true;
+							}
+						}
+					}
+				}
+
+				if ( found !== undefined ) {
+					if ( !inplace ) {
+						curLoop = result;
+					}
+
+					expr = expr.replace( Expr.match[ type ], "" );
+
+					if ( !anyFound ) {
+						return [];
+					}
+
+					break;
+				}
+			}
+		}
+
+		// Improper expression
+		if ( expr === old ) {
+			if ( anyFound == null ) {
+				Sizzle.error( expr );
+
+			} else {
+				break;
+			}
+		}
+
+		old = expr;
+	}
+
+	return curLoop;
+};
+
+Sizzle.error = function( msg ) {
+	throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Utility function for retreiving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+var getText = Sizzle.getText = function( elem ) {
+    var i, node,
+		nodeType = elem.nodeType,
+		ret = "";
+
+	if ( nodeType ) {
+		if ( nodeType === 1 || nodeType === 9 ) {
+			// Use textContent || innerText for elements
+			if ( typeof elem.textContent === 'string' ) {
+				return elem.textContent;
+			} else if ( typeof elem.innerText === 'string' ) {
+				// Replace IE's carriage returns
+				return elem.innerText.replace( rReturn, '' );
+			} else {
+				// Traverse it's children
+				for ( elem = elem.firstChild; elem; elem = elem.nextSibling) {
+					ret += getText( elem );
+				}
+			}
+		} else if ( nodeType === 3 || nodeType === 4 ) {
+			return elem.nodeValue;
+		}
+	} else {
+
+		// If no nodeType, this is expected to be an array
+		for ( i = 0; (node = elem[i]); i++ ) {
+			// Do not traverse comment nodes
+			if ( node.nodeType !== 8 ) {
+				ret += getText( node );
+			}
+		}
+	}
+	return ret;
+};
+
+var Expr = Sizzle.selectors = {
+	order: [ "ID", "NAME", "TAG" ],
+
+	match: {
+		ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
+		CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
+		NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
+		ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
+		TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
+		CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
+		POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
+		PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
+	},
+
+	leftMatch: {},
+
+	attrMap: {
+		"class": "className",
+		"for": "htmlFor"
+	},
+
+	attrHandle: {
+		href: function( elem ) {
+			return elem.getAttribute( "href" );
+		},
+		type: function( elem ) {
+			return elem.getAttribute( "type" );
+		}
+	},
+
+	relative: {
+		"+": function(checkSet, part){
+			var isPartStr = typeof part === "string",
+				isTag = isPartStr && !rNonWord.test( part ),
+				isPartStrNotTag = isPartStr && !isTag;
+
+			if ( isTag ) {
+				part = part.toLowerCase();
+			}
+
+			for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
+				if ( (elem = checkSet[i]) ) {
+					while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
+
+					checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
+						elem || false :
+						elem === part;
+				}
+			}
+
+			if ( isPartStrNotTag ) {
+				Sizzle.filter( part, checkSet, true );
+			}
+		},
+
+		">": function( checkSet, part ) {
+			var elem,
+				isPartStr = typeof part === "string",
+				i = 0,
+				l = checkSet.length;
+
+			if ( isPartStr && !rNonWord.test( part ) ) {
+				part = part.toLowerCase();
+
+				for ( ; i < l; i++ ) {
+					elem = checkSet[i];
+
+					if ( elem ) {
+						var parent = elem.parentNode;
+						checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
+					}
+				}
+
+			} else {
+				for ( ; i < l; i++ ) {
+					elem = checkSet[i];
+
+					if ( elem ) {
+						checkSet[i] = isPartStr ?
+							elem.parentNode :
+							elem.parentNode === part;
+					}
+				}
+
+				if ( isPartStr ) {
+					Sizzle.filter( part, checkSet, true );
+				}
+			}
+		},
+
+		"": function(checkSet, part, isXML){
+			var nodeCheck,
+				doneName = done++,
+				checkFn = dirCheck;
+
+			if ( typeof part === "string" && !rNonWord.test( part ) ) {
+				part = part.toLowerCase();
+				nodeCheck = part;
+				checkFn = dirNodeCheck;
+			}
+
+			checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
+		},
+
+		"~": function( checkSet, part, isXML ) {
+			var nodeCheck,
+				doneName = done++,
+				checkFn = dirCheck;
+
+			if ( typeof part === "string" && !rNonWord.test( part ) ) {
+				part = part.toLowerCase();
+				nodeCheck = part;
+				checkFn = dirNodeCheck;
+			}
+
+			checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
+		}
+	},
+
+	find: {
+		ID: function( match, context, isXML ) {
+			if ( typeof context.getElementById !== "undefined" && !isXML ) {
+				var m = context.getElementById(match[1]);
+				// Check parentNode to catch when Blackberry 4.6 returns
+				// nodes that are no longer in the document #6963
+				return m && m.parentNode ? [m] : [];
+			}
+		},
+
+		NAME: function( match, context ) {
+			if ( typeof context.getElementsByName !== "undefined" ) {
+				var ret = [],
+					results = context.getElementsByName( match[1] );
+
+				for ( var i = 0, l = results.length; i < l; i++ ) {
+					if ( results[i].getAttribute("name") === match[1] ) {
+						ret.push( results[i] );
+					}
+				}
+
+				return ret.length === 0 ? null : ret;
+			}
+		},
+
+		TAG: function( match, context ) {
+			if ( typeof context.getElementsByTagName !== "undefined" ) {
+				return context.getElementsByTagName( match[1] );
+			}
+		}
+	},
+	preFilter: {
+		CLASS: function( match, curLoop, inplace, result, not, isXML ) {
+			match = " " + match[1].replace( rBackslash, "" ) + " ";
+
+			if ( isXML ) {
+				return match;
+			}
+
+			for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
+				if ( elem ) {
+					if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
+						if ( !inplace ) {
+							result.push( elem );
+						}
+
+					} else if ( inplace ) {
+						curLoop[i] = false;
+					}
+				}
+			}
+
+			return false;
+		},
+
+		ID: function( match ) {
+			return match[1].replace( rBackslash, "" );
+		},
+
+		TAG: function( match, curLoop ) {
+			return match[1].replace( rBackslash, "" ).toLowerCase();
+		},
+
+		CHILD: function( match ) {
+			if ( match[1] === "nth" ) {
+				if ( !match[2] ) {
+					Sizzle.error( match[0] );
+				}
+
+				match[2] = match[2].replace(/^\+|\s*/g, '');
+
+				// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
+				var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
+					match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
+					!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
+
+				// calculate the numbers (first)n+(last) including if they are negative
+				match[2] = (test[1] + (test[2] || 1)) - 0;
+				match[3] = test[3] - 0;
+			}
+			else if ( match[2] ) {
+				Sizzle.error( match[0] );
+			}
+
+			// TODO: Move to normal caching system
+			match[0] = done++;
+
+			return match;
+		},
+
+		ATTR: function( match, curLoop, inplace, result, not, isXML ) {
+			var name = match[1] = match[1].replace( rBackslash, "" );
+			
+			if ( !isXML && Expr.attrMap[name] ) {
+				match[1] = Expr.attrMap[name];
+			}
+
+			// Handle if an un-quoted value was used
+			match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );
+
+			if ( match[2] === "~=" ) {
+				match[4] = " " + match[4] + " ";
+			}
+
+			return match;
+		},
+
+		PSEUDO: function( match, curLoop, inplace, result, not ) {
+			if ( match[1] === "not" ) {
+				// If we're dealing with a complex expression, or a simple one
+				if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
+					match[3] = Sizzle(match[3], null, null, curLoop);
+
+				} else {
+					var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
+
+					if ( !inplace ) {
+						result.push.apply( result, ret );
+					}
+
+					return false;
+				}
+
+			} else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
+				return true;
+			}
+			
+			return match;
+		},
+
+		POS: function( match ) {
+			match.unshift( true );
+
+			return match;
+		}
+	},
+	
+	filters: {
+		enabled: function( elem ) {
+			return elem.disabled === false && elem.type !== "hidden";
+		},
+
+		disabled: function( elem ) {
+			return elem.disabled === true;
+		},
+
+		checked: function( elem ) {
+			return elem.checked === true;
+		},
+		
+		selected: function( elem ) {
+			// Accessing this property makes selected-by-default
+			// options in Safari work properly
+			if ( elem.parentNode ) {
+				elem.parentNode.selectedIndex;
+			}
+			
+			return elem.selected === true;
+		},
+
+		parent: function( elem ) {
+			return !!elem.firstChild;
+		},
+
+		empty: function( elem ) {
+			return !elem.firstChild;
+		},
+
+		has: function( elem, i, match ) {
+			return !!Sizzle( match[3], elem ).length;
+		},
+
+		header: function( elem ) {
+			return (/h\d/i).test( elem.nodeName );
+		},
+
+		text: function( elem ) {
+			var attr = elem.getAttribute( "type" ), type = elem.type;
+			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) 
+			// use getAttribute instead to test this case
+			return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null );
+		},
+
+		radio: function( elem ) {
+			return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type;
+		},
+
+		checkbox: function( elem ) {
+			return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type;
+		},
+
+		file: function( elem ) {
+			return elem.nodeName.toLowerCase() === "input" && "file" === elem.type;
+		},
+
+		password: function( elem ) {
+			return elem.nodeName.toLowerCase() === "input" && "password" === elem.type;
+		},
+
+		submit: function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return (name === "input" || name === "button") && "submit" === elem.type;
+		},
+
+		image: function( elem ) {
+			return elem.nodeName.toLowerCase() === "input" && "image" === elem.type;
+		},
+
+		reset: function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return (name === "input" || name === "button") && "reset" === elem.type;
+		},
+
+		button: function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return name === "input" && "button" === elem.type || name === "button";
+		},
+
+		input: function( elem ) {
+			return (/input|select|textarea|button/i).test( elem.nodeName );
+		},
+
+		focus: function( elem ) {
+			return elem === elem.ownerDocument.activeElement;
+		}
+	},
+	setFilters: {
+		first: function( elem, i ) {
+			return i === 0;
+		},
+
+		last: function( elem, i, match, array ) {
+			return i === array.length - 1;
+		},
+
+		even: function( elem, i ) {
+			return i % 2 === 0;
+		},
+
+		odd: function( elem, i ) {
+			return i % 2 === 1;
+		},
+
+		lt: function( elem, i, match ) {
+			return i < match[3] - 0;
+		},
+
+		gt: function( elem, i, match ) {
+			return i > match[3] - 0;
+		},
+
+		nth: function( elem, i, match ) {
+			return match[3] - 0 === i;
+		},
+
+		eq: function( elem, i, match ) {
+			return match[3] - 0 === i;
+		}
+	},
+	filter: {
+		PSEUDO: function( elem, match, i, array ) {
+			var name = match[1],
+				filter = Expr.filters[ name ];
+
+			if ( filter ) {
+				return filter( elem, i, match, array );
+
+			} else if ( name === "contains" ) {
+				return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
+
+			} else if ( name === "not" ) {
+				var not = match[3];
+
+				for ( var j = 0, l = not.length; j < l; j++ ) {
+					if ( not[j] === elem ) {
+						return false;
+					}
+				}
+
+				return true;
+
+			} else {
+				Sizzle.error( name );
+			}
+		},
+
+		CHILD: function( elem, match ) {
+			var first, last,
+				doneName, parent, cache,
+				count, diff,
+				type = match[1],
+				node = elem;
+
+			switch ( type ) {
+				case "only":
+				case "first":
+					while ( (node = node.previousSibling) )	 {
+						if ( node.nodeType === 1 ) { 
+							return false; 
+						}
+					}
+
+					if ( type === "first" ) { 
+						return true; 
+					}
+
+					node = elem;
+
+				case "last":
+					while ( (node = node.nextSibling) )	 {
+						if ( node.nodeType === 1 ) { 
+							return false; 
+						}
+					}
+
+					return true;
+
+				case "nth":
+					first = match[2];
+					last = match[3];
+
+					if ( first === 1 && last === 0 ) {
+						return true;
+					}
+					
+					doneName = match[0];
+					parent = elem.parentNode;
+	
+					if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) {
+						count = 0;
+						
+						for ( node = parent.firstChild; node; node = node.nextSibling ) {
+							if ( node.nodeType === 1 ) {
+								node.nodeIndex = ++count;
+							}
+						} 
+
+						parent[ expando ] = doneName;
+					}
+					
+					diff = elem.nodeIndex - last;
+
+					if ( first === 0 ) {
+						return diff === 0;
+
+					} else {
+						return ( diff % first === 0 && diff / first >= 0 );
+					}
+			}
+		},
+
+		ID: function( elem, match ) {
+			return elem.nodeType === 1 && elem.getAttribute("id") === match;
+		},
+
+		TAG: function( elem, match ) {
+			return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match;
+		},
+		
+		CLASS: function( elem, match ) {
+			return (" " + (elem.className || elem.getAttribute("class")) + " ")
+				.indexOf( match ) > -1;
+		},
+
+		ATTR: function( elem, match ) {
+			var name = match[1],
+				result = Sizzle.attr ?
+					Sizzle.attr( elem, name ) :
+					Expr.attrHandle[ name ] ?
+					Expr.attrHandle[ name ]( elem ) :
+					elem[ name ] != null ?
+						elem[ name ] :
+						elem.getAttribute( name ),
+				value = result + "",
+				type = match[2],
+				check = match[4];
+
+			return result == null ?
+				type === "!=" :
+				!type && Sizzle.attr ?
+				result != null :
+				type === "=" ?
+				value === check :
+				type === "*=" ?
+				value.indexOf(check) >= 0 :
+				type === "~=" ?
+				(" " + value + " ").indexOf(check) >= 0 :
+				!check ?
+				value && result !== false :
+				type === "!=" ?
+				value !== check :
+				type === "^=" ?
+				value.indexOf(check) === 0 :
+				type === "$=" ?
+				value.substr(value.length - check.length) === check :
+				type === "|=" ?
+				value === check || value.substr(0, check.length + 1) === check + "-" :
+				false;
+		},
+
+		POS: function( elem, match, i, array ) {
+			var name = match[2],
+				filter = Expr.setFilters[ name ];
+
+			if ( filter ) {
+				return filter( elem, i, match, array );
+			}
+		}
+	}
+};
+
+var origPOS = Expr.match.POS,
+	fescape = function(all, num){
+		return "\\" + (num - 0 + 1);
+	};
+
+for ( var type in Expr.match ) {
+	Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
+	Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
+}
+
+var makeArray = function( array, results ) {
+	array = Array.prototype.slice.call( array, 0 );
+
+	if ( results ) {
+		results.push.apply( results, array );
+		return results;
+	}
+	
+	return array;
+};
+
+// Perform a simple check to determine if the browser is capable of
+// converting a NodeList to an array using builtin methods.
+// Also verifies that the returned array holds DOM nodes
+// (which is not the case in the Blackberry browser)
+try {
+	Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
+
+// Provide a fallback method if it does not work
+} catch( e ) {
+	makeArray = function( array, results ) {
+		var i = 0,
+			ret = results || [];
+
+		if ( toString.call(array) === "[object Array]" ) {
+			Array.prototype.push.apply( ret, array );
+
+		} else {
+			if ( typeof array.length === "number" ) {
+				for ( var l = array.length; i < l; i++ ) {
+					ret.push( array[i] );
+				}
+
+			} else {
+				for ( ; array[i]; i++ ) {
+					ret.push( array[i] );
+				}
+			}
+		}
+
+		return ret;
+	};
+}
+
+var sortOrder, siblingCheck;
+
+if ( document.documentElement.compareDocumentPosition ) {
+	sortOrder = function( a, b ) {
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+
+		if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
+			return a.compareDocumentPosition ? -1 : 1;
+		}
+
+		return a.compareDocumentPosition(b) & 4 ? -1 : 1;
+	};
+
+} else {
+	sortOrder = function( a, b ) {
+		// The nodes are identical, we can exit early
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+
+		// Fallback to using sourceIndex (in IE) if it's available on both nodes
+		} else if ( a.sourceIndex && b.sourceIndex ) {
+			return a.sourceIndex - b.sourceIndex;
+		}
+
+		var al, bl,
+			ap = [],
+			bp = [],
+			aup = a.parentNode,
+			bup = b.parentNode,
+			cur = aup;
+
+		// If the nodes are siblings (or identical) we can do a quick check
+		if ( aup === bup ) {
+			return siblingCheck( a, b );
+
+		// If no parents were found then the nodes are disconnected
+		} else if ( !aup ) {
+			return -1;
+
+		} else if ( !bup ) {
+			return 1;
+		}
+
+		// Otherwise they're somewhere else in the tree so we need
+		// to build up a full list of the parentNodes for comparison
+		while ( cur ) {
+			ap.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		cur = bup;
+
+		while ( cur ) {
+			bp.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		al = ap.length;
+		bl = bp.length;
+
+		// Start walking down the tree looking for a discrepancy
+		for ( var i = 0; i < al && i < bl; i++ ) {
+			if ( ap[i] !== bp[i] ) {
+				return siblingCheck( ap[i], bp[i] );
+			}
+		}
+
+		// We ended someplace up the tree so do a sibling check
+		return i === al ?
+			siblingCheck( a, bp[i], -1 ) :
+			siblingCheck( ap[i], b, 1 );
+	};
+
+	siblingCheck = function( a, b, ret ) {
+		if ( a === b ) {
+			return ret;
+		}
+
+		var cur = a.nextSibling;
+
+		while ( cur ) {
+			if ( cur === b ) {
+				return -1;
+			}
+
+			cur = cur.nextSibling;
+		}
+
+		return 1;
+	};
+}
+
+// Check to see if the browser returns elements by name when
+// querying by getElementById (and provide a workaround)
+(function(){
+	// We're going to inject a fake input element with a specified name
+	var form = document.createElement("div"),
+		id = "script" + (new Date()).getTime(),
+		root = document.documentElement;
+
+	form.innerHTML = "<a name='" + id + "'/>";
+
+	// Inject it into the root element, check its status, and remove it quickly
+	root.insertBefore( form, root.firstChild );
+
+	// The workaround has to do additional checks after a getElementById
+	// Which slows things down for other browsers (hence the branching)
+	if ( document.getElementById( id ) ) {
+		Expr.find.ID = function( match, context, isXML ) {
+			if ( typeof context.getElementById !== "undefined" && !isXML ) {
+				var m = context.getElementById(match[1]);
+
+				return m ?
+					m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
+						[m] :
+						undefined :
+					[];
+			}
+		};
+
+		Expr.filter.ID = function( elem, match ) {
+			var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
+
+			return elem.nodeType === 1 && node && node.nodeValue === match;
+		};
+	}
+
+	root.removeChild( form );
+
+	// release memory in IE
+	root = form = null;
+})();
+
+(function(){
+	// Check to see if the browser returns only elements
+	// when doing getElementsByTagName("*")
+
+	// Create a fake element
+	var div = document.createElement("div");
+	div.appendChild( document.createComment("") );
+
+	// Make sure no comments are found
+	if ( div.getElementsByTagName("*").length > 0 ) {
+		Expr.find.TAG = function( match, context ) {
+			var results = context.getElementsByTagName( match[1] );
+
+			// Filter out possible comments
+			if ( match[1] === "*" ) {
+				var tmp = [];
+
+				for ( var i = 0; results[i]; i++ ) {
+					if ( results[i].nodeType === 1 ) {
+						tmp.push( results[i] );
+					}
+				}
+
+				results = tmp;
+			}
+
+			return results;
+		};
+	}
+
+	// Check to see if an attribute returns normalized href attributes
+	div.innerHTML = "<a href='#'></a>";
+
+	if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
+			div.firstChild.getAttribute("href") !== "#" ) {
+
+		Expr.attrHandle.href = function( elem ) {
+			return elem.getAttribute( "href", 2 );
+		};
+	}
+
+	// release memory in IE
+	div = null;
+})();
+
+if ( document.querySelectorAll ) {
+	(function(){
+		var oldSizzle = Sizzle,
+			div = document.createElement("div"),
+			id = "__sizzle__";
+
+		div.innerHTML = "<p class='TEST'></p>";
+
+		// Safari can't handle uppercase or unicode characters when
+		// in quirks mode.
+		if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+			return;
+		}
+	
+		Sizzle = function( query, context, extra, seed ) {
+			context = context || document;
+
+			// Only use querySelectorAll on non-XML documents
+			// (ID selectors don't work in non-HTML documents)
+			if ( !seed && !Sizzle.isXML(context) ) {
+				// See if we find a selector to speed up
+				var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
+				
+				if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
+					// Speed-up: Sizzle("TAG")
+					if ( match[1] ) {
+						return makeArray( context.getElementsByTagName( query ), extra );
+					
+					// Speed-up: Sizzle(".CLASS")
+					} else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
+						return makeArray( context.getElementsByClassName( match[2] ), extra );
+					}
+				}
+				
+				if ( context.nodeType === 9 ) {
+					// Speed-up: Sizzle("body")
+					// The body element only exists once, optimize finding it
+					if ( query === "body" && context.body ) {
+						return makeArray( [ context.body ], extra );
+						
+					// Speed-up: Sizzle("#ID")
+					} else if ( match && match[3] ) {
+						var elem = context.getElementById( match[3] );
+
+						// Check parentNode to catch when Blackberry 4.6 returns
+						// nodes that are no longer in the document #6963
+						if ( elem && elem.parentNode ) {
+							// Handle the case where IE and Opera return items
+							// by name instead of ID
+							if ( elem.id === match[3] ) {
+								return makeArray( [ elem ], extra );
+							}
+							
+						} else {
+							return makeArray( [], extra );
+						}
+					}
+					
+					try {
+						return makeArray( context.querySelectorAll(query), extra );
+					} catch(qsaError) {}
+
+				// qSA works strangely on Element-rooted queries
+				// We can work around this by specifying an extra ID on the root
+				// and working up from there (Thanks to Andrew Dupont for the technique)
+				// IE 8 doesn't work on object elements
+				} else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+					var oldContext = context,
+						old = context.getAttribute( "id" ),
+						nid = old || id,
+						hasParent = context.parentNode,
+						relativeHierarchySelector = /^\s*[+~]/.test( query );
+
+					if ( !old ) {
+						context.setAttribute( "id", nid );
+					} else {
+						nid = nid.replace( /'/g, "\\$&" );
+					}
+					if ( relativeHierarchySelector && hasParent ) {
+						context = context.parentNode;
+					}
+
+					try {
+						if ( !relativeHierarchySelector || hasParent ) {
+							return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
+						}
+
+					} catch(pseudoError) {
+					} finally {
+						if ( !old ) {
+							oldContext.removeAttribute( "id" );
+						}
+					}
+				}
+			}
+		
+			return oldSizzle(query, context, extra, seed);
+		};
+
+		for ( var prop in oldSizzle ) {
+			Sizzle[ prop ] = oldSizzle[ prop ];
+		}
+
+		// release memory in IE
+		div = null;
+	})();
+}
+
+(function(){
+	var html = document.documentElement,
+		matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
+
+	if ( matches ) {
+		// Check to see if it's possible to do matchesSelector
+		// on a disconnected node (IE 9 fails this)
+		var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
+			pseudoWorks = false;
+
+		try {
+			// This should fail with an exception
+			// Gecko does not error, returns false instead
+			matches.call( document.documentElement, "[test!='']:sizzle" );
+	
+		} catch( pseudoError ) {
+			pseudoWorks = true;
+		}
+
+		Sizzle.matchesSelector = function( node, expr ) {
+			// Make sure that attribute selectors are quoted
+			expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
+
+			if ( !Sizzle.isXML( node ) ) {
+				try { 
+					if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
+						var ret = matches.call( node, expr );
+
+						// IE 9's matchesSelector returns false on disconnected nodes
+						if ( ret || !disconnectedMatch ||
+								// As well, disconnected nodes are said to be in a document
+								// fragment in IE 9, so check for that
+								node.document && node.document.nodeType !== 11 ) {
+							return ret;
+						}
+					}
+				} catch(e) {}
+			}
+
+			return Sizzle(expr, null, null, [node]).length > 0;
+		};
+	}
+})();
+
+(function(){
+	var div = document.createElement("div");
+
+	div.innerHTML = "<div class='test e'></div><div class='test'></div>";
+
+	// Opera can't find a second classname (in 9.6)
+	// Also, make sure that getElementsByClassName actually exists
+	if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
+		return;
+	}
+
+	// Safari caches class attributes, doesn't catch changes (in 3.2)
+	div.lastChild.className = "e";
+
+	if ( div.getElementsByClassName("e").length === 1 ) {
+		return;
+	}
+	
+	Expr.order.splice(1, 0, "CLASS");
+	Expr.find.CLASS = function( match, context, isXML ) {
+		if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
+			return context.getElementsByClassName(match[1]);
+		}
+	};
+
+	// release memory in IE
+	div = null;
+})();
+
+function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+		var elem = checkSet[i];
+
+		if ( elem ) {
+			var match = false;
+
+			elem = elem[dir];
+
+			while ( elem ) {
+				if ( elem[ expando ] === doneName ) {
+					match = checkSet[elem.sizset];
+					break;
+				}
+
+				if ( elem.nodeType === 1 && !isXML ){
+					elem[ expando ] = doneName;
+					elem.sizset = i;
+				}
+
+				if ( elem.nodeName.toLowerCase() === cur ) {
+					match = elem;
+					break;
+				}
+
+				elem = elem[dir];
+			}
+
+			checkSet[i] = match;
+		}
+	}
+}
+
+function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+	for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+		var elem = checkSet[i];
+
+		if ( elem ) {
+			var match = false;
+			
+			elem = elem[dir];
+
+			while ( elem ) {
+				if ( elem[ expando ] === doneName ) {
+					match = checkSet[elem.sizset];
+					break;
+				}
+
+				if ( elem.nodeType === 1 ) {
+					if ( !isXML ) {
+						elem[ expando ] = doneName;
+						elem.sizset = i;
+					}
+
+					if ( typeof cur !== "string" ) {
+						if ( elem === cur ) {
+							match = true;
+							break;
+						}
+
+					} else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
+						match = elem;
+						break;
+					}
+				}
+
+				elem = elem[dir];
+			}
+
+			checkSet[i] = match;
+		}
+	}
+}
+
+if ( document.documentElement.contains ) {
+	Sizzle.contains = function( a, b ) {
+		return a !== b && (a.contains ? a.contains(b) : true);
+	};
+
+} else if ( document.documentElement.compareDocumentPosition ) {
+	Sizzle.contains = function( a, b ) {
+		return !!(a.compareDocumentPosition(b) & 16);
+	};
+
+} else {
+	Sizzle.contains = function() {
+		return false;
+	};
+}
+
+Sizzle.isXML = function( elem ) {
+	// documentElement is verified for cases where it doesn't yet exist
+	// (such as loading iframes in IE - #4833) 
+	var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
+
+	return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+var posProcess = function( selector, context, seed ) {
+	var match,
+		tmpSet = [],
+		later = "",
+		root = context.nodeType ? [context] : context;
+
+	// Position selectors must be done after the filter
+	// And so must :not(positional) so we move all PSEUDOs to the end
+	while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
+		later += match[0];
+		selector = selector.replace( Expr.match.PSEUDO, "" );
+	}
+
+	selector = Expr.relative[selector] ? selector + "*" : selector;
+
+	for ( var i = 0, l = root.length; i < l; i++ ) {
+		Sizzle( selector, root[i], tmpSet, seed );
+	}
+
+	return Sizzle.filter( later, tmpSet );
+};
+
+// EXPOSE
+// Override sizzle attribute retrieval
+Sizzle.attr = jQuery.attr;
+Sizzle.selectors.attrMap = {};
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.filters;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})();
+
+
+var runtil = /Until$/,
+	rparentsprev = /^(?:parents|prevUntil|prevAll)/,
+	// Note: This RegExp should be improved, or likely pulled from Sizzle
+	rmultiselector = /,/,
+	isSimple = /^.[^:#\[\.,]*$/,
+	slice = Array.prototype.slice,
+	POS = jQuery.expr.match.POS,
+	// methods guaranteed to produce a unique set when starting from a unique set
+	guaranteedUnique = {
+		children: true,
+		contents: true,
+		next: true,
+		prev: true
+	};
+
+jQuery.fn.extend({
+	find: function( selector ) {
+		var self = this,
+			i, l;
+
+		if ( typeof selector !== "string" ) {
+			return jQuery( selector ).filter(function() {
+				for ( i = 0, l = self.length; i < l; i++ ) {
+					if ( jQuery.contains( self[ i ], this ) ) {
+						return true;
+					}
+				}
+			});
+		}
+
+		var ret = this.pushStack( "", "find", selector ),
+			length, n, r;
+
+		for ( i = 0, l = this.length; i < l; i++ ) {
+			length = ret.length;
+			jQuery.find( selector, this[i], ret );
+
+			if ( i > 0 ) {
+				// Make sure that the results are unique
+				for ( n = length; n < ret.length; n++ ) {
+					for ( r = 0; r < length; r++ ) {
+						if ( ret[r] === ret[n] ) {
+							ret.splice(n--, 1);
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	has: function( target ) {
+		var targets = jQuery( target );
+		return this.filter(function() {
+			for ( var i = 0, l = targets.length; i < l; i++ ) {
+				if ( jQuery.contains( this, targets[i] ) ) {
+					return true;
+				}
+			}
+		});
+	},
+
+	not: function( selector ) {
+		return this.pushStack( winnow(this, selector, false), "not", selector);
+	},
+
+	filter: function( selector ) {
+		return this.pushStack( winnow(this, selector, true), "filter", selector );
+	},
+
+	is: function( selector ) {
+		return !!selector && ( 
+			typeof selector === "string" ?
+				// If this is a positional selector, check membership in the returned set
+				// so $("p:first").is("p:last") won't return true for a doc with two "p".
+				POS.test( selector ) ? 
+					jQuery( selector, this.context ).index( this[0] ) >= 0 :
+					jQuery.filter( selector, this ).length > 0 :
+				this.filter( selector ).length > 0 );
+	},
+
+	closest: function( selectors, context ) {
+		var ret = [], i, l, cur = this[0];
+		
+		// Array (deprecated as of jQuery 1.7)
+		if ( jQuery.isArray( selectors ) ) {
+			var level = 1;
+
+			while ( cur && cur.ownerDocument && cur !== context ) {
+				for ( i = 0; i < selectors.length; i++ ) {
+
+					if ( jQuery( cur ).is( selectors[ i ] ) ) {
+						ret.push({ selector: selectors[ i ], elem: cur, level: level });
+					}
+				}
+
+				cur = cur.parentNode;
+				level++;
+			}
+
+			return ret;
+		}
+
+		// String
+		var pos = POS.test( selectors ) || typeof selectors !== "string" ?
+				jQuery( selectors, context || this.context ) :
+				0;
+
+		for ( i = 0, l = this.length; i < l; i++ ) {
+			cur = this[i];
+
+			while ( cur ) {
+				if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+					ret.push( cur );
+					break;
+
+				} else {
+					cur = cur.parentNode;
+					if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) {
+						break;
+					}
+				}
+			}
+		}
+
+		ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
+
+		return this.pushStack( ret, "closest", selectors );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+
+		// No argument, return index in parent
+		if ( !elem ) {
+			return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;
+		}
+
+		// index in selector
+		if ( typeof elem === "string" ) {
+			return jQuery.inArray( this[0], jQuery( elem ) );
+		}
+
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem.jquery ? elem[0] : elem, this );
+	},
+
+	add: function( selector, context ) {
+		var set = typeof selector === "string" ?
+				jQuery( selector, context ) :
+				jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+			all = jQuery.merge( this.get(), set );
+
+		return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+			all :
+			jQuery.unique( all ) );
+	},
+
+	andSelf: function() {
+		return this.add( this.prevObject );
+	}
+});
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+	return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+jQuery.each({
+	parent: function( elem ) {
+		var parent = elem.parentNode;
+		return parent && parent.nodeType !== 11 ? parent : null;
+	},
+	parents: function( elem ) {
+		return jQuery.dir( elem, "parentNode" );
+	},
+	parentsUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "parentNode", until );
+	},
+	next: function( elem ) {
+		return jQuery.nth( elem, 2, "nextSibling" );
+	},
+	prev: function( elem ) {
+		return jQuery.nth( elem, 2, "previousSibling" );
+	},
+	nextAll: function( elem ) {
+		return jQuery.dir( elem, "nextSibling" );
+	},
+	prevAll: function( elem ) {
+		return jQuery.dir( elem, "previousSibling" );
+	},
+	nextUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "nextSibling", until );
+	},
+	prevUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "previousSibling", until );
+	},
+	siblings: function( elem ) {
+		return jQuery.sibling( elem.parentNode.firstChild, elem );
+	},
+	children: function( elem ) {
+		return jQuery.sibling( elem.firstChild );
+	},
+	contents: function( elem ) {
+		return jQuery.nodeName( elem, "iframe" ) ?
+			elem.contentDocument || elem.contentWindow.document :
+			jQuery.makeArray( elem.childNodes );
+	}
+}, function( name, fn ) {
+	jQuery.fn[ name ] = function( until, selector ) {
+		var ret = jQuery.map( this, fn, until );
+
+		if ( !runtil.test( name ) ) {
+			selector = until;
+		}
+
+		if ( selector && typeof selector === "string" ) {
+			ret = jQuery.filter( selector, ret );
+		}
+
+		ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+		if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+			ret = ret.reverse();
+		}
+
+		return this.pushStack( ret, name, slice.call( arguments ).join(",") );
+	};
+});
+
+jQuery.extend({
+	filter: function( expr, elems, not ) {
+		if ( not ) {
+			expr = ":not(" + expr + ")";
+		}
+
+		return elems.length === 1 ?
+			jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+			jQuery.find.matches(expr, elems);
+	},
+
+	dir: function( elem, dir, until ) {
+		var matched = [],
+			cur = elem[ dir ];
+
+		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+			if ( cur.nodeType === 1 ) {
+				matched.push( cur );
+			}
+			cur = cur[dir];
+		}
+		return matched;
+	},
+
+	nth: function( cur, result, dir, elem ) {
+		result = result || 1;
+		var num = 0;
+
+		for ( ; cur; cur = cur[dir] ) {
+			if ( cur.nodeType === 1 && ++num === result ) {
+				break;
+			}
+		}
+
+		return cur;
+	},
+
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType === 1 && n !== elem ) {
+				r.push( n );
+			}
+		}
+
+		return r;
+	}
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+	// Can't pass null or undefined to indexOf in Firefox 4
+	// Set to 0 to skip string check
+	qualifier = qualifier || 0;
+
+	if ( jQuery.isFunction( qualifier ) ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			var retVal = !!qualifier.call( elem, i, elem );
+			return retVal === keep;
+		});
+
+	} else if ( qualifier.nodeType ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			return ( elem === qualifier ) === keep;
+		});
+
+	} else if ( typeof qualifier === "string" ) {
+		var filtered = jQuery.grep(elements, function( elem ) {
+			return elem.nodeType === 1;
+		});
+
+		if ( isSimple.test( qualifier ) ) {
+			return jQuery.filter(qualifier, filtered, !keep);
+		} else {
+			qualifier = jQuery.filter( qualifier, filtered );
+		}
+	}
+
+	return jQuery.grep(elements, function( elem, i ) {
+		return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
+	});
+}
+
+
+
+
+function createSafeFragment( document ) {
+	var list = nodeNames.split( "|" ),
+	safeFrag = document.createDocumentFragment();
+
+	if ( safeFrag.createElement ) {
+		while ( list.length ) {
+			safeFrag.createElement(
+				list.pop()
+			);
+		}
+	}
+	return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" +
+		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+	rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
+	rleadingWhitespace = /^\s+/,
+	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
+	rtagName = /<([\w:]+)/,
+	rtbody = /<tbody/i,
+	rhtml = /<|&#?\w+;/,
+	rnoInnerhtml = /<(?:script|style)/i,
+	rnocache = /<(?:script|object|embed|option|style)/i,
+	rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"),
+	// checked="checked" or checked
+	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+	rscriptType = /\/(java|ecma)script/i,
+	rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/,
+	wrapMap = {
+		option: [ 1, "<select multiple='multiple'>", "</select>" ],
+		legend: [ 1, "<fieldset>", "</fieldset>" ],
+		thead: [ 1, "<table>", "</table>" ],
+		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+		area: [ 1, "<map>", "</map>" ],
+		_default: [ 0, "", "" ]
+	},
+	safeFragment = createSafeFragment( document );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE can't serialize <link> and <script> tags normally
+if ( !jQuery.support.htmlSerialize ) {
+	wrapMap._default = [ 1, "div<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+	text: function( text ) {
+		if ( jQuery.isFunction(text) ) {
+			return this.each(function(i) {
+				var self = jQuery( this );
+
+				self.text( text.call(this, i, self.text()) );
+			});
+		}
+
+		if ( typeof text !== "object" && text !== undefined ) {
+			return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+		}
+
+		return jQuery.text( this );
+	},
+
+	wrapAll: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapAll( html.call(this, i) );
+			});
+		}
+
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+			if ( this[0].parentNode ) {
+				wrap.insertBefore( this[0] );
+			}
+
+			wrap.map(function() {
+				var elem = this;
+
+				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+					elem = elem.firstChild;
+				}
+
+				return elem;
+			}).append( this );
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapInner( html.call(this, i) );
+			});
+		}
+
+		return this.each(function() {
+			var self = jQuery( this ),
+				contents = self.contents();
+
+			if ( contents.length ) {
+				contents.wrapAll( html );
+
+			} else {
+				self.append( html );
+			}
+		});
+	},
+
+	wrap: function( html ) {
+		var isFunction = jQuery.isFunction( html );
+
+		return this.each(function(i) {
+			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+		});
+	},
+
+	unwrap: function() {
+		return this.parent().each(function() {
+			if ( !jQuery.nodeName( this, "body" ) ) {
+				jQuery( this ).replaceWith( this.childNodes );
+			}
+		}).end();
+	},
+
+	append: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 ) {
+				this.appendChild( elem );
+			}
+		});
+	},
+
+	prepend: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 ) {
+				this.insertBefore( elem, this.firstChild );
+			}
+		});
+	},
+
+	before: function() {
+		if ( this[0] && this[0].parentNode ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this );
+			});
+		} else if ( arguments.length ) {
+			var set = jQuery.clean( arguments );
+			set.push.apply( set, this.toArray() );
+			return this.pushStack( set, "before", arguments );
+		}
+	},
+
+	after: function() {
+		if ( this[0] && this[0].parentNode ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this.nextSibling );
+			});
+		} else if ( arguments.length ) {
+			var set = this.pushStack( this, "after", arguments );
+			set.push.apply( set, jQuery.clean(arguments) );
+			return set;
+		}
+	},
+
+	// keepData is for internal use only--do not document
+	remove: function( selector, keepData ) {
+		for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+			if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+				if ( !keepData && elem.nodeType === 1 ) {
+					jQuery.cleanData( elem.getElementsByTagName("*") );
+					jQuery.cleanData( [ elem ] );
+				}
+
+				if ( elem.parentNode ) {
+					elem.parentNode.removeChild( elem );
+				}
+			}
+		}
+
+		return this;
+	},
+
+	empty: function() {
+		for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+			// Remove element nodes and prevent memory leaks
+			if ( elem.nodeType === 1 ) {
+				jQuery.cleanData( elem.getElementsByTagName("*") );
+			}
+
+			// Remove any remaining nodes
+			while ( elem.firstChild ) {
+				elem.removeChild( elem.firstChild );
+			}
+		}
+
+		return this;
+	},
+
+	clone: function( dataAndEvents, deepDataAndEvents ) {
+		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+		return this.map( function () {
+			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+		});
+	},
+
+	html: function( value ) {
+		if ( value === undefined ) {
+			return this[0] && this[0].nodeType === 1 ?
+				this[0].innerHTML.replace(rinlinejQuery, "") :
+				null;
+
+		// See if we can take a shortcut and just use innerHTML
+		} else if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+			(jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
+			!wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
+
+			value = value.replace(rxhtmlTag, "<$1></$2>");
+
+			try {
+				for ( var i = 0, l = this.length; i < l; i++ ) {
+					// Remove element nodes and prevent memory leaks
+					if ( this[i].nodeType === 1 ) {
+						jQuery.cleanData( this[i].getElementsByTagName("*") );
+						this[i].innerHTML = value;
+					}
+				}
+
+			// If using innerHTML throws an exception, use the fallback method
+			} catch(e) {
+				this.empty().append( value );
+			}
+
+		} else if ( jQuery.isFunction( value ) ) {
+			this.each(function(i){
+				var self = jQuery( this );
+
+				self.html( value.call(this, i, self.html()) );
+			});
+
+		} else {
+			this.empty().append( value );
+		}
+
+		return this;
+	},
+
+	replaceWith: function( value ) {
+		if ( this[0] && this[0].parentNode ) {
+			// Make sure that the elements are removed from the DOM before they are inserted
+			// this can help fix replacing a parent with child elements
+			if ( jQuery.isFunction( value ) ) {
+				return this.each(function(i) {
+					var self = jQuery(this), old = self.html();
+					self.replaceWith( value.call( this, i, old ) );
+				});
+			}
+
+			if ( typeof value !== "string" ) {
+				value = jQuery( value ).detach();
+			}
+
+			return this.each(function() {
+				var next = this.nextSibling,
+					parent = this.parentNode;
+
+				jQuery( this ).remove();
+
+				if ( next ) {
+					jQuery(next).before( value );
+				} else {
+					jQuery(parent).append( value );
+				}
+			});
+		} else {
+			return this.length ?
+				this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+				this;
+		}
+	},
+
+	detach: function( selector ) {
+		return this.remove( selector, true );
+	},
+
+	domManip: function( args, table, callback ) {
+		var results, first, fragment, parent,
+			value = args[0],
+			scripts = [];
+
+		// We can't cloneNode fragments that contain checked, in WebKit
+		if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
+			return this.each(function() {
+				jQuery(this).domManip( args, table, callback, true );
+			});
+		}
+
+		if ( jQuery.isFunction(value) ) {
+			return this.each(function(i) {
+				var self = jQuery(this);
+				args[0] = value.call(this, i, table ? self.html() : undefined);
+				self.domManip( args, table, callback );
+			});
+		}
+
+		if ( this[0] ) {
+			parent = value && value.parentNode;
+
+			// If we're in a fragment, just use that instead of building a new one
+			if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
+				results = { fragment: parent };
+
+			} else {
+				results = jQuery.buildFragment( args, this, scripts );
+			}
+
+			fragment = results.fragment;
+
+			if ( fragment.childNodes.length === 1 ) {
+				first = fragment = fragment.firstChild;
+			} else {
+				first = fragment.firstChild;
+			}
+
+			if ( first ) {
+				table = table && jQuery.nodeName( first, "tr" );
+
+				for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
+					callback.call(
+						table ?
+							root(this[i], first) :
+							this[i],
+						// Make sure that we do not leak memory by inadvertently discarding
+						// the original fragment (which might have attached data) instead of
+						// using it; in addition, use the original fragment object for the last
+						// item instead of first because it can end up being emptied incorrectly
+						// in certain situations (Bug #8070).
+						// Fragments from the fragment cache must always be cloned and never used
+						// in place.
+						results.cacheable || ( l > 1 && i < lastIndex ) ?
+							jQuery.clone( fragment, true, true ) :
+							fragment
+					);
+				}
+			}
+
+			if ( scripts.length ) {
+				jQuery.each( scripts, evalScript );
+			}
+		}
+
+		return this;
+	}
+});
+
+function root( elem, cur ) {
+	return jQuery.nodeName(elem, "table") ?
+		(elem.getElementsByTagName("tbody")[0] ||
+		elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+		elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+
+	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+		return;
+	}
+
+	var type, i, l,
+		oldData = jQuery._data( src ),
+		curData = jQuery._data( dest, oldData ),
+		events = oldData.events;
+
+	if ( events ) {
+		delete curData.handle;
+		curData.events = {};
+
+		for ( type in events ) {
+			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+				jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data );
+			}
+		}
+	}
+
+	// make the cloned public data object a copy from the original
+	if ( curData.data ) {
+		curData.data = jQuery.extend( {}, curData.data );
+	}
+}
+
+function cloneFixAttributes( src, dest ) {
+	var nodeName;
+
+	// We do not need to do anything for non-Elements
+	if ( dest.nodeType !== 1 ) {
+		return;
+	}
+
+	// clearAttributes removes the attributes, which we don't want,
+	// but also removes the attachEvent events, which we *do* want
+	if ( dest.clearAttributes ) {
+		dest.clearAttributes();
+	}
+
+	// mergeAttributes, in contrast, only merges back on the
+	// original attributes, not the events
+	if ( dest.mergeAttributes ) {
+		dest.mergeAttributes( src );
+	}
+
+	nodeName = dest.nodeName.toLowerCase();
+
+	// IE6-8 fail to clone children inside object elements that use
+	// the proprietary classid attribute value (rather than the type
+	// attribute) to identify the type of content to display
+	if ( nodeName === "object" ) {
+		dest.outerHTML = src.outerHTML;
+
+	} else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) {
+		// IE6-8 fails to persist the checked state of a cloned checkbox
+		// or radio button. Worse, IE6-7 fail to give the cloned element
+		// a checked appearance if the defaultChecked value isn't also set
+		if ( src.checked ) {
+			dest.defaultChecked = dest.checked = src.checked;
+		}
+
+		// IE6-7 get confused and end up setting the value of a cloned
+		// checkbox/radio button to an empty string instead of "on"
+		if ( dest.value !== src.value ) {
+			dest.value = src.value;
+		}
+
+	// IE6-8 fails to return the selected option to the default selected
+	// state when cloning options
+	} else if ( nodeName === "option" ) {
+		dest.selected = src.defaultSelected;
+
+	// IE6-8 fails to set the defaultValue to the correct value when
+	// cloning other types of input fields
+	} else if ( nodeName === "input" || nodeName === "textarea" ) {
+		dest.defaultValue = src.defaultValue;
+	}
+
+	// Event data gets referenced instead of copied if the expando
+	// gets copied too
+	dest.removeAttribute( jQuery.expando );
+}
+
+jQuery.buildFragment = function( args, nodes, scripts ) {
+	var fragment, cacheable, cacheresults, doc,
+	first = args[ 0 ];
+
+	// nodes may contain either an explicit document object,
+	// a jQuery collection or context object.
+	// If nodes[0] contains a valid object to assign to doc
+	if ( nodes && nodes[0] ) {
+		doc = nodes[0].ownerDocument || nodes[0];
+	}
+
+	// Ensure that an attr object doesn't incorrectly stand in as a document object
+	// Chrome and Firefox seem to allow this to occur and will throw exception
+	// Fixes #8950
+	if ( !doc.createDocumentFragment ) {
+		doc = document;
+	}
+
+	// Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+	// Cloning options loses the selected state, so don't cache them
+	// IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+	// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+	// Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501
+	if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document &&
+		first.charAt(0) === "<" && !rnocache.test( first ) &&
+		(jQuery.support.checkClone || !rchecked.test( first )) &&
+		(jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {
+
+		cacheable = true;
+
+		cacheresults = jQuery.fragments[ first ];
+		if ( cacheresults && cacheresults !== 1 ) {
+			fragment = cacheresults;
+		}
+	}
+
+	if ( !fragment ) {
+		fragment = doc.createDocumentFragment();
+		jQuery.clean( args, doc, fragment, scripts );
+	}
+
+	if ( cacheable ) {
+		jQuery.fragments[ first ] = cacheresults ? fragment : 1;
+	}
+
+	return { fragment: fragment, cacheable: cacheable };
+};
+
+jQuery.fragments = {};
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function( name, original ) {
+	jQuery.fn[ name ] = function( selector ) {
+		var ret = [],
+			insert = jQuery( selector ),
+			parent = this.length === 1 && this[0].parentNode;
+
+		if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+			insert[ original ]( this[0] );
+			return this;
+
+		} else {
+			for ( var i = 0, l = insert.length; i < l; i++ ) {
+				var elems = ( i > 0 ? this.clone(true) : this ).get();
+				jQuery( insert[i] )[ original ]( elems );
+				ret = ret.concat( elems );
+			}
+
+			return this.pushStack( ret, name, insert.selector );
+		}
+	};
+});
+
+function getAll( elem ) {
+	if ( typeof elem.getElementsByTagName !== "undefined" ) {
+		return elem.getElementsByTagName( "*" );
+
+	} else if ( typeof elem.querySelectorAll !== "undefined" ) {
+		return elem.querySelectorAll( "*" );
+
+	} else {
+		return [];
+	}
+}
+
+// Used in clean, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+	if ( elem.type === "checkbox" || elem.type === "radio" ) {
+		elem.defaultChecked = elem.checked;
+	}
+}
+// Finds all inputs and passes them to fixDefaultChecked
+function findInputs( elem ) {
+	var nodeName = ( elem.nodeName || "" ).toLowerCase();
+	if ( nodeName === "input" ) {
+		fixDefaultChecked( elem );
+	// Skip scripts, get other children
+	} else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) {
+		jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
+	}
+}
+
+// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js
+function shimCloneNode( elem ) {
+	var div = document.createElement( "div" );
+	safeFragment.appendChild( div );
+
+	div.innerHTML = elem.outerHTML;
+	return div.firstChild;
+}
+
+jQuery.extend({
+	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+		var srcElements,
+			destElements,
+			i,
+			// IE<=8 does not properly clone detached, unknown element nodes
+			clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ?
+				elem.cloneNode( true ) :
+				shimCloneNode( elem );
+
+		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+			// IE copies events bound via attachEvent when using cloneNode.
+			// Calling detachEvent on the clone will also remove the events
+			// from the original. In order to get around this, we use some
+			// proprietary methods to clear the events. Thanks to MooTools
+			// guys for this hotness.
+
+			cloneFixAttributes( elem, clone );
+
+			// Using Sizzle here is crazy slow, so we use getElementsByTagName instead
+			srcElements = getAll( elem );
+			destElements = getAll( clone );
+
+			// Weird iteration because IE will replace the length property
+			// with an element if you are cloning the body and one of the
+			// elements on the page has a name or id of "length"
+			for ( i = 0; srcElements[i]; ++i ) {
+				// Ensure that the destination node is not null; Fixes #9587
+				if ( destElements[i] ) {
+					cloneFixAttributes( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		// Copy the events from the original to the clone
+		if ( dataAndEvents ) {
+			cloneCopyEvent( elem, clone );
+
+			if ( deepDataAndEvents ) {
+				srcElements = getAll( elem );
+				destElements = getAll( clone );
+
+				for ( i = 0; srcElements[i]; ++i ) {
+					cloneCopyEvent( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		srcElements = destElements = null;
+
+		// Return the cloned set
+		return clone;
+	},
+
+	clean: function( elems, context, fragment, scripts ) {
+		var checkScriptType;
+
+		context = context || document;
+
+		// !context.createElement fails in IE with an error but returns typeof 'object'
+		if ( typeof context.createElement === "undefined" ) {
+			context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+		}
+
+		var ret = [], j;
+
+		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+			if ( typeof elem === "number" ) {
+				elem += "";
+			}
+
+			if ( !elem ) {
+				continue;
+			}
+
+			// Convert html string into DOM nodes
+			if ( typeof elem === "string" ) {
+				if ( !rhtml.test( elem ) ) {
+					elem = context.createTextNode( elem );
+				} else {
+					// Fix "XHTML"-style tags in all browsers
+					elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+					// Trim whitespace, otherwise indexOf won't work as expected
+					var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(),
+						wrap = wrapMap[ tag ] || wrapMap._default,
+						depth = wrap[0],
+						div = context.createElement("div");
+
+					// Append wrapper element to unknown element safe doc fragment
+					if ( context === document ) {
+						// Use the fragment we've already created for this document
+						safeFragment.appendChild( div );
+					} else {
+						// Use a fragment created with the owner document
+						createSafeFragment( context ).appendChild( div );
+					}
+
+					// Go to html and back, then peel off extra wrappers
+					div.innerHTML = wrap[1] + elem + wrap[2];
+
+					// Move to the right depth
+					while ( depth-- ) {
+						div = div.lastChild;
+					}
+
+					// Remove IE's autoinserted <tbody> from table fragments
+					if ( !jQuery.support.tbody ) {
+
+						// String was a <table>, *may* have spurious <tbody>
+						var hasBody = rtbody.test(elem),
+							tbody = tag === "table" && !hasBody ?
+								div.firstChild && div.firstChild.childNodes :
+
+								// String was a bare <thead> or <tfoot>
+								wrap[1] === "<table>" && !hasBody ?
+									div.childNodes :
+									[];
+
+						for ( j = tbody.length - 1; j >= 0 ; --j ) {
+							if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+								tbody[ j ].parentNode.removeChild( tbody[ j ] );
+							}
+						}
+					}
+
+					// IE completely kills leading whitespace when innerHTML is used
+					if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+						div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+					}
+
+					elem = div.childNodes;
+				}
+			}
+
+			// Resets defaultChecked for any radios and checkboxes
+			// about to be appended to the DOM in IE 6/7 (#8060)
+			var len;
+			if ( !jQuery.support.appendChecked ) {
+				if ( elem[0] && typeof (len = elem.length) === "number" ) {
+					for ( j = 0; j < len; j++ ) {
+						findInputs( elem[j] );
+					}
+				} else {
+					findInputs( elem );
+				}
+			}
+
+			if ( elem.nodeType ) {
+				ret.push( elem );
+			} else {
+				ret = jQuery.merge( ret, elem );
+			}
+		}
+
+		if ( fragment ) {
+			checkScriptType = function( elem ) {
+				return !elem.type || rscriptType.test( elem.type );
+			};
+			for ( i = 0; ret[i]; i++ ) {
+				if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+					scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+
+				} else {
+					if ( ret[i].nodeType === 1 ) {
+						var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType );
+
+						ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
+					}
+					fragment.appendChild( ret[i] );
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	cleanData: function( elems ) {
+		var data, id,
+			cache = jQuery.cache,
+			special = jQuery.event.special,
+			deleteExpando = jQuery.support.deleteExpando;
+
+		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+			if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+				continue;
+			}
+
+			id = elem[ jQuery.expando ];
+
+			if ( id ) {
+				data = cache[ id ];
+
+				if ( data && data.events ) {
+					for ( var type in data.events ) {
+						if ( special[ type ] ) {
+							jQuery.event.remove( elem, type );
+
+						// This is a shortcut to avoid jQuery.event.remove's overhead
+						} else {
+							jQuery.removeEvent( elem, type, data.handle );
+						}
+					}
+
+					// Null the DOM reference to avoid IE6/7/8 leak (#7054)
+					if ( data.handle ) {
+						data.handle.elem = null;
+					}
+				}
+
+				if ( deleteExpando ) {
+					delete elem[ jQuery.expando ];
+
+				} else if ( elem.removeAttribute ) {
+					elem.removeAttribute( jQuery.expando );
+				}
+
+				delete cache[ id ];
+			}
+		}
+	}
+});
+
+function evalScript( i, elem ) {
+	if ( elem.src ) {
+		jQuery.ajax({
+			url: elem.src,
+			async: false,
+			dataType: "script"
+		});
+	} else {
+		jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) );
+	}
+
+	if ( elem.parentNode ) {
+		elem.parentNode.removeChild( elem );
+	}
+}
+
+
+
+
+var ralpha = /alpha\([^)]*\)/i,
+	ropacity = /opacity=([^)]*)/,
+	// fixed for IE9, see #8346
+	rupper = /([A-Z]|^ms)/g,
+	rnumpx = /^-?\d+(?:px)?$/i,
+	rnum = /^-?\d/,
+	rrelNum = /^([\-+])=([\-+.\de]+)/,
+
+	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+	cssWidth = [ "Left", "Right" ],
+	cssHeight = [ "Top", "Bottom" ],
+	curCSS,
+
+	getComputedStyle,
+	currentStyle;
+
+jQuery.fn.css = function( name, value ) {
+	// Setting 'undefined' is a no-op
+	if ( arguments.length === 2 && value === undefined ) {
+		return this;
+	}
+
+	return jQuery.access( this, name, value, true, function( elem, name, value ) {
+		return value !== undefined ?
+			jQuery.style( elem, name, value ) :
+			jQuery.css( elem, name );
+	});
+};
+
+jQuery.extend({
+	// Add in style property hooks for overriding the default
+	// behavior of getting and setting a style property
+	cssHooks: {
+		opacity: {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// We should always get a number back from opacity
+					var ret = curCSS( elem, "opacity", "opacity" );
+					return ret === "" ? "1" : ret;
+
+				} else {
+					return elem.style.opacity;
+				}
+			}
+		}
+	},
+
+	// Exclude the following css properties to add px
+	cssNumber: {
+		"fillOpacity": true,
+		"fontWeight": true,
+		"lineHeight": true,
+		"opacity": true,
+		"orphans": true,
+		"widows": true,
+		"zIndex": true,
+		"zoom": true
+	},
+
+	// Add in properties whose names you wish to fix before
+	// setting or getting the value
+	cssProps: {
+		// normalize float css property
+		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+	},
+
+	// Get and set the style property on a DOM Node
+	style: function( elem, name, value, extra ) {
+		// Don't set styles on text and comment nodes
+		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+			return;
+		}
+
+		// Make sure that we're working with the right name
+		var ret, type, origName = jQuery.camelCase( name ),
+			style = elem.style, hooks = jQuery.cssHooks[ origName ];
+
+		name = jQuery.cssProps[ origName ] || origName;
+
+		// Check if we're setting a value
+		if ( value !== undefined ) {
+			type = typeof value;
+
+			// convert relative number strings (+= or -=) to relative numbers. #7345
+			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+				value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) );
+				// Fixes bug #9237
+				type = "number";
+			}
+
+			// Make sure that NaN and null values aren't set. See: #7116
+			if ( value == null || type === "number" && isNaN( value ) ) {
+				return;
+			}
+
+			// If a number was passed in, add 'px' to the (except for certain CSS properties)
+			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+				value += "px";
+			}
+
+			// If a hook was provided, use that value, otherwise just set the specified value
+			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {
+				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+				// Fixes bug #5509
+				try {
+					style[ name ] = value;
+				} catch(e) {}
+			}
+
+		} else {
+			// If a hook was provided get the non-computed value from there
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+				return ret;
+			}
+
+			// Otherwise just get the value from the style object
+			return style[ name ];
+		}
+	},
+
+	css: function( elem, name, extra ) {
+		var ret, hooks;
+
+		// Make sure that we're working with the right name
+		name = jQuery.camelCase( name );
+		hooks = jQuery.cssHooks[ name ];
+		name = jQuery.cssProps[ name ] || name;
+
+		// cssFloat needs a special treatment
+		if ( name === "cssFloat" ) {
+			name = "float";
+		}
+
+		// If a hook was provided get the computed value from there
+		if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
+			return ret;
+
+		// Otherwise, if a way to get the computed value exists, use that
+		} else if ( curCSS ) {
+			return curCSS( elem, name );
+		}
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations
+	swap: function( elem, options, callback ) {
+		var old = {};
+
+		// Remember the old values, and insert the new ones
+		for ( var name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		callback.call( elem );
+
+		// Revert the old values
+		for ( name in options ) {
+			elem.style[ name ] = old[ name ];
+		}
+	}
+});
+
+// DEPRECATED, Use jQuery.css() instead
+jQuery.curCSS = jQuery.css;
+
+jQuery.each(["height", "width"], function( i, name ) {
+	jQuery.cssHooks[ name ] = {
+		get: function( elem, computed, extra ) {
+			var val;
+
+			if ( computed ) {
+				if ( elem.offsetWidth !== 0 ) {
+					return getWH( elem, name, extra );
+				} else {
+					jQuery.swap( elem, cssShow, function() {
+						val = getWH( elem, name, extra );
+					});
+				}
+
+				return val;
+			}
+		},
+
+		set: function( elem, value ) {
+			if ( rnumpx.test( value ) ) {
+				// ignore negative width and height values #1599
+				value = parseFloat( value );
+
+				if ( value >= 0 ) {
+					return value + "px";
+				}
+
+			} else {
+				return value;
+			}
+		}
+	};
+});
+
+if ( !jQuery.support.opacity ) {
+	jQuery.cssHooks.opacity = {
+		get: function( elem, computed ) {
+			// IE uses filters for opacity
+			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+				( parseFloat( RegExp.$1 ) / 100 ) + "" :
+				computed ? "1" : "";
+		},
+
+		set: function( elem, value ) {
+			var style = elem.style,
+				currentStyle = elem.currentStyle,
+				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+				filter = currentStyle && currentStyle.filter || style.filter || "";
+
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			style.zoom = 1;
+
+			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+			if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) {
+
+				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+				// if "filter:" is present at all, clearType is disabled, we want to avoid this
+				// style.removeAttribute is IE Only, but so apparently is this code path...
+				style.removeAttribute( "filter" );
+
+				// if there there is no filter style applied in a css rule, we are done
+				if ( currentStyle && !currentStyle.filter ) {
+					return;
+				}
+			}
+
+			// otherwise, set new filter values
+			style.filter = ralpha.test( filter ) ?
+				filter.replace( ralpha, opacity ) :
+				filter + " " + opacity;
+		}
+	};
+}
+
+jQuery(function() {
+	// This hook cannot be added until DOM ready because the support test
+	// for it is not run until after DOM ready
+	if ( !jQuery.support.reliableMarginRight ) {
+		jQuery.cssHooks.marginRight = {
+			get: function( elem, computed ) {
+				// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+				// Work around by temporarily setting element display to inline-block
+				var ret;
+				jQuery.swap( elem, { "display": "inline-block" }, function() {
+					if ( computed ) {
+						ret = curCSS( elem, "margin-right", "marginRight" );
+					} else {
+						ret = elem.style.marginRight;
+					}
+				});
+				return ret;
+			}
+		};
+	}
+});
+
+if ( document.defaultView && document.defaultView.getComputedStyle ) {
+	getComputedStyle = function( elem, name ) {
+		var ret, defaultView, computedStyle;
+
+		name = name.replace( rupper, "-$1" ).toLowerCase();
+
+		if ( (defaultView = elem.ownerDocument.defaultView) &&
+				(computedStyle = defaultView.getComputedStyle( elem, null )) ) {
+			ret = computedStyle.getPropertyValue( name );
+			if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) {
+				ret = jQuery.style( elem, name );
+			}
+		}
+
+		return ret;
+	};
+}
+
+if ( document.documentElement.currentStyle ) {
+	currentStyle = function( elem, name ) {
+		var left, rsLeft, uncomputed,
+			ret = elem.currentStyle && elem.currentStyle[ name ],
+			style = elem.style;
+
+		// Avoid setting ret to empty string here
+		// so we don't default to auto
+		if ( ret === null && style && (uncomputed = style[ name ]) ) {
+			ret = uncomputed;
+		}
+
+		// From the awesome hack by Dean Edwards
+		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+		// If we're not dealing with a regular pixel number
+		// but a number that has a weird ending, we need to convert it to pixels
+		if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
+
+			// Remember the original values
+			left = style.left;
+			rsLeft = elem.runtimeStyle && elem.runtimeStyle.left;
+
+			// Put in the new values to get a computed value out
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = elem.currentStyle.left;
+			}
+			style.left = name === "fontSize" ? "1em" : ( ret || 0 );
+			ret = style.pixelLeft + "px";
+
+			// Revert the changed values
+			style.left = left;
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = rsLeft;
+			}
+		}
+
+		return ret === "" ? "auto" : ret;
+	};
+}
+
+curCSS = getComputedStyle || currentStyle;
+
+function getWH( elem, name, extra ) {
+
+	// Start with offset property
+	var val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+		which = name === "width" ? cssWidth : cssHeight,
+		i = 0,
+		len = which.length;
+
+	if ( val > 0 ) {
+		if ( extra !== "border" ) {
+			for ( ; i < len; i++ ) {
+				if ( !extra ) {
+					val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0;
+				}
+				if ( extra === "margin" ) {
+					val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0;
+				} else {
+					val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0;
+				}
+			}
+		}
+
+		return val + "px";
+	}
+
+	// Fall back to computed then uncomputed css if necessary
+	val = curCSS( elem, name, name );
+	if ( val < 0 || val == null ) {
+		val = elem.style[ name ] || 0;
+	}
+	// Normalize "", auto, and prepare for extra
+	val = parseFloat( val ) || 0;
+
+	// Add padding, border, margin
+	if ( extra ) {
+		for ( ; i < len; i++ ) {
+			val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0;
+			if ( extra !== "padding" ) {
+				val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0;
+			}
+			if ( extra === "margin" ) {
+				val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0;
+			}
+		}
+	}
+
+	return val + "px";
+}
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.hidden = function( elem ) {
+		var width = elem.offsetWidth,
+			height = elem.offsetHeight;
+
+		return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none");
+	};
+
+	jQuery.expr.filters.visible = function( elem ) {
+		return !jQuery.expr.filters.hidden( elem );
+	};
+}
+
+
+
+
+var r20 = /%20/g,
+	rbracket = /\[\]$/,
+	rCRLF = /\r?\n/g,
+	rhash = /#.*$/,
+	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+	rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
+	// #7653, #8125, #8152: local protocol detection
+	rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,
+	rnoContent = /^(?:GET|HEAD)$/,
+	rprotocol = /^\/\//,
+	rquery = /\?/,
+	rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
+	rselectTextarea = /^(?:select|textarea)/i,
+	rspacesAjax = /\s+/,
+	rts = /([?&])_=[^&]*/,
+	rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,
+
+	// Keep a copy of the old load method
+	_load = jQuery.fn.load,
+
+	/* Prefilters
+	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+	 * 2) These are called:
+	 *    - BEFORE asking for a transport
+	 *    - AFTER param serialization (s.data is a string if s.processData is true)
+	 * 3) key is the dataType
+	 * 4) the catchall symbol "*" can be used
+	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+	 */
+	prefilters = {},
+
+	/* Transports bindings
+	 * 1) key is the dataType
+	 * 2) the catchall symbol "*" can be used
+	 * 3) selection will start with transport dataType and THEN go to "*" if needed
+	 */
+	transports = {},
+
+	// Document location
+	ajaxLocation,
+
+	// Document location segments
+	ajaxLocParts,
+
+	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+	allTypes = ["*/"] + ["*"];
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+	ajaxLocation = location.href;
+} catch( e ) {
+	// Use the href attribute of an A element
+	// since IE will modify it given document.location
+	ajaxLocation = document.createElement( "a" );
+	ajaxLocation.href = "";
+	ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+	// dataTypeExpression is optional and defaults to "*"
+	return function( dataTypeExpression, func ) {
+
+		if ( typeof dataTypeExpression !== "string" ) {
+			func = dataTypeExpression;
+			dataTypeExpression = "*";
+		}
+
+		if ( jQuery.isFunction( func ) ) {
+			var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),
+				i = 0,
+				length = dataTypes.length,
+				dataType,
+				list,
+				placeBefore;
+
+			// For each dataType in the dataTypeExpression
+			for ( ; i < length; i++ ) {
+				dataType = dataTypes[ i ];
+				// We control if we're asked to add before
+				// any existing element
+				placeBefore = /^\+/.test( dataType );
+				if ( placeBefore ) {
+					dataType = dataType.substr( 1 ) || "*";
+				}
+				list = structure[ dataType ] = structure[ dataType ] || [];
+				// then we add to the structure accordingly
+				list[ placeBefore ? "unshift" : "push" ]( func );
+			}
+		}
+	};
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
+		dataType /* internal */, inspected /* internal */ ) {
+
+	dataType = dataType || options.dataTypes[ 0 ];
+	inspected = inspected || {};
+
+	inspected[ dataType ] = true;
+
+	var list = structure[ dataType ],
+		i = 0,
+		length = list ? list.length : 0,
+		executeOnly = ( structure === prefilters ),
+		selection;
+
+	for ( ; i < length && ( executeOnly || !selection ); i++ ) {
+		selection = list[ i ]( options, originalOptions, jqXHR );
+		// If we got redirected to another dataType
+		// we try there if executing only and not done already
+		if ( typeof selection === "string" ) {
+			if ( !executeOnly || inspected[ selection ] ) {
+				selection = undefined;
+			} else {
+				options.dataTypes.unshift( selection );
+				selection = inspectPrefiltersOrTransports(
+						structure, options, originalOptions, jqXHR, selection, inspected );
+			}
+		}
+	}
+	// If we're only executing or nothing was selected
+	// we try the catchall dataType if not done already
+	if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
+		selection = inspectPrefiltersOrTransports(
+				structure, options, originalOptions, jqXHR, "*", inspected );
+	}
+	// unnecessary when only executing (prefilters)
+	// but it'll be ignored by the caller in that case
+	return selection;
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+	var key, deep,
+		flatOptions = jQuery.ajaxSettings.flatOptions || {};
+	for ( key in src ) {
+		if ( src[ key ] !== undefined ) {
+			( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+		}
+	}
+	if ( deep ) {
+		jQuery.extend( true, target, deep );
+	}
+}
+
+jQuery.fn.extend({
+	load: function( url, params, callback ) {
+		if ( typeof url !== "string" && _load ) {
+			return _load.apply( this, arguments );
+
+		// Don't do a request if no elements are being requested
+		} else if ( !this.length ) {
+			return this;
+		}
+
+		var off = url.indexOf( " " );
+		if ( off >= 0 ) {
+			var selector = url.slice( off, url.length );
+			url = url.slice( 0, off );
+		}
+
+		// Default to a GET request
+		var type = "GET";
+
+		// If the second parameter was provided
+		if ( params ) {
+			// If it's a function
+			if ( jQuery.isFunction( params ) ) {
+				// We assume that it's the callback
+				callback = params;
+				params = undefined;
+
+			// Otherwise, build a param string
+			} else if ( typeof params === "object" ) {
+				params = jQuery.param( params, jQuery.ajaxSettings.traditional );
+				type = "POST";
+			}
+		}
+
+		var self = this;
+
+		// Request the remote document
+		jQuery.ajax({
+			url: url,
+			type: type,
+			dataType: "html",
+			data: params,
+			// Complete callback (responseText is used internally)
+			complete: function( jqXHR, status, responseText ) {
+				// Store the response as specified by the jqXHR object
+				responseText = jqXHR.responseText;
+				// If successful, inject the HTML into all the matched elements
+				if ( jqXHR.isResolved() ) {
+					// #4825: Get the actual response in case
+					// a dataFilter is present in ajaxSettings
+					jqXHR.done(function( r ) {
+						responseText = r;
+					});
+					// See if a selector was specified
+					self.html( selector ?
+						// Create a dummy div to hold the results
+						jQuery("<div>")
+							// inject the contents of the document in, removing the scripts
+							// to avoid any 'Permission Denied' errors in IE
+							.append(responseText.replace(rscript, ""))
+
+							// Locate the specified elements
+							.find(selector) :
+
+						// If not, just inject the full result
+						responseText );
+				}
+
+				if ( callback ) {
+					self.each( callback, [ responseText, status, jqXHR ] );
+				}
+			}
+		});
+
+		return this;
+	},
+
+	serialize: function() {
+		return jQuery.param( this.serializeArray() );
+	},
+
+	serializeArray: function() {
+		return this.map(function(){
+			return this.elements ? jQuery.makeArray( this.elements ) : this;
+		})
+		.filter(function(){
+			return this.name && !this.disabled &&
+				( this.checked || rselectTextarea.test( this.nodeName ) ||
+					rinput.test( this.type ) );
+		})
+		.map(function( i, elem ){
+			var val = jQuery( this ).val();
+
+			return val == null ?
+				null :
+				jQuery.isArray( val ) ?
+					jQuery.map( val, function( val, i ){
+						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+					}) :
+					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+		}).get();
+	}
+});
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
+	jQuery.fn[ o ] = function( f ){
+		return this.on( o, f );
+	};
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+	jQuery[ method ] = function( url, data, callback, type ) {
+		// shift arguments if data argument was omitted
+		if ( jQuery.isFunction( data ) ) {
+			type = type || callback;
+			callback = data;
+			data = undefined;
+		}
+
+		return jQuery.ajax({
+			type: method,
+			url: url,
+			data: data,
+			success: callback,
+			dataType: type
+		});
+	};
+});
+
+jQuery.extend({
+
+	getScript: function( url, callback ) {
+		return jQuery.get( url, undefined, callback, "script" );
+	},
+
+	getJSON: function( url, data, callback ) {
+		return jQuery.get( url, data, callback, "json" );
+	},
+
+	// Creates a full fledged settings object into target
+	// with both ajaxSettings and settings fields.
+	// If target is omitted, writes into ajaxSettings.
+	ajaxSetup: function( target, settings ) {
+		if ( settings ) {
+			// Building a settings object
+			ajaxExtend( target, jQuery.ajaxSettings );
+		} else {
+			// Extending ajaxSettings
+			settings = target;
+			target = jQuery.ajaxSettings;
+		}
+		ajaxExtend( target, settings );
+		return target;
+	},
+
+	ajaxSettings: {
+		url: ajaxLocation,
+		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+		global: true,
+		type: "GET",
+		contentType: "application/x-www-form-urlencoded",
+		processData: true,
+		async: true,
+		/*
+		timeout: 0,
+		data: null,
+		dataType: null,
+		username: null,
+		password: null,
+		cache: null,
+		traditional: false,
+		headers: {},
+		*/
+
+		accepts: {
+			xml: "application/xml, text/xml",
+			html: "text/html",
+			text: "text/plain",
+			json: "application/json, text/javascript",
+			"*": allTypes
+		},
+
+		contents: {
+			xml: /xml/,
+			html: /html/,
+			json: /json/
+		},
+
+		responseFields: {
+			xml: "responseXML",
+			text: "responseText"
+		},
+
+		// List of data converters
+		// 1) key format is "source_type destination_type" (a single space in-between)
+		// 2) the catchall symbol "*" can be used for source_type
+		converters: {
+
+			// Convert anything to text
+			"* text": window.String,
+
+			// Text to html (true = no transformation)
+			"text html": true,
+
+			// Evaluate text as a json expression
+			"text json": jQuery.parseJSON,
+
+			// Parse text as xml
+			"text xml": jQuery.parseXML
+		},
+
+		// For options that shouldn't be deep extended:
+		// you can add your own custom options here if
+		// and when you create one that shouldn't be
+		// deep extended (see ajaxExtend)
+		flatOptions: {
+			context: true,
+			url: true
+		}
+	},
+
+	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+	ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+	// Main method
+	ajax: function( url, options ) {
+
+		// If url is an object, simulate pre-1.5 signature
+		if ( typeof url === "object" ) {
+			options = url;
+			url = undefined;
+		}
+
+		// Force options to be an object
+		options = options || {};
+
+		var // Create the final options object
+			s = jQuery.ajaxSetup( {}, options ),
+			// Callbacks context
+			callbackContext = s.context || s,
+			// Context for global events
+			// It's the callbackContext if one was provided in the options
+			// and if it's a DOM node or a jQuery collection
+			globalEventContext = callbackContext !== s &&
+				( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+						jQuery( callbackContext ) : jQuery.event,
+			// Deferreds
+			deferred = jQuery.Deferred(),
+			completeDeferred = jQuery.Callbacks( "once memory" ),
+			// Status-dependent callbacks
+			statusCode = s.statusCode || {},
+			// ifModified key
+			ifModifiedKey,
+			// Headers (they are sent all at once)
+			requestHeaders = {},
+			requestHeadersNames = {},
+			// Response headers
+			responseHeadersString,
+			responseHeaders,
+			// transport
+			transport,
+			// timeout handle
+			timeoutTimer,
+			// Cross-domain detection vars
+			parts,
+			// The jqXHR state
+			state = 0,
+			// To know if global events are to be dispatched
+			fireGlobals,
+			// Loop variable
+			i,
+			// Fake xhr
+			jqXHR = {
+
+				readyState: 0,
+
+				// Caches the header
+				setRequestHeader: function( name, value ) {
+					if ( !state ) {
+						var lname = name.toLowerCase();
+						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+						requestHeaders[ name ] = value;
+					}
+					return this;
+				},
+
+				// Raw string
+				getAllResponseHeaders: function() {
+					return state === 2 ? responseHeadersString : null;
+				},
+
+				// Builds headers hashtable if needed
+				getResponseHeader: function( key ) {
+					var match;
+					if ( state === 2 ) {
+						if ( !responseHeaders ) {
+							responseHeaders = {};
+							while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+							}
+						}
+						match = responseHeaders[ key.toLowerCase() ];
+					}
+					return match === undefined ? null : match;
+				},
+
+				// Overrides response content-type header
+				overrideMimeType: function( type ) {
+					if ( !state ) {
+						s.mimeType = type;
+					}
+					return this;
+				},
+
+				// Cancel the request
+				abort: function( statusText ) {
+					statusText = statusText || "abort";
+					if ( transport ) {
+						transport.abort( statusText );
+					}
+					done( 0, statusText );
+					return this;
+				}
+			};
+
+		// Callback for when everything is done
+		// It is defined here because jslint complains if it is declared
+		// at the end of the function (which would be more logical and readable)
+		function done( status, nativeStatusText, responses, headers ) {
+
+			// Called once
+			if ( state === 2 ) {
+				return;
+			}
+
+			// State is "done" now
+			state = 2;
+
+			// Clear timeout if it exists
+			if ( timeoutTimer ) {
+				clearTimeout( timeoutTimer );
+			}
+
+			// Dereference transport for early garbage collection
+			// (no matter how long the jqXHR object will be used)
+			transport = undefined;
+
+			// Cache response headers
+			responseHeadersString = headers || "";
+
+			// Set readyState
+			jqXHR.readyState = status > 0 ? 4 : 0;
+
+			var isSuccess,
+				success,
+				error,
+				statusText = nativeStatusText,
+				response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,
+				lastModified,
+				etag;
+
+			// If successful, handle type chaining
+			if ( status >= 200 && status < 300 || status === 304 ) {
+
+				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+				if ( s.ifModified ) {
+
+					if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) {
+						jQuery.lastModified[ ifModifiedKey ] = lastModified;
+					}
+					if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) {
+						jQuery.etag[ ifModifiedKey ] = etag;
+					}
+				}
+
+				// If not modified
+				if ( status === 304 ) {
+
+					statusText = "notmodified";
+					isSuccess = true;
+
+				// If we have data
+				} else {
+
+					try {
+						success = ajaxConvert( s, response );
+						statusText = "success";
+						isSuccess = true;
+					} catch(e) {
+						// We have a parsererror
+						statusText = "parsererror";
+						error = e;
+					}
+				}
+			} else {
+				// We extract error from statusText
+				// then normalize statusText and status for non-aborts
+				error = statusText;
+				if ( !statusText || status ) {
+					statusText = "error";
+					if ( status < 0 ) {
+						status = 0;
+					}
+				}
+			}
+
+			// Set data for the fake xhr object
+			jqXHR.status = status;
+			jqXHR.statusText = "" + ( nativeStatusText || statusText );
+
+			// Success/Error
+			if ( isSuccess ) {
+				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+			} else {
+				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+			}
+
+			// Status-dependent callbacks
+			jqXHR.statusCode( statusCode );
+			statusCode = undefined;
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+						[ jqXHR, s, isSuccess ? success : error ] );
+			}
+
+			// Complete
+			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+				// Handle the global AJAX counter
+				if ( !( --jQuery.active ) ) {
+					jQuery.event.trigger( "ajaxStop" );
+				}
+			}
+		}
+
+		// Attach deferreds
+		deferred.promise( jqXHR );
+		jqXHR.success = jqXHR.done;
+		jqXHR.error = jqXHR.fail;
+		jqXHR.complete = completeDeferred.add;
+
+		// Status-dependent callbacks
+		jqXHR.statusCode = function( map ) {
+			if ( map ) {
+				var tmp;
+				if ( state < 2 ) {
+					for ( tmp in map ) {
+						statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+					}
+				} else {
+					tmp = map[ jqXHR.status ];
+					jqXHR.then( tmp, tmp );
+				}
+			}
+			return this;
+		};
+
+		// Remove hash character (#7531: and string promotion)
+		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+		// We also use the url parameter if available
+		s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+		// Extract dataTypes list
+		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax );
+
+		// Determine if a cross-domain request is in order
+		if ( s.crossDomain == null ) {
+			parts = rurl.exec( s.url.toLowerCase() );
+			s.crossDomain = !!( parts &&
+				( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||
+					( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+						( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+			);
+		}
+
+		// Convert data if not already a string
+		if ( s.data && s.processData && typeof s.data !== "string" ) {
+			s.data = jQuery.param( s.data, s.traditional );
+		}
+
+		// Apply prefilters
+		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+		// If request was aborted inside a prefiler, stop there
+		if ( state === 2 ) {
+			return false;
+		}
+
+		// We can fire global events as of now if asked to
+		fireGlobals = s.global;
+
+		// Uppercase the type
+		s.type = s.type.toUpperCase();
+
+		// Determine if request has content
+		s.hasContent = !rnoContent.test( s.type );
+
+		// Watch for a new set of requests
+		if ( fireGlobals && jQuery.active++ === 0 ) {
+			jQuery.event.trigger( "ajaxStart" );
+		}
+
+		// More options handling for requests with no content
+		if ( !s.hasContent ) {
+
+			// If data is available, append data to url
+			if ( s.data ) {
+				s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+				// #9682: remove data so that it's not used in an eventual retry
+				delete s.data;
+			}
+
+			// Get ifModifiedKey before adding the anti-cache parameter
+			ifModifiedKey = s.url;
+
+			// Add anti-cache in url if needed
+			if ( s.cache === false ) {
+
+				var ts = jQuery.now(),
+					// try replacing _= if it is there
+					ret = s.url.replace( rts, "$1_=" + ts );
+
+				// if nothing was replaced, add timestamp to the end
+				s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+			}
+		}
+
+		// Set the correct header, if data is being sent
+		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+			jqXHR.setRequestHeader( "Content-Type", s.contentType );
+		}
+
+		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+		if ( s.ifModified ) {
+			ifModifiedKey = ifModifiedKey || s.url;
+			if ( jQuery.lastModified[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] );
+			}
+			if ( jQuery.etag[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] );
+			}
+		}
+
+		// Set the Accepts header for the server, depending on the dataType
+		jqXHR.setRequestHeader(
+			"Accept",
+			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+				s.accepts[ "*" ]
+		);
+
+		// Check for headers option
+		for ( i in s.headers ) {
+			jqXHR.setRequestHeader( i, s.headers[ i ] );
+		}
+
+		// Allow custom headers/mimetypes and early abort
+		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+				// Abort if not done already
+				jqXHR.abort();
+				return false;
+
+		}
+
+		// Install callbacks on deferreds
+		for ( i in { success: 1, error: 1, complete: 1 } ) {
+			jqXHR[ i ]( s[ i ] );
+		}
+
+		// Get transport
+		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+		// If no transport, we auto-abort
+		if ( !transport ) {
+			done( -1, "No Transport" );
+		} else {
+			jqXHR.readyState = 1;
+			// Send global event
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+			}
+			// Timeout
+			if ( s.async && s.timeout > 0 ) {
+				timeoutTimer = setTimeout( function(){
+					jqXHR.abort( "timeout" );
+				}, s.timeout );
+			}
+
+			try {
+				state = 1;
+				transport.send( requestHeaders, done );
+			} catch (e) {
+				// Propagate exception as error if not done
+				if ( state < 2 ) {
+					done( -1, e );
+				// Simply rethrow otherwise
+				} else {
+					throw e;
+				}
+			}
+		}
+
+		return jqXHR;
+	},
+
+	// Serialize an array of form elements or a set of
+	// key/values into a query string
+	param: function( a, traditional ) {
+		var s = [],
+			add = function( key, value ) {
+				// If value is a function, invoke it and return its value
+				value = jQuery.isFunction( value ) ? value() : value;
+				s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+			};
+
+		// Set traditional to true for jQuery <= 1.3.2 behavior.
+		if ( traditional === undefined ) {
+			traditional = jQuery.ajaxSettings.traditional;
+		}
+
+		// If an array was passed in, assume that it is an array of form elements.
+		if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+			// Serialize the form elements
+			jQuery.each( a, function() {
+				add( this.name, this.value );
+			});
+
+		} else {
+			// If traditional, encode the "old" way (the way 1.3.2 or older
+			// did it), otherwise encode params recursively.
+			for ( var prefix in a ) {
+				buildParams( prefix, a[ prefix ], traditional, add );
+			}
+		}
+
+		// Return the resulting serialization
+		return s.join( "&" ).replace( r20, "+" );
+	}
+});
+
+function buildParams( prefix, obj, traditional, add ) {
+	if ( jQuery.isArray( obj ) ) {
+		// Serialize array item.
+		jQuery.each( obj, function( i, v ) {
+			if ( traditional || rbracket.test( prefix ) ) {
+				// Treat each array item as a scalar.
+				add( prefix, v );
+
+			} else {
+				// If array item is non-scalar (array or object), encode its
+				// numeric index to resolve deserialization ambiguity issues.
+				// Note that rack (as of 1.0.0) can't currently deserialize
+				// nested arrays properly, and attempting to do so may cause
+				// a server error. Possible fixes are to modify rack's
+				// deserialization algorithm or to provide an option or flag
+				// to force array serialization to be shallow.
+				buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add );
+			}
+		});
+
+	} else if ( !traditional && obj != null && typeof obj === "object" ) {
+		// Serialize object item.
+		for ( var name in obj ) {
+			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+		}
+
+	} else {
+		// Serialize scalar item.
+		add( prefix, obj );
+	}
+}
+
+// This is still on the jQuery object... for now
+// Want to move this to jQuery.ajax some day
+jQuery.extend({
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Last-Modified header cache for next request
+	lastModified: {},
+	etag: {}
+
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+	var contents = s.contents,
+		dataTypes = s.dataTypes,
+		responseFields = s.responseFields,
+		ct,
+		type,
+		finalDataType,
+		firstDataType;
+
+	// Fill responseXXX fields
+	for ( type in responseFields ) {
+		if ( type in responses ) {
+			jqXHR[ responseFields[type] ] = responses[ type ];
+		}
+	}
+
+	// Remove auto dataType and get content-type in the process
+	while( dataTypes[ 0 ] === "*" ) {
+		dataTypes.shift();
+		if ( ct === undefined ) {
+			ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
+		}
+	}
+
+	// Check if we're dealing with a known content-type
+	if ( ct ) {
+		for ( type in contents ) {
+			if ( contents[ type ] && contents[ type ].test( ct ) ) {
+				dataTypes.unshift( type );
+				break;
+			}
+		}
+	}
+
+	// Check to see if we have a response for the expected dataType
+	if ( dataTypes[ 0 ] in responses ) {
+		finalDataType = dataTypes[ 0 ];
+	} else {
+		// Try convertible dataTypes
+		for ( type in responses ) {
+			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+				finalDataType = type;
+				break;
+			}
+			if ( !firstDataType ) {
+				firstDataType = type;
+			}
+		}
+		// Or just use first one
+		finalDataType = finalDataType || firstDataType;
+	}
+
+	// If we found a dataType
+	// We add the dataType to the list if needed
+	// and return the corresponding response
+	if ( finalDataType ) {
+		if ( finalDataType !== dataTypes[ 0 ] ) {
+			dataTypes.unshift( finalDataType );
+		}
+		return responses[ finalDataType ];
+	}
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+
+	// Apply the dataFilter if provided
+	if ( s.dataFilter ) {
+		response = s.dataFilter( response, s.dataType );
+	}
+
+	var dataTypes = s.dataTypes,
+		converters = {},
+		i,
+		key,
+		length = dataTypes.length,
+		tmp,
+		// Current and previous dataTypes
+		current = dataTypes[ 0 ],
+		prev,
+		// Conversion expression
+		conversion,
+		// Conversion function
+		conv,
+		// Conversion functions (transitive conversion)
+		conv1,
+		conv2;
+
+	// For each dataType in the chain
+	for ( i = 1; i < length; i++ ) {
+
+		// Create converters map
+		// with lowercased keys
+		if ( i === 1 ) {
+			for ( key in s.converters ) {
+				if ( typeof key === "string" ) {
+					converters[ key.toLowerCase() ] = s.converters[ key ];
+				}
+			}
+		}
+
+		// Get the dataTypes
+		prev = current;
+		current = dataTypes[ i ];
+
+		// If current is auto dataType, update it to prev
+		if ( current === "*" ) {
+			current = prev;
+		// If no auto and dataTypes are actually different
+		} else if ( prev !== "*" && prev !== current ) {
+
+			// Get the converter
+			conversion = prev + " " + current;
+			conv = converters[ conversion ] || converters[ "* " + current ];
+
+			// If there is no direct converter, search transitively
+			if ( !conv ) {
+				conv2 = undefined;
+				for ( conv1 in converters ) {
+					tmp = conv1.split( " " );
+					if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) {
+						conv2 = converters[ tmp[1] + " " + current ];
+						if ( conv2 ) {
+							conv1 = converters[ conv1 ];
+							if ( conv1 === true ) {
+								conv = conv2;
+							} else if ( conv2 === true ) {
+								conv = conv1;
+							}
+							break;
+						}
+					}
+				}
+			}
+			// If we found no converter, dispatch an error
+			if ( !( conv || conv2 ) ) {
+				jQuery.error( "No conversion from " + conversion.replace(" "," to ") );
+			}
+			// If found converter is not an equivalence
+			if ( conv !== true ) {
+				// Convert with 1 or 2 converters accordingly
+				response = conv ? conv( response ) : conv2( conv1(response) );
+			}
+		}
+	}
+	return response;
+}
+
+
+
+
+var jsc = jQuery.now(),
+	jsre = /(\=)\?(&|$)|\?\?/i;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+	jsonp: "callback",
+	jsonpCallback: function() {
+		return jQuery.expando + "_" + ( jsc++ );
+	}
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+	var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
+		( typeof s.data === "string" );
+
+	if ( s.dataTypes[ 0 ] === "jsonp" ||
+		s.jsonp !== false && ( jsre.test( s.url ) ||
+				inspectData && jsre.test( s.data ) ) ) {
+
+		var responseContainer,
+			jsonpCallback = s.jsonpCallback =
+				jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback,
+			previous = window[ jsonpCallback ],
+			url = s.url,
+			data = s.data,
+			replace = "$1" + jsonpCallback + "$2";
+
+		if ( s.jsonp !== false ) {
+			url = url.replace( jsre, replace );
+			if ( s.url === url ) {
+				if ( inspectData ) {
+					data = data.replace( jsre, replace );
+				}
+				if ( s.data === data ) {
+					// Add callback manually
+					url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback;
+				}
+			}
+		}
+
+		s.url = url;
+		s.data = data;
+
+		// Install callback
+		window[ jsonpCallback ] = function( response ) {
+			responseContainer = [ response ];
+		};
+
+		// Clean-up function
+		jqXHR.always(function() {
+			// Set callback back to previous value
+			window[ jsonpCallback ] = previous;
+			// Call if it was a function and we have a response
+			if ( responseContainer && jQuery.isFunction( previous ) ) {
+				window[ jsonpCallback ]( responseContainer[ 0 ] );
+			}
+		});
+
+		// Use data converter to retrieve json after script execution
+		s.converters["script json"] = function() {
+			if ( !responseContainer ) {
+				jQuery.error( jsonpCallback + " was not called" );
+			}
+			return responseContainer[ 0 ];
+		};
+
+		// force json dataType
+		s.dataTypes[ 0 ] = "json";
+
+		// Delegate to script
+		return "script";
+	}
+});
+
+
+
+
+// Install script dataType
+jQuery.ajaxSetup({
+	accepts: {
+		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+	},
+	contents: {
+		script: /javascript|ecmascript/
+	},
+	converters: {
+		"text script": function( text ) {
+			jQuery.globalEval( text );
+			return text;
+		}
+	}
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+	if ( s.cache === undefined ) {
+		s.cache = false;
+	}
+	if ( s.crossDomain ) {
+		s.type = "GET";
+		s.global = false;
+	}
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+	// This transport only deals with cross domain requests
+	if ( s.crossDomain ) {
+
+		var script,
+			head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
+
+		return {
+
+			send: function( _, callback ) {
+
+				script = document.createElement( "script" );
+
+				script.async = "async";
+
+				if ( s.scriptCharset ) {
+					script.charset = s.scriptCharset;
+				}
+
+				script.src = s.url;
+
+				// Attach handlers for all browsers
+				script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+					if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+						// Handle memory leak in IE
+						script.onload = script.onreadystatechange = null;
+
+						// Remove the script
+						if ( head && script.parentNode ) {
+							head.removeChild( script );
+						}
+
+						// Dereference the script
+						script = undefined;
+
+						// Callback if not abort
+						if ( !isAbort ) {
+							callback( 200, "success" );
+						}
+					}
+				};
+				// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+				// This arises when a base node is used (#2709 and #4378).
+				head.insertBefore( script, head.firstChild );
+			},
+
+			abort: function() {
+				if ( script ) {
+					script.onload( 0, 1 );
+				}
+			}
+		};
+	}
+});
+
+
+
+
+var // #5280: Internet Explorer will keep connections alive if we don't abort on unload
+	xhrOnUnloadAbort = window.ActiveXObject ? function() {
+		// Abort all pending requests
+		for ( var key in xhrCallbacks ) {
+			xhrCallbacks[ key ]( 0, 1 );
+		}
+	} : false,
+	xhrId = 0,
+	xhrCallbacks;
+
+// Functions to create xhrs
+function createStandardXHR() {
+	try {
+		return new window.XMLHttpRequest();
+	} catch( e ) {}
+}
+
+function createActiveXHR() {
+	try {
+		return new window.ActiveXObject( "Microsoft.XMLHTTP" );
+	} catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+	/* Microsoft failed to properly
+	 * implement the XMLHttpRequest in IE7 (can't request local files),
+	 * so we use the ActiveXObject when it is available
+	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+	 * we need a fallback.
+	 */
+	function() {
+		return !this.isLocal && createStandardXHR() || createActiveXHR();
+	} :
+	// For all other browsers, use the standard XMLHttpRequest object
+	createStandardXHR;
+
+// Determine support properties
+(function( xhr ) {
+	jQuery.extend( jQuery.support, {
+		ajax: !!xhr,
+		cors: !!xhr && ( "withCredentials" in xhr )
+	});
+})( jQuery.ajaxSettings.xhr() );
+
+// Create transport if the browser can provide an xhr
+if ( jQuery.support.ajax ) {
+
+	jQuery.ajaxTransport(function( s ) {
+		// Cross domain only allowed if supported through XMLHttpRequest
+		if ( !s.crossDomain || jQuery.support.cors ) {
+
+			var callback;
+
+			return {
+				send: function( headers, complete ) {
+
+					// Get a new xhr
+					var xhr = s.xhr(),
+						handle,
+						i;
+
+					// Open the socket
+					// Passing null username, generates a login popup on Opera (#2865)
+					if ( s.username ) {
+						xhr.open( s.type, s.url, s.async, s.username, s.password );
+					} else {
+						xhr.open( s.type, s.url, s.async );
+					}
+
+					// Apply custom fields if provided
+					if ( s.xhrFields ) {
+						for ( i in s.xhrFields ) {
+							xhr[ i ] = s.xhrFields[ i ];
+						}
+					}
+
+					// Override mime type if needed
+					if ( s.mimeType && xhr.overrideMimeType ) {
+						xhr.overrideMimeType( s.mimeType );
+					}
+
+					// X-Requested-With header
+					// For cross-domain requests, seeing as conditions for a preflight are
+					// akin to a jigsaw puzzle, we simply never set it to be sure.
+					// (it can always be set on a per-request basis or even using ajaxSetup)
+					// For same-domain requests, won't change header if already provided.
+					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+						headers[ "X-Requested-With" ] = "XMLHttpRequest";
+					}
+
+					// Need an extra try/catch for cross domain requests in Firefox 3
+					try {
+						for ( i in headers ) {
+							xhr.setRequestHeader( i, headers[ i ] );
+						}
+					} catch( _ ) {}
+
+					// Do send the request
+					// This may raise an exception which is actually
+					// handled in jQuery.ajax (so no try/catch here)
+					xhr.send( ( s.hasContent && s.data ) || null );
+
+					// Listener
+					callback = function( _, isAbort ) {
+
+						var status,
+							statusText,
+							responseHeaders,
+							responses,
+							xml;
+
+						// Firefox throws exceptions when accessing properties
+						// of an xhr when a network error occured
+						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+						try {
+
+							// Was never called and is aborted or complete
+							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+								// Only called once
+								callback = undefined;
+
+								// Do not keep as active anymore
+								if ( handle ) {
+									xhr.onreadystatechange = jQuery.noop;
+									if ( xhrOnUnloadAbort ) {
+										delete xhrCallbacks[ handle ];
+									}
+								}
+
+								// If it's an abort
+								if ( isAbort ) {
+									// Abort it manually if needed
+									if ( xhr.readyState !== 4 ) {
+										xhr.abort();
+									}
+								} else {
+									status = xhr.status;
+									responseHeaders = xhr.getAllResponseHeaders();
+									responses = {};
+									xml = xhr.responseXML;
+
+									// Construct response list
+									if ( xml && xml.documentElement /* #4958 */ ) {
+										responses.xml = xml;
+									}
+									responses.text = xhr.responseText;
+
+									// Firefox throws an exception when accessing
+									// statusText for faulty cross-domain requests
+									try {
+										statusText = xhr.statusText;
+									} catch( e ) {
+										// We normalize with Webkit giving an empty statusText
+										statusText = "";
+									}
+
+									// Filter status for non standard behaviors
+
+									// If the request is local and we have data: assume a success
+									// (success with no data won't get notified, that's the best we
+									// can do given current implementations)
+									if ( !status && s.isLocal && !s.crossDomain ) {
+										status = responses.text ? 200 : 404;
+									// IE - #1450: sometimes returns 1223 when it should be 204
+									} else if ( status === 1223 ) {
+										status = 204;
+									}
+								}
+							}
+						} catch( firefoxAccessException ) {
+							if ( !isAbort ) {
+								complete( -1, firefoxAccessException );
+							}
+						}
+
+						// Call complete if needed
+						if ( responses ) {
+							complete( status, statusText, responses, responseHeaders );
+						}
+					};
+
+					// if we're in sync mode or it's in cache
+					// and has been retrieved directly (IE6 & IE7)
+					// we need to manually fire the callback
+					if ( !s.async || xhr.readyState === 4 ) {
+						callback();
+					} else {
+						handle = ++xhrId;
+						if ( xhrOnUnloadAbort ) {
+							// Create the active xhrs callbacks list if needed
+							// and attach the unload handler
+							if ( !xhrCallbacks ) {
+								xhrCallbacks = {};
+								jQuery( window ).unload( xhrOnUnloadAbort );
+							}
+							// Add to list of active xhrs callbacks
+							xhrCallbacks[ handle ] = callback;
+						}
+						xhr.onreadystatechange = callback;
+					}
+				},
+
+				abort: function() {
+					if ( callback ) {
+						callback(0,1);
+					}
+				}
+			};
+		}
+	});
+}
+
+
+
+
+var elemdisplay = {},
+	iframe, iframeDoc,
+	rfxtypes = /^(?:toggle|show|hide)$/,
+	rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,
+	timerId,
+	fxAttrs = [
+		// height animations
+		[ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
+		// width animations
+		[ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
+		// opacity animations
+		[ "opacity" ]
+	],
+	fxNow;
+
+jQuery.fn.extend({
+	show: function( speed, easing, callback ) {
+		var elem, display;
+
+		if ( speed || speed === 0 ) {
+			return this.animate( genFx("show", 3), speed, easing, callback );
+
+		} else {
+			for ( var i = 0, j = this.length; i < j; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.style ) {
+					display = elem.style.display;
+
+					// Reset the inline display of this element to learn if it is
+					// being hidden by cascaded rules or not
+					if ( !jQuery._data(elem, "olddisplay") && display === "none" ) {
+						display = elem.style.display = "";
+					}
+
+					// Set elements which have been overridden with display: none
+					// in a stylesheet to whatever the default browser style is
+					// for such an element
+					if ( display === "" && jQuery.css(elem, "display") === "none" ) {
+						jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) );
+					}
+				}
+			}
+
+			// Set the display of most of the elements in a second loop
+			// to avoid the constant reflow
+			for ( i = 0; i < j; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.style ) {
+					display = elem.style.display;
+
+					if ( display === "" || display === "none" ) {
+						elem.style.display = jQuery._data( elem, "olddisplay" ) || "";
+					}
+				}
+			}
+
+			return this;
+		}
+	},
+
+	hide: function( speed, easing, callback ) {
+		if ( speed || speed === 0 ) {
+			return this.animate( genFx("hide", 3), speed, easing, callback);
+
+		} else {
+			var elem, display,
+				i = 0,
+				j = this.length;
+
+			for ( ; i < j; i++ ) {
+				elem = this[i];
+				if ( elem.style ) {
+					display = jQuery.css( elem, "display" );
+
+					if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) {
+						jQuery._data( elem, "olddisplay", display );
+					}
+				}
+			}
+
+			// Set the display of the elements in a second loop
+			// to avoid the constant reflow
+			for ( i = 0; i < j; i++ ) {
+				if ( this[i].style ) {
+					this[i].style.display = "none";
+				}
+			}
+
+			return this;
+		}
+	},
+
+	// Save the old toggle function
+	_toggle: jQuery.fn.toggle,
+
+	toggle: function( fn, fn2, callback ) {
+		var bool = typeof fn === "boolean";
+
+		if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
+			this._toggle.apply( this, arguments );
+
+		} else if ( fn == null || bool ) {
+			this.each(function() {
+				var state = bool ? fn : jQuery(this).is(":hidden");
+				jQuery(this)[ state ? "show" : "hide" ]();
+			});
+
+		} else {
+			this.animate(genFx("toggle", 3), fn, fn2, callback);
+		}
+
+		return this;
+	},
+
+	fadeTo: function( speed, to, easing, callback ) {
+		return this.filter(":hidden").css("opacity", 0).show().end()
+					.animate({opacity: to}, speed, easing, callback);
+	},
+
+	animate: function( prop, speed, easing, callback ) {
+		var optall = jQuery.speed( speed, easing, callback );
+
+		if ( jQuery.isEmptyObject( prop ) ) {
+			return this.each( optall.complete, [ false ] );
+		}
+
+		// Do not change referenced properties as per-property easing will be lost
+		prop = jQuery.extend( {}, prop );
+
+		function doAnimation() {
+			// XXX 'this' does not always have a nodeName when running the
+			// test suite
+
+			if ( optall.queue === false ) {
+				jQuery._mark( this );
+			}
+
+			var opt = jQuery.extend( {}, optall ),
+				isElement = this.nodeType === 1,
+				hidden = isElement && jQuery(this).is(":hidden"),
+				name, val, p, e,
+				parts, start, end, unit,
+				method;
+
+			// will store per property easing and be used to determine when an animation is complete
+			opt.animatedProperties = {};
+
+			for ( p in prop ) {
+
+				// property name normalization
+				name = jQuery.camelCase( p );
+				if ( p !== name ) {
+					prop[ name ] = prop[ p ];
+					delete prop[ p ];
+				}
+
+				val = prop[ name ];
+
+				// easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default)
+				if ( jQuery.isArray( val ) ) {
+					opt.animatedProperties[ name ] = val[ 1 ];
+					val = prop[ name ] = val[ 0 ];
+				} else {
+					opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing';
+				}
+
+				if ( val === "hide" && hidden || val === "show" && !hidden ) {
+					return opt.complete.call( this );
+				}
+
+				if ( isElement && ( name === "height" || name === "width" ) ) {
+					// Make sure that nothing sneaks out
+					// Record all 3 overflow attributes because IE does not
+					// change the overflow attribute when overflowX and
+					// overflowY are set to the same value
+					opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];
+
+					// Set display property to inline-block for height/width
+					// animations on inline elements that are having width/height animated
+					if ( jQuery.css( this, "display" ) === "inline" &&
+							jQuery.css( this, "float" ) === "none" ) {
+
+						// inline-level elements accept inline-block;
+						// block-level elements need to be inline with layout
+						if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) {
+							this.style.display = "inline-block";
+
+						} else {
+							this.style.zoom = 1;
+						}
+					}
+				}
+			}
+
+			if ( opt.overflow != null ) {
+				this.style.overflow = "hidden";
+			}
+
+			for ( p in prop ) {
+				e = new jQuery.fx( this, opt, p );
+				val = prop[ p ];
+
+				if ( rfxtypes.test( val ) ) {
+
+					// Tracks whether to show or hide based on private
+					// data attached to the element
+					method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 );
+					if ( method ) {
+						jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" );
+						e[ method ]();
+					} else {
+						e[ val ]();
+					}
+
+				} else {
+					parts = rfxnum.exec( val );
+					start = e.cur();
+
+					if ( parts ) {
+						end = parseFloat( parts[2] );
+						unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" );
+
+						// We need to compute starting value
+						if ( unit !== "px" ) {
+							jQuery.style( this, p, (end || 1) + unit);
+							start = ( (end || 1) / e.cur() ) * start;
+							jQuery.style( this, p, start + unit);
+						}
+
+						// If a +=/-= token was provided, we're doing a relative animation
+						if ( parts[1] ) {
+							end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start;
+						}
+
+						e.custom( start, end, unit );
+
+					} else {
+						e.custom( start, val, "" );
+					}
+				}
+			}
+
+			// For JS strict compliance
+			return true;
+		}
+
+		return optall.queue === false ?
+			this.each( doAnimation ) :
+			this.queue( optall.queue, doAnimation );
+	},
+
+	stop: function( type, clearQueue, gotoEnd ) {
+		if ( typeof type !== "string" ) {
+			gotoEnd = clearQueue;
+			clearQueue = type;
+			type = undefined;
+		}
+		if ( clearQueue && type !== false ) {
+			this.queue( type || "fx", [] );
+		}
+
+		return this.each(function() {
+			var index,
+				hadTimers = false,
+				timers = jQuery.timers,
+				data = jQuery._data( this );
+
+			// clear marker counters if we know they won't be
+			if ( !gotoEnd ) {
+				jQuery._unmark( true, this );
+			}
+
+			function stopQueue( elem, data, index ) {
+				var hooks = data[ index ];
+				jQuery.removeData( elem, index, true );
+				hooks.stop( gotoEnd );
+			}
+
+			if ( type == null ) {
+				for ( index in data ) {
+					if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) {
+						stopQueue( this, data, index );
+					}
+				}
+			} else if ( data[ index = type + ".run" ] && data[ index ].stop ){
+				stopQueue( this, data, index );
+			}
+
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+					if ( gotoEnd ) {
+
+						// force the next step to be the last
+						timers[ index ]( true );
+					} else {
+						timers[ index ].saveState();
+					}
+					hadTimers = true;
+					timers.splice( index, 1 );
+				}
+			}
+
+			// start the next in the queue if the last step wasn't forced
+			// timers currently will call their complete callbacks, which will dequeue
+			// but only if they were gotoEnd
+			if ( !( gotoEnd && hadTimers ) ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	}
+
+});
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+	setTimeout( clearFxNow, 0 );
+	return ( fxNow = jQuery.now() );
+}
+
+function clearFxNow() {
+	fxNow = undefined;
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, num ) {
+	var obj = {};
+
+	jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() {
+		obj[ this ] = type;
+	});
+
+	return obj;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+	slideDown: genFx( "show", 1 ),
+	slideUp: genFx( "hide", 1 ),
+	slideToggle: genFx( "toggle", 1 ),
+	fadeIn: { opacity: "show" },
+	fadeOut: { opacity: "hide" },
+	fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return this.animate( props, speed, easing, callback );
+	};
+});
+
+jQuery.extend({
+	speed: function( speed, easing, fn ) {
+		var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+			complete: fn || !fn && easing ||
+				jQuery.isFunction( speed ) && speed,
+			duration: speed,
+			easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+		};
+
+		opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+			opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+		// normalize opt.queue - true/undefined/null -> "fx"
+		if ( opt.queue == null || opt.queue === true ) {
+			opt.queue = "fx";
+		}
+
+		// Queueing
+		opt.old = opt.complete;
+
+		opt.complete = function( noUnmark ) {
+			if ( jQuery.isFunction( opt.old ) ) {
+				opt.old.call( this );
+			}
+
+			if ( opt.queue ) {
+				jQuery.dequeue( this, opt.queue );
+			} else if ( noUnmark !== false ) {
+				jQuery._unmark( this );
+			}
+		};
+
+		return opt;
+	},
+
+	easing: {
+		linear: function( p, n, firstNum, diff ) {
+			return firstNum + diff * p;
+		},
+		swing: function( p, n, firstNum, diff ) {
+			return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum;
+		}
+	},
+
+	timers: [],
+
+	fx: function( elem, options, prop ) {
+		this.options = options;
+		this.elem = elem;
+		this.prop = prop;
+
+		options.orig = options.orig || {};
+	}
+
+});
+
+jQuery.fx.prototype = {
+	// Simple function for setting a style value
+	update: function() {
+		if ( this.options.step ) {
+			this.options.step.call( this.elem, this.now, this );
+		}
+
+		( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this );
+	},
+
+	// Get the current size
+	cur: function() {
+		if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) {
+			return this.elem[ this.prop ];
+		}
+
+		var parsed,
+			r = jQuery.css( this.elem, this.prop );
+		// Empty strings, null, undefined and "auto" are converted to 0,
+		// complex values such as "rotate(1rad)" are returned as is,
+		// simple values such as "10px" are parsed to Float.
+		return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
+	},
+
+	// Start an animation from one number to another
+	custom: function( from, to, unit ) {
+		var self = this,
+			fx = jQuery.fx;
+
+		this.startTime = fxNow || createFxNow();
+		this.end = to;
+		this.now = this.start = from;
+		this.pos = this.state = 0;
+		this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" );
+
+		function t( gotoEnd ) {
+			return self.step( gotoEnd );
+		}
+
+		t.queue = this.options.queue;
+		t.elem = this.elem;
+		t.saveState = function() {
+			if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) {
+				jQuery._data( self.elem, "fxshow" + self.prop, self.start );
+			}
+		};
+
+		if ( t() && jQuery.timers.push(t) && !timerId ) {
+			timerId = setInterval( fx.tick, fx.interval );
+		}
+	},
+
+	// Simple 'show' function
+	show: function() {
+		var dataShow = jQuery._data( this.elem, "fxshow" + this.prop );
+
+		// Remember where we started, so that we can go back to it later
+		this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop );
+		this.options.show = true;
+
+		// Begin the animation
+		// Make sure that we start at a small width/height to avoid any flash of content
+		if ( dataShow !== undefined ) {
+			// This show is picking up where a previous hide or show left off
+			this.custom( this.cur(), dataShow );
+		} else {
+			this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() );
+		}
+
+		// Start by showing the element
+		jQuery( this.elem ).show();
+	},
+
+	// Simple 'hide' function
+	hide: function() {
+		// Remember where we started, so that we can go back to it later
+		this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop );
+		this.options.hide = true;
+
+		// Begin the animation
+		this.custom( this.cur(), 0 );
+	},
+
+	// Each step of an animation
+	step: function( gotoEnd ) {
+		var p, n, complete,
+			t = fxNow || createFxNow(),
+			done = true,
+			elem = this.elem,
+			options = this.options;
+
+		if ( gotoEnd || t >= options.duration + this.startTime ) {
+			this.now = this.end;
+			this.pos = this.state = 1;
+			this.update();
+
+			options.animatedProperties[ this.prop ] = true;
+
+			for ( p in options.animatedProperties ) {
+				if ( options.animatedProperties[ p ] !== true ) {
+					done = false;
+				}
+			}
+
+			if ( done ) {
+				// Reset the overflow
+				if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) {
+
+					jQuery.each( [ "", "X", "Y" ], function( index, value ) {
+						elem.style[ "overflow" + value ] = options.overflow[ index ];
+					});
+				}
+
+				// Hide the element if the "hide" operation was done
+				if ( options.hide ) {
+					jQuery( elem ).hide();
+				}
+
+				// Reset the properties, if the item has been hidden or shown
+				if ( options.hide || options.show ) {
+					for ( p in options.animatedProperties ) {
+						jQuery.style( elem, p, options.orig[ p ] );
+						jQuery.removeData( elem, "fxshow" + p, true );
+						// Toggle data is no longer needed
+						jQuery.removeData( elem, "toggle" + p, true );
+					}
+				}
+
+				// Execute the complete function
+				// in the event that the complete function throws an exception
+				// we must ensure it won't be called twice. #5684
+
+				complete = options.complete;
+				if ( complete ) {
+
+					options.complete = false;
+					complete.call( elem );
+				}
+			}
+
+			return false;
+
+		} else {
+			// classical easing cannot be used with an Infinity duration
+			if ( options.duration == Infinity ) {
+				this.now = t;
+			} else {
+				n = t - this.startTime;
+				this.state = n / options.duration;
+
+				// Perform the easing function, defaults to swing
+				this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration );
+				this.now = this.start + ( (this.end - this.start) * this.pos );
+			}
+			// Perform the next step of the animation
+			this.update();
+		}
+
+		return true;
+	}
+};
+
+jQuery.extend( jQuery.fx, {
+	tick: function() {
+		var timer,
+			timers = jQuery.timers,
+			i = 0;
+
+		for ( ; i < timers.length; i++ ) {
+			timer = timers[ i ];
+			// Checks the timer has not already been removed
+			if ( !timer() && timers[ i ] === timer ) {
+				timers.splice( i--, 1 );
+			}
+		}
+
+		if ( !timers.length ) {
+			jQuery.fx.stop();
+		}
+	},
+
+	interval: 13,
+
+	stop: function() {
+		clearInterval( timerId );
+		timerId = null;
+	},
+
+	speeds: {
+		slow: 600,
+		fast: 200,
+		// Default speed
+		_default: 400
+	},
+
+	step: {
+		opacity: function( fx ) {
+			jQuery.style( fx.elem, "opacity", fx.now );
+		},
+
+		_default: function( fx ) {
+			if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
+				fx.elem.style[ fx.prop ] = fx.now + fx.unit;
+			} else {
+				fx.elem[ fx.prop ] = fx.now;
+			}
+		}
+	}
+});
+
+// Adds width/height step functions
+// Do not set anything below 0
+jQuery.each([ "width", "height" ], function( i, prop ) {
+	jQuery.fx.step[ prop ] = function( fx ) {
+		jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit );
+	};
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.animated = function( elem ) {
+		return jQuery.grep(jQuery.timers, function( fn ) {
+			return elem === fn.elem;
+		}).length;
+	};
+}
+
+// Try to restore the default display value of an element
+function defaultDisplay( nodeName ) {
+
+	if ( !elemdisplay[ nodeName ] ) {
+
+		var body = document.body,
+			elem = jQuery( "<" + nodeName + ">" ).appendTo( body ),
+			display = elem.css( "display" );
+		elem.remove();
+
+		// If the simple way fails,
+		// get element's real default display by attaching it to a temp iframe
+		if ( display === "none" || display === "" ) {
+			// No iframe to use yet, so create it
+			if ( !iframe ) {
+				iframe = document.createElement( "iframe" );
+				iframe.frameBorder = iframe.width = iframe.height = 0;
+			}
+
+			body.appendChild( iframe );
+
+			// Create a cacheable copy of the iframe document on first call.
+			// IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML
+			// document to it; WebKit & Firefox won't allow reusing the iframe document.
+			if ( !iframeDoc || !iframe.createElement ) {
+				iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;
+				iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) + "<html><body>" );
+				iframeDoc.close();
+			}
+
+			elem = iframeDoc.createElement( nodeName );
+
+			iframeDoc.body.appendChild( elem );
+
+			display = jQuery.css( elem, "display" );
+			body.removeChild( iframe );
+		}
+
+		// Store the correct default display
+		elemdisplay[ nodeName ] = display;
+	}
+
+	return elemdisplay[ nodeName ];
+}
+
+
+
+
+var rtable = /^t(?:able|d|h)$/i,
+	rroot = /^(?:body|html)$/i;
+
+if ( "getBoundingClientRect" in document.documentElement ) {
+	jQuery.fn.offset = function( options ) {
+		var elem = this[0], box;
+
+		if ( options ) {
+			return this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+		}
+
+		if ( !elem || !elem.ownerDocument ) {
+			return null;
+		}
+
+		if ( elem === elem.ownerDocument.body ) {
+			return jQuery.offset.bodyOffset( elem );
+		}
+
+		try {
+			box = elem.getBoundingClientRect();
+		} catch(e) {}
+
+		var doc = elem.ownerDocument,
+			docElem = doc.documentElement;
+
+		// Make sure we're not dealing with a disconnected DOM node
+		if ( !box || !jQuery.contains( docElem, elem ) ) {
+			return box ? { top: box.top, left: box.left } : { top: 0, left: 0 };
+		}
+
+		var body = doc.body,
+			win = getWindow(doc),
+			clientTop  = docElem.clientTop  || body.clientTop  || 0,
+			clientLeft = docElem.clientLeft || body.clientLeft || 0,
+			scrollTop  = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop,
+			scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,
+			top  = box.top  + scrollTop  - clientTop,
+			left = box.left + scrollLeft - clientLeft;
+
+		return { top: top, left: left };
+	};
+
+} else {
+	jQuery.fn.offset = function( options ) {
+		var elem = this[0];
+
+		if ( options ) {
+			return this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+		}
+
+		if ( !elem || !elem.ownerDocument ) {
+			return null;
+		}
+
+		if ( elem === elem.ownerDocument.body ) {
+			return jQuery.offset.bodyOffset( elem );
+		}
+
+		var computedStyle,
+			offsetParent = elem.offsetParent,
+			prevOffsetParent = elem,
+			doc = elem.ownerDocument,
+			docElem = doc.documentElement,
+			body = doc.body,
+			defaultView = doc.defaultView,
+			prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
+			top = elem.offsetTop,
+			left = elem.offsetLeft;
+
+		while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
+			if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) {
+				break;
+			}
+
+			computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
+			top  -= elem.scrollTop;
+			left -= elem.scrollLeft;
+
+			if ( elem === offsetParent ) {
+				top  += elem.offsetTop;
+				left += elem.offsetLeft;
+
+				if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) {
+					top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
+					left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+				}
+
+				prevOffsetParent = offsetParent;
+				offsetParent = elem.offsetParent;
+			}
+
+			if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
+				top  += parseFloat( computedStyle.borderTopWidth  ) || 0;
+				left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+			}
+
+			prevComputedStyle = computedStyle;
+		}
+
+		if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
+			top  += body.offsetTop;
+			left += body.offsetLeft;
+		}
+
+		if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) {
+			top  += Math.max( docElem.scrollTop, body.scrollTop );
+			left += Math.max( docElem.scrollLeft, body.scrollLeft );
+		}
+
+		return { top: top, left: left };
+	};
+}
+
+jQuery.offset = {
+
+	bodyOffset: function( body ) {
+		var top = body.offsetTop,
+			left = body.offsetLeft;
+
+		if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) {
+			top  += parseFloat( jQuery.css(body, "marginTop") ) || 0;
+			left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
+		}
+
+		return { top: top, left: left };
+	},
+
+	setOffset: function( elem, options, i ) {
+		var position = jQuery.css( elem, "position" );
+
+		// set position first, in-case top/left are set even on static elem
+		if ( position === "static" ) {
+			elem.style.position = "relative";
+		}
+
+		var curElem = jQuery( elem ),
+			curOffset = curElem.offset(),
+			curCSSTop = jQuery.css( elem, "top" ),
+			curCSSLeft = jQuery.css( elem, "left" ),
+			calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+			props = {}, curPosition = {}, curTop, curLeft;
+
+		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+		if ( calculatePosition ) {
+			curPosition = curElem.position();
+			curTop = curPosition.top;
+			curLeft = curPosition.left;
+		} else {
+			curTop = parseFloat( curCSSTop ) || 0;
+			curLeft = parseFloat( curCSSLeft ) || 0;
+		}
+
+		if ( jQuery.isFunction( options ) ) {
+			options = options.call( elem, i, curOffset );
+		}
+
+		if ( options.top != null ) {
+			props.top = ( options.top - curOffset.top ) + curTop;
+		}
+		if ( options.left != null ) {
+			props.left = ( options.left - curOffset.left ) + curLeft;
+		}
+
+		if ( "using" in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	}
+};
+
+
+jQuery.fn.extend({
+
+	position: function() {
+		if ( !this[0] ) {
+			return null;
+		}
+
+		var elem = this[0],
+
+		// Get *real* offsetParent
+		offsetParent = this.offsetParent(),
+
+		// Get correct offsets
+		offset       = this.offset(),
+		parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+		// Subtract element margins
+		// note: when an element has margin: auto the offsetLeft and marginLeft
+		// are the same in Safari causing offset.left to incorrectly be 0
+		offset.top  -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+		offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+		// Add offsetParent borders
+		parentOffset.top  += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+		parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+		// Subtract the two offsets
+		return {
+			top:  offset.top  - parentOffset.top,
+			left: offset.left - parentOffset.left
+		};
+	},
+
+	offsetParent: function() {
+		return this.map(function() {
+			var offsetParent = this.offsetParent || document.body;
+			while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+				offsetParent = offsetParent.offsetParent;
+			}
+			return offsetParent;
+		});
+	}
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( ["Left", "Top"], function( i, name ) {
+	var method = "scroll" + name;
+
+	jQuery.fn[ method ] = function( val ) {
+		var elem, win;
+
+		if ( val === undefined ) {
+			elem = this[ 0 ];
+
+			if ( !elem ) {
+				return null;
+			}
+
+			win = getWindow( elem );
+
+			// Return the scroll offset
+			return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+				jQuery.support.boxModel && win.document.documentElement[ method ] ||
+					win.document.body[ method ] :
+				elem[ method ];
+		}
+
+		// Set the scroll offset
+		return this.each(function() {
+			win = getWindow( this );
+
+			if ( win ) {
+				win.scrollTo(
+					!i ? val : jQuery( win ).scrollLeft(),
+					 i ? val : jQuery( win ).scrollTop()
+				);
+
+			} else {
+				this[ method ] = val;
+			}
+		});
+	};
+});
+
+function getWindow( elem ) {
+	return jQuery.isWindow( elem ) ?
+		elem :
+		elem.nodeType === 9 ?
+			elem.defaultView || elem.parentWindow :
+			false;
+}
+
+
+
+
+// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods
+jQuery.each([ "Height", "Width" ], function( i, name ) {
+
+	var type = name.toLowerCase();
+
+	// innerHeight and innerWidth
+	jQuery.fn[ "inner" + name ] = function() {
+		var elem = this[0];
+		return elem ?
+			elem.style ?
+			parseFloat( jQuery.css( elem, type, "padding" ) ) :
+			this[ type ]() :
+			null;
+	};
+
+	// outerHeight and outerWidth
+	jQuery.fn[ "outer" + name ] = function( margin ) {
+		var elem = this[0];
+		return elem ?
+			elem.style ?
+			parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) :
+			this[ type ]() :
+			null;
+	};
+
+	jQuery.fn[ type ] = function( size ) {
+		// Get window width or height
+		var elem = this[0];
+		if ( !elem ) {
+			return size == null ? null : this;
+		}
+
+		if ( jQuery.isFunction( size ) ) {
+			return this.each(function( i ) {
+				var self = jQuery( this );
+				self[ type ]( size.call( this, i, self[ type ]() ) );
+			});
+		}
+
+		if ( jQuery.isWindow( elem ) ) {
+			// Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+			// 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat
+			var docElemProp = elem.document.documentElement[ "client" + name ],
+				body = elem.document.body;
+			return elem.document.compatMode === "CSS1Compat" && docElemProp ||
+				body && body[ "client" + name ] || docElemProp;
+
+		// Get document width or height
+		} else if ( elem.nodeType === 9 ) {
+			// Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+			return Math.max(
+				elem.documentElement["client" + name],
+				elem.body["scroll" + name], elem.documentElement["scroll" + name],
+				elem.body["offset" + name], elem.documentElement["offset" + name]
+			);
+
+		// Get or set width or height on the element
+		} else if ( size === undefined ) {
+			var orig = jQuery.css( elem, type ),
+				ret = parseFloat( orig );
+
+			return jQuery.isNumeric( ret ) ? ret : orig;
+
+		// Set the width or height on the element (default to pixels if value is unitless)
+		} else {
+			return this.css( type, typeof size === "string" ? size : size + "px" );
+		}
+	};
+
+});
+
+
+
+
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+	define( "jquery", [], function () { return jQuery; } );
+}
+
+
+
+})( window );
diff --git a/client/apollo/jslib/jqueryui/accordion.js b/client/apollo/jslib/jqueryui/accordion.js
new file mode 100644
index 0000000..70cec79
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/accordion.js
@@ -0,0 +1,614 @@
+define(['jquery','./core','./widget'], function (jQuery) {
+/*
+ * jQuery UI Accordion @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget( "ui.accordion", {
+	options: {
+		active: 0,
+		animated: "slide",
+		autoHeight: true,
+		clearStyle: false,
+		collapsible: false,
+		event: "click",
+		fillSpace: false,
+		header: "> li > :first-child,> :not(li):even",
+		icons: {
+			header: "ui-icon-triangle-1-e",
+			headerSelected: "ui-icon-triangle-1-s"
+		},
+		navigation: false,
+		navigationFilter: function() {
+			return this.href.toLowerCase() === location.href.toLowerCase();
+		}
+	},
+
+	_create: function() {
+		var self = this,
+			options = self.options;
+
+		self.running = 0;
+
+		self.element
+			.addClass( "ui-accordion ui-widget ui-helper-reset" )
+			// in lack of child-selectors in CSS
+			// we need to mark top-LIs in a UL-accordion for some IE-fix
+			.children( "li" )
+				.addClass( "ui-accordion-li-fix" );
+
+		self.headers = self.element.find( options.header )
+			.addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" )
+			.bind( "mouseenter.accordion", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).addClass( "ui-state-hover" );
+			})
+			.bind( "mouseleave.accordion", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( "ui-state-hover" );
+			})
+			.bind( "focus.accordion", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).addClass( "ui-state-focus" );
+			})
+			.bind( "blur.accordion", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( "ui-state-focus" );
+			});
+
+		self.headers.next()
+			.addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" );
+
+		if ( options.navigation ) {
+			var current = self.element.find( "a" ).filter( options.navigationFilter ).eq( 0 );
+			if ( current.length ) {
+				var header = current.closest( ".ui-accordion-header" );
+				if ( header.length ) {
+					// anchor within header
+					self.active = header;
+				} else {
+					// anchor within content
+					self.active = current.closest( ".ui-accordion-content" ).prev();
+				}
+			}
+		}
+
+		self.active = self._findActive( self.active || options.active )
+			.addClass( "ui-state-default ui-state-active" )
+			.toggleClass( "ui-corner-all" )
+			.toggleClass( "ui-corner-top" );
+		self.active.next().addClass( "ui-accordion-content-active" );
+
+		self._createIcons();
+		self.resize();
+		
+		// ARIA
+		self.element.attr( "role", "tablist" );
+
+		self.headers
+			.attr( "role", "tab" )
+			.bind( "keydown.accordion", function( event ) {
+				return self._keydown( event );
+			})
+			.next()
+				.attr( "role", "tabpanel" );
+
+		self.headers
+			.not( self.active || "" )
+			.attr({
+				"aria-expanded": "false",
+				"aria-selected": "false",
+				tabIndex: -1
+			})
+			.next()
+				.hide();
+
+		// make sure at least one header is in the tab order
+		if ( !self.active.length ) {
+			self.headers.eq( 0 ).attr( "tabIndex", 0 );
+		} else {
+			self.active
+				.attr({
+					"aria-expanded": "true",
+					"aria-selected": "true",
+					tabIndex: 0
+				});
+		}
+
+		// only need links in tab order for Safari
+		if ( !$.browser.safari ) {
+			self.headers.find( "a" ).attr( "tabIndex", -1 );
+		}
+
+		if ( options.event ) {
+			self.headers.bind( options.event.split(" ").join(".accordion ") + ".accordion", function(event) {
+				self._clickHandler.call( self, event, this );
+				event.preventDefault();
+			});
+		}
+	},
+
+	_createIcons: function() {
+		var options = this.options;
+		if ( options.icons ) {
+			$( "<span></span>" )
+				.addClass( "ui-icon " + options.icons.header )
+				.prependTo( this.headers );
+			this.active.children( ".ui-icon" )
+				.toggleClass(options.icons.header)
+				.toggleClass(options.icons.headerSelected);
+			this.element.addClass( "ui-accordion-icons" );
+		}
+	},
+
+	_destroyIcons: function() {
+		this.headers.children( ".ui-icon" ).remove();
+		this.element.removeClass( "ui-accordion-icons" );
+	},
+
+	destroy: function() {
+		var options = this.options;
+
+		this.element
+			.removeClass( "ui-accordion ui-widget ui-helper-reset" )
+			.removeAttr( "role" );
+
+		this.headers
+			.unbind( ".accordion" )
+			.removeClass( "ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-expanded" )
+			.removeAttr( "aria-selected" )
+			.removeAttr( "tabIndex" );
+
+		this.headers.find( "a" ).removeAttr( "tabIndex" );
+		this._destroyIcons();
+		var contents = this.headers.next()
+			.css( "display", "" )
+			.removeAttr( "role" )
+			.removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled" );
+		if ( options.autoHeight || options.fillHeight ) {
+			contents.css( "height", "" );
+		}
+
+		return $.Widget.prototype.destroy.call( this );
+	},
+
+	_setOption: function( key, value ) {
+		$.Widget.prototype._setOption.apply( this, arguments );
+			
+		if ( key == "active" ) {
+			this.activate( value );
+		}
+		if ( key == "icons" ) {
+			this._destroyIcons();
+			if ( value ) {
+				this._createIcons();
+			}
+		}
+		// #5332 - opacity doesn't cascade to positioned elements in IE
+		// so we need to add the disabled class to the headers and panels
+		if ( key == "disabled" ) {
+			this.headers.add(this.headers.next())
+				[ value ? "addClass" : "removeClass" ](
+					"ui-accordion-disabled ui-state-disabled" );
+		}
+	},
+
+	_keydown: function( event ) {
+		if ( this.options.disabled || event.altKey || event.ctrlKey ) {
+			return;
+		}
+
+		var keyCode = $.ui.keyCode,
+			length = this.headers.length,
+			currentIndex = this.headers.index( event.target ),
+			toFocus = false;
+
+		switch ( event.keyCode ) {
+			case keyCode.RIGHT:
+			case keyCode.DOWN:
+				toFocus = this.headers[ ( currentIndex + 1 ) % length ];
+				break;
+			case keyCode.LEFT:
+			case keyCode.UP:
+				toFocus = this.headers[ ( currentIndex - 1 + length ) % length ];
+				break;
+			case keyCode.SPACE:
+			case keyCode.ENTER:
+				this._clickHandler( { target: event.target }, event.target );
+				event.preventDefault();
+		}
+
+		if ( toFocus ) {
+			$( event.target ).attr( "tabIndex", -1 );
+			$( toFocus ).attr( "tabIndex", 0 );
+			toFocus.focus();
+			return false;
+		}
+
+		return true;
+	},
+
+	resize: function() {
+		var options = this.options,
+			maxHeight;
+
+		if ( options.fillSpace ) {
+			if ( $.browser.msie ) {
+				var defOverflow = this.element.parent().css( "overflow" );
+				this.element.parent().css( "overflow", "hidden");
+			}
+			maxHeight = this.element.parent().height();
+			if ($.browser.msie) {
+				this.element.parent().css( "overflow", defOverflow );
+			}
+
+			this.headers.each(function() {
+				maxHeight -= $( this ).outerHeight( true );
+			});
+
+			this.headers.next()
+				.each(function() {
+					$( this ).height( Math.max( 0, maxHeight -
+						$( this ).innerHeight() + $( this ).height() ) );
+				})
+				.css( "overflow", "auto" );
+		} else if ( options.autoHeight ) {
+			maxHeight = 0;
+			this.headers.next()
+				.each(function() {
+					maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
+				})
+				.height( maxHeight );
+		}
+
+		return this;
+	},
+
+	activate: function( index ) {
+		// TODO this gets called on init, changing the option without an explicit call for that
+		this.options.active = index;
+		// call clickHandler with custom event
+		var active = this._findActive( index )[ 0 ];
+		this._clickHandler( { target: active }, active );
+
+		return this;
+	},
+
+	_findActive: function( selector ) {
+		return selector
+			? typeof selector === "number"
+				? this.headers.filter( ":eq(" + selector + ")" )
+				: this.headers.not( this.headers.not( selector ) )
+			: selector === false
+				? $( [] )
+				: this.headers.filter( ":eq(0)" );
+	},
+
+	// TODO isn't event.target enough? why the separate target argument?
+	_clickHandler: function( event, target ) {
+		var options = this.options;
+		if ( options.disabled ) {
+			return;
+		}
+
+		// called only when using activate(false) to close all parts programmatically
+		if ( !event.target ) {
+			if ( !options.collapsible ) {
+				return;
+			}
+			this.active
+				.removeClass( "ui-state-active ui-corner-top" )
+				.addClass( "ui-state-default ui-corner-all" )
+				.children( ".ui-icon" )
+					.removeClass( options.icons.headerSelected )
+					.addClass( options.icons.header );
+			this.active.next().addClass( "ui-accordion-content-active" );
+			var toHide = this.active.next(),
+				data = {
+					options: options,
+					newHeader: $( [] ),
+					oldHeader: options.active,
+					newContent: $( [] ),
+					oldContent: toHide
+				},
+				toShow = ( this.active = $( [] ) );
+			this._toggle( toShow, toHide, data );
+			return;
+		}
+
+		// get the click target
+		var clicked = $( event.currentTarget || target ),
+			clickedIsActive = clicked[0] === this.active[0];
+
+		// TODO the option is changed, is that correct?
+		// TODO if it is correct, shouldn't that happen after determining that the click is valid?
+		options.active = options.collapsible && clickedIsActive ?
+			false :
+			this.headers.index( clicked );
+
+		// if animations are still active, or the active header is the target, ignore click
+		if ( this.running || ( !options.collapsible && clickedIsActive ) ) {
+			return;
+		}
+
+		// find elements to show and hide
+		var active = this.active,
+			toShow = clicked.next(),
+			toHide = this.active.next(),
+			data = {
+				options: options,
+				newHeader: clickedIsActive && options.collapsible ? $([]) : clicked,
+				oldHeader: this.active,
+				newContent: clickedIsActive && options.collapsible ? $([]) : toShow,
+				oldContent: toHide
+			},
+			down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] );
+
+		// when the call to ._toggle() comes after the class changes
+		// it causes a very odd bug in IE 8 (see #6720)
+		this.active = clickedIsActive ? $([]) : clicked;
+		this._toggle( toShow, toHide, data, clickedIsActive, down );
+
+		// switch classes
+		active
+			.removeClass( "ui-state-active ui-corner-top" )
+			.addClass( "ui-state-default ui-corner-all" )
+			.children( ".ui-icon" )
+				.removeClass( options.icons.headerSelected )
+				.addClass( options.icons.header );
+		if ( !clickedIsActive ) {
+			clicked
+				.removeClass( "ui-state-default ui-corner-all" )
+				.addClass( "ui-state-active ui-corner-top" )
+				.children( ".ui-icon" )
+					.removeClass( options.icons.header )
+					.addClass( options.icons.headerSelected );
+			clicked
+				.next()
+				.addClass( "ui-accordion-content-active" );
+		}
+
+		return;
+	},
+
+	_toggle: function( toShow, toHide, data, clickedIsActive, down ) {
+		var self = this,
+			options = self.options;
+
+		self.toShow = toShow;
+		self.toHide = toHide;
+		self.data = data;
+
+		var complete = function() {
+			if ( !self ) {
+				return;
+			}
+			return self._completed.apply( self, arguments );
+		};
+
+		// trigger changestart event
+		self._trigger( "changestart", null, self.data );
+
+		// count elements to animate
+		self.running = toHide.size() === 0 ? toShow.size() : toHide.size();
+
+		if ( options.animated ) {
+			var animOptions = {};
+
+			if ( options.collapsible && clickedIsActive ) {
+				animOptions = {
+					toShow: $( [] ),
+					toHide: toHide,
+					complete: complete,
+					down: down,
+					autoHeight: options.autoHeight || options.fillSpace
+				};
+			} else {
+				animOptions = {
+					toShow: toShow,
+					toHide: toHide,
+					complete: complete,
+					down: down,
+					autoHeight: options.autoHeight || options.fillSpace
+				};
+			}
+
+			if ( !options.proxied ) {
+				options.proxied = options.animated;
+			}
+
+			if ( !options.proxiedDuration ) {
+				options.proxiedDuration = options.duration;
+			}
+
+			options.animated = $.isFunction( options.proxied ) ?
+				options.proxied( animOptions ) :
+				options.proxied;
+
+			options.duration = $.isFunction( options.proxiedDuration ) ?
+				options.proxiedDuration( animOptions ) :
+				options.proxiedDuration;
+
+			var animations = $.ui.accordion.animations,
+				duration = options.duration,
+				easing = options.animated;
+
+			if ( easing && !animations[ easing ] && !$.easing[ easing ] ) {
+				easing = "slide";
+			}
+			if ( !animations[ easing ] ) {
+				animations[ easing ] = function( options ) {
+					this.slide( options, {
+						easing: easing,
+						duration: duration || 700
+					});
+				};
+			}
+
+			animations[ easing ]( animOptions );
+		} else {
+			if ( options.collapsible && clickedIsActive ) {
+				toShow.toggle();
+			} else {
+				toHide.hide();
+				toShow.show();
+			}
+
+			complete( true );
+		}
+
+		// TODO assert that the blur and focus triggers are really necessary, remove otherwise
+		toHide.prev()
+			.attr({
+				"aria-expanded": "false",
+				"aria-selected": "false",
+				tabIndex: -1
+			})
+			.blur();
+		toShow.prev()
+			.attr({
+				"aria-expanded": "true",
+				"aria-selected": "true",
+				tabIndex: 0
+			})
+			.focus();
+	},
+
+	_completed: function( cancel ) {
+		this.running = cancel ? 0 : --this.running;
+		if ( this.running ) {
+			return;
+		}
+
+		if ( this.options.clearStyle ) {
+			this.toShow.add( this.toHide ).css({
+				height: "",
+				overflow: ""
+			});
+		}
+
+		// other classes are removed before the animation; this one needs to stay until completed
+		this.toHide.removeClass( "ui-accordion-content-active" );
+		// Work around for rendering bug in IE (#5421)
+		if ( this.toHide.length ) {
+			this.toHide.parent()[0].className = this.toHide.parent()[0].className;
+		}
+
+		this._trigger( "change", null, this.data );
+	}
+});
+
+$.extend( $.ui.accordion, {
+	version: "@VERSION",
+	animations: {
+		slide: function( options, additions ) {
+			options = $.extend({
+				easing: "swing",
+				duration: 300
+			}, options, additions );
+			if ( !options.toHide.size() ) {
+				options.toShow.animate({
+					height: "show",
+					paddingTop: "show",
+					paddingBottom: "show"
+				}, options );
+				return;
+			}
+			if ( !options.toShow.size() ) {
+				options.toHide.animate({
+					height: "hide",
+					paddingTop: "hide",
+					paddingBottom: "hide"
+				}, options );
+				return;
+			}
+			var overflow = options.toShow.css( "overflow" ),
+				percentDone = 0,
+				showProps = {},
+				hideProps = {},
+				fxAttrs = [ "height", "paddingTop", "paddingBottom" ],
+				originalWidth;
+			// fix width before calculating height of hidden element
+			var s = options.toShow;
+			originalWidth = s[0].style.width;
+			s.width( s.parent().width()
+				- parseFloat( s.css( "paddingLeft" ) )
+				- parseFloat( s.css( "paddingRight" ) )
+				- ( parseFloat( s.css( "borderLeftWidth" ) ) || 0 )
+				- ( parseFloat( s.css( "borderRightWidth" ) ) || 0 ) );
+
+			$.each( fxAttrs, function( i, prop ) {
+				hideProps[ prop ] = "hide";
+
+				var parts = ( "" + $.css( options.toShow[0], prop ) ).match( /^([\d+-.]+)(.*)$/ );
+				showProps[ prop ] = {
+					value: parts[ 1 ],
+					unit: parts[ 2 ] || "px"
+				};
+			});
+			options.toShow.css({ height: 0, overflow: "hidden" }).show();
+			options.toHide
+				.filter( ":hidden" )
+					.each( options.complete )
+				.end()
+				.filter( ":visible" )
+				.animate( hideProps, {
+				step: function( now, settings ) {
+					// only calculate the percent when animating height
+					// IE gets very inconsistent results when animating elements
+					// with small values, which is common for padding
+					if ( settings.prop == "height" ) {
+						percentDone = ( settings.end - settings.start === 0 ) ? 0 :
+							( settings.now - settings.start ) / ( settings.end - settings.start );
+					}
+
+					options.toShow[ 0 ].style[ settings.prop ] =
+						( percentDone * showProps[ settings.prop ].value )
+						+ showProps[ settings.prop ].unit;
+				},
+				duration: options.duration,
+				easing: options.easing,
+				complete: function() {
+					if ( !options.autoHeight ) {
+						options.toShow.css( "height", "" );
+					}
+					options.toShow.css({
+						width: originalWidth,
+						overflow: overflow
+					});
+					options.complete();
+				}
+			});
+		},
+		bounceslide: function( options ) {
+			this.slide( options, {
+				easing: options.down ? "easeOutBounce" : "swing",
+				duration: options.down ? 1000 : 200
+			});
+		}
+	}
+});
+
+})( jQuery );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/autocomplete.js b/client/apollo/jslib/jqueryui/autocomplete.js
new file mode 100644
index 0000000..9bc475e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/autocomplete.js
@@ -0,0 +1,627 @@
+define(['jquery','./core','./widget','./position'], function (jQuery) {
+/*
+ * jQuery UI Autocomplete @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ */
+(function( $, undefined ) {
+
+// used to prevent race conditions with remote data sources
+var requestIndex = 0;
+
+$.widget( "ui.autocomplete", {
+	options: {
+		appendTo: "body",
+		autoFocus: false,
+		delay: 300,
+		minLength: 1,
+		position: {
+			my: "left top",
+			at: "left bottom",
+			collision: "none"
+		},
+		source: null
+	},
+
+	pending: 0,
+
+	_create: function() {
+		var self = this,
+			doc = this.element[ 0 ].ownerDocument,
+			suppressKeyPress;
+
+		this.element
+			.addClass( "ui-autocomplete-input" )
+			.attr( "autocomplete", "off" )
+			// TODO verify these actually work as intended
+			.attr({
+				role: "textbox",
+				"aria-autocomplete": "list",
+				"aria-haspopup": "true"
+			})
+			.bind( "keydown.autocomplete", function( event ) {
+				if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
+					return;
+				}
+
+				suppressKeyPress = false;
+				var keyCode = $.ui.keyCode;
+				switch( event.keyCode ) {
+				case keyCode.PAGE_UP:
+					self._move( "previousPage", event );
+					break;
+				case keyCode.PAGE_DOWN:
+					self._move( "nextPage", event );
+					break;
+				case keyCode.UP:
+					self._move( "previous", event );
+					// prevent moving cursor to beginning of text field in some browsers
+					event.preventDefault();
+					break;
+				case keyCode.DOWN:
+					self._move( "next", event );
+					// prevent moving cursor to end of text field in some browsers
+					event.preventDefault();
+					break;
+				case keyCode.ENTER:
+				case keyCode.NUMPAD_ENTER:
+					// when menu is open and has focus
+					if ( self.menu.active ) {
+						// #6055 - Opera still allows the keypress to occur
+						// which causes forms to submit
+						suppressKeyPress = true;
+						event.preventDefault();
+					}
+					//passthrough - ENTER and TAB both select the current element
+				case keyCode.TAB:
+					if ( !self.menu.active ) {
+						return;
+					}
+					self.menu.select( event );
+					break;
+				case keyCode.ESCAPE:
+					self.element.val( self.term );
+					self.close( event );
+					break;
+				default:
+					// keypress is triggered before the input value is changed
+					clearTimeout( self.searching );
+					self.searching = setTimeout(function() {
+						// only search if the value has changed
+						if ( self.term != self.element.val() ) {
+							self.selectedItem = null;
+							self.search( null, event );
+						}
+					}, self.options.delay );
+					break;
+				}
+			})
+			.bind( "keypress.autocomplete", function( event ) {
+				if ( suppressKeyPress ) {
+					suppressKeyPress = false;
+					event.preventDefault();
+				}
+			})
+			.bind( "focus.autocomplete", function() {
+				if ( self.options.disabled ) {
+					return;
+				}
+
+				self.selectedItem = null;
+				self.previous = self.element.val();
+			})
+			.bind( "blur.autocomplete", function( event ) {
+				if ( self.options.disabled ) {
+					return;
+				}
+
+				clearTimeout( self.searching );
+				// clicks on the menu (or a button to trigger a search) will cause a blur event
+				self.closing = setTimeout(function() {
+					self.close( event );
+					self._change( event );
+				}, 150 );
+			});
+		this._initSource();
+		this.response = function() {
+			return self._response.apply( self, arguments );
+		};
+		this.menu = $( "<ul></ul>" )
+			.addClass( "ui-autocomplete" )
+			.appendTo( $( this.options.appendTo || "body", doc )[0] )
+			// prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
+			.mousedown(function( event ) {
+				// clicking on the scrollbar causes focus to shift to the body
+				// but we can't detect a mouseup or a click immediately afterward
+				// so we have to track the next mousedown and close the menu if
+				// the user clicks somewhere outside of the autocomplete
+				var menuElement = self.menu.element[ 0 ];
+				if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
+					setTimeout(function() {
+						$( document ).one( 'mousedown', function( event ) {
+							if ( event.target !== self.element[ 0 ] &&
+								event.target !== menuElement &&
+								!$.ui.contains( menuElement, event.target ) ) {
+								self.close();
+							}
+						});
+					}, 1 );
+				}
+
+				// use another timeout to make sure the blur-event-handler on the input was already triggered
+				setTimeout(function() {
+					clearTimeout( self.closing );
+				}, 13);
+			})
+			.menu({
+				focus: function( event, ui ) {
+					var item = ui.item.data( "item.autocomplete" );
+					if ( false !== self._trigger( "focus", event, { item: item } ) ) {
+						// use value to match what will end up in the input, if it was a key event
+						if ( /^key/.test(event.originalEvent.type) ) {
+							self.element.val( item.value );
+						}
+					}
+				},
+				selected: function( event, ui ) {
+					var item = ui.item.data( "item.autocomplete" ),
+						previous = self.previous;
+
+					// only trigger when focus was lost (click on menu)
+					if ( self.element[0] !== doc.activeElement ) {
+						self.element.focus();
+						self.previous = previous;
+						// #6109 - IE triggers two focus events and the second
+						// is asynchronous, so we need to reset the previous
+						// term synchronously and asynchronously :-(
+						setTimeout(function() {
+							self.previous = previous;
+							self.selectedItem = item;
+						}, 1);
+					}
+
+					if ( false !== self._trigger( "select", event, { item: item } ) ) {
+						self.element.val( item.value );
+					}
+					// reset the term after the select event
+					// this allows custom select handling to work properly
+					self.term = self.element.val();
+
+					self.close( event );
+					self.selectedItem = item;
+				},
+				blur: function( event, ui ) {
+					// don't set the value of the text field if it's already correct
+					// this prevents moving the cursor unnecessarily
+					if ( self.menu.element.is(":visible") &&
+						( self.element.val() !== self.term ) ) {
+						self.element.val( self.term );
+					}
+				}
+			})
+			.zIndex( this.element.zIndex() + 1 )
+			// workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
+			.css({ top: 0, left: 0 })
+			.hide()
+			.data( "menu" );
+		if ( $.fn.bgiframe ) {
+			 this.menu.element.bgiframe();
+		}
+		// turning off autocomplete prevents the browser from remembering the
+		// value when navigating through history, so we re-enable autocomplete
+		// if the page is unloaded before the widget is destroyed. #7790
+		self.beforeunloadHandler = function() {
+			self.element.removeAttr( "autocomplete" );
+		};
+		$( window ).bind( "beforeunload", self.beforeunloadHandler );
+	},
+
+	destroy: function() {
+		this.element
+			.removeClass( "ui-autocomplete-input" )
+			.removeAttr( "autocomplete" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-autocomplete" )
+			.removeAttr( "aria-haspopup" );
+		this.menu.element.remove();
+		$( window ).unbind( "beforeunload", this.beforeunloadHandler );
+		$.Widget.prototype.destroy.call( this );
+	},
+
+	_setOption: function( key, value ) {
+		$.Widget.prototype._setOption.apply( this, arguments );
+		if ( key === "source" ) {
+			this._initSource();
+		}
+		if ( key === "appendTo" ) {
+			this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
+		}
+		if ( key === "disabled" && value && this.xhr ) {
+			this.xhr.abort();
+		}
+	},
+
+	_initSource: function() {
+		var self = this,
+			array,
+			url;
+		if ( $.isArray(this.options.source) ) {
+			array = this.options.source;
+			this.source = function( request, response ) {
+				response( $.ui.autocomplete.filter(array, request.term) );
+			};
+		} else if ( typeof this.options.source === "string" ) {
+			url = this.options.source;
+			this.source = function( request, response ) {
+				if ( self.xhr ) {
+					self.xhr.abort();
+				}
+				self.xhr = $.ajax({
+					url: url,
+					data: request,
+					dataType: "json",
+					context: {
+						autocompleteRequest: ++requestIndex
+					},
+					success: function( data, status ) {
+						if ( this.autocompleteRequest === requestIndex ) {
+							response( data );
+						}
+					},
+					error: function() {
+						if ( this.autocompleteRequest === requestIndex ) {
+							response( [] );
+						}
+					}
+				});
+			};
+		} else {
+			this.source = this.options.source;
+		}
+	},
+
+	search: function( value, event ) {
+		value = value != null ? value : this.element.val();
+
+		// always save the actual value, not the one passed as an argument
+		this.term = this.element.val();
+
+		if ( value.length < this.options.minLength ) {
+			return this.close( event );
+		}
+
+		clearTimeout( this.closing );
+		if ( this._trigger( "search", event ) === false ) {
+			return;
+		}
+
+		return this._search( value );
+	},
+
+	_search: function( value ) {
+		this.pending++;
+		this.element.addClass( "ui-autocomplete-loading" );
+
+		this.source( { term: value }, this.response );
+	},
+
+	_response: function( content ) {
+		if ( !this.options.disabled && content && content.length ) {
+			content = this._normalize( content );
+			this._suggest( content );
+			this._trigger( "open" );
+		} else {
+			this.close();
+		}
+		this.pending--;
+		if ( !this.pending ) {
+			this.element.removeClass( "ui-autocomplete-loading" );
+		}
+	},
+
+	close: function( event ) {
+		clearTimeout( this.closing );
+		if ( this.menu.element.is(":visible") ) {
+			this.menu.element.hide();
+			this.menu.deactivate();
+			this._trigger( "close", event );
+		}
+	},
+	
+	_change: function( event ) {
+		if ( this.previous !== this.element.val() ) {
+			this._trigger( "change", event, { item: this.selectedItem } );
+		}
+	},
+
+	_normalize: function( items ) {
+		// assume all items have the right format when the first item is complete
+		if ( items.length && items[0].label && items[0].value ) {
+			return items;
+		}
+		return $.map( items, function(item) {
+			if ( typeof item === "string" ) {
+				return {
+					label: item,
+					value: item
+				};
+			}
+			return $.extend({
+				label: item.label || item.value,
+				value: item.value || item.label
+			}, item );
+		});
+	},
+
+	_suggest: function( items ) {
+		var ul = this.menu.element
+			.empty()
+			.zIndex( this.element.zIndex() + 1 );
+		this._renderMenu( ul, items );
+		// TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
+		this.menu.deactivate();
+		this.menu.refresh();
+
+		// size and position menu
+		ul.show();
+		this._resizeMenu();
+		ul.position( $.extend({
+			of: this.element
+		}, this.options.position ));
+
+		if ( this.options.autoFocus ) {
+			this.menu.next( new $.Event("mouseover") );
+		}
+	},
+
+	_resizeMenu: function() {
+		var ul = this.menu.element;
+		ul.outerWidth( Math.max(
+			// Firefox wraps long text (possibly a rounding bug)
+			// so we add 1px to avoid the wrapping (#7513)
+			ul.width( "" ).outerWidth() + 1,
+			this.element.outerWidth()
+		) );
+	},
+
+	_renderMenu: function( ul, items ) {
+		var self = this;
+		$.each( items, function( index, item ) {
+			self._renderItem( ul, item );
+		});
+	},
+
+	_renderItem: function( ul, item) {
+		return $( "<li></li>" )
+			.data( "item.autocomplete", item )
+			.append( $( "<a></a>" ).text( item.label ) )
+			.appendTo( ul );
+	},
+
+	_move: function( direction, event ) {
+		if ( !this.menu.element.is(":visible") ) {
+			this.search( null, event );
+			return;
+		}
+		if ( this.menu.first() && /^previous/.test(direction) ||
+				this.menu.last() && /^next/.test(direction) ) {
+			this.element.val( this.term );
+			this.menu.deactivate();
+			return;
+		}
+		this.menu[ direction ]( event );
+	},
+
+	widget: function() {
+		return this.menu.element;
+	}
+});
+
+$.extend( $.ui.autocomplete, {
+	escapeRegex: function( value ) {
+		return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+	},
+	filter: function(array, term) {
+		var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
+		return $.grep( array, function(value) {
+			return matcher.test( value.label || value.value || value );
+		});
+	}
+});
+
+}( jQuery ));
+
+/*
+ * jQuery UI Menu (not officially released)
+ * 
+ * This widget isn't yet finished and the API is subject to change. We plan to finish
+ * it for the next release. You're welcome to give it a try anyway and give us feedback,
+ * as long as you're okay with migrating your code later on. We can help with that, too.
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *  jquery.ui.widget.js
+ */
+(function($) {
+
+$.widget("ui.menu", {
+	_create: function() {
+		var self = this;
+		this.element
+			.addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
+			.attr({
+				role: "listbox",
+				"aria-activedescendant": "ui-active-menuitem"
+			})
+			.click(function( event ) {
+				if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
+					return;
+				}
+				// temporary
+				event.preventDefault();
+				self.select( event );
+			});
+		this.refresh();
+	},
+	
+	refresh: function() {
+		var self = this;
+
+		// don't refresh list items that are already adapted
+		var items = this.element.children("li:not(.ui-menu-item):has(a)")
+			.addClass("ui-menu-item")
+			.attr("role", "menuitem");
+		
+		items.children("a")
+			.addClass("ui-corner-all")
+			.attr("tabindex", -1)
+			// mouseenter doesn't work with event delegation
+			.mouseenter(function( event ) {
+				self.activate( event, $(this).parent() );
+			})
+			.mouseleave(function() {
+				self.deactivate();
+			});
+	},
+
+	activate: function( event, item ) {
+		this.deactivate();
+		if (this.hasScroll()) {
+			var offset = item.offset().top - this.element.offset().top,
+				scroll = this.element.scrollTop(),
+				elementHeight = this.element.height();
+			if (offset < 0) {
+				this.element.scrollTop( scroll + offset);
+			} else if (offset >= elementHeight) {
+				this.element.scrollTop( scroll + offset - elementHeight + item.height());
+			}
+		}
+		this.active = item.eq(0)
+			.children("a")
+				.addClass("ui-state-hover")
+				.attr("id", "ui-active-menuitem")
+			.end();
+		this._trigger("focus", event, { item: item });
+	},
+
+	deactivate: function() {
+		if (!this.active) { return; }
+
+		this.active.children("a")
+			.removeClass("ui-state-hover")
+			.removeAttr("id");
+		this._trigger("blur");
+		this.active = null;
+	},
+
+	next: function(event) {
+		this.move("next", ".ui-menu-item:first", event);
+	},
+
+	previous: function(event) {
+		this.move("prev", ".ui-menu-item:last", event);
+	},
+
+	first: function() {
+		return this.active && !this.active.prevAll(".ui-menu-item").length;
+	},
+
+	last: function() {
+		return this.active && !this.active.nextAll(".ui-menu-item").length;
+	},
+
+	move: function(direction, edge, event) {
+		if (!this.active) {
+			this.activate(event, this.element.children(edge));
+			return;
+		}
+		var next = this.active[direction + "All"](".ui-menu-item").eq(0);
+		if (next.length) {
+			this.activate(event, next);
+		} else {
+			this.activate(event, this.element.children(edge));
+		}
+	},
+
+	// TODO merge with previousPage
+	nextPage: function(event) {
+		if (this.hasScroll()) {
+			// TODO merge with no-scroll-else
+			if (!this.active || this.last()) {
+				this.activate(event, this.element.children(".ui-menu-item:first"));
+				return;
+			}
+			var base = this.active.offset().top,
+				height = this.element.height(),
+				result = this.element.children(".ui-menu-item").filter(function() {
+					var close = $(this).offset().top - base - height + $(this).height();
+					// TODO improve approximation
+					return close < 10 && close > -10;
+				});
+
+			// TODO try to catch this earlier when scrollTop indicates the last page anyway
+			if (!result.length) {
+				result = this.element.children(".ui-menu-item:last");
+			}
+			this.activate(event, result);
+		} else {
+			this.activate(event, this.element.children(".ui-menu-item")
+				.filter(!this.active || this.last() ? ":first" : ":last"));
+		}
+	},
+
+	// TODO merge with nextPage
+	previousPage: function(event) {
+		if (this.hasScroll()) {
+			// TODO merge with no-scroll-else
+			if (!this.active || this.first()) {
+				this.activate(event, this.element.children(".ui-menu-item:last"));
+				return;
+			}
+
+			var base = this.active.offset().top,
+				height = this.element.height();
+				result = this.element.children(".ui-menu-item").filter(function() {
+					var close = $(this).offset().top - base + height - $(this).height();
+					// TODO improve approximation
+					return close < 10 && close > -10;
+				});
+
+			// TODO try to catch this earlier when scrollTop indicates the last page anyway
+			if (!result.length) {
+				result = this.element.children(".ui-menu-item:first");
+			}
+			this.activate(event, result);
+		} else {
+			this.activate(event, this.element.children(".ui-menu-item")
+				.filter(!this.active || this.first() ? ":last" : ":first"));
+		}
+	},
+
+	hasScroll: function() {
+		return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
+	},
+
+	select: function( event ) {
+		this._trigger("selected", event, { item: this.active });
+	}
+});
+
+}(jQuery));
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/button.js b/client/apollo/jslib/jqueryui/button.js
new file mode 100644
index 0000000..3d9ba86
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/button.js
@@ -0,0 +1,417 @@
+define(['jquery','./core','./widget'], function (jQuery) {
+/*
+ * jQuery UI Button @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var lastActive, startXPos, startYPos, clickDragged,
+	baseClasses = "ui-button ui-widget ui-state-default ui-corner-all",
+	stateClasses = "ui-state-hover ui-state-active ",
+	typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",
+	formResetHandler = function() {
+		var buttons = $( this ).find( ":ui-button" );
+		setTimeout(function() {
+			buttons.button( "refresh" );
+		}, 1 );
+	},
+	radioGroup = function( radio ) {
+		var name = radio.name,
+			form = radio.form,
+			radios = $( [] );
+		if ( name ) {
+			if ( form ) {
+				radios = $( form ).find( "[name='" + name + "']" );
+			} else {
+				radios = $( "[name='" + name + "']", radio.ownerDocument )
+					.filter(function() {
+						return !this.form;
+					});
+			}
+		}
+		return radios;
+	};
+
+$.widget( "ui.button", {
+	options: {
+		disabled: null,
+		text: true,
+		label: null,
+		icons: {
+			primary: null,
+			secondary: null
+		}
+	},
+	_create: function() {
+		this.element.closest( "form" )
+			.unbind( "reset.button" )
+			.bind( "reset.button", formResetHandler );
+
+		if ( typeof this.options.disabled !== "boolean" ) {
+			this.options.disabled = !!this.element.propAttr( "disabled" );
+		} else {
+			this.element.propAttr( "disabled", this.options.disabled );
+		}
+
+		this._determineButtonType();
+		this.hasTitle = !!this.buttonElement.attr( "title" );
+
+		var self = this,
+			options = this.options,
+			toggleButton = this.type === "checkbox" || this.type === "radio",
+			hoverClass = "ui-state-hover" + ( !toggleButton ? " ui-state-active" : "" ),
+			focusClass = "ui-state-focus";
+
+		if ( options.label === null ) {
+			options.label = this.buttonElement.html();
+		}
+
+		this.buttonElement
+			.addClass( baseClasses )
+			.attr( "role", "button" )
+			.bind( "mouseenter.button", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).addClass( "ui-state-hover" );
+				if ( this === lastActive ) {
+					$( this ).addClass( "ui-state-active" );
+				}
+			})
+			.bind( "mouseleave.button", function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( hoverClass );
+			})
+			.bind( "click.button", function( event ) {
+				if ( options.disabled ) {
+					event.preventDefault();
+					event.stopImmediatePropagation();
+				}
+			});
+
+		this.element
+			.bind( "focus.button", function() {
+				// no need to check disabled, focus won't be triggered anyway
+				self.buttonElement.addClass( focusClass );
+			})
+			.bind( "blur.button", function() {
+				self.buttonElement.removeClass( focusClass );
+			});
+
+		if ( toggleButton ) {
+			this.element.bind( "change.button", function() {
+				if ( clickDragged ) {
+					return;
+				}
+				self.refresh();
+			});
+			// if mouse moves between mousedown and mouseup (drag) set clickDragged flag
+			// prevents issue where button state changes but checkbox/radio checked state
+			// does not in Firefox (see ticket #6970)
+			this.buttonElement
+				.bind( "mousedown.button", function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					clickDragged = false;
+					startXPos = event.pageX;
+					startYPos = event.pageY;
+				})
+				.bind( "mouseup.button", function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					if ( startXPos !== event.pageX || startYPos !== event.pageY ) {
+						clickDragged = true;
+					}
+			});
+		}
+
+		if ( this.type === "checkbox" ) {
+			this.buttonElement.bind( "click.button", function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).toggleClass( "ui-state-active" );
+				self.buttonElement.attr( "aria-pressed", self.element[0].checked );
+			});
+		} else if ( this.type === "radio" ) {
+			this.buttonElement.bind( "click.button", function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).addClass( "ui-state-active" );
+				self.buttonElement.attr( "aria-pressed", "true" );
+
+				var radio = self.element[ 0 ];
+				radioGroup( radio )
+					.not( radio )
+					.map(function() {
+						return $( this ).button( "widget" )[ 0 ];
+					})
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			});
+		} else {
+			this.buttonElement
+				.bind( "mousedown.button", function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).addClass( "ui-state-active" );
+					lastActive = this;
+					$( document ).one( "mouseup", function() {
+						lastActive = null;
+					});
+				})
+				.bind( "mouseup.button", function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).removeClass( "ui-state-active" );
+				})
+				.bind( "keydown.button", function(event) {
+					if ( options.disabled ) {
+						return false;
+					}
+					if ( event.keyCode == $.ui.keyCode.SPACE || event.keyCode == $.ui.keyCode.ENTER ) {
+						$( this ).addClass( "ui-state-active" );
+					}
+				})
+				.bind( "keyup.button", function() {
+					$( this ).removeClass( "ui-state-active" );
+				});
+
+			if ( this.buttonElement.is("a") ) {
+				this.buttonElement.keyup(function(event) {
+					if ( event.keyCode === $.ui.keyCode.SPACE ) {
+						// TODO pass through original event correctly (just as 2nd argument doesn't work)
+						$( this ).click();
+					}
+				});
+			}
+		}
+
+		// TODO: pull out $.Widget's handling for the disabled option into
+		// $.Widget.prototype._setOptionDisabled so it's easy to proxy and can
+		// be overridden by individual plugins
+		this._setOption( "disabled", options.disabled );
+		this._resetButton();
+	},
+
+	_determineButtonType: function() {
+
+		if ( this.element.is(":checkbox") ) {
+			this.type = "checkbox";
+		} else if ( this.element.is(":radio") ) {
+			this.type = "radio";
+		} else if ( this.element.is("input") ) {
+			this.type = "input";
+		} else {
+			this.type = "button";
+		}
+
+		if ( this.type === "checkbox" || this.type === "radio" ) {
+			// we don't search against the document in case the element
+			// is disconnected from the DOM
+			var ancestor = this.element.parents().filter(":last"),
+				labelSelector = "label[for='" + this.element.attr("id") + "']";
+			this.buttonElement = ancestor.find( labelSelector );
+			if ( !this.buttonElement.length ) {
+				ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings();
+				this.buttonElement = ancestor.filter( labelSelector );
+				if ( !this.buttonElement.length ) {
+					this.buttonElement = ancestor.find( labelSelector );
+				}
+			}
+			this.element.addClass( "ui-helper-hidden-accessible" );
+
+			var checked = this.element.is( ":checked" );
+			if ( checked ) {
+				this.buttonElement.addClass( "ui-state-active" );
+			}
+			this.buttonElement.attr( "aria-pressed", checked );
+		} else {
+			this.buttonElement = this.element;
+		}
+	},
+
+	widget: function() {
+		return this.buttonElement;
+	},
+
+	destroy: function() {
+		this.element
+			.removeClass( "ui-helper-hidden-accessible" );
+		this.buttonElement
+			.removeClass( baseClasses + " " + stateClasses + " " + typeClasses )
+			.removeAttr( "role" )
+			.removeAttr( "aria-pressed" )
+			.html( this.buttonElement.find(".ui-button-text").html() );
+
+		if ( !this.hasTitle ) {
+			this.buttonElement.removeAttr( "title" );
+		}
+
+		$.Widget.prototype.destroy.call( this );
+	},
+
+	_setOption: function( key, value ) {
+		$.Widget.prototype._setOption.apply( this, arguments );
+		if ( key === "disabled" ) {
+			if ( value ) {
+				this.element.propAttr( "disabled", true );
+			} else {
+				this.element.propAttr( "disabled", false );
+			}
+			return;
+		}
+		this._resetButton();
+	},
+
+	refresh: function() {
+		var isDisabled = this.element.is( ":disabled" );
+		if ( isDisabled !== this.options.disabled ) {
+			this._setOption( "disabled", isDisabled );
+		}
+		if ( this.type === "radio" ) {
+			radioGroup( this.element[0] ).each(function() {
+				if ( $( this ).is( ":checked" ) ) {
+					$( this ).button( "widget" )
+						.addClass( "ui-state-active" )
+						.attr( "aria-pressed", "true" );
+				} else {
+					$( this ).button( "widget" )
+						.removeClass( "ui-state-active" )
+						.attr( "aria-pressed", "false" );
+				}
+			});
+		} else if ( this.type === "checkbox" ) {
+			if ( this.element.is( ":checked" ) ) {
+				this.buttonElement
+					.addClass( "ui-state-active" )
+					.attr( "aria-pressed", "true" );
+			} else {
+				this.buttonElement
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			}
+		}
+	},
+
+	_resetButton: function() {
+		if ( this.type === "input" ) {
+			if ( this.options.label ) {
+				this.element.val( this.options.label );
+			}
+			return;
+		}
+		var buttonElement = this.buttonElement.removeClass( typeClasses ),
+			buttonText = $( "<span></span>", this.element[0].ownerDocument )
+				.addClass( "ui-button-text" )
+				.html( this.options.label )
+				.appendTo( buttonElement.empty() )
+				.text(),
+			icons = this.options.icons,
+			multipleIcons = icons.primary && icons.secondary,
+			buttonClasses = [];  
+
+		if ( icons.primary || icons.secondary ) {
+			if ( this.options.text ) {
+				buttonClasses.push( "ui-button-text-icon" + ( multipleIcons ? "s" : ( icons.primary ? "-primary" : "-secondary" ) ) );
+			}
+
+			if ( icons.primary ) {
+				buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" );
+			}
+
+			if ( icons.secondary ) {
+				buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" );
+			}
+
+			if ( !this.options.text ) {
+				buttonClasses.push( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" );
+
+				if ( !this.hasTitle ) {
+					buttonElement.attr( "title", buttonText );
+				}
+			}
+		} else {
+			buttonClasses.push( "ui-button-text-only" );
+		}
+		buttonElement.addClass( buttonClasses.join( " " ) );
+	}
+});
+
+$.widget( "ui.buttonset", {
+	options: {
+		items: ":button, :submit, :reset, :checkbox, :radio, a, :data(button)"
+	},
+
+	_create: function() {
+		this.element.addClass( "ui-buttonset" );
+	},
+	
+	_init: function() {
+		this.refresh();
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "disabled" ) {
+			this.buttons.button( "option", key, value );
+		}
+
+		$.Widget.prototype._setOption.apply( this, arguments );
+	},
+	
+	refresh: function() {
+		var rtl = this.element.css( "direction" ) === "rtl";
+		
+		this.buttons = this.element.find( this.options.items )
+			.filter( ":ui-button" )
+				.button( "refresh" )
+			.end()
+			.not( ":ui-button" )
+				.button()
+			.end()
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-all ui-corner-left ui-corner-right" )
+				.filter( ":first" )
+					.addClass( rtl ? "ui-corner-right" : "ui-corner-left" )
+				.end()
+				.filter( ":last" )
+					.addClass( rtl ? "ui-corner-left" : "ui-corner-right" )
+				.end()
+			.end();
+	},
+
+	destroy: function() {
+		this.element.removeClass( "ui-buttonset" );
+		this.buttons
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-left ui-corner-right" )
+			.end()
+			.button( "destroy" );
+
+		$.Widget.prototype.destroy.call( this );
+	}
+});
+
+}( jQuery ) );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/core.js b/client/apollo/jslib/jqueryui/core.js
new file mode 100644
index 0000000..c6ee100
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/core.js
@@ -0,0 +1,322 @@
+define(['jquery'], function (jQuery) {
+/*!
+ * jQuery UI @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI
+ */
+(function( $, undefined ) {
+
+// prevent duplicate loading
+// this is only a problem because we proxy existing functions
+// and we don't want to double proxy them
+$.ui = $.ui || {};
+if ( $.ui.version ) {
+	return;
+}
+
+$.extend( $.ui, {
+	version: "@VERSION",
+
+	keyCode: {
+		ALT: 18,
+		BACKSPACE: 8,
+		CAPS_LOCK: 20,
+		COMMA: 188,
+		COMMAND: 91,
+		COMMAND_LEFT: 91, // COMMAND
+		COMMAND_RIGHT: 93,
+		CONTROL: 17,
+		DELETE: 46,
+		DOWN: 40,
+		END: 35,
+		ENTER: 13,
+		ESCAPE: 27,
+		HOME: 36,
+		INSERT: 45,
+		LEFT: 37,
+		MENU: 93, // COMMAND_RIGHT
+		NUMPAD_ADD: 107,
+		NUMPAD_DECIMAL: 110,
+		NUMPAD_DIVIDE: 111,
+		NUMPAD_ENTER: 108,
+		NUMPAD_MULTIPLY: 106,
+		NUMPAD_SUBTRACT: 109,
+		PAGE_DOWN: 34,
+		PAGE_UP: 33,
+		PERIOD: 190,
+		RIGHT: 39,
+		SHIFT: 16,
+		SPACE: 32,
+		TAB: 9,
+		UP: 38,
+		WINDOWS: 91 // COMMAND
+	}
+});
+
+// plugins
+$.fn.extend({
+	propAttr: $.fn.prop || $.fn.attr,
+
+	_focus: $.fn.focus,
+	focus: function( delay, fn ) {
+		return typeof delay === "number" ?
+			this.each(function() {
+				var elem = this;
+				setTimeout(function() {
+					$( elem ).focus();
+					if ( fn ) {
+						fn.call( elem );
+					}
+				}, delay );
+			}) :
+			this._focus.apply( this, arguments );
+	},
+
+	scrollParent: function() {
+		var scrollParent;
+		if (($.browser.msie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) {
+			scrollParent = this.parents().filter(function() {
+				return (/(relative|absolute|fixed)/).test($.curCSS(this,'position',1)) && (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1));
+			}).eq(0);
+		} else {
+			scrollParent = this.parents().filter(function() {
+				return (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1));
+			}).eq(0);
+		}
+
+		return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent;
+	},
+
+	zIndex: function( zIndex ) {
+		if ( zIndex !== undefined ) {
+			return this.css( "zIndex", zIndex );
+		}
+
+		if ( this.length ) {
+			var elem = $( this[ 0 ] ), position, value;
+			while ( elem.length && elem[ 0 ] !== document ) {
+				// Ignore z-index if position is set to a value where z-index is ignored by the browser
+				// This makes behavior of this function consistent across browsers
+				// WebKit always returns auto if the element is positioned
+				position = elem.css( "position" );
+				if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+					// IE returns 0 when zIndex is not specified
+					// other browsers return a string
+					// we ignore the case of nested elements with an explicit value of 0
+					// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+					value = parseInt( elem.css( "zIndex" ), 10 );
+					if ( !isNaN( value ) && value !== 0 ) {
+						return value;
+					}
+				}
+				elem = elem.parent();
+			}
+		}
+
+		return 0;
+	},
+
+	disableSelection: function() {
+		return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
+			".ui-disableSelection", function( event ) {
+				event.preventDefault();
+			});
+	},
+
+	enableSelection: function() {
+		return this.unbind( ".ui-disableSelection" );
+	}
+});
+
+$.each( [ "Width", "Height" ], function( i, name ) {
+	var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
+		type = name.toLowerCase(),
+		orig = {
+			innerWidth: $.fn.innerWidth,
+			innerHeight: $.fn.innerHeight,
+			outerWidth: $.fn.outerWidth,
+			outerHeight: $.fn.outerHeight
+		};
+
+	function reduce( elem, size, border, margin ) {
+		$.each( side, function() {
+			size -= parseFloat( $.curCSS( elem, "padding" + this, true) ) || 0;
+			if ( border ) {
+				size -= parseFloat( $.curCSS( elem, "border" + this + "Width", true) ) || 0;
+			}
+			if ( margin ) {
+				size -= parseFloat( $.curCSS( elem, "margin" + this, true) ) || 0;
+			}
+		});
+		return size;
+	}
+
+	$.fn[ "inner" + name ] = function( size ) {
+		if ( size === undefined ) {
+			return orig[ "inner" + name ].call( this );
+		}
+
+		return this.each(function() {
+			$( this ).css( type, reduce( this, size ) + "px" );
+		});
+	};
+
+	$.fn[ "outer" + name] = function( size, margin ) {
+		if ( typeof size !== "number" ) {
+			return orig[ "outer" + name ].call( this, size );
+		}
+
+		return this.each(function() {
+			$( this).css( type, reduce( this, size, true, margin ) + "px" );
+		});
+	};
+});
+
+// selectors
+function focusable( element, isTabIndexNotNaN ) {
+	var nodeName = element.nodeName.toLowerCase();
+	if ( "area" === nodeName ) {
+		var map = element.parentNode,
+			mapName = map.name,
+			img;
+		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
+			return false;
+		}
+		img = $( "img[usemap=#" + mapName + "]" )[0];
+		return !!img && visible( img );
+	}
+	return ( /input|select|textarea|button|object/.test( nodeName )
+		? !element.disabled
+		: "a" == nodeName
+			? element.href || isTabIndexNotNaN
+			: isTabIndexNotNaN)
+		// the element and all of its ancestors must be visible
+		&& visible( element );
+}
+
+function visible( element ) {
+	return !$( element ).parents().andSelf().filter(function() {
+		return $.curCSS( this, "visibility" ) === "hidden" ||
+			$.expr.filters.hidden( this );
+	}).length;
+}
+
+$.extend( $.expr[ ":" ], {
+	data: function( elem, i, match ) {
+		return !!$.data( elem, match[ 3 ] );
+	},
+
+	focusable: function( element ) {
+		return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
+	},
+
+	tabbable: function( element ) {
+		var tabIndex = $.attr( element, "tabindex" ),
+			isTabIndexNaN = isNaN( tabIndex );
+		return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
+	}
+});
+
+// support
+$(function() {
+	var body = document.body,
+		div = body.appendChild( div = document.createElement( "div" ) );
+
+	// access offsetHeight before setting the style to prevent a layout bug
+	// in IE 9 which causes the elemnt to continue to take up space even
+	// after it is removed from the DOM (#8026)
+	div.offsetHeight;
+
+	$.extend( div.style, {
+		minHeight: "100px",
+		height: "auto",
+		padding: 0,
+		borderWidth: 0
+	});
+
+	$.support.minHeight = div.offsetHeight === 100;
+	$.support.selectstart = "onselectstart" in div;
+
+	// set display to none to avoid a layout bug in IE
+	// http://dev.jquery.com/ticket/4014
+	body.removeChild( div ).style.display = "none";
+});
+
+
+
+
+
+// deprecated
+$.extend( $.ui, {
+	// $.ui.plugin is deprecated.  Use the proxy pattern instead.
+	plugin: {
+		add: function( module, option, set ) {
+			var proto = $.ui[ module ].prototype;
+			for ( var i in set ) {
+				proto.plugins[ i ] = proto.plugins[ i ] || [];
+				proto.plugins[ i ].push( [ option, set[ i ] ] );
+			}
+		},
+		call: function( instance, name, args ) {
+			var set = instance.plugins[ name ];
+			if ( !set || !instance.element[ 0 ].parentNode ) {
+				return;
+			}
+	
+			for ( var i = 0; i < set.length; i++ ) {
+				if ( instance.options[ set[ i ][ 0 ] ] ) {
+					set[ i ][ 1 ].apply( instance.element, args );
+				}
+			}
+		}
+	},
+	
+	// will be deprecated when we switch to jQuery 1.4 - use jQuery.contains()
+	contains: function( a, b ) {
+		return document.compareDocumentPosition ?
+			a.compareDocumentPosition( b ) & 16 :
+			a !== b && a.contains( b );
+	},
+	
+	// only used by resizable
+	hasScroll: function( el, a ) {
+	
+		//If overflow is hidden, the element might have extra content, but the user wants to hide it
+		if ( $( el ).css( "overflow" ) === "hidden") {
+			return false;
+		}
+	
+		var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
+			has = false;
+	
+		if ( el[ scroll ] > 0 ) {
+			return true;
+		}
+	
+		// TODO: determine which cases actually cause this to happen
+		// if the element doesn't have the scroll set, see if it's possible to
+		// set the scroll
+		el[ scroll ] = 1;
+		has = ( el[ scroll ] > 0 );
+		el[ scroll ] = 0;
+		return has;
+	},
+	
+	// these are odd functions, fix the API or move into individual plugins
+	isOverAxis: function( x, reference, size ) {
+		//Determines when x coordinate is over "b" element axis
+		return ( x > reference ) && ( x < ( reference + size ) );
+	},
+	isOver: function( y, x, top, left, height, width ) {
+		//Determines when x, y coordinates is over "b" element
+		return $.ui.isOverAxis( y, top, height ) && $.ui.isOverAxis( x, left, width );
+	}
+});
+
+})( jQuery );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-af.js b/client/apollo/jslib/jqueryui/datepicker-af.js
new file mode 100644
index 0000000..20dd8c9
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-af.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Afrikaans initialisation for the jQuery UI date picker plugin. */
+/* Written by Renier Pretorius. */
+jQuery(function($){
+	$.datepicker.regional['af'] = {
+		closeText: 'Selekteer',
+		prevText: 'Vorige',
+		nextText: 'Volgende',
+		currentText: 'Vandag',
+		monthNames: ['Januarie','Februarie','Maart','April','Mei','Junie',
+		'Julie','Augustus','September','Oktober','November','Desember'],
+		monthNamesShort: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun',
+		'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
+		dayNames: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag'],
+		dayNamesShort: ['Son', 'Maa', 'Din', 'Woe', 'Don', 'Vry', 'Sat'],
+		dayNamesMin: ['So','Ma','Di','Wo','Do','Vr','Sa'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['af']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ar-DZ.js b/client/apollo/jslib/jqueryui/datepicker-ar-DZ.js
new file mode 100644
index 0000000..3f39b4b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ar-DZ.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Algerian Arabic Translation for jQuery UI date picker plugin. (can be used for Tunisia)*/
+/* Mohamed Cherif BOUCHELAGHEM -- cherifbouchelaghem at yahoo.fr */
+
+jQuery(function($){
+	$.datepicker.regional['ar-DZ'] = {
+		closeText: 'إغلاق',
+		prevText: '&#x3c;السابق',
+		nextText: 'التالي&#x3e;',
+		currentText: 'اليوم',
+		monthNames: ['جانفي', 'فيفري', 'مارس', 'أفريل', 'ماي', 'جوان',
+		'جويلية', 'أوت', 'سبتمبر','أكتوبر', 'نوفمبر', 'ديسمبر'],
+		monthNamesShort: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+		dayNames: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		dayNamesShort: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		dayNamesMin: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		weekHeader: 'أسبوع',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 6,
+  		isRTL: true,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ar-DZ']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ar.js b/client/apollo/jslib/jqueryui/datepicker-ar.js
new file mode 100644
index 0000000..50ea246
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ar.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Arabic Translation for jQuery UI date picker plugin. */
+/* Khaled Alhourani -- me at khaledalhourani.com */
+/* NOTE: monthNames are the original months names and they are the Arabic names, not the new months name فبراير - يناير and there isn't any Arabic roots for these months */
+jQuery(function($){
+	$.datepicker.regional['ar'] = {
+		closeText: 'إغلاق',
+		prevText: '&#x3c;السابق',
+		nextText: 'التالي&#x3e;',
+		currentText: 'اليوم',
+		monthNames: ['كانون الثاني', 'شباط', 'آذار', 'نيسان', 'مايو', 'حزيران',
+		'تموز', 'آب', 'أيلول',	'تشرين الأول', 'تشرين الثاني', 'كانون الأول'],
+		monthNamesShort: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+		dayNames: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		dayNamesShort: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		dayNamesMin: ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
+		weekHeader: 'أسبوع',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 6,
+  		isRTL: true,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ar']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-az.js b/client/apollo/jslib/jqueryui/datepicker-az.js
new file mode 100644
index 0000000..90c6d65
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-az.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Azerbaijani (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Jamil Najafov (necefov33 at gmail.com). */
+jQuery(function($) {
+	$.datepicker.regional['az'] = {
+		closeText: 'Bağla',
+		prevText: '&#x3c;Geri',
+		nextText: 'İrəli&#x3e;',
+		currentText: 'Bugün',
+		monthNames: ['Yanvar','Fevral','Mart','Aprel','May','İyun',
+		'İyul','Avqust','Sentyabr','Oktyabr','Noyabr','Dekabr'],
+		monthNamesShort: ['Yan','Fev','Mar','Apr','May','İyun',
+		'İyul','Avq','Sen','Okt','Noy','Dek'],
+		dayNames: ['Bazar','Bazar ertəsi','Çərşənbə axşamı','Çərşənbə','Cümə axşamı','Cümə','Şənbə'],
+		dayNamesShort: ['B','Be','Ça','Ç','Ca','C','Ş'],
+		dayNamesMin: ['B','B','Ç','С','Ç','C','Ş'],
+		weekHeader: 'Hf',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['az']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-bg.js b/client/apollo/jslib/jqueryui/datepicker-bg.js
new file mode 100644
index 0000000..7d2767f
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-bg.js
@@ -0,0 +1,27 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Bulgarian initialisation for the jQuery UI date picker plugin. */
+/* Written by Stoyan Kyosev (http://svest.org). */
+jQuery(function($){
+    $.datepicker.regional['bg'] = {
+        closeText: 'затвори',
+        prevText: '&#x3c;назад',
+        nextText: 'напред&#x3e;',
+		nextBigText: '&#x3e;&#x3e;',
+        currentText: 'днес',
+        monthNames: ['Януари','Февруари','Март','Април','Май','Юни',
+        'Юли','Август','Септември','Октомври','Ноември','Декември'],
+        monthNamesShort: ['Яну','Фев','Мар','Апр','Май','Юни',
+        'Юли','Авг','Сеп','Окт','Нов','Дек'],
+        dayNames: ['Неделя','Понеделник','Вторник','Сряда','Четвъртък','Петък','Събота'],
+        dayNamesShort: ['Нед','Пон','Вто','Сря','Чет','Пет','Съб'],
+        dayNamesMin: ['Не','По','Вт','Ср','Че','Пе','Съ'],
+		weekHeader: 'Wk',
+        dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+        isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+    $.datepicker.setDefaults($.datepicker.regional['bg']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-bs.js b/client/apollo/jslib/jqueryui/datepicker-bs.js
new file mode 100644
index 0000000..6b3afa7
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-bs.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Bosnian i18n for the jQuery UI date picker plugin. */
+/* Written by Kenan Konjo. */
+jQuery(function($){
+	$.datepicker.regional['bs'] = {
+		closeText: 'Zatvori', 
+		prevText: '&#x3c;', 
+		nextText: '&#x3e;', 
+		currentText: 'Danas', 
+		monthNames: ['Januar','Februar','Mart','April','Maj','Juni',
+		'Juli','August','Septembar','Oktobar','Novembar','Decembar'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedelja','Ponedeljak','Utorak','Srijeda','Četvrtak','Petak','Subota'],
+		dayNamesShort: ['Ned','Pon','Uto','Sri','Čet','Pet','Sub'],
+		dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['bs']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ca.js b/client/apollo/jslib/jqueryui/datepicker-ca.js
new file mode 100644
index 0000000..c73f04a
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ca.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Inicialització en català per a l'extenció 'calendar' per jQuery. */
+/* Writers: (joan.leon at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ca'] = {
+		closeText: 'Tancar',
+		prevText: '&#x3c;Ant',
+		nextText: 'Seg&#x3e;',
+		currentText: 'Avui',
+		monthNames: ['Gener','Febrer','Març','Abril','Maig','Juny',
+		'Juliol','Agost','Setembre','Octubre','Novembre','Desembre'],
+		monthNamesShort: ['Gen','Feb','Mar','Abr','Mai','Jun',
+		'Jul','Ago','Set','Oct','Nov','Des'],
+		dayNames: ['Diumenge','Dilluns','Dimarts','Dimecres','Dijous','Divendres','Dissabte'],
+		dayNamesShort: ['Dug','Dln','Dmt','Dmc','Djs','Dvn','Dsb'],
+		dayNamesMin: ['Dg','Dl','Dt','Dc','Dj','Dv','Ds'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ca']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-cs.js b/client/apollo/jslib/jqueryui/datepicker-cs.js
new file mode 100644
index 0000000..ed35aa3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-cs.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Czech initialisation for the jQuery UI date picker plugin. */
+/* Written by Tomas Muller (tomas at tomas-muller.net). */
+jQuery(function($){
+	$.datepicker.regional['cs'] = {
+		closeText: 'Zavřít',
+		prevText: '&#x3c;Dříve',
+		nextText: 'Později&#x3e;',
+		currentText: 'Nyní',
+		monthNames: ['leden','únor','březen','duben','květen','červen',
+        'červenec','srpen','září','říjen','listopad','prosinec'],
+		monthNamesShort: ['led','úno','bře','dub','kvě','čer',
+		'čvc','srp','zář','říj','lis','pro'],
+		dayNames: ['neděle', 'pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota'],
+		dayNamesShort: ['ne', 'po', 'út', 'st', 'čt', 'pá', 'so'],
+		dayNamesMin: ['ne','po','út','st','čt','pá','so'],
+		weekHeader: 'Týd',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['cs']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-cy-GB.js b/client/apollo/jslib/jqueryui/datepicker-cy-GB.js
new file mode 100644
index 0000000..5ec1a54
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-cy-GB.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Welsh/UK initialisation for the jQuery UI date picker plugin. */
+/* Written by William Griffiths. */
+jQuery(function($){
+	$.datepicker.regional['cy-GB'] = {
+		closeText: 'Done',
+		prevText: 'Prev',
+		nextText: 'Next',
+		currentText: 'Today',
+		monthNames: ['Ionawr','Chwefror','Mawrth','Ebrill','Mai','Mehefin',
+		'Gorffennaf','Awst','Medi','Hydref','Tachwedd','Rhagfyr'],
+		monthNamesShort: ['Ion', 'Chw', 'Maw', 'Ebr', 'Mai', 'Meh',
+		'Gor', 'Aws', 'Med', 'Hyd', 'Tac', 'Rha'],
+		dayNames: ['Dydd Sul', 'Dydd Llun', 'Dydd Mawrth', 'Dydd Mercher', 'Dydd Iau', 'Dydd Gwener', 'Dydd Sadwrn'],
+		dayNamesShort: ['Sul', 'Llu', 'Maw', 'Mer', 'Iau', 'Gwe', 'Sad'],
+		dayNamesMin: ['Su','Ll','Ma','Me','Ia','Gw','Sa'],
+		weekHeader: 'Wy',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['cy-GB']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-da.js b/client/apollo/jslib/jqueryui/datepicker-da.js
new file mode 100644
index 0000000..dc9b820
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-da.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Danish initialisation for the jQuery UI date picker plugin. */
+/* Written by Jan Christensen ( deletestuff at gmail.com). */
+jQuery(function($){
+    $.datepicker.regional['da'] = {
+		closeText: 'Luk',
+        prevText: '&#x3c;Forrige',
+		nextText: 'Næste&#x3e;',
+		currentText: 'Idag',
+        monthNames: ['Januar','Februar','Marts','April','Maj','Juni',
+        'Juli','August','September','Oktober','November','December'],
+        monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+        'Jul','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'],
+		dayNamesShort: ['Søn','Man','Tir','Ons','Tor','Fre','Lør'],
+		dayNamesMin: ['Sø','Ma','Ti','On','To','Fr','Lø'],
+		weekHeader: 'Uge',
+        dateFormat: 'dd-mm-yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+    $.datepicker.setDefaults($.datepicker.regional['da']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-de.js b/client/apollo/jslib/jqueryui/datepicker-de.js
new file mode 100644
index 0000000..a21cba1
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-de.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* German initialisation for the jQuery UI date picker plugin. */
+/* Written by Milian Wolff (mail at milianw.de). */
+jQuery(function($){
+	$.datepicker.regional['de'] = {
+		closeText: 'schließen',
+		prevText: '&#x3c;zurück',
+		nextText: 'Vor&#x3e;',
+		currentText: 'heute',
+		monthNames: ['Januar','Februar','März','April','Mai','Juni',
+		'Juli','August','September','Oktober','November','Dezember'],
+		monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun',
+		'Jul','Aug','Sep','Okt','Nov','Dez'],
+		dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'],
+		dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+		dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+		weekHeader: 'Wo',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['de']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-el.js b/client/apollo/jslib/jqueryui/datepicker-el.js
new file mode 100644
index 0000000..27940ed
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-el.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Greek (el) initialisation for the jQuery UI date picker plugin. */
+/* Written by Alex Cicovic (http://www.alexcicovic.com) */
+jQuery(function($){
+	$.datepicker.regional['el'] = {
+		closeText: 'Κλείσιμο',
+		prevText: 'Προηγούμενος',
+		nextText: 'Επόμενος',
+		currentText: 'Τρέχων Μήνας',
+		monthNames: ['Ιανουάριος','Φεβρουάριος','Μάρτιος','Απρίλιος','Μάιος','Ιούνιος',
+		'Ιούλιος','Αύγουστος','Σεπτέμβριος','Οκτώβριος','Νοέμβριος','Δεκέμβριος'],
+		monthNamesShort: ['Ιαν','Φεβ','Μαρ','Απρ','Μαι','Ιουν',
+		'Ιουλ','Αυγ','Σεπ','Οκτ','Νοε','Δεκ'],
+		dayNames: ['Κυριακή','Δευτέρα','Τρίτη','Τετάρτη','Πέμπτη','Παρασκευή','Σάββατο'],
+		dayNamesShort: ['Κυρ','Δευ','Τρι','Τετ','Πεμ','Παρ','Σαβ'],
+		dayNamesMin: ['Κυ','Δε','Τρ','Τε','Πε','Πα','Σα'],
+		weekHeader: 'Εβδ',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['el']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-en-AU.js b/client/apollo/jslib/jqueryui/datepicker-en-AU.js
new file mode 100644
index 0000000..b4756e6
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-en-AU.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* English/Australia initialisation for the jQuery UI date picker plugin. */
+/* Based on the en-GB initialisation. */
+jQuery(function($){
+	$.datepicker.regional['en-AU'] = {
+		closeText: 'Done',
+		prevText: 'Prev',
+		nextText: 'Next',
+		currentText: 'Today',
+		monthNames: ['January','February','March','April','May','June',
+		'July','August','September','October','November','December'],
+		monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+		'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+		dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+		dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+		dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['en-AU']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-en-GB.js b/client/apollo/jslib/jqueryui/datepicker-en-GB.js
new file mode 100644
index 0000000..0562a9b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-en-GB.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* English/UK initialisation for the jQuery UI date picker plugin. */
+/* Written by Stuart. */
+jQuery(function($){
+	$.datepicker.regional['en-GB'] = {
+		closeText: 'Done',
+		prevText: 'Prev',
+		nextText: 'Next',
+		currentText: 'Today',
+		monthNames: ['January','February','March','April','May','June',
+		'July','August','September','October','November','December'],
+		monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+		'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+		dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+		dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+		dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['en-GB']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-en-NZ.js b/client/apollo/jslib/jqueryui/datepicker-en-NZ.js
new file mode 100644
index 0000000..56a3025
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-en-NZ.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* English/New Zealand initialisation for the jQuery UI date picker plugin. */
+/* Based on the en-GB initialisation. */
+jQuery(function($){
+	$.datepicker.regional['en-NZ'] = {
+		closeText: 'Done',
+		prevText: 'Prev',
+		nextText: 'Next',
+		currentText: 'Today',
+		monthNames: ['January','February','March','April','May','June',
+		'July','August','September','October','November','December'],
+		monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+		'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+		dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+		dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+		dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['en-NZ']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-eo.js b/client/apollo/jslib/jqueryui/datepicker-eo.js
new file mode 100644
index 0000000..92fd5c0
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-eo.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Esperanto initialisation for the jQuery UI date picker plugin. */
+/* Written by Olivier M. (olivierweb at ifrance.com). */
+jQuery(function($){
+	$.datepicker.regional['eo'] = {
+		closeText: 'Fermi',
+		prevText: '<Anta',
+		nextText: 'Sekv>',
+		currentText: 'Nuna',
+		monthNames: ['Januaro','Februaro','Marto','Aprilo','Majo','Junio',
+		'Julio','Aŭgusto','Septembro','Oktobro','Novembro','Decembro'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Aŭg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Dimanĉo','Lundo','Mardo','Merkredo','Ĵaŭdo','Vendredo','Sabato'],
+		dayNamesShort: ['Dim','Lun','Mar','Mer','Ĵaŭ','Ven','Sab'],
+		dayNamesMin: ['Di','Lu','Ma','Me','Ĵa','Ve','Sa'],
+		weekHeader: 'Sb',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['eo']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-es.js b/client/apollo/jslib/jqueryui/datepicker-es.js
new file mode 100644
index 0000000..ab4569e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-es.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Inicialización en español para la extensión 'UI date picker' para jQuery. */
+/* Traducido por Vester (xvester at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['es'] = {
+		closeText: 'Cerrar',
+		prevText: '&#x3c;Ant',
+		nextText: 'Sig&#x3e;',
+		currentText: 'Hoy',
+		monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio',
+		'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
+		monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun',
+		'Jul','Ago','Sep','Oct','Nov','Dic'],
+		dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'],
+		dayNamesShort: ['Dom','Lun','Mar','Mié','Juv','Vie','Sáb'],
+		dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','Sá'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['es']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-et.js b/client/apollo/jslib/jqueryui/datepicker-et.js
new file mode 100644
index 0000000..20a7602
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-et.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Estonian initialisation for the jQuery UI date picker plugin. */
+/* Written by Mart Sõmermaa (mrts.pydev at gmail com). */
+jQuery(function($){
+	$.datepicker.regional['et'] = {
+		closeText: 'Sulge',
+		prevText: 'Eelnev',
+		nextText: 'Järgnev',
+		currentText: 'Täna',
+		monthNames: ['Jaanuar','Veebruar','Märts','Aprill','Mai','Juuni',
+		'Juuli','August','September','Oktoober','November','Detsember'],
+		monthNamesShort: ['Jaan', 'Veebr', 'Märts', 'Apr', 'Mai', 'Juuni',
+		'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'],
+		dayNames: ['Pühapäev', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev'],
+		dayNamesShort: ['Pühap', 'Esmasp', 'Teisip', 'Kolmap', 'Neljap', 'Reede', 'Laup'],
+		dayNamesMin: ['P','E','T','K','N','R','L'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['et']);
+}); 
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-eu.js b/client/apollo/jslib/jqueryui/datepicker-eu.js
new file mode 100644
index 0000000..8f379b3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-eu.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Euskarako oinarria 'UI date picker' jquery-ko extentsioarentzat */
+/* Karrikas-ek itzulia (karrikas at karrikas.com) */
+jQuery(function($){
+	$.datepicker.regional['eu'] = {
+		closeText: 'Egina',
+		prevText: '&#x3c;Aur',
+		nextText: 'Hur&#x3e;',
+		currentText: 'Gaur',
+		monthNames: ['Urtarrila','Otsaila','Martxoa','Apirila','Maiatza','Ekaina',
+		'Uztaila','Abuztua','Iraila','Urria','Azaroa','Abendua'],
+		monthNamesShort: ['Urt','Ots','Mar','Api','Mai','Eka',
+		'Uzt','Abu','Ira','Urr','Aza','Abe'],
+		dayNames: ['Igandea','Astelehena','Asteartea','Asteazkena','Osteguna','Ostirala','Larunbata'],
+		dayNamesShort: ['Iga','Ast','Ast','Ast','Ost','Ost','Lar'],
+		dayNamesMin: ['Ig','As','As','As','Os','Os','La'],
+		weekHeader: 'Wk',
+		dateFormat: 'yy/mm/dd',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['eu']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-fa.js b/client/apollo/jslib/jqueryui/datepicker-fa.js
new file mode 100644
index 0000000..711c208
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-fa.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Persian (Farsi) Translation for the jQuery UI date picker plugin. */
+/* Javad Mowlanezhad -- jmowla at gmail.com */
+/* Jalali calendar should supported soon! (Its implemented but I have to test it) */
+jQuery(function($) {
+	$.datepicker.regional['fa'] = {
+		closeText: 'بستن',
+		prevText: '&#x3c;قبلي',
+		nextText: 'بعدي&#x3e;',
+		currentText: 'امروز',
+		monthNames: ['فروردين','ارديبهشت','خرداد','تير','مرداد','شهريور',
+		'مهر','آبان','آذر','دي','بهمن','اسفند'],
+		monthNamesShort: ['1','2','3','4','5','6','7','8','9','10','11','12'],
+		dayNames: ['يکشنبه','دوشنبه','سه‌شنبه','چهارشنبه','پنجشنبه','جمعه','شنبه'],
+		dayNamesShort: ['ي','د','س','چ','پ','ج', 'ش'],
+		dayNamesMin: ['ي','د','س','چ','پ','ج', 'ش'],
+		weekHeader: 'هف',
+		dateFormat: 'yy/mm/dd',
+		firstDay: 6,
+		isRTL: true,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['fa']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-fi.js b/client/apollo/jslib/jqueryui/datepicker-fi.js
new file mode 100644
index 0000000..b38850f
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-fi.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Finnish initialisation for the jQuery UI date picker plugin. */
+/* Written by Harri Kilpi� (harrikilpio at gmail.com). */
+jQuery(function($){
+    $.datepicker.regional['fi'] = {
+		closeText: 'Sulje',
+		prevText: '«Edellinen',
+		nextText: 'Seuraava»',
+		currentText: 'Tänään',
+        monthNames: ['Tammikuu','Helmikuu','Maaliskuu','Huhtikuu','Toukokuu','Kesäkuu',
+        'Heinäkuu','Elokuu','Syyskuu','Lokakuu','Marraskuu','Joulukuu'],
+        monthNamesShort: ['Tammi','Helmi','Maalis','Huhti','Touko','Kesä',
+        'Heinä','Elo','Syys','Loka','Marras','Joulu'],
+		dayNamesShort: ['Su','Ma','Ti','Ke','To','Pe','Su'],
+		dayNames: ['Sunnuntai','Maanantai','Tiistai','Keskiviikko','Torstai','Perjantai','Lauantai'],
+		dayNamesMin: ['Su','Ma','Ti','Ke','To','Pe','La'],
+		weekHeader: 'Vk',
+        dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+    $.datepicker.setDefaults($.datepicker.regional['fi']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-fo.js b/client/apollo/jslib/jqueryui/datepicker-fo.js
new file mode 100644
index 0000000..4de14b7
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-fo.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Faroese initialisation for the jQuery UI date picker plugin */
+/* Written by Sverri Mohr Olsen, sverrimo at gmail.com */
+jQuery(function($){
+	$.datepicker.regional['fo'] = {
+		closeText: 'Lat aftur',
+		prevText: '&#x3c;Fyrra',
+		nextText: 'Næsta&#x3e;',
+		currentText: 'Í dag',
+		monthNames: ['Januar','Februar','Mars','Apríl','Mei','Juni',
+		'Juli','August','September','Oktober','November','Desember'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun',
+		'Jul','Aug','Sep','Okt','Nov','Des'],
+		dayNames: ['Sunnudagur','Mánadagur','Týsdagur','Mikudagur','Hósdagur','Fríggjadagur','Leyardagur'],
+		dayNamesShort: ['Sun','Mán','Týs','Mik','Hós','Frí','Ley'],
+		dayNamesMin: ['Su','Má','Tý','Mi','Hó','Fr','Le'],
+		weekHeader: 'Vk',
+		dateFormat: 'dd-mm-yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['fo']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-fr-CH.js b/client/apollo/jslib/jqueryui/datepicker-fr-CH.js
new file mode 100644
index 0000000..742a835
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-fr-CH.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Swiss-French initialisation for the jQuery UI date picker plugin. */
+/* Written Martin Voelkle (martin.voelkle at e-tc.ch). */
+jQuery(function($){
+	$.datepicker.regional['fr-CH'] = {
+		closeText: 'Fermer',
+		prevText: '&#x3c;Préc',
+		nextText: 'Suiv&#x3e;',
+		currentText: 'Courant',
+		monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin',
+		'Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+		monthNamesShort: ['Jan','Fév','Mar','Avr','Mai','Jun',
+		'Jul','Aoû','Sep','Oct','Nov','Déc'],
+		dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+		dayNamesShort: ['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'],
+		dayNamesMin: ['Di','Lu','Ma','Me','Je','Ve','Sa'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['fr-CH']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-fr.js b/client/apollo/jslib/jqueryui/datepicker-fr.js
new file mode 100644
index 0000000..9c2a216
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-fr.js
@@ -0,0 +1,28 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* French initialisation for the jQuery UI date picker plugin. */
+/* Written by Keith Wood (kbwood{at}iinet.com.au),
+              Stéphane Nahmani (sholby at sholby.net),
+              Stéphane Raimbault <stephane.raimbault at gmail.com> */
+jQuery(function($){
+	$.datepicker.regional['fr'] = {
+		closeText: 'Fermer',
+		prevText: 'Précédent',
+		nextText: 'Suivant',
+		currentText: 'Aujourd\'hui',
+		monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin',
+		'Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+		monthNamesShort: ['Janv.','Févr.','Mars','Avril','Mai','Juin',
+		'Juil.','Août','Sept.','Oct.','Nov.','Déc.'],
+		dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+		dayNamesShort: ['Dim.','Lun.','Mar.','Mer.','Jeu.','Ven.','Sam.'],
+		dayNamesMin: ['D','L','M','M','J','V','S'],
+		weekHeader: 'Sem.',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['fr']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-gl.js b/client/apollo/jslib/jqueryui/datepicker-gl.js
new file mode 100644
index 0000000..22db1cf
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-gl.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Galician localization for 'UI date picker' jQuery extension. */
+/* Translated by Jorge Barreiro <yortx.barry at gmail.com>. */
+jQuery(function($){
+	$.datepicker.regional['gl'] = {
+		closeText: 'Pechar',
+		prevText: '&#x3c;Ant',
+		nextText: 'Seg&#x3e;',
+		currentText: 'Hoxe',
+		monthNames: ['Xaneiro','Febreiro','Marzo','Abril','Maio','Xuño',
+		'Xullo','Agosto','Setembro','Outubro','Novembro','Decembro'],
+		monthNamesShort: ['Xan','Feb','Mar','Abr','Mai','Xuñ',
+		'Xul','Ago','Set','Out','Nov','Dec'],
+		dayNames: ['Domingo','Luns','Martes','Mércores','Xoves','Venres','Sábado'],
+		dayNamesShort: ['Dom','Lun','Mar','Mér','Xov','Ven','Sáb'],
+		dayNamesMin: ['Do','Lu','Ma','Mé','Xo','Ve','Sá'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['gl']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-he.js b/client/apollo/jslib/jqueryui/datepicker-he.js
new file mode 100644
index 0000000..0e6df3e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-he.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Hebrew initialisation for the UI Datepicker extension. */
+/* Written by Amir Hardon (ahardon at gmail dot com). */
+jQuery(function($){
+	$.datepicker.regional['he'] = {
+		closeText: 'סגור',
+		prevText: '&#x3c;הקודם',
+		nextText: 'הבא&#x3e;',
+		currentText: 'היום',
+		monthNames: ['ינואר','פברואר','מרץ','אפריל','מאי','יוני',
+		'יולי','אוגוסט','ספטמבר','אוקטובר','נובמבר','דצמבר'],
+		monthNamesShort: ['ינו','פבר','מרץ','אפר','מאי','יוני',
+		'יולי','אוג','ספט','אוק','נוב','דצמ'],
+		dayNames: ['ראשון','שני','שלישי','רביעי','חמישי','שישי','שבת'],
+		dayNamesShort: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'],
+		dayNamesMin: ['א\'','ב\'','ג\'','ד\'','ה\'','ו\'','שבת'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: true,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['he']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-hi.js b/client/apollo/jslib/jqueryui/datepicker-hi.js
new file mode 100644
index 0000000..91d0469
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-hi.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Hindi initialisation for the jQuery UI date picker plugin. */
+/* Written by Michael Dawart. */
+jQuery(function($){
+	$.datepicker.regional['hi'] = {
+		closeText: 'होकर',
+		prevText: 'अगला',
+		nextText: 'नेक्स्ट',
+		currentText: 'आज',
+		monthNames: ['जनवरी ','फरवरी','मार्च','अप्रेल','मै','जून',
+		'जूलाई','अगस्त ','सितम्बर','आक्टोबर','नवम्बर','दिसम्बर'],
+		monthNamesShort: ['जन', 'फर', 'मार्च', 'अप्रेल', 'मै', 'जून',
+		'जूलाई', 'अग', 'सित', 'आक्ट', 'नव', 'िद'],
+		dayNames: ['रविवासर', 'सोमवासर', 'मंगलवासर', 'बुधवासर', 'गुरुवासर', 'शुक्रवासर', 'शनिवासर'],
+		dayNamesShort: ['रवि', 'सोम', 'मंगल', 'बुध', 'गुरु', 'शुक्र', 'शनि'],
+		dayNamesMin: ['रवि', 'सोम', 'मंगल', 'बुध', 'गुरु', 'शुक्र', 'शनि'],
+		weekHeader: 'हफ्ता',
+		dateFormat: 'mm/dd/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['hi']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-hr.js b/client/apollo/jslib/jqueryui/datepicker-hr.js
new file mode 100644
index 0000000..388b638
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-hr.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Croatian i18n for the jQuery UI date picker plugin. */
+/* Written by Vjekoslav Nesek. */
+jQuery(function($){
+	$.datepicker.regional['hr'] = {
+		closeText: 'Zatvori',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Danas',
+		monthNames: ['Siječanj','Veljača','Ožujak','Travanj','Svibanj','Lipanj',
+		'Srpanj','Kolovoz','Rujan','Listopad','Studeni','Prosinac'],
+		monthNamesShort: ['Sij','Velj','Ožu','Tra','Svi','Lip',
+		'Srp','Kol','Ruj','Lis','Stu','Pro'],
+		dayNames: ['Nedjelja','Ponedjeljak','Utorak','Srijeda','Četvrtak','Petak','Subota'],
+		dayNamesShort: ['Ned','Pon','Uto','Sri','Čet','Pet','Sub'],
+		dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'],
+		weekHeader: 'Tje',
+		dateFormat: 'dd.mm.yy.',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['hr']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-hu.js b/client/apollo/jslib/jqueryui/datepicker-hu.js
new file mode 100644
index 0000000..4defbd9
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-hu.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Hungarian initialisation for the jQuery UI date picker plugin. */
+/* Written by Istvan Karaszi (jquery at spam.raszi.hu). */
+jQuery(function($){
+	$.datepicker.regional['hu'] = {
+		closeText: 'bezár',
+		prevText: 'vissza',
+		nextText: 'előre',
+		currentText: 'ma',
+		monthNames: ['Január', 'Február', 'Március', 'Április', 'Május', 'Június',
+		'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'],
+		monthNamesShort: ['Jan', 'Feb', 'Már', 'Ápr', 'Máj', 'Jún',
+		'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'],
+		dayNames: ['Vasárnap', 'Hétfő', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'],
+		dayNamesShort: ['Vas', 'Hét', 'Ked', 'Sze', 'Csü', 'Pén', 'Szo'],
+		dayNamesMin: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'],
+		weekHeader: 'Hét',
+		dateFormat: 'yy.mm.dd.',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['hu']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-hy.js b/client/apollo/jslib/jqueryui/datepicker-hy.js
new file mode 100644
index 0000000..2ee10d5
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-hy.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Armenian(UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Levon Zakaryan (levon.zakaryan at gmail.com)*/
+jQuery(function($){
+	$.datepicker.regional['hy'] = {
+		closeText: 'Փակել',
+		prevText: '&#x3c;Նախ.',
+		nextText: 'Հաջ.&#x3e;',
+		currentText: 'Այսօր',
+		monthNames: ['Հունվար','Փետրվար','Մարտ','Ապրիլ','Մայիս','Հունիս',
+		'Հուլիս','Օգոստոս','Սեպտեմբեր','Հոկտեմբեր','Նոյեմբեր','Դեկտեմբեր'],
+		monthNamesShort: ['Հունվ','Փետր','Մարտ','Ապր','Մայիս','Հունիս',
+		'Հուլ','Օգս','Սեպ','Հոկ','Նոյ','Դեկ'],
+		dayNames: ['կիրակի','եկուշաբթի','երեքշաբթի','չորեքշաբթի','հինգշաբթի','ուրբաթ','շաբաթ'],
+		dayNamesShort: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'],
+		dayNamesMin: ['կիր','երկ','երք','չրք','հնգ','ուրբ','շբթ'],
+		weekHeader: 'ՇԲՏ',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['hy']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-id.js b/client/apollo/jslib/jqueryui/datepicker-id.js
new file mode 100644
index 0000000..7f65038
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-id.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Indonesian initialisation for the jQuery UI date picker plugin. */
+/* Written by Deden Fathurahman (dedenf at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['id'] = {
+		closeText: 'Tutup',
+		prevText: '&#x3c;mundur',
+		nextText: 'maju&#x3e;',
+		currentText: 'hari ini',
+		monthNames: ['Januari','Februari','Maret','April','Mei','Juni',
+		'Juli','Agustus','September','Oktober','Nopember','Desember'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Mei','Jun',
+		'Jul','Agus','Sep','Okt','Nop','Des'],
+		dayNames: ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'],
+		dayNamesShort: ['Min','Sen','Sel','Rab','kam','Jum','Sab'],
+		dayNamesMin: ['Mg','Sn','Sl','Rb','Km','jm','Sb'],
+		weekHeader: 'Mg',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['id']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-is.js b/client/apollo/jslib/jqueryui/datepicker-is.js
new file mode 100644
index 0000000..697cf1e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-is.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Icelandic initialisation for the jQuery UI date picker plugin. */
+/* Written by Haukur H. Thorsson (haukur at eskill.is). */
+jQuery(function($){
+	$.datepicker.regional['is'] = {
+		closeText: 'Loka',
+		prevText: '&#x3c; Fyrri',
+		nextText: 'Næsti &#x3e;',
+		currentText: 'Í dag',
+		monthNames: ['Janúar','Febrúar','Mars','Apríl','Ma&iacute','Júní',
+		'Júlí','Ágúst','September','Október','Nóvember','Desember'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maí','Jún',
+		'Júl','Ágú','Sep','Okt','Nóv','Des'],
+		dayNames: ['Sunnudagur','Mánudagur','Þriðjudagur','Miðvikudagur','Fimmtudagur','Föstudagur','Laugardagur'],
+		dayNamesShort: ['Sun','Mán','Þri','Mið','Fim','Fös','Lau'],
+		dayNamesMin: ['Su','Má','Þr','Mi','Fi','Fö','La'],
+		weekHeader: 'Vika',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['is']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-it.js b/client/apollo/jslib/jqueryui/datepicker-it.js
new file mode 100644
index 0000000..0bb515c
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-it.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Italian initialisation for the jQuery UI date picker plugin. */
+/* Written by Antonello Pasella (antonello.pasella at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['it'] = {
+		closeText: 'Chiudi',
+		prevText: '&#x3c;Prec',
+		nextText: 'Succ&#x3e;',
+		currentText: 'Oggi',
+		monthNames: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno',
+			'Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'],
+		monthNamesShort: ['Gen','Feb','Mar','Apr','Mag','Giu',
+			'Lug','Ago','Set','Ott','Nov','Dic'],
+		dayNames: ['Domenica','Luned&#236','Marted&#236','Mercoled&#236','Gioved&#236','Venerd&#236','Sabato'],
+		dayNamesShort: ['Dom','Lun','Mar','Mer','Gio','Ven','Sab'],
+		dayNamesMin: ['Do','Lu','Ma','Me','Gi','Ve','Sa'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['it']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ja.js b/client/apollo/jslib/jqueryui/datepicker-ja.js
new file mode 100644
index 0000000..9ed6d32
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ja.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Japanese initialisation for the jQuery UI date picker plugin. */
+/* Written by Kentaro SATO (kentaro at ranvis.com). */
+jQuery(function($){
+	$.datepicker.regional['ja'] = {
+		closeText: '閉じる',
+		prevText: '&#x3c;前',
+		nextText: '次&#x3e;',
+		currentText: '今日',
+		monthNames: ['1月','2月','3月','4月','5月','6月',
+		'7月','8月','9月','10月','11月','12月'],
+		monthNamesShort: ['1月','2月','3月','4月','5月','6月',
+		'7月','8月','9月','10月','11月','12月'],
+		dayNames: ['日曜日','月曜日','火曜日','水曜日','木曜日','金曜日','土曜日'],
+		dayNamesShort: ['日','月','火','水','木','金','土'],
+		dayNamesMin: ['日','月','火','水','木','金','土'],
+		weekHeader: '週',
+		dateFormat: 'yy/mm/dd',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: '年'};
+	$.datepicker.setDefaults($.datepicker.regional['ja']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-kk.js b/client/apollo/jslib/jqueryui/datepicker-kk.js
new file mode 100644
index 0000000..95900aa
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-kk.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Kazakh (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Dmitriy Karasyov (dmitriy.karasyov at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['kk'] = {
+		closeText: 'Жабу',
+		prevText: '&#x3c;Алдыңғы',
+		nextText: 'Келесі&#x3e;',
+		currentText: 'Бүгін',
+		monthNames: ['Қаңтар','Ақпан','Наурыз','Сәуір','Мамыр','Маусым',
+		'Шілде','Тамыз','Қыркүйек','Қазан','Қараша','Желтоқсан'],
+		monthNamesShort: ['Қаң','Ақп','Нау','Сәу','Мам','Мау',
+		'Шіл','Там','Қыр','Қаз','Қар','Жел'],
+		dayNames: ['Жексенбі','Дүйсенбі','Сейсенбі','Сәрсенбі','Бейсенбі','Жұма','Сенбі'],
+		dayNamesShort: ['жкс','дсн','ссн','срс','бсн','жма','снб'],
+		dayNamesMin: ['Жк','Дс','Сс','Ср','Бс','Жм','Сн'],
+		weekHeader: 'Не',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['kk']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-km.js b/client/apollo/jslib/jqueryui/datepicker-km.js
new file mode 100644
index 0000000..575a855
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-km.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Khmer initialisation for the jQuery calendar extension. */
+/* Written by Chandara Om (chandara.teacher at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['km'] = {
+		closeText: 'ធ្វើ​រួច',
+		prevText: 'មុន',
+		nextText: 'បន្ទាប់',
+		currentText: 'ថ្ងៃ​នេះ',
+		monthNames: ['មករា','កុម្ភៈ','មីនា','មេសា','ឧសភា','មិថុនា',
+		'កក្កដា','សីហា','កញ្ញា','តុលា','វិច្ឆិកា','ធ្នូ'],
+		monthNamesShort: ['មករា','កុម្ភៈ','មីនា','មេសា','ឧសភា','មិថុនា',
+		'កក្កដា','សីហា','កញ្ញា','តុលា','វិច្ឆិកា','ធ្នូ'],
+		dayNames: ['អាទិត្យ', 'ចន្ទ', 'អង្គារ', 'ពុធ', 'ព្រហស្បតិ៍', 'សុក្រ', 'សៅរ៍'],
+		dayNamesShort: ['អា', 'ច', 'អ', 'ពុ', 'ព្រហ', 'សុ', 'សៅ'],
+		dayNamesMin: ['អា', 'ច', 'អ', 'ពុ', 'ព្រហ', 'សុ', 'សៅ'],
+		weekHeader: 'សប្ដាហ៍',
+		dateFormat: 'dd-mm-yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['km']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ko.js b/client/apollo/jslib/jqueryui/datepicker-ko.js
new file mode 100644
index 0000000..dacd646
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ko.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Korean initialisation for the jQuery calendar extension. */
+/* Written by DaeKwon Kang (ncrash.dk at gmail.com), Edited by Genie. */
+jQuery(function($){
+	$.datepicker.regional['ko'] = {
+		closeText: '닫기',
+		prevText: '이전달',
+		nextText: '다음달',
+		currentText: '오늘',
+		monthNames: ['1월','2월','3월','4월','5월','6월',
+		'7월','8월','9월','10월','11월','12월'],
+		monthNamesShort: ['1월','2월','3월','4월','5월','6월',
+		'7월','8월','9월','10월','11월','12월'],
+		dayNames: ['일요일','월요일','화요일','수요일','목요일','금요일','토요일'],
+		dayNamesShort: ['일','월','화','수','목','금','토'],
+		dayNamesMin: ['일','월','화','수','목','금','토'],
+		weekHeader: 'Wk',
+		dateFormat: 'yy-mm-dd',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: '년'};
+	$.datepicker.setDefaults($.datepicker.regional['ko']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-lb.js b/client/apollo/jslib/jqueryui/datepicker-lb.js
new file mode 100644
index 0000000..20dc979
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-lb.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Luxembourgish initialisation for the jQuery UI date picker plugin. */
+/* Written by Michel Weimerskirch <michel at weimerskirch.net> */
+jQuery(function($){
+	$.datepicker.regional['lb'] = {
+		closeText: 'Fäerdeg',
+		prevText: 'Zréck',
+		nextText: 'Weider',
+		currentText: 'Haut',
+		monthNames: ['Januar','Februar','Mäerz','Abrëll','Mee','Juni',
+		'Juli','August','September','Oktober','November','Dezember'],
+		monthNamesShort: ['Jan', 'Feb', 'Mäe', 'Abr', 'Mee', 'Jun',
+		'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+		dayNames: ['Sonndeg', 'Méindeg', 'Dënschdeg', 'Mëttwoch', 'Donneschdeg', 'Freideg', 'Samschdeg'],
+		dayNamesShort: ['Son', 'Méi', 'Dën', 'Mët', 'Don', 'Fre', 'Sam'],
+		dayNamesMin: ['So','Mé','Dë','Më','Do','Fr','Sa'],
+		weekHeader: 'W',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['lb']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-lt.js b/client/apollo/jslib/jqueryui/datepicker-lt.js
new file mode 100644
index 0000000..9e0787b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-lt.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Lithuanian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* @author Arturas Paleicikas <arturas at avalon.lt> */
+jQuery(function($){
+	$.datepicker.regional['lt'] = {
+		closeText: 'Uždaryti',
+		prevText: '&#x3c;Atgal',
+		nextText: 'Pirmyn&#x3e;',
+		currentText: 'Šiandien',
+		monthNames: ['Sausis','Vasaris','Kovas','Balandis','Gegužė','Birželis',
+		'Liepa','Rugpjūtis','Rugsėjis','Spalis','Lapkritis','Gruodis'],
+		monthNamesShort: ['Sau','Vas','Kov','Bal','Geg','Bir',
+		'Lie','Rugp','Rugs','Spa','Lap','Gru'],
+		dayNames: ['sekmadienis','pirmadienis','antradienis','trečiadienis','ketvirtadienis','penktadienis','šeštadienis'],
+		dayNamesShort: ['sek','pir','ant','tre','ket','pen','šeš'],
+		dayNamesMin: ['Se','Pr','An','Tr','Ke','Pe','Še'],
+		weekHeader: 'Wk',
+		dateFormat: 'yy-mm-dd',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['lt']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-lv.js b/client/apollo/jslib/jqueryui/datepicker-lv.js
new file mode 100644
index 0000000..791ccb3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-lv.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Latvian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* @author Arturas Paleicikas <arturas.paleicikas at metasite.net> */
+jQuery(function($){
+	$.datepicker.regional['lv'] = {
+		closeText: 'Aizvērt',
+		prevText: 'Iepr',
+		nextText: 'Nāka',
+		currentText: 'Šodien',
+		monthNames: ['Janvāris','Februāris','Marts','Aprīlis','Maijs','Jūnijs',
+		'Jūlijs','Augusts','Septembris','Oktobris','Novembris','Decembris'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Mai','Jūn',
+		'Jūl','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['svētdiena','pirmdiena','otrdiena','trešdiena','ceturtdiena','piektdiena','sestdiena'],
+		dayNamesShort: ['svt','prm','otr','tre','ctr','pkt','sst'],
+		dayNamesMin: ['Sv','Pr','Ot','Tr','Ct','Pk','Ss'],
+		weekHeader: 'Nav',
+		dateFormat: 'dd-mm-yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['lv']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-mk.js b/client/apollo/jslib/jqueryui/datepicker-mk.js
new file mode 100644
index 0000000..3bc3de8
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-mk.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Macedonian i18n for the jQuery UI date picker plugin. */
+/* Written by Stojce Slavkovski. */
+jQuery(function($){
+	$.datepicker.regional['mk'] = {
+		closeText: 'Затвори',
+		prevText: '&#x3C;',
+		nextText: '&#x3E;',
+		currentText: 'Денес',
+		monthNames: ['Јануари','Фебруари','Март','Април','Мај','Јуни',
+		'Јули','Август','Септември','Октомври','Ноември','Декември'],
+		monthNamesShort: ['Јан','Феб','Мар','Апр','Мај','Јун',
+		'Јул','Авг','Сеп','Окт','Ное','Дек'],
+		dayNames: ['Недела','Понеделник','Вторник','Среда','Четврток','Петок','Сабота'],
+		dayNamesShort: ['Нед','Пон','Вто','Сре','Чет','Пет','Саб'],
+		dayNamesMin: ['Не','По','Вт','Ср','Че','Пе','Са'],
+		weekHeader: 'Сед',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['mk']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ml.js b/client/apollo/jslib/jqueryui/datepicker-ml.js
new file mode 100644
index 0000000..7621d43
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ml.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Malayalam (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Saji Nediyanchath (saji89 at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ml'] = {
+		closeText: 'ശരി',
+		prevText: 'മുന്നത്തെ',  
+		nextText: 'അടുത്തത് ',
+		currentText: 'ഇന്ന്',
+		monthNames: ['ജനുവരി','ഫെബ്രുവരി','മാര്‍ച്ച്','ഏപ്രില്‍','മേയ്','ജൂണ്‍',
+		'ജൂലൈ','ആഗസ്റ്റ്','സെപ്റ്റംബര്‍','ഒക്ടോബര്‍','നവംബര്‍','ഡിസംബര്‍'],
+		monthNamesShort: ['ജനു', 'ഫെബ്', 'മാര്‍', 'ഏപ്രി', 'മേയ്', 'ജൂണ്‍',
+		'ജൂലാ', 'ആഗ', 'സെപ്', 'ഒക്ടോ', 'നവം', 'ഡിസ'],
+		dayNames: ['ഞായര്‍', 'തിങ്കള്‍', 'ചൊവ്വ', 'ബുധന്‍', 'വ്യാഴം', 'വെള്ളി', 'ശനി'],
+		dayNamesShort: ['ഞായ', 'തിങ്ക', 'ചൊവ്വ', 'ബുധ', 'വ്യാഴം', 'വെള്ളി', 'ശനി'],
+		dayNamesMin: ['ഞാ','തി','ചൊ','ബു','വ്യാ','വെ','ശ'],
+		weekHeader: 'ആ',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ml']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ms.js b/client/apollo/jslib/jqueryui/datepicker-ms.js
new file mode 100644
index 0000000..f3ab08d
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ms.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Malaysian initialisation for the jQuery UI date picker plugin. */
+/* Written by Mohd Nawawi Mohamad Jamili (nawawi at ronggeng.net). */
+jQuery(function($){
+	$.datepicker.regional['ms'] = {
+		closeText: 'Tutup',
+		prevText: '&#x3c;Sebelum',
+		nextText: 'Selepas&#x3e;',
+		currentText: 'hari ini',
+		monthNames: ['Januari','Februari','Mac','April','Mei','Jun',
+		'Julai','Ogos','September','Oktober','November','Disember'],
+		monthNamesShort: ['Jan','Feb','Mac','Apr','Mei','Jun',
+		'Jul','Ogo','Sep','Okt','Nov','Dis'],
+		dayNames: ['Ahad','Isnin','Selasa','Rabu','Khamis','Jumaat','Sabtu'],
+		dayNamesShort: ['Aha','Isn','Sel','Rab','kha','Jum','Sab'],
+		dayNamesMin: ['Ah','Is','Se','Ra','Kh','Ju','Sa'],
+		weekHeader: 'Mg',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ms']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-nl-BE.js b/client/apollo/jslib/jqueryui/datepicker-nl-BE.js
new file mode 100644
index 0000000..456f3e1
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-nl-BE.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Dutch (Belgium) initialisation for the jQuery UI date picker plugin. */
+/* David De Sloovere @DavidDeSloovere */
+jQuery(function($){
+	$.datepicker.regional['nl-BE'] = {
+		closeText: 'Sluiten',
+		prevText: '←',
+		nextText: '→',
+		currentText: 'Vandaag',
+		monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',
+		'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+		monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun',
+		'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+		dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+		dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],
+		dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['nl-BE']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-nl.js b/client/apollo/jslib/jqueryui/datepicker-nl.js
new file mode 100644
index 0000000..2c04b1b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-nl.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Mathias Bynens <http://mathiasbynens.be/> */
+jQuery(function($){
+	$.datepicker.regional.nl = {
+		closeText: 'Sluiten',
+		prevText: '←',
+		nextText: '→',
+		currentText: 'Vandaag',
+		monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',
+		'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+		monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun',
+		'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+		dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+		dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],
+		dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd-mm-yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional.nl);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-no.js b/client/apollo/jslib/jqueryui/datepicker-no.js
new file mode 100644
index 0000000..5decba3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-no.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Norwegian initialisation for the jQuery UI date picker plugin. */
+/* Written by Naimdjon Takhirov (naimdjon at gmail.com). */
+
+jQuery(function($){
+  $.datepicker.regional['no'] = {
+    closeText: 'Lukk',
+    prevText: '«Forrige',
+    nextText: 'Neste»',
+    currentText: 'I dag',
+    monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'],
+    monthNamesShort: ['jan','feb','mar','apr','mai','jun','jul','aug','sep','okt','nov','des'],
+    dayNamesShort: ['søn','man','tir','ons','tor','fre','lør'],
+    dayNames: ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'],
+    dayNamesMin: ['sø','ma','ti','on','to','fr','lø'],
+    weekHeader: 'Uke',
+    dateFormat: 'dd.mm.yy',
+    firstDay: 1,
+    isRTL: false,
+    showMonthAfterYear: false,
+    yearSuffix: ''
+  };
+  $.datepicker.setDefaults($.datepicker.regional['no']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-pl.js b/client/apollo/jslib/jqueryui/datepicker-pl.js
new file mode 100644
index 0000000..ff32cb3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-pl.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Polish initialisation for the jQuery UI date picker plugin. */
+/* Written by Jacek Wysocki (jacek.wysocki at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['pl'] = {
+		closeText: 'Zamknij',
+		prevText: '&#x3c;Poprzedni',
+		nextText: 'Następny&#x3e;',
+		currentText: 'Dziś',
+		monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec',
+		'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'],
+		monthNamesShort: ['Sty','Lu','Mar','Kw','Maj','Cze',
+		'Lip','Sie','Wrz','Pa','Lis','Gru'],
+		dayNames: ['Niedziela','Poniedziałek','Wtorek','Środa','Czwartek','Piątek','Sobota'],
+		dayNamesShort: ['Nie','Pn','Wt','Śr','Czw','Pt','So'],
+		dayNamesMin: ['N','Pn','Wt','Śr','Cz','Pt','So'],
+		weekHeader: 'Tydz',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['pl']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-pt-BR.js b/client/apollo/jslib/jqueryui/datepicker-pt-BR.js
new file mode 100644
index 0000000..86fc7e2
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-pt-BR.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Brazilian initialisation for the jQuery UI date picker plugin. */
+/* Written by Leonildo Costa Silva (leocsilva at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['pt-BR'] = {
+		closeText: 'Fechar',
+		prevText: '&#x3c;Anterior',
+		nextText: 'Próximo&#x3e;',
+		currentText: 'Hoje',
+		monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
+		'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
+		monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun',
+		'Jul','Ago','Set','Out','Nov','Dez'],
+		dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'],
+		dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
+		dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
+		weekHeader: 'Sm',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['pt-BR']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-pt.js b/client/apollo/jslib/jqueryui/datepicker-pt.js
new file mode 100644
index 0000000..d92726b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-pt.js
@@ -0,0 +1,24 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Portuguese initialisation for the jQuery UI date picker plugin. */
+jQuery(function($){
+	$.datepicker.regional['pt'] = {
+		closeText: 'Fechar',
+		prevText: '&#x3c;Anterior',
+		nextText: 'Seguinte',
+		currentText: 'Hoje',
+		monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
+		'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],
+		monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun',
+		'Jul','Ago','Set','Out','Nov','Dez'],
+		dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'],
+		dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
+		dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'],
+		weekHeader: 'Sem',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['pt']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-rm.js b/client/apollo/jslib/jqueryui/datepicker-rm.js
new file mode 100644
index 0000000..7c342c6
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-rm.js
@@ -0,0 +1,24 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Romansh initialisation for the jQuery UI date picker plugin. */
+/* Written by Yvonne Gienal (yvonne.gienal at educa.ch). */
+jQuery(function($){
+	$.datepicker.regional['rm'] = {
+		closeText: 'Serrar',
+		prevText: '&#x3c;Suandant',
+		nextText: 'Precedent&#x3e;',
+		currentText: 'Actual',
+		monthNames: ['Schaner','Favrer','Mars','Avrigl','Matg','Zercladur', 'Fanadur','Avust','Settember','October','November','December'],
+		monthNamesShort: ['Scha','Fev','Mar','Avr','Matg','Zer', 'Fan','Avu','Sett','Oct','Nov','Dec'],
+		dayNames: ['Dumengia','Glindesdi','Mardi','Mesemna','Gievgia','Venderdi','Sonda'],
+		dayNamesShort: ['Dum','Gli','Mar','Mes','Gie','Ven','Som'],
+		dayNamesMin: ['Du','Gl','Ma','Me','Gi','Ve','So'],
+		weekHeader: 'emna',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['rm']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ro.js b/client/apollo/jslib/jqueryui/datepicker-ro.js
new file mode 100644
index 0000000..62cd0e4
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ro.js
@@ -0,0 +1,29 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Romanian initialisation for the jQuery UI date picker plugin.
+ *
+ * Written by Edmond L. (ll_edmond at walla.com)
+ * and Ionut G. Stan (ionut.g.stan at gmail.com)
+ */
+jQuery(function($){
+	$.datepicker.regional['ro'] = {
+		closeText: 'Închide',
+		prevText: '« Luna precedentă',
+		nextText: 'Luna următoare »',
+		currentText: 'Azi',
+		monthNames: ['Ianuarie','Februarie','Martie','Aprilie','Mai','Iunie',
+		'Iulie','August','Septembrie','Octombrie','Noiembrie','Decembrie'],
+		monthNamesShort: ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun',
+		'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+		dayNames: ['Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'],
+		dayNamesShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'],
+		dayNamesMin: ['Du','Lu','Ma','Mi','Jo','Vi','Sâ'],
+		weekHeader: 'Săpt',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ro']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ru.js b/client/apollo/jslib/jqueryui/datepicker-ru.js
new file mode 100644
index 0000000..797335d
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ru.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Russian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Andrew Stromnov (stromnov at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['ru'] = {
+		closeText: 'Закрыть',
+		prevText: '&#x3c;Пред',
+		nextText: 'След&#x3e;',
+		currentText: 'Сегодня',
+		monthNames: ['Январь','Февраль','Март','Апрель','Май','Июнь',
+		'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'],
+		monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн',
+		'Июл','Авг','Сен','Окт','Ноя','Дек'],
+		dayNames: ['воскресенье','понедельник','вторник','среда','четверг','пятница','суббота'],
+		dayNamesShort: ['вск','пнд','втр','срд','чтв','птн','сбт'],
+		dayNamesMin: ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'],
+		weekHeader: 'Нед',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ru']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sk.js b/client/apollo/jslib/jqueryui/datepicker-sk.js
new file mode 100644
index 0000000..b9761e3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sk.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Slovak initialisation for the jQuery UI date picker plugin. */
+/* Written by Vojtech Rinik (vojto at hmm.sk). */
+jQuery(function($){
+	$.datepicker.regional['sk'] = {
+		closeText: 'Zavrieť',
+		prevText: '&#x3c;Predchádzajúci',
+		nextText: 'Nasledujúci&#x3e;',
+		currentText: 'Dnes',
+		monthNames: ['Január','Február','Marec','Apríl','Máj','Jún',
+		'Júl','August','September','Október','November','December'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Máj','Jún',
+		'Júl','Aug','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedeľa','Pondelok','Utorok','Streda','Štvrtok','Piatok','Sobota'],
+		dayNamesShort: ['Ned','Pon','Uto','Str','Štv','Pia','Sob'],
+		dayNamesMin: ['Ne','Po','Ut','St','Št','Pia','So'],
+		weekHeader: 'Ty',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['sk']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sl.js b/client/apollo/jslib/jqueryui/datepicker-sl.js
new file mode 100644
index 0000000..63637e6
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sl.js
@@ -0,0 +1,27 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Slovenian initialisation for the jQuery UI date picker plugin. */
+/* Written by Jaka Jancar (jaka at kubje.org). */
+/* c = &#x10D;, s = &#x161; z = &#x17E; C = &#x10C; S = &#x160; Z = &#x17D; */
+jQuery(function($){
+	$.datepicker.regional['sl'] = {
+		closeText: 'Zapri',
+		prevText: '<Prej&#x161;nji',
+		nextText: 'Naslednji>',
+		currentText: 'Trenutni',
+		monthNames: ['Januar','Februar','Marec','April','Maj','Junij',
+		'Julij','Avgust','September','Oktober','November','December'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Avg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedelja','Ponedeljek','Torek','Sreda','&#x10C;etrtek','Petek','Sobota'],
+		dayNamesShort: ['Ned','Pon','Tor','Sre','&#x10C;et','Pet','Sob'],
+		dayNamesMin: ['Ne','Po','To','Sr','&#x10C;e','Pe','So'],
+		weekHeader: 'Teden',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['sl']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sq.js b/client/apollo/jslib/jqueryui/datepicker-sq.js
new file mode 100644
index 0000000..f336729
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sq.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Albanian initialisation for the jQuery UI date picker plugin. */
+/* Written by Flakron Bytyqi (flakron at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['sq'] = {
+		closeText: 'mbylle',
+		prevText: '&#x3c;mbrapa',
+		nextText: 'Përpara&#x3e;',
+		currentText: 'sot',
+		monthNames: ['Janar','Shkurt','Mars','Prill','Maj','Qershor',
+		'Korrik','Gusht','Shtator','Tetor','Nëntor','Dhjetor'],
+		monthNamesShort: ['Jan','Shk','Mar','Pri','Maj','Qer',
+		'Kor','Gus','Sht','Tet','Nën','Dhj'],
+		dayNames: ['E Diel','E Hënë','E Martë','E Mërkurë','E Enjte','E Premte','E Shtune'],
+		dayNamesShort: ['Di','Hë','Ma','Më','En','Pr','Sh'],
+		dayNamesMin: ['Di','Hë','Ma','Më','En','Pr','Sh'],
+		weekHeader: 'Ja',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['sq']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sr-SR.js b/client/apollo/jslib/jqueryui/datepicker-sr-SR.js
new file mode 100644
index 0000000..5a08da7
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sr-SR.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Serbian i18n for the jQuery UI date picker plugin. */
+/* Written by Dejan Dimić. */
+jQuery(function($){
+	$.datepicker.regional['sr-SR'] = {
+		closeText: 'Zatvori',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Danas',
+		monthNames: ['Januar','Februar','Mart','April','Maj','Jun',
+		'Jul','Avgust','Septembar','Oktobar','Novembar','Decembar'],
+		monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+		'Jul','Avg','Sep','Okt','Nov','Dec'],
+		dayNames: ['Nedelja','Ponedeljak','Utorak','Sreda','Četvrtak','Petak','Subota'],
+		dayNamesShort: ['Ned','Pon','Uto','Sre','Čet','Pet','Sub'],
+		dayNamesMin: ['Ne','Po','Ut','Sr','Če','Pe','Su'],
+		weekHeader: 'Sed',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['sr-SR']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sr.js b/client/apollo/jslib/jqueryui/datepicker-sr.js
new file mode 100644
index 0000000..526dc9d
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sr.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Serbian i18n for the jQuery UI date picker plugin. */
+/* Written by Dejan Dimić. */
+jQuery(function($){
+	$.datepicker.regional['sr'] = {
+		closeText: 'Затвори',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Данас',
+		monthNames: ['Јануар','Фебруар','Март','Април','Мај','Јун',
+		'Јул','Август','Септембар','Октобар','Новембар','Децембар'],
+		monthNamesShort: ['Јан','Феб','Мар','Апр','Мај','Јун',
+		'Јул','Авг','Сеп','Окт','Нов','Дец'],
+		dayNames: ['Недеља','Понедељак','Уторак','Среда','Четвртак','Петак','Субота'],
+		dayNamesShort: ['Нед','Пон','Уто','Сре','Чет','Пет','Суб'],
+		dayNamesMin: ['Не','По','Ут','Ср','Че','Пе','Су'],
+		weekHeader: 'Сед',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['sr']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-sv.js b/client/apollo/jslib/jqueryui/datepicker-sv.js
new file mode 100644
index 0000000..3a44778
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-sv.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Swedish initialisation for the jQuery UI date picker plugin. */
+/* Written by Anders Ekdahl ( anders at nomadiz.se). */
+jQuery(function($){
+    $.datepicker.regional['sv'] = {
+		closeText: 'Stäng',
+        prevText: '«Förra',
+		nextText: 'Nästa»',
+		currentText: 'Idag',
+        monthNames: ['Januari','Februari','Mars','April','Maj','Juni',
+        'Juli','Augusti','September','Oktober','November','December'],
+        monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun',
+        'Jul','Aug','Sep','Okt','Nov','Dec'],
+		dayNamesShort: ['Sön','Mån','Tis','Ons','Tor','Fre','Lör'],
+		dayNames: ['Söndag','Måndag','Tisdag','Onsdag','Torsdag','Fredag','Lördag'],
+		dayNamesMin: ['Sö','Må','Ti','On','To','Fr','Lö'],
+		weekHeader: 'Ve',
+        dateFormat: 'yy-mm-dd',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+    $.datepicker.setDefaults($.datepicker.regional['sv']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-ta.js b/client/apollo/jslib/jqueryui/datepicker-ta.js
new file mode 100644
index 0000000..545313e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-ta.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Tamil (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by S A Sureshkumar (saskumar at live.com). */
+jQuery(function($){
+	$.datepicker.regional['ta'] = {
+		closeText: 'மூடு',
+		prevText: 'முன்னையது',
+		nextText: 'அடுத்தது',
+		currentText: 'இன்று',
+		monthNames: ['தை','மாசி','பங்குனி','சித்திரை','வைகாசி','ஆனி',
+		'ஆடி','ஆவணி','புரட்டாசி','ஐப்பசி','கார்த்திகை','மார்கழி'],
+		monthNamesShort: ['தை','மாசி','பங்','சித்','வைகா','ஆனி',
+		'ஆடி','ஆவ','புர','ஐப்','கார்','மார்'],
+		dayNames: ['ஞாயிற்றுக்கிழமை','திங்கட்கிழமை','செவ்வாய்க்கிழமை','புதன்கிழமை','வியாழக்கிழமை','வெள்ளிக்கிழமை','சனிக்கிழமை'],
+		dayNamesShort: ['ஞாயிறு','திங்கள்','செவ்வாய்','புதன்','வியாழன்','வெள்ளி','சனி'],
+		dayNamesMin: ['ஞா','தி','செ','பு','வி','வெ','ச'],
+		weekHeader: 'Не',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['ta']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-th.js b/client/apollo/jslib/jqueryui/datepicker-th.js
new file mode 100644
index 0000000..4279f2e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-th.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Thai initialisation for the jQuery UI date picker plugin. */
+/* Written by pipo (pipo at sixhead.com). */
+jQuery(function($){
+	$.datepicker.regional['th'] = {
+		closeText: 'ปิด',
+		prevText: '« ย้อน',
+		nextText: 'ถัดไป »',
+		currentText: 'วันนี้',
+		monthNames: ['มกราคม','กุมภาพันธ์','มีนาคม','เมษายน','พฤษภาคม','มิถุนายน',
+		'กรกฎาคม','สิงหาคม','กันยายน','ตุลาคม','พฤศจิกายน','ธันวาคม'],
+		monthNamesShort: ['ม.ค.','ก.พ.','มี.ค.','เม.ย.','พ.ค.','มิ.ย.',
+		'ก.ค.','ส.ค.','ก.ย.','ต.ค.','พ.ย.','ธ.ค.'],
+		dayNames: ['อาทิตย์','จันทร์','อังคาร','พุธ','พฤหัสบดี','ศุกร์','เสาร์'],
+		dayNamesShort: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'],
+		dayNamesMin: ['อา.','จ.','อ.','พ.','พฤ.','ศ.','ส.'],
+		weekHeader: 'Wk',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['th']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-tj.js b/client/apollo/jslib/jqueryui/datepicker-tj.js
new file mode 100644
index 0000000..f8d2319
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-tj.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Tajiki (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Abdurahmon Saidov (saidovab at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['tj'] = {
+		closeText: 'Идома',
+		prevText: '&#x3c;Қафо',
+		nextText: 'Пеш&#x3e;',
+		currentText: 'Имрӯз',
+		monthNames: ['Январ','Феврал','Март','Апрел','Май','Июн',
+		'Июл','Август','Сентябр','Октябр','Ноябр','Декабр'],
+		monthNamesShort: ['Янв','Фев','Мар','Апр','Май','Июн',
+		'Июл','Авг','Сен','Окт','Ноя','Дек'],
+		dayNames: ['якшанбе','душанбе','сешанбе','чоршанбе','панҷшанбе','ҷумъа','шанбе'],
+		dayNamesShort: ['якш','душ','сеш','чор','пан','ҷум','шан'],
+		dayNamesMin: ['Як','Дш','Сш','Чш','Пш','Ҷм','Шн'],
+		weekHeader: 'Хф',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['tj']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-tr.js b/client/apollo/jslib/jqueryui/datepicker-tr.js
new file mode 100644
index 0000000..d668666
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-tr.js
@@ -0,0 +1,25 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Turkish initialisation for the jQuery UI date picker plugin. */
+/* Written by Izzet Emre Erkan (kara at karalamalar.net). */
+jQuery(function($){
+	$.datepicker.regional['tr'] = {
+		closeText: 'kapat',
+		prevText: '&#x3c;geri',
+		nextText: 'ileri&#x3e',
+		currentText: 'bugün',
+		monthNames: ['Ocak','Şubat','Mart','Nisan','Mayıs','Haziran',
+		'Temmuz','Ağustos','Eylül','Ekim','Kasım','Aralık'],
+		monthNamesShort: ['Oca','Şub','Mar','Nis','May','Haz',
+		'Tem','Ağu','Eyl','Eki','Kas','Ara'],
+		dayNames: ['Pazar','Pazartesi','Salı','Çarşamba','Perşembe','Cuma','Cumartesi'],
+		dayNamesShort: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
+		dayNamesMin: ['Pz','Pt','Sa','Ça','Pe','Cu','Ct'],
+		weekHeader: 'Hf',
+		dateFormat: 'dd.mm.yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['tr']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-uk.js b/client/apollo/jslib/jqueryui/datepicker-uk.js
new file mode 100644
index 0000000..a2c4fb2
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-uk.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Ukrainian (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Maxim Drogobitskiy (maxdao at gmail.com). */
+/* Corrected by Igor Milla (igor.fsp.milla at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['uk'] = {
+		closeText: 'Закрити',
+		prevText: '&#x3c;',
+		nextText: '&#x3e;',
+		currentText: 'Сьогодні',
+		monthNames: ['Січень','Лютий','Березень','Квітень','Травень','Червень',
+		'Липень','Серпень','Вересень','Жовтень','Листопад','Грудень'],
+		monthNamesShort: ['Січ','Лют','Бер','Кві','Тра','Чер',
+		'Лип','Сер','Вер','Жов','Лис','Гру'],
+		dayNames: ['неділя','понеділок','вівторок','середа','четвер','п’ятниця','субота'],
+		dayNamesShort: ['нед','пнд','вів','срд','чтв','птн','сбт'],
+		dayNamesMin: ['Нд','Пн','Вт','Ср','Чт','Пт','Сб'],
+		weekHeader: 'Тиж',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['uk']);
+});
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-vi.js b/client/apollo/jslib/jqueryui/datepicker-vi.js
new file mode 100644
index 0000000..454f757
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-vi.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Vietnamese initialisation for the jQuery UI date picker plugin. */
+/* Translated by Le Thanh Huy (lthanhhuy at cit.ctu.edu.vn). */
+jQuery(function($){
+	$.datepicker.regional['vi'] = {
+		closeText: 'Đóng',
+		prevText: '&#x3c;Trước',
+		nextText: 'Tiếp&#x3e;',
+		currentText: 'Hôm nay',
+		monthNames: ['Tháng Một', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu',
+		'Tháng Bảy', 'Tháng Tám', 'Tháng Chín', 'Tháng Mười', 'Tháng Mười Một', 'Tháng Mười Hai'],
+		monthNamesShort: ['Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6',
+		'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'],
+		dayNames: ['Chủ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy'],
+		dayNamesShort: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'],
+		dayNamesMin: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'],
+		weekHeader: 'Tu',
+		dateFormat: 'dd/mm/yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: false,
+		yearSuffix: ''};
+	$.datepicker.setDefaults($.datepicker.regional['vi']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-zh-CN.js b/client/apollo/jslib/jqueryui/datepicker-zh-CN.js
new file mode 100644
index 0000000..78cb3e6
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-zh-CN.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Chinese initialisation for the jQuery UI date picker plugin. */
+/* Written by Cloudream (cloudream at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['zh-CN'] = {
+		closeText: '关闭',
+		prevText: '&#x3c;上月',
+		nextText: '下月&#x3e;',
+		currentText: '今天',
+		monthNames: ['一月','二月','三月','四月','五月','六月',
+		'七月','八月','九月','十月','十一月','十二月'],
+		monthNamesShort: ['一','二','三','四','五','六',
+		'七','八','九','十','十一','十二'],
+		dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
+		dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
+		dayNamesMin: ['日','一','二','三','四','五','六'],
+		weekHeader: '周',
+		dateFormat: 'yy-mm-dd',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: '年'};
+	$.datepicker.setDefaults($.datepicker.regional['zh-CN']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-zh-HK.js b/client/apollo/jslib/jqueryui/datepicker-zh-HK.js
new file mode 100644
index 0000000..c530e9c
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-zh-HK.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Chinese initialisation for the jQuery UI date picker plugin. */
+/* Written by SCCY (samuelcychan at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['zh-HK'] = {
+		closeText: '關閉',
+		prevText: '&#x3c;上月',
+		nextText: '下月&#x3e;',
+		currentText: '今天',
+		monthNames: ['一月','二月','三月','四月','五月','六月',
+		'七月','八月','九月','十月','十一月','十二月'],
+		monthNamesShort: ['一','二','三','四','五','六',
+		'七','八','九','十','十一','十二'],
+		dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
+		dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
+		dayNamesMin: ['日','一','二','三','四','五','六'],
+		weekHeader: '周',
+		dateFormat: 'dd-mm-yy',
+		firstDay: 0,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: '年'};
+	$.datepicker.setDefaults($.datepicker.regional['zh-HK']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker-zh-TW.js b/client/apollo/jslib/jqueryui/datepicker-zh-TW.js
new file mode 100644
index 0000000..6a6156f
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker-zh-TW.js
@@ -0,0 +1,26 @@
+define(['jquery','jqueryui/datepicker'], function (jQuery) {
+/* Chinese initialisation for the jQuery UI date picker plugin. */
+/* Written by Ressol (ressol at gmail.com). */
+jQuery(function($){
+	$.datepicker.regional['zh-TW'] = {
+		closeText: '關閉',
+		prevText: '&#x3c;上月',
+		nextText: '下月&#x3e;',
+		currentText: '今天',
+		monthNames: ['一月','二月','三月','四月','五月','六月',
+		'七月','八月','九月','十月','十一月','十二月'],
+		monthNamesShort: ['一','二','三','四','五','六',
+		'七','八','九','十','十一','十二'],
+		dayNames: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
+		dayNamesShort: ['周日','周一','周二','周三','周四','周五','周六'],
+		dayNamesMin: ['日','一','二','三','四','五','六'],
+		weekHeader: '周',
+		dateFormat: 'yy/mm/dd',
+		firstDay: 1,
+		isRTL: false,
+		showMonthAfterYear: true,
+		yearSuffix: '年'};
+	$.datepicker.setDefaults($.datepicker.regional['zh-TW']);
+});
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/datepicker.js b/client/apollo/jslib/jqueryui/datepicker.js
new file mode 100644
index 0000000..9191ff7
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/datepicker.js
@@ -0,0 +1,1829 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Datepicker @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ */
+(function( $, undefined ) {
+
+$.extend($.ui, { datepicker: { version: "@VERSION" } });
+
+var PROP_NAME = 'datepicker';
+var dpuuid = new Date().getTime();
+var instActive;
+
+/* Date picker manager.
+   Use the singleton instance of this class, $.datepicker, to interact with the date picker.
+   Settings for (groups of) date pickers are maintained in an instance object,
+   allowing multiple different settings on the same page. */
+
+function Datepicker() {
+	this.debug = false; // Change this to true to start debugging
+	this._curInst = null; // The current instance in use
+	this._keyEvent = false; // If the last event was a key event
+	this._disabledInputs = []; // List of date picker inputs that have been disabled
+	this._datepickerShowing = false; // True if the popup picker is showing , false if not
+	this._inDialog = false; // True if showing within a "dialog", false if not
+	this._mainDivId = 'ui-datepicker-div'; // The ID of the main datepicker division
+	this._inlineClass = 'ui-datepicker-inline'; // The name of the inline marker class
+	this._appendClass = 'ui-datepicker-append'; // The name of the append marker class
+	this._triggerClass = 'ui-datepicker-trigger'; // The name of the trigger marker class
+	this._dialogClass = 'ui-datepicker-dialog'; // The name of the dialog marker class
+	this._disableClass = 'ui-datepicker-disabled'; // The name of the disabled covering marker class
+	this._unselectableClass = 'ui-datepicker-unselectable'; // The name of the unselectable cell marker class
+	this._currentClass = 'ui-datepicker-current-day'; // The name of the current day marker class
+	this._dayOverClass = 'ui-datepicker-days-cell-over'; // The name of the day hover marker class
+	this.regional = []; // Available regional settings, indexed by language code
+	this.regional[''] = { // Default regional settings
+		closeText: 'Done', // Display text for close link
+		prevText: 'Prev', // Display text for previous month link
+		nextText: 'Next', // Display text for next month link
+		currentText: 'Today', // Display text for current month link
+		monthNames: ['January','February','March','April','May','June',
+			'July','August','September','October','November','December'], // Names of months for drop-down and formatting
+		monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], // For formatting
+		dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // For formatting
+		dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], // For formatting
+		dayNamesMin: ['Su','Mo','Tu','We','Th','Fr','Sa'], // Column headings for days starting at Sunday
+		weekHeader: 'Wk', // Column header for week of the year
+		dateFormat: 'mm/dd/yy', // See format options on parseDate
+		firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ...
+		isRTL: false, // True if right-to-left language, false if left-to-right
+		showMonthAfterYear: false, // True if the year select precedes month, false for month then year
+		yearSuffix: '' // Additional text to append to the year in the month headers
+	};
+	this._defaults = { // Global defaults for all the date picker instances
+		showOn: 'focus', // 'focus' for popup on focus,
+			// 'button' for trigger button, or 'both' for either
+		showAnim: 'fadeIn', // Name of jQuery animation for popup
+		showOptions: {}, // Options for enhanced animations
+		defaultDate: null, // Used when field is blank: actual date,
+			// +/-number for offset from today, null for today
+		appendText: '', // Display text following the input box, e.g. showing the format
+		buttonText: '...', // Text for trigger button
+		buttonImage: '', // URL for trigger button image
+		buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
+		hideIfNoPrevNext: false, // True to hide next/previous month links
+			// if not applicable, false to just disable them
+		navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links
+		gotoCurrent: false, // True if today link goes back to current selection instead
+		changeMonth: false, // True if month can be selected directly, false if only prev/next
+		changeYear: false, // True if year can be selected directly, false if only prev/next
+		yearRange: 'c-10:c+10', // Range of years to display in drop-down,
+			// either relative to today's year (-nn:+nn), relative to currently displayed year
+			// (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)
+		showOtherMonths: false, // True to show dates in other months, false to leave blank
+		selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable
+		showWeek: false, // True to show week of the year, false to not show it
+		calculateWeek: this.iso8601Week, // How to calculate the week of the year,
+			// takes a Date and returns the number of the week for it
+		shortYearCutoff: '+10', // Short year values < this are in the current century,
+			// > this are in the previous century,
+			// string value starting with '+' for current year + value
+		minDate: null, // The earliest selectable date, or null for no limit
+		maxDate: null, // The latest selectable date, or null for no limit
+		duration: 'fast', // Duration of display/closure
+		beforeShowDay: null, // Function that takes a date and returns an array with
+			// [0] = true if selectable, false if not, [1] = custom CSS class name(s) or '',
+			// [2] = cell title (optional), e.g. $.datepicker.noWeekends
+		beforeShow: null, // Function that takes an input field and
+			// returns a set of custom settings for the date picker
+		onSelect: null, // Define a callback function when a date is selected
+		onChangeMonthYear: null, // Define a callback function when the month or year is changed
+		onClose: null, // Define a callback function when the datepicker is closed
+		numberOfMonths: 1, // Number of months to show at a time
+		showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)
+		stepMonths: 1, // Number of months to step back/forward
+		stepBigMonths: 12, // Number of months to step back/forward for the big links
+		altField: '', // Selector for an alternate field to store selected dates into
+		altFormat: '', // The date format to use for the alternate field
+		constrainInput: true, // The input is constrained by the current date format
+		showButtonPanel: false, // True to show button panel, false to not show it
+		autoSize: false, // True to size the input for the date format, false to leave as is
+		disabled: false // The initial disabled state
+	};
+	$.extend(this._defaults, this.regional['']);
+	this.dpDiv = bindHover($('<div id="' + this._mainDivId + '" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'));
+}
+
+$.extend(Datepicker.prototype, {
+	/* Class name added to elements to indicate already configured with a date picker. */
+	markerClassName: 'hasDatepicker',
+	
+	//Keep track of the maximum number of rows displayed (see #7043)
+	maxRows: 4,
+
+	/* Debug logging (if enabled). */
+	log: function () {
+		if (this.debug)
+			console.log.apply('', arguments);
+	},
+	
+	// TODO rename to "widget" when switching to widget factory
+	_widgetDatepicker: function() {
+		return this.dpDiv;
+	},
+
+	/* Override the default settings for all instances of the date picker.
+	   @param  settings  object - the new settings to use as defaults (anonymous object)
+	   @return the manager object */
+	setDefaults: function(settings) {
+		extendRemove(this._defaults, settings || {});
+		return this;
+	},
+
+	/* Attach the date picker to a jQuery selection.
+	   @param  target    element - the target input field or division or span
+	   @param  settings  object - the new settings to use for this date picker instance (anonymous) */
+	_attachDatepicker: function(target, settings) {
+		// check for settings on the control itself - in namespace 'date:'
+		var inlineSettings = null;
+		for (var attrName in this._defaults) {
+			var attrValue = target.getAttribute('date:' + attrName);
+			if (attrValue) {
+				inlineSettings = inlineSettings || {};
+				try {
+					inlineSettings[attrName] = eval(attrValue);
+				} catch (err) {
+					inlineSettings[attrName] = attrValue;
+				}
+			}
+		}
+		var nodeName = target.nodeName.toLowerCase();
+		var inline = (nodeName == 'div' || nodeName == 'span');
+		if (!target.id) {
+			this.uuid += 1;
+			target.id = 'dp' + this.uuid;
+		}
+		var inst = this._newInst($(target), inline);
+		inst.settings = $.extend({}, settings || {}, inlineSettings || {});
+		if (nodeName == 'input') {
+			this._connectDatepicker(target, inst);
+		} else if (inline) {
+			this._inlineDatepicker(target, inst);
+		}
+	},
+
+	/* Create a new instance object. */
+	_newInst: function(target, inline) {
+		var id = target[0].id.replace(/([^A-Za-z0-9_-])/g, '\\\\$1'); // escape jQuery meta chars
+		return {id: id, input: target, // associated target
+			selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection
+			drawMonth: 0, drawYear: 0, // month being drawn
+			inline: inline, // is datepicker inline or not
+			dpDiv: (!inline ? this.dpDiv : // presentation div
+			bindHover($('<div class="' + this._inlineClass + ' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')))};
+	},
+
+	/* Attach the date picker to an input field. */
+	_connectDatepicker: function(target, inst) {
+		var input = $(target);
+		inst.append = $([]);
+		inst.trigger = $([]);
+		if (input.hasClass(this.markerClassName))
+			return;
+		this._attachments(input, inst);
+		input.addClass(this.markerClassName).keydown(this._doKeyDown).
+			keypress(this._doKeyPress).keyup(this._doKeyUp).
+			bind("setData.datepicker", function(event, key, value) {
+				inst.settings[key] = value;
+			}).bind("getData.datepicker", function(event, key) {
+				return this._get(inst, key);
+			});
+		this._autoSize(inst);
+		$.data(target, PROP_NAME, inst);
+		//If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665)
+		if( inst.settings.disabled ) {
+			this._disableDatepicker( target );
+		}
+	},
+
+	/* Make attachments based on settings. */
+	_attachments: function(input, inst) {
+		var appendText = this._get(inst, 'appendText');
+		var isRTL = this._get(inst, 'isRTL');
+		if (inst.append)
+			inst.append.remove();
+		if (appendText) {
+			inst.append = $('<span class="' + this._appendClass + '">' + appendText + '</span>');
+			input[isRTL ? 'before' : 'after'](inst.append);
+		}
+		input.unbind('focus', this._showDatepicker);
+		if (inst.trigger)
+			inst.trigger.remove();
+		var showOn = this._get(inst, 'showOn');
+		if (showOn == 'focus' || showOn == 'both') // pop-up date picker when in the marked field
+			input.focus(this._showDatepicker);
+		if (showOn == 'button' || showOn == 'both') { // pop-up date picker when button clicked
+			var buttonText = this._get(inst, 'buttonText');
+			var buttonImage = this._get(inst, 'buttonImage');
+			inst.trigger = $(this._get(inst, 'buttonImageOnly') ?
+				$('<img/>').addClass(this._triggerClass).
+					attr({ src: buttonImage, alt: buttonText, title: buttonText }) :
+				$('<button type="button"></button>').addClass(this._triggerClass).
+					html(buttonImage == '' ? buttonText : $('<img/>').attr(
+					{ src:buttonImage, alt:buttonText, title:buttonText })));
+			input[isRTL ? 'before' : 'after'](inst.trigger);
+			inst.trigger.click(function() {
+				if ($.datepicker._datepickerShowing && $.datepicker._lastInput == input[0])
+					$.datepicker._hideDatepicker();
+				else if ($.datepicker._datepickerShowing && $.datepicker._lastInput != input[0]) {
+					$.datepicker._hideDatepicker(); 
+					$.datepicker._showDatepicker(input[0]);
+				} else
+					$.datepicker._showDatepicker(input[0]);
+				return false;
+			});
+		}
+	},
+
+	/* Apply the maximum length for the date format. */
+	_autoSize: function(inst) {
+		if (this._get(inst, 'autoSize') && !inst.inline) {
+			var date = new Date(2009, 12 - 1, 20); // Ensure double digits
+			var dateFormat = this._get(inst, 'dateFormat');
+			if (dateFormat.match(/[DM]/)) {
+				var findMax = function(names) {
+					var max = 0;
+					var maxI = 0;
+					for (var i = 0; i < names.length; i++) {
+						if (names[i].length > max) {
+							max = names[i].length;
+							maxI = i;
+						}
+					}
+					return maxI;
+				};
+				date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ?
+					'monthNames' : 'monthNamesShort'))));
+				date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ?
+					'dayNames' : 'dayNamesShort'))) + 20 - date.getDay());
+			}
+			inst.input.attr('size', this._formatDate(inst, date).length);
+		}
+	},
+
+	/* Attach an inline date picker to a div. */
+	_inlineDatepicker: function(target, inst) {
+		var divSpan = $(target);
+		if (divSpan.hasClass(this.markerClassName))
+			return;
+		divSpan.addClass(this.markerClassName).append(inst.dpDiv).
+			bind("setData.datepicker", function(event, key, value){
+				inst.settings[key] = value;
+			}).bind("getData.datepicker", function(event, key){
+				return this._get(inst, key);
+			});
+		$.data(target, PROP_NAME, inst);
+		this._setDate(inst, this._getDefaultDate(inst), true);
+		this._updateDatepicker(inst);
+		this._updateAlternate(inst);
+		//If disabled option is true, disable the datepicker before showing it (see ticket #5665)
+		if( inst.settings.disabled ) {
+			this._disableDatepicker( target );
+		}
+		// Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements
+		// http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height
+		inst.dpDiv.css( "display", "block" );
+	},
+
+	/* Pop-up the date picker in a "dialog" box.
+	   @param  input     element - ignored
+	   @param  date      string or Date - the initial date to display
+	   @param  onSelect  function - the function to call when a date is selected
+	   @param  settings  object - update the dialog date picker instance's settings (anonymous object)
+	   @param  pos       int[2] - coordinates for the dialog's position within the screen or
+	                     event - with x/y coordinates or
+	                     leave empty for default (screen centre)
+	   @return the manager object */
+	_dialogDatepicker: function(input, date, onSelect, settings, pos) {
+		var inst = this._dialogInst; // internal instance
+		if (!inst) {
+			this.uuid += 1;
+			var id = 'dp' + this.uuid;
+			this._dialogInput = $('<input type="text" id="' + id +
+				'" style="position: absolute; top: -100px; width: 0px; z-index: -10;"/>');
+			this._dialogInput.keydown(this._doKeyDown);
+			$('body').append(this._dialogInput);
+			inst = this._dialogInst = this._newInst(this._dialogInput, false);
+			inst.settings = {};
+			$.data(this._dialogInput[0], PROP_NAME, inst);
+		}
+		extendRemove(inst.settings, settings || {});
+		date = (date && date.constructor == Date ? this._formatDate(inst, date) : date);
+		this._dialogInput.val(date);
+
+		this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null);
+		if (!this._pos) {
+			var browserWidth = document.documentElement.clientWidth;
+			var browserHeight = document.documentElement.clientHeight;
+			var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
+			var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
+			this._pos = // should use actual width/height below
+				[(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY];
+		}
+
+		// move input on screen for focus, but hidden behind dialog
+		this._dialogInput.css('left', (this._pos[0] + 20) + 'px').css('top', this._pos[1] + 'px');
+		inst.settings.onSelect = onSelect;
+		this._inDialog = true;
+		this.dpDiv.addClass(this._dialogClass);
+		this._showDatepicker(this._dialogInput[0]);
+		if ($.blockUI)
+			$.blockUI(this.dpDiv);
+		$.data(this._dialogInput[0], PROP_NAME, inst);
+		return this;
+	},
+
+	/* Detach a datepicker from its control.
+	   @param  target    element - the target input field or division or span */
+	_destroyDatepicker: function(target) {
+		var $target = $(target);
+		var inst = $.data(target, PROP_NAME);
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+		var nodeName = target.nodeName.toLowerCase();
+		$.removeData(target, PROP_NAME);
+		if (nodeName == 'input') {
+			inst.append.remove();
+			inst.trigger.remove();
+			$target.removeClass(this.markerClassName).
+				unbind('focus', this._showDatepicker).
+				unbind('keydown', this._doKeyDown).
+				unbind('keypress', this._doKeyPress).
+				unbind('keyup', this._doKeyUp);
+		} else if (nodeName == 'div' || nodeName == 'span')
+			$target.removeClass(this.markerClassName).empty();
+	},
+
+	/* Enable the date picker to a jQuery selection.
+	   @param  target    element - the target input field or division or span */
+	_enableDatepicker: function(target) {
+		var $target = $(target);
+		var inst = $.data(target, PROP_NAME);
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+		var nodeName = target.nodeName.toLowerCase();
+		if (nodeName == 'input') {
+			target.disabled = false;
+			inst.trigger.filter('button').
+				each(function() { this.disabled = false; }).end().
+				filter('img').css({opacity: '1.0', cursor: ''});
+		}
+		else if (nodeName == 'div' || nodeName == 'span') {
+			var inline = $target.children('.' + this._inlineClass);
+			inline.children().removeClass('ui-state-disabled');
+			inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
+				removeAttr("disabled");
+		}
+		this._disabledInputs = $.map(this._disabledInputs,
+			function(value) { return (value == target ? null : value); }); // delete entry
+	},
+
+	/* Disable the date picker to a jQuery selection.
+	   @param  target    element - the target input field or division or span */
+	_disableDatepicker: function(target) {
+		var $target = $(target);
+		var inst = $.data(target, PROP_NAME);
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+		var nodeName = target.nodeName.toLowerCase();
+		if (nodeName == 'input') {
+			target.disabled = true;
+			inst.trigger.filter('button').
+				each(function() { this.disabled = true; }).end().
+				filter('img').css({opacity: '0.5', cursor: 'default'});
+		}
+		else if (nodeName == 'div' || nodeName == 'span') {
+			var inline = $target.children('.' + this._inlineClass);
+			inline.children().addClass('ui-state-disabled');
+			inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
+				attr("disabled", "disabled");
+		}
+		this._disabledInputs = $.map(this._disabledInputs,
+			function(value) { return (value == target ? null : value); }); // delete entry
+		this._disabledInputs[this._disabledInputs.length] = target;
+	},
+
+	/* Is the first field in a jQuery collection disabled as a datepicker?
+	   @param  target    element - the target input field or division or span
+	   @return boolean - true if disabled, false if enabled */
+	_isDisabledDatepicker: function(target) {
+		if (!target) {
+			return false;
+		}
+		for (var i = 0; i < this._disabledInputs.length; i++) {
+			if (this._disabledInputs[i] == target)
+				return true;
+		}
+		return false;
+	},
+
+	/* Retrieve the instance data for the target control.
+	   @param  target  element - the target input field or division or span
+	   @return  object - the associated instance data
+	   @throws  error if a jQuery problem getting data */
+	_getInst: function(target) {
+		try {
+			return $.data(target, PROP_NAME);
+		}
+		catch (err) {
+			throw 'Missing instance data for this datepicker';
+		}
+	},
+
+	/* Update or retrieve the settings for a date picker attached to an input field or division.
+	   @param  target  element - the target input field or division or span
+	   @param  name    object - the new settings to update or
+	                   string - the name of the setting to change or retrieve,
+	                   when retrieving also 'all' for all instance settings or
+	                   'defaults' for all global defaults
+	   @param  value   any - the new value for the setting
+	                   (omit if above is an object or to retrieve a value) */
+	_optionDatepicker: function(target, name, value) {
+		var inst = this._getInst(target);
+		if (arguments.length == 2 && typeof name == 'string') {
+			return (name == 'defaults' ? $.extend({}, $.datepicker._defaults) :
+				(inst ? (name == 'all' ? $.extend({}, inst.settings) :
+				this._get(inst, name)) : null));
+		}
+		var settings = name || {};
+		if (typeof name == 'string') {
+			settings = {};
+			settings[name] = value;
+		}
+		if (inst) {
+			if (this._curInst == inst) {
+				this._hideDatepicker();
+			}
+			var date = this._getDateDatepicker(target, true);
+			var minDate = this._getMinMaxDate(inst, 'min');
+			var maxDate = this._getMinMaxDate(inst, 'max');
+			extendRemove(inst.settings, settings);
+			// reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided
+			if (minDate !== null && settings['dateFormat'] !== undefined && settings['minDate'] === undefined)
+				inst.settings.minDate = this._formatDate(inst, minDate);
+			if (maxDate !== null && settings['dateFormat'] !== undefined && settings['maxDate'] === undefined)
+				inst.settings.maxDate = this._formatDate(inst, maxDate);
+			this._attachments($(target), inst);
+			this._autoSize(inst);
+			this._setDate(inst, date);
+			this._updateAlternate(inst);
+			this._updateDatepicker(inst);
+		}
+	},
+
+	// change method deprecated
+	_changeDatepicker: function(target, name, value) {
+		this._optionDatepicker(target, name, value);
+	},
+
+	/* Redraw the date picker attached to an input field or division.
+	   @param  target  element - the target input field or division or span */
+	_refreshDatepicker: function(target) {
+		var inst = this._getInst(target);
+		if (inst) {
+			this._updateDatepicker(inst);
+		}
+	},
+
+	/* Set the dates for a jQuery selection.
+	   @param  target   element - the target input field or division or span
+	   @param  date     Date - the new date */
+	_setDateDatepicker: function(target, date) {
+		var inst = this._getInst(target);
+		if (inst) {
+			this._setDate(inst, date);
+			this._updateDatepicker(inst);
+			this._updateAlternate(inst);
+		}
+	},
+
+	/* Get the date(s) for the first entry in a jQuery selection.
+	   @param  target     element - the target input field or division or span
+	   @param  noDefault  boolean - true if no default date is to be used
+	   @return Date - the current date */
+	_getDateDatepicker: function(target, noDefault) {
+		var inst = this._getInst(target);
+		if (inst && !inst.inline)
+			this._setDateFromField(inst, noDefault);
+		return (inst ? this._getDate(inst) : null);
+	},
+
+	/* Handle keystrokes. */
+	_doKeyDown: function(event) {
+		var inst = $.datepicker._getInst(event.target);
+		var handled = true;
+		var isRTL = inst.dpDiv.is('.ui-datepicker-rtl');
+		inst._keyEvent = true;
+		if ($.datepicker._datepickerShowing)
+			switch (event.keyCode) {
+				case 9: $.datepicker._hideDatepicker();
+						handled = false;
+						break; // hide on tab out
+				case 13: var sel = $('td.' + $.datepicker._dayOverClass + ':not(.' + 
+									$.datepicker._currentClass + ')', inst.dpDiv);
+						if (sel[0])
+							$.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]);
+							var onSelect = $.datepicker._get(inst, 'onSelect');
+							if (onSelect) {
+								var dateStr = $.datepicker._formatDate(inst);
+
+								// trigger custom callback
+								onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);
+							}
+						else
+							$.datepicker._hideDatepicker();
+						return false; // don't submit the form
+						break; // select the value on enter
+				case 27: $.datepicker._hideDatepicker();
+						break; // hide on escape
+				case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+							-$.datepicker._get(inst, 'stepBigMonths') :
+							-$.datepicker._get(inst, 'stepMonths')), 'M');
+						break; // previous month/year on page up/+ ctrl
+				case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+							+$.datepicker._get(inst, 'stepBigMonths') :
+							+$.datepicker._get(inst, 'stepMonths')), 'M');
+						break; // next month/year on page down/+ ctrl
+				case 35: if (event.ctrlKey || event.metaKey) $.datepicker._clearDate(event.target);
+						handled = event.ctrlKey || event.metaKey;
+						break; // clear on ctrl or command +end
+				case 36: if (event.ctrlKey || event.metaKey) $.datepicker._gotoToday(event.target);
+						handled = event.ctrlKey || event.metaKey;
+						break; // current on ctrl or command +home
+				case 37: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), 'D');
+						handled = event.ctrlKey || event.metaKey;
+						// -1 day on ctrl or command +left
+						if (event.originalEvent.altKey) $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+									-$.datepicker._get(inst, 'stepBigMonths') :
+									-$.datepicker._get(inst, 'stepMonths')), 'M');
+						// next month/year on alt +left on Mac
+						break;
+				case 38: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, -7, 'D');
+						handled = event.ctrlKey || event.metaKey;
+						break; // -1 week on ctrl or command +up
+				case 39: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), 'D');
+						handled = event.ctrlKey || event.metaKey;
+						// +1 day on ctrl or command +right
+						if (event.originalEvent.altKey) $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+									+$.datepicker._get(inst, 'stepBigMonths') :
+									+$.datepicker._get(inst, 'stepMonths')), 'M');
+						// next month/year on alt +right
+						break;
+				case 40: if (event.ctrlKey || event.metaKey) $.datepicker._adjustDate(event.target, +7, 'D');
+						handled = event.ctrlKey || event.metaKey;
+						break; // +1 week on ctrl or command +down
+				default: handled = false;
+			}
+		else if (event.keyCode == 36 && event.ctrlKey) // display the date picker on ctrl+home
+			$.datepicker._showDatepicker(this);
+		else {
+			handled = false;
+		}
+		if (handled) {
+			event.preventDefault();
+			event.stopPropagation();
+		}
+	},
+
+	/* Filter entered characters - based on date format. */
+	_doKeyPress: function(event) {
+		var inst = $.datepicker._getInst(event.target);
+		if ($.datepicker._get(inst, 'constrainInput')) {
+			var chars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat'));
+			var chr = String.fromCharCode(event.charCode == undefined ? event.keyCode : event.charCode);
+			return event.ctrlKey || event.metaKey || (chr < ' ' || !chars || chars.indexOf(chr) > -1);
+		}
+	},
+
+	/* Synchronise manual entry and field/alternate field. */
+	_doKeyUp: function(event) {
+		var inst = $.datepicker._getInst(event.target);
+		if (inst.input.val() != inst.lastVal) {
+			try {
+				var date = $.datepicker.parseDate($.datepicker._get(inst, 'dateFormat'),
+					(inst.input ? inst.input.val() : null),
+					$.datepicker._getFormatConfig(inst));
+				if (date) { // only if valid
+					$.datepicker._setDateFromField(inst);
+					$.datepicker._updateAlternate(inst);
+					$.datepicker._updateDatepicker(inst);
+				}
+			}
+			catch (event) {
+				$.datepicker.log(event);
+			}
+		}
+		return true;
+	},
+
+	/* Pop-up the date picker for a given input field.
+       If false returned from beforeShow event handler do not show. 
+	   @param  input  element - the input field attached to the date picker or
+	                  event - if triggered by focus */
+	_showDatepicker: function(input) {
+		input = input.target || input;
+		if (input.nodeName.toLowerCase() != 'input') // find from button/image trigger
+			input = $('input', input.parentNode)[0];
+		if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput == input) // already here
+			return;
+		var inst = $.datepicker._getInst(input);
+		if ($.datepicker._curInst && $.datepicker._curInst != inst) {
+			$.datepicker._curInst.dpDiv.stop(true, true);
+			if ( inst && $.datepicker._datepickerShowing ) {
+				$.datepicker._hideDatepicker( $.datepicker._curInst.input[0] );
+			}
+		}
+		var beforeShow = $.datepicker._get(inst, 'beforeShow');
+		var beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {};
+		if(beforeShowSettings === false){
+            //false
+			return;
+		}
+		extendRemove(inst.settings, beforeShowSettings);
+		inst.lastVal = null;
+		$.datepicker._lastInput = input;
+		$.datepicker._setDateFromField(inst);
+		if ($.datepicker._inDialog) // hide cursor
+			input.value = '';
+		if (!$.datepicker._pos) { // position below input
+			$.datepicker._pos = $.datepicker._findPos(input);
+			$.datepicker._pos[1] += input.offsetHeight; // add the height
+		}
+		var isFixed = false;
+		$(input).parents().each(function() {
+			isFixed |= $(this).css('position') == 'fixed';
+			return !isFixed;
+		});
+		if (isFixed && $.browser.opera) { // correction for Opera when fixed and scrolled
+			$.datepicker._pos[0] -= document.documentElement.scrollLeft;
+			$.datepicker._pos[1] -= document.documentElement.scrollTop;
+		}
+		var offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]};
+		$.datepicker._pos = null;
+		//to avoid flashes on Firefox
+		inst.dpDiv.empty();
+		// determine sizing offscreen
+		inst.dpDiv.css({position: 'absolute', display: 'block', top: '-1000px'});
+		$.datepicker._updateDatepicker(inst);
+		// fix width for dynamic number of date pickers
+		// and adjust position before showing
+		offset = $.datepicker._checkOffset(inst, offset, isFixed);
+		inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ?
+			'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none',
+			left: offset.left + 'px', top: offset.top + 'px'});
+		if (!inst.inline) {
+			var showAnim = $.datepicker._get(inst, 'showAnim');
+			var duration = $.datepicker._get(inst, 'duration');
+			var postProcess = function() {
+				var cover = inst.dpDiv.find('iframe.ui-datepicker-cover'); // IE6- only
+				if( !! cover.length ){
+					var borders = $.datepicker._getBorders(inst.dpDiv);
+					cover.css({left: -borders[0], top: -borders[1],
+						width: inst.dpDiv.outerWidth(), height: inst.dpDiv.outerHeight()});
+				}
+			};
+			inst.dpDiv.zIndex($(input).zIndex()+1);
+			$.datepicker._datepickerShowing = true;
+			if ($.effects && $.effects[showAnim])
+				inst.dpDiv.show(showAnim, $.datepicker._get(inst, 'showOptions'), duration, postProcess);
+			else
+				inst.dpDiv[showAnim || 'show']((showAnim ? duration : null), postProcess);
+			if (!showAnim || !duration)
+				postProcess();
+			if (inst.input.is(':visible') && !inst.input.is(':disabled'))
+				inst.input.focus();
+			$.datepicker._curInst = inst;
+		}
+	},
+
+	/* Generate the date picker content. */
+	_updateDatepicker: function(inst) {
+		var self = this;
+		self.maxRows = 4; //Reset the max number of rows being displayed (see #7043)
+		var borders = $.datepicker._getBorders(inst.dpDiv);
+		instActive = inst; // for delegate hover events
+		inst.dpDiv.empty().append(this._generateHTML(inst));
+		var cover = inst.dpDiv.find('iframe.ui-datepicker-cover'); // IE6- only
+		if( !!cover.length ){ //avoid call to outerXXXX() when not in IE6
+			cover.css({left: -borders[0], top: -borders[1], width: inst.dpDiv.outerWidth(), height: inst.dpDiv.outerHeight()})
+		}
+		inst.dpDiv.find('.' + this._dayOverClass + ' a').mouseover();
+		var numMonths = this._getNumberOfMonths(inst);
+		var cols = numMonths[1];
+		var width = 17;
+		inst.dpDiv.removeClass('ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4').width('');
+		if (cols > 1)
+			inst.dpDiv.addClass('ui-datepicker-multi-' + cols).css('width', (width * cols) + 'em');
+		inst.dpDiv[(numMonths[0] != 1 || numMonths[1] != 1 ? 'add' : 'remove') +
+			'Class']('ui-datepicker-multi');
+		inst.dpDiv[(this._get(inst, 'isRTL') ? 'add' : 'remove') +
+			'Class']('ui-datepicker-rtl');
+		if (inst == $.datepicker._curInst && $.datepicker._datepickerShowing && inst.input &&
+				// #6694 - don't focus the input if it's already focused
+				// this breaks the change event in IE
+				inst.input.is(':visible') && !inst.input.is(':disabled') && inst.input[0] != document.activeElement)
+			inst.input.focus();
+		// deffered render of the years select (to avoid flashes on Firefox) 
+		if( inst.yearshtml ){
+			var origyearshtml = inst.yearshtml;
+			setTimeout(function(){
+				//assure that inst.yearshtml didn't change.
+				if( origyearshtml === inst.yearshtml && inst.yearshtml ){
+					inst.dpDiv.find('select.ui-datepicker-year:first').replaceWith(inst.yearshtml);
+				}
+				origyearshtml = inst.yearshtml = null;
+			}, 0);
+		}
+	},
+
+	/* Retrieve the size of left and top borders for an element.
+	   @param  elem  (jQuery object) the element of interest
+	   @return  (number[2]) the left and top borders */
+	_getBorders: function(elem) {
+		var convert = function(value) {
+			return {thin: 1, medium: 2, thick: 3}[value] || value;
+		};
+		return [parseFloat(convert(elem.css('border-left-width'))),
+			parseFloat(convert(elem.css('border-top-width')))];
+	},
+
+	/* Check positioning to remain on screen. */
+	_checkOffset: function(inst, offset, isFixed) {
+		var dpWidth = inst.dpDiv.outerWidth();
+		var dpHeight = inst.dpDiv.outerHeight();
+		var inputWidth = inst.input ? inst.input.outerWidth() : 0;
+		var inputHeight = inst.input ? inst.input.outerHeight() : 0;
+		var viewWidth = document.documentElement.clientWidth + $(document).scrollLeft();
+		var viewHeight = document.documentElement.clientHeight + $(document).scrollTop();
+
+		offset.left -= (this._get(inst, 'isRTL') ? (dpWidth - inputWidth) : 0);
+		offset.left -= (isFixed && offset.left == inst.input.offset().left) ? $(document).scrollLeft() : 0;
+		offset.top -= (isFixed && offset.top == (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0;
+
+		// now check if datepicker is showing outside window viewport - move to a better place if so.
+		offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
+			Math.abs(offset.left + dpWidth - viewWidth) : 0);
+		offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+			Math.abs(dpHeight + inputHeight) : 0);
+
+		return offset;
+	},
+
+	/* Find an object's position on the screen. */
+	_findPos: function(obj) {
+		var inst = this._getInst(obj);
+		var isRTL = this._get(inst, 'isRTL');
+        while (obj && (obj.type == 'hidden' || obj.nodeType != 1 || $.expr.filters.hidden(obj))) {
+            obj = obj[isRTL ? 'previousSibling' : 'nextSibling'];
+        }
+        var position = $(obj).offset();
+	    return [position.left, position.top];
+	},
+
+	/* Hide the date picker from view.
+	   @param  input  element - the input field attached to the date picker */
+	_hideDatepicker: function(input) {
+		var inst = this._curInst;
+		if (!inst || (input && inst != $.data(input, PROP_NAME)))
+			return;
+		if (this._datepickerShowing) {
+			var showAnim = this._get(inst, 'showAnim');
+			var duration = this._get(inst, 'duration');
+			var self = this;
+			var postProcess = function() {
+				$.datepicker._tidyDialog(inst);
+				self._curInst = null;
+			};
+			if ($.effects && $.effects[showAnim])
+				inst.dpDiv.hide(showAnim, $.datepicker._get(inst, 'showOptions'), duration, postProcess);
+			else
+				inst.dpDiv[(showAnim == 'slideDown' ? 'slideUp' :
+					(showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess);
+			if (!showAnim)
+				postProcess();
+			this._datepickerShowing = false;
+			var onClose = this._get(inst, 'onClose');
+			if (onClose)
+				onClose.apply((inst.input ? inst.input[0] : null),
+					[(inst.input ? inst.input.val() : ''), inst]);
+			this._lastInput = null;
+			if (this._inDialog) {
+				this._dialogInput.css({ position: 'absolute', left: '0', top: '-100px' });
+				if ($.blockUI) {
+					$.unblockUI();
+					$('body').append(this.dpDiv);
+				}
+			}
+			this._inDialog = false;
+		}
+	},
+
+	/* Tidy up after a dialog display. */
+	_tidyDialog: function(inst) {
+		inst.dpDiv.removeClass(this._dialogClass).unbind('.ui-datepicker-calendar');
+	},
+
+	/* Close date picker if clicked elsewhere. */
+	_checkExternalClick: function(event) {
+		if (!$.datepicker._curInst)
+			return;
+
+		var $target = $(event.target),
+			inst = $.datepicker._getInst($target[0]);
+
+		if ( ( ( $target[0].id != $.datepicker._mainDivId &&
+				$target.parents('#' + $.datepicker._mainDivId).length == 0 &&
+				!$target.hasClass($.datepicker.markerClassName) &&
+				!$target.closest("." + $.datepicker._triggerClass).length &&
+				$.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) ||
+			( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst != inst ) )
+			$.datepicker._hideDatepicker();
+	},
+
+	/* Adjust one of the date sub-fields. */
+	_adjustDate: function(id, offset, period) {
+		var target = $(id);
+		var inst = this._getInst(target[0]);
+		if (this._isDisabledDatepicker(target[0])) {
+			return;
+		}
+		this._adjustInstDate(inst, offset +
+			(period == 'M' ? this._get(inst, 'showCurrentAtPos') : 0), // undo positioning
+			period);
+		this._updateDatepicker(inst);
+	},
+
+	/* Action for current link. */
+	_gotoToday: function(id) {
+		var target = $(id);
+		var inst = this._getInst(target[0]);
+		if (this._get(inst, 'gotoCurrent') && inst.currentDay) {
+			inst.selectedDay = inst.currentDay;
+			inst.drawMonth = inst.selectedMonth = inst.currentMonth;
+			inst.drawYear = inst.selectedYear = inst.currentYear;
+		}
+		else {
+			var date = new Date();
+			inst.selectedDay = date.getDate();
+			inst.drawMonth = inst.selectedMonth = date.getMonth();
+			inst.drawYear = inst.selectedYear = date.getFullYear();
+		}
+		this._notifyChange(inst);
+		this._adjustDate(target);
+	},
+
+	/* Action for selecting a new month/year. */
+	_selectMonthYear: function(id, select, period) {
+		var target = $(id);
+		var inst = this._getInst(target[0]);
+		inst['selected' + (period == 'M' ? 'Month' : 'Year')] =
+		inst['draw' + (period == 'M' ? 'Month' : 'Year')] =
+			parseInt(select.options[select.selectedIndex].value,10);
+		this._notifyChange(inst);
+		this._adjustDate(target);
+	},
+
+	/* Action for selecting a day. */
+	_selectDay: function(id, month, year, td) {
+		var target = $(id);
+		if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) {
+			return;
+		}
+		var inst = this._getInst(target[0]);
+		inst.selectedDay = inst.currentDay = $('a', td).html();
+		inst.selectedMonth = inst.currentMonth = month;
+		inst.selectedYear = inst.currentYear = year;
+		this._selectDate(id, this._formatDate(inst,
+			inst.currentDay, inst.currentMonth, inst.currentYear));
+	},
+
+	/* Erase the input field and hide the date picker. */
+	_clearDate: function(id) {
+		var target = $(id);
+		var inst = this._getInst(target[0]);
+		this._selectDate(target, '');
+	},
+
+	/* Update the input field with the selected date. */
+	_selectDate: function(id, dateStr) {
+		var target = $(id);
+		var inst = this._getInst(target[0]);
+		dateStr = (dateStr != null ? dateStr : this._formatDate(inst));
+		if (inst.input)
+			inst.input.val(dateStr);
+		this._updateAlternate(inst);
+		var onSelect = this._get(inst, 'onSelect');
+		if (onSelect)
+			onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);  // trigger custom callback
+		else if (inst.input)
+			inst.input.trigger('change'); // fire the change event
+		if (inst.inline)
+			this._updateDatepicker(inst);
+		else {
+			this._hideDatepicker();
+			this._lastInput = inst.input[0];
+			if (typeof(inst.input[0]) != 'object')
+				inst.input.focus(); // restore focus
+			this._lastInput = null;
+		}
+	},
+
+	/* Update any alternate field to synchronise with the main field. */
+	_updateAlternate: function(inst) {
+		var altField = this._get(inst, 'altField');
+		if (altField) { // update alternate field too
+			var altFormat = this._get(inst, 'altFormat') || this._get(inst, 'dateFormat');
+			var date = this._getDate(inst);
+			var dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst));
+			$(altField).each(function() { $(this).val(dateStr); });
+		}
+	},
+
+	/* Set as beforeShowDay function to prevent selection of weekends.
+	   @param  date  Date - the date to customise
+	   @return [boolean, string] - is this date selectable?, what is its CSS class? */
+	noWeekends: function(date) {
+		var day = date.getDay();
+		return [(day > 0 && day < 6), ''];
+	},
+
+	/* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+	   @param  date  Date - the date to get the week for
+	   @return  number - the number of the week within the year that contains this date */
+	iso8601Week: function(date) {
+		var checkDate = new Date(date.getTime());
+		// Find Thursday of this week starting on Monday
+		checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+		var time = checkDate.getTime();
+		checkDate.setMonth(0); // Compare with Jan 1
+		checkDate.setDate(1);
+		return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+	},
+
+	/* Parse a string value into a date object.
+	   See formatDate below for the possible formats.
+
+	   @param  format    string - the expected format of the date
+	   @param  value     string - the date in the above format
+	   @param  settings  Object - attributes include:
+	                     shortYearCutoff  number - the cutoff year for determining the century (optional)
+	                     dayNamesShort    string[7] - abbreviated names of the days from Sunday (optional)
+	                     dayNames         string[7] - names of the days from Sunday (optional)
+	                     monthNamesShort  string[12] - abbreviated names of the months (optional)
+	                     monthNames       string[12] - names of the months (optional)
+	   @return  Date - the extracted date value or null if value is blank */
+	parseDate: function (format, value, settings) {
+		if (format == null || value == null)
+			throw 'Invalid arguments';
+		value = (typeof value == 'object' ? value.toString() : value + '');
+		if (value == '')
+			return null;
+		var shortYearCutoff = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff;
+		shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff :
+				new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10));
+		var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort;
+		var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames;
+		var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort;
+		var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames;
+		var year = -1;
+		var month = -1;
+		var day = -1;
+		var doy = -1;
+		var literal = false;
+		// Check whether a format character is doubled
+		var lookAhead = function(match) {
+			var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match);
+			if (matches)
+				iFormat++;
+			return matches;
+		};
+		// Extract a number from the string value
+		var getNumber = function(match) {
+			var isDoubled = lookAhead(match);
+			var size = (match == '@' ? 14 : (match == '!' ? 20 :
+				(match == 'y' && isDoubled ? 4 : (match == 'o' ? 3 : 2))));
+			var digits = new RegExp('^\\d{1,' + size + '}');
+			var num = value.substring(iValue).match(digits);
+			if (!num)
+				throw 'Missing number at position ' + iValue;
+			iValue += num[0].length;
+			return parseInt(num[0], 10);
+		};
+		// Extract a name from the string value and convert to an index
+		var getName = function(match, shortNames, longNames) {
+			var names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) {
+				return [ [k, v] ];
+			}).sort(function (a, b) {
+				return -(a[1].length - b[1].length);
+			});
+			var index = -1;
+			$.each(names, function (i, pair) {
+				var name = pair[1];
+				if (value.substr(iValue, name.length).toLowerCase() == name.toLowerCase()) {
+					index = pair[0];
+					iValue += name.length;
+					return false;
+				}
+			});
+			if (index != -1)
+				return index + 1;
+			else
+				throw 'Unknown name at position ' + iValue;
+		};
+		// Confirm that a literal character matches the string value
+		var checkLiteral = function() {
+			if (value.charAt(iValue) != format.charAt(iFormat))
+				throw 'Unexpected literal at position ' + iValue;
+			iValue++;
+		};
+		var iValue = 0;
+		for (var iFormat = 0; iFormat < format.length; iFormat++) {
+			if (literal)
+				if (format.charAt(iFormat) == "'" && !lookAhead("'"))
+					literal = false;
+				else
+					checkLiteral();
+			else
+				switch (format.charAt(iFormat)) {
+					case 'd':
+						day = getNumber('d');
+						break;
+					case 'D':
+						getName('D', dayNamesShort, dayNames);
+						break;
+					case 'o':
+						doy = getNumber('o');
+						break;
+					case 'm':
+						month = getNumber('m');
+						break;
+					case 'M':
+						month = getName('M', monthNamesShort, monthNames);
+						break;
+					case 'y':
+						year = getNumber('y');
+						break;
+					case '@':
+						var date = new Date(getNumber('@'));
+						year = date.getFullYear();
+						month = date.getMonth() + 1;
+						day = date.getDate();
+						break;
+					case '!':
+						var date = new Date((getNumber('!') - this._ticksTo1970) / 10000);
+						year = date.getFullYear();
+						month = date.getMonth() + 1;
+						day = date.getDate();
+						break;
+					case "'":
+						if (lookAhead("'"))
+							checkLiteral();
+						else
+							literal = true;
+						break;
+					default:
+						checkLiteral();
+				}
+		}
+		if (iValue < value.length){
+			throw "Extra/unparsed characters found in date: " + value.substring(iValue);
+		}
+		if (year == -1)
+			year = new Date().getFullYear();
+		else if (year < 100)
+			year += new Date().getFullYear() - new Date().getFullYear() % 100 +
+				(year <= shortYearCutoff ? 0 : -100);
+		if (doy > -1) {
+			month = 1;
+			day = doy;
+			do {
+				var dim = this._getDaysInMonth(year, month - 1);
+				if (day <= dim)
+					break;
+				month++;
+				day -= dim;
+			} while (true);
+		}
+		var date = this._daylightSavingAdjust(new Date(year, month - 1, day));
+		if (date.getFullYear() != year || date.getMonth() + 1 != month || date.getDate() != day)
+			throw 'Invalid date'; // E.g. 31/02/00
+		return date;
+	},
+
+	/* Standard date formats. */
+	ATOM: 'yy-mm-dd', // RFC 3339 (ISO 8601)
+	COOKIE: 'D, dd M yy',
+	ISO_8601: 'yy-mm-dd',
+	RFC_822: 'D, d M y',
+	RFC_850: 'DD, dd-M-y',
+	RFC_1036: 'D, d M y',
+	RFC_1123: 'D, d M yy',
+	RFC_2822: 'D, d M yy',
+	RSS: 'D, d M y', // RFC 822
+	TICKS: '!',
+	TIMESTAMP: '@',
+	W3C: 'yy-mm-dd', // ISO 8601
+
+	_ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) +
+		Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000),
+
+	/* Format a date object into a string value.
+	   The format can be combinations of the following:
+	   d  - day of month (no leading zero)
+	   dd - day of month (two digit)
+	   o  - day of year (no leading zeros)
+	   oo - day of year (three digit)
+	   D  - day name short
+	   DD - day name long
+	   m  - month of year (no leading zero)
+	   mm - month of year (two digit)
+	   M  - month name short
+	   MM - month name long
+	   y  - year (two digit)
+	   yy - year (four digit)
+	   @ - Unix timestamp (ms since 01/01/1970)
+	   ! - Windows ticks (100ns since 01/01/0001)
+	   '...' - literal text
+	   '' - single quote
+
+	   @param  format    string - the desired format of the date
+	   @param  date      Date - the date value to format
+	   @param  settings  Object - attributes include:
+	                     dayNamesShort    string[7] - abbreviated names of the days from Sunday (optional)
+	                     dayNames         string[7] - names of the days from Sunday (optional)
+	                     monthNamesShort  string[12] - abbreviated names of the months (optional)
+	                     monthNames       string[12] - names of the months (optional)
+	   @return  string - the date in the above format */
+	formatDate: function (format, date, settings) {
+		if (!date)
+			return '';
+		var dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort;
+		var dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames;
+		var monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort;
+		var monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames;
+		// Check whether a format character is doubled
+		var lookAhead = function(match) {
+			var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match);
+			if (matches)
+				iFormat++;
+			return matches;
+		};
+		// Format a number, with leading zero if necessary
+		var formatNumber = function(match, value, len) {
+			var num = '' + value;
+			if (lookAhead(match))
+				while (num.length < len)
+					num = '0' + num;
+			return num;
+		};
+		// Format a name, short or long as requested
+		var formatName = function(match, value, shortNames, longNames) {
+			return (lookAhead(match) ? longNames[value] : shortNames[value]);
+		};
+		var output = '';
+		var literal = false;
+		if (date)
+			for (var iFormat = 0; iFormat < format.length; iFormat++) {
+				if (literal)
+					if (format.charAt(iFormat) == "'" && !lookAhead("'"))
+						literal = false;
+					else
+						output += format.charAt(iFormat);
+				else
+					switch (format.charAt(iFormat)) {
+						case 'd':
+							output += formatNumber('d', date.getDate(), 2);
+							break;
+						case 'D':
+							output += formatName('D', date.getDay(), dayNamesShort, dayNames);
+							break;
+						case 'o':
+							output += formatNumber('o',
+								Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3);
+							break;
+						case 'm':
+							output += formatNumber('m', date.getMonth() + 1, 2);
+							break;
+						case 'M':
+							output += formatName('M', date.getMonth(), monthNamesShort, monthNames);
+							break;
+						case 'y':
+							output += (lookAhead('y') ? date.getFullYear() :
+								(date.getYear() % 100 < 10 ? '0' : '') + date.getYear() % 100);
+							break;
+						case '@':
+							output += date.getTime();
+							break;
+						case '!':
+							output += date.getTime() * 10000 + this._ticksTo1970;
+							break;
+						case "'":
+							if (lookAhead("'"))
+								output += "'";
+							else
+								literal = true;
+							break;
+						default:
+							output += format.charAt(iFormat);
+					}
+			}
+		return output;
+	},
+
+	/* Extract all possible characters from the date format. */
+	_possibleChars: function (format) {
+		var chars = '';
+		var literal = false;
+		// Check whether a format character is doubled
+		var lookAhead = function(match) {
+			var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) == match);
+			if (matches)
+				iFormat++;
+			return matches;
+		};
+		for (var iFormat = 0; iFormat < format.length; iFormat++)
+			if (literal)
+				if (format.charAt(iFormat) == "'" && !lookAhead("'"))
+					literal = false;
+				else
+					chars += format.charAt(iFormat);
+			else
+				switch (format.charAt(iFormat)) {
+					case 'd': case 'm': case 'y': case '@':
+						chars += '0123456789';
+						break;
+					case 'D': case 'M':
+						return null; // Accept anything
+					case "'":
+						if (lookAhead("'"))
+							chars += "'";
+						else
+							literal = true;
+						break;
+					default:
+						chars += format.charAt(iFormat);
+				}
+		return chars;
+	},
+
+	/* Get a setting value, defaulting if necessary. */
+	_get: function(inst, name) {
+		return inst.settings[name] !== undefined ?
+			inst.settings[name] : this._defaults[name];
+	},
+
+	/* Parse existing date and initialise date picker. */
+	_setDateFromField: function(inst, noDefault) {
+		if (inst.input.val() == inst.lastVal) {
+			return;
+		}
+		var dateFormat = this._get(inst, 'dateFormat');
+		var dates = inst.lastVal = inst.input ? inst.input.val() : null;
+		var date, defaultDate;
+		date = defaultDate = this._getDefaultDate(inst);
+		var settings = this._getFormatConfig(inst);
+		try {
+			date = this.parseDate(dateFormat, dates, settings) || defaultDate;
+		} catch (event) {
+			this.log(event);
+			dates = (noDefault ? '' : dates);
+		}
+		inst.selectedDay = date.getDate();
+		inst.drawMonth = inst.selectedMonth = date.getMonth();
+		inst.drawYear = inst.selectedYear = date.getFullYear();
+		inst.currentDay = (dates ? date.getDate() : 0);
+		inst.currentMonth = (dates ? date.getMonth() : 0);
+		inst.currentYear = (dates ? date.getFullYear() : 0);
+		this._adjustInstDate(inst);
+	},
+
+	/* Retrieve the default date shown on opening. */
+	_getDefaultDate: function(inst) {
+		return this._restrictMinMax(inst,
+			this._determineDate(inst, this._get(inst, 'defaultDate'), new Date()));
+	},
+
+	/* A date may be specified as an exact value or a relative one. */
+	_determineDate: function(inst, date, defaultDate) {
+		var offsetNumeric = function(offset) {
+			var date = new Date();
+			date.setDate(date.getDate() + offset);
+			return date;
+		};
+		var offsetString = function(offset) {
+			try {
+				return $.datepicker.parseDate($.datepicker._get(inst, 'dateFormat'),
+					offset, $.datepicker._getFormatConfig(inst));
+			}
+			catch (e) {
+				// Ignore
+			}
+			var date = (offset.toLowerCase().match(/^c/) ?
+				$.datepicker._getDate(inst) : null) || new Date();
+			var year = date.getFullYear();
+			var month = date.getMonth();
+			var day = date.getDate();
+			var pattern = /([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g;
+			var matches = pattern.exec(offset);
+			while (matches) {
+				switch (matches[2] || 'd') {
+					case 'd' : case 'D' :
+						day += parseInt(matches[1],10); break;
+					case 'w' : case 'W' :
+						day += parseInt(matches[1],10) * 7; break;
+					case 'm' : case 'M' :
+						month += parseInt(matches[1],10);
+						day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
+						break;
+					case 'y': case 'Y' :
+						year += parseInt(matches[1],10);
+						day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
+						break;
+				}
+				matches = pattern.exec(offset);
+			}
+			return new Date(year, month, day);
+		};
+		var newDate = (date == null || date === '' ? defaultDate : (typeof date == 'string' ? offsetString(date) :
+			(typeof date == 'number' ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime()))));
+		newDate = (newDate && newDate.toString() == 'Invalid Date' ? defaultDate : newDate);
+		if (newDate) {
+			newDate.setHours(0);
+			newDate.setMinutes(0);
+			newDate.setSeconds(0);
+			newDate.setMilliseconds(0);
+		}
+		return this._daylightSavingAdjust(newDate);
+	},
+
+	/* Handle switch to/from daylight saving.
+	   Hours may be non-zero on daylight saving cut-over:
+	   > 12 when midnight changeover, but then cannot generate
+	   midnight datetime, so jump to 1AM, otherwise reset.
+	   @param  date  (Date) the date to check
+	   @return  (Date) the corrected date */
+	_daylightSavingAdjust: function(date) {
+		if (!date) return null;
+		date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0);
+		return date;
+	},
+
+	/* Set the date(s) directly. */
+	_setDate: function(inst, date, noChange) {
+		var clear = !date;
+		var origMonth = inst.selectedMonth;
+		var origYear = inst.selectedYear;
+		var newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date()));
+		inst.selectedDay = inst.currentDay = newDate.getDate();
+		inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth();
+		inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear();
+		if ((origMonth != inst.selectedMonth || origYear != inst.selectedYear) && !noChange)
+			this._notifyChange(inst);
+		this._adjustInstDate(inst);
+		if (inst.input) {
+			inst.input.val(clear ? '' : this._formatDate(inst));
+		}
+	},
+
+	/* Retrieve the date(s) directly. */
+	_getDate: function(inst) {
+		var startDate = (!inst.currentYear || (inst.input && inst.input.val() == '') ? null :
+			this._daylightSavingAdjust(new Date(
+			inst.currentYear, inst.currentMonth, inst.currentDay)));
+			return startDate;
+	},
+
+	/* Generate the HTML for the current state of the date picker. */
+	_generateHTML: function(inst) {
+		var today = new Date();
+		today = this._daylightSavingAdjust(
+			new Date(today.getFullYear(), today.getMonth(), today.getDate())); // clear time
+		var isRTL = this._get(inst, 'isRTL');
+		var showButtonPanel = this._get(inst, 'showButtonPanel');
+		var hideIfNoPrevNext = this._get(inst, 'hideIfNoPrevNext');
+		var navigationAsDateFormat = this._get(inst, 'navigationAsDateFormat');
+		var numMonths = this._getNumberOfMonths(inst);
+		var showCurrentAtPos = this._get(inst, 'showCurrentAtPos');
+		var stepMonths = this._get(inst, 'stepMonths');
+		var isMultiMonth = (numMonths[0] != 1 || numMonths[1] != 1);
+		var currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) :
+			new Date(inst.currentYear, inst.currentMonth, inst.currentDay)));
+		var minDate = this._getMinMaxDate(inst, 'min');
+		var maxDate = this._getMinMaxDate(inst, 'max');
+		var drawMonth = inst.drawMonth - showCurrentAtPos;
+		var drawYear = inst.drawYear;
+		if (drawMonth < 0) {
+			drawMonth += 12;
+			drawYear--;
+		}
+		if (maxDate) {
+			var maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(),
+				maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate()));
+			maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw);
+			while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) {
+				drawMonth--;
+				if (drawMonth < 0) {
+					drawMonth = 11;
+					drawYear--;
+				}
+			}
+		}
+		inst.drawMonth = drawMonth;
+		inst.drawYear = drawYear;
+		var prevText = this._get(inst, 'prevText');
+		prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText,
+			this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)),
+			this._getFormatConfig(inst)));
+		var prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ?
+			'<a class="ui-datepicker-prev ui-corner-all" onclick="DP_jQuery_' + dpuuid +
+			'.datepicker._adjustDate(\'#' + inst.id + '\', -' + stepMonths + ', \'M\');"' +
+			' title="' + prevText + '"><span class="ui-icon ui-icon-circle-triangle-' + ( isRTL ? 'e' : 'w') + '">' + prevText + '</span></a>' :
+			(hideIfNoPrevNext ? '' : '<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+ prevText +'"><span class="ui-icon ui-icon-circle-triangle-' + ( isRTL ? 'e' : 'w') + '">' + prevText + '</span></a>'));
+		var nextText = this._get(inst, 'nextText');
+		nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText,
+			this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)),
+			this._getFormatConfig(inst)));
+		var next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ?
+			'<a class="ui-datepicker-next ui-corner-all" onclick="DP_jQuery_' + dpuuid +
+			'.datepicker._adjustDate(\'#' + inst.id + '\', +' + stepMonths + ', \'M\');"' +
+			' title="' + nextText + '"><span class="ui-icon ui-icon-circle-triangle-' + ( isRTL ? 'w' : 'e') + '">' + nextText + '</span></a>' :
+			(hideIfNoPrevNext ? '' : '<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+ nextText + '"><span class="ui-icon ui-icon-circle-triangle-' + ( isRTL ? 'w' : 'e') + '">' + nextText + '</span></a>'));
+		var currentText = this._get(inst, 'currentText');
+		var gotoDate = (this._get(inst, 'gotoCurrent') && inst.currentDay ? currentDate : today);
+		currentText = (!navigationAsDateFormat ? currentText :
+			this.formatDate(currentText, gotoDate, this._getFormatConfig(inst)));
+		var controls = (!inst.inline ? '<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="DP_jQuery_' + dpuuid +
+			'.datepicker._hideDatepicker();">' + this._get(inst, 'closeText') + '</button>' : '');
+		var buttonPanel = (showButtonPanel) ? '<div class="ui-datepicker-buttonpane ui-widget-content">' + (isRTL ? controls : '') +
+			(this._isInRange(inst, gotoDate) ? '<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" onclick="DP_jQuery_' + dpuuid +
+			'.datepicker._gotoToday(\'#' + inst.id + '\');"' +
+			'>' + currentText + '</button>' : '') + (isRTL ? '' : controls) + '</div>' : '';
+		var firstDay = parseInt(this._get(inst, 'firstDay'),10);
+		firstDay = (isNaN(firstDay) ? 0 : firstDay);
+		var showWeek = this._get(inst, 'showWeek');
+		var dayNames = this._get(inst, 'dayNames');
+		var dayNamesShort = this._get(inst, 'dayNamesShort');
+		var dayNamesMin = this._get(inst, 'dayNamesMin');
+		var monthNames = this._get(inst, 'monthNames');
+		var monthNamesShort = this._get(inst, 'monthNamesShort');
+		var beforeShowDay = this._get(inst, 'beforeShowDay');
+		var showOtherMonths = this._get(inst, 'showOtherMonths');
+		var selectOtherMonths = this._get(inst, 'selectOtherMonths');
+		var calculateWeek = this._get(inst, 'calculateWeek') || this.iso8601Week;
+		var defaultDate = this._getDefaultDate(inst);
+		var html = '';
+		for (var row = 0; row < numMonths[0]; row++) {
+			var group = '';
+			this.maxRows = 4;
+			for (var col = 0; col < numMonths[1]; col++) {
+				var selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay));
+				var cornerClass = ' ui-corner-all';
+				var calender = '';
+				if (isMultiMonth) {
+					calender += '<div class="ui-datepicker-group';
+					if (numMonths[1] > 1)
+						switch (col) {
+							case 0: calender += ' ui-datepicker-group-first';
+								cornerClass = ' ui-corner-' + (isRTL ? 'right' : 'left'); break;
+							case numMonths[1]-1: calender += ' ui-datepicker-group-last';
+								cornerClass = ' ui-corner-' + (isRTL ? 'left' : 'right'); break;
+							default: calender += ' ui-datepicker-group-middle'; cornerClass = ''; break;
+						}
+					calender += '">';
+				}
+				calender += '<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix' + cornerClass + '">' +
+					(/all|left/.test(cornerClass) && row == 0 ? (isRTL ? next : prev) : '') +
+					(/all|right/.test(cornerClass) && row == 0 ? (isRTL ? prev : next) : '') +
+					this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate,
+					row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers
+					'</div><table class="ui-datepicker-calendar"><thead>' +
+					'<tr>';
+				var thead = (showWeek ? '<th class="ui-datepicker-week-col">' + this._get(inst, 'weekHeader') + '</th>' : '');
+				for (var dow = 0; dow < 7; dow++) { // days of the week
+					var day = (dow + firstDay) % 7;
+					thead += '<th' + ((dow + firstDay + 6) % 7 >= 5 ? ' class="ui-datepicker-week-end"' : '') + '>' +
+						'<span title="' + dayNames[day] + '">' + dayNamesMin[day] + '</span></th>';
+				}
+				calender += thead + '</tr></thead><tbody>';
+				var daysInMonth = this._getDaysInMonth(drawYear, drawMonth);
+				if (drawYear == inst.selectedYear && drawMonth == inst.selectedMonth)
+					inst.selectedDay = Math.min(inst.selectedDay, daysInMonth);
+				var leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7;
+				var curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate
+				var numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043)
+				this.maxRows = numRows;
+				var printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays));
+				for (var dRow = 0; dRow < numRows; dRow++) { // create date picker rows
+					calender += '<tr>';
+					var tbody = (!showWeek ? '' : '<td class="ui-datepicker-week-col">' +
+						this._get(inst, 'calculateWeek')(printDate) + '</td>');
+					for (var dow = 0; dow < 7; dow++) { // create date picker days
+						var daySettings = (beforeShowDay ?
+							beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, '']);
+						var otherMonth = (printDate.getMonth() != drawMonth);
+						var unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] ||
+							(minDate && printDate < minDate) || (maxDate && printDate > maxDate);
+						tbody += '<td class="' +
+							((dow + firstDay + 6) % 7 >= 5 ? ' ui-datepicker-week-end' : '') + // highlight weekends
+							(otherMonth ? ' ui-datepicker-other-month' : '') + // highlight days from other months
+							((printDate.getTime() == selectedDate.getTime() && drawMonth == inst.selectedMonth && inst._keyEvent) || // user pressed key
+							(defaultDate.getTime() == printDate.getTime() && defaultDate.getTime() == selectedDate.getTime()) ?
+							// or defaultDate is current printedDate and defaultDate is selectedDate
+							' ' + this._dayOverClass : '') + // highlight selected day
+							(unselectable ? ' ' + this._unselectableClass + ' ui-state-disabled': '') +  // highlight unselectable days
+							(otherMonth && !showOtherMonths ? '' : ' ' + daySettings[1] + // highlight custom dates
+							(printDate.getTime() == currentDate.getTime() ? ' ' + this._currentClass : '') + // highlight selected day
+							(printDate.getTime() == today.getTime() ? ' ui-datepicker-today' : '')) + '"' + // highlight today (if different)
+							((!otherMonth || showOtherMonths) && daySettings[2] ? ' title="' + daySettings[2] + '"' : '') + // cell title
+							(unselectable ? '' : ' onclick="DP_jQuery_' + dpuuid + '.datepicker._selectDay(\'#' +
+							inst.id + '\',' + printDate.getMonth() + ',' + printDate.getFullYear() + ', this);return false;"') + '>' + // actions
+							(otherMonth && !showOtherMonths ? '&#xa0;' : // display for other months
+							(unselectable ? '<span class="ui-state-default">' + printDate.getDate() + '</span>' : '<a class="ui-state-default' +
+							(printDate.getTime() == today.getTime() ? ' ui-state-highlight' : '') +
+							(printDate.getTime() == currentDate.getTime() ? ' ui-state-active' : '') + // highlight selected day
+							(otherMonth ? ' ui-priority-secondary' : '') + // distinguish dates from other months
+							'" href="#">' + printDate.getDate() + '</a>')) + '</td>'; // display selectable date
+						printDate.setDate(printDate.getDate() + 1);
+						printDate = this._daylightSavingAdjust(printDate);
+					}
+					calender += tbody + '</tr>';
+				}
+				drawMonth++;
+				if (drawMonth > 11) {
+					drawMonth = 0;
+					drawYear++;
+				}
+				calender += '</tbody></table>' + (isMultiMonth ? '</div>' + 
+							((numMonths[0] > 0 && col == numMonths[1]-1) ? '<div class="ui-datepicker-row-break"></div>' : '') : '');
+				group += calender;
+			}
+			html += group;
+		}
+		html += buttonPanel + ($.browser.msie && parseInt($.browser.version,10) < 7 && !inst.inline ?
+			'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>' : '');
+		inst._keyEvent = false;
+		return html;
+	},
+
+	/* Generate the month and year header. */
+	_generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate,
+			secondary, monthNames, monthNamesShort) {
+		var changeMonth = this._get(inst, 'changeMonth');
+		var changeYear = this._get(inst, 'changeYear');
+		var showMonthAfterYear = this._get(inst, 'showMonthAfterYear');
+		var html = '<div class="ui-datepicker-title">';
+		var monthHtml = '';
+		// month selection
+		if (secondary || !changeMonth)
+			monthHtml += '<span class="ui-datepicker-month">' + monthNames[drawMonth] + '</span>';
+		else {
+			var inMinYear = (minDate && minDate.getFullYear() == drawYear);
+			var inMaxYear = (maxDate && maxDate.getFullYear() == drawYear);
+			monthHtml += '<select class="ui-datepicker-month" ' +
+				'onchange="DP_jQuery_' + dpuuid + '.datepicker._selectMonthYear(\'#' + inst.id + '\', this, \'M\');" ' +
+			 	'>';
+			for (var month = 0; month < 12; month++) {
+				if ((!inMinYear || month >= minDate.getMonth()) &&
+						(!inMaxYear || month <= maxDate.getMonth()))
+					monthHtml += '<option value="' + month + '"' +
+						(month == drawMonth ? ' selected="selected"' : '') +
+						'>' + monthNamesShort[month] + '</option>';
+			}
+			monthHtml += '</select>';
+		}
+		if (!showMonthAfterYear)
+			html += monthHtml + (secondary || !(changeMonth && changeYear) ? '&#xa0;' : '');
+		// year selection
+		if ( !inst.yearshtml ) {
+			inst.yearshtml = '';
+			if (secondary || !changeYear)
+				html += '<span class="ui-datepicker-year">' + drawYear + '</span>';
+			else {
+				// determine range of years to display
+				var years = this._get(inst, 'yearRange').split(':');
+				var thisYear = new Date().getFullYear();
+				var determineYear = function(value) {
+					var year = (value.match(/c[+-].*/) ? drawYear + parseInt(value.substring(1), 10) :
+						(value.match(/[+-].*/) ? thisYear + parseInt(value, 10) :
+						parseInt(value, 10)));
+					return (isNaN(year) ? thisYear : year);
+				};
+				var year = determineYear(years[0]);
+				var endYear = Math.max(year, determineYear(years[1] || ''));
+				year = (minDate ? Math.max(year, minDate.getFullYear()) : year);
+				endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear);
+				inst.yearshtml += '<select class="ui-datepicker-year" ' +
+					'onchange="DP_jQuery_' + dpuuid + '.datepicker._selectMonthYear(\'#' + inst.id + '\', this, \'Y\');" ' +
+					'>';
+				for (; year <= endYear; year++) {
+					inst.yearshtml += '<option value="' + year + '"' +
+						(year == drawYear ? ' selected="selected"' : '') +
+						'>' + year + '</option>';
+				}
+				inst.yearshtml += '</select>';
+				
+				html += inst.yearshtml;
+				inst.yearshtml = null;
+			}
+		}
+		html += this._get(inst, 'yearSuffix');
+		if (showMonthAfterYear)
+			html += (secondary || !(changeMonth && changeYear) ? '&#xa0;' : '') + monthHtml;
+		html += '</div>'; // Close datepicker_header
+		return html;
+	},
+
+	/* Adjust one of the date sub-fields. */
+	_adjustInstDate: function(inst, offset, period) {
+		var year = inst.drawYear + (period == 'Y' ? offset : 0);
+		var month = inst.drawMonth + (period == 'M' ? offset : 0);
+		var day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) +
+			(period == 'D' ? offset : 0);
+		var date = this._restrictMinMax(inst,
+			this._daylightSavingAdjust(new Date(year, month, day)));
+		inst.selectedDay = date.getDate();
+		inst.drawMonth = inst.selectedMonth = date.getMonth();
+		inst.drawYear = inst.selectedYear = date.getFullYear();
+		if (period == 'M' || period == 'Y')
+			this._notifyChange(inst);
+	},
+
+	/* Ensure a date is within any min/max bounds. */
+	_restrictMinMax: function(inst, date) {
+		var minDate = this._getMinMaxDate(inst, 'min');
+		var maxDate = this._getMinMaxDate(inst, 'max');
+		var newDate = (minDate && date < minDate ? minDate : date);
+		newDate = (maxDate && newDate > maxDate ? maxDate : newDate);
+		return newDate;
+	},
+
+	/* Notify change of month/year. */
+	_notifyChange: function(inst) {
+		var onChange = this._get(inst, 'onChangeMonthYear');
+		if (onChange)
+			onChange.apply((inst.input ? inst.input[0] : null),
+				[inst.selectedYear, inst.selectedMonth + 1, inst]);
+	},
+
+	/* Determine the number of months to show. */
+	_getNumberOfMonths: function(inst) {
+		var numMonths = this._get(inst, 'numberOfMonths');
+		return (numMonths == null ? [1, 1] : (typeof numMonths == 'number' ? [1, numMonths] : numMonths));
+	},
+
+	/* Determine the current maximum date - ensure no time components are set. */
+	_getMinMaxDate: function(inst, minMax) {
+		return this._determineDate(inst, this._get(inst, minMax + 'Date'), null);
+	},
+
+	/* Find the number of days in a given month. */
+	_getDaysInMonth: function(year, month) {
+		return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate();
+	},
+
+	/* Find the day of the week of the first of a month. */
+	_getFirstDayOfMonth: function(year, month) {
+		return new Date(year, month, 1).getDay();
+	},
+
+	/* Determines if we should allow a "next/prev" month display change. */
+	_canAdjustMonth: function(inst, offset, curYear, curMonth) {
+		var numMonths = this._getNumberOfMonths(inst);
+		var date = this._daylightSavingAdjust(new Date(curYear,
+			curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1));
+		if (offset < 0)
+			date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth()));
+		return this._isInRange(inst, date);
+	},
+
+	/* Is the given date in the accepted range? */
+	_isInRange: function(inst, date) {
+		var minDate = this._getMinMaxDate(inst, 'min');
+		var maxDate = this._getMinMaxDate(inst, 'max');
+		return ((!minDate || date.getTime() >= minDate.getTime()) &&
+			(!maxDate || date.getTime() <= maxDate.getTime()));
+	},
+
+	/* Provide the configuration settings for formatting/parsing. */
+	_getFormatConfig: function(inst) {
+		var shortYearCutoff = this._get(inst, 'shortYearCutoff');
+		shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff :
+			new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10));
+		return {shortYearCutoff: shortYearCutoff,
+			dayNamesShort: this._get(inst, 'dayNamesShort'), dayNames: this._get(inst, 'dayNames'),
+			monthNamesShort: this._get(inst, 'monthNamesShort'), monthNames: this._get(inst, 'monthNames')};
+	},
+
+	/* Format the given date for display. */
+	_formatDate: function(inst, day, month, year) {
+		if (!day) {
+			inst.currentDay = inst.selectedDay;
+			inst.currentMonth = inst.selectedMonth;
+			inst.currentYear = inst.selectedYear;
+		}
+		var date = (day ? (typeof day == 'object' ? day :
+			this._daylightSavingAdjust(new Date(year, month, day))) :
+			this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay)));
+		return this.formatDate(this._get(inst, 'dateFormat'), date, this._getFormatConfig(inst));
+	}
+});
+
+/*
+ * Bind hover events for datepicker elements.
+ * Done via delegate so the binding only occurs once in the lifetime of the parent div.
+ * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker.
+ */ 
+function bindHover(dpDiv) {
+	var selector = 'button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a';
+	return dpDiv.bind('mouseout', function(event) {
+			var elem = $( event.target ).closest( selector );
+			if ( !elem.length ) {
+				return;
+			}
+			elem.removeClass( "ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover" );
+		})
+		.bind('mouseover', function(event) {
+			var elem = $( event.target ).closest( selector );
+			if ($.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0]) ||
+					!elem.length ) {
+				return;
+			}
+			elem.parents('.ui-datepicker-calendar').find('a').removeClass('ui-state-hover');
+			elem.addClass('ui-state-hover');
+			if (elem.hasClass('ui-datepicker-prev')) elem.addClass('ui-datepicker-prev-hover');
+			if (elem.hasClass('ui-datepicker-next')) elem.addClass('ui-datepicker-next-hover');
+		});
+}
+
+/* jQuery extend now ignores nulls! */
+function extendRemove(target, props) {
+	$.extend(target, props);
+	for (var name in props)
+		if (props[name] == null || props[name] == undefined)
+			target[name] = props[name];
+	return target;
+};
+
+/* Determine whether an object is an array. */
+function isArray(a) {
+	return (a && (($.browser.safari && typeof a == 'object' && a.length) ||
+		(a.constructor && a.constructor.toString().match(/\Array\(\)/))));
+};
+
+/* Invoke the datepicker functionality.
+   @param  options  string - a command, optionally followed by additional parameters or
+                    Object - settings for attaching new datepicker functionality
+   @return  jQuery object */
+$.fn.datepicker = function(options){
+	
+	/* Verify an empty collection wasn't passed - Fixes #6976 */
+	if ( !this.length ) {
+		return this;
+	}
+	
+	/* Initialise the date picker. */
+	if (!$.datepicker.initialized) {
+		$(document).mousedown($.datepicker._checkExternalClick).
+			find('body').append($.datepicker.dpDiv);
+		$.datepicker.initialized = true;
+	}
+
+	var otherArgs = Array.prototype.slice.call(arguments, 1);
+	if (typeof options == 'string' && (options == 'isDisabled' || options == 'getDate' || options == 'widget'))
+		return $.datepicker['_' + options + 'Datepicker'].
+			apply($.datepicker, [this[0]].concat(otherArgs));
+	if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string')
+		return $.datepicker['_' + options + 'Datepicker'].
+			apply($.datepicker, [this[0]].concat(otherArgs));
+	return this.each(function() {
+		typeof options == 'string' ?
+			$.datepicker['_' + options + 'Datepicker'].
+				apply($.datepicker, [this].concat(otherArgs)) :
+			$.datepicker._attachDatepicker(this, options);
+	});
+};
+
+$.datepicker = new Datepicker(); // singleton instance
+$.datepicker.initialized = false;
+$.datepicker.uuid = new Date().getTime();
+$.datepicker.version = "@VERSION";
+
+// Workaround for #4055
+// Add another global to avoid noConflict issues with inline event handlers
+window['DP_jQuery_' + dpuuid] = $;
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/dialog.js b/client/apollo/jslib/jqueryui/dialog.js
new file mode 100644
index 0000000..b11e4fc
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/dialog.js
@@ -0,0 +1,881 @@
+define(['jquery','./core','./widget','./button','./draggable','./mouse','./position','./resizable'], function (jQuery) {
+/*
+ * jQuery UI Dialog @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *  jquery.ui.button.js
+ *	jquery.ui.draggable.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.position.js
+ *	jquery.ui.resizable.js
+ */
+(function( $, undefined ) {
+
+var uiDialogClasses =
+		'ui-dialog ' +
+		'ui-widget ' +
+		'ui-widget-content ' +
+		'ui-corner-all ',
+	sizeRelatedOptions = {
+		buttons: true,
+		height: true,
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true,
+		width: true
+	},
+	resizableRelatedOptions = {
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true
+	},
+	// support for jQuery 1.3.2 - handle common attrFn methods for dialog
+	attrFn = $.attrFn || {
+		val: true,
+		css: true,
+		html: true,
+		text: true,
+		data: true,
+		width: true,
+		height: true,
+		offset: true,
+		click: true
+	};
+
+$.widget("ui.dialog", {
+	options: {
+		autoOpen: true,
+		buttons: {},
+		closeOnEscape: true,
+		closeText: 'close',
+		dialogClass: '',
+		draggable: true,
+		hide: null,
+		height: 'auto',
+		maxHeight: false,
+		maxWidth: false,
+		minHeight: 150,
+		minWidth: 150,
+		modal: false,
+		position: {
+			my: 'center',
+			at: 'center',
+			collision: 'fit',
+			// ensure that the titlebar is never outside the document
+			using: function(pos) {
+				var topOffset = $(this).css(pos).offset().top;
+				if (topOffset < 0) {
+					$(this).css('top', pos.top - topOffset);
+				}
+			}
+		},
+		resizable: true,
+		show: null,
+		stack: true,
+		title: '',
+		width: 300,
+		zIndex: 1000
+	},
+
+	_create: function() {
+		this.originalTitle = this.element.attr('title');
+		// #5742 - .attr() might return a DOMElement
+		if ( typeof this.originalTitle !== "string" ) {
+			this.originalTitle = "";
+		}
+
+		this.options.title = this.options.title || this.originalTitle;
+		var self = this,
+			options = self.options,
+
+			title = options.title || ' ',
+			titleId = $.ui.dialog.getTitleId(self.element),
+
+			uiDialog = (self.uiDialog = $('<div></div>'))
+				.appendTo(document.body)
+				.hide()
+				.addClass(uiDialogClasses + options.dialogClass)
+				.css({
+					zIndex: options.zIndex
+				})
+				// setting tabIndex makes the div focusable
+				// setting outline to 0 prevents a border on focus in Mozilla
+				.attr('tabIndex', -1).css('outline', 0).keydown(function(event) {
+					if (options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+						event.keyCode === $.ui.keyCode.ESCAPE) {
+						
+						self.close(event);
+						event.preventDefault();
+					}
+				})
+				.attr({
+					role: 'dialog',
+					'aria-labelledby': titleId
+				})
+				.mousedown(function(event) {
+					self.moveToTop(false, event);
+				}),
+
+			uiDialogContent = self.element
+				.show()
+				.removeAttr('title')
+				.addClass(
+					'ui-dialog-content ' +
+					'ui-widget-content')
+				.appendTo(uiDialog),
+
+			uiDialogTitlebar = (self.uiDialogTitlebar = $('<div></div>'))
+				.addClass(
+					'ui-dialog-titlebar ' +
+					'ui-widget-header ' +
+					'ui-corner-all ' +
+					'ui-helper-clearfix'
+				)
+				.prependTo(uiDialog),
+
+			uiDialogTitlebarClose = $('<a href="#"></a>')
+				.addClass(
+					'ui-dialog-titlebar-close ' +
+					'ui-corner-all'
+				)
+				.attr('role', 'button')
+				.hover(
+					function() {
+						uiDialogTitlebarClose.addClass('ui-state-hover');
+					},
+					function() {
+						uiDialogTitlebarClose.removeClass('ui-state-hover');
+					}
+				)
+				.focus(function() {
+					uiDialogTitlebarClose.addClass('ui-state-focus');
+				})
+				.blur(function() {
+					uiDialogTitlebarClose.removeClass('ui-state-focus');
+				})
+				.click(function(event) {
+					self.close(event);
+					return false;
+				})
+				.appendTo(uiDialogTitlebar),
+
+			uiDialogTitlebarCloseText = (self.uiDialogTitlebarCloseText = $('<span></span>'))
+				.addClass(
+					'ui-icon ' +
+					'ui-icon-closethick'
+				)
+				.text(options.closeText)
+				.appendTo(uiDialogTitlebarClose),
+
+			uiDialogTitle = $('<span></span>')
+				.addClass('ui-dialog-title')
+				.attr('id', titleId)
+				.html(title)
+				.prependTo(uiDialogTitlebar);
+
+		//handling of deprecated beforeclose (vs beforeClose) option
+		//Ticket #4669 http://dev.jqueryui.com/ticket/4669
+		//TODO: remove in 1.9pre
+		if ($.isFunction(options.beforeclose) && !$.isFunction(options.beforeClose)) {
+			options.beforeClose = options.beforeclose;
+		}
+
+		uiDialogTitlebar.find("*").add(uiDialogTitlebar).disableSelection();
+
+		if (options.draggable && $.fn.draggable) {
+			self._makeDraggable();
+		}
+		if (options.resizable && $.fn.resizable) {
+			self._makeResizable();
+		}
+
+		self._createButtons(options.buttons);
+		self._isOpen = false;
+
+		if ($.fn.bgiframe) {
+			uiDialog.bgiframe();
+		}
+	},
+
+	_init: function() {
+		if ( this.options.autoOpen ) {
+			this.open();
+		}
+	},
+
+	destroy: function() {
+		var self = this;
+		
+		if (self.overlay) {
+			self.overlay.destroy();
+		}
+		self.uiDialog.hide();
+		self.element
+			.unbind('.dialog')
+			.removeData('dialog')
+			.removeClass('ui-dialog-content ui-widget-content')
+			.hide().appendTo('body');
+		self.uiDialog.remove();
+
+		if (self.originalTitle) {
+			self.element.attr('title', self.originalTitle);
+		}
+
+		return self;
+	},
+
+	widget: function() {
+		return this.uiDialog;
+	},
+
+	close: function(event) {
+		var self = this,
+			maxZ, thisZ;
+		
+		if (false === self._trigger('beforeClose', event)) {
+			return;
+		}
+
+		if (self.overlay) {
+			self.overlay.destroy();
+		}
+		self.uiDialog.unbind('keypress.ui-dialog');
+
+		self._isOpen = false;
+
+		if (self.options.hide) {
+			self.uiDialog.hide(self.options.hide, function() {
+				self._trigger('close', event);
+			});
+		} else {
+			self.uiDialog.hide();
+			self._trigger('close', event);
+		}
+
+		$.ui.dialog.overlay.resize();
+
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		if (self.options.modal) {
+			maxZ = 0;
+			$('.ui-dialog').each(function() {
+				if (this !== self.uiDialog[0]) {
+					thisZ = $(this).css('z-index');
+					if(!isNaN(thisZ)) {
+						maxZ = Math.max(maxZ, thisZ);
+					}
+				}
+			});
+			$.ui.dialog.maxZ = maxZ;
+		}
+
+		return self;
+	},
+
+	isOpen: function() {
+		return this._isOpen;
+	},
+
+	// the force parameter allows us to move modal dialogs to their correct
+	// position on open
+	moveToTop: function(force, event) {
+		var self = this,
+			options = self.options,
+			saveScroll;
+
+		if ((options.modal && !force) ||
+			(!options.stack && !options.modal)) {
+			return self._trigger('focus', event);
+		}
+
+		if (options.zIndex > $.ui.dialog.maxZ) {
+			$.ui.dialog.maxZ = options.zIndex;
+		}
+		if (self.overlay) {
+			$.ui.dialog.maxZ += 1;
+			self.overlay.$el.css('z-index', $.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ);
+		}
+
+		//Save and then restore scroll since Opera 9.5+ resets when parent z-Index is changed.
+		//  http://ui.jquery.com/bugs/ticket/3193
+		saveScroll = { scrollTop: self.element.scrollTop(), scrollLeft: self.element.scrollLeft() };
+		$.ui.dialog.maxZ += 1;
+		self.uiDialog.css('z-index', $.ui.dialog.maxZ);
+		self.element.attr(saveScroll);
+		self._trigger('focus', event);
+
+		return self;
+	},
+
+	open: function() {
+		if (this._isOpen) { return; }
+
+		var self = this,
+			options = self.options,
+			uiDialog = self.uiDialog;
+
+		self.overlay = options.modal ? new $.ui.dialog.overlay(self) : null;
+		self._size();
+		self._position(options.position);
+		uiDialog.show(options.show);
+		self.moveToTop(true);
+
+		// prevent tabbing out of modal dialogs
+		if ( options.modal ) {
+			uiDialog.bind( "keydown.ui-dialog", function( event ) {
+				if ( event.keyCode !== $.ui.keyCode.TAB ) {
+					return;
+				}
+
+				var tabbables = $(':tabbable', this),
+					first = tabbables.filter(':first'),
+					last  = tabbables.filter(':last');
+
+				if (event.target === last[0] && !event.shiftKey) {
+					first.focus(1);
+					return false;
+				} else if (event.target === first[0] && event.shiftKey) {
+					last.focus(1);
+					return false;
+				}
+			});
+		}
+
+		// set focus to the first tabbable element in the content area or the first button
+		// if there are no tabbable elements, set focus on the dialog itself
+		$(self.element.find(':tabbable').get().concat(
+			uiDialog.find('.ui-dialog-buttonpane :tabbable').get().concat(
+				uiDialog.get()))).eq(0).focus();
+
+		self._isOpen = true;
+		self._trigger('open');
+
+		return self;
+	},
+
+	_createButtons: function(buttons) {
+		var self = this,
+			hasButtons = false,
+			uiDialogButtonPane = $('<div></div>')
+				.addClass(
+					'ui-dialog-buttonpane ' +
+					'ui-widget-content ' +
+					'ui-helper-clearfix'
+				),
+			uiButtonSet = $( "<div></div>" )
+				.addClass( "ui-dialog-buttonset" )
+				.appendTo( uiDialogButtonPane );
+
+		// if we already have a button pane, remove it
+		self.uiDialog.find('.ui-dialog-buttonpane').remove();
+
+		if (typeof buttons === 'object' && buttons !== null) {
+			$.each(buttons, function() {
+				return !(hasButtons = true);
+			});
+		}
+		if (hasButtons) {
+			$.each(buttons, function(name, props) {
+				props = $.isFunction( props ) ?
+					{ click: props, text: name } :
+					props;
+				var button = $('<button type="button"></button>')
+					.click(function() {
+						props.click.apply(self.element[0], arguments);
+					})
+					.appendTo(uiButtonSet);
+				// can't use .attr( props, true ) with jQuery 1.3.2.
+				$.each( props, function( key, value ) {
+					if ( key === "click" ) {
+						return;
+					}
+					if ( key in attrFn ) {
+						button[ key ]( value );
+					} else {
+						button.attr( key, value );
+					}
+				});
+				if ($.fn.button) {
+					button.button();
+				}
+			});
+			uiDialogButtonPane.appendTo(self.uiDialog);
+		}
+	},
+
+	_makeDraggable: function() {
+		var self = this,
+			options = self.options,
+			doc = $(document),
+			heightBeforeDrag;
+
+		function filteredUi(ui) {
+			return {
+				position: ui.position,
+				offset: ui.offset
+			};
+		}
+
+		self.uiDialog.draggable({
+			cancel: '.ui-dialog-content, .ui-dialog-titlebar-close',
+			handle: '.ui-dialog-titlebar',
+			containment: 'document',
+			start: function(event, ui) {
+				heightBeforeDrag = options.height === "auto" ? "auto" : $(this).height();
+				$(this).height($(this).height()).addClass("ui-dialog-dragging");
+				self._trigger('dragStart', event, filteredUi(ui));
+			},
+			drag: function(event, ui) {
+				self._trigger('drag', event, filteredUi(ui));
+			},
+			stop: function(event, ui) {
+				options.position = [ui.position.left - doc.scrollLeft(),
+					ui.position.top - doc.scrollTop()];
+				$(this).removeClass("ui-dialog-dragging").height(heightBeforeDrag);
+				self._trigger('dragStop', event, filteredUi(ui));
+				$.ui.dialog.overlay.resize();
+			}
+		});
+	},
+
+	_makeResizable: function(handles) {
+		handles = (handles === undefined ? this.options.resizable : handles);
+		var self = this,
+			options = self.options,
+			// .ui-resizable has position: relative defined in the stylesheet
+			// but dialogs have to use absolute or fixed positioning
+			position = self.uiDialog.css('position'),
+			resizeHandles = (typeof handles === 'string' ?
+				handles	:
+				'n,e,s,w,se,sw,ne,nw'
+			);
+
+		function filteredUi(ui) {
+			return {
+				originalPosition: ui.originalPosition,
+				originalSize: ui.originalSize,
+				position: ui.position,
+				size: ui.size
+			};
+		}
+
+		self.uiDialog.resizable({
+			cancel: '.ui-dialog-content',
+			containment: 'document',
+			alsoResize: self.element,
+			maxWidth: options.maxWidth,
+			maxHeight: options.maxHeight,
+			minWidth: options.minWidth,
+			minHeight: self._minHeight(),
+			handles: resizeHandles,
+			start: function(event, ui) {
+				$(this).addClass("ui-dialog-resizing");
+				self._trigger('resizeStart', event, filteredUi(ui));
+			},
+			resize: function(event, ui) {
+				self._trigger('resize', event, filteredUi(ui));
+			},
+			stop: function(event, ui) {
+				$(this).removeClass("ui-dialog-resizing");
+				options.height = $(this).height();
+				options.width = $(this).width();
+				self._trigger('resizeStop', event, filteredUi(ui));
+				$.ui.dialog.overlay.resize();
+			}
+		})
+		.css('position', position)
+		.find('.ui-resizable-se').addClass('ui-icon ui-icon-grip-diagonal-se');
+	},
+
+	_minHeight: function() {
+		var options = this.options;
+
+		if (options.height === 'auto') {
+			return options.minHeight;
+		} else {
+			return Math.min(options.minHeight, options.height);
+		}
+	},
+
+	_position: function(position) {
+		var myAt = [],
+			offset = [0, 0],
+			isVisible;
+
+		if (position) {
+			// deep extending converts arrays to objects in jQuery <= 1.3.2 :-(
+	//		if (typeof position == 'string' || $.isArray(position)) {
+	//			myAt = $.isArray(position) ? position : position.split(' ');
+
+			if (typeof position === 'string' || (typeof position === 'object' && '0' in position)) {
+				myAt = position.split ? position.split(' ') : [position[0], position[1]];
+				if (myAt.length === 1) {
+					myAt[1] = myAt[0];
+				}
+
+				$.each(['left', 'top'], function(i, offsetPosition) {
+					if (+myAt[i] === myAt[i]) {
+						offset[i] = myAt[i];
+						myAt[i] = offsetPosition;
+					}
+				});
+
+				position = {
+					my: myAt.join(" "),
+					at: myAt.join(" "),
+					offset: offset.join(" ")
+				};
+			} 
+
+			position = $.extend({}, $.ui.dialog.prototype.options.position, position);
+		} else {
+			position = $.ui.dialog.prototype.options.position;
+		}
+
+		// need to show the dialog to get the actual offset in the position plugin
+		isVisible = this.uiDialog.is(':visible');
+		if (!isVisible) {
+			this.uiDialog.show();
+		}
+		this.uiDialog
+			// workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
+			.css({ top: 0, left: 0 })
+			.position($.extend({ of: window }, position));
+		if (!isVisible) {
+			this.uiDialog.hide();
+		}
+	},
+
+	_setOptions: function( options ) {
+		var self = this,
+			resizableOptions = {},
+			resize = false;
+
+		$.each( options, function( key, value ) {
+			self._setOption( key, value );
+			
+			if ( key in sizeRelatedOptions ) {
+				resize = true;
+			}
+			if ( key in resizableRelatedOptions ) {
+				resizableOptions[ key ] = value;
+			}
+		});
+
+		if ( resize ) {
+			this._size();
+		}
+		if ( this.uiDialog.is( ":data(resizable)" ) ) {
+			this.uiDialog.resizable( "option", resizableOptions );
+		}
+	},
+
+	_setOption: function(key, value){
+		var self = this,
+			uiDialog = self.uiDialog;
+
+		switch (key) {
+			//handling of deprecated beforeclose (vs beforeClose) option
+			//Ticket #4669 http://dev.jqueryui.com/ticket/4669
+			//TODO: remove in 1.9pre
+			case "beforeclose":
+				key = "beforeClose";
+				break;
+			case "buttons":
+				self._createButtons(value);
+				break;
+			case "closeText":
+				// ensure that we always pass a string
+				self.uiDialogTitlebarCloseText.text("" + value);
+				break;
+			case "dialogClass":
+				uiDialog
+					.removeClass(self.options.dialogClass)
+					.addClass(uiDialogClasses + value);
+				break;
+			case "disabled":
+				if (value) {
+					uiDialog.addClass('ui-dialog-disabled');
+				} else {
+					uiDialog.removeClass('ui-dialog-disabled');
+				}
+				break;
+			case "draggable":
+				var isDraggable = uiDialog.is( ":data(draggable)" );
+				if ( isDraggable && !value ) {
+					uiDialog.draggable( "destroy" );
+				}
+				
+				if ( !isDraggable && value ) {
+					self._makeDraggable();
+				}
+				break;
+			case "position":
+				self._position(value);
+				break;
+			case "resizable":
+				// currently resizable, becoming non-resizable
+				var isResizable = uiDialog.is( ":data(resizable)" );
+				if (isResizable && !value) {
+					uiDialog.resizable('destroy');
+				}
+
+				// currently resizable, changing handles
+				if (isResizable && typeof value === 'string') {
+					uiDialog.resizable('option', 'handles', value);
+				}
+
+				// currently non-resizable, becoming resizable
+				if (!isResizable && value !== false) {
+					self._makeResizable(value);
+				}
+				break;
+			case "title":
+				// convert whatever was passed in o a string, for html() to not throw up
+				$(".ui-dialog-title", self.uiDialogTitlebar).html("" + (value || ' '));
+				break;
+		}
+
+		$.Widget.prototype._setOption.apply(self, arguments);
+	},
+
+	_size: function() {
+		/* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content
+		 * divs will both have width and height set, so we need to reset them
+		 */
+		var options = this.options,
+			nonContentHeight,
+			minContentHeight,
+			isVisible = this.uiDialog.is( ":visible" );
+
+		// reset content sizing
+		this.element.show().css({
+			width: 'auto',
+			minHeight: 0,
+			height: 0
+		});
+
+		if (options.minWidth > options.width) {
+			options.width = options.minWidth;
+		}
+
+		// reset wrapper sizing
+		// determine the height of all the non-content elements
+		nonContentHeight = this.uiDialog.css({
+				height: 'auto',
+				width: options.width
+			})
+			.height();
+		minContentHeight = Math.max( 0, options.minHeight - nonContentHeight );
+		
+		if ( options.height === "auto" ) {
+			// only needed for IE6 support
+			if ( $.support.minHeight ) {
+				this.element.css({
+					minHeight: minContentHeight,
+					height: "auto"
+				});
+			} else {
+				this.uiDialog.show();
+				var autoHeight = this.element.css( "height", "auto" ).height();
+				if ( !isVisible ) {
+					this.uiDialog.hide();
+				}
+				this.element.height( Math.max( autoHeight, minContentHeight ) );
+			}
+		} else {
+			this.element.height( Math.max( options.height - nonContentHeight, 0 ) );
+		}
+
+		if (this.uiDialog.is(':data(resizable)')) {
+			this.uiDialog.resizable('option', 'minHeight', this._minHeight());
+		}
+	}
+});
+
+$.extend($.ui.dialog, {
+	version: "@VERSION",
+
+	uuid: 0,
+	maxZ: 0,
+
+	getTitleId: function($el) {
+		var id = $el.attr('id');
+		if (!id) {
+			this.uuid += 1;
+			id = this.uuid;
+		}
+		return 'ui-dialog-title-' + id;
+	},
+
+	overlay: function(dialog) {
+		this.$el = $.ui.dialog.overlay.create(dialog);
+	}
+});
+
+$.extend($.ui.dialog.overlay, {
+	instances: [],
+	// reuse old instances due to IE memory leak with alpha transparency (see #5185)
+	oldInstances: [],
+	maxZ: 0,
+	events: $.map('focus,mousedown,mouseup,keydown,keypress,click'.split(','),
+		function(event) { return event + '.dialog-overlay'; }).join(' '),
+	create: function(dialog) {
+		if (this.instances.length === 0) {
+			// prevent use of anchors and inputs
+			// we use a setTimeout in case the overlay is created from an
+			// event that we're going to be cancelling (see #2804)
+			setTimeout(function() {
+				// handle $(el).dialog().dialog('close') (see #4065)
+				if ($.ui.dialog.overlay.instances.length) {
+					$(document).bind($.ui.dialog.overlay.events, function(event) {
+						// stop events if the z-index of the target is < the z-index of the overlay
+						// we cannot return true when we don't want to cancel the event (#3523)
+						if ($(event.target).zIndex() < $.ui.dialog.overlay.maxZ) {
+							return false;
+						}
+					});
+				}
+			}, 1);
+
+			// allow closing by pressing the escape key
+			$(document).bind('keydown.dialog-overlay', function(event) {
+				if (dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+					event.keyCode === $.ui.keyCode.ESCAPE) {
+					
+					dialog.close(event);
+					event.preventDefault();
+				}
+			});
+
+			// handle window resize
+			$(window).bind('resize.dialog-overlay', $.ui.dialog.overlay.resize);
+		}
+
+		var $el = (this.oldInstances.pop() || $('<div></div>').addClass('ui-widget-overlay'))
+			.appendTo(document.body)
+			.css({
+				width: this.width(),
+				height: this.height()
+			});
+
+		if ($.fn.bgiframe) {
+			$el.bgiframe();
+		}
+
+		this.instances.push($el);
+		return $el;
+	},
+
+	destroy: function($el) {
+		var indexOf = $.inArray($el, this.instances);
+		if (indexOf != -1){
+			this.oldInstances.push(this.instances.splice(indexOf, 1)[0]);
+		}
+
+		if (this.instances.length === 0) {
+			$([document, window]).unbind('.dialog-overlay');
+		}
+
+		$el.remove();
+		
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		var maxZ = 0;
+		$.each(this.instances, function() {
+			maxZ = Math.max(maxZ, this.css('z-index'));
+		});
+		this.maxZ = maxZ;
+	},
+
+	height: function() {
+		var scrollHeight,
+			offsetHeight;
+		// handle IE 6
+		if ($.browser.msie && $.browser.version < 7) {
+			scrollHeight = Math.max(
+				document.documentElement.scrollHeight,
+				document.body.scrollHeight
+			);
+			offsetHeight = Math.max(
+				document.documentElement.offsetHeight,
+				document.body.offsetHeight
+			);
+
+			if (scrollHeight < offsetHeight) {
+				return $(window).height() + 'px';
+			} else {
+				return scrollHeight + 'px';
+			}
+		// handle "good" browsers
+		} else {
+			return $(document).height() + 'px';
+		}
+	},
+
+	width: function() {
+		var scrollWidth,
+			offsetWidth;
+		// handle IE
+		if ( $.browser.msie ) {
+			scrollWidth = Math.max(
+				document.documentElement.scrollWidth,
+				document.body.scrollWidth
+			);
+			offsetWidth = Math.max(
+				document.documentElement.offsetWidth,
+				document.body.offsetWidth
+			);
+
+			if (scrollWidth < offsetWidth) {
+				return $(window).width() + 'px';
+			} else {
+				return scrollWidth + 'px';
+			}
+		// handle "good" browsers
+		} else {
+			return $(document).width() + 'px';
+		}
+	},
+
+	resize: function() {
+		/* If the dialog is draggable and the user drags it past the
+		 * right edge of the window, the document becomes wider so we
+		 * need to stretch the overlay. If the user then drags the
+		 * dialog back to the left, the document will become narrower,
+		 * so we need to shrink the overlay to the appropriate size.
+		 * This is handled by shrinking the overlay before setting it
+		 * to the full document size.
+		 */
+		var $overlays = $([]);
+		$.each($.ui.dialog.overlay.instances, function() {
+			$overlays = $overlays.add(this);
+		});
+
+		$overlays.css({
+			width: 0,
+			height: 0
+		}).css({
+			width: $.ui.dialog.overlay.width(),
+			height: $.ui.dialog.overlay.height()
+		});
+	}
+});
+
+$.extend($.ui.dialog.overlay.prototype, {
+	destroy: function() {
+		$.ui.dialog.overlay.destroy(this.$el);
+	}
+});
+
+}(jQuery));
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/draggable.js b/client/apollo/jslib/jqueryui/draggable.js
new file mode 100644
index 0000000..3a22912
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/draggable.js
@@ -0,0 +1,828 @@
+define(['jquery','./core','./mouse','./widget'], function (jQuery) {
+/*
+ * jQuery UI Draggable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Draggables
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget("ui.draggable", $.ui.mouse, {
+	widgetEventPrefix: "drag",
+	options: {
+		addClasses: true,
+		appendTo: "parent",
+		axis: false,
+		connectToSortable: false,
+		containment: false,
+		cursor: "auto",
+		cursorAt: false,
+		grid: false,
+		handle: false,
+		helper: "original",
+		iframeFix: false,
+		opacity: false,
+		refreshPositions: false,
+		revert: false,
+		revertDuration: 500,
+		scope: "default",
+		scroll: true,
+		scrollSensitivity: 20,
+		scrollSpeed: 20,
+		snap: false,
+		snapMode: "both",
+		snapTolerance: 20,
+		stack: false,
+		zIndex: false
+	},
+	_create: function() {
+
+		if (this.options.helper == 'original' && !(/^(?:r|a|f)/).test(this.element.css("position")))
+			this.element[0].style.position = 'relative';
+
+		(this.options.addClasses && this.element.addClass("ui-draggable"));
+		(this.options.disabled && this.element.addClass("ui-draggable-disabled"));
+
+		this._mouseInit();
+
+	},
+
+	destroy: function() {
+		if(!this.element.data('draggable')) return;
+		this.element
+			.removeData("draggable")
+			.unbind(".draggable")
+			.removeClass("ui-draggable"
+				+ " ui-draggable-dragging"
+				+ " ui-draggable-disabled");
+		this._mouseDestroy();
+
+		return this;
+	},
+
+	_mouseCapture: function(event) {
+
+		var o = this.options;
+
+		// among others, prevent a drag on a resizable-handle
+		if (this.helper || o.disabled || $(event.target).is('.ui-resizable-handle'))
+			return false;
+
+		//Quit if we're not on a valid handle
+		this.handle = this._getHandle(event);
+		if (!this.handle)
+			return false;
+		
+		if ( o.iframeFix ) {
+			$(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() {
+				$('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>')
+				.css({
+					width: this.offsetWidth+"px", height: this.offsetHeight+"px",
+					position: "absolute", opacity: "0.001", zIndex: 1000
+				})
+				.css($(this).offset())
+				.appendTo("body");
+			});
+		}
+
+		return true;
+
+	},
+
+	_mouseStart: function(event) {
+
+		var o = this.options;
+
+		//Create and append the visible helper
+		this.helper = this._createHelper(event);
+
+		//Cache the helper size
+		this._cacheHelperProportions();
+
+		//If ddmanager is used for droppables, set the global draggable
+		if($.ui.ddmanager)
+			$.ui.ddmanager.current = this;
+
+		/*
+		 * - Position generation -
+		 * This block generates everything position related - it's the core of draggables.
+		 */
+
+		//Cache the margins of the original element
+		this._cacheMargins();
+
+		//Store the helper's css position
+		this.cssPosition = this.helper.css("position");
+		this.scrollParent = this.helper.scrollParent();
+
+		//The element's absolute position on the page minus margins
+		this.offset = this.positionAbs = this.element.offset();
+		this.offset = {
+			top: this.offset.top - this.margins.top,
+			left: this.offset.left - this.margins.left
+		};
+
+		$.extend(this.offset, {
+			click: { //Where the click happened, relative to the element
+				left: event.pageX - this.offset.left,
+				top: event.pageY - this.offset.top
+			},
+			parent: this._getParentOffset(),
+			relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
+		});
+
+		//Generate the original position
+		this.originalPosition = this.position = this._generatePosition(event);
+		this.originalPageX = event.pageX;
+		this.originalPageY = event.pageY;
+
+		//Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
+		(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+		//Set a containment if given in the options
+		if(o.containment)
+			this._setContainment();
+
+		//Trigger event + callbacks
+		if(this._trigger("start", event) === false) {
+			this._clear();
+			return false;
+		}
+
+		//Recache the helper size
+		this._cacheHelperProportions();
+
+		//Prepare the droppable offsets
+		if ($.ui.ddmanager && !o.dropBehaviour)
+			$.ui.ddmanager.prepareOffsets(this, event);
+
+		this.helper.addClass("ui-draggable-dragging");
+		this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+		
+		//If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003)
+		if ( $.ui.ddmanager ) $.ui.ddmanager.dragStart(this, event);
+		
+		return true;
+	},
+
+	_mouseDrag: function(event, noPropagation) {
+
+		//Compute the helpers position
+		this.position = this._generatePosition(event);
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		//Call plugins and callbacks and use the resulting position if something is returned
+		if (!noPropagation) {
+			var ui = this._uiHash();
+			if(this._trigger('drag', event, ui) === false) {
+				this._mouseUp({});
+				return false;
+			}
+			this.position = ui.position;
+		}
+
+		if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px';
+		if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px';
+		if($.ui.ddmanager) $.ui.ddmanager.drag(this, event);
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+
+		//If we are using droppables, inform the manager about the drop
+		var dropped = false;
+		if ($.ui.ddmanager && !this.options.dropBehaviour)
+			dropped = $.ui.ddmanager.drop(this, event);
+
+		//if a drop comes from outside (a sortable)
+		if(this.dropped) {
+			dropped = this.dropped;
+			this.dropped = false;
+		}
+		
+		//if the original element is removed, don't bother to continue if helper is set to "original"
+		if((!this.element[0] || !this.element[0].parentNode) && this.options.helper == "original")
+			return false;
+
+		if((this.options.revert == "invalid" && !dropped) || (this.options.revert == "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) {
+			var self = this;
+			$(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() {
+				if(self._trigger("stop", event) !== false) {
+					self._clear();
+				}
+			});
+		} else {
+			if(this._trigger("stop", event) !== false) {
+				this._clear();
+			}
+		}
+
+		return false;
+	},
+	
+	_mouseUp: function(event) {
+		if (this.options.iframeFix === true) {
+			$("div.ui-draggable-iframeFix").each(function() { 
+				this.parentNode.removeChild(this); 
+			}); //Remove frame helpers
+		}
+		
+		//If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003)
+		if( $.ui.ddmanager ) $.ui.ddmanager.dragStop(this, event);
+		
+		return $.ui.mouse.prototype._mouseUp.call(this, event);
+	},
+	
+	cancel: function() {
+		
+		if(this.helper.is(".ui-draggable-dragging")) {
+			this._mouseUp({});
+		} else {
+			this._clear();
+		}
+		
+		return this;
+		
+	},
+
+	_getHandle: function(event) {
+
+		var handle = !this.options.handle || !$(this.options.handle, this.element).length ? true : false;
+		$(this.options.handle, this.element)
+			.find("*")
+			.andSelf()
+			.each(function() {
+				if(this == event.target) handle = true;
+			});
+
+		return handle;
+
+	},
+
+	_createHelper: function(event) {
+
+		var o = this.options;
+		var helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper == 'clone' ? this.element.clone().removeAttr('id') : this.element);
+
+		if(!helper.parents('body').length)
+			helper.appendTo((o.appendTo == 'parent' ? this.element[0].parentNode : o.appendTo));
+
+		if(helper[0] != this.element[0] && !(/(fixed|absolute)/).test(helper.css("position")))
+			helper.css("position", "absolute");
+
+		return helper;
+
+	},
+
+	_adjustOffsetFromHelper: function(obj) {
+		if (typeof obj == 'string') {
+			obj = obj.split(' ');
+		}
+		if ($.isArray(obj)) {
+			obj = {left: +obj[0], top: +obj[1] || 0};
+		}
+		if ('left' in obj) {
+			this.offset.click.left = obj.left + this.margins.left;
+		}
+		if ('right' in obj) {
+			this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+		}
+		if ('top' in obj) {
+			this.offset.click.top = obj.top + this.margins.top;
+		}
+		if ('bottom' in obj) {
+			this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+		}
+	},
+
+	_getParentOffset: function() {
+
+		//Get the offsetParent and cache its position
+		this.offsetParent = this.helper.offsetParent();
+		var po = this.offsetParent.offset();
+
+		// This is a special case where we need to modify a offset calculated on start, since the following happened:
+		// 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+		// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+		//    the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+		if(this.cssPosition == 'absolute' && this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) {
+			po.left += this.scrollParent.scrollLeft();
+			po.top += this.scrollParent.scrollTop();
+		}
+
+		if((this.offsetParent[0] == document.body) //This needs to be actually done for all browsers, since pageX/pageY includes this information
+		|| (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() == 'html' && $.browser.msie)) //Ugly IE fix
+			po = { top: 0, left: 0 };
+
+		return {
+			top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0),
+			left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0)
+		};
+
+	},
+
+	_getRelativeOffset: function() {
+
+		if(this.cssPosition == "relative") {
+			var p = this.element.position();
+			return {
+				top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(),
+				left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft()
+			};
+		} else {
+			return { top: 0, left: 0 };
+		}
+
+	},
+
+	_cacheMargins: function() {
+		this.margins = {
+			left: (parseInt(this.element.css("marginLeft"),10) || 0),
+			top: (parseInt(this.element.css("marginTop"),10) || 0),
+			right: (parseInt(this.element.css("marginRight"),10) || 0),
+			bottom: (parseInt(this.element.css("marginBottom"),10) || 0)
+		};
+	},
+
+	_cacheHelperProportions: function() {
+		this.helperProportions = {
+			width: this.helper.outerWidth(),
+			height: this.helper.outerHeight()
+		};
+	},
+
+	_setContainment: function() {
+
+		var o = this.options;
+		if(o.containment == 'parent') o.containment = this.helper[0].parentNode;
+		if(o.containment == 'document' || o.containment == 'window') this.containment = [
+			o.containment == 'document' ? 0 : $(window).scrollLeft() - this.offset.relative.left - this.offset.parent.left,
+			o.containment == 'document' ? 0 : $(window).scrollTop() - this.offset.relative.top - this.offset.parent.top,
+			(o.containment == 'document' ? 0 : $(window).scrollLeft()) + $(o.containment == 'document' ? document : window).width() - this.helperProportions.width - this.margins.left,
+			(o.containment == 'document' ? 0 : $(window).scrollTop()) + ($(o.containment == 'document' ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top
+		];
+
+		if(!(/^(document|window|parent)$/).test(o.containment) && o.containment.constructor != Array) {
+		        var c = $(o.containment);
+			var ce = c[0]; if(!ce) return;
+			var co = c.offset();
+			var over = ($(ce).css("overflow") != 'hidden');
+
+			this.containment = [
+				(parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0),
+				(parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0),
+				(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left - this.margins.right,
+				(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top  - this.margins.bottom
+			];
+			this.relative_container = c;
+
+		} else if(o.containment.constructor == Array) {
+			this.containment = o.containment;
+		}
+
+	},
+
+	_convertPositionTo: function(d, pos) {
+
+		if(!pos) pos = this.position;
+		var mod = d == "absolute" ? 1 : -1;
+		var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		return {
+			top: (
+				pos.top																	// The absolute mouse position
+				+ this.offset.relative.top * mod										// Only for relative positioned nodes: Relative offset from element to offset parent
+				+ this.offset.parent.top * mod											// The offsetParent's offset without borders (offset + border)
+				- ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod)
+			),
+			left: (
+				pos.left																// The absolute mouse position
+				+ this.offset.relative.left * mod										// Only for relative positioned nodes: Relative offset from element to offset parent
+				+ this.offset.parent.left * mod											// The offsetParent's offset without borders (offset + border)
+				- ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod)
+			)
+		};
+
+	},
+
+	_generatePosition: function(event) {
+
+		var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+		var pageX = event.pageX;
+		var pageY = event.pageY;
+
+		/*
+		 * - Position constraining -
+		 * Constrain the position to a mix of grid, containment.
+		 */
+
+		if(this.originalPosition) { //If we are not dragging yet, we won't check for options
+		         var containment;
+		         if(this.containment) {
+				 if (this.relative_container){
+				     var co = this.relative_container.offset();
+				     containment = [ this.containment[0] + co.left,
+						     this.containment[1] + co.top,
+						     this.containment[2] + co.left,
+						     this.containment[3] + co.top ];
+				 }
+				 else {
+				     containment = this.containment;
+				 }
+
+				if(event.pageX - this.offset.click.left < containment[0]) pageX = containment[0] + this.offset.click.left;
+				if(event.pageY - this.offset.click.top < containment[1]) pageY = containment[1] + this.offset.click.top;
+				if(event.pageX - this.offset.click.left > containment[2]) pageX = containment[2] + this.offset.click.left;
+				if(event.pageY - this.offset.click.top > containment[3]) pageY = containment[3] + this.offset.click.top;
+			}
+
+			if(o.grid) {
+				//Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950)
+				var top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY;
+				pageY = containment ? (!(top - this.offset.click.top < containment[1] || top - this.offset.click.top > containment[3]) ? top : (!(top - this.offset.click.top < containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+				var left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX;
+				pageX = containment ? (!(left - this.offset.click.left < containment[0] || left - this.offset.click.left > containment[2]) ? left : (!(left - this.offset.click.left < containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+			}
+
+		}
+
+		return {
+			top: (
+				pageY																// The absolute mouse position
+				- this.offset.click.top													// Click offset (relative to the element)
+				- this.offset.relative.top												// Only for relative positioned nodes: Relative offset from element to offset parent
+				- this.offset.parent.top												// The offsetParent's offset without borders (offset + border)
+				+ ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
+			),
+			left: (
+				pageX																// The absolute mouse position
+				- this.offset.click.left												// Click offset (relative to the element)
+				- this.offset.relative.left												// Only for relative positioned nodes: Relative offset from element to offset parent
+				- this.offset.parent.left												// The offsetParent's offset without borders (offset + border)
+				+ ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
+			)
+		};
+
+	},
+
+	_clear: function() {
+		this.helper.removeClass("ui-draggable-dragging");
+		if(this.helper[0] != this.element[0] && !this.cancelHelperRemoval) this.helper.remove();
+		//if($.ui.ddmanager) $.ui.ddmanager.current = null;
+		this.helper = null;
+		this.cancelHelperRemoval = false;
+	},
+
+	// From now on bulk stuff - mainly helpers
+
+	_trigger: function(type, event, ui) {
+		ui = ui || this._uiHash();
+		$.ui.plugin.call(this, type, [event, ui]);
+		if(type == "drag") this.positionAbs = this._convertPositionTo("absolute"); //The absolute position has to be recalculated after plugins
+		return $.Widget.prototype._trigger.call(this, type, event, ui);
+	},
+
+	plugins: {},
+
+	_uiHash: function(event) {
+		return {
+			helper: this.helper,
+			position: this.position,
+			originalPosition: this.originalPosition,
+			offset: this.positionAbs
+		};
+	}
+
+});
+
+$.extend($.ui.draggable, {
+	version: "@VERSION"
+});
+
+$.ui.plugin.add("draggable", "connectToSortable", {
+	start: function(event, ui) {
+
+		var inst = $(this).data("draggable"), o = inst.options,
+			uiSortable = $.extend({}, ui, { item: inst.element });
+		inst.sortables = [];
+		$(o.connectToSortable).each(function() {
+			var sortable = $.data(this, 'sortable');
+			if (sortable && !sortable.options.disabled) {
+				inst.sortables.push({
+					instance: sortable,
+					shouldRevert: sortable.options.revert
+				});
+				sortable.refreshPositions();	// Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page).
+				sortable._trigger("activate", event, uiSortable);
+			}
+		});
+
+	},
+	stop: function(event, ui) {
+
+		//If we are still over the sortable, we fake the stop event of the sortable, but also remove helper
+		var inst = $(this).data("draggable"),
+			uiSortable = $.extend({}, ui, { item: inst.element });
+
+		$.each(inst.sortables, function() {
+			if(this.instance.isOver) {
+
+				this.instance.isOver = 0;
+
+				inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance
+				this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work)
+
+				//The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: 'valid/invalid'
+				if(this.shouldRevert) this.instance.options.revert = true;
+
+				//Trigger the stop of the sortable
+				this.instance._mouseStop(event);
+
+				this.instance.options.helper = this.instance.options._helper;
+
+				//If the helper has been the original item, restore properties in the sortable
+				if(inst.options.helper == 'original')
+					this.instance.currentItem.css({ top: 'auto', left: 'auto' });
+
+			} else {
+				this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance
+				this.instance._trigger("deactivate", event, uiSortable);
+			}
+
+		});
+
+	},
+	drag: function(event, ui) {
+
+		var inst = $(this).data("draggable"), self = this;
+
+		var checkPos = function(o) {
+			var dyClick = this.offset.click.top, dxClick = this.offset.click.left;
+			var helperTop = this.positionAbs.top, helperLeft = this.positionAbs.left;
+			var itemHeight = o.height, itemWidth = o.width;
+			var itemTop = o.top, itemLeft = o.left;
+
+			return $.ui.isOver(helperTop + dyClick, helperLeft + dxClick, itemTop, itemLeft, itemHeight, itemWidth);
+		};
+
+		$.each(inst.sortables, function(i) {
+			
+			//Copy over some variables to allow calling the sortable's native _intersectsWith
+			this.instance.positionAbs = inst.positionAbs;
+			this.instance.helperProportions = inst.helperProportions;
+			this.instance.offset.click = inst.offset.click;
+			
+			if(this.instance._intersectsWith(this.instance.containerCache)) {
+
+				//If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once
+				if(!this.instance.isOver) {
+
+					this.instance.isOver = 1;
+					//Now we fake the start of dragging for the sortable instance,
+					//by cloning the list group item, appending it to the sortable and using it as inst.currentItem
+					//We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one)
+					this.instance.currentItem = $(self).clone().removeAttr('id').appendTo(this.instance.element).data("sortable-item", true);
+					this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it
+					this.instance.options.helper = function() { return ui.helper[0]; };
+
+					event.target = this.instance.currentItem[0];
+					this.instance._mouseCapture(event, true);
+					this.instance._mouseStart(event, true, true);
+
+					//Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes
+					this.instance.offset.click.top = inst.offset.click.top;
+					this.instance.offset.click.left = inst.offset.click.left;
+					this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left;
+					this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top;
+
+					inst._trigger("toSortable", event);
+					inst.dropped = this.instance.element; //draggable revert needs that
+					//hack so receive/update callbacks work (mostly)
+					inst.currentItem = inst.element;
+					this.instance.fromOutside = inst;
+
+				}
+
+				//Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable
+				if(this.instance.currentItem) this.instance._mouseDrag(event);
+
+			} else {
+
+				//If it doesn't intersect with the sortable, and it intersected before,
+				//we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval
+				if(this.instance.isOver) {
+
+					this.instance.isOver = 0;
+					this.instance.cancelHelperRemoval = true;
+					
+					//Prevent reverting on this forced stop
+					this.instance.options.revert = false;
+					
+					// The out event needs to be triggered independently
+					this.instance._trigger('out', event, this.instance._uiHash(this.instance));
+					
+					this.instance._mouseStop(event, true);
+					this.instance.options.helper = this.instance.options._helper;
+
+					//Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size
+					this.instance.currentItem.remove();
+					if(this.instance.placeholder) this.instance.placeholder.remove();
+
+					inst._trigger("fromSortable", event);
+					inst.dropped = false; //draggable revert needs that
+				}
+
+			};
+
+		});
+
+	}
+});
+
+$.ui.plugin.add("draggable", "cursor", {
+	start: function(event, ui) {
+		var t = $('body'), o = $(this).data('draggable').options;
+		if (t.css("cursor")) o._cursor = t.css("cursor");
+		t.css("cursor", o.cursor);
+	},
+	stop: function(event, ui) {
+		var o = $(this).data('draggable').options;
+		if (o._cursor) $('body').css("cursor", o._cursor);
+	}
+});
+
+$.ui.plugin.add("draggable", "opacity", {
+	start: function(event, ui) {
+		var t = $(ui.helper), o = $(this).data('draggable').options;
+		if(t.css("opacity")) o._opacity = t.css("opacity");
+		t.css('opacity', o.opacity);
+	},
+	stop: function(event, ui) {
+		var o = $(this).data('draggable').options;
+		if(o._opacity) $(ui.helper).css('opacity', o._opacity);
+	}
+});
+
+$.ui.plugin.add("draggable", "scroll", {
+	start: function(event, ui) {
+		var i = $(this).data("draggable");
+		if(i.scrollParent[0] != document && i.scrollParent[0].tagName != 'HTML') i.overflowOffset = i.scrollParent.offset();
+	},
+	drag: function(event, ui) {
+
+		var i = $(this).data("draggable"), o = i.options, scrolled = false;
+
+		if(i.scrollParent[0] != document && i.scrollParent[0].tagName != 'HTML') {
+
+			if(!o.axis || o.axis != 'x') {
+				if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity)
+					i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed;
+				else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity)
+					i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed;
+			}
+
+			if(!o.axis || o.axis != 'y') {
+				if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity)
+					i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed;
+				else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity)
+					i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed;
+			}
+
+		} else {
+
+			if(!o.axis || o.axis != 'x') {
+				if(event.pageY - $(document).scrollTop() < o.scrollSensitivity)
+					scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+				else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity)
+					scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+			}
+
+			if(!o.axis || o.axis != 'y') {
+				if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity)
+					scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+				else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity)
+					scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+			}
+
+		}
+
+		if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour)
+			$.ui.ddmanager.prepareOffsets(i, event);
+
+	}
+});
+
+$.ui.plugin.add("draggable", "snap", {
+	start: function(event, ui) {
+
+		var i = $(this).data("draggable"), o = i.options;
+		i.snapElements = [];
+
+		$(o.snap.constructor != String ? ( o.snap.items || ':data(draggable)' ) : o.snap).each(function() {
+			var $t = $(this); var $o = $t.offset();
+			if(this != i.element[0]) i.snapElements.push({
+				item: this,
+				width: $t.outerWidth(), height: $t.outerHeight(),
+				top: $o.top, left: $o.left
+			});
+		});
+
+	},
+	drag: function(event, ui) {
+
+		var inst = $(this).data("draggable"), o = inst.options;
+		var d = o.snapTolerance;
+
+		var x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width,
+			y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height;
+
+		for (var i = inst.snapElements.length - 1; i >= 0; i--){
+
+			var l = inst.snapElements[i].left, r = l + inst.snapElements[i].width,
+				t = inst.snapElements[i].top, b = t + inst.snapElements[i].height;
+
+			//Yes, I know, this is insane ;)
+			if(!((l-d < x1 && x1 < r+d && t-d < y1 && y1 < b+d) || (l-d < x1 && x1 < r+d && t-d < y2 && y2 < b+d) || (l-d < x2 && x2 < r+d && t-d < y1 && y1 < b+d) || (l-d < x2 && x2 < r+d && t-d < y2 && y2 < b+d))) {
+				if(inst.snapElements[i].snapping) (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item })));
+				inst.snapElements[i].snapping = false;
+				continue;
+			}
+
+			if(o.snapMode != 'inner') {
+				var ts = Math.abs(t - y2) <= d;
+				var bs = Math.abs(b - y1) <= d;
+				var ls = Math.abs(l - x2) <= d;
+				var rs = Math.abs(r - x1) <= d;
+				if(ts) ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top;
+				if(bs) ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top;
+				if(ls) ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left;
+				if(rs) ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left;
+			}
+
+			var first = (ts || bs || ls || rs);
+
+			if(o.snapMode != 'outer') {
+				var ts = Math.abs(t - y1) <= d;
+				var bs = Math.abs(b - y2) <= d;
+				var ls = Math.abs(l - x1) <= d;
+				var rs = Math.abs(r - x2) <= d;
+				if(ts) ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top;
+				if(bs) ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top;
+				if(ls) ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left;
+				if(rs) ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left;
+			}
+
+			if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first))
+				(inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item })));
+			inst.snapElements[i].snapping = (ts || bs || ls || rs || first);
+
+		};
+
+	}
+});
+
+$.ui.plugin.add("draggable", "stack", {
+	start: function(event, ui) {
+
+		var o = $(this).data("draggable").options;
+
+		var group = $.makeArray($(o.stack)).sort(function(a,b) {
+			return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0);
+		});
+		if (!group.length) { return; }
+		
+		var min = parseInt(group[0].style.zIndex) || 0;
+		$(group).each(function(i) {
+			this.style.zIndex = min + i;
+		});
+
+		this[0].style.zIndex = min + group.length;
+
+	}
+});
+
+$.ui.plugin.add("draggable", "zIndex", {
+	start: function(event, ui) {
+		var t = $(ui.helper), o = $(this).data("draggable").options;
+		if(t.css("zIndex")) o._zIndex = t.css("zIndex");
+		t.css('zIndex', o.zIndex);
+	},
+	stop: function(event, ui) {
+		var o = $(this).data("draggable").options;
+		if(o._zIndex) $(ui.helper).css('zIndex', o._zIndex);
+	}
+});
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/droppable.js b/client/apollo/jslib/jqueryui/droppable.js
new file mode 100644
index 0000000..2adbe7e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/droppable.js
@@ -0,0 +1,299 @@
+define(['jquery','./core','./widget','./mouse','./draggable'], function (jQuery) {
+/*
+ * jQuery UI Droppable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Droppables
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.draggable.js
+ */
+(function( $, undefined ) {
+
+$.widget("ui.droppable", {
+	widgetEventPrefix: "drop",
+	options: {
+		accept: '*',
+		activeClass: false,
+		addClasses: true,
+		greedy: false,
+		hoverClass: false,
+		scope: 'default',
+		tolerance: 'intersect'
+	},
+	_create: function() {
+
+		var o = this.options, accept = o.accept;
+		this.isover = 0; this.isout = 1;
+
+		this.accept = $.isFunction(accept) ? accept : function(d) {
+			return d.is(accept);
+		};
+
+		//Store the droppable's proportions
+		this.proportions = { width: this.element[0].offsetWidth, height: this.element[0].offsetHeight };
+
+		// Add the reference and positions to the manager
+		$.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || [];
+		$.ui.ddmanager.droppables[o.scope].push(this);
+
+		(o.addClasses && this.element.addClass("ui-droppable"));
+
+	},
+
+	destroy: function() {
+		var drop = $.ui.ddmanager.droppables[this.options.scope];
+		for ( var i = 0; i < drop.length; i++ )
+			if ( drop[i] == this )
+				drop.splice(i, 1);
+
+		this.element
+			.removeClass("ui-droppable ui-droppable-disabled")
+			.removeData("droppable")
+			.unbind(".droppable");
+
+		return this;
+	},
+
+	_setOption: function(key, value) {
+
+		if(key == 'accept') {
+			this.accept = $.isFunction(value) ? value : function(d) {
+				return d.is(value);
+			};
+		}
+		$.Widget.prototype._setOption.apply(this, arguments);
+	},
+
+	_activate: function(event) {
+		var draggable = $.ui.ddmanager.current;
+		if(this.options.activeClass) this.element.addClass(this.options.activeClass);
+		(draggable && this._trigger('activate', event, this.ui(draggable)));
+	},
+
+	_deactivate: function(event) {
+		var draggable = $.ui.ddmanager.current;
+		if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
+		(draggable && this._trigger('deactivate', event, this.ui(draggable)));
+	},
+
+	_over: function(event) {
+
+		var draggable = $.ui.ddmanager.current;
+		if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element
+
+		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
+			if(this.options.hoverClass) this.element.addClass(this.options.hoverClass);
+			this._trigger('over', event, this.ui(draggable));
+		}
+
+	},
+
+	_out: function(event) {
+
+		var draggable = $.ui.ddmanager.current;
+		if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return; // Bail if draggable and droppable are same element
+
+		if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
+			if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
+			this._trigger('out', event, this.ui(draggable));
+		}
+
+	},
+
+	_drop: function(event,custom) {
+
+		var draggable = custom || $.ui.ddmanager.current;
+		if (!draggable || (draggable.currentItem || draggable.element)[0] == this.element[0]) return false; // Bail if draggable and droppable are same element
+
+		var childrenIntersection = false;
+		this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function() {
+			var inst = $.data(this, 'droppable');
+			if(
+				inst.options.greedy
+				&& !inst.options.disabled
+				&& inst.options.scope == draggable.options.scope
+				&& inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element))
+				&& $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance)
+			) { childrenIntersection = true; return false; }
+		});
+		if(childrenIntersection) return false;
+
+		if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
+			if(this.options.activeClass) this.element.removeClass(this.options.activeClass);
+			if(this.options.hoverClass) this.element.removeClass(this.options.hoverClass);
+			this._trigger('drop', event, this.ui(draggable));
+			return this.element;
+		}
+
+		return false;
+
+	},
+
+	ui: function(c) {
+		return {
+			draggable: (c.currentItem || c.element),
+			helper: c.helper,
+			position: c.position,
+			offset: c.positionAbs
+		};
+	}
+
+});
+
+$.extend($.ui.droppable, {
+	version: "@VERSION"
+});
+
+$.ui.intersect = function(draggable, droppable, toleranceMode) {
+
+	if (!droppable.offset) return false;
+
+	var x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
+		y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height;
+	var l = droppable.offset.left, r = l + droppable.proportions.width,
+		t = droppable.offset.top, b = t + droppable.proportions.height;
+
+	switch (toleranceMode) {
+		case 'fit':
+			return (l <= x1 && x2 <= r
+				&& t <= y1 && y2 <= b);
+			break;
+		case 'intersect':
+			return (l < x1 + (draggable.helperProportions.width / 2) // Right Half
+				&& x2 - (draggable.helperProportions.width / 2) < r // Left Half
+				&& t < y1 + (draggable.helperProportions.height / 2) // Bottom Half
+				&& y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
+			break;
+		case 'pointer':
+			var draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left),
+				draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top),
+				isOver = $.ui.isOver(draggableTop, draggableLeft, t, l, droppable.proportions.height, droppable.proportions.width);
+			return isOver;
+			break;
+		case 'touch':
+			return (
+					(y1 >= t && y1 <= b) ||	// Top edge touching
+					(y2 >= t && y2 <= b) ||	// Bottom edge touching
+					(y1 < t && y2 > b)		// Surrounded vertically
+				) && (
+					(x1 >= l && x1 <= r) ||	// Left edge touching
+					(x2 >= l && x2 <= r) ||	// Right edge touching
+					(x1 < l && x2 > r)		// Surrounded horizontally
+				);
+			break;
+		default:
+			return false;
+			break;
+		}
+
+};
+
+/*
+	This manager tracks offsets of draggables and droppables
+*/
+$.ui.ddmanager = {
+	current: null,
+	droppables: { 'default': [] },
+	prepareOffsets: function(t, event) {
+
+		var m = $.ui.ddmanager.droppables[t.options.scope] || [];
+		var type = event ? event.type : null; // workaround for #2317
+		var list = (t.currentItem || t.element).find(":data(droppable)").andSelf();
+
+		droppablesLoop: for (var i = 0; i < m.length; i++) {
+
+			if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) continue;	//No disabled and non-accepted
+			for (var j=0; j < list.length; j++) { if(list[j] == m[i].element[0]) { m[i].proportions.height = 0; continue droppablesLoop; } }; //Filter out elements in the current dragged item
+			m[i].visible = m[i].element.css("display") != "none"; if(!m[i].visible) continue; 									//If the element is not visible, continue
+
+			if(type == "mousedown") m[i]._activate.call(m[i], event); //Activate the droppable if used directly from draggables
+
+			m[i].offset = m[i].element.offset();
+			m[i].proportions = { width: m[i].element[0].offsetWidth, height: m[i].element[0].offsetHeight };
+
+		}
+
+	},
+	drop: function(draggable, event) {
+
+		var dropped = false;
+		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {
+
+			if(!this.options) return;
+			if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance))
+				dropped = this._drop.call(this, event) || dropped;
+
+			if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
+				this.isout = 1; this.isover = 0;
+				this._deactivate.call(this, event);
+			}
+
+		});
+		return dropped;
+
+	},
+	dragStart: function( draggable, event ) {
+		//Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003)
+		draggable.element.parents( ":not(body,html)" ).bind( "scroll.droppable", function() {
+			if( !draggable.options.refreshPositions ) $.ui.ddmanager.prepareOffsets( draggable, event );
+		});
+	},
+	drag: function(draggable, event) {
+
+		//If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
+		if(draggable.options.refreshPositions) $.ui.ddmanager.prepareOffsets(draggable, event);
+
+		//Run through all droppables and check their positions based on specific tolerance options
+		$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {
+
+			if(this.options.disabled || this.greedyChild || !this.visible) return;
+			var intersects = $.ui.intersect(draggable, this, this.options.tolerance);
+
+			var c = !intersects && this.isover == 1 ? 'isout' : (intersects && this.isover == 0 ? 'isover' : null);
+			if(!c) return;
+
+			var parentInstance;
+			if (this.options.greedy) {
+				var parent = this.element.parents(':data(droppable):eq(0)');
+				if (parent.length) {
+					parentInstance = $.data(parent[0], 'droppable');
+					parentInstance.greedyChild = (c == 'isover' ? 1 : 0);
+				}
+			}
+
+			// we just moved into a greedy child
+			if (parentInstance && c == 'isover') {
+				parentInstance['isover'] = 0;
+				parentInstance['isout'] = 1;
+				parentInstance._out.call(parentInstance, event);
+			}
+
+			this[c] = 1; this[c == 'isout' ? 'isover' : 'isout'] = 0;
+			this[c == "isover" ? "_over" : "_out"].call(this, event);
+
+			// we just moved out of a greedy child
+			if (parentInstance && c == 'isout') {
+				parentInstance['isout'] = 0;
+				parentInstance['isover'] = 1;
+				parentInstance._over.call(parentInstance, event);
+			}
+		});
+
+	},
+	dragStop: function( draggable, event ) {
+		draggable.element.parents( ":not(body,html)" ).unbind( "scroll.droppable" );
+		//Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003)
+		if( !draggable.options.refreshPositions ) $.ui.ddmanager.prepareOffsets( draggable, event );
+	}
+};
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/effects/blind.js b/client/apollo/jslib/jqueryui/effects/blind.js
new file mode 100644
index 0000000..4bd1c1a
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/blind.js
@@ -0,0 +1,52 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Blind @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Blind
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.blind = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode
+		var direction = o.options.direction || 'vertical'; // Default direction
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper
+		var ref = (direction == 'vertical') ? 'height' : 'width';
+		var distance = (direction == 'vertical') ? wrapper.height() : wrapper.width();
+		if(mode == 'show') wrapper.css(ref, 0); // Shift
+
+		// Animation
+		var animation = {};
+		animation[ref] = mode == 'show' ? distance : 0;
+
+		// Animate
+		wrapper.animate(animation, o.duration, o.options.easing, function() {
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(el[0], arguments); // Callback
+			el.dequeue();
+		});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/bounce.js b/client/apollo/jslib/jqueryui/effects/bounce.js
new file mode 100644
index 0000000..6945db8
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/bounce.js
@@ -0,0 +1,81 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Bounce @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Bounce
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.bounce = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode
+		var direction = o.options.direction || 'up'; // Default direction
+		var distance = o.options.distance || 20; // Default distance
+		var times = o.options.times || 5; // Default # of times
+		var speed = o.duration || 250; // Default speed per bounce
+		if (/show|hide/.test(mode)) props.push('opacity'); // Avoid touching opacity to prevent clearType and PNG issues in IE
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		$.effects.createWrapper(el); // Create Wrapper
+		var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left';
+		var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg';
+		var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) / 3 : el.outerWidth({margin:true}) / 3);
+		if (mode == 'show') el.css('opacity', 0).css(ref, motion == 'pos' ? -distance : distance); // Shift
+		if (mode == 'hide') distance = distance / (times * 2);
+		if (mode != 'hide') times--;
+
+		// Animate
+		if (mode == 'show') { // Show Bounce
+			var animation = {opacity: 1};
+			animation[ref] = (motion == 'pos' ? '+=' : '-=') + distance;
+			el.animate(animation, speed / 2, o.options.easing);
+			distance = distance / 2;
+			times--;
+		};
+		for (var i = 0; i < times; i++) { // Bounces
+			var animation1 = {}, animation2 = {};
+			animation1[ref] = (motion == 'pos' ? '-=' : '+=') + distance;
+			animation2[ref] = (motion == 'pos' ? '+=' : '-=') + distance;
+			el.animate(animation1, speed / 2, o.options.easing).animate(animation2, speed / 2, o.options.easing);
+			distance = (mode == 'hide') ? distance * 2 : distance / 2;
+		};
+		if (mode == 'hide') { // Last Bounce
+			var animation = {opacity: 0};
+			animation[ref] = (motion == 'pos' ? '-=' : '+=')  + distance;
+			el.animate(animation, speed / 2, o.options.easing, function(){
+				el.hide(); // Hide
+				$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+				if(o.callback) o.callback.apply(this, arguments); // Callback
+			});
+		} else {
+			var animation1 = {}, animation2 = {};
+			animation1[ref] = (motion == 'pos' ? '-=' : '+=') + distance;
+			animation2[ref] = (motion == 'pos' ? '+=' : '-=') + distance;
+			el.animate(animation1, speed / 2, o.options.easing).animate(animation2, speed / 2, o.options.easing, function(){
+				$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+				if(o.callback) o.callback.apply(this, arguments); // Callback
+			});
+		};
+		el.queue('fx', function() { el.dequeue(); });
+		el.dequeue();
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/clip.js b/client/apollo/jslib/jqueryui/effects/clip.js
new file mode 100644
index 0000000..d6cbddc
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/clip.js
@@ -0,0 +1,57 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Clip @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Clip
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.clip = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right','height','width'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode
+		var direction = o.options.direction || 'vertical'; // Default direction
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper
+		var animate = el[0].tagName == 'IMG' ? wrapper : el;
+		var ref = {
+			size: (direction == 'vertical') ? 'height' : 'width',
+			position: (direction == 'vertical') ? 'top' : 'left'
+		};
+		var distance = (direction == 'vertical') ? animate.height() : animate.width();
+		if(mode == 'show') { animate.css(ref.size, 0); animate.css(ref.position, distance / 2); } // Shift
+
+		// Animation
+		var animation = {};
+		animation[ref.size] = mode == 'show' ? distance : 0;
+		animation[ref.position] = mode == 'show' ? 0 : distance / 2;
+
+		// Animate
+		animate.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() {
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(el[0], arguments); // Callback
+			el.dequeue();
+		}});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/core.js b/client/apollo/jslib/jqueryui/effects/core.js
new file mode 100644
index 0000000..f212c83
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/core.js
@@ -0,0 +1,766 @@
+define(['jquery'], function (jQuery) {
+/*
+ * jQuery UI Effects @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/
+ */
+;jQuery.effects || (function($, undefined) {
+
+$.effects = {};
+
+
+
+/******************************************************************************/
+/****************************** COLOR ANIMATIONS ******************************/
+/******************************************************************************/
+
+// override the animation for color styles
+$.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor',
+	'borderRightColor', 'borderTopColor', 'borderColor', 'color', 'outlineColor'],
+function(i, attr) {
+	$.fx.step[attr] = function(fx) {
+		if (!fx.colorInit) {
+			fx.start = getColor(fx.elem, attr);
+			fx.end = getRGB(fx.end);
+			fx.colorInit = true;
+		}
+
+		fx.elem.style[attr] = 'rgb(' +
+			Math.max(Math.min(parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10), 255), 0) + ',' +
+			Math.max(Math.min(parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10), 255), 0) + ',' +
+			Math.max(Math.min(parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10), 255), 0) + ')';
+	};
+});
+
+// Color Conversion functions from highlightFade
+// By Blair Mitchelmore
+// http://jquery.offput.ca/highlightFade/
+
+// Parse strings looking for color tuples [255,255,255]
+function getRGB(color) {
+		var result;
+
+		// Check if we're already dealing with an array of colors
+		if ( color && color.constructor == Array && color.length == 3 )
+				return color;
+
+		// Look for rgb(num,num,num)
+		if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color))
+				return [parseInt(result[1],10), parseInt(result[2],10), parseInt(result[3],10)];
+
+		// Look for rgb(num%,num%,num%)
+		if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color))
+				return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55];
+
+		// Look for #a0b1c2
+		if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color))
+				return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
+
+		// Look for #fff
+		if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color))
+				return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
+
+		// Look for rgba(0, 0, 0, 0) == transparent in Safari 3
+		if (result = /rgba\(0, 0, 0, 0\)/.exec(color))
+				return colors['transparent'];
+
+		// Otherwise, we're most likely dealing with a named color
+		return colors[$.trim(color).toLowerCase()];
+}
+
+function getColor(elem, attr) {
+		var color;
+
+		do {
+				color = $.curCSS(elem, attr);
+
+				// Keep going until we find an element that has color, or we hit the body
+				if ( color != '' && color != 'transparent' || $.nodeName(elem, "body") )
+						break;
+
+				attr = "backgroundColor";
+		} while ( elem = elem.parentNode );
+
+		return getRGB(color);
+};
+
+// Some named colors to work with
+// From Interface by Stefan Petre
+// http://interface.eyecon.ro/
+
+var colors = {
+	aqua:[0,255,255],
+	azure:[240,255,255],
+	beige:[245,245,220],
+	black:[0,0,0],
+	blue:[0,0,255],
+	brown:[165,42,42],
+	cyan:[0,255,255],
+	darkblue:[0,0,139],
+	darkcyan:[0,139,139],
+	darkgrey:[169,169,169],
+	darkgreen:[0,100,0],
+	darkkhaki:[189,183,107],
+	darkmagenta:[139,0,139],
+	darkolivegreen:[85,107,47],
+	darkorange:[255,140,0],
+	darkorchid:[153,50,204],
+	darkred:[139,0,0],
+	darksalmon:[233,150,122],
+	darkviolet:[148,0,211],
+	fuchsia:[255,0,255],
+	gold:[255,215,0],
+	green:[0,128,0],
+	indigo:[75,0,130],
+	khaki:[240,230,140],
+	lightblue:[173,216,230],
+	lightcyan:[224,255,255],
+	lightgreen:[144,238,144],
+	lightgrey:[211,211,211],
+	lightpink:[255,182,193],
+	lightyellow:[255,255,224],
+	lime:[0,255,0],
+	magenta:[255,0,255],
+	maroon:[128,0,0],
+	navy:[0,0,128],
+	olive:[128,128,0],
+	orange:[255,165,0],
+	pink:[255,192,203],
+	purple:[128,0,128],
+	violet:[128,0,128],
+	red:[255,0,0],
+	silver:[192,192,192],
+	white:[255,255,255],
+	yellow:[255,255,0],
+	transparent: [255,255,255]
+};
+
+
+
+/******************************************************************************/
+/****************************** CLASS ANIMATIONS ******************************/
+/******************************************************************************/
+
+var classAnimationActions = ['add', 'remove', 'toggle'],
+	shorthandStyles = {
+		border: 1,
+		borderBottom: 1,
+		borderColor: 1,
+		borderLeft: 1,
+		borderRight: 1,
+		borderTop: 1,
+		borderWidth: 1,
+		margin: 1,
+		padding: 1
+	};
+
+function getElementStyles() {
+	var style = document.defaultView
+			? document.defaultView.getComputedStyle(this, null)
+			: this.currentStyle,
+		newStyle = {},
+		key,
+		camelCase;
+
+	// webkit enumerates style porperties
+	if (style && style.length && style[0] && style[style[0]]) {
+		var len = style.length;
+		while (len--) {
+			key = style[len];
+			if (typeof style[key] == 'string') {
+				camelCase = key.replace(/\-(\w)/g, function(all, letter){
+					return letter.toUpperCase();
+				});
+				newStyle[camelCase] = style[key];
+			}
+		}
+	} else {
+		for (key in style) {
+			if (typeof style[key] === 'string') {
+				newStyle[key] = style[key];
+			}
+		}
+	}
+	
+	return newStyle;
+}
+
+function filterStyles(styles) {
+	var name, value;
+	for (name in styles) {
+		value = styles[name];
+		if (
+			// ignore null and undefined values
+			value == null ||
+			// ignore functions (when does this occur?)
+			$.isFunction(value) ||
+			// shorthand styles that need to be expanded
+			name in shorthandStyles ||
+			// ignore scrollbars (break in IE)
+			(/scrollbar/).test(name) ||
+
+			// only colors or values that can be converted to numbers
+			(!(/color/i).test(name) && isNaN(parseFloat(value)))
+		) {
+			delete styles[name];
+		}
+	}
+	
+	return styles;
+}
+
+function styleDifference(oldStyle, newStyle) {
+	var diff = { _: 0 }, // http://dev.jquery.com/ticket/5459
+		name;
+
+	for (name in newStyle) {
+		if (oldStyle[name] != newStyle[name]) {
+			diff[name] = newStyle[name];
+		}
+	}
+
+	return diff;
+}
+
+$.effects.animateClass = function(value, duration, easing, callback) {
+	if ($.isFunction(easing)) {
+		callback = easing;
+		easing = null;
+	}
+
+	return this.queue(function() {
+		var that = $(this),
+			originalStyleAttr = that.attr('style') || ' ',
+			originalStyle = filterStyles(getElementStyles.call(this)),
+			newStyle,
+			className = that.attr('class');
+
+		$.each(classAnimationActions, function(i, action) {
+			if (value[action]) {
+				that[action + 'Class'](value[action]);
+			}
+		});
+		newStyle = filterStyles(getElementStyles.call(this));
+		that.attr('class', className);
+
+		that.animate(styleDifference(originalStyle, newStyle), {
+			queue: false,
+			duration: duration,
+			easing: easing,
+			complete: function() {
+				$.each(classAnimationActions, function(i, action) {
+					if (value[action]) { that[action + 'Class'](value[action]); }
+				});
+				// work around bug in IE by clearing the cssText before setting it
+				if (typeof that.attr('style') == 'object') {
+					that.attr('style').cssText = '';
+					that.attr('style').cssText = originalStyleAttr;
+				} else {
+					that.attr('style', originalStyleAttr);
+				}
+				if (callback) { callback.apply(this, arguments); }
+				$.dequeue( this );
+			}
+		});
+	});
+};
+
+$.fn.extend({
+	_addClass: $.fn.addClass,
+	addClass: function(classNames, speed, easing, callback) {
+		return speed ? $.effects.animateClass.apply(this, [{ add: classNames },speed,easing,callback]) : this._addClass(classNames);
+	},
+
+	_removeClass: $.fn.removeClass,
+	removeClass: function(classNames,speed,easing,callback) {
+		return speed ? $.effects.animateClass.apply(this, [{ remove: classNames },speed,easing,callback]) : this._removeClass(classNames);
+	},
+
+	_toggleClass: $.fn.toggleClass,
+	toggleClass: function(classNames, force, speed, easing, callback) {
+		if ( typeof force == "boolean" || force === undefined ) {
+			if ( !speed ) {
+				// without speed parameter;
+				return this._toggleClass(classNames, force);
+			} else {
+				return $.effects.animateClass.apply(this, [(force?{add:classNames}:{remove:classNames}),speed,easing,callback]);
+			}
+		} else {
+			// without switch parameter;
+			return $.effects.animateClass.apply(this, [{ toggle: classNames },force,speed,easing]);
+		}
+	},
+
+	switchClass: function(remove,add,speed,easing,callback) {
+		return $.effects.animateClass.apply(this, [{ add: add, remove: remove },speed,easing,callback]);
+	}
+});
+
+
+
+/******************************************************************************/
+/*********************************** EFFECTS **********************************/
+/******************************************************************************/
+
+$.extend($.effects, {
+	version: "@VERSION",
+
+	// Saves a set of properties in a data storage
+	save: function(element, set) {
+		for(var i=0; i < set.length; i++) {
+			if(set[i] !== null) element.data("ec.storage."+set[i], element[0].style[set[i]]);
+		}
+	},
+
+	// Restores a set of previously saved properties from a data storage
+	restore: function(element, set) {
+		for(var i=0; i < set.length; i++) {
+			if(set[i] !== null) element.css(set[i], element.data("ec.storage."+set[i]));
+		}
+	},
+
+	setMode: function(el, mode) {
+		if (mode == 'toggle') mode = el.is(':hidden') ? 'show' : 'hide'; // Set for toggle
+		return mode;
+	},
+
+	getBaseline: function(origin, original) { // Translates a [top,left] array into a baseline value
+		// this should be a little more flexible in the future to handle a string & hash
+		var y, x;
+		switch (origin[0]) {
+			case 'top': y = 0; break;
+			case 'middle': y = 0.5; break;
+			case 'bottom': y = 1; break;
+			default: y = origin[0] / original.height;
+		};
+		switch (origin[1]) {
+			case 'left': x = 0; break;
+			case 'center': x = 0.5; break;
+			case 'right': x = 1; break;
+			default: x = origin[1] / original.width;
+		};
+		return {x: x, y: y};
+	},
+
+	// Wraps the element around a wrapper that copies position properties
+	createWrapper: function(element) {
+
+		// if the element is already wrapped, return it
+		if (element.parent().is('.ui-effects-wrapper')) {
+			return element.parent();
+		}
+
+		// wrap the element
+		var props = {
+				width: element.outerWidth(true),
+				height: element.outerHeight(true),
+				'float': element.css('float')
+			},
+			wrapper = $('<div></div>')
+				.addClass('ui-effects-wrapper')
+				.css({
+					fontSize: '100%',
+					background: 'transparent',
+					border: 'none',
+					margin: 0,
+					padding: 0
+				}),
+			active = document.activeElement;
+
+		element.wrap(wrapper);
+
+		// Fixes #7595 - Elements lose focus when wrapped.
+		if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+			$( active ).focus();
+		}
+		
+		wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually loose the reference to the wrapped element
+
+		// transfer positioning properties to the wrapper
+		if (element.css('position') == 'static') {
+			wrapper.css({ position: 'relative' });
+			element.css({ position: 'relative' });
+		} else {
+			$.extend(props, {
+				position: element.css('position'),
+				zIndex: element.css('z-index')
+			});
+			$.each(['top', 'left', 'bottom', 'right'], function(i, pos) {
+				props[pos] = element.css(pos);
+				if (isNaN(parseInt(props[pos], 10))) {
+					props[pos] = 'auto';
+				}
+			});
+			element.css({position: 'relative', top: 0, left: 0, right: 'auto', bottom: 'auto' });
+		}
+
+		return wrapper.css(props).show();
+	},
+
+	removeWrapper: function(element) {
+		var parent,
+			active = document.activeElement;
+		
+		if (element.parent().is('.ui-effects-wrapper')) {
+			parent = element.parent().replaceWith(element);
+			// Fixes #7595 - Elements lose focus when wrapped.
+			if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+				$( active ).focus();
+			}
+			return parent;
+		}
+			
+		return element;
+	},
+
+	setTransition: function(element, list, factor, value) {
+		value = value || {};
+		$.each(list, function(i, x){
+			unit = element.cssUnit(x);
+			if (unit[0] > 0) value[x] = unit[0] * factor + unit[1];
+		});
+		return value;
+	}
+});
+
+
+function _normalizeArguments(effect, options, speed, callback) {
+	// shift params for method overloading
+	if (typeof effect == 'object') {
+		callback = options;
+		speed = null;
+		options = effect;
+		effect = options.effect;
+	}
+	if ($.isFunction(options)) {
+		callback = options;
+		speed = null;
+		options = {};
+	}
+        if (typeof options == 'number' || $.fx.speeds[options]) {
+		callback = speed;
+		speed = options;
+		options = {};
+	}
+	if ($.isFunction(speed)) {
+		callback = speed;
+		speed = null;
+	}
+
+	options = options || {};
+
+	speed = speed || options.duration;
+	speed = $.fx.off ? 0 : typeof speed == 'number'
+		? speed : speed in $.fx.speeds ? $.fx.speeds[speed] : $.fx.speeds._default;
+
+	callback = callback || options.complete;
+
+	return [effect, options, speed, callback];
+}
+
+function standardSpeed( speed ) {
+	// valid standard speeds
+	if ( !speed || typeof speed === "number" || $.fx.speeds[ speed ] ) {
+		return true;
+	}
+	
+	// invalid strings - treat as "normal" speed
+	if ( typeof speed === "string" && !$.effects[ speed ] ) {
+		return true;
+	}
+	
+	return false;
+}
+
+$.fn.extend({
+	effect: function(effect, options, speed, callback) {
+		var args = _normalizeArguments.apply(this, arguments),
+			// TODO: make effects take actual parameters instead of a hash
+			args2 = {
+				options: args[1],
+				duration: args[2],
+				callback: args[3]
+			},
+			mode = args2.options.mode,
+			effectMethod = $.effects[effect];
+		
+		if ( $.fx.off || !effectMethod ) {
+			// delegate to the original method (e.g., .show()) if possible
+			if ( mode ) {
+				return this[ mode ]( args2.duration, args2.callback );
+			} else {
+				return this.each(function() {
+					if ( args2.callback ) {
+						args2.callback.call( this );
+					}
+				});
+			}
+		}
+		
+		return effectMethod.call(this, args2);
+	},
+
+	_show: $.fn.show,
+	show: function(speed) {
+		if ( standardSpeed( speed ) ) {
+			return this._show.apply(this, arguments);
+		} else {
+			var args = _normalizeArguments.apply(this, arguments);
+			args[1].mode = 'show';
+			return this.effect.apply(this, args);
+		}
+	},
+
+	_hide: $.fn.hide,
+	hide: function(speed) {
+		if ( standardSpeed( speed ) ) {
+			return this._hide.apply(this, arguments);
+		} else {
+			var args = _normalizeArguments.apply(this, arguments);
+			args[1].mode = 'hide';
+			return this.effect.apply(this, args);
+		}
+	},
+
+	// jQuery core overloads toggle and creates _toggle
+	__toggle: $.fn.toggle,
+	toggle: function(speed) {
+		if ( standardSpeed( speed ) || typeof speed === "boolean" || $.isFunction( speed ) ) {
+			return this.__toggle.apply(this, arguments);
+		} else {
+			var args = _normalizeArguments.apply(this, arguments);
+			args[1].mode = 'toggle';
+			return this.effect.apply(this, args);
+		}
+	},
+
+	// helper functions
+	cssUnit: function(key) {
+		var style = this.css(key), val = [];
+		$.each( ['em','px','%','pt'], function(i, unit){
+			if(style.indexOf(unit) > 0)
+				val = [parseFloat(style), unit];
+		});
+		return val;
+	}
+});
+
+
+
+/******************************************************************************/
+/*********************************** EASING ***********************************/
+/******************************************************************************/
+
+/*
+ * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/
+ *
+ * Uses the built in easing capabilities added In jQuery 1.1
+ * to offer multiple easing options
+ *
+ * TERMS OF USE - jQuery Easing
+ *
+ * Open source under the BSD License.
+ *
+ * Copyright 2008 George McGinley Smith
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list
+ * of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution.
+ *
+ * Neither the name of the author nor the names of contributors may be used to endorse
+ * or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+*/
+
+// t: current time, b: begInnIng value, c: change In value, d: duration
+$.easing.jswing = $.easing.swing;
+
+$.extend($.easing,
+{
+	def: 'easeOutQuad',
+	swing: function (x, t, b, c, d) {
+		//alert($.easing.default);
+		return $.easing[$.easing.def](x, t, b, c, d);
+	},
+	easeInQuad: function (x, t, b, c, d) {
+		return c*(t/=d)*t + b;
+	},
+	easeOutQuad: function (x, t, b, c, d) {
+		return -c *(t/=d)*(t-2) + b;
+	},
+	easeInOutQuad: function (x, t, b, c, d) {
+		if ((t/=d/2) < 1) return c/2*t*t + b;
+		return -c/2 * ((--t)*(t-2) - 1) + b;
+	},
+	easeInCubic: function (x, t, b, c, d) {
+		return c*(t/=d)*t*t + b;
+	},
+	easeOutCubic: function (x, t, b, c, d) {
+		return c*((t=t/d-1)*t*t + 1) + b;
+	},
+	easeInOutCubic: function (x, t, b, c, d) {
+		if ((t/=d/2) < 1) return c/2*t*t*t + b;
+		return c/2*((t-=2)*t*t + 2) + b;
+	},
+	easeInQuart: function (x, t, b, c, d) {
+		return c*(t/=d)*t*t*t + b;
+	},
+	easeOutQuart: function (x, t, b, c, d) {
+		return -c * ((t=t/d-1)*t*t*t - 1) + b;
+	},
+	easeInOutQuart: function (x, t, b, c, d) {
+		if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
+		return -c/2 * ((t-=2)*t*t*t - 2) + b;
+	},
+	easeInQuint: function (x, t, b, c, d) {
+		return c*(t/=d)*t*t*t*t + b;
+	},
+	easeOutQuint: function (x, t, b, c, d) {
+		return c*((t=t/d-1)*t*t*t*t + 1) + b;
+	},
+	easeInOutQuint: function (x, t, b, c, d) {
+		if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
+		return c/2*((t-=2)*t*t*t*t + 2) + b;
+	},
+	easeInSine: function (x, t, b, c, d) {
+		return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
+	},
+	easeOutSine: function (x, t, b, c, d) {
+		return c * Math.sin(t/d * (Math.PI/2)) + b;
+	},
+	easeInOutSine: function (x, t, b, c, d) {
+		return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
+	},
+	easeInExpo: function (x, t, b, c, d) {
+		return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
+	},
+	easeOutExpo: function (x, t, b, c, d) {
+		return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
+	},
+	easeInOutExpo: function (x, t, b, c, d) {
+		if (t==0) return b;
+		if (t==d) return b+c;
+		if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
+		return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
+	},
+	easeInCirc: function (x, t, b, c, d) {
+		return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
+	},
+	easeOutCirc: function (x, t, b, c, d) {
+		return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
+	},
+	easeInOutCirc: function (x, t, b, c, d) {
+		if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
+		return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
+	},
+	easeInElastic: function (x, t, b, c, d) {
+		var s=1.70158;var p=0;var a=c;
+		if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
+		if (a < Math.abs(c)) { a=c; var s=p/4; }
+		else var s = p/(2*Math.PI) * Math.asin (c/a);
+		return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
+	},
+	easeOutElastic: function (x, t, b, c, d) {
+		var s=1.70158;var p=0;var a=c;
+		if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
+		if (a < Math.abs(c)) { a=c; var s=p/4; }
+		else var s = p/(2*Math.PI) * Math.asin (c/a);
+		return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
+	},
+	easeInOutElastic: function (x, t, b, c, d) {
+		var s=1.70158;var p=0;var a=c;
+		if (t==0) return b;  if ((t/=d/2)==2) return b+c;  if (!p) p=d*(.3*1.5);
+		if (a < Math.abs(c)) { a=c; var s=p/4; }
+		else var s = p/(2*Math.PI) * Math.asin (c/a);
+		if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
+		return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
+	},
+	easeInBack: function (x, t, b, c, d, s) {
+		if (s == undefined) s = 1.70158;
+		return c*(t/=d)*t*((s+1)*t - s) + b;
+	},
+	easeOutBack: function (x, t, b, c, d, s) {
+		if (s == undefined) s = 1.70158;
+		return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
+	},
+	easeInOutBack: function (x, t, b, c, d, s) {
+		if (s == undefined) s = 1.70158;
+		if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
+		return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
+	},
+	easeInBounce: function (x, t, b, c, d) {
+		return c - $.easing.easeOutBounce (x, d-t, 0, c, d) + b;
+	},
+	easeOutBounce: function (x, t, b, c, d) {
+		if ((t/=d) < (1/2.75)) {
+			return c*(7.5625*t*t) + b;
+		} else if (t < (2/2.75)) {
+			return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
+		} else if (t < (2.5/2.75)) {
+			return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
+		} else {
+			return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
+		}
+	},
+	easeInOutBounce: function (x, t, b, c, d) {
+		if (t < d/2) return $.easing.easeInBounce (x, t*2, 0, c, d) * .5 + b;
+		return $.easing.easeOutBounce (x, t*2-d, 0, c, d) * .5 + c*.5 + b;
+	}
+});
+
+/*
+ *
+ * TERMS OF USE - EASING EQUATIONS
+ *
+ * Open source under the BSD License.
+ *
+ * Copyright 2001 Robert Penner
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, this list
+ * of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution.
+ *
+ * Neither the name of the author nor the names of contributors may be used to endorse
+ * or promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/effects/drop.js b/client/apollo/jslib/jqueryui/effects/drop.js
new file mode 100644
index 0000000..8ccc191
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/drop.js
@@ -0,0 +1,53 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Drop @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Drop
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.drop = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right','opacity'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode
+		var direction = o.options.direction || 'left'; // Default Direction
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		$.effects.createWrapper(el); // Create Wrapper
+		var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left';
+		var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg';
+		var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) / 2 : el.outerWidth({margin:true}) / 2);
+		if (mode == 'show') el.css('opacity', 0).css(ref, motion == 'pos' ? -distance : distance); // Shift
+
+		// Animation
+		var animation = {opacity: mode == 'show' ? 1 : 0};
+		animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance;
+
+		// Animate
+		el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() {
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(this, arguments); // Callback
+			el.dequeue();
+		}});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/explode.js b/client/apollo/jslib/jqueryui/effects/explode.js
new file mode 100644
index 0000000..84ed338
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/explode.js
@@ -0,0 +1,82 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Explode @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Explode
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.explode = function(o) {
+
+	return this.queue(function() {
+
+	var rows = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3;
+	var cells = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3;
+
+	o.options.mode = o.options.mode == 'toggle' ? ($(this).is(':visible') ? 'hide' : 'show') : o.options.mode;
+	var el = $(this).show().css('visibility', 'hidden');
+	var offset = el.offset();
+
+	//Substract the margins - not fixing the problem yet.
+	offset.top -= parseInt(el.css("marginTop"),10) || 0;
+	offset.left -= parseInt(el.css("marginLeft"),10) || 0;
+
+	var width = el.outerWidth(true);
+	var height = el.outerHeight(true);
+
+	for(var i=0;i<rows;i++) { // =
+		for(var j=0;j<cells;j++) { // ||
+			el
+				.clone()
+				.appendTo('body')
+				.wrap('<div></div>')
+				.css({
+					position: 'absolute',
+					visibility: 'visible',
+					left: -j*(width/cells),
+					top: -i*(height/rows)
+				})
+				.parent()
+				.addClass('ui-effects-explode')
+				.css({
+					position: 'absolute',
+					overflow: 'hidden',
+					width: width/cells,
+					height: height/rows,
+					left: offset.left + j*(width/cells) + (o.options.mode == 'show' ? (j-Math.floor(cells/2))*(width/cells) : 0),
+					top: offset.top + i*(height/rows) + (o.options.mode == 'show' ? (i-Math.floor(rows/2))*(height/rows) : 0),
+					opacity: o.options.mode == 'show' ? 0 : 1
+				}).animate({
+					left: offset.left + j*(width/cells) + (o.options.mode == 'show' ? 0 : (j-Math.floor(cells/2))*(width/cells)),
+					top: offset.top + i*(height/rows) + (o.options.mode == 'show' ? 0 : (i-Math.floor(rows/2))*(height/rows)),
+					opacity: o.options.mode == 'show' ? 1 : 0
+				}, o.duration || 500);
+		}
+	}
+
+	// Set a timeout, to call the callback approx. when the other animations have finished
+	setTimeout(function() {
+
+		o.options.mode == 'show' ? el.css({ visibility: 'visible' }) : el.css({ visibility: 'visible' }).hide();
+				if(o.callback) o.callback.apply(el[0]); // Callback
+				el.dequeue();
+
+				$('div.ui-effects-explode').remove();
+
+	}, o.duration || 500);
+
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/fade.js b/client/apollo/jslib/jqueryui/effects/fade.js
new file mode 100644
index 0000000..e1c0dd3
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/fade.js
@@ -0,0 +1,35 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Fade @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fade
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.fade = function(o) {
+	return this.queue(function() {
+		var elem = $(this),
+			mode = $.effects.setMode(elem, o.options.mode || 'hide');
+
+		elem.animate({ opacity: mode }, {
+			queue: false,
+			duration: o.duration,
+			easing: o.options.easing,
+			complete: function() {
+				(o.callback && o.callback.apply(this, arguments));
+				elem.dequeue();
+			}
+		});
+	});
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/fold.js b/client/apollo/jslib/jqueryui/effects/fold.js
new file mode 100644
index 0000000..fd38f63
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/fold.js
@@ -0,0 +1,59 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Fold @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fold
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.fold = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode
+		var size = o.options.size || 15; // Default fold size
+		var horizFirst = !(!o.options.horizFirst); // Ensure a boolean value
+		var duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2;
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		var wrapper = $.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper
+		var widthFirst = ((mode == 'show') != horizFirst);
+		var ref = widthFirst ? ['width', 'height'] : ['height', 'width'];
+		var distance = widthFirst ? [wrapper.width(), wrapper.height()] : [wrapper.height(), wrapper.width()];
+		var percent = /([0-9]+)%/.exec(size);
+		if(percent) size = parseInt(percent[1],10) / 100 * distance[mode == 'hide' ? 0 : 1];
+		if(mode == 'show') wrapper.css(horizFirst ? {height: 0, width: size} : {height: size, width: 0}); // Shift
+
+		// Animation
+		var animation1 = {}, animation2 = {};
+		animation1[ref[0]] = mode == 'show' ? distance[0] : size;
+		animation2[ref[1]] = mode == 'show' ? distance[1] : 0;
+
+		// Animate
+		wrapper.animate(animation1, duration, o.options.easing)
+		.animate(animation2, duration, o.options.easing, function() {
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(el[0], arguments); // Callback
+			el.dequeue();
+		});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/highlight.js b/client/apollo/jslib/jqueryui/effects/highlight.js
new file mode 100644
index 0000000..2e95f6e
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/highlight.js
@@ -0,0 +1,53 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Highlight @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Highlight
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.highlight = function(o) {
+	return this.queue(function() {
+		var elem = $(this),
+			props = ['backgroundImage', 'backgroundColor', 'opacity'],
+			mode = $.effects.setMode(elem, o.options.mode || 'show'),
+			animation = {
+				backgroundColor: elem.css('backgroundColor')
+			};
+
+		if (mode == 'hide') {
+			animation.opacity = 0;
+		}
+
+		$.effects.save(elem, props);
+		elem
+			.show()
+			.css({
+				backgroundImage: 'none',
+				backgroundColor: o.options.color || '#ffff99'
+			})
+			.animate(animation, {
+				queue: false,
+				duration: o.duration,
+				easing: o.options.easing,
+				complete: function() {
+					(mode == 'hide' && elem.hide());
+					$.effects.restore(elem, props);
+					(mode == 'show' && !$.support.opacity && this.style.removeAttribute('filter'));
+					(o.callback && o.callback.apply(this, arguments));
+					elem.dequeue();
+				}
+			});
+	});
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/pulsate.js b/client/apollo/jslib/jqueryui/effects/pulsate.js
new file mode 100644
index 0000000..cd62687
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/pulsate.js
@@ -0,0 +1,54 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Pulsate @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Pulsate
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.pulsate = function(o) {
+	return this.queue(function() {
+		var elem = $(this),
+			mode = $.effects.setMode(elem, o.options.mode || 'show');
+			times = ((o.options.times || 5) * 2) - 1;
+			duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2,
+			isVisible = elem.is(':visible'),
+			animateTo = 0;
+
+		if (!isVisible) {
+			elem.css('opacity', 0).show();
+			animateTo = 1;
+		}
+
+		if ((mode == 'hide' && isVisible) || (mode == 'show' && !isVisible)) {
+			times--;
+		}
+
+		for (var i = 0; i < times; i++) {
+			elem.animate({ opacity: animateTo }, duration, o.options.easing);
+			animateTo = (animateTo + 1) % 2;
+		}
+
+		elem.animate({ opacity: animateTo }, duration, o.options.easing, function() {
+			if (animateTo == 0) {
+				elem.hide();
+			}
+			(o.callback && o.callback.apply(this, arguments));
+		});
+
+		elem
+			.queue('fx', function() { elem.dequeue(); })
+			.dequeue();
+	});
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/scale.js b/client/apollo/jslib/jqueryui/effects/scale.js
new file mode 100644
index 0000000..c43f40a
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/scale.js
@@ -0,0 +1,181 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Scale @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Scale
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.puff = function(o) {
+	return this.queue(function() {
+		var elem = $(this),
+			mode = $.effects.setMode(elem, o.options.mode || 'hide'),
+			percent = parseInt(o.options.percent, 10) || 150,
+			factor = percent / 100,
+			original = { height: elem.height(), width: elem.width() };
+
+		$.extend(o.options, {
+			fade: true,
+			mode: mode,
+			percent: mode == 'hide' ? percent : 100,
+			from: mode == 'hide'
+				? original
+				: {
+					height: original.height * factor,
+					width: original.width * factor
+				}
+		});
+
+		elem.effect('scale', o.options, o.duration, o.callback);
+		elem.dequeue();
+	});
+};
+
+$.effects.scale = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this);
+
+		// Set options
+		var options = $.extend(true, {}, o.options);
+		var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode
+		var percent = parseInt(o.options.percent,10) || (parseInt(o.options.percent,10) == 0 ? 0 : (mode == 'hide' ? 0 : 100)); // Set default scaling percent
+		var direction = o.options.direction || 'both'; // Set default axis
+		var origin = o.options.origin; // The origin of the scaling
+		if (mode != 'effect') { // Set default origin and restore for show/hide
+			options.origin = origin || ['middle','center'];
+			options.restore = true;
+		}
+		var original = {height: el.height(), width: el.width()}; // Save original
+		el.from = o.options.from || (mode == 'show' ? {height: 0, width: 0} : original); // Default from state
+
+		// Adjust
+		var factor = { // Set scaling factor
+			y: direction != 'horizontal' ? (percent / 100) : 1,
+			x: direction != 'vertical' ? (percent / 100) : 1
+		};
+		el.to = {height: original.height * factor.y, width: original.width * factor.x}; // Set to state
+
+		if (o.options.fade) { // Fade option to support puff
+			if (mode == 'show') {el.from.opacity = 0; el.to.opacity = 1;};
+			if (mode == 'hide') {el.from.opacity = 1; el.to.opacity = 0;};
+		};
+
+		// Animation
+		options.from = el.from; options.to = el.to; options.mode = mode;
+
+		// Animate
+		el.effect('size', options, o.duration, o.callback);
+		el.dequeue();
+	});
+
+};
+
+$.effects.size = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right','width','height','overflow','opacity'];
+		var props1 = ['position','top','bottom','left','right','overflow','opacity']; // Always restore
+		var props2 = ['width','height','overflow']; // Copy for children
+		var cProps = ['fontSize'];
+		var vProps = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom'];
+		var hProps = ['borderLeftWidth', 'borderRightWidth', 'paddingLeft', 'paddingRight'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode
+		var restore = o.options.restore || false; // Default restore
+		var scale = o.options.scale || 'both'; // Default scale mode
+		var origin = o.options.origin; // The origin of the sizing
+		var original = {height: el.height(), width: el.width()}; // Save original
+		el.from = o.options.from || original; // Default from state
+		el.to = o.options.to || original; // Default to state
+		// Adjust
+		if (origin) { // Calculate baseline shifts
+			var baseline = $.effects.getBaseline(origin, original);
+			el.from.top = (original.height - el.from.height) * baseline.y;
+			el.from.left = (original.width - el.from.width) * baseline.x;
+			el.to.top = (original.height - el.to.height) * baseline.y;
+			el.to.left = (original.width - el.to.width) * baseline.x;
+		};
+		var factor = { // Set scaling factor
+			from: {y: el.from.height / original.height, x: el.from.width / original.width},
+			to: {y: el.to.height / original.height, x: el.to.width / original.width}
+		};
+		if (scale == 'box' || scale == 'both') { // Scale the css box
+			if (factor.from.y != factor.to.y) { // Vertical props scaling
+				props = props.concat(vProps);
+				el.from = $.effects.setTransition(el, vProps, factor.from.y, el.from);
+				el.to = $.effects.setTransition(el, vProps, factor.to.y, el.to);
+			};
+			if (factor.from.x != factor.to.x) { // Horizontal props scaling
+				props = props.concat(hProps);
+				el.from = $.effects.setTransition(el, hProps, factor.from.x, el.from);
+				el.to = $.effects.setTransition(el, hProps, factor.to.x, el.to);
+			};
+		};
+		if (scale == 'content' || scale == 'both') { // Scale the content
+			if (factor.from.y != factor.to.y) { // Vertical props scaling
+				props = props.concat(cProps);
+				el.from = $.effects.setTransition(el, cProps, factor.from.y, el.from);
+				el.to = $.effects.setTransition(el, cProps, factor.to.y, el.to);
+			};
+		};
+		$.effects.save(el, restore ? props : props1); el.show(); // Save & Show
+		$.effects.createWrapper(el); // Create Wrapper
+		el.css('overflow','hidden').css(el.from); // Shift
+
+		// Animate
+		if (scale == 'content' || scale == 'both') { // Scale the children
+			vProps = vProps.concat(['marginTop','marginBottom']).concat(cProps); // Add margins/font-size
+			hProps = hProps.concat(['marginLeft','marginRight']); // Add margins
+			props2 = props.concat(vProps).concat(hProps); // Concat
+			el.find("*[width]").each(function(){
+				child = $(this);
+				if (restore) $.effects.save(child, props2);
+				var c_original = {height: child.height(), width: child.width()}; // Save original
+				child.from = {height: c_original.height * factor.from.y, width: c_original.width * factor.from.x};
+				child.to = {height: c_original.height * factor.to.y, width: c_original.width * factor.to.x};
+				if (factor.from.y != factor.to.y) { // Vertical props scaling
+					child.from = $.effects.setTransition(child, vProps, factor.from.y, child.from);
+					child.to = $.effects.setTransition(child, vProps, factor.to.y, child.to);
+				};
+				if (factor.from.x != factor.to.x) { // Horizontal props scaling
+					child.from = $.effects.setTransition(child, hProps, factor.from.x, child.from);
+					child.to = $.effects.setTransition(child, hProps, factor.to.x, child.to);
+				};
+				child.css(child.from); // Shift children
+				child.animate(child.to, o.duration, o.options.easing, function(){
+					if (restore) $.effects.restore(child, props2); // Restore children
+				}); // Animate children
+			});
+		};
+
+		// Animate
+		el.animate(el.to, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() {
+			if (el.to.opacity === 0) {
+				el.css('opacity', el.from.opacity);
+			}
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, restore ? props : props1); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(this, arguments); // Callback
+			el.dequeue();
+		}});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/shake.js b/client/apollo/jslib/jqueryui/effects/shake.js
new file mode 100644
index 0000000..f15fd94
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/shake.js
@@ -0,0 +1,60 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Shake @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Shake
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.shake = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode
+		var direction = o.options.direction || 'left'; // Default direction
+		var distance = o.options.distance || 20; // Default distance
+		var times = o.options.times || 3; // Default # of times
+		var speed = o.duration || o.options.duration || 140; // Default speed per shake
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		$.effects.createWrapper(el); // Create Wrapper
+		var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left';
+		var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg';
+
+		// Animation
+		var animation = {}, animation1 = {}, animation2 = {};
+		animation[ref] = (motion == 'pos' ? '-=' : '+=')  + distance;
+		animation1[ref] = (motion == 'pos' ? '+=' : '-=')  + distance * 2;
+		animation2[ref] = (motion == 'pos' ? '-=' : '+=')  + distance * 2;
+
+		// Animate
+		el.animate(animation, speed, o.options.easing);
+		for (var i = 1; i < times; i++) { // Shakes
+			el.animate(animation1, speed, o.options.easing).animate(animation2, speed, o.options.easing);
+		};
+		el.animate(animation1, speed, o.options.easing).
+		animate(animation, speed / 2, o.options.easing, function(){ // Last shake
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(this, arguments); // Callback
+		});
+		el.queue('fx', function() { el.dequeue(); });
+		el.dequeue();
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/slide.js b/client/apollo/jslib/jqueryui/effects/slide.js
new file mode 100644
index 0000000..1ea4bc8
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/slide.js
@@ -0,0 +1,53 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Slide @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Slide
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.slide = function(o) {
+
+	return this.queue(function() {
+
+		// Create element
+		var el = $(this), props = ['position','top','bottom','left','right'];
+
+		// Set options
+		var mode = $.effects.setMode(el, o.options.mode || 'show'); // Set Mode
+		var direction = o.options.direction || 'left'; // Default Direction
+
+		// Adjust
+		$.effects.save(el, props); el.show(); // Save & Show
+		$.effects.createWrapper(el).css({overflow:'hidden'}); // Create Wrapper
+		var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left';
+		var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg';
+		var distance = o.options.distance || (ref == 'top' ? el.outerHeight({margin:true}) : el.outerWidth({margin:true}));
+		if (mode == 'show') el.css(ref, motion == 'pos' ? (isNaN(distance) ? "-" + distance : -distance) : distance); // Shift
+
+		// Animation
+		var animation = {};
+		animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance;
+
+		// Animate
+		el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() {
+			if(mode == 'hide') el.hide(); // Hide
+			$.effects.restore(el, props); $.effects.removeWrapper(el); // Restore
+			if(o.callback) o.callback.apply(this, arguments); // Callback
+			el.dequeue();
+		}});
+
+	});
+
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/effects/transfer.js b/client/apollo/jslib/jqueryui/effects/transfer.js
new file mode 100644
index 0000000..b8cbd49
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/effects/transfer.js
@@ -0,0 +1,48 @@
+define(['jquery','./core'], function (jQuery) {
+/*
+ * jQuery UI Effects Transfer @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Transfer
+ *
+ * Depends:
+ *	jquery.effects.core.js
+ */
+(function( $, undefined ) {
+
+$.effects.transfer = function(o) {
+	return this.queue(function() {
+		var elem = $(this),
+			target = $(o.options.to),
+			endPosition = target.offset(),
+			animation = {
+				top: endPosition.top,
+				left: endPosition.left,
+				height: target.innerHeight(),
+				width: target.innerWidth()
+			},
+			startPosition = elem.offset(),
+			transfer = $('<div class="ui-effects-transfer"></div>')
+				.appendTo(document.body)
+				.addClass(o.options.className)
+				.css({
+					top: startPosition.top,
+					left: startPosition.left,
+					height: elem.innerHeight(),
+					width: elem.innerWidth(),
+					position: 'absolute'
+				})
+				.animate(animation, o.duration, o.options.easing, function() {
+					transfer.remove();
+					(o.callback && o.callback.apply(elem[0], arguments));
+					elem.dequeue();
+				});
+	});
+};
+
+})(jQuery);
+
+});
diff --git a/client/apollo/jslib/jqueryui/mouse.js b/client/apollo/jslib/jqueryui/mouse.js
new file mode 100644
index 0000000..1aceb73
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/mouse.js
@@ -0,0 +1,165 @@
+define(['jquery','./widget'], function (jQuery) {
+/*!
+ * jQuery UI Mouse @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var mouseHandled = false;
+$( document ).mouseup( function( e ) {
+	mouseHandled = false;
+});
+
+$.widget("ui.mouse", {
+	options: {
+		cancel: ':input,option',
+		distance: 1,
+		delay: 0
+	},
+	_mouseInit: function() {
+		var self = this;
+
+		this.element
+			.bind('mousedown.'+this.widgetName, function(event) {
+				return self._mouseDown(event);
+			})
+			.bind('click.'+this.widgetName, function(event) {
+				if (true === $.data(event.target, self.widgetName + '.preventClickEvent')) {
+				    $.removeData(event.target, self.widgetName + '.preventClickEvent');
+					event.stopImmediatePropagation();
+					return false;
+				}
+			});
+
+		this.started = false;
+	},
+
+	// TODO: make sure destroying one instance of mouse doesn't mess with
+	// other instances of mouse
+	_mouseDestroy: function() {
+		this.element.unbind('.'+this.widgetName);
+	},
+
+	_mouseDown: function(event) {
+		// don't let more than one widget handle mouseStart
+		if( mouseHandled ) { return };
+
+		// we may have missed mouseup (out of window)
+		(this._mouseStarted && this._mouseUp(event));
+
+		this._mouseDownEvent = event;
+
+		var self = this,
+			btnIsLeft = (event.which == 1),
+			// event.target.nodeName works around a bug in IE 8 with
+			// disabled inputs (#7620)
+			elIsCancel = (typeof this.options.cancel == "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false);
+		if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) {
+			return true;
+		}
+
+		this.mouseDelayMet = !this.options.delay;
+		if (!this.mouseDelayMet) {
+			this._mouseDelayTimer = setTimeout(function() {
+				self.mouseDelayMet = true;
+			}, this.options.delay);
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted = (this._mouseStart(event) !== false);
+			if (!this._mouseStarted) {
+				event.preventDefault();
+				return true;
+			}
+		}
+
+		// Click event may never have fired (Gecko & Opera)
+		if (true === $.data(event.target, this.widgetName + '.preventClickEvent')) {
+			$.removeData(event.target, this.widgetName + '.preventClickEvent');
+		}
+
+		// these delegates are required to keep context
+		this._mouseMoveDelegate = function(event) {
+			return self._mouseMove(event);
+		};
+		this._mouseUpDelegate = function(event) {
+			return self._mouseUp(event);
+		};
+		$(document)
+			.bind('mousemove.'+this.widgetName, this._mouseMoveDelegate)
+			.bind('mouseup.'+this.widgetName, this._mouseUpDelegate);
+
+		event.preventDefault();
+		
+		mouseHandled = true;
+		return true;
+	},
+
+	_mouseMove: function(event) {
+		// IE mouseup check - mouseup happened when mouse was out of window
+		if ($.browser.msie && !(document.documentMode >= 9) && !event.button) {
+			return this._mouseUp(event);
+		}
+
+		if (this._mouseStarted) {
+			this._mouseDrag(event);
+			return event.preventDefault();
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted =
+				(this._mouseStart(this._mouseDownEvent, event) !== false);
+			(this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event));
+		}
+
+		return !this._mouseStarted;
+	},
+
+	_mouseUp: function(event) {
+		$(document)
+			.unbind('mousemove.'+this.widgetName, this._mouseMoveDelegate)
+			.unbind('mouseup.'+this.widgetName, this._mouseUpDelegate);
+
+		if (this._mouseStarted) {
+			this._mouseStarted = false;
+
+			if (event.target == this._mouseDownEvent.target) {
+			    $.data(event.target, this.widgetName + '.preventClickEvent', true);
+			}
+
+			this._mouseStop(event);
+		}
+
+		return false;
+	},
+
+	_mouseDistanceMet: function(event) {
+		return (Math.max(
+				Math.abs(this._mouseDownEvent.pageX - event.pageX),
+				Math.abs(this._mouseDownEvent.pageY - event.pageY)
+			) >= this.options.distance
+		);
+	},
+
+	_mouseDelayMet: function(event) {
+		return this.mouseDelayMet;
+	},
+
+	// These are placeholder methods, to be overriden by extending plugin
+	_mouseStart: function(event) {},
+	_mouseDrag: function(event) {},
+	_mouseStop: function(event) {},
+	_mouseCapture: function(event) { return true; }
+});
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/position.js b/client/apollo/jslib/jqueryui/position.js
new file mode 100644
index 0000000..04a5cdb
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/position.js
@@ -0,0 +1,301 @@
+define(['jquery'], function (jQuery) {
+/*
+ * jQuery UI Position @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Position
+ */
+(function( $, undefined ) {
+
+$.ui = $.ui || {};
+
+var horizontalPositions = /left|center|right/,
+	verticalPositions = /top|center|bottom/,
+	center = "center",
+	support = {},
+	_position = $.fn.position,
+	_offset = $.fn.offset;
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var target = $( options.of ),
+		targetElem = target[0],
+		collision = ( options.collision || "flip" ).split( " " ),
+		offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ],
+		targetWidth,
+		targetHeight,
+		basePosition;
+
+	if ( targetElem.nodeType === 9 ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		basePosition = { top: 0, left: 0 };
+	// TODO: use $.isWindow() in 1.9
+	} else if ( targetElem.setTimeout ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		basePosition = { top: target.scrollTop(), left: target.scrollLeft() };
+	} else if ( targetElem.preventDefault ) {
+		// force left top to allow flipping
+		options.at = "left top";
+		targetWidth = targetHeight = 0;
+		basePosition = { top: options.of.pageY, left: options.of.pageX };
+	} else {
+		targetWidth = target.outerWidth();
+		targetHeight = target.outerHeight();
+		basePosition = target.offset();
+	}
+
+	// force my and at to have valid horizontal and veritcal positions
+	// if a value is missing or invalid, it will be converted to center 
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[this] || "" ).split( " " );
+		if ( pos.length === 1) {
+			pos = horizontalPositions.test( pos[0] ) ?
+				pos.concat( [center] ) :
+				verticalPositions.test( pos[0] ) ?
+					[ center ].concat( pos ) :
+					[ center, center ];
+		}
+		pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center;
+		pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center;
+		options[ this ] = pos;
+	});
+
+	// normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	// normalize offset option
+	offset[ 0 ] = parseInt( offset[0], 10 ) || 0;
+	if ( offset.length === 1 ) {
+		offset[ 1 ] = offset[ 0 ];
+	}
+	offset[ 1 ] = parseInt( offset[1], 10 ) || 0;
+
+	if ( options.at[0] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[0] === center ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[1] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[1] === center ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	basePosition.left += offset[ 0 ];
+	basePosition.top += offset[ 1 ];
+
+	return this.each(function() {
+		var elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0,
+			marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0,
+			collisionWidth = elemWidth + marginLeft +
+				( parseInt( $.curCSS( this, "marginRight", true ) ) || 0 ),
+			collisionHeight = elemHeight + marginTop +
+				( parseInt( $.curCSS( this, "marginBottom", true ) ) || 0 ),
+			position = $.extend( {}, basePosition ),
+			collisionPosition;
+
+		if ( options.my[0] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[0] === center ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[1] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[1] === center ) {
+			position.top -= elemHeight / 2;
+		}
+
+		// prevent fractions if jQuery version doesn't support them (see #5280)
+		if ( !support.fractions ) {
+			position.left = Math.round( position.left );
+			position.top = Math.round( position.top );
+		}
+
+		collisionPosition = {
+			left: position.left - marginLeft,
+			top: position.top - marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[i] ] ) {
+				$.ui.position[ collision[i] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: offset,
+					my: options.my,
+					at: options.at
+				});
+			}
+		});
+
+		if ( $.fn.bgiframe ) {
+			elem.bgiframe();
+		}
+		elem.offset( $.extend( position, { using: options.using } ) );
+	});
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var win = $( window ),
+				over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft();
+			position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left );
+		},
+		top: function( position, data ) {
+			var win = $( window ),
+				over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop();
+			position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top );
+		}
+	},
+
+	flip: {
+		left: function( position, data ) {
+			if ( data.at[0] === center ) {
+				return;
+			}
+			var win = $( window ),
+				over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(),
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					-data.targetWidth,
+				offset = -2 * data.offset[ 0 ];
+			position.left += data.collisionPosition.left < 0 ?
+				myOffset + atOffset + offset :
+				over > 0 ?
+					myOffset + atOffset + offset :
+					0;
+		},
+		top: function( position, data ) {
+			if ( data.at[1] === center ) {
+				return;
+			}
+			var win = $( window ),
+				over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(),
+				myOffset = data.my[ 1 ] === "top" ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					-data.targetHeight,
+				offset = -2 * data.offset[ 1 ];
+			position.top += data.collisionPosition.top < 0 ?
+				myOffset + atOffset + offset :
+				over > 0 ?
+					myOffset + atOffset + offset :
+					0;
+		}
+	}
+};
+
+// offset setter from jQuery 1.4
+if ( !$.offset.setOffset ) {
+	$.offset.setOffset = function( elem, options ) {
+		// set position first, in-case top/left are set even on static elem
+		if ( /static/.test( $.curCSS( elem, "position" ) ) ) {
+			elem.style.position = "relative";
+		}
+		var curElem   = $( elem ),
+			curOffset = curElem.offset(),
+			curTop    = parseInt( $.curCSS( elem, "top",  true ), 10 ) || 0,
+			curLeft   = parseInt( $.curCSS( elem, "left", true ), 10)  || 0,
+			props     = {
+				top:  (options.top  - curOffset.top)  + curTop,
+				left: (options.left - curOffset.left) + curLeft
+			};
+		
+		if ( 'using' in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	};
+
+	$.fn.offset = function( options ) {
+		var elem = this[ 0 ];
+		if ( !elem || !elem.ownerDocument ) { return null; }
+		if ( options ) { 
+			return this.each(function() {
+				$.offset.setOffset( this, options );
+			});
+		}
+		return _offset.call( this );
+	};
+}
+
+// fraction support test (older versions of jQuery don't support fractions)
+(function () {
+	var body = document.getElementsByTagName( "body" )[ 0 ], 
+		div = document.createElement( "div" ),
+		testElement, testElementParent, testElementStyle, offset, offsetTotal;
+
+	//Create a "fake body" for testing based on method used in jQuery.support
+	testElement = document.createElement( body ? "div" : "body" );
+	testElementStyle = {
+		visibility: "hidden",
+		width: 0,
+		height: 0,
+		border: 0,
+		margin: 0,
+		background: "none"
+	};
+	if ( body ) {
+		$.extend( testElementStyle, {
+			position: "absolute",
+			left: "-1000px",
+			top: "-1000px"
+		});
+	}
+	for ( var i in testElementStyle ) {
+		testElement.style[ i ] = testElementStyle[ i ];
+	}
+	testElement.appendChild( div );
+	testElementParent = body || document.documentElement;
+	testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+	div.style.cssText = "position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;";
+
+	offset = $( div ).offset( function( _, offset ) {
+		return offset;
+	}).offset();
+
+	testElement.innerHTML = "";
+	testElementParent.removeChild( testElement );
+
+	offsetTotal = offset.top + offset.left + ( body ? 2000 : 0 );
+	support.fractions = offsetTotal > 21 && offsetTotal < 22;
+})();
+
+}( jQuery ));
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/progressbar.js b/client/apollo/jslib/jqueryui/progressbar.js
new file mode 100644
index 0000000..ff4d0bd
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/progressbar.js
@@ -0,0 +1,112 @@
+define(['jquery','./core','./widget'], function (jQuery) {
+/*
+ * jQuery UI Progressbar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar
+ *
+ * Depends:
+ *   jquery.ui.core.js
+ *   jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget( "ui.progressbar", {
+	options: {
+		value: 0,
+		max: 100
+	},
+
+	min: 0,
+
+	_create: function() {
+		this.element
+			.addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" )
+			.attr({
+				role: "progressbar",
+				"aria-valuemin": this.min,
+				"aria-valuemax": this.options.max,
+				"aria-valuenow": this._value()
+			});
+
+		this.valueDiv = $( "<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>" )
+			.appendTo( this.element );
+
+		this.oldValue = this._value();
+		this._refreshValue();
+	},
+
+	destroy: function() {
+		this.element
+			.removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-valuemin" )
+			.removeAttr( "aria-valuemax" )
+			.removeAttr( "aria-valuenow" );
+
+		this.valueDiv.remove();
+
+		$.Widget.prototype.destroy.apply( this, arguments );
+	},
+
+	value: function( newValue ) {
+		if ( newValue === undefined ) {
+			return this._value();
+		}
+
+		this._setOption( "value", newValue );
+		return this;
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "value" ) {
+			this.options.value = value;
+			this._refreshValue();
+			if ( this._value() === this.options.max ) {
+				this._trigger( "complete" );
+			}
+		}
+
+		$.Widget.prototype._setOption.apply( this, arguments );
+	},
+
+	_value: function() {
+		var val = this.options.value;
+		// normalize invalid value
+		if ( typeof val !== "number" ) {
+			val = 0;
+		}
+		return Math.min( this.options.max, Math.max( this.min, val ) );
+	},
+
+	_percentage: function() {
+		return 100 * this._value() / this.options.max;
+	},
+
+	_refreshValue: function() {
+		var value = this.value();
+		var percentage = this._percentage();
+
+		if ( this.oldValue !== value ) {
+			this.oldValue = value;
+			this._trigger( "change" );
+		}
+
+		this.valueDiv
+			.toggle( value > this.min )
+			.toggleClass( "ui-corner-right", value === this.options.max )
+			.width( percentage.toFixed(0) + "%" );
+		this.element.attr( "aria-valuenow", value );
+	}
+});
+
+$.extend( $.ui.progressbar, {
+	version: "@VERSION"
+});
+
+})( jQuery );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/resizable.js b/client/apollo/jslib/jqueryui/resizable.js
new file mode 100644
index 0000000..0991370
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/resizable.js
@@ -0,0 +1,811 @@
+define(['jquery','./core','./mouse','./widget'], function (jQuery) {
+/*
+ * jQuery UI Resizable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizables
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget("ui.resizable", $.ui.mouse, {
+	widgetEventPrefix: "resize",
+	options: {
+		alsoResize: false,
+		animate: false,
+		animateDuration: "slow",
+		animateEasing: "swing",
+		aspectRatio: false,
+		autoHide: false,
+		containment: false,
+		ghost: false,
+		grid: false,
+		handles: "e,s,se",
+		helper: false,
+		maxHeight: null,
+		maxWidth: null,
+		minHeight: 10,
+		minWidth: 10,
+		zIndex: 1000
+	},
+	_create: function() {
+
+		var self = this, o = this.options;
+		this.element.addClass("ui-resizable");
+
+		$.extend(this, {
+			_aspectRatio: !!(o.aspectRatio),
+			aspectRatio: o.aspectRatio,
+			originalElement: this.element,
+			_proportionallyResizeElements: [],
+			_helper: o.helper || o.ghost || o.animate ? o.helper || 'ui-resizable-helper' : null
+		});
+
+		//Wrap the element if it cannot hold child nodes
+		if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)) {
+
+			//Create a wrapper element and set the wrapper to the new current internal element
+			this.element.wrap(
+				$('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({
+					position: this.element.css('position'),
+					width: this.element.outerWidth(),
+					height: this.element.outerHeight(),
+					top: this.element.css('top'),
+					left: this.element.css('left')
+				})
+			);
+
+			//Overwrite the original this.element
+			this.element = this.element.parent().data(
+				"resizable", this.element.data('resizable')
+			);
+
+			this.elementIsWrapper = true;
+
+			//Move margins to the wrapper
+			this.element.css({ marginLeft: this.originalElement.css("marginLeft"), marginTop: this.originalElement.css("marginTop"), marginRight: this.originalElement.css("marginRight"), marginBottom: this.originalElement.css("marginBottom") });
+			this.originalElement.css({ marginLeft: 0, marginTop: 0, marginRight: 0, marginBottom: 0});
+
+			//Prevent Safari textarea resize
+			this.originalResizeStyle = this.originalElement.css('resize');
+			this.originalElement.css('resize', 'none');
+
+			//Push the actual element to our proportionallyResize internal array
+			this._proportionallyResizeElements.push(this.originalElement.css({ position: 'static', zoom: 1, display: 'block' }));
+
+			// avoid IE jump (hard set the margin)
+			this.originalElement.css({ margin: this.originalElement.css('margin') });
+
+			// fix handlers offset
+			this._proportionallyResize();
+
+		}
+
+		this.handles = o.handles || (!$('.ui-resizable-handle', this.element).length ? "e,s,se" : { n: '.ui-resizable-n', e: '.ui-resizable-e', s: '.ui-resizable-s', w: '.ui-resizable-w', se: '.ui-resizable-se', sw: '.ui-resizable-sw', ne: '.ui-resizable-ne', nw: '.ui-resizable-nw' });
+		if(this.handles.constructor == String) {
+
+			if(this.handles == 'all') this.handles = 'n,e,s,w,se,sw,ne,nw';
+			var n = this.handles.split(","); this.handles = {};
+
+			for(var i = 0; i < n.length; i++) {
+
+				var handle = $.trim(n[i]), hname = 'ui-resizable-'+handle;
+				var axis = $('<div class="ui-resizable-handle ' + hname + '"></div>');
+
+				// increase zIndex of sw, se, ne, nw axis
+				//TODO : this modifies original option
+				if(/sw|se|ne|nw/.test(handle)) axis.css({ zIndex: ++o.zIndex });
+
+				//TODO : What's going on here?
+				if ('se' == handle) {
+					axis.addClass('ui-icon ui-icon-gripsmall-diagonal-se');
+				};
+
+				//Insert into internal handles object and append to element
+				this.handles[handle] = '.ui-resizable-'+handle;
+				this.element.append(axis);
+			}
+
+		}
+
+		this._renderAxis = function(target) {
+
+			target = target || this.element;
+
+			for(var i in this.handles) {
+
+				if(this.handles[i].constructor == String)
+					this.handles[i] = $(this.handles[i], this.element).show();
+
+				//Apply pad to wrapper element, needed to fix axis position (textarea, inputs, scrolls)
+				if (this.elementIsWrapper && this.originalElement[0].nodeName.match(/textarea|input|select|button/i)) {
+
+					var axis = $(this.handles[i], this.element), padWrapper = 0;
+
+					//Checking the correct pad and border
+					padWrapper = /sw|ne|nw|se|n|s/.test(i) ? axis.outerHeight() : axis.outerWidth();
+
+					//The padding type i have to apply...
+					var padPos = [ 'padding',
+						/ne|nw|n/.test(i) ? 'Top' :
+						/se|sw|s/.test(i) ? 'Bottom' :
+						/^e$/.test(i) ? 'Right' : 'Left' ].join("");
+
+					target.css(padPos, padWrapper);
+
+					this._proportionallyResize();
+
+				}
+
+				//TODO: What's that good for? There's not anything to be executed left
+				if(!$(this.handles[i]).length)
+					continue;
+
+			}
+		};
+
+		//TODO: make renderAxis a prototype function
+		this._renderAxis(this.element);
+
+		this._handles = $('.ui-resizable-handle', this.element)
+			.disableSelection();
+
+		//Matching axis name
+		this._handles.mouseover(function() {
+			if (!self.resizing) {
+				if (this.className)
+					var axis = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);
+				//Axis, default = se
+				self.axis = axis && axis[1] ? axis[1] : 'se';
+			}
+		});
+
+		//If we want to auto hide the elements
+		if (o.autoHide) {
+			this._handles.hide();
+			$(this.element)
+				.addClass("ui-resizable-autohide")
+				.hover(function() {
+					if (o.disabled) return;
+					$(this).removeClass("ui-resizable-autohide");
+					self._handles.show();
+				},
+				function(){
+					if (o.disabled) return;
+					if (!self.resizing) {
+						$(this).addClass("ui-resizable-autohide");
+						self._handles.hide();
+					}
+				});
+		}
+
+		//Initialize the mouse interaction
+		this._mouseInit();
+
+	},
+
+	destroy: function() {
+
+		this._mouseDestroy();
+
+		var _destroy = function(exp) {
+			$(exp).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing")
+				.removeData("resizable").unbind(".resizable").find('.ui-resizable-handle').remove();
+		};
+
+		//TODO: Unwrap at same DOM position
+		if (this.elementIsWrapper) {
+			_destroy(this.element);
+			var wrapper = this.element;
+			wrapper.after(
+				this.originalElement.css({
+					position: wrapper.css('position'),
+					width: wrapper.outerWidth(),
+					height: wrapper.outerHeight(),
+					top: wrapper.css('top'),
+					left: wrapper.css('left')
+				})
+			).remove();
+		}
+
+		this.originalElement.css('resize', this.originalResizeStyle);
+		_destroy(this.originalElement);
+
+		return this;
+	},
+
+	_mouseCapture: function(event) {
+		var handle = false;
+		for (var i in this.handles) {
+			if ($(this.handles[i])[0] == event.target) {
+				handle = true;
+			}
+		}
+
+		return !this.options.disabled && handle;
+	},
+
+	_mouseStart: function(event) {
+
+		var o = this.options, iniPos = this.element.position(), el = this.element;
+
+		this.resizing = true;
+		this.documentScroll = { top: $(document).scrollTop(), left: $(document).scrollLeft() };
+
+		// bugfix for http://dev.jquery.com/ticket/1749
+		if (el.is('.ui-draggable') || (/absolute/).test(el.css('position'))) {
+			el.css({ position: 'absolute', top: iniPos.top, left: iniPos.left });
+		}
+
+		this._renderProxy();
+
+		var curleft = num(this.helper.css('left')), curtop = num(this.helper.css('top'));
+
+		if (o.containment) {
+			curleft += $(o.containment).scrollLeft() || 0;
+			curtop += $(o.containment).scrollTop() || 0;
+		}
+
+		//Store needed variables
+		this.offset = this.helper.offset();
+		this.position = { left: curleft, top: curtop };
+		this.size = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() };
+		this.originalSize = this._helper ? { width: el.outerWidth(), height: el.outerHeight() } : { width: el.width(), height: el.height() };
+		this.originalPosition = { left: curleft, top: curtop };
+		this.sizeDiff = { width: el.outerWidth() - el.width(), height: el.outerHeight() - el.height() };
+		this.originalMousePosition = { left: event.pageX, top: event.pageY };
+
+		//Aspect Ratio
+		this.aspectRatio = (typeof o.aspectRatio == 'number') ? o.aspectRatio : ((this.originalSize.width / this.originalSize.height) || 1);
+
+	    var cursor = $('.ui-resizable-' + this.axis).css('cursor');
+	    $('body').css('cursor', cursor == 'auto' ? this.axis + '-resize' : cursor);
+
+		el.addClass("ui-resizable-resizing");
+		this._propagate("start", event);
+		return true;
+	},
+
+	_mouseDrag: function(event) {
+
+		//Increase performance, avoid regex
+		var el = this.helper, o = this.options, props = {},
+			self = this, smp = this.originalMousePosition, a = this.axis;
+
+		var dx = (event.pageX-smp.left)||0, dy = (event.pageY-smp.top)||0;
+		var trigger = this._change[a];
+		if (!trigger) return false;
+
+		// Calculate the attrs that will be change
+		var data = trigger.apply(this, [event, dx, dy]), ie6 = $.browser.msie && $.browser.version < 7, csdif = this.sizeDiff;
+
+		// Put this in the mouseDrag handler since the user can start pressing shift while resizing
+		this._updateVirtualBoundaries(event.shiftKey);
+		if (this._aspectRatio || event.shiftKey)
+			data = this._updateRatio(data, event);
+
+		data = this._respectSize(data, event);
+
+		// plugins callbacks need to be called first
+		this._propagate("resize", event);
+
+		el.css({
+			top: this.position.top + "px", left: this.position.left + "px",
+			width: this.size.width + "px", height: this.size.height + "px"
+		});
+
+		if (!this._helper && this._proportionallyResizeElements.length)
+			this._proportionallyResize();
+
+		this._updateCache(data);
+
+		// calling the user callback at the end
+		this._trigger('resize', event, this.ui());
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+
+		this.resizing = false;
+		var o = this.options, self = this;
+
+		if(this._helper) {
+			var pr = this._proportionallyResizeElements, ista = pr.length && (/textarea/i).test(pr[0].nodeName),
+				soffseth = ista && $.ui.hasScroll(pr[0], 'left') /* TODO - jump height */ ? 0 : self.sizeDiff.height,
+				soffsetw = ista ? 0 : self.sizeDiff.width;
+
+			var s = { width: (self.helper.width()  - soffsetw), height: (self.helper.height() - soffseth) },
+				left = (parseInt(self.element.css('left'), 10) + (self.position.left - self.originalPosition.left)) || null,
+				top = (parseInt(self.element.css('top'), 10) + (self.position.top - self.originalPosition.top)) || null;
+
+			if (!o.animate)
+				this.element.css($.extend(s, { top: top, left: left }));
+
+			self.helper.height(self.size.height);
+			self.helper.width(self.size.width);
+
+			if (this._helper && !o.animate) this._proportionallyResize();
+		}
+
+		$('body').css('cursor', 'auto');
+
+		this.element.removeClass("ui-resizable-resizing");
+
+		this._propagate("stop", event);
+
+		if (this._helper) this.helper.remove();
+		return false;
+
+	},
+
+    _updateVirtualBoundaries: function(forceAspectRatio) {
+        var o = this.options, pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b;
+
+        b = {
+            minWidth: isNumber(o.minWidth) ? o.minWidth : 0,
+            maxWidth: isNumber(o.maxWidth) ? o.maxWidth : Infinity,
+            minHeight: isNumber(o.minHeight) ? o.minHeight : 0,
+            maxHeight: isNumber(o.maxHeight) ? o.maxHeight : Infinity
+        };
+
+        if(this._aspectRatio || forceAspectRatio) {
+            // We want to create an enclosing box whose aspect ration is the requested one
+            // First, compute the "projected" size for each dimension based on the aspect ratio and other dimension
+            pMinWidth = b.minHeight * this.aspectRatio;
+            pMinHeight = b.minWidth / this.aspectRatio;
+            pMaxWidth = b.maxHeight * this.aspectRatio;
+            pMaxHeight = b.maxWidth / this.aspectRatio;
+
+            if(pMinWidth > b.minWidth) b.minWidth = pMinWidth;
+            if(pMinHeight > b.minHeight) b.minHeight = pMinHeight;
+            if(pMaxWidth < b.maxWidth) b.maxWidth = pMaxWidth;
+            if(pMaxHeight < b.maxHeight) b.maxHeight = pMaxHeight;
+        }
+        this._vBoundaries = b;
+    },
+
+	_updateCache: function(data) {
+		var o = this.options;
+		this.offset = this.helper.offset();
+		if (isNumber(data.left)) this.position.left = data.left;
+		if (isNumber(data.top)) this.position.top = data.top;
+		if (isNumber(data.height)) this.size.height = data.height;
+		if (isNumber(data.width)) this.size.width = data.width;
+	},
+
+	_updateRatio: function(data, event) {
+
+		var o = this.options, cpos = this.position, csize = this.size, a = this.axis;
+
+		if (isNumber(data.height)) data.width = (data.height * this.aspectRatio);
+		else if (isNumber(data.width)) data.height = (data.width / this.aspectRatio);
+
+		if (a == 'sw') {
+			data.left = cpos.left + (csize.width - data.width);
+			data.top = null;
+		}
+		if (a == 'nw') {
+			data.top = cpos.top + (csize.height - data.height);
+			data.left = cpos.left + (csize.width - data.width);
+		}
+
+		return data;
+	},
+
+	_respectSize: function(data, event) {
+
+		var el = this.helper, o = this._vBoundaries, pRatio = this._aspectRatio || event.shiftKey, a = this.axis,
+				ismaxw = isNumber(data.width) && o.maxWidth && (o.maxWidth < data.width), ismaxh = isNumber(data.height) && o.maxHeight && (o.maxHeight < data.height),
+					isminw = isNumber(data.width) && o.minWidth && (o.minWidth > data.width), isminh = isNumber(data.height) && o.minHeight && (o.minHeight > data.height);
+
+		if (isminw) data.width = o.minWidth;
+		if (isminh) data.height = o.minHeight;
+		if (ismaxw) data.width = o.maxWidth;
+		if (ismaxh) data.height = o.maxHeight;
+
+		var dw = this.originalPosition.left + this.originalSize.width, dh = this.position.top + this.size.height;
+		var cw = /sw|nw|w/.test(a), ch = /nw|ne|n/.test(a);
+
+		if (isminw && cw) data.left = dw - o.minWidth;
+		if (ismaxw && cw) data.left = dw - o.maxWidth;
+		if (isminh && ch)	data.top = dh - o.minHeight;
+		if (ismaxh && ch)	data.top = dh - o.maxHeight;
+
+		// fixing jump error on top/left - bug #2330
+		var isNotwh = !data.width && !data.height;
+		if (isNotwh && !data.left && data.top) data.top = null;
+		else if (isNotwh && !data.top && data.left) data.left = null;
+
+		return data;
+	},
+
+	_proportionallyResize: function() {
+
+		var o = this.options;
+		if (!this._proportionallyResizeElements.length) return;
+		var element = this.helper || this.element;
+
+		for (var i=0; i < this._proportionallyResizeElements.length; i++) {
+
+			var prel = this._proportionallyResizeElements[i];
+
+			if (!this.borderDif) {
+				var b = [prel.css('borderTopWidth'), prel.css('borderRightWidth'), prel.css('borderBottomWidth'), prel.css('borderLeftWidth')],
+					p = [prel.css('paddingTop'), prel.css('paddingRight'), prel.css('paddingBottom'), prel.css('paddingLeft')];
+
+				this.borderDif = $.map(b, function(v, i) {
+					var border = parseInt(v,10)||0, padding = parseInt(p[i],10)||0;
+					return border + padding;
+				});
+			}
+
+			if ($.browser.msie && !(!($(element).is(':hidden') || $(element).parents(':hidden').length)))
+				continue;
+
+			prel.css({
+				height: (element.height() - this.borderDif[0] - this.borderDif[2]) || 0,
+				width: (element.width() - this.borderDif[1] - this.borderDif[3]) || 0
+			});
+
+		};
+
+	},
+
+	_renderProxy: function() {
+
+		var el = this.element, o = this.options;
+		this.elementOffset = el.offset();
+
+		if(this._helper) {
+
+			this.helper = this.helper || $('<div style="overflow:hidden;"></div>');
+
+			// fix ie6 offset TODO: This seems broken
+			var ie6 = $.browser.msie && $.browser.version < 7, ie6offset = (ie6 ? 1 : 0),
+			pxyoffset = ( ie6 ? 2 : -1 );
+
+			this.helper.addClass(this._helper).css({
+				width: this.element.outerWidth() + pxyoffset,
+				height: this.element.outerHeight() + pxyoffset,
+				position: 'absolute',
+				left: this.elementOffset.left - ie6offset +'px',
+				top: this.elementOffset.top - ie6offset +'px',
+				zIndex: ++o.zIndex //TODO: Don't modify option
+			});
+
+			this.helper
+				.appendTo("body")
+				.disableSelection();
+
+		} else {
+			this.helper = this.element;
+		}
+
+	},
+
+	_change: {
+		e: function(event, dx, dy) {
+			return { width: this.originalSize.width + dx };
+		},
+		w: function(event, dx, dy) {
+			var o = this.options, cs = this.originalSize, sp = this.originalPosition;
+			return { left: sp.left + dx, width: cs.width - dx };
+		},
+		n: function(event, dx, dy) {
+			var o = this.options, cs = this.originalSize, sp = this.originalPosition;
+			return { top: sp.top + dy, height: cs.height - dy };
+		},
+		s: function(event, dx, dy) {
+			return { height: this.originalSize.height + dy };
+		},
+		se: function(event, dx, dy) {
+			return $.extend(this._change.s.apply(this, arguments), this._change.e.apply(this, [event, dx, dy]));
+		},
+		sw: function(event, dx, dy) {
+			return $.extend(this._change.s.apply(this, arguments), this._change.w.apply(this, [event, dx, dy]));
+		},
+		ne: function(event, dx, dy) {
+			return $.extend(this._change.n.apply(this, arguments), this._change.e.apply(this, [event, dx, dy]));
+		},
+		nw: function(event, dx, dy) {
+			return $.extend(this._change.n.apply(this, arguments), this._change.w.apply(this, [event, dx, dy]));
+		}
+	},
+
+	_propagate: function(n, event) {
+		$.ui.plugin.call(this, n, [event, this.ui()]);
+		(n != "resize" && this._trigger(n, event, this.ui()));
+	},
+
+	plugins: {},
+
+	ui: function() {
+		return {
+			originalElement: this.originalElement,
+			element: this.element,
+			helper: this.helper,
+			position: this.position,
+			size: this.size,
+			originalSize: this.originalSize,
+			originalPosition: this.originalPosition
+		};
+	}
+
+});
+
+$.extend($.ui.resizable, {
+	version: "@VERSION"
+});
+
+/*
+ * Resizable Extensions
+ */
+
+$.ui.plugin.add("resizable", "alsoResize", {
+
+	start: function (event, ui) {
+		var self = $(this).data("resizable"), o = self.options;
+
+		var _store = function (exp) {
+			$(exp).each(function() {
+				var el = $(this);
+				el.data("resizable-alsoresize", {
+					width: parseInt(el.width(), 10), height: parseInt(el.height(), 10),
+					left: parseInt(el.css('left'), 10), top: parseInt(el.css('top'), 10)
+				});
+			});
+		};
+
+		if (typeof(o.alsoResize) == 'object' && !o.alsoResize.parentNode) {
+			if (o.alsoResize.length) { o.alsoResize = o.alsoResize[0]; _store(o.alsoResize); }
+			else { $.each(o.alsoResize, function (exp) { _store(exp); }); }
+		}else{
+			_store(o.alsoResize);
+		}
+	},
+
+	resize: function (event, ui) {
+		var self = $(this).data("resizable"), o = self.options, os = self.originalSize, op = self.originalPosition;
+
+		var delta = {
+			height: (self.size.height - os.height) || 0, width: (self.size.width - os.width) || 0,
+			top: (self.position.top - op.top) || 0, left: (self.position.left - op.left) || 0
+		},
+
+		_alsoResize = function (exp, c) {
+			$(exp).each(function() {
+				var el = $(this), start = $(this).data("resizable-alsoresize"), style = {}, 
+					css = c && c.length ? c : el.parents(ui.originalElement[0]).length ? ['width', 'height'] : ['width', 'height', 'top', 'left'];
+
+				$.each(css, function (i, prop) {
+					var sum = (start[prop]||0) + (delta[prop]||0);
+					if (sum && sum >= 0)
+						style[prop] = sum || null;
+				});
+
+				el.css(style);
+			});
+		};
+
+		if (typeof(o.alsoResize) == 'object' && !o.alsoResize.nodeType) {
+			$.each(o.alsoResize, function (exp, c) { _alsoResize(exp, c); });
+		}else{
+			_alsoResize(o.alsoResize);
+		}
+	},
+
+	stop: function (event, ui) {
+		$(this).removeData("resizable-alsoresize");
+	}
+});
+
+$.ui.plugin.add("resizable", "animate", {
+
+	stop: function(event, ui) {
+		var self = $(this).data("resizable"), o = self.options;
+
+		var pr = self._proportionallyResizeElements, ista = pr.length && (/textarea/i).test(pr[0].nodeName),
+					soffseth = ista && $.ui.hasScroll(pr[0], 'left') /* TODO - jump height */ ? 0 : self.sizeDiff.height,
+						soffsetw = ista ? 0 : self.sizeDiff.width;
+
+		var style = { width: (self.size.width - soffsetw), height: (self.size.height - soffseth) },
+					left = (parseInt(self.element.css('left'), 10) + (self.position.left - self.originalPosition.left)) || null,
+						top = (parseInt(self.element.css('top'), 10) + (self.position.top - self.originalPosition.top)) || null;
+
+		self.element.animate(
+			$.extend(style, top && left ? { top: top, left: left } : {}), {
+				duration: o.animateDuration,
+				easing: o.animateEasing,
+				step: function() {
+
+					var data = {
+						width: parseInt(self.element.css('width'), 10),
+						height: parseInt(self.element.css('height'), 10),
+						top: parseInt(self.element.css('top'), 10),
+						left: parseInt(self.element.css('left'), 10)
+					};
+
+					if (pr && pr.length) $(pr[0]).css({ width: data.width, height: data.height });
+
+					// propagating resize, and updating values for each animation step
+					self._updateCache(data);
+					self._propagate("resize", event);
+
+				}
+			}
+		);
+	}
+
+});
+
+$.ui.plugin.add("resizable", "containment", {
+
+	start: function(event, ui) {
+		var self = $(this).data("resizable"), o = self.options, el = self.element;
+		var oc = o.containment,	ce = (oc instanceof $) ? oc.get(0) : (/parent/.test(oc)) ? el.parent().get(0) : oc;
+		if (!ce) return;
+
+		self.containerElement = $(ce);
+
+		if (/document/.test(oc) || oc == document) {
+			self.containerOffset = { left: 0, top: 0 };
+			self.containerPosition = { left: 0, top: 0 };
+
+			self.parentData = {
+				element: $(document), left: 0, top: 0,
+				width: $(document).width(), height: $(document).height() || document.body.parentNode.scrollHeight
+			};
+		}
+
+		// i'm a node, so compute top, left, right, bottom
+		else {
+			var element = $(ce), p = [];
+			$([ "Top", "Right", "Left", "Bottom" ]).each(function(i, name) { p[i] = num(element.css("padding" + name)); });
+
+			self.containerOffset = element.offset();
+			self.containerPosition = element.position();
+			self.containerSize = { height: (element.innerHeight() - p[3]), width: (element.innerWidth() - p[1]) };
+
+			var co = self.containerOffset, ch = self.containerSize.height,	cw = self.containerSize.width,
+						width = ($.ui.hasScroll(ce, "left") ? ce.scrollWidth : cw ), height = ($.ui.hasScroll(ce) ? ce.scrollHeight : ch);
+
+			self.parentData = {
+				element: ce, left: co.left, top: co.top, width: width, height: height
+			};
+		}
+	},
+
+	resize: function(event, ui) {
+		var self = $(this).data("resizable"), o = self.options,
+				ps = self.containerSize, co = self.containerOffset, cs = self.size, cp = self.position,
+				pRatio = self._aspectRatio || event.shiftKey, cop = { top:0, left:0 }, ce = self.containerElement;
+
+		if (ce[0] != document && (/static/).test(ce.css('position'))) cop = co;
+
+		if (cp.left < (self._helper ? co.left : 0)) {
+			self.size.width = self.size.width + (self._helper ? (self.position.left - co.left) : (self.position.left - cop.left));
+			if (pRatio) self.size.height = self.size.width / o.aspectRatio;
+			self.position.left = o.helper ? co.left : 0;
+		}
+
+		if (cp.top < (self._helper ? co.top : 0)) {
+			self.size.height = self.size.height + (self._helper ? (self.position.top - co.top) : self.position.top);
+			if (pRatio) self.size.width = self.size.height * o.aspectRatio;
+			self.position.top = self._helper ? co.top : 0;
+		}
+
+		self.offset.left = self.parentData.left+self.position.left;
+		self.offset.top = self.parentData.top+self.position.top;
+
+		var woset = Math.abs( (self._helper ? self.offset.left - cop.left : (self.offset.left - cop.left)) + self.sizeDiff.width ),
+					hoset = Math.abs( (self._helper ? self.offset.top - cop.top : (self.offset.top - co.top)) + self.sizeDiff.height );
+
+		var isParent = self.containerElement.get(0) == self.element.parent().get(0),
+		    isOffsetRelative = /relative|absolute/.test(self.containerElement.css('position'));
+
+		if(isParent && isOffsetRelative) woset -= self.parentData.left;
+
+		if (woset + self.size.width >= self.parentData.width) {
+			self.size.width = self.parentData.width - woset;
+			if (pRatio) self.size.height = self.size.width / self.aspectRatio;
+		}
+
+		if (hoset + self.size.height >= self.parentData.height) {
+			self.size.height = self.parentData.height - hoset;
+			if (pRatio) self.size.width = self.size.height * self.aspectRatio;
+		}
+	},
+
+	stop: function(event, ui){
+		var self = $(this).data("resizable"), o = self.options, cp = self.position,
+				co = self.containerOffset, cop = self.containerPosition, ce = self.containerElement;
+
+		var helper = $(self.helper), ho = helper.offset(), w = helper.outerWidth() - self.sizeDiff.width, h = helper.outerHeight() - self.sizeDiff.height;
+
+		if (self._helper && !o.animate && (/relative/).test(ce.css('position')))
+			$(this).css({ left: ho.left - cop.left - co.left, width: w, height: h });
+
+		if (self._helper && !o.animate && (/static/).test(ce.css('position')))
+			$(this).css({ left: ho.left - cop.left - co.left, width: w, height: h });
+
+	}
+});
+
+$.ui.plugin.add("resizable", "ghost", {
+
+	start: function(event, ui) {
+
+		var self = $(this).data("resizable"), o = self.options, cs = self.size;
+
+		self.ghost = self.originalElement.clone();
+		self.ghost
+			.css({ opacity: .25, display: 'block', position: 'relative', height: cs.height, width: cs.width, margin: 0, left: 0, top: 0 })
+			.addClass('ui-resizable-ghost')
+			.addClass(typeof o.ghost == 'string' ? o.ghost : '');
+
+		self.ghost.appendTo(self.helper);
+
+	},
+
+	resize: function(event, ui){
+		var self = $(this).data("resizable"), o = self.options;
+		if (self.ghost) self.ghost.css({ position: 'relative', height: self.size.height, width: self.size.width });
+	},
+
+	stop: function(event, ui){
+		var self = $(this).data("resizable"), o = self.options;
+		if (self.ghost && self.helper) self.helper.get(0).removeChild(self.ghost.get(0));
+	}
+
+});
+
+$.ui.plugin.add("resizable", "grid", {
+
+	resize: function(event, ui) {
+		var self = $(this).data("resizable"), o = self.options, cs = self.size, os = self.originalSize, op = self.originalPosition, a = self.axis, ratio = o._aspectRatio || event.shiftKey;
+		o.grid = typeof o.grid == "number" ? [o.grid, o.grid] : o.grid;
+		var ox = Math.round((cs.width - os.width) / (o.grid[0]||1)) * (o.grid[0]||1), oy = Math.round((cs.height - os.height) / (o.grid[1]||1)) * (o.grid[1]||1);
+
+		if (/^(se|s|e)$/.test(a)) {
+			self.size.width = os.width + ox;
+			self.size.height = os.height + oy;
+		}
+		else if (/^(ne)$/.test(a)) {
+			self.size.width = os.width + ox;
+			self.size.height = os.height + oy;
+			self.position.top = op.top - oy;
+		}
+		else if (/^(sw)$/.test(a)) {
+			self.size.width = os.width + ox;
+			self.size.height = os.height + oy;
+			self.position.left = op.left - ox;
+		}
+		else {
+			self.size.width = os.width + ox;
+			self.size.height = os.height + oy;
+			self.position.top = op.top - oy;
+			self.position.left = op.left - ox;
+		}
+	}
+
+});
+
+var num = function(v) {
+	return parseInt(v, 10) || 0;
+};
+
+var isNumber = function(value) {
+	return !isNaN(parseInt(value, 10));
+};
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/selectable.js b/client/apollo/jslib/jqueryui/selectable.js
new file mode 100644
index 0000000..5df10e7
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/selectable.js
@@ -0,0 +1,270 @@
+define(['jquery','./core','./mouse','./widget'], function (jQuery) {
+/*
+ * jQuery UI Selectable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectables
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget("ui.selectable", $.ui.mouse, {
+	options: {
+		appendTo: 'body',
+		autoRefresh: true,
+		distance: 0,
+		filter: '*',
+		tolerance: 'touch'
+	},
+	_create: function() {
+		var self = this;
+
+		this.element.addClass("ui-selectable");
+
+		this.dragged = false;
+
+		// cache selectee children based on filter
+		var selectees;
+		this.refresh = function() {
+			selectees = $(self.options.filter, self.element[0]);
+			selectees.addClass("ui-selectee");
+			selectees.each(function() {
+				var $this = $(this);
+				var pos = $this.offset();
+				$.data(this, "selectable-item", {
+					element: this,
+					$element: $this,
+					left: pos.left,
+					top: pos.top,
+					right: pos.left + $this.outerWidth(),
+					bottom: pos.top + $this.outerHeight(),
+					startselected: false,
+					selected: $this.hasClass('ui-selected'),
+					selecting: $this.hasClass('ui-selecting'),
+					unselecting: $this.hasClass('ui-unselecting')
+				});
+			});
+		};
+		this.refresh();
+
+		this.selectees = selectees.addClass("ui-selectee");
+
+		this._mouseInit();
+
+		this.helper = $("<div class='ui-selectable-helper'></div>");
+	},
+
+	destroy: function() {
+		this.selectees
+			.removeClass("ui-selectee")
+			.removeData("selectable-item");
+		this.element
+			.removeClass("ui-selectable ui-selectable-disabled")
+			.removeData("selectable")
+			.unbind(".selectable");
+		this._mouseDestroy();
+
+		return this;
+	},
+
+	_mouseStart: function(event) {
+		var self = this;
+
+		this.opos = [event.pageX, event.pageY];
+
+		if (this.options.disabled)
+			return;
+
+		var options = this.options;
+
+		this.selectees = $(options.filter, this.element[0]);
+
+		this._trigger("start", event);
+
+		$(options.appendTo).append(this.helper);
+		// position helper (lasso)
+		this.helper.css({
+			"left": event.clientX,
+			"top": event.clientY,
+			"width": 0,
+			"height": 0
+		});
+
+		if (options.autoRefresh) {
+			this.refresh();
+		}
+
+		this.selectees.filter('.ui-selected').each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.startselected = true;
+			if (!event.metaKey && !event.ctrlKey) {
+				selectee.$element.removeClass('ui-selected');
+				selectee.selected = false;
+				selectee.$element.addClass('ui-unselecting');
+				selectee.unselecting = true;
+				// selectable UNSELECTING callback
+				self._trigger("unselecting", event, {
+					unselecting: selectee.element
+				});
+			}
+		});
+
+		$(event.target).parents().andSelf().each(function() {
+			var selectee = $.data(this, "selectable-item");
+			if (selectee) {
+				var doSelect = (!event.metaKey && !event.ctrlKey) || !selectee.$element.hasClass('ui-selected');
+				selectee.$element
+					.removeClass(doSelect ? "ui-unselecting" : "ui-selected")
+					.addClass(doSelect ? "ui-selecting" : "ui-unselecting");
+				selectee.unselecting = !doSelect;
+				selectee.selecting = doSelect;
+				selectee.selected = doSelect;
+				// selectable (UN)SELECTING callback
+				if (doSelect) {
+					self._trigger("selecting", event, {
+						selecting: selectee.element
+					});
+				} else {
+					self._trigger("unselecting", event, {
+						unselecting: selectee.element
+					});
+				}
+				return false;
+			}
+		});
+
+	},
+
+	_mouseDrag: function(event) {
+		var self = this;
+		this.dragged = true;
+
+		if (this.options.disabled)
+			return;
+
+		var options = this.options;
+
+		var x1 = this.opos[0], y1 = this.opos[1], x2 = event.pageX, y2 = event.pageY;
+		if (x1 > x2) { var tmp = x2; x2 = x1; x1 = tmp; }
+		if (y1 > y2) { var tmp = y2; y2 = y1; y1 = tmp; }
+		this.helper.css({left: x1, top: y1, width: x2-x1, height: y2-y1});
+
+		this.selectees.each(function() {
+			var selectee = $.data(this, "selectable-item");
+			//prevent helper from being selected if appendTo: selectable
+			if (!selectee || selectee.element == self.element[0])
+				return;
+			var hit = false;
+			if (options.tolerance == 'touch') {
+				hit = ( !(selectee.left > x2 || selectee.right < x1 || selectee.top > y2 || selectee.bottom < y1) );
+			} else if (options.tolerance == 'fit') {
+				hit = (selectee.left > x1 && selectee.right < x2 && selectee.top > y1 && selectee.bottom < y2);
+			}
+
+			if (hit) {
+				// SELECT
+				if (selectee.selected) {
+					selectee.$element.removeClass('ui-selected');
+					selectee.selected = false;
+				}
+				if (selectee.unselecting) {
+					selectee.$element.removeClass('ui-unselecting');
+					selectee.unselecting = false;
+				}
+				if (!selectee.selecting) {
+					selectee.$element.addClass('ui-selecting');
+					selectee.selecting = true;
+					// selectable SELECTING callback
+					self._trigger("selecting", event, {
+						selecting: selectee.element
+					});
+				}
+			} else {
+				// UNSELECT
+				if (selectee.selecting) {
+					if ((event.metaKey || event.ctrlKey) && selectee.startselected) {
+						selectee.$element.removeClass('ui-selecting');
+						selectee.selecting = false;
+						selectee.$element.addClass('ui-selected');
+						selectee.selected = true;
+					} else {
+						selectee.$element.removeClass('ui-selecting');
+						selectee.selecting = false;
+						if (selectee.startselected) {
+							selectee.$element.addClass('ui-unselecting');
+							selectee.unselecting = true;
+						}
+						// selectable UNSELECTING callback
+						self._trigger("unselecting", event, {
+							unselecting: selectee.element
+						});
+					}
+				}
+				if (selectee.selected) {
+					if (!event.metaKey && !event.ctrlKey && !selectee.startselected) {
+						selectee.$element.removeClass('ui-selected');
+						selectee.selected = false;
+
+						selectee.$element.addClass('ui-unselecting');
+						selectee.unselecting = true;
+						// selectable UNSELECTING callback
+						self._trigger("unselecting", event, {
+							unselecting: selectee.element
+						});
+					}
+				}
+			}
+		});
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+		var self = this;
+
+		this.dragged = false;
+
+		var options = this.options;
+
+		$('.ui-unselecting', this.element[0]).each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.$element.removeClass('ui-unselecting');
+			selectee.unselecting = false;
+			selectee.startselected = false;
+			self._trigger("unselected", event, {
+				unselected: selectee.element
+			});
+		});
+		$('.ui-selecting', this.element[0]).each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.$element.removeClass('ui-selecting').addClass('ui-selected');
+			selectee.selecting = false;
+			selectee.selected = true;
+			selectee.startselected = true;
+			self._trigger("selected", event, {
+				selected: selectee.element
+			});
+		});
+		this._trigger("stop", event);
+
+		this.helper.remove();
+
+		return false;
+	}
+
+});
+
+$.extend($.ui.selectable, {
+	version: "@VERSION"
+});
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/slider.js b/client/apollo/jslib/jqueryui/slider.js
new file mode 100644
index 0000000..ddd8c2f
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/slider.js
@@ -0,0 +1,665 @@
+define(['jquery','./core','./mouse','./widget'], function (jQuery) {
+/*
+ * jQuery UI Slider @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+// number of pages in a slider
+// (how many times can you page up/down to go through the whole range)
+var numPages = 5;
+
+$.widget( "ui.slider", $.ui.mouse, {
+
+	widgetEventPrefix: "slide",
+
+	options: {
+		animate: false,
+		distance: 0,
+		max: 100,
+		min: 0,
+		orientation: "horizontal",
+		range: false,
+		step: 1,
+		value: 0,
+		values: null
+	},
+
+	_create: function() {
+		var self = this,
+			o = this.options,
+			existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ),
+			handle = "<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",
+			handleCount = ( o.values && o.values.length ) || 1,
+			handles = [];
+
+		this._keySliding = false;
+		this._mouseSliding = false;
+		this._animateOff = true;
+		this._handleIndex = null;
+		this._detectOrientation();
+		this._mouseInit();
+
+		this.element
+			.addClass( "ui-slider" +
+				" ui-slider-" + this.orientation +
+				" ui-widget" +
+				" ui-widget-content" +
+				" ui-corner-all" +
+				( o.disabled ? " ui-slider-disabled ui-disabled" : "" ) );
+
+		this.range = $([]);
+
+		if ( o.range ) {
+			if ( o.range === true ) {
+				if ( !o.values ) {
+					o.values = [ this._valueMin(), this._valueMin() ];
+				}
+				if ( o.values.length && o.values.length !== 2 ) {
+					o.values = [ o.values[0], o.values[0] ];
+				}
+			}
+
+			this.range = $( "<div></div>" )
+				.appendTo( this.element )
+				.addClass( "ui-slider-range" +
+				// note: this isn't the most fittingly semantic framework class for this element,
+				// but worked best visually with a variety of themes
+				" ui-widget-header" + 
+				( ( o.range === "min" || o.range === "max" ) ? " ui-slider-range-" + o.range : "" ) );
+		}
+
+		for ( var i = existingHandles.length; i < handleCount; i += 1 ) {
+			handles.push( handle );
+		}
+
+		this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( self.element ) );
+
+		this.handle = this.handles.eq( 0 );
+
+		this.handles.add( this.range ).filter( "a" )
+			.click(function( event ) {
+				event.preventDefault();
+			})
+			.hover(function() {
+				if ( !o.disabled ) {
+					$( this ).addClass( "ui-state-hover" );
+				}
+			}, function() {
+				$( this ).removeClass( "ui-state-hover" );
+			})
+			.focus(function() {
+				if ( !o.disabled ) {
+					$( ".ui-slider .ui-state-focus" ).removeClass( "ui-state-focus" );
+					$( this ).addClass( "ui-state-focus" );
+				} else {
+					$( this ).blur();
+				}
+			})
+			.blur(function() {
+				$( this ).removeClass( "ui-state-focus" );
+			});
+
+		this.handles.each(function( i ) {
+			$( this ).data( "index.ui-slider-handle", i );
+		});
+
+		this.handles
+			.keydown(function( event ) {
+				var index = $( this ).data( "index.ui-slider-handle" ),
+					allowed,
+					curVal,
+					newVal,
+					step;
+	
+				if ( self.options.disabled ) {
+					return;
+				}
+	
+				switch ( event.keyCode ) {
+					case $.ui.keyCode.HOME:
+					case $.ui.keyCode.END:
+					case $.ui.keyCode.PAGE_UP:
+					case $.ui.keyCode.PAGE_DOWN:
+					case $.ui.keyCode.UP:
+					case $.ui.keyCode.RIGHT:
+					case $.ui.keyCode.DOWN:
+					case $.ui.keyCode.LEFT:
+						event.preventDefault();
+						if ( !self._keySliding ) {
+							self._keySliding = true;
+							$( this ).addClass( "ui-state-active" );
+							allowed = self._start( event, index );
+							if ( allowed === false ) {
+								return;
+							}
+						}
+						break;
+				}
+	
+				step = self.options.step;
+				if ( self.options.values && self.options.values.length ) {
+					curVal = newVal = self.values( index );
+				} else {
+					curVal = newVal = self.value();
+				}
+	
+				switch ( event.keyCode ) {
+					case $.ui.keyCode.HOME:
+						newVal = self._valueMin();
+						break;
+					case $.ui.keyCode.END:
+						newVal = self._valueMax();
+						break;
+					case $.ui.keyCode.PAGE_UP:
+						newVal = self._trimAlignValue( curVal + ( (self._valueMax() - self._valueMin()) / numPages ) );
+						break;
+					case $.ui.keyCode.PAGE_DOWN:
+						newVal = self._trimAlignValue( curVal - ( (self._valueMax() - self._valueMin()) / numPages ) );
+						break;
+					case $.ui.keyCode.UP:
+					case $.ui.keyCode.RIGHT:
+						if ( curVal === self._valueMax() ) {
+							return;
+						}
+						newVal = self._trimAlignValue( curVal + step );
+						break;
+					case $.ui.keyCode.DOWN:
+					case $.ui.keyCode.LEFT:
+						if ( curVal === self._valueMin() ) {
+							return;
+						}
+						newVal = self._trimAlignValue( curVal - step );
+						break;
+				}
+	
+				self._slide( event, index, newVal );
+			})
+			.keyup(function( event ) {
+				var index = $( this ).data( "index.ui-slider-handle" );
+	
+				if ( self._keySliding ) {
+					self._keySliding = false;
+					self._stop( event, index );
+					self._change( event, index );
+					$( this ).removeClass( "ui-state-active" );
+				}
+	
+			});
+
+		this._refreshValue();
+
+		this._animateOff = false;
+	},
+
+	destroy: function() {
+		this.handles.remove();
+		this.range.remove();
+
+		this.element
+			.removeClass( "ui-slider" +
+				" ui-slider-horizontal" +
+				" ui-slider-vertical" +
+				" ui-slider-disabled" +
+				" ui-widget" +
+				" ui-widget-content" +
+				" ui-corner-all" )
+			.removeData( "slider" )
+			.unbind( ".slider" );
+
+		this._mouseDestroy();
+
+		return this;
+	},
+
+	_mouseCapture: function( event ) {
+		var o = this.options,
+			position,
+			normValue,
+			distance,
+			closestHandle,
+			self,
+			index,
+			allowed,
+			offset,
+			mouseOverHandle;
+
+		if ( o.disabled ) {
+			return false;
+		}
+
+		this.elementSize = {
+			width: this.element.outerWidth(),
+			height: this.element.outerHeight()
+		};
+		this.elementOffset = this.element.offset();
+
+		position = { x: event.pageX, y: event.pageY };
+		normValue = this._normValueFromMouse( position );
+		distance = this._valueMax() - this._valueMin() + 1;
+		self = this;
+		this.handles.each(function( i ) {
+			var thisDistance = Math.abs( normValue - self.values(i) );
+			if ( distance > thisDistance ) {
+				distance = thisDistance;
+				closestHandle = $( this );
+				index = i;
+			}
+		});
+
+		// workaround for bug #3736 (if both handles of a range are at 0,
+		// the first is always used as the one with least distance,
+		// and moving it is obviously prevented by preventing negative ranges)
+		if( o.range === true && this.values(1) === o.min ) {
+			index += 1;
+			closestHandle = $( this.handles[index] );
+		}
+
+		allowed = this._start( event, index );
+		if ( allowed === false ) {
+			return false;
+		}
+		this._mouseSliding = true;
+
+		self._handleIndex = index;
+
+		closestHandle
+			.addClass( "ui-state-active" )
+			.focus();
+		
+		offset = closestHandle.offset();
+		mouseOverHandle = !$( event.target ).parents().andSelf().is( ".ui-slider-handle" );
+		this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : {
+			left: event.pageX - offset.left - ( closestHandle.width() / 2 ),
+			top: event.pageY - offset.top -
+				( closestHandle.height() / 2 ) -
+				( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) -
+				( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) +
+				( parseInt( closestHandle.css("marginTop"), 10 ) || 0)
+		};
+
+		if ( !this.handles.hasClass( "ui-state-hover" ) ) {
+			this._slide( event, index, normValue );
+		}
+		this._animateOff = true;
+		return true;
+	},
+
+	_mouseStart: function( event ) {
+		return true;
+	},
+
+	_mouseDrag: function( event ) {
+		var position = { x: event.pageX, y: event.pageY },
+			normValue = this._normValueFromMouse( position );
+		
+		this._slide( event, this._handleIndex, normValue );
+
+		return false;
+	},
+
+	_mouseStop: function( event ) {
+		this.handles.removeClass( "ui-state-active" );
+		this._mouseSliding = false;
+
+		this._stop( event, this._handleIndex );
+		this._change( event, this._handleIndex );
+
+		this._handleIndex = null;
+		this._clickOffset = null;
+		this._animateOff = false;
+
+		return false;
+	},
+	
+	_detectOrientation: function() {
+		this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal";
+	},
+
+	_normValueFromMouse: function( position ) {
+		var pixelTotal,
+			pixelMouse,
+			percentMouse,
+			valueTotal,
+			valueMouse;
+
+		if ( this.orientation === "horizontal" ) {
+			pixelTotal = this.elementSize.width;
+			pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 );
+		} else {
+			pixelTotal = this.elementSize.height;
+			pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 );
+		}
+
+		percentMouse = ( pixelMouse / pixelTotal );
+		if ( percentMouse > 1 ) {
+			percentMouse = 1;
+		}
+		if ( percentMouse < 0 ) {
+			percentMouse = 0;
+		}
+		if ( this.orientation === "vertical" ) {
+			percentMouse = 1 - percentMouse;
+		}
+
+		valueTotal = this._valueMax() - this._valueMin();
+		valueMouse = this._valueMin() + percentMouse * valueTotal;
+
+		return this._trimAlignValue( valueMouse );
+	},
+
+	_start: function( event, index ) {
+		var uiHash = {
+			handle: this.handles[ index ],
+			value: this.value()
+		};
+		if ( this.options.values && this.options.values.length ) {
+			uiHash.value = this.values( index );
+			uiHash.values = this.values();
+		}
+		return this._trigger( "start", event, uiHash );
+	},
+
+	_slide: function( event, index, newVal ) {
+		var otherVal,
+			newValues,
+			allowed;
+
+		if ( this.options.values && this.options.values.length ) {
+			otherVal = this.values( index ? 0 : 1 );
+
+			if ( ( this.options.values.length === 2 && this.options.range === true ) && 
+					( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) )
+				) {
+				newVal = otherVal;
+			}
+
+			if ( newVal !== this.values( index ) ) {
+				newValues = this.values();
+				newValues[ index ] = newVal;
+				// A slide can be canceled by returning false from the slide callback
+				allowed = this._trigger( "slide", event, {
+					handle: this.handles[ index ],
+					value: newVal,
+					values: newValues
+				} );
+				otherVal = this.values( index ? 0 : 1 );
+				if ( allowed !== false ) {
+					this.values( index, newVal, true );
+				}
+			}
+		} else {
+			if ( newVal !== this.value() ) {
+				// A slide can be canceled by returning false from the slide callback
+				allowed = this._trigger( "slide", event, {
+					handle: this.handles[ index ],
+					value: newVal
+				} );
+				if ( allowed !== false ) {
+					this.value( newVal );
+				}
+			}
+		}
+	},
+
+	_stop: function( event, index ) {
+		var uiHash = {
+			handle: this.handles[ index ],
+			value: this.value()
+		};
+		if ( this.options.values && this.options.values.length ) {
+			uiHash.value = this.values( index );
+			uiHash.values = this.values();
+		}
+
+		this._trigger( "stop", event, uiHash );
+	},
+
+	_change: function( event, index ) {
+		if ( !this._keySliding && !this._mouseSliding ) {
+			var uiHash = {
+				handle: this.handles[ index ],
+				value: this.value()
+			};
+			if ( this.options.values && this.options.values.length ) {
+				uiHash.value = this.values( index );
+				uiHash.values = this.values();
+			}
+
+			this._trigger( "change", event, uiHash );
+		}
+	},
+
+	value: function( newValue ) {
+		if ( arguments.length ) {
+			this.options.value = this._trimAlignValue( newValue );
+			this._refreshValue();
+			this._change( null, 0 );
+			return;
+		}
+
+		return this._value();
+	},
+
+	values: function( index, newValue ) {
+		var vals,
+			newValues,
+			i;
+
+		if ( arguments.length > 1 ) {
+			this.options.values[ index ] = this._trimAlignValue( newValue );
+			this._refreshValue();
+			this._change( null, index );
+			return;
+		}
+
+		if ( arguments.length ) {
+			if ( $.isArray( arguments[ 0 ] ) ) {
+				vals = this.options.values;
+				newValues = arguments[ 0 ];
+				for ( i = 0; i < vals.length; i += 1 ) {
+					vals[ i ] = this._trimAlignValue( newValues[ i ] );
+					this._change( null, i );
+				}
+				this._refreshValue();
+			} else {
+				if ( this.options.values && this.options.values.length ) {
+					return this._values( index );
+				} else {
+					return this.value();
+				}
+			}
+		} else {
+			return this._values();
+		}
+	},
+
+	_setOption: function( key, value ) {
+		var i,
+			valsLength = 0;
+
+		if ( $.isArray( this.options.values ) ) {
+			valsLength = this.options.values.length;
+		}
+
+		$.Widget.prototype._setOption.apply( this, arguments );
+
+		switch ( key ) {
+			case "disabled":
+				if ( value ) {
+					this.handles.filter( ".ui-state-focus" ).blur();
+					this.handles.removeClass( "ui-state-hover" );
+					this.handles.propAttr( "disabled", true );
+					this.element.addClass( "ui-disabled" );
+				} else {
+					this.handles.propAttr( "disabled", false );
+					this.element.removeClass( "ui-disabled" );
+				}
+				break;
+			case "orientation":
+				this._detectOrientation();
+				this.element
+					.removeClass( "ui-slider-horizontal ui-slider-vertical" )
+					.addClass( "ui-slider-" + this.orientation );
+				this._refreshValue();
+				break;
+			case "value":
+				this._animateOff = true;
+				this._refreshValue();
+				this._change( null, 0 );
+				this._animateOff = false;
+				break;
+			case "values":
+				this._animateOff = true;
+				this._refreshValue();
+				for ( i = 0; i < valsLength; i += 1 ) {
+					this._change( null, i );
+				}
+				this._animateOff = false;
+				break;
+		}
+	},
+
+	//internal value getter
+	// _value() returns value trimmed by min and max, aligned by step
+	_value: function() {
+		var val = this.options.value;
+		val = this._trimAlignValue( val );
+
+		return val;
+	},
+
+	//internal values getter
+	// _values() returns array of values trimmed by min and max, aligned by step
+	// _values( index ) returns single value trimmed by min and max, aligned by step
+	_values: function( index ) {
+		var val,
+			vals,
+			i;
+
+		if ( arguments.length ) {
+			val = this.options.values[ index ];
+			val = this._trimAlignValue( val );
+
+			return val;
+		} else {
+			// .slice() creates a copy of the array
+			// this copy gets trimmed by min and max and then returned
+			vals = this.options.values.slice();
+			for ( i = 0; i < vals.length; i+= 1) {
+				vals[ i ] = this._trimAlignValue( vals[ i ] );
+			}
+
+			return vals;
+		}
+	},
+	
+	// returns the step-aligned value that val is closest to, between (inclusive) min and max
+	_trimAlignValue: function( val ) {
+		if ( val <= this._valueMin() ) {
+			return this._valueMin();
+		}
+		if ( val >= this._valueMax() ) {
+			return this._valueMax();
+		}
+		var step = ( this.options.step > 0 ) ? this.options.step : 1,
+			valModStep = (val - this._valueMin()) % step,
+			alignValue = val - valModStep;
+
+		if ( Math.abs(valModStep) * 2 >= step ) {
+			alignValue += ( valModStep > 0 ) ? step : ( -step );
+		}
+
+		// Since JavaScript has problems with large floats, round
+		// the final value to 5 digits after the decimal point (see #4124)
+		return parseFloat( alignValue.toFixed(5) );
+	},
+
+	_valueMin: function() {
+		return this.options.min;
+	},
+
+	_valueMax: function() {
+		return this.options.max;
+	},
+	
+	_refreshValue: function() {
+		var oRange = this.options.range,
+			o = this.options,
+			self = this,
+			animate = ( !this._animateOff ) ? o.animate : false,
+			valPercent,
+			_set = {},
+			lastValPercent,
+			value,
+			valueMin,
+			valueMax;
+
+		if ( this.options.values && this.options.values.length ) {
+			this.handles.each(function( i, j ) {
+				valPercent = ( self.values(i) - self._valueMin() ) / ( self._valueMax() - self._valueMin() ) * 100;
+				_set[ self.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
+				$( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
+				if ( self.options.range === true ) {
+					if ( self.orientation === "horizontal" ) {
+						if ( i === 0 ) {
+							self.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate );
+						}
+						if ( i === 1 ) {
+							self.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } );
+						}
+					} else {
+						if ( i === 0 ) {
+							self.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate );
+						}
+						if ( i === 1 ) {
+							self.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } );
+						}
+					}
+				}
+				lastValPercent = valPercent;
+			});
+		} else {
+			value = this.value();
+			valueMin = this._valueMin();
+			valueMax = this._valueMax();
+			valPercent = ( valueMax !== valueMin ) ?
+					( value - valueMin ) / ( valueMax - valueMin ) * 100 :
+					0;
+			_set[ self.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
+			this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
+
+			if ( oRange === "min" && this.orientation === "horizontal" ) {
+				this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate );
+			}
+			if ( oRange === "max" && this.orientation === "horizontal" ) {
+				this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } );
+			}
+			if ( oRange === "min" && this.orientation === "vertical" ) {
+				this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate );
+			}
+			if ( oRange === "max" && this.orientation === "vertical" ) {
+				this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } );
+			}
+		}
+	}
+
+});
+
+$.extend( $.ui.slider, {
+	version: "@VERSION"
+});
+
+}(jQuery));
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/sortable.js b/client/apollo/jslib/jqueryui/sortable.js
new file mode 100644
index 0000000..13ffb16
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/sortable.js
@@ -0,0 +1,1084 @@
+define(['jquery','./core','./mouse','./widget'], function (jQuery) {
+/*
+ * jQuery UI Sortable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Sortables
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+$.widget("ui.sortable", $.ui.mouse, {
+	widgetEventPrefix: "sort",
+	ready: false,
+	options: {
+		appendTo: "parent",
+		axis: false,
+		connectWith: false,
+		containment: false,
+		cursor: 'auto',
+		cursorAt: false,
+		dropOnEmpty: true,
+		forcePlaceholderSize: false,
+		forceHelperSize: false,
+		grid: false,
+		handle: false,
+		helper: "original",
+		items: '> *',
+		opacity: false,
+		placeholder: false,
+		revert: false,
+		scroll: true,
+		scrollSensitivity: 20,
+		scrollSpeed: 20,
+		scope: "default",
+		tolerance: "intersect",
+		zIndex: 1000
+	},
+	_create: function() {
+
+		var o = this.options;
+		this.containerCache = {};
+		this.element.addClass("ui-sortable");
+
+		//Get the items
+		this.refresh();
+
+		//Let's determine if the items are being displayed horizontally
+		this.floating = this.items.length ? o.axis === 'x' || (/left|right/).test(this.items[0].item.css('float')) || (/inline|table-cell/).test(this.items[0].item.css('display')) : false;
+
+		//Let's determine the parent's offset
+		this.offset = this.element.offset();
+
+		//Initialize mouse events for interaction
+		this._mouseInit();
+		
+		//We're ready to go
+		this.ready = true
+
+	},
+
+	destroy: function() {
+		$.Widget.prototype.destroy.call( this );
+		this.element
+			.removeClass("ui-sortable ui-sortable-disabled");
+		this._mouseDestroy();
+
+		for ( var i = this.items.length - 1; i >= 0; i-- )
+			this.items[i].item.removeData(this.widgetName + "-item");
+
+		return this;
+	},
+
+	_setOption: function(key, value){
+		if ( key === "disabled" ) {
+			this.options[ key ] = value;
+	
+			this.widget()
+				[ value ? "addClass" : "removeClass"]( "ui-sortable-disabled" );
+		} else {
+			// Don't call widget base _setOption for disable as it adds ui-state-disabled class
+			$.Widget.prototype._setOption.apply(this, arguments);
+		}
+	},
+
+	_mouseCapture: function(event, overrideHandle) {
+		var that = this;
+
+		if (this.reverting) {
+			return false;
+		}
+
+		if(this.options.disabled || this.options.type == 'static') return false;
+
+		//We have to refresh the items data once first
+		this._refreshItems(event);
+
+		//Find out if the clicked node (or one of its parents) is a actual item in this.items
+		var currentItem = null, self = this, nodes = $(event.target).parents().each(function() {
+			if($.data(this, that.widgetName + '-item') == self) {
+				currentItem = $(this);
+				return false;
+			}
+		});
+		if($.data(event.target, that.widgetName + '-item') == self) currentItem = $(event.target);
+
+		if(!currentItem) return false;
+		if(this.options.handle && !overrideHandle) {
+			var validHandle = false;
+
+			$(this.options.handle, currentItem).find("*").andSelf().each(function() { if(this == event.target) validHandle = true; });
+			if(!validHandle) return false;
+		}
+
+		this.currentItem = currentItem;
+		this._removeCurrentsFromItems();
+		return true;
+
+	},
+
+	_mouseStart: function(event, overrideHandle, noActivation) {
+
+		var o = this.options, self = this;
+		this.currentContainer = this;
+
+		//We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
+		this.refreshPositions();
+
+		//Create and append the visible helper
+		this.helper = this._createHelper(event);
+
+		//Cache the helper size
+		this._cacheHelperProportions();
+
+		/*
+		 * - Position generation -
+		 * This block generates everything position related - it's the core of draggables.
+		 */
+
+		//Cache the margins of the original element
+		this._cacheMargins();
+
+		//Get the next scrolling parent
+		this.scrollParent = this.helper.scrollParent();
+
+		//The element's absolute position on the page minus margins
+		this.offset = this.currentItem.offset();
+		this.offset = {
+			top: this.offset.top - this.margins.top,
+			left: this.offset.left - this.margins.left
+		};
+
+		// Only after we got the offset, we can change the helper's position to absolute
+		// TODO: Still need to figure out a way to make relative sorting possible
+		this.helper.css("position", "absolute");
+		this.cssPosition = this.helper.css("position");
+
+		$.extend(this.offset, {
+			click: { //Where the click happened, relative to the element
+				left: event.pageX - this.offset.left,
+				top: event.pageY - this.offset.top
+			},
+			parent: this._getParentOffset(),
+			relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
+		});
+
+		//Generate the original position
+		this.originalPosition = this._generatePosition(event);
+		this.originalPageX = event.pageX;
+		this.originalPageY = event.pageY;
+
+		//Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
+		(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+		//Cache the former DOM position
+		this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
+
+		//If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
+		if(this.helper[0] != this.currentItem[0]) {
+			this.currentItem.hide();
+		}
+
+		//Create the placeholder
+		this._createPlaceholder();
+
+		//Set a containment if given in the options
+		if(o.containment)
+			this._setContainment();
+
+		if(o.cursor) { // cursor option
+			if ($('body').css("cursor")) this._storedCursor = $('body').css("cursor");
+			$('body').css("cursor", o.cursor);
+		}
+
+		if(o.opacity) { // opacity option
+			if (this.helper.css("opacity")) this._storedOpacity = this.helper.css("opacity");
+			this.helper.css("opacity", o.opacity);
+		}
+
+		if(o.zIndex) { // zIndex option
+			if (this.helper.css("zIndex")) this._storedZIndex = this.helper.css("zIndex");
+			this.helper.css("zIndex", o.zIndex);
+		}
+
+		//Prepare scrolling
+		if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML')
+			this.overflowOffset = this.scrollParent.offset();
+
+		//Call callbacks
+		this._trigger("start", event, this._uiHash());
+
+		//Recache the helper size
+		if(!this._preserveHelperProportions)
+			this._cacheHelperProportions();
+
+
+		//Post 'activate' events to possible containers
+		if(!noActivation) {
+			 for (var i = this.containers.length - 1; i >= 0; i--) { this.containers[i]._trigger("activate", event, self._uiHash(this)); }
+		}
+
+		//Prepare possible droppables
+		if($.ui.ddmanager)
+			$.ui.ddmanager.current = this;
+
+		if ($.ui.ddmanager && !o.dropBehaviour)
+			$.ui.ddmanager.prepareOffsets(this, event);
+
+		this.dragging = true;
+
+		this.helper.addClass("ui-sortable-helper");
+		this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+		return true;
+
+	},
+
+	_mouseDrag: function(event) {
+
+		//Compute the helpers position
+		this.position = this._generatePosition(event);
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		if (!this.lastPositionAbs) {
+			this.lastPositionAbs = this.positionAbs;
+		}
+
+		//Do scrolling
+		if(this.options.scroll) {
+			var o = this.options, scrolled = false;
+			if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') {
+
+				if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity)
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
+				else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity)
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
+
+				if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity)
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
+				else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity)
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
+
+			} else {
+
+				if(event.pageY - $(document).scrollTop() < o.scrollSensitivity)
+					scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+				else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity)
+					scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+
+				if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity)
+					scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+				else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity)
+					scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+
+			}
+
+			if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour)
+				$.ui.ddmanager.prepareOffsets(this, event);
+		}
+
+		//Regenerate the absolute position used for position checks
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		//Set the helper position
+		if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px';
+		if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px';
+
+		//Rearrange
+		for (var i = this.items.length - 1; i >= 0; i--) {
+
+			//Cache variables and intersection, continue if no intersection
+			var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item);
+			if (!intersection) continue;
+
+			if(itemElement != this.currentItem[0] //cannot intersect with itself
+				&&	this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before
+				&&	!$.ui.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked
+				&& (this.options.type == 'semi-dynamic' ? !$.ui.contains(this.element[0], itemElement) : true)
+				//&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
+			) {
+
+				this.direction = intersection == 1 ? "down" : "up";
+
+				if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) {
+					this._rearrange(event, item);
+				} else {
+					break;
+				}
+
+				this._trigger("change", event, this._uiHash());
+				break;
+			}
+		}
+
+		//Post events to containers
+		this._contactContainers(event);
+
+		//Interconnect with droppables
+		if($.ui.ddmanager) $.ui.ddmanager.drag(this, event);
+
+		//Call callbacks
+		this._trigger('sort', event, this._uiHash());
+
+		this.lastPositionAbs = this.positionAbs;
+		return false;
+
+	},
+
+	_mouseStop: function(event, noPropagation) {
+
+		if(!event) return;
+
+		//If we are using droppables, inform the manager about the drop
+		if ($.ui.ddmanager && !this.options.dropBehaviour)
+			$.ui.ddmanager.drop(this, event);
+
+		if(this.options.revert) {
+			var self = this;
+			var cur = self.placeholder.offset();
+
+			self.reverting = true;
+
+			$(this.helper).animate({
+				left: cur.left - this.offset.parent.left - self.margins.left + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollLeft),
+				top: cur.top - this.offset.parent.top - self.margins.top + (this.offsetParent[0] == document.body ? 0 : this.offsetParent[0].scrollTop)
+			}, parseInt(this.options.revert, 10) || 500, function() {
+				self._clear(event);
+			});
+		} else {
+			this._clear(event, noPropagation);
+		}
+
+		return false;
+
+	},
+
+	cancel: function() {
+
+		var self = this;
+
+		if(this.dragging) {
+
+			this._mouseUp({ target: null });
+
+			if(this.options.helper == "original")
+				this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+			else
+				this.currentItem.show();
+
+			//Post deactivating events to containers
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				this.containers[i]._trigger("deactivate", null, self._uiHash(this));
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", null, self._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+
+		if (this.placeholder) {
+			//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+			if(this.placeholder[0].parentNode) this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+			if(this.options.helper != "original" && this.helper && this.helper[0].parentNode) this.helper.remove();
+
+			$.extend(this, {
+				helper: null,
+				dragging: false,
+				reverting: false,
+				_noFinalSort: null
+			});
+
+			if(this.domPosition.prev) {
+				$(this.domPosition.prev).after(this.currentItem);
+			} else {
+				$(this.domPosition.parent).prepend(this.currentItem);
+			}
+		}
+
+		return this;
+
+	},
+
+	serialize: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected);
+		var str = []; o = o || {};
+
+		$(items).each(function() {
+			var res = ($(o.item || this).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
+			if(res) str.push((o.key || res[1]+'[]')+'='+(o.key && o.expression ? res[1] : res[2]));
+		});
+
+		if(!str.length && o.key) {
+			str.push(o.key + '=');
+		}
+
+		return str.join('&');
+
+	},
+
+	toArray: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected);
+		var ret = []; o = o || {};
+
+		items.each(function() { ret.push($(o.item || this).attr(o.attribute || 'id') || ''); });
+		return ret;
+
+	},
+
+	/* Be careful with the following core functions */
+	_intersectsWith: function(item) {
+
+		var x1 = this.positionAbs.left,
+			x2 = x1 + this.helperProportions.width,
+			y1 = this.positionAbs.top,
+			y2 = y1 + this.helperProportions.height;
+
+		var l = item.left,
+			r = l + item.width,
+			t = item.top,
+			b = t + item.height;
+
+		var dyClick = this.offset.click.top,
+			dxClick = this.offset.click.left;
+
+		var isOverElement = (y1 + dyClick) > t && (y1 + dyClick) < b && (x1 + dxClick) > l && (x1 + dxClick) < r;
+
+		if(	   this.options.tolerance == "pointer"
+			|| this.options.forcePointerForContainers
+			|| (this.options.tolerance != "pointer" && this.helperProportions[this.floating ? 'width' : 'height'] > item[this.floating ? 'width' : 'height'])
+		) {
+			return isOverElement;
+		} else {
+
+			return (l < x1 + (this.helperProportions.width / 2) // Right Half
+				&& x2 - (this.helperProportions.width / 2) < r // Left Half
+				&& t < y1 + (this.helperProportions.height / 2) // Bottom Half
+				&& y2 - (this.helperProportions.height / 2) < b ); // Top Half
+
+		}
+	},
+
+	_intersectsWithPointer: function(item) {
+
+		var isOverElementHeight = $.ui.isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
+			isOverElementWidth = $.ui.isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),
+			isOverElement = isOverElementHeight && isOverElementWidth,
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (!isOverElement)
+			return false;
+
+		return this.floating ?
+			( ((horizontalDirection && horizontalDirection == "right") || verticalDirection == "down") ? 2 : 1 )
+			: ( verticalDirection && (verticalDirection == "down" ? 2 : 1) );
+
+	},
+
+	_intersectsWithSides: function(item) {
+
+		var isOverBottomHalf = $.ui.isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height),
+			isOverRightHalf = $.ui.isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width),
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (this.floating && horizontalDirection) {
+			return ((horizontalDirection == "right" && isOverRightHalf) || (horizontalDirection == "left" && !isOverRightHalf));
+		} else {
+			return verticalDirection && ((verticalDirection == "down" && isOverBottomHalf) || (verticalDirection == "up" && !isOverBottomHalf));
+		}
+
+	},
+
+	_getDragVerticalDirection: function() {
+		var delta = this.positionAbs.top - this.lastPositionAbs.top;
+		return delta != 0 && (delta > 0 ? "down" : "up");
+	},
+
+	_getDragHorizontalDirection: function() {
+		var delta = this.positionAbs.left - this.lastPositionAbs.left;
+		return delta != 0 && (delta > 0 ? "right" : "left");
+	},
+
+	refresh: function(event) {
+		this._refreshItems(event);
+		this.refreshPositions();
+		return this;
+	},
+
+	_connectWith: function() {
+		var options = this.options;
+		return options.connectWith.constructor == String
+			? [options.connectWith]
+			: options.connectWith;
+	},
+	
+	_getItemsAsjQuery: function(connected) {
+
+		var self = this;
+		var items = [];
+		var queries = [];
+		var connectWith = this._connectWith();
+
+		if(connectWith && connected) {
+			for (var i = connectWith.length - 1; i >= 0; i--){
+				var cur = $(connectWith[i]);
+				for (var j = cur.length - 1; j >= 0; j--){
+					var inst = $.data(cur[j], this.widgetName);
+					if(inst && inst != this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not('.ui-sortable-placeholder'), inst]);
+					}
+				};
+			};
+		}
+
+		queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not('.ui-sortable-placeholder'), this]);
+
+		for (var i = queries.length - 1; i >= 0; i--){
+			queries[i][0].each(function() {
+				items.push(this);
+			});
+		};
+
+		return $(items);
+
+	},
+
+	_removeCurrentsFromItems: function() {
+
+		var list = this.currentItem.find(":data(" + this.widgetName + "-item)");
+
+		for (var i=0; i < this.items.length; i++) {
+
+			for (var j=0; j < list.length; j++) {
+				if(list[j] == this.items[i].item[0])
+					this.items.splice(i,1);
+			};
+
+		};
+
+	},
+
+	_refreshItems: function(event) {
+
+		this.items = [];
+		this.containers = [this];
+		var items = this.items;
+		var self = this;
+		var queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]];
+		var connectWith = this._connectWith();
+
+		if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down
+			for (var i = connectWith.length - 1; i >= 0; i--){
+				var cur = $(connectWith[i]);
+				for (var j = cur.length - 1; j >= 0; j--){
+					var inst = $.data(cur[j], this.widgetName);
+					if(inst && inst != this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]);
+						this.containers.push(inst);
+					}
+				};
+			};
+		}
+
+		for (var i = queries.length - 1; i >= 0; i--) {
+			var targetData = queries[i][1];
+			var _queries = queries[i][0];
+
+			for (var j=0, queriesLength = _queries.length; j < queriesLength; j++) {
+				var item = $(_queries[j]);
+
+				item.data(this.widgetName + '-item', targetData); // Data for target checking (mouse manager)
+
+				items.push({
+					item: item,
+					instance: targetData,
+					width: 0, height: 0,
+					left: 0, top: 0
+				});
+			};
+		};
+
+	},
+
+	refreshPositions: function(fast) {
+
+		//This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
+		if(this.offsetParent && this.helper) {
+			this.offset.parent = this._getParentOffset();
+		}
+
+		for (var i = this.items.length - 1; i >= 0; i--){
+			var item = this.items[i];
+
+			//We ignore calculating positions of all connected containers when we're not over them
+			if(item.instance != this.currentContainer && this.currentContainer && item.item[0] != this.currentItem[0])
+				continue;
+
+			var t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item;
+
+			if (!fast) {
+				item.width = t.outerWidth();
+				item.height = t.outerHeight();
+			}
+
+			var p = t.offset();
+			item.left = p.left;
+			item.top = p.top;
+		};
+
+		if(this.options.custom && this.options.custom.refreshContainers) {
+			this.options.custom.refreshContainers.call(this);
+		} else {
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				var p = this.containers[i].element.offset();
+				this.containers[i].containerCache.left = p.left;
+				this.containers[i].containerCache.top = p.top;
+				this.containers[i].containerCache.width	= this.containers[i].element.outerWidth();
+				this.containers[i].containerCache.height = this.containers[i].element.outerHeight();
+			};
+		}
+
+		return this;
+	},
+
+	_createPlaceholder: function(that) {
+
+		var self = that || this, o = self.options;
+
+		if(!o.placeholder || o.placeholder.constructor == String) {
+			var className = o.placeholder;
+			o.placeholder = {
+				element: function() {
+
+					var el = $(document.createElement(self.currentItem[0].nodeName))
+						.addClass(className || self.currentItem[0].className+" ui-sortable-placeholder")
+						.removeClass("ui-sortable-helper")[0];
+
+					if(!className)
+						el.style.visibility = "hidden";
+
+					return el;
+				},
+				update: function(container, p) {
+
+					// 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
+					// 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
+					if(className && !o.forcePlaceholderSize) return;
+
+					//If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item
+					if(!p.height()) { p.height(self.currentItem.innerHeight() - parseInt(self.currentItem.css('paddingTop')||0, 10) - parseInt(self.currentItem.css('paddingBottom')||0, 10)); };
+					if(!p.width()) { p.width(self.currentItem.innerWidth() - parseInt(self.currentItem.css('paddingLeft')||0, 10) - parseInt(self.currentItem.css('paddingRight')||0, 10)); };
+				}
+			};
+		}
+
+		//Create the placeholder
+		self.placeholder = $(o.placeholder.element.call(self.element, self.currentItem));
+
+		//Append it after the actual current item
+		self.currentItem.after(self.placeholder);
+
+		//Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
+		o.placeholder.update(self, self.placeholder);
+
+	},
+
+	_contactContainers: function(event) {
+		
+		// get innermost container that intersects with item 
+		var innermostContainer = null, innermostIndex = null;		
+		
+		
+		for (var i = this.containers.length - 1; i >= 0; i--){
+
+			// never consider a container that's located within the item itself 
+			if($.ui.contains(this.currentItem[0], this.containers[i].element[0]))
+				continue;
+
+			if(this._intersectsWith(this.containers[i].containerCache)) {
+
+				// if we've already found a container and it's more "inner" than this, then continue 
+				if(innermostContainer && $.ui.contains(this.containers[i].element[0], innermostContainer.element[0]))
+					continue;
+
+				innermostContainer = this.containers[i]; 
+				innermostIndex = i;
+					
+			} else {
+				// container doesn't intersect. trigger "out" event if necessary 
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", event, this._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+		
+		// if no intersecting containers found, return 
+		if(!innermostContainer) return; 
+
+		// move the item into the container if it's not there already
+		if(this.containers.length === 1) {
+			this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+			this.containers[innermostIndex].containerCache.over = 1;
+		} else if(this.currentContainer != this.containers[innermostIndex]) { 
+
+			//When entering a new container, we will find the item with the least distance and append our item near it 
+			var dist = 10000; var itemWithLeastDistance = null; var base = this.positionAbs[this.containers[innermostIndex].floating ? 'left' : 'top']; 
+			for (var j = this.items.length - 1; j >= 0; j--) { 
+				if(!$.ui.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) continue; 
+				var cur = this.items[j][this.containers[innermostIndex].floating ? 'left' : 'top']; 
+				if(Math.abs(cur - base) < dist) { 
+					dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; 
+				} 
+			} 
+
+			if(!itemWithLeastDistance && !this.options.dropOnEmpty) //Check if dropOnEmpty is enabled 
+				return; 
+
+			this.currentContainer = this.containers[innermostIndex]; 
+			itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); 
+			this._trigger("change", event, this._uiHash()); 
+			this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); 
+
+			//Update the placeholder 
+			this.options.placeholder.update(this.currentContainer, this.placeholder); 
+		
+			this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); 
+			this.containers[innermostIndex].containerCache.over = 1;
+		} 
+	
+		
+	},
+
+	_createHelper: function(event) {
+
+		var o = this.options;
+		var helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper == 'clone' ? this.currentItem.clone() : this.currentItem);
+
+		if(!helper.parents('body').length) //Add the helper to the DOM if that didn't happen already
+			$(o.appendTo != 'parent' ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]);
+
+		if(helper[0] == this.currentItem[0])
+			this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") };
+
+		if(helper[0].style.width == '' || o.forceHelperSize) helper.width(this.currentItem.width());
+		if(helper[0].style.height == '' || o.forceHelperSize) helper.height(this.currentItem.height());
+
+		return helper;
+
+	},
+
+	_adjustOffsetFromHelper: function(obj) {
+		if (typeof obj == 'string') {
+			obj = obj.split(' ');
+		}
+		if ($.isArray(obj)) {
+			obj = {left: +obj[0], top: +obj[1] || 0};
+		}
+		if ('left' in obj) {
+			this.offset.click.left = obj.left + this.margins.left;
+		}
+		if ('right' in obj) {
+			this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+		}
+		if ('top' in obj) {
+			this.offset.click.top = obj.top + this.margins.top;
+		}
+		if ('bottom' in obj) {
+			this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+		}
+	},
+
+	_getParentOffset: function() {
+
+
+		//Get the offsetParent and cache its position
+		this.offsetParent = this.helper.offsetParent();
+		var po = this.offsetParent.offset();
+
+		// This is a special case where we need to modify a offset calculated on start, since the following happened:
+		// 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+		// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+		//    the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+		if(this.cssPosition == 'absolute' && this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) {
+			po.left += this.scrollParent.scrollLeft();
+			po.top += this.scrollParent.scrollTop();
+		}
+
+		if((this.offsetParent[0] == document.body) //This needs to be actually done for all browsers, since pageX/pageY includes this information
+		|| (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() == 'html' && $.browser.msie)) //Ugly IE fix
+			po = { top: 0, left: 0 };
+
+		return {
+			top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0),
+			left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0)
+		};
+
+	},
+
+	_getRelativeOffset: function() {
+
+		if(this.cssPosition == "relative") {
+			var p = this.currentItem.position();
+			return {
+				top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(),
+				left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft()
+			};
+		} else {
+			return { top: 0, left: 0 };
+		}
+
+	},
+
+	_cacheMargins: function() {
+		this.margins = {
+			left: (parseInt(this.currentItem.css("marginLeft"),10) || 0),
+			top: (parseInt(this.currentItem.css("marginTop"),10) || 0)
+		};
+	},
+
+	_cacheHelperProportions: function() {
+		this.helperProportions = {
+			width: this.helper.outerWidth(),
+			height: this.helper.outerHeight()
+		};
+	},
+
+	_setContainment: function() {
+
+		var o = this.options;
+		if(o.containment == 'parent') o.containment = this.helper[0].parentNode;
+		if(o.containment == 'document' || o.containment == 'window') this.containment = [
+			0 - this.offset.relative.left - this.offset.parent.left,
+			0 - this.offset.relative.top - this.offset.parent.top,
+			$(o.containment == 'document' ? document : window).width() - this.helperProportions.width - this.margins.left,
+			($(o.containment == 'document' ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top
+		];
+
+		if(!(/^(document|window|parent)$/).test(o.containment)) {
+			var ce = $(o.containment)[0];
+			var co = $(o.containment).offset();
+			var over = ($(ce).css("overflow") != 'hidden');
+
+			this.containment = [
+				co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left,
+				co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top,
+				co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left,
+				co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top
+			];
+		}
+
+	},
+
+	_convertPositionTo: function(d, pos) {
+
+		if(!pos) pos = this.position;
+		var mod = d == "absolute" ? 1 : -1;
+		var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		return {
+			top: (
+				pos.top																	// The absolute mouse position
+				+ this.offset.relative.top * mod										// Only for relative positioned nodes: Relative offset from element to offset parent
+				+ this.offset.parent.top * mod											// The offsetParent's offset without borders (offset + border)
+				- ($.browser.safari && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod)
+			),
+			left: (
+				pos.left																// The absolute mouse position
+				+ this.offset.relative.left * mod										// Only for relative positioned nodes: Relative offset from element to offset parent
+				+ this.offset.parent.left * mod											// The offsetParent's offset without borders (offset + border)
+				- ($.browser.safari && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod)
+			)
+		};
+
+	},
+
+	_generatePosition: function(event) {
+
+		var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		// This is another very weird special case that only happens for relative elements:
+		// 1. If the css position is relative
+		// 2. and the scroll parent is the document or similar to the offset parent
+		// we have to refresh the relative offset during the scroll so there are no jumps
+		if(this.cssPosition == 'relative' && !(this.scrollParent[0] != document && this.scrollParent[0] != this.offsetParent[0])) {
+			this.offset.relative = this._getRelativeOffset();
+		}
+
+		var pageX = event.pageX;
+		var pageY = event.pageY;
+
+		/*
+		 * - Position constraining -
+		 * Constrain the position to a mix of grid, containment.
+		 */
+
+		if(this.originalPosition) { //If we are not dragging yet, we won't check for options
+
+			if(this.containment) {
+				if(event.pageX - this.offset.click.left < this.containment[0]) pageX = this.containment[0] + this.offset.click.left;
+				if(event.pageY - this.offset.click.top < this.containment[1]) pageY = this.containment[1] + this.offset.click.top;
+				if(event.pageX - this.offset.click.left > this.containment[2]) pageX = this.containment[2] + this.offset.click.left;
+				if(event.pageY - this.offset.click.top > this.containment[3]) pageY = this.containment[3] + this.offset.click.top;
+			}
+
+			if(o.grid) {
+				var top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1];
+				pageY = this.containment ? (!(top - this.offset.click.top < this.containment[1] || top - this.offset.click.top > this.containment[3]) ? top : (!(top - this.offset.click.top < this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+				var left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0];
+				pageX = this.containment ? (!(left - this.offset.click.left < this.containment[0] || left - this.offset.click.left > this.containment[2]) ? left : (!(left - this.offset.click.left < this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+			}
+
+		}
+
+		return {
+			top: (
+				pageY																// The absolute mouse position
+				- this.offset.click.top													// Click offset (relative to the element)
+				- this.offset.relative.top												// Only for relative positioned nodes: Relative offset from element to offset parent
+				- this.offset.parent.top												// The offsetParent's offset without borders (offset + border)
+				+ ($.browser.safari && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
+			),
+			left: (
+				pageX																// The absolute mouse position
+				- this.offset.click.left												// Click offset (relative to the element)
+				- this.offset.relative.left												// Only for relative positioned nodes: Relative offset from element to offset parent
+				- this.offset.parent.left												// The offsetParent's offset without borders (offset + border)
+				+ ($.browser.safari && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
+			)
+		};
+
+	},
+
+	_rearrange: function(event, i, a, hardRefresh) {
+
+		a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction == 'down' ? i.item[0] : i.item[0].nextSibling));
+
+		//Various things done here to improve the performance:
+		// 1. we create a setTimeout, that calls refreshPositions
+		// 2. on the instance, we have a counter variable, that get's higher after every append
+		// 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
+		// 4. this lets only the last addition to the timeout stack through
+		this.counter = this.counter ? ++this.counter : 1;
+		var self = this, counter = this.counter;
+
+		window.setTimeout(function() {
+			if(counter == self.counter) self.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove
+		},0);
+
+	},
+
+	_clear: function(event, noPropagation) {
+
+		this.reverting = false;
+		// We delay all events that have to be triggered to after the point where the placeholder has been removed and
+		// everything else normalized again
+		var delayedTriggers = [], self = this;
+
+		// We first have to update the dom position of the actual currentItem
+		// Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
+		if(!this._noFinalSort && this.currentItem.parent().length) this.placeholder.before(this.currentItem);
+		this._noFinalSort = null;
+
+		if(this.helper[0] == this.currentItem[0]) {
+			for(var i in this._storedCSS) {
+				if(this._storedCSS[i] == 'auto' || this._storedCSS[i] == 'static') this._storedCSS[i] = '';
+			}
+			this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+		} else {
+			this.currentItem.show();
+		}
+
+		if(this.fromOutside && !noPropagation) delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); });
+		if((this.fromOutside || this.domPosition.prev != this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent != this.currentItem.parent()[0]) && !noPropagation) delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed
+		if(!$.ui.contains(this.element[0], this.currentItem[0])) { //Node was moved out of the current element
+			if(!noPropagation) delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				if($.ui.contains(this.containers[i].element[0], this.currentItem[0]) && !noPropagation) {
+					delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); };  }).call(this, this.containers[i]));
+					delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this));  }; }).call(this, this.containers[i]));
+				}
+			};
+		};
+
+		//Post events to containers
+		for (var i = this.containers.length - 1; i >= 0; i--){
+			if(!noPropagation) delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); };  }).call(this, this.containers[i]));
+			if(this.containers[i].containerCache.over) {
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); };  }).call(this, this.containers[i]));
+				this.containers[i].containerCache.over = 0;
+			}
+		}
+
+		//Do what was originally in plugins
+		if(this._storedCursor) $('body').css("cursor", this._storedCursor); //Reset cursor
+		if(this._storedOpacity) this.helper.css("opacity", this._storedOpacity); //Reset opacity
+		if(this._storedZIndex) this.helper.css("zIndex", this._storedZIndex == 'auto' ? '' : this._storedZIndex); //Reset z-index
+
+		this.dragging = false;
+		if(this.cancelHelperRemoval) {
+			if(!noPropagation) {
+				this._trigger("beforeStop", event, this._uiHash());
+				for (var i=0; i < delayedTriggers.length; i++) { delayedTriggers[i].call(this, event); }; //Trigger all delayed events
+				this._trigger("stop", event, this._uiHash());
+			}
+			return false;
+		}
+
+		if(!noPropagation) this._trigger("beforeStop", event, this._uiHash());
+
+		//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+		this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+
+		if(this.helper[0] != this.currentItem[0]) this.helper.remove(); this.helper = null;
+
+		if(!noPropagation) {
+			for (var i=0; i < delayedTriggers.length; i++) { delayedTriggers[i].call(this, event); }; //Trigger all delayed events
+			this._trigger("stop", event, this._uiHash());
+		}
+
+		this.fromOutside = false;
+		return true;
+
+	},
+
+	_trigger: function() {
+		if ($.Widget.prototype._trigger.apply(this, arguments) === false) {
+			this.cancel();
+		}
+	},
+
+	_uiHash: function(inst) {
+		var self = inst || this;
+		return {
+			helper: self.helper,
+			placeholder: self.placeholder || $([]),
+			position: self.position,
+			originalPosition: self.originalPosition,
+			offset: self.positionAbs,
+			item: self.currentItem,
+			sender: inst ? inst.element : null
+		};
+	}
+
+});
+
+$.extend($.ui.sortable, {
+	version: "@VERSION"
+});
+
+})(jQuery);
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/tabs.js b/client/apollo/jslib/jqueryui/tabs.js
new file mode 100644
index 0000000..78442d4
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/tabs.js
@@ -0,0 +1,761 @@
+define(['jquery','./core','./widget'], function (jQuery) {
+/*
+ * jQuery UI Tabs @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var tabId = 0,
+	listId = 0;
+
+function getNextTabId() {
+	return ++tabId;
+}
+
+function getNextListId() {
+	return ++listId;
+}
+
+$.widget( "ui.tabs", {
+	options: {
+		add: null,
+		ajaxOptions: null,
+		cache: false,
+		cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
+		collapsible: false,
+		disable: null,
+		disabled: [],
+		enable: null,
+		event: "click",
+		fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
+		idPrefix: "ui-tabs-",
+		load: null,
+		panelTemplate: "<div></div>",
+		remove: null,
+		select: null,
+		show: null,
+		spinner: "<em>Loading…</em>",
+		tabTemplate: "<li><a href='#{href}'><span>#{label}</span></a></li>"
+	},
+
+	_create: function() {
+		this._tabify( true );
+	},
+
+	_setOption: function( key, value ) {
+		if ( key == "selected" ) {
+			if (this.options.collapsible && value == this.options.selected ) {
+				return;
+			}
+			this.select( value );
+		} else {
+			this.options[ key ] = value;
+			this._tabify();
+		}
+	},
+
+	_tabId: function( a ) {
+		return a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) ||
+			this.options.idPrefix + getNextTabId();
+	},
+
+	_sanitizeSelector: function( hash ) {
+		// we need this because an id may contain a ":"
+		return hash.replace( /:/g, "\\:" );
+	},
+
+	_cookie: function() {
+		var cookie = this.cookie ||
+			( this.cookie = this.options.cookie.name || "ui-tabs-" + getNextListId() );
+		return $.cookie.apply( null, [ cookie ].concat( $.makeArray( arguments ) ) );
+	},
+
+	_ui: function( tab, panel ) {
+		return {
+			tab: tab,
+			panel: panel,
+			index: this.anchors.index( tab )
+		};
+	},
+
+	_cleanup: function() {
+		// restore all former loading tabs labels
+		this.lis.filter( ".ui-state-processing" )
+			.removeClass( "ui-state-processing" )
+			.find( "span:data(label.tabs)" )
+				.each(function() {
+					var el = $( this );
+					el.html( el.data( "label.tabs" ) ).removeData( "label.tabs" );
+				});
+	},
+
+	_tabify: function( init ) {
+		var self = this,
+			o = this.options,
+			fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash
+
+		this.list = this.element.find( "ol,ul" ).eq( 0 );
+		this.lis = $( " > li:has(a[href])", this.list );
+		this.anchors = this.lis.map(function() {
+			return $( "a", this )[ 0 ];
+		});
+		this.panels = $( [] );
+
+		this.anchors.each(function( i, a ) {
+			var href = $( a ).attr( "href" );
+			// For dynamically created HTML that contains a hash as href IE < 8 expands
+			// such href to the full page url with hash and then misinterprets tab as ajax.
+			// Same consideration applies for an added tab with a fragment identifier
+			// since a[href=#fragment-identifier] does unexpectedly not match.
+			// Thus normalize href attribute...
+			var hrefBase = href.split( "#" )[ 0 ],
+				baseEl;
+			if ( hrefBase && ( hrefBase === location.toString().split( "#" )[ 0 ] ||
+					( baseEl = $( "base" )[ 0 ]) && hrefBase === baseEl.href ) ) {
+				href = a.hash;
+				a.href = href;
+			}
+
+			// inline tab
+			if ( fragmentId.test( href ) ) {
+				self.panels = self.panels.add( self.element.find( self._sanitizeSelector( href ) ) );
+			// remote tab
+			// prevent loading the page itself if href is just "#"
+			} else if ( href && href !== "#" ) {
+				// required for restore on destroy
+				$.data( a, "href.tabs", href );
+
+				// TODO until #3808 is fixed strip fragment identifier from url
+				// (IE fails to load from such url)
+				$.data( a, "load.tabs", href.replace( /#.*$/, "" ) );
+
+				var id = self._tabId( a );
+				a.href = "#" + id;
+				var $panel = self.element.find( "#" + id );
+				if ( !$panel.length ) {
+					$panel = $( o.panelTemplate )
+						.attr( "id", id )
+						.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
+						.insertAfter( self.panels[ i - 1 ] || self.list );
+					$panel.data( "destroy.tabs", true );
+				}
+				self.panels = self.panels.add( $panel );
+			// invalid tab href
+			} else {
+				o.disabled.push( i );
+			}
+		});
+
+		// initialization from scratch
+		if ( init ) {
+			// attach necessary classes for styling
+			this.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" );
+			this.list.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" );
+			this.lis.addClass( "ui-state-default ui-corner-top" );
+			this.panels.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" );
+
+			// Selected tab
+			// use "selected" option or try to retrieve:
+			// 1. from fragment identifier in url
+			// 2. from cookie
+			// 3. from selected class attribute on <li>
+			if ( o.selected === undefined ) {
+				if ( location.hash ) {
+					this.anchors.each(function( i, a ) {
+						if ( a.hash == location.hash ) {
+							o.selected = i;
+							return false;
+						}
+					});
+				}
+				if ( typeof o.selected !== "number" && o.cookie ) {
+					o.selected = parseInt( self._cookie(), 10 );
+				}
+				if ( typeof o.selected !== "number" && this.lis.filter( ".ui-tabs-selected" ).length ) {
+					o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) );
+				}
+				o.selected = o.selected || ( this.lis.length ? 0 : -1 );
+			} else if ( o.selected === null ) { // usage of null is deprecated, TODO remove in next release
+				o.selected = -1;
+			}
+
+			// sanity check - default to first tab...
+			o.selected = ( ( o.selected >= 0 && this.anchors[ o.selected ] ) || o.selected < 0 )
+				? o.selected
+				: 0;
+
+			// Take disabling tabs via class attribute from HTML
+			// into account and update option properly.
+			// A selected tab cannot become disabled.
+			o.disabled = $.unique( o.disabled.concat(
+				$.map( this.lis.filter( ".ui-state-disabled" ), function( n, i ) {
+					return self.lis.index( n );
+				})
+			) ).sort();
+
+			if ( $.inArray( o.selected, o.disabled ) != -1 ) {
+				o.disabled.splice( $.inArray( o.selected, o.disabled ), 1 );
+			}
+
+			// highlight selected tab
+			this.panels.addClass( "ui-tabs-hide" );
+			this.lis.removeClass( "ui-tabs-selected ui-state-active" );
+			// check for length avoids error when initializing empty list
+			if ( o.selected >= 0 && this.anchors.length ) {
+				self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) ).removeClass( "ui-tabs-hide" );
+				this.lis.eq( o.selected ).addClass( "ui-tabs-selected ui-state-active" );
+
+				// seems to be expected behavior that the show callback is fired
+				self.element.queue( "tabs", function() {
+					self._trigger( "show", null,
+						self._ui( self.anchors[ o.selected ], self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) )[ 0 ] ) );
+				});
+
+				this.load( o.selected );
+			}
+
+			// clean up to avoid memory leaks in certain versions of IE 6
+			// TODO: namespace this event
+			$( window ).bind( "unload", function() {
+				self.lis.add( self.anchors ).unbind( ".tabs" );
+				self.lis = self.anchors = self.panels = null;
+			});
+		// update selected after add/remove
+		} else {
+			o.selected = this.lis.index( this.lis.filter( ".ui-tabs-selected" ) );
+		}
+
+		// update collapsible
+		// TODO: use .toggleClass()
+		this.element[ o.collapsible ? "addClass" : "removeClass" ]( "ui-tabs-collapsible" );
+
+		// set or update cookie after init and add/remove respectively
+		if ( o.cookie ) {
+			this._cookie( o.selected, o.cookie );
+		}
+
+		// disable tabs
+		for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) {
+			$( li )[ $.inArray( i, o.disabled ) != -1 &&
+				// TODO: use .toggleClass()
+				!$( li ).hasClass( "ui-tabs-selected" ) ? "addClass" : "removeClass" ]( "ui-state-disabled" );
+		}
+
+		// reset cache if switching from cached to not cached
+		if ( o.cache === false ) {
+			this.anchors.removeData( "cache.tabs" );
+		}
+
+		// remove all handlers before, tabify may run on existing tabs after add or option change
+		this.lis.add( this.anchors ).unbind( ".tabs" );
+
+		if ( o.event !== "mouseover" ) {
+			var addState = function( state, el ) {
+				if ( el.is( ":not(.ui-state-disabled)" ) ) {
+					el.addClass( "ui-state-" + state );
+				}
+			};
+			var removeState = function( state, el ) {
+				el.removeClass( "ui-state-" + state );
+			};
+			this.lis.bind( "mouseover.tabs" , function() {
+				addState( "hover", $( this ) );
+			});
+			this.lis.bind( "mouseout.tabs", function() {
+				removeState( "hover", $( this ) );
+			});
+			this.anchors.bind( "focus.tabs", function() {
+				addState( "focus", $( this ).closest( "li" ) );
+			});
+			this.anchors.bind( "blur.tabs", function() {
+				removeState( "focus", $( this ).closest( "li" ) );
+			});
+		}
+
+		// set up animations
+		var hideFx, showFx;
+		if ( o.fx ) {
+			if ( $.isArray( o.fx ) ) {
+				hideFx = o.fx[ 0 ];
+				showFx = o.fx[ 1 ];
+			} else {
+				hideFx = showFx = o.fx;
+			}
+		}
+
+		// Reset certain styles left over from animation
+		// and prevent IE's ClearType bug...
+		function resetStyle( $el, fx ) {
+			$el.css( "display", "" );
+			if ( !$.support.opacity && fx.opacity ) {
+				$el[ 0 ].style.removeAttribute( "filter" );
+			}
+		}
+
+		// Show a tab...
+		var showTab = showFx
+			? function( clicked, $show ) {
+				$( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" );
+				$show.hide().removeClass( "ui-tabs-hide" ) // avoid flicker that way
+					.animate( showFx, showFx.duration || "normal", function() {
+						resetStyle( $show, showFx );
+						self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) );
+					});
+			}
+			: function( clicked, $show ) {
+				$( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" );
+				$show.removeClass( "ui-tabs-hide" );
+				self._trigger( "show", null, self._ui( clicked, $show[ 0 ] ) );
+			};
+
+		// Hide a tab, $show is optional...
+		var hideTab = hideFx
+			? function( clicked, $hide ) {
+				$hide.animate( hideFx, hideFx.duration || "normal", function() {
+					self.lis.removeClass( "ui-tabs-selected ui-state-active" );
+					$hide.addClass( "ui-tabs-hide" );
+					resetStyle( $hide, hideFx );
+					self.element.dequeue( "tabs" );
+				});
+			}
+			: function( clicked, $hide, $show ) {
+				self.lis.removeClass( "ui-tabs-selected ui-state-active" );
+				$hide.addClass( "ui-tabs-hide" );
+				self.element.dequeue( "tabs" );
+			};
+
+		// attach tab event handler, unbind to avoid duplicates from former tabifying...
+		this.anchors.bind( o.event + ".tabs", function() {
+			var el = this,
+				$li = $(el).closest( "li" ),
+				$hide = self.panels.filter( ":not(.ui-tabs-hide)" ),
+				$show = self.element.find( self._sanitizeSelector( el.hash ) );
+
+			// If tab is already selected and not collapsible or tab disabled or
+			// or is already loading or click callback returns false stop here.
+			// Check if click handler returns false last so that it is not executed
+			// for a disabled or loading tab!
+			if ( ( $li.hasClass( "ui-tabs-selected" ) && !o.collapsible) ||
+				$li.hasClass( "ui-state-disabled" ) ||
+				$li.hasClass( "ui-state-processing" ) ||
+				self.panels.filter( ":animated" ).length ||
+				self._trigger( "select", null, self._ui( this, $show[ 0 ] ) ) === false ) {
+				this.blur();
+				return false;
+			}
+
+			o.selected = self.anchors.index( this );
+
+			self.abort();
+
+			// if tab may be closed
+			if ( o.collapsible ) {
+				if ( $li.hasClass( "ui-tabs-selected" ) ) {
+					o.selected = -1;
+
+					if ( o.cookie ) {
+						self._cookie( o.selected, o.cookie );
+					}
+
+					self.element.queue( "tabs", function() {
+						hideTab( el, $hide );
+					}).dequeue( "tabs" );
+
+					this.blur();
+					return false;
+				} else if ( !$hide.length ) {
+					if ( o.cookie ) {
+						self._cookie( o.selected, o.cookie );
+					}
+
+					self.element.queue( "tabs", function() {
+						showTab( el, $show );
+					});
+
+					// TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171
+					self.load( self.anchors.index( this ) );
+
+					this.blur();
+					return false;
+				}
+			}
+
+			if ( o.cookie ) {
+				self._cookie( o.selected, o.cookie );
+			}
+
+			// show new tab
+			if ( $show.length ) {
+				if ( $hide.length ) {
+					self.element.queue( "tabs", function() {
+						hideTab( el, $hide );
+					});
+				}
+				self.element.queue( "tabs", function() {
+					showTab( el, $show );
+				});
+
+				self.load( self.anchors.index( this ) );
+			} else {
+				throw "jQuery UI Tabs: Mismatching fragment identifier.";
+			}
+
+			// Prevent IE from keeping other link focussed when using the back button
+			// and remove dotted border from clicked link. This is controlled via CSS
+			// in modern browsers; blur() removes focus from address bar in Firefox
+			// which can become a usability and annoying problem with tabs('rotate').
+			if ( $.browser.msie ) {
+				this.blur();
+			}
+		});
+
+		// disable click in any case
+		this.anchors.bind( "click.tabs", function(){
+			return false;
+		});
+	},
+
+    _getIndex: function( index ) {
+		// meta-function to give users option to provide a href string instead of a numerical index.
+		// also sanitizes numerical indexes to valid values.
+		if ( typeof index == "string" ) {
+			index = this.anchors.index( this.anchors.filter( "[href$=" + index + "]" ) );
+		}
+
+		return index;
+	},
+
+	destroy: function() {
+		var o = this.options;
+
+		this.abort();
+
+		this.element
+			.unbind( ".tabs" )
+			.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" )
+			.removeData( "tabs" );
+
+		this.list.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" );
+
+		this.anchors.each(function() {
+			var href = $.data( this, "href.tabs" );
+			if ( href ) {
+				this.href = href;
+			}
+			var $this = $( this ).unbind( ".tabs" );
+			$.each( [ "href", "load", "cache" ], function( i, prefix ) {
+				$this.removeData( prefix + ".tabs" );
+			});
+		});
+
+		this.lis.unbind( ".tabs" ).add( this.panels ).each(function() {
+			if ( $.data( this, "destroy.tabs" ) ) {
+				$( this ).remove();
+			} else {
+				$( this ).removeClass([
+					"ui-state-default",
+					"ui-corner-top",
+					"ui-tabs-selected",
+					"ui-state-active",
+					"ui-state-hover",
+					"ui-state-focus",
+					"ui-state-disabled",
+					"ui-tabs-panel",
+					"ui-widget-content",
+					"ui-corner-bottom",
+					"ui-tabs-hide"
+				].join( " " ) );
+			}
+		});
+
+		if ( o.cookie ) {
+			this._cookie( null, o.cookie );
+		}
+
+		return this;
+	},
+
+	add: function( url, label, index ) {
+		if ( index === undefined ) {
+			index = this.anchors.length;
+		}
+
+		var self = this,
+			o = this.options,
+			$li = $( o.tabTemplate.replace( /#\{href\}/g, url ).replace( /#\{label\}/g, label ) ),
+			id = !url.indexOf( "#" ) ? url.replace( "#", "" ) : this._tabId( $( "a", $li )[ 0 ] );
+
+		$li.addClass( "ui-state-default ui-corner-top" ).data( "destroy.tabs", true );
+
+		// try to find an existing element before creating a new one
+		var $panel = self.element.find( "#" + id );
+		if ( !$panel.length ) {
+			$panel = $( o.panelTemplate )
+				.attr( "id", id )
+				.data( "destroy.tabs", true );
+		}
+		$panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide" );
+
+		if ( index >= this.lis.length ) {
+			$li.appendTo( this.list );
+			$panel.appendTo( this.list[ 0 ].parentNode );
+		} else {
+			$li.insertBefore( this.lis[ index ] );
+			$panel.insertBefore( this.panels[ index ] );
+		}
+
+		o.disabled = $.map( o.disabled, function( n, i ) {
+			return n >= index ? ++n : n;
+		});
+
+		this._tabify();
+
+		if ( this.anchors.length == 1 ) {
+			o.selected = 0;
+			$li.addClass( "ui-tabs-selected ui-state-active" );
+			$panel.removeClass( "ui-tabs-hide" );
+			this.element.queue( "tabs", function() {
+				self._trigger( "show", null, self._ui( self.anchors[ 0 ], self.panels[ 0 ] ) );
+			});
+
+			this.load( 0 );
+		}
+
+		this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
+		return this;
+	},
+
+	remove: function( index ) {
+		index = this._getIndex( index );
+		var o = this.options,
+			$li = this.lis.eq( index ).remove(),
+			$panel = this.panels.eq( index ).remove();
+
+		// If selected tab was removed focus tab to the right or
+		// in case the last tab was removed the tab to the left.
+		if ( $li.hasClass( "ui-tabs-selected" ) && this.anchors.length > 1) {
+			this.select( index + ( index + 1 < this.anchors.length ? 1 : -1 ) );
+		}
+
+		o.disabled = $.map(
+			$.grep( o.disabled, function(n, i) {
+				return n != index;
+			}),
+			function( n, i ) {
+				return n >= index ? --n : n;
+			});
+
+		this._tabify();
+
+		this._trigger( "remove", null, this._ui( $li.find( "a" )[ 0 ], $panel[ 0 ] ) );
+		return this;
+	},
+
+	enable: function( index ) {
+		index = this._getIndex( index );
+		var o = this.options;
+		if ( $.inArray( index, o.disabled ) == -1 ) {
+			return;
+		}
+
+		this.lis.eq( index ).removeClass( "ui-state-disabled" );
+		o.disabled = $.grep( o.disabled, function( n, i ) {
+			return n != index;
+		});
+
+		this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
+		return this;
+	},
+
+	disable: function( index ) {
+		index = this._getIndex( index );
+		var self = this, o = this.options;
+		// cannot disable already selected tab
+		if ( index != o.selected ) {
+			this.lis.eq( index ).addClass( "ui-state-disabled" );
+
+			o.disabled.push( index );
+			o.disabled.sort();
+
+			this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) );
+		}
+
+		return this;
+	},
+
+	select: function( index ) {
+		index = this._getIndex( index );
+		if ( index == -1 ) {
+			if ( this.options.collapsible && this.options.selected != -1 ) {
+				index = this.options.selected;
+			} else {
+				return this;
+			}
+		}
+		this.anchors.eq( index ).trigger( this.options.event + ".tabs" );
+		return this;
+	},
+
+	load: function( index ) {
+		index = this._getIndex( index );
+		var self = this,
+			o = this.options,
+			a = this.anchors.eq( index )[ 0 ],
+			url = $.data( a, "load.tabs" );
+
+		this.abort();
+
+		// not remote or from cache
+		if ( !url || this.element.queue( "tabs" ).length !== 0 && $.data( a, "cache.tabs" ) ) {
+			this.element.dequeue( "tabs" );
+			return;
+		}
+
+		// load remote from here on
+		this.lis.eq( index ).addClass( "ui-state-processing" );
+
+		if ( o.spinner ) {
+			var span = $( "span", a );
+			span.data( "label.tabs", span.html() ).html( o.spinner );
+		}
+
+		this.xhr = $.ajax( $.extend( {}, o.ajaxOptions, {
+			url: url,
+			success: function( r, s ) {
+				self.element.find( self._sanitizeSelector( a.hash ) ).html( r );
+
+				// take care of tab labels
+				self._cleanup();
+
+				if ( o.cache ) {
+					$.data( a, "cache.tabs", true );
+				}
+
+				self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) );
+				try {
+					o.ajaxOptions.success( r, s );
+				}
+				catch ( e ) {}
+			},
+			error: function( xhr, s, e ) {
+				// take care of tab labels
+				self._cleanup();
+
+				self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) );
+				try {
+					// Passing index avoid a race condition when this method is
+					// called after the user has selected another tab.
+					// Pass the anchor that initiated this request allows
+					// loadError to manipulate the tab content panel via $(a.hash)
+					o.ajaxOptions.error( xhr, s, index, a );
+				}
+				catch ( e ) {}
+			}
+		} ) );
+
+		// last, so that load event is fired before show...
+		self.element.dequeue( "tabs" );
+
+		return this;
+	},
+
+	abort: function() {
+		// stop possibly running animations
+		this.element.queue( [] );
+		this.panels.stop( false, true );
+
+		// "tabs" queue must not contain more than two elements,
+		// which are the callbacks for the latest clicked tab...
+		this.element.queue( "tabs", this.element.queue( "tabs" ).splice( -2, 2 ) );
+
+		// terminate pending requests from other tabs
+		if ( this.xhr ) {
+			this.xhr.abort();
+			delete this.xhr;
+		}
+
+		// take care of tab labels
+		this._cleanup();
+		return this;
+	},
+
+	url: function( index, url ) {
+		this.anchors.eq( index ).removeData( "cache.tabs" ).data( "load.tabs", url );
+		return this;
+	},
+
+	length: function() {
+		return this.anchors.length;
+	}
+});
+
+$.extend( $.ui.tabs, {
+	version: "@VERSION"
+});
+
+/*
+ * Tabs Extensions
+ */
+
+/*
+ * Rotate
+ */
+$.extend( $.ui.tabs.prototype, {
+	rotation: null,
+	rotate: function( ms, continuing ) {
+		var self = this,
+			o = this.options;
+
+		var rotate = self._rotate || ( self._rotate = function( e ) {
+			clearTimeout( self.rotation );
+			self.rotation = setTimeout(function() {
+				var t = o.selected;
+				self.select( ++t < self.anchors.length ? t : 0 );
+			}, ms );
+			
+			if ( e ) {
+				e.stopPropagation();
+			}
+		});
+
+		var stop = self._unrotate || ( self._unrotate = !continuing
+			? function(e) {
+				if (e.clientX) { // in case of a true click
+					self.rotate(null);
+				}
+			}
+			: function( e ) {
+				t = o.selected;
+				rotate();
+			});
+
+		// start rotation
+		if ( ms ) {
+			this.element.bind( "tabsshow", rotate );
+			this.anchors.bind( o.event + ".tabs", stop );
+			rotate();
+		// stop rotation
+		} else {
+			clearTimeout( self.rotation );
+			this.element.unbind( "tabsshow", rotate );
+			this.anchors.unbind( o.event + ".tabs", stop );
+			delete this._rotate;
+			delete this._unrotate;
+		}
+
+		return this;
+	}
+});
+
+})( jQuery );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100755
index 0000000..5b5dab2
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_75_ffffff_40x100.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_55_fbf9ee_1x400.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100755
index 0000000..ad3d634
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_65_ffffff_1x400.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100755
index 0000000..42ccba2
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_dadada_1x400.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100755
index 0000000..5a46b47
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_e6e6e6_1x400.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100755
index 0000000..86c2baa
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_95_fef1ec_1x400.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000..4443fdc
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100755
index 0000000..7c9fa6c
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_222222_256x240.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_222222_256x240.png
new file mode 100755
index 0000000..ee039dc
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_222222_256x240.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_2e83ff_256x240.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_2e83ff_256x240.png
new file mode 100755
index 0000000..45e8928
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_2e83ff_256x240.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_454545_256x240.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_454545_256x240.png
new file mode 100755
index 0000000..7ec70d1
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_454545_256x240.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_888888_256x240.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_888888_256x240.png
new file mode 100755
index 0000000..5ba708c
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_888888_256x240.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_cd0a0a_256x240.png b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_cd0a0a_256x240.png
new file mode 100755
index 0000000..7930a55
Binary files /dev/null and b/client/apollo/jslib/jqueryui/themes/base/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.accordion.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.accordion.css
new file mode 100644
index 0000000..c309417
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.accordion.css
@@ -0,0 +1,19 @@
+/*
+ * jQuery UI Accordion @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+/* IE/Win - Fix animation bug - #4615 */
+.ui-accordion { width: 100%; }
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
+.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.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; }
+.ui-accordion .ui-accordion-content-active { display: block; }
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.all.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.all.css
new file mode 100644
index 0000000..96b15aa
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.all.css
@@ -0,0 +1,11 @@
+/*
+ * jQuery UI CSS Framework @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import "jquery.ui.base.css";
+ at import "jquery.ui.theme.css";
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.autocomplete.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.autocomplete.css
new file mode 100644
index 0000000..0d30d77
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.autocomplete.css
@@ -0,0 +1,53 @@
+/*
+ * jQuery UI Autocomplete @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete { position: absolute; cursor: default; }	
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
+
+/*
+ * jQuery UI Menu @VERSION
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu {
+	list-style:none;
+	padding: 2px;
+	margin: 0;
+	display:block;
+	float: left;
+}
+.ui-menu .ui-menu {
+	margin-top: -3px;
+}
+.ui-menu .ui-menu-item {
+	margin:0;
+	padding: 0;
+	zoom: 1;
+	float: left;
+	clear: left;
+	width: 100%;
+}
+.ui-menu .ui-menu-item a {
+	text-decoration:none;
+	display:block;
+	padding:.2em .4em;
+	line-height:1.5;
+	zoom:1;
+}
+.ui-menu .ui-menu-item a.ui-state-hover,
+.ui-menu .ui-menu-item a.ui-state-active {
+	font-weight: normal;
+	margin: -1px;
+}
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.base.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.base.css
new file mode 100644
index 0000000..2170383
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.base.css
@@ -0,0 +1,21 @@
+/*
+ * jQuery UI CSS Framework @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import url("jquery.ui.core.css");
+
+ at import url("jquery.ui.accordion.css");
+ at import url("jquery.ui.autocomplete.css");
+ at import url("jquery.ui.button.css");
+ at import url("jquery.ui.datepicker.css");
+ at import url("jquery.ui.dialog.css");
+ at import url("jquery.ui.progressbar.css");
+ at import url("jquery.ui.resizable.css");
+ at import url("jquery.ui.selectable.css");
+ at import url("jquery.ui.slider.css");
+ at import url("jquery.ui.tabs.css");
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.button.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.button.css
new file mode 100644
index 0000000..2ae50af
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.button.css
@@ -0,0 +1,38 @@
+/*
+ * jQuery UI Button @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: hidden; *overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; } 
+button.ui-button-icons-only { width: 3.7em; } 
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.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; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.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; }
+.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; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.core.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.core.css
new file mode 100644
index 0000000..143d6c2
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.core.css
@@ -0,0 +1,38 @@
+/*
+ * jQuery UI CSS Framework @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.datepicker.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.datepicker.css
new file mode 100644
index 0000000..8d574ba
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.datepicker.css
@@ -0,0 +1,68 @@
+/*
+ * jQuery UI Datepicker @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.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;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.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; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    display: none; /*sorry for IE5*/
+    display/**/: block; /*sorry for IE5*/
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.dialog.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.dialog.css
new file mode 100644
index 0000000..12609ed
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.dialog.css
@@ -0,0 +1,21 @@
+/*
+ * jQuery UI Dialog @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.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; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.progressbar.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.progressbar.css
new file mode 100644
index 0000000..79d786f
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.progressbar.css
@@ -0,0 +1,11 @@
+/*
+ * jQuery UI Progressbar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.resizable.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.resizable.css
new file mode 100644
index 0000000..ed63952
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.resizable.css
@@ -0,0 +1,20 @@
+/*
+ * jQuery UI Resizable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.selectable.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.selectable.css
new file mode 100644
index 0000000..5177f6b
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.selectable.css
@@ -0,0 +1,10 @@
+/*
+ * jQuery UI Selectable @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.slider.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.slider.css
new file mode 100644
index 0000000..8d9b403
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.slider.css
@@ -0,0 +1,24 @@
+/*
+ * jQuery UI Slider @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.tabs.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.tabs.css
new file mode 100644
index 0000000..12666fa
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.tabs.css
@@ -0,0 +1,18 @@
+/*
+ * jQuery UI Tabs @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.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; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
+.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; }
+.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... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
diff --git a/client/apollo/jslib/jqueryui/themes/base/jquery.ui.theme.css b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.theme.css
new file mode 100644
index 0000000..be26769
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/themes/base/jquery.ui.theme.css
@@ -0,0 +1,247 @@
+/*
+ * jQuery UI CSS Framework @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(images/ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; }
+.ui-widget-content a { color: #222222/*{fcContent}*/; }
+.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; }
+.ui-widget-header a { color: #222222/*{fcHeader}*/; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; }
+.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 #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #212121/*{fcHover}*/; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; }
+.ui-widget :active { outline: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -khtml-border-top-left-radius: 4px/*{cornerRadius}*/; border-top-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -khtml-border-top-right-radius: 4px/*{cornerRadius}*/; border-top-right-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-left-radius: 4px/*{cornerRadius}*/; border-bottom-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; }
+.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRad [...]
\ No newline at end of file
diff --git a/client/apollo/jslib/jqueryui/widget.js b/client/apollo/jslib/jqueryui/widget.js
new file mode 100644
index 0000000..9160ce5
--- /dev/null
+++ b/client/apollo/jslib/jqueryui/widget.js
@@ -0,0 +1,275 @@
+define(['jquery'], function (jQuery) {
+/*!
+ * jQuery UI Widget @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Widget
+ */
+(function( $, undefined ) {
+
+// jQuery 1.4+
+if ( $.cleanData ) {
+	var _cleanData = $.cleanData;
+	$.cleanData = function( elems ) {
+		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+			try {
+				$( elem ).triggerHandler( "remove" );
+			// http://bugs.jquery.com/ticket/8235
+			} catch( e ) {}
+		}
+		_cleanData( elems );
+	};
+} else {
+	var _remove = $.fn.remove;
+	$.fn.remove = function( selector, keepData ) {
+		return this.each(function() {
+			if ( !keepData ) {
+				if ( !selector || $.filter( selector, [ this ] ).length ) {
+					$( "*", this ).add( [ this ] ).each(function() {
+						try {
+							$( this ).triggerHandler( "remove" );
+						// http://bugs.jquery.com/ticket/8235
+						} catch( e ) {}
+					});
+				}
+			}
+			return _remove.call( $(this), selector, keepData );
+		});
+	};
+}
+
+$.widget = function( name, base, prototype ) {
+	var namespace = name.split( "." )[ 0 ],
+		fullName;
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName ] = function( elem ) {
+		return !!$.data( elem, name );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	$[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without initializing for simple inheritance
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+
+	var basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+//	$.each( basePrototype, function( key, val ) {
+//		if ( $.isPlainObject(val) ) {
+//			basePrototype[ key ] = $.extend( {}, val );
+//		}
+//	});
+	basePrototype.options = $.extend( true, {}, basePrototype.options );
+	$[ namespace ][ name ].prototype = $.extend( true, basePrototype, {
+		namespace: namespace,
+		widgetName: name,
+		widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name,
+		widgetBaseClass: fullName
+	}, prototype );
+
+	$.widget.bridge( name, $[ namespace ][ name ] );
+};
+
+$.widget.bridge = function( name, object ) {
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = Array.prototype.slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.extend.apply( null, [ true, options ].concat(args) ) :
+			options;
+
+		// prevent calls to internal methods
+		if ( isMethodCall && options.charAt( 0 ) === "_" ) {
+			return returnValue;
+		}
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var instance = $.data( this, name ),
+					methodValue = instance && $.isFunction( instance[options] ) ?
+						instance[ options ].apply( instance, args ) :
+						instance;
+				// TODO: add this back in 1.9 and use $.error() (see #5972)
+//				if ( !instance ) {
+//					throw "cannot call methods on " + name + " prior to initialization; " +
+//						"attempted to call method '" + options + "'";
+//				}
+//				if ( !$.isFunction( instance[options] ) ) {
+//					throw "no such method '" + options + "' for " + name + " widget instance";
+//				}
+//				var methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, name );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					$.data( this, name, new object( options, this ) );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( options, element ) {
+	// allow instantiation without initializing for simple inheritance
+	if ( arguments.length ) {
+		this._createWidget( options, element );
+	}
+};
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	options: {
+		disabled: false
+	},
+	_createWidget: function( options, element ) {
+		// $.widget.bridge stores the plugin instance, but we do it anyway
+		// so that it's stored even before the _create function runs
+		$.data( element, this.widgetName, this );
+		this.element = $( element );
+		this.options = $.extend( true, {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		var self = this;
+		this.element.bind( "remove." + this.widgetName, function() {
+			self.destroy();
+		});
+
+		this._create();
+		this._trigger( "create" );
+		this._init();
+	},
+	_getCreateOptions: function() {
+		return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
+	},
+	_create: function() {},
+	_init: function() {},
+
+	destroy: function() {
+		this.element
+			.unbind( "." + this.widgetName )
+			.removeData( this.widgetName );
+		this.widget()
+			.unbind( "." + this.widgetName )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetBaseClass + "-disabled " +
+				"ui-state-disabled" );
+	},
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.extend( {}, this.options );
+		}
+
+		if  (typeof key === "string" ) {
+			if ( value === undefined ) {
+				return this.options[ key ];
+			}
+			options = {};
+			options[ key ] = value;
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var self = this;
+		$.each( options, function( key, value ) {
+			self._setOption( key, value );
+		});
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				[ value ? "addClass" : "removeClass"](
+					this.widgetBaseClass + "-disabled" + " " +
+					"ui-state-disabled" )
+				.attr( "aria-disabled", value );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+
+		return !( $.isFunction(callback) &&
+			callback.call( this.element[0], event, data ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+})( jQuery );
+
+});
\ No newline at end of file
diff --git a/client/apollo/jslib/websocket/spring-websocket.js b/client/apollo/jslib/websocket/spring-websocket.js
new file mode 100644
index 0000000..1a39a93
--- /dev/null
+++ b/client/apollo/jslib/websocket/spring-websocket.js
@@ -0,0 +1,2884 @@
+/* SockJS client, version 0.3.4, http://sockjs.org, MIT License
+
+Copyright (c) 2011-2012 VMware, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// JSON2 by Douglas Crockford (minified).
+var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";e=h.l [...]
+
+
+//     [*] Including lib/index.js
+// Public object
+SockJS = (function(){
+              var _document = document;
+              var _window = window;
+              var utils = {};
+
+
+//         [*] Including lib/reventtarget.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+/* Simplified implementation of DOM2 EventTarget.
+ *   http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget
+ */
+var REventTarget = function() {};
+REventTarget.prototype.addEventListener = function (eventType, listener) {
+    if(!this._listeners) {
+         this._listeners = {};
+    }
+    if(!(eventType in this._listeners)) {
+        this._listeners[eventType] = [];
+    }
+    var arr = this._listeners[eventType];
+    if(utils.arrIndexOf(arr, listener) === -1) {
+        arr.push(listener);
+    }
+    return;
+};
+
+REventTarget.prototype.removeEventListener = function (eventType, listener) {
+    if(!(this._listeners && (eventType in this._listeners))) {
+        return;
+    }
+    var arr = this._listeners[eventType];
+    var idx = utils.arrIndexOf(arr, listener);
+    if (idx !== -1) {
+        if(arr.length > 1) {
+            this._listeners[eventType] = arr.slice(0, idx).concat( arr.slice(idx+1) );
+        } else {
+            delete this._listeners[eventType];
+        }
+        return;
+    }
+    return;
+};
+
+REventTarget.prototype.dispatchEvent = function (event) {
+    var t = event.type;
+    var args = Array.prototype.slice.call(arguments, 0);
+    if (this['on'+t]) {
+        this['on'+t].apply(this, args);
+    }
+    if (this._listeners && t in this._listeners) {
+        for(var i=0; i < this._listeners[t].length; i++) {
+            this._listeners[t][i].apply(this, args);
+        }
+    }
+};
+//         [*] End of lib/reventtarget.js
+
+
+//         [*] Including lib/simpleevent.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var SimpleEvent = function(type, obj) {
+    this.type = type;
+    if (typeof obj !== 'undefined') {
+        for(var k in obj) {
+            if (!obj.hasOwnProperty(k)) continue;
+            this[k] = obj[k];
+        }
+    }
+};
+
+SimpleEvent.prototype.toString = function() {
+    var r = [];
+    for(var k in this) {
+        if (!this.hasOwnProperty(k)) continue;
+        var v = this[k];
+        if (typeof v === 'function') v = '[function]';
+        r.push(k + '=' + v);
+    }
+    return 'SimpleEvent(' + r.join(', ') + ')';
+};
+//         [*] End of lib/simpleevent.js
+
+
+//         [*] Including lib/eventemitter.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EventEmitter = function(events) {
+    var that = this;
+    that._events = events || [];
+    that._listeners = {};
+};
+EventEmitter.prototype.emit = function(type) {
+    var that = this;
+    that._verifyType(type);
+    if (that._nuked) return;
+
+    var args = Array.prototype.slice.call(arguments, 1);
+    if (that['on'+type]) {
+        that['on'+type].apply(that, args);
+    }
+    if (type in that._listeners) {
+        for(var i = 0; i < that._listeners[type].length; i++) {
+            that._listeners[type][i].apply(that, args);
+        }
+    }
+};
+
+EventEmitter.prototype.on = function(type, callback) {
+    var that = this;
+    that._verifyType(type);
+    if (that._nuked) return;
+
+    if (!(type in that._listeners)) {
+        that._listeners[type] = [];
+    }
+    that._listeners[type].push(callback);
+};
+
+EventEmitter.prototype._verifyType = function(type) {
+    var that = this;
+    if (utils.arrIndexOf(that._events, type) === -1) {
+        utils.log('Event ' + JSON.stringify(type) +
+                  ' not listed ' + JSON.stringify(that._events) +
+                  ' in ' + that);
+    }
+};
+
+EventEmitter.prototype.nuke = function() {
+    var that = this;
+    that._nuked = true;
+    for(var i=0; i<that._events.length; i++) {
+        delete that[that._events[i]];
+    }
+    that._listeners = {};
+};
+//         [*] End of lib/eventemitter.js
+
+
+//         [*] Including lib/utils.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var random_string_chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
+utils.random_string = function(length, max) {
+    max = max || random_string_chars.length;
+    var i, ret = [];
+    for(i=0; i < length; i++) {
+        ret.push( random_string_chars.substr(Math.floor(Math.random() * max),1) );
+    }
+    return ret.join('');
+};
+utils.random_number = function(max) {
+    return Math.floor(Math.random() * max);
+};
+utils.random_number_string = function(max) {
+    var t = (''+(max - 1)).length;
+    var p = Array(t+1).join('0');
+    return (p + utils.random_number(max)).slice(-t);
+};
+
+// Assuming that url looks like: http://asdasd:111/asd
+utils.getOrigin = function(url) {
+    url += '/';
+    var parts = url.split('/').slice(0, 3);
+    return parts.join('/');
+};
+
+utils.isSameOriginUrl = function(url_a, url_b) {
+    // location.origin would do, but it's not always available.
+    if (!url_b) url_b = _window.location.href;
+
+    return (url_a.split('/').slice(0,3).join('/')
+                ===
+            url_b.split('/').slice(0,3).join('/'));
+};
+
+utils.getParentDomain = function(url) {
+    // ipv4 ip address
+    if (/^[0-9.]*$/.test(url)) return url;
+    // ipv6 ip address
+    if (/^\[/.test(url)) return url;
+    // no dots
+    if (!(/[.]/.test(url))) return url;
+
+    var parts = url.split('.').slice(1);
+    return parts.join('.');
+};
+
+utils.objectExtend = function(dst, src) {
+    for(var k in src) {
+        if (src.hasOwnProperty(k)) {
+            dst[k] = src[k];
+        }
+    }
+    return dst;
+};
+
+var WPrefix = '_jp';
+
+utils.polluteGlobalNamespace = function() {
+    if (!(WPrefix in _window)) {
+        _window[WPrefix] = {};
+    }
+};
+
+utils.closeFrame = function (code, reason) {
+    return 'c'+JSON.stringify([code, reason]);
+};
+
+utils.userSetCode = function (code) {
+    return code === 1000 || (code >= 3000 && code <= 4999);
+};
+
+// See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/
+// and RFC 2988.
+utils.countRTO = function (rtt) {
+    var rto;
+    if (rtt > 100) {
+        rto = 3 * rtt; // rto > 300msec
+    } else {
+        rto = rtt + 200; // 200msec < rto <= 300msec
+    }
+    return rto;
+}
+
+utils.log = function() {
+    if (_window.console && console.log && console.log.apply) {
+        console.log.apply(console, arguments);
+    }
+};
+
+utils.bind = function(fun, that) {
+    if (fun.bind) {
+        return fun.bind(that);
+    } else {
+        return function() {
+            return fun.apply(that, arguments);
+        };
+    }
+};
+
+utils.flatUrl = function(url) {
+    return url.indexOf('?') === -1 && url.indexOf('#') === -1;
+};
+
+utils.amendUrl = function(url) {
+    var dl = _document.location;
+    if (!url) {
+        throw new Error('Wrong url for SockJS');
+    }
+    if (!utils.flatUrl(url)) {
+        throw new Error('Only basic urls are supported in SockJS');
+    }
+
+    //  '//abc' --> 'http://abc'
+    if (url.indexOf('//') === 0) {
+        url = dl.protocol + url;
+    }
+    // '/abc' --> 'http://localhost:80/abc'
+    if (url.indexOf('/') === 0) {
+        url = dl.protocol + '//' + dl.host + url;
+    }
+    // strip trailing slashes
+    url = url.replace(/[/]+$/,'');
+    return url;
+};
+
+// IE doesn't support [].indexOf.
+utils.arrIndexOf = function(arr, obj){
+    for(var i=0; i < arr.length; i++){
+        if(arr[i] === obj){
+            return i;
+        }
+    }
+    return -1;
+};
+
+utils.arrSkip = function(arr, obj) {
+    var idx = utils.arrIndexOf(arr, obj);
+    if (idx === -1) {
+        return arr.slice();
+    } else {
+        var dst = arr.slice(0, idx);
+        return dst.concat(arr.slice(idx+1));
+    }
+};
+
+// Via: https://gist.github.com/1133122/2121c601c5549155483f50be3da5305e83b8c5df
+utils.isArray = Array.isArray || function(value) {
+    return {}.toString.call(value).indexOf('Array') >= 0
+};
+
+utils.delay = function(t, fun) {
+    if(typeof t === 'function') {
+        fun = t;
+        t = 0;
+    }
+    return setTimeout(fun, t);
+};
+
+
+// Chars worth escaping, as defined by Douglas Crockford:
+//   https://github.com/douglascrockford/JSON-js/blob/47a9882cddeb1e8529e07af9736218075372b8ac/json2.js#L196
+var json_escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+    json_lookup = {
+"\u0000":"\\u0000","\u0001":"\\u0001","\u0002":"\\u0002","\u0003":"\\u0003",
+"\u0004":"\\u0004","\u0005":"\\u0005","\u0006":"\\u0006","\u0007":"\\u0007",
+"\b":"\\b","\t":"\\t","\n":"\\n","\u000b":"\\u000b","\f":"\\f","\r":"\\r",
+"\u000e":"\\u000e","\u000f":"\\u000f","\u0010":"\\u0010","\u0011":"\\u0011",
+"\u0012":"\\u0012","\u0013":"\\u0013","\u0014":"\\u0014","\u0015":"\\u0015",
+"\u0016":"\\u0016","\u0017":"\\u0017","\u0018":"\\u0018","\u0019":"\\u0019",
+"\u001a":"\\u001a","\u001b":"\\u001b","\u001c":"\\u001c","\u001d":"\\u001d",
+"\u001e":"\\u001e","\u001f":"\\u001f","\"":"\\\"","\\":"\\\\",
+"\u007f":"\\u007f","\u0080":"\\u0080","\u0081":"\\u0081","\u0082":"\\u0082",
+"\u0083":"\\u0083","\u0084":"\\u0084","\u0085":"\\u0085","\u0086":"\\u0086",
+"\u0087":"\\u0087","\u0088":"\\u0088","\u0089":"\\u0089","\u008a":"\\u008a",
+"\u008b":"\\u008b","\u008c":"\\u008c","\u008d":"\\u008d","\u008e":"\\u008e",
+"\u008f":"\\u008f","\u0090":"\\u0090","\u0091":"\\u0091","\u0092":"\\u0092",
+"\u0093":"\\u0093","\u0094":"\\u0094","\u0095":"\\u0095","\u0096":"\\u0096",
+"\u0097":"\\u0097","\u0098":"\\u0098","\u0099":"\\u0099","\u009a":"\\u009a",
+"\u009b":"\\u009b","\u009c":"\\u009c","\u009d":"\\u009d","\u009e":"\\u009e",
+"\u009f":"\\u009f","\u00ad":"\\u00ad","\u0600":"\\u0600","\u0601":"\\u0601",
+"\u0602":"\\u0602","\u0603":"\\u0603","\u0604":"\\u0604","\u070f":"\\u070f",
+"\u17b4":"\\u17b4","\u17b5":"\\u17b5","\u200c":"\\u200c","\u200d":"\\u200d",
+"\u200e":"\\u200e","\u200f":"\\u200f","\u2028":"\\u2028","\u2029":"\\u2029",
+"\u202a":"\\u202a","\u202b":"\\u202b","\u202c":"\\u202c","\u202d":"\\u202d",
+"\u202e":"\\u202e","\u202f":"\\u202f","\u2060":"\\u2060","\u2061":"\\u2061",
+"\u2062":"\\u2062","\u2063":"\\u2063","\u2064":"\\u2064","\u2065":"\\u2065",
+"\u2066":"\\u2066","\u2067":"\\u2067","\u2068":"\\u2068","\u2069":"\\u2069",
+"\u206a":"\\u206a","\u206b":"\\u206b","\u206c":"\\u206c","\u206d":"\\u206d",
+"\u206e":"\\u206e","\u206f":"\\u206f","\ufeff":"\\ufeff","\ufff0":"\\ufff0",
+"\ufff1":"\\ufff1","\ufff2":"\\ufff2","\ufff3":"\\ufff3","\ufff4":"\\ufff4",
+"\ufff5":"\\ufff5","\ufff6":"\\ufff6","\ufff7":"\\ufff7","\ufff8":"\\ufff8",
+"\ufff9":"\\ufff9","\ufffa":"\\ufffa","\ufffb":"\\ufffb","\ufffc":"\\ufffc",
+"\ufffd":"\\ufffd","\ufffe":"\\ufffe","\uffff":"\\uffff"};
+
+// Some extra characters that Chrome gets wrong, and substitutes with
+// something else on the wire.
+var extra_escapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0 [...]
+    extra_lookup;
+
+// JSON Quote string. Use native implementation when possible.
+var JSONQuote = (JSON && JSON.stringify) || function(string) {
+    json_escapable.lastIndex = 0;
+    if (json_escapable.test(string)) {
+        string = string.replace(json_escapable, function(a) {
+            return json_lookup[a];
+        });
+    }
+    return '"' + string + '"';
+};
+
+// This may be quite slow, so let's delay until user actually uses bad
+// characters.
+var unroll_lookup = function(escapable) {
+    var i;
+    var unrolled = {}
+    var c = []
+    for(i=0; i<65536; i++) {
+        c.push( String.fromCharCode(i) );
+    }
+    escapable.lastIndex = 0;
+    c.join('').replace(escapable, function (a) {
+        unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+        return '';
+    });
+    escapable.lastIndex = 0;
+    return unrolled;
+};
+
+// Quote string, also taking care of unicode characters that browsers
+// often break. Especially, take care of unicode surrogates:
+//    http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates
+utils.quote = function(string) {
+    var quoted = JSONQuote(string);
+
+    // In most cases this should be very fast and good enough.
+    extra_escapable.lastIndex = 0;
+    if(!extra_escapable.test(quoted)) {
+        return quoted;
+    }
+
+    if(!extra_lookup) extra_lookup = unroll_lookup(extra_escapable);
+
+    return quoted.replace(extra_escapable, function(a) {
+        return extra_lookup[a];
+    });
+}
+
+var _all_protocols = ['websocket',
+                      'xdr-streaming',
+                      'xhr-streaming',
+                      'iframe-eventsource',
+                      'iframe-htmlfile',
+                      'xdr-polling',
+                      'xhr-polling',
+                      'iframe-xhr-polling',
+                      'jsonp-polling'];
+
+utils.probeProtocols = function() {
+    var probed = {};
+    for(var i=0; i<_all_protocols.length; i++) {
+        var protocol = _all_protocols[i];
+        // User can have a typo in protocol name.
+        probed[protocol] = SockJS[protocol] &&
+                           SockJS[protocol].enabled();
+    }
+    return probed;
+};
+
+utils.detectProtocols = function(probed, protocols_whitelist, info) {
+    var pe = {},
+        protocols = [];
+    if (!protocols_whitelist) protocols_whitelist = _all_protocols;
+    for(var i=0; i<protocols_whitelist.length; i++) {
+        var protocol = protocols_whitelist[i];
+        pe[protocol] = probed[protocol];
+    }
+    var maybe_push = function(protos) {
+        var proto = protos.shift();
+        if (pe[proto]) {
+            protocols.push(proto);
+        } else {
+            if (protos.length > 0) {
+                maybe_push(protos);
+            }
+        }
+    }
+
+    // 1. Websocket
+    if (info.websocket !== false) {
+        maybe_push(['websocket']);
+    }
+
+    // 2. Streaming
+    if (pe['xhr-streaming'] && !info.null_origin) {
+        protocols.push('xhr-streaming');
+    } else {
+        if (pe['xdr-streaming'] && !info.cookie_needed && !info.null_origin) {
+            protocols.push('xdr-streaming');
+        } else {
+            maybe_push(['iframe-eventsource',
+                        'iframe-htmlfile']);
+        }
+    }
+
+    // 3. Polling
+    if (pe['xhr-polling'] && !info.null_origin) {
+        protocols.push('xhr-polling');
+    } else {
+        if (pe['xdr-polling'] && !info.cookie_needed && !info.null_origin) {
+            protocols.push('xdr-polling');
+        } else {
+            maybe_push(['iframe-xhr-polling',
+                        'jsonp-polling']);
+        }
+    }
+    return protocols;
+}
+//         [*] End of lib/utils.js
+
+
+//         [*] Including lib/dom.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// May be used by htmlfile jsonp and transports.
+var MPrefix = '_sockjs_global';
+utils.createHook = function() {
+    var window_id = 'a' + utils.random_string(8);
+    if (!(MPrefix in _window)) {
+        var map = {};
+        _window[MPrefix] = function(window_id) {
+            if (!(window_id in map)) {
+                map[window_id] = {
+                    id: window_id,
+                    del: function() {delete map[window_id];}
+                };
+            }
+            return map[window_id];
+        }
+    }
+    return _window[MPrefix](window_id);
+};
+
+
+
+utils.attachMessage = function(listener) {
+    utils.attachEvent('message', listener);
+};
+utils.attachEvent = function(event, listener) {
+    if (typeof _window.addEventListener !== 'undefined') {
+        _window.addEventListener(event, listener, false);
+    } else {
+        // IE quirks.
+        // According to: http://stevesouders.com/misc/test-postmessage.php
+        // the message gets delivered only to 'document', not 'window'.
+        _document.attachEvent("on" + event, listener);
+        // I get 'window' for ie8.
+        _window.attachEvent("on" + event, listener);
+    }
+};
+
+utils.detachMessage = function(listener) {
+    utils.detachEvent('message', listener);
+};
+utils.detachEvent = function(event, listener) {
+    if (typeof _window.addEventListener !== 'undefined') {
+        _window.removeEventListener(event, listener, false);
+    } else {
+        _document.detachEvent("on" + event, listener);
+        _window.detachEvent("on" + event, listener);
+    }
+};
+
+
+var on_unload = {};
+// Things registered after beforeunload are to be called immediately.
+var after_unload = false;
+
+var trigger_unload_callbacks = function() {
+    for(var ref in on_unload) {
+        on_unload[ref]();
+        delete on_unload[ref];
+    };
+};
+
+var unload_triggered = function() {
+    if(after_unload) return;
+    after_unload = true;
+    trigger_unload_callbacks();
+};
+
+// 'unload' alone is not reliable in opera within an iframe, but we
+// can't use `beforeunload` as IE fires it on javascript: links.
+utils.attachEvent('unload', unload_triggered);
+
+utils.unload_add = function(listener) {
+    var ref = utils.random_string(8);
+    on_unload[ref] = listener;
+    if (after_unload) {
+        utils.delay(trigger_unload_callbacks);
+    }
+    return ref;
+};
+utils.unload_del = function(ref) {
+    if (ref in on_unload)
+        delete on_unload[ref];
+};
+
+
+utils.createIframe = function (iframe_url, error_callback) {
+    var iframe = _document.createElement('iframe');
+    var tref, unload_ref;
+    var unattach = function() {
+        clearTimeout(tref);
+        // Explorer had problems with that.
+        try {iframe.onload = null;} catch (x) {}
+        iframe.onerror = null;
+    };
+    var cleanup = function() {
+        if (iframe) {
+            unattach();
+            // This timeout makes chrome fire onbeforeunload event
+            // within iframe. Without the timeout it goes straight to
+            // onunload.
+            setTimeout(function() {
+                if(iframe) {
+                    iframe.parentNode.removeChild(iframe);
+                }
+                iframe = null;
+            }, 0);
+            utils.unload_del(unload_ref);
+        }
+    };
+    var onerror = function(r) {
+        if (iframe) {
+            cleanup();
+            error_callback(r);
+        }
+    };
+    var post = function(msg, origin) {
+        try {
+            // When the iframe is not loaded, IE raises an exception
+            // on 'contentWindow'.
+            if (iframe && iframe.contentWindow) {
+                iframe.contentWindow.postMessage(msg, origin);
+            }
+        } catch (x) {};
+    };
+
+    iframe.src = iframe_url;
+    iframe.style.display = 'none';
+    iframe.style.position = 'absolute';
+    iframe.onerror = function(){onerror('onerror');};
+    iframe.onload = function() {
+        // `onload` is triggered before scripts on the iframe are
+        // executed. Give it few seconds to actually load stuff.
+        clearTimeout(tref);
+        tref = setTimeout(function(){onerror('onload timeout');}, 2000);
+    };
+    _document.body.appendChild(iframe);
+    tref = setTimeout(function(){onerror('timeout');}, 15000);
+    unload_ref = utils.unload_add(cleanup);
+    return {
+        post: post,
+        cleanup: cleanup,
+        loaded: unattach
+    };
+};
+
+utils.createHtmlfile = function (iframe_url, error_callback) {
+    var doc = new ActiveXObject('htmlfile');
+    var tref, unload_ref;
+    var iframe;
+    var unattach = function() {
+        clearTimeout(tref);
+    };
+    var cleanup = function() {
+        if (doc) {
+            unattach();
+            utils.unload_del(unload_ref);
+            iframe.parentNode.removeChild(iframe);
+            iframe = doc = null;
+            CollectGarbage();
+        }
+    };
+    var onerror = function(r)  {
+        if (doc) {
+            cleanup();
+            error_callback(r);
+        }
+    };
+    var post = function(msg, origin) {
+        try {
+            // When the iframe is not loaded, IE raises an exception
+            // on 'contentWindow'.
+            if (iframe && iframe.contentWindow) {
+                iframe.contentWindow.postMessage(msg, origin);
+            }
+        } catch (x) {};
+    };
+
+    doc.open();
+    doc.write('<html><s' + 'cript>' +
+              'document.domain="' + document.domain + '";' +
+              '</s' + 'cript></html>');
+    doc.close();
+    doc.parentWindow[WPrefix] = _window[WPrefix];
+    var c = doc.createElement('div');
+    doc.body.appendChild(c);
+    iframe = doc.createElement('iframe');
+    c.appendChild(iframe);
+    iframe.src = iframe_url;
+    tref = setTimeout(function(){onerror('timeout');}, 15000);
+    unload_ref = utils.unload_add(cleanup);
+    return {
+        post: post,
+        cleanup: cleanup,
+        loaded: unattach
+    };
+};
+//         [*] End of lib/dom.js
+
+
+//         [*] Including lib/dom2.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var AbstractXHRObject = function(){};
+AbstractXHRObject.prototype = new EventEmitter(['chunk', 'finish']);
+
+AbstractXHRObject.prototype._start = function(method, url, payload, opts) {
+    var that = this;
+
+    try {
+        that.xhr = new XMLHttpRequest();
+    } catch(x) {};
+
+    if (!that.xhr) {
+        try {
+            that.xhr = new _window.ActiveXObject('Microsoft.XMLHTTP');
+        } catch(x) {};
+    }
+    if (_window.ActiveXObject || _window.XDomainRequest) {
+        // IE8 caches even POSTs
+        url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date);
+    }
+
+    // Explorer tends to keep connection open, even after the
+    // tab gets closed: http://bugs.jquery.com/ticket/5280
+    that.unload_ref = utils.unload_add(function(){that._cleanup(true);});
+    try {
+        that.xhr.open(method, url, true);
+    } catch(e) {
+        // IE raises an exception on wrong port.
+        that.emit('finish', 0, '');
+        that._cleanup();
+        return;
+    };
+
+    if (!opts || !opts.no_credentials) {
+        // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest :
+        // "This never affects same-site requests."
+        that.xhr.withCredentials = 'true';
+    }
+    if (opts && opts.headers) {
+        for(var key in opts.headers) {
+            that.xhr.setRequestHeader(key, opts.headers[key]);
+        }
+    }
+
+    that.xhr.onreadystatechange = function() {
+        if (that.xhr) {
+            var x = that.xhr;
+            switch (x.readyState) {
+            case 3:
+                // IE doesn't like peeking into responseText or status
+                // on Microsoft.XMLHTTP and readystate=3
+                try {
+                    var status = x.status;
+                    var text = x.responseText;
+                } catch (x) {};
+                // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450
+                if (status === 1223) status = 204;
+
+                // IE does return readystate == 3 for 404 answers.
+                if (text && text.length > 0) {
+                    that.emit('chunk', status, text);
+                }
+                break;
+            case 4:
+                var status = x.status;
+                // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450
+                if (status === 1223) status = 204;
+
+                that.emit('finish', status, x.responseText);
+                that._cleanup(false);
+                break;
+            }
+        }
+    };
+    that.xhr.send(payload);
+};
+
+AbstractXHRObject.prototype._cleanup = function(abort) {
+    var that = this;
+    if (!that.xhr) return;
+    utils.unload_del(that.unload_ref);
+
+    // IE needs this field to be a function
+    that.xhr.onreadystatechange = function(){};
+
+    if (abort) {
+        try {
+            that.xhr.abort();
+        } catch(x) {};
+    }
+    that.unload_ref = that.xhr = null;
+};
+
+AbstractXHRObject.prototype.close = function() {
+    var that = this;
+    that.nuke();
+    that._cleanup(true);
+};
+
+var XHRCorsObject = utils.XHRCorsObject = function() {
+    var that = this, args = arguments;
+    utils.delay(function(){that._start.apply(that, args);});
+};
+XHRCorsObject.prototype = new AbstractXHRObject();
+
+var XHRLocalObject = utils.XHRLocalObject = function(method, url, payload) {
+    var that = this;
+    utils.delay(function(){
+        that._start(method, url, payload, {
+            no_credentials: true
+        });
+    });
+};
+XHRLocalObject.prototype = new AbstractXHRObject();
+
+
+
+// References:
+//   http://ajaxian.com/archives/100-line-ajax-wrapper
+//   http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx
+var XDRObject = utils.XDRObject = function(method, url, payload) {
+    var that = this;
+    utils.delay(function(){that._start(method, url, payload);});
+};
+XDRObject.prototype = new EventEmitter(['chunk', 'finish']);
+XDRObject.prototype._start = function(method, url, payload) {
+    var that = this;
+    var xdr = new XDomainRequest();
+    // IE caches even POSTs
+    url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date);
+
+    var onerror = xdr.ontimeout = xdr.onerror = function() {
+        that.emit('finish', 0, '');
+        that._cleanup(false);
+    };
+    xdr.onprogress = function() {
+        that.emit('chunk', 200, xdr.responseText);
+    };
+    xdr.onload = function() {
+        that.emit('finish', 200, xdr.responseText);
+        that._cleanup(false);
+    };
+    that.xdr = xdr;
+    that.unload_ref = utils.unload_add(function(){that._cleanup(true);});
+    try {
+        // Fails with AccessDenied if port number is bogus
+        that.xdr.open(method, url);
+        that.xdr.send(payload);
+    } catch(x) {
+        onerror();
+    }
+};
+
+XDRObject.prototype._cleanup = function(abort) {
+    var that = this;
+    if (!that.xdr) return;
+    utils.unload_del(that.unload_ref);
+
+    that.xdr.ontimeout = that.xdr.onerror = that.xdr.onprogress =
+        that.xdr.onload = null;
+    if (abort) {
+        try {
+            that.xdr.abort();
+        } catch(x) {};
+    }
+    that.unload_ref = that.xdr = null;
+};
+
+XDRObject.prototype.close = function() {
+    var that = this;
+    that.nuke();
+    that._cleanup(true);
+};
+
+// 1. Is natively via XHR
+// 2. Is natively via XDR
+// 3. Nope, but postMessage is there so it should work via the Iframe.
+// 4. Nope, sorry.
+utils.isXHRCorsCapable = function() {
+    if (_window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()) {
+        return 1;
+    }
+    // XDomainRequest doesn't work if page is served from file://
+    if (_window.XDomainRequest && _document.domain) {
+        return 2;
+    }
+    if (IframeTransport.enabled()) {
+        return 3;
+    }
+    return 4;
+};
+//         [*] End of lib/dom2.js
+
+
+//         [*] Including lib/sockjs.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var SockJS = function(url, dep_protocols_whitelist, options) {
+    if (this === _window) {
+        // makes `new` optional
+        return new SockJS(url, dep_protocols_whitelist, options);
+    }
+    
+    var that = this, protocols_whitelist;
+    that._options = {devel: false, debug: false, protocols_whitelist: [],
+                     info: undefined, rtt: undefined};
+    if (options) {
+        utils.objectExtend(that._options, options);
+    }
+    that._base_url = utils.amendUrl(url);
+    that._server = that._options.server || utils.random_number_string(1000);
+    if (that._options.protocols_whitelist &&
+        that._options.protocols_whitelist.length) {
+        protocols_whitelist = that._options.protocols_whitelist;
+    } else {
+        // Deprecated API
+        if (typeof dep_protocols_whitelist === 'string' &&
+            dep_protocols_whitelist.length > 0) {
+            protocols_whitelist = [dep_protocols_whitelist];
+        } else if (utils.isArray(dep_protocols_whitelist)) {
+            protocols_whitelist = dep_protocols_whitelist
+        } else {
+            protocols_whitelist = null;
+        }
+        if (protocols_whitelist) {
+            that._debug('Deprecated API: Use "protocols_whitelist" option ' +
+                        'instead of supplying protocol list as a second ' +
+                        'parameter to SockJS constructor.');
+        }
+    }
+    that._protocols = [];
+    that.protocol = null;
+    that.readyState = SockJS.CONNECTING;
+    that._ir = createInfoReceiver(that._base_url);
+    that._ir.onfinish = function(info, rtt) {
+        that._ir = null;
+        if (info) {
+            if (that._options.info) {
+                // Override if user supplies the option
+                info = utils.objectExtend(info, that._options.info);
+            }
+            if (that._options.rtt) {
+                rtt = that._options.rtt;
+            }
+            that._applyInfo(info, rtt, protocols_whitelist);
+            that._didClose();
+        } else {
+            that._didClose(1002, 'Can\'t connect to server', true);
+        }
+    };
+};
+// Inheritance
+SockJS.prototype = new REventTarget();
+
+SockJS.version = "0.3.4";
+
+SockJS.CONNECTING = 0;
+SockJS.OPEN = 1;
+SockJS.CLOSING = 2;
+SockJS.CLOSED = 3;
+
+SockJS.prototype._debug = function() {
+    if (this._options.debug)
+        utils.log.apply(utils, arguments);
+};
+
+SockJS.prototype._dispatchOpen = function() {
+    var that = this;
+    if (that.readyState === SockJS.CONNECTING) {
+        if (that._transport_tref) {
+            clearTimeout(that._transport_tref);
+            that._transport_tref = null;
+        }
+        that.readyState = SockJS.OPEN;
+        that.dispatchEvent(new SimpleEvent("open"));
+    } else {
+        // The server might have been restarted, and lost track of our
+        // connection.
+        that._didClose(1006, "Server lost session");
+    }
+};
+
+SockJS.prototype._dispatchMessage = function(data) {
+    var that = this;
+    if (that.readyState !== SockJS.OPEN)
+            return;
+    that.dispatchEvent(new SimpleEvent("message", {data: data}));
+};
+
+SockJS.prototype._dispatchHeartbeat = function(data) {
+    var that = this;
+    if (that.readyState !== SockJS.OPEN)
+        return;
+    that.dispatchEvent(new SimpleEvent('heartbeat', {}));
+};
+
+SockJS.prototype._didClose = function(code, reason, force) {
+    var that = this;
+    if (that.readyState !== SockJS.CONNECTING &&
+        that.readyState !== SockJS.OPEN &&
+        that.readyState !== SockJS.CLOSING)
+            throw new Error('INVALID_STATE_ERR');
+    if (that._ir) {
+        that._ir.nuke();
+        that._ir = null;
+    }
+
+    if (that._transport) {
+        that._transport.doCleanup();
+        that._transport = null;
+    }
+
+    var close_event = new SimpleEvent("close", {
+        code: code,
+        reason: reason,
+        wasClean: utils.userSetCode(code)});
+
+    if (!utils.userSetCode(code) &&
+        that.readyState === SockJS.CONNECTING && !force) {
+        if (that._try_next_protocol(close_event)) {
+            return;
+        }
+        close_event = new SimpleEvent("close", {code: 2000,
+                                                reason: "All transports failed",
+                                                wasClean: false,
+                                                last_event: close_event});
+    }
+    that.readyState = SockJS.CLOSED;
+
+    utils.delay(function() {
+                   that.dispatchEvent(close_event);
+                });
+};
+
+SockJS.prototype._didMessage = function(data) {
+    var that = this;
+    var type = data.slice(0, 1);
+    switch(type) {
+    case 'o':
+        that._dispatchOpen();
+        break;
+    case 'a':
+        var payload = JSON.parse(data.slice(1) || '[]');
+        for(var i=0; i < payload.length; i++){
+            that._dispatchMessage(payload[i]);
+        }
+        break;
+    case 'm':
+        var payload = JSON.parse(data.slice(1) || 'null');
+        that._dispatchMessage(payload);
+        break;
+    case 'c':
+        var payload = JSON.parse(data.slice(1) || '[]');
+        that._didClose(payload[0], payload[1]);
+        break;
+    case 'h':
+        that._dispatchHeartbeat();
+        break;
+    }
+};
+
+SockJS.prototype._try_next_protocol = function(close_event) {
+    var that = this;
+    if (that.protocol) {
+        that._debug('Closed transport:', that.protocol, ''+close_event);
+        that.protocol = null;
+    }
+    if (that._transport_tref) {
+        clearTimeout(that._transport_tref);
+        that._transport_tref = null;
+    }
+
+    while(1) {
+        var protocol = that.protocol = that._protocols.shift();
+        if (!protocol) {
+            return false;
+        }
+        // Some protocols require access to `body`, what if were in
+        // the `head`?
+        if (SockJS[protocol] &&
+            SockJS[protocol].need_body === true &&
+            (!_document.body ||
+             (typeof _document.readyState !== 'undefined'
+              && _document.readyState !== 'complete'))) {
+            that._protocols.unshift(protocol);
+            that.protocol = 'waiting-for-load';
+            utils.attachEvent('load', function(){
+                that._try_next_protocol();
+            });
+            return true;
+        }
+
+        if (!SockJS[protocol] ||
+              !SockJS[protocol].enabled(that._options)) {
+            that._debug('Skipping transport:', protocol);
+        } else {
+            var roundTrips = SockJS[protocol].roundTrips || 1;
+            var to = ((that._options.rto || 0) * roundTrips) || 5000;
+            that._transport_tref = utils.delay(to, function() {
+                if (that.readyState === SockJS.CONNECTING) {
+                    // I can't understand how it is possible to run
+                    // this timer, when the state is CLOSED, but
+                    // apparently in IE everythin is possible.
+                    that._didClose(2007, "Transport timeouted");
+                }
+            });
+
+            var connid = utils.random_string(8);
+            var trans_url = that._base_url + '/' + that._server + '/' + connid;
+            that._debug('Opening transport:', protocol, ' url:'+trans_url,
+                        ' RTO:'+that._options.rto);
+            that._transport = new SockJS[protocol](that, trans_url,
+                                                   that._base_url);
+            return true;
+        }
+    }
+};
+
+SockJS.prototype.close = function(code, reason) {
+    var that = this;
+    if (code && !utils.userSetCode(code))
+        throw new Error("INVALID_ACCESS_ERR");
+    if(that.readyState !== SockJS.CONNECTING &&
+       that.readyState !== SockJS.OPEN) {
+        return false;
+    }
+    that.readyState = SockJS.CLOSING;
+    that._didClose(code || 1000, reason || "Normal closure");
+    return true;
+};
+
+SockJS.prototype.send = function(data) {
+    var that = this;
+    if (that.readyState === SockJS.CONNECTING)
+        throw new Error('INVALID_STATE_ERR');
+    if (that.readyState === SockJS.OPEN) {
+        that._transport.doSend(utils.quote('' + data));
+    }
+    return true;
+};
+
+SockJS.prototype._applyInfo = function(info, rtt, protocols_whitelist) {
+    var that = this;
+    that._options.info = info;
+    that._options.rtt = rtt;
+    that._options.rto = utils.countRTO(rtt);
+    that._options.info.null_origin = !_document.domain;
+    var probed = utils.probeProtocols();
+    that._protocols = utils.detectProtocols(probed, protocols_whitelist, info);
+};
+//         [*] End of lib/sockjs.js
+
+
+//         [*] Including lib/trans-websocket.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var WebSocketTransport = SockJS.websocket = function(ri, trans_url) {
+    var that = this;
+    var url = trans_url + '/websocket';
+    if (url.slice(0, 5) === 'https') {
+        url = 'wss' + url.slice(5);
+    } else {
+        url = 'ws' + url.slice(4);
+    }
+    that.ri = ri;
+    that.url = url;
+    var Constructor = _window.WebSocket || _window.MozWebSocket;
+
+    that.ws = new Constructor(that.url);
+    that.ws.onmessage = function(e) {
+        that.ri._didMessage(e.data);
+    };
+    // Firefox has an interesting bug. If a websocket connection is
+    // created after onunload, it stays alive even when user
+    // navigates away from the page. In such situation let's lie -
+    // let's not open the ws connection at all. See:
+    // https://github.com/sockjs/sockjs-client/issues/28
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=696085
+    that.unload_ref = utils.unload_add(function(){that.ws.close()});
+    that.ws.onclose = function() {
+        that.ri._didMessage(utils.closeFrame(1006, "WebSocket connection broken"));
+    };
+};
+
+WebSocketTransport.prototype.doSend = function(data) {
+    this.ws.send('[' + data + ']');
+};
+
+WebSocketTransport.prototype.doCleanup = function() {
+    var that = this;
+    var ws = that.ws;
+    if (ws) {
+        ws.onmessage = ws.onclose = null;
+        ws.close();
+        utils.unload_del(that.unload_ref);
+        that.unload_ref = that.ri = that.ws = null;
+    }
+};
+
+WebSocketTransport.enabled = function() {
+    return !!(_window.WebSocket || _window.MozWebSocket);
+};
+
+// In theory, ws should require 1 round trip. But in chrome, this is
+// not very stable over SSL. Most likely a ws connection requires a
+// separate SSL connection, in which case 2 round trips are an
+// absolute minumum.
+WebSocketTransport.roundTrips = 2;
+//         [*] End of lib/trans-websocket.js
+
+
+//         [*] Including lib/trans-sender.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var BufferedSender = function() {};
+BufferedSender.prototype.send_constructor = function(sender) {
+    var that = this;
+    that.send_buffer = [];
+    that.sender = sender;
+};
+BufferedSender.prototype.doSend = function(message) {
+    var that = this;
+    that.send_buffer.push(message);
+    if (!that.send_stop) {
+        that.send_schedule();
+    }
+};
+
+// For polling transports in a situation when in the message callback,
+// new message is being send. If the sending connection was started
+// before receiving one, it is possible to saturate the network and
+// timeout due to the lack of receiving socket. To avoid that we delay
+// sending messages by some small time, in order to let receiving
+// connection be started beforehand. This is only a halfmeasure and
+// does not fix the big problem, but it does make the tests go more
+// stable on slow networks.
+BufferedSender.prototype.send_schedule_wait = function() {
+    var that = this;
+    var tref;
+    that.send_stop = function() {
+        that.send_stop = null;
+        clearTimeout(tref);
+    };
+    tref = utils.delay(25, function() {
+        that.send_stop = null;
+        that.send_schedule();
+    });
+};
+
+BufferedSender.prototype.send_schedule = function() {
+    var that = this;
+    if (that.send_buffer.length > 0) {
+        var payload = '[' + that.send_buffer.join(',') + ']';
+        that.send_stop = that.sender(that.trans_url, payload, function(success, abort_reason) {
+            that.send_stop = null;
+            if (success === false) {
+                that.ri._didClose(1006, 'Sending error ' + abort_reason);
+            } else {
+                that.send_schedule_wait();
+            }
+        });
+        that.send_buffer = [];
+    }
+};
+
+BufferedSender.prototype.send_destructor = function() {
+    var that = this;
+    if (that._send_stop) {
+        that._send_stop();
+    }
+    that._send_stop = null;
+};
+
+var jsonPGenericSender = function(url, payload, callback) {
+    var that = this;
+
+    if (!('_send_form' in that)) {
+        var form = that._send_form = _document.createElement('form');
+        var area = that._send_area = _document.createElement('textarea');
+        area.name = 'd';
+        form.style.display = 'none';
+        form.style.position = 'absolute';
+        form.method = 'POST';
+        form.enctype = 'application/x-www-form-urlencoded';
+        form.acceptCharset = "UTF-8";
+        form.appendChild(area);
+        _document.body.appendChild(form);
+    }
+    var form = that._send_form;
+    var area = that._send_area;
+    var id = 'a' + utils.random_string(8);
+    form.target = id;
+    form.action = url + '/jsonp_send?i=' + id;
+
+    var iframe;
+    try {
+        // ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
+        iframe = _document.createElement('<iframe name="'+ id +'">');
+    } catch(x) {
+        iframe = _document.createElement('iframe');
+        iframe.name = id;
+    }
+    iframe.id = id;
+    form.appendChild(iframe);
+    iframe.style.display = 'none';
+
+    try {
+        area.value = payload;
+    } catch(e) {
+        utils.log('Your browser is seriously broken. Go home! ' + e.message);
+    }
+    form.submit();
+
+    var completed = function(e) {
+        if (!iframe.onerror) return;
+        iframe.onreadystatechange = iframe.onerror = iframe.onload = null;
+        // Opera mini doesn't like if we GC iframe
+        // immediately, thus this timeout.
+        utils.delay(500, function() {
+                       iframe.parentNode.removeChild(iframe);
+                       iframe = null;
+                   });
+        area.value = '';
+        // It is not possible to detect if the iframe succeeded or
+        // failed to submit our form.
+        callback(true);
+    };
+    iframe.onerror = iframe.onload = completed;
+    iframe.onreadystatechange = function(e) {
+        if (iframe.readyState == 'complete') completed();
+    };
+    return completed;
+};
+
+var createAjaxSender = function(AjaxObject) {
+    return function(url, payload, callback) {
+        var xo = new AjaxObject('POST', url + '/xhr_send', payload);
+        xo.onfinish = function(status, text) {
+            callback(status === 200 || status === 204,
+                     'http status ' + status);
+        };
+        return function(abort_reason) {
+            callback(false, abort_reason);
+        };
+    };
+};
+//         [*] End of lib/trans-sender.js
+
+
+//         [*] Including lib/trans-jsonp-receiver.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// Parts derived from Socket.io:
+//    https://github.com/LearnBoost/socket.io/blob/0.6.17/lib/socket.io/transports/jsonp-polling.js
+// and jQuery-JSONP:
+//    https://code.google.com/p/jquery-jsonp/source/browse/trunk/core/jquery.jsonp.js
+var jsonPGenericReceiver = function(url, callback) {
+    var tref;
+    var script = _document.createElement('script');
+    var script2;  // Opera synchronous load trick.
+    var close_script = function(frame) {
+        if (script2) {
+            script2.parentNode.removeChild(script2);
+            script2 = null;
+        }
+        if (script) {
+            clearTimeout(tref);
+            // Unfortunately, you can't really abort script loading of
+            // the script.
+            script.parentNode.removeChild(script);
+            script.onreadystatechange = script.onerror =
+                script.onload = script.onclick = null;
+            script = null;
+            callback(frame);
+            callback = null;
+        }
+    };
+
+    // IE9 fires 'error' event after orsc or before, in random order.
+    var loaded_okay = false;
+    var error_timer = null;
+
+    script.id = 'a' + utils.random_string(8);
+    script.src = url;
+    script.type = 'text/javascript';
+    script.charset = 'UTF-8';
+    script.onerror = function(e) {
+        if (!error_timer) {
+            // Delay firing close_script.
+            error_timer = setTimeout(function() {
+                if (!loaded_okay) {
+                    close_script(utils.closeFrame(
+                        1006,
+                        "JSONP script loaded abnormally (onerror)"));
+                }
+            }, 1000);
+        }
+    };
+    script.onload = function(e) {
+        close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onload)"));
+    };
+
+    script.onreadystatechange = function(e) {
+        if (/loaded|closed/.test(script.readyState)) {
+            if (script && script.htmlFor && script.onclick) {
+                loaded_okay = true;
+                try {
+                    // In IE, actually execute the script.
+                    script.onclick();
+                } catch (x) {}
+            }
+            if (script) {
+                close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onreadystatechange)"));
+            }
+        }
+    };
+    // IE: event/htmlFor/onclick trick.
+    // One can't rely on proper order for onreadystatechange. In order to
+    // make sure, set a 'htmlFor' and 'event' properties, so that
+    // script code will be installed as 'onclick' handler for the
+    // script object. Later, onreadystatechange, manually execute this
+    // code. FF and Chrome doesn't work with 'event' and 'htmlFor'
+    // set. For reference see:
+    //   http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
+    // Also, read on that about script ordering:
+    //   http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
+    if (typeof script.async === 'undefined' && _document.attachEvent) {
+        // According to mozilla docs, in recent browsers script.async defaults
+        // to 'true', so we may use it to detect a good browser:
+        // https://developer.mozilla.org/en/HTML/Element/script
+        if (!/opera/i.test(navigator.userAgent)) {
+            // Naively assume we're in IE
+            try {
+                script.htmlFor = script.id;
+                script.event = "onclick";
+            } catch (x) {}
+            script.async = true;
+        } else {
+            // Opera, second sync script hack
+            script2 = _document.createElement('script');
+            script2.text = "try{var a = document.getElementById('"+script.id+"'); if(a)a.onerror();}catch(x){};";
+            script.async = script2.async = false;
+        }
+    }
+    if (typeof script.async !== 'undefined') {
+        script.async = true;
+    }
+
+    // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty.
+    tref = setTimeout(function() {
+                          close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (timeout)"));
+                      }, 35000);
+
+    var head = _document.getElementsByTagName('head')[0];
+    head.insertBefore(script, head.firstChild);
+    if (script2) {
+        head.insertBefore(script2, head.firstChild);
+    }
+    return close_script;
+};
+//         [*] End of lib/trans-jsonp-receiver.js
+
+
+//         [*] Including lib/trans-jsonp-polling.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// The simplest and most robust transport, using the well-know cross
+// domain hack - JSONP. This transport is quite inefficient - one
+// mssage could use up to one http request. But at least it works almost
+// everywhere.
+// Known limitations:
+//   o you will get a spinning cursor
+//   o for Konqueror a dumb timer is needed to detect errors
+
+
+var JsonPTransport = SockJS['jsonp-polling'] = function(ri, trans_url) {
+    utils.polluteGlobalNamespace();
+    var that = this;
+    that.ri = ri;
+    that.trans_url = trans_url;
+    that.send_constructor(jsonPGenericSender);
+    that._schedule_recv();
+};
+
+// Inheritnace
+JsonPTransport.prototype = new BufferedSender();
+
+JsonPTransport.prototype._schedule_recv = function() {
+    var that = this;
+    var callback = function(data) {
+        that._recv_stop = null;
+        if (data) {
+            // no data - heartbeat;
+            if (!that._is_closing) {
+                that.ri._didMessage(data);
+            }
+        }
+        // The message can be a close message, and change is_closing state.
+        if (!that._is_closing) {
+            that._schedule_recv();
+        }
+    };
+    that._recv_stop = jsonPReceiverWrapper(that.trans_url + '/jsonp',
+                                           jsonPGenericReceiver, callback);
+};
+
+JsonPTransport.enabled = function() {
+    return true;
+};
+
+JsonPTransport.need_body = true;
+
+
+JsonPTransport.prototype.doCleanup = function() {
+    var that = this;
+    that._is_closing = true;
+    if (that._recv_stop) {
+        that._recv_stop();
+    }
+    that.ri = that._recv_stop = null;
+    that.send_destructor();
+};
+
+
+// Abstract away code that handles global namespace pollution.
+var jsonPReceiverWrapper = function(url, constructReceiver, user_callback) {
+    var id = 'a' + utils.random_string(6);
+    var url_id = url + '?c=' + escape(WPrefix + '.' + id);
+
+    // Unfortunately it is not possible to abort loading of the
+    // script. We need to keep track of frake close frames.
+    var aborting = 0;
+
+    // Callback will be called exactly once.
+    var callback = function(frame) {
+        switch(aborting) {
+        case 0:
+            // Normal behaviour - delete hook _and_ emit message.
+            delete _window[WPrefix][id];
+            user_callback(frame);
+            break;
+        case 1:
+            // Fake close frame - emit but don't delete hook.
+            user_callback(frame);
+            aborting = 2;
+            break;
+        case 2:
+            // Got frame after connection was closed, delete hook, don't emit.
+            delete _window[WPrefix][id];
+            break;
+        }
+    };
+
+    var close_script = constructReceiver(url_id, callback);
+    _window[WPrefix][id] = close_script;
+    var stop = function() {
+        if (_window[WPrefix][id]) {
+            aborting = 1;
+            _window[WPrefix][id](utils.closeFrame(1000, "JSONP user aborted read"));
+        }
+    };
+    return stop;
+};
+//         [*] End of lib/trans-jsonp-polling.js
+
+
+//         [*] Including lib/trans-xhr.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var AjaxBasedTransport = function() {};
+AjaxBasedTransport.prototype = new BufferedSender();
+
+AjaxBasedTransport.prototype.run = function(ri, trans_url,
+                                            url_suffix, Receiver, AjaxObject) {
+    var that = this;
+    that.ri = ri;
+    that.trans_url = trans_url;
+    that.send_constructor(createAjaxSender(AjaxObject));
+    that.poll = new Polling(ri, Receiver,
+                            trans_url + url_suffix, AjaxObject);
+};
+
+AjaxBasedTransport.prototype.doCleanup = function() {
+    var that = this;
+    if (that.poll) {
+        that.poll.abort();
+        that.poll = null;
+    }
+};
+
+// xhr-streaming
+var XhrStreamingTransport = SockJS['xhr-streaming'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XHRCorsObject);
+};
+
+XhrStreamingTransport.prototype = new AjaxBasedTransport();
+
+XhrStreamingTransport.enabled = function() {
+    // Support for CORS Ajax aka Ajax2? Opera 12 claims CORS but
+    // doesn't do streaming.
+    return (_window.XMLHttpRequest &&
+            'withCredentials' in new XMLHttpRequest() &&
+            (!/opera/i.test(navigator.userAgent)));
+};
+XhrStreamingTransport.roundTrips = 2; // preflight, ajax
+
+// Safari gets confused when a streaming ajax request is started
+// before onload. This causes the load indicator to spin indefinetely.
+XhrStreamingTransport.need_body = true;
+
+
+// According to:
+//   http://stackoverflow.com/questions/1641507/detect-browser-support-for-cross-domain-xmlhttprequests
+//   http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
+
+
+// xdr-streaming
+var XdrStreamingTransport = SockJS['xdr-streaming'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XDRObject);
+};
+
+XdrStreamingTransport.prototype = new AjaxBasedTransport();
+
+XdrStreamingTransport.enabled = function() {
+    return !!_window.XDomainRequest;
+};
+XdrStreamingTransport.roundTrips = 2; // preflight, ajax
+
+
+
+// xhr-polling
+var XhrPollingTransport = SockJS['xhr-polling'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRCorsObject);
+};
+
+XhrPollingTransport.prototype = new AjaxBasedTransport();
+
+XhrPollingTransport.enabled = XhrStreamingTransport.enabled;
+XhrPollingTransport.roundTrips = 2; // preflight, ajax
+
+
+// xdr-polling
+var XdrPollingTransport = SockJS['xdr-polling'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XDRObject);
+};
+
+XdrPollingTransport.prototype = new AjaxBasedTransport();
+
+XdrPollingTransport.enabled = XdrStreamingTransport.enabled;
+XdrPollingTransport.roundTrips = 2; // preflight, ajax
+//         [*] End of lib/trans-xhr.js
+
+
+//         [*] Including lib/trans-iframe.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// Few cool transports do work only for same-origin. In order to make
+// them working cross-domain we shall use iframe, served form the
+// remote domain. New browsers, have capabilities to communicate with
+// cross domain iframe, using postMessage(). In IE it was implemented
+// from IE 8+, but of course, IE got some details wrong:
+//    http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx
+//    http://stevesouders.com/misc/test-postmessage.php
+
+var IframeTransport = function() {};
+
+IframeTransport.prototype.i_constructor = function(ri, trans_url, base_url) {
+    var that = this;
+    that.ri = ri;
+    that.origin = utils.getOrigin(base_url);
+    that.base_url = base_url;
+    that.trans_url = trans_url;
+
+    var iframe_url = base_url + '/iframe.html';
+    if (that.ri._options.devel) {
+        iframe_url += '?t=' + (+new Date);
+    }
+    that.window_id = utils.random_string(8);
+    iframe_url += '#' + that.window_id;
+
+    that.iframeObj = utils.createIframe(iframe_url, function(r) {
+                                            that.ri._didClose(1006, "Unable to load an iframe (" + r + ")");
+                                        });
+
+    that.onmessage_cb = utils.bind(that.onmessage, that);
+    utils.attachMessage(that.onmessage_cb);
+};
+
+IframeTransport.prototype.doCleanup = function() {
+    var that = this;
+    if (that.iframeObj) {
+        utils.detachMessage(that.onmessage_cb);
+        try {
+            // When the iframe is not loaded, IE raises an exception
+            // on 'contentWindow'.
+            if (that.iframeObj.iframe.contentWindow) {
+                that.postMessage('c');
+            }
+        } catch (x) {}
+        that.iframeObj.cleanup();
+        that.iframeObj = null;
+        that.onmessage_cb = that.iframeObj = null;
+    }
+};
+
+IframeTransport.prototype.onmessage = function(e) {
+    var that = this;
+    if (e.origin !== that.origin) return;
+    var window_id = e.data.slice(0, 8);
+    var type = e.data.slice(8, 9);
+    var data = e.data.slice(9);
+
+    if (window_id !== that.window_id) return;
+
+    switch(type) {
+    case 's':
+        that.iframeObj.loaded();
+        that.postMessage('s', JSON.stringify([SockJS.version, that.protocol, that.trans_url, that.base_url]));
+        break;
+    case 't':
+        that.ri._didMessage(data);
+        break;
+    }
+};
+
+IframeTransport.prototype.postMessage = function(type, data) {
+    var that = this;
+    that.iframeObj.post(that.window_id + type + (data || ''), that.origin);
+};
+
+IframeTransport.prototype.doSend = function (message) {
+    this.postMessage('m', message);
+};
+
+IframeTransport.enabled = function() {
+    // postMessage misbehaves in konqueror 4.6.5 - the messages are delivered with
+    // huge delay, or not at all.
+    var konqueror = navigator && navigator.userAgent && navigator.userAgent.indexOf('Konqueror') !== -1;
+    return ((typeof _window.postMessage === 'function' ||
+            typeof _window.postMessage === 'object') && (!konqueror));
+};
+//         [*] End of lib/trans-iframe.js
+
+
+//         [*] Including lib/trans-iframe-within.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var curr_window_id;
+
+var postMessage = function (type, data) {
+    if(parent !== _window) {
+        parent.postMessage(curr_window_id + type + (data || ''), '*');
+    } else {
+        utils.log("Can't postMessage, no parent window.", type, data);
+    }
+};
+
+var FacadeJS = function() {};
+FacadeJS.prototype._didClose = function (code, reason) {
+    postMessage('t', utils.closeFrame(code, reason));
+};
+FacadeJS.prototype._didMessage = function (frame) {
+    postMessage('t', frame);
+};
+FacadeJS.prototype._doSend = function (data) {
+    this._transport.doSend(data);
+};
+FacadeJS.prototype._doCleanup = function () {
+    this._transport.doCleanup();
+};
+
+utils.parent_origin = undefined;
+
+SockJS.bootstrap_iframe = function() {
+    var facade;
+    curr_window_id = _document.location.hash.slice(1);
+    var onMessage = function(e) {
+        if(e.source !== parent) return;
+        if(typeof utils.parent_origin === 'undefined')
+            utils.parent_origin = e.origin;
+        if (e.origin !== utils.parent_origin) return;
+
+        var window_id = e.data.slice(0, 8);
+        var type = e.data.slice(8, 9);
+        var data = e.data.slice(9);
+        if (window_id !== curr_window_id) return;
+        switch(type) {
+        case 's':
+            var p = JSON.parse(data);
+            var version = p[0];
+            var protocol = p[1];
+            var trans_url = p[2];
+            var base_url = p[3];
+            if (version !== SockJS.version) {
+                utils.log("Incompatibile SockJS! Main site uses:" +
+                          " \"" + version + "\", the iframe:" +
+                          " \"" + SockJS.version + "\".");
+            }
+            if (!utils.flatUrl(trans_url) || !utils.flatUrl(base_url)) {
+                utils.log("Only basic urls are supported in SockJS");
+                return;
+            }
+
+            if (!utils.isSameOriginUrl(trans_url) ||
+                !utils.isSameOriginUrl(base_url)) {
+                utils.log("Can't connect to different domain from within an " +
+                          "iframe. (" + JSON.stringify([_window.location.href, trans_url, base_url]) +
+                          ")");
+                return;
+            }
+            facade = new FacadeJS();
+            facade._transport = new FacadeJS[protocol](facade, trans_url, base_url);
+            break;
+        case 'm':
+            facade._doSend(data);
+            break;
+        case 'c':
+            if (facade)
+                facade._doCleanup();
+            facade = null;
+            break;
+        }
+    };
+
+    // alert('test ticker');
+    // facade = new FacadeJS();
+    // facade._transport = new FacadeJS['w-iframe-xhr-polling'](facade, 'http://host.com:9999/ticker/12/basd');
+
+    utils.attachMessage(onMessage);
+
+    // Start
+    postMessage('s');
+};
+//         [*] End of lib/trans-iframe-within.js
+
+
+//         [*] Including lib/info.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var InfoReceiver = function(base_url, AjaxObject) {
+    var that = this;
+    utils.delay(function(){that.doXhr(base_url, AjaxObject);});
+};
+
+InfoReceiver.prototype = new EventEmitter(['finish']);
+
+InfoReceiver.prototype.doXhr = function(base_url, AjaxObject) {
+    var that = this;
+    var t0 = (new Date()).getTime();
+    var xo = new AjaxObject('GET', base_url + '/info');
+
+    var tref = utils.delay(8000,
+                           function(){xo.ontimeout();});
+
+    xo.onfinish = function(status, text) {
+        clearTimeout(tref);
+        tref = null;
+        if (status === 200) {
+            var rtt = (new Date()).getTime() - t0;
+            var info = JSON.parse(text);
+            if (typeof info !== 'object') info = {};
+            that.emit('finish', info, rtt);
+        } else {
+            that.emit('finish');
+        }
+    };
+    xo.ontimeout = function() {
+        xo.close();
+        that.emit('finish');
+    };
+};
+
+var InfoReceiverIframe = function(base_url) {
+    var that = this;
+    var go = function() {
+        var ifr = new IframeTransport();
+        ifr.protocol = 'w-iframe-info-receiver';
+        var fun = function(r) {
+            if (typeof r === 'string' && r.substr(0,1) === 'm') {
+                var d = JSON.parse(r.substr(1));
+                var info = d[0], rtt = d[1];
+                that.emit('finish', info, rtt);
+            } else {
+                that.emit('finish');
+            }
+            ifr.doCleanup();
+            ifr = null;
+        };
+        var mock_ri = {
+            _options: {},
+            _didClose: fun,
+            _didMessage: fun
+        };
+        ifr.i_constructor(mock_ri, base_url, base_url);
+    }
+    if(!_document.body) {
+        utils.attachEvent('load', go);
+    } else {
+        go();
+    }
+};
+InfoReceiverIframe.prototype = new EventEmitter(['finish']);
+
+
+var InfoReceiverFake = function() {
+    // It may not be possible to do cross domain AJAX to get the info
+    // data, for example for IE7. But we want to run JSONP, so let's
+    // fake the response, with rtt=2s (rto=6s).
+    var that = this;
+    utils.delay(function() {
+        that.emit('finish', {}, 2000);
+    });
+};
+InfoReceiverFake.prototype = new EventEmitter(['finish']);
+
+var createInfoReceiver = function(base_url) {
+    if (utils.isSameOriginUrl(base_url)) {
+        // If, for some reason, we have SockJS locally - there's no
+        // need to start up the complex machinery. Just use ajax.
+        return new InfoReceiver(base_url, utils.XHRLocalObject);
+    }
+    switch (utils.isXHRCorsCapable()) {
+    case 1:
+        // XHRLocalObject -> no_credentials=true
+        return new InfoReceiver(base_url, utils.XHRLocalObject);
+    case 2:
+        return new InfoReceiver(base_url, utils.XDRObject);
+    case 3:
+        // Opera
+        return new InfoReceiverIframe(base_url);
+    default:
+        // IE 7
+        return new InfoReceiverFake();
+    };
+};
+
+
+var WInfoReceiverIframe = FacadeJS['w-iframe-info-receiver'] = function(ri, _trans_url, base_url) {
+    var ir = new InfoReceiver(base_url, utils.XHRLocalObject);
+    ir.onfinish = function(info, rtt) {
+        ri._didMessage('m'+JSON.stringify([info, rtt]));
+        ri._didClose();
+    }
+};
+WInfoReceiverIframe.prototype.doCleanup = function() {};
+//         [*] End of lib/info.js
+
+
+//         [*] Including lib/trans-iframe-eventsource.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EventSourceIframeTransport = SockJS['iframe-eventsource'] = function () {
+    var that = this;
+    that.protocol = 'w-iframe-eventsource';
+    that.i_constructor.apply(that, arguments);
+};
+
+EventSourceIframeTransport.prototype = new IframeTransport();
+
+EventSourceIframeTransport.enabled = function () {
+    return ('EventSource' in _window) && IframeTransport.enabled();
+};
+
+EventSourceIframeTransport.need_body = true;
+EventSourceIframeTransport.roundTrips = 3; // html, javascript, eventsource
+
+
+// w-iframe-eventsource
+var EventSourceTransport = FacadeJS['w-iframe-eventsource'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/eventsource', EventSourceReceiver, utils.XHRLocalObject);
+}
+EventSourceTransport.prototype = new AjaxBasedTransport();
+//         [*] End of lib/trans-iframe-eventsource.js
+
+
+//         [*] Including lib/trans-iframe-xhr-polling.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var XhrPollingIframeTransport = SockJS['iframe-xhr-polling'] = function () {
+    var that = this;
+    that.protocol = 'w-iframe-xhr-polling';
+    that.i_constructor.apply(that, arguments);
+};
+
+XhrPollingIframeTransport.prototype = new IframeTransport();
+
+XhrPollingIframeTransport.enabled = function () {
+    return _window.XMLHttpRequest && IframeTransport.enabled();
+};
+
+XhrPollingIframeTransport.need_body = true;
+XhrPollingIframeTransport.roundTrips = 3; // html, javascript, xhr
+
+
+// w-iframe-xhr-polling
+var XhrPollingITransport = FacadeJS['w-iframe-xhr-polling'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRLocalObject);
+};
+
+XhrPollingITransport.prototype = new AjaxBasedTransport();
+//         [*] End of lib/trans-iframe-xhr-polling.js
+
+
+//         [*] Including lib/trans-iframe-htmlfile.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// This transport generally works in any browser, but will cause a
+// spinning cursor to appear in any browser other than IE.
+// We may test this transport in all browsers - why not, but in
+// production it should be only run in IE.
+
+var HtmlFileIframeTransport = SockJS['iframe-htmlfile'] = function () {
+    var that = this;
+    that.protocol = 'w-iframe-htmlfile';
+    that.i_constructor.apply(that, arguments);
+};
+
+// Inheritance.
+HtmlFileIframeTransport.prototype = new IframeTransport();
+
+HtmlFileIframeTransport.enabled = function() {
+    return IframeTransport.enabled();
+};
+
+HtmlFileIframeTransport.need_body = true;
+HtmlFileIframeTransport.roundTrips = 3; // html, javascript, htmlfile
+
+
+// w-iframe-htmlfile
+var HtmlFileTransport = FacadeJS['w-iframe-htmlfile'] = function(ri, trans_url) {
+    this.run(ri, trans_url, '/htmlfile', HtmlfileReceiver, utils.XHRLocalObject);
+};
+HtmlFileTransport.prototype = new AjaxBasedTransport();
+//         [*] End of lib/trans-iframe-htmlfile.js
+
+
+//         [*] Including lib/trans-polling.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var Polling = function(ri, Receiver, recv_url, AjaxObject) {
+    var that = this;
+    that.ri = ri;
+    that.Receiver = Receiver;
+    that.recv_url = recv_url;
+    that.AjaxObject = AjaxObject;
+    that._scheduleRecv();
+};
+
+Polling.prototype._scheduleRecv = function() {
+    var that = this;
+    var poll = that.poll = new that.Receiver(that.recv_url, that.AjaxObject);
+    var msg_counter = 0;
+    poll.onmessage = function(e) {
+        msg_counter += 1;
+        that.ri._didMessage(e.data);
+    };
+    poll.onclose = function(e) {
+        that.poll = poll = poll.onmessage = poll.onclose = null;
+        if (!that.poll_is_closing) {
+            if (e.reason === 'permanent') {
+                that.ri._didClose(1006, 'Polling error (' + e.reason + ')');
+            } else {
+                that._scheduleRecv();
+            }
+        }
+    };
+};
+
+Polling.prototype.abort = function() {
+    var that = this;
+    that.poll_is_closing = true;
+    if (that.poll) {
+        that.poll.abort();
+    }
+};
+//         [*] End of lib/trans-polling.js
+
+
+//         [*] Including lib/trans-receiver-eventsource.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var EventSourceReceiver = function(url) {
+    var that = this;
+    var es = new EventSource(url);
+    es.onmessage = function(e) {
+        that.dispatchEvent(new SimpleEvent('message',
+                                           {'data': unescape(e.data)}));
+    };
+    that.es_close = es.onerror = function(e, abort_reason) {
+        // ES on reconnection has readyState = 0 or 1.
+        // on network error it's CLOSED = 2
+        var reason = abort_reason ? 'user' :
+            (es.readyState !== 2 ? 'network' : 'permanent');
+        that.es_close = es.onmessage = es.onerror = null;
+        // EventSource reconnects automatically.
+        es.close();
+        es = null;
+        // Safari and chrome < 15 crash if we close window before
+        // waiting for ES cleanup. See:
+        //   https://code.google.com/p/chromium/issues/detail?id=89155
+        utils.delay(200, function() {
+                        that.dispatchEvent(new SimpleEvent('close', {reason: reason}));
+                    });
+    };
+};
+
+EventSourceReceiver.prototype = new REventTarget();
+
+EventSourceReceiver.prototype.abort = function() {
+    var that = this;
+    if (that.es_close) {
+        that.es_close({}, true);
+    }
+};
+//         [*] End of lib/trans-receiver-eventsource.js
+
+
+//         [*] Including lib/trans-receiver-htmlfile.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var _is_ie_htmlfile_capable;
+var isIeHtmlfileCapable = function() {
+    if (_is_ie_htmlfile_capable === undefined) {
+        if ('ActiveXObject' in _window) {
+            try {
+                _is_ie_htmlfile_capable = !!new ActiveXObject('htmlfile');
+            } catch (x) {}
+        } else {
+            _is_ie_htmlfile_capable = false;
+        }
+    }
+    return _is_ie_htmlfile_capable;
+};
+
+
+var HtmlfileReceiver = function(url) {
+    var that = this;
+    utils.polluteGlobalNamespace();
+
+    that.id = 'a' + utils.random_string(6, 26);
+    url += ((url.indexOf('?') === -1) ? '?' : '&') +
+        'c=' + escape(WPrefix + '.' + that.id);
+
+    var constructor = isIeHtmlfileCapable() ?
+        utils.createHtmlfile : utils.createIframe;
+
+    var iframeObj;
+    _window[WPrefix][that.id] = {
+        start: function () {
+            iframeObj.loaded();
+        },
+        message: function (data) {
+            that.dispatchEvent(new SimpleEvent('message', {'data': data}));
+        },
+        stop: function () {
+            that.iframe_close({}, 'network');
+        }
+    };
+    that.iframe_close = function(e, abort_reason) {
+        iframeObj.cleanup();
+        that.iframe_close = iframeObj = null;
+        delete _window[WPrefix][that.id];
+        that.dispatchEvent(new SimpleEvent('close', {reason: abort_reason}));
+    };
+    iframeObj = constructor(url, function(e) {
+                                that.iframe_close({}, 'permanent');
+                            });
+};
+
+HtmlfileReceiver.prototype = new REventTarget();
+
+HtmlfileReceiver.prototype.abort = function() {
+    var that = this;
+    if (that.iframe_close) {
+        that.iframe_close({}, 'user');
+    }
+};
+//         [*] End of lib/trans-receiver-htmlfile.js
+
+
+//         [*] Including lib/trans-receiver-xhr.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+var XhrReceiver = function(url, AjaxObject) {
+    var that = this;
+    var buf_pos = 0;
+
+    that.xo = new AjaxObject('POST', url, null);
+    that.xo.onchunk = function(status, text) {
+        if (status !== 200) return;
+        while (1) {
+            var buf = text.slice(buf_pos);
+            var p = buf.indexOf('\n');
+            if (p === -1) break;
+            buf_pos += p+1;
+            var msg = buf.slice(0, p);
+            that.dispatchEvent(new SimpleEvent('message', {data: msg}));
+        }
+    };
+    that.xo.onfinish = function(status, text) {
+        that.xo.onchunk(status, text);
+        that.xo = null;
+        var reason = status === 200 ? 'network' : 'permanent';
+        that.dispatchEvent(new SimpleEvent('close', {reason: reason}));
+    }
+};
+
+XhrReceiver.prototype = new REventTarget();
+
+XhrReceiver.prototype.abort = function() {
+    var that = this;
+    if (that.xo) {
+        that.xo.close();
+        that.dispatchEvent(new SimpleEvent('close', {reason: 'user'}));
+        that.xo = null;
+    }
+};
+//         [*] End of lib/trans-receiver-xhr.js
+
+
+//         [*] Including lib/test-hooks.js
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Copyright (c) 2011-2012 VMware, Inc.
+ *
+ * For the license see COPYING.
+ * ***** END LICENSE BLOCK *****
+ */
+
+// For testing
+SockJS.getUtils = function(){
+    return utils;
+};
+
+SockJS.getIframeTransport = function(){
+    return IframeTransport;
+};
+//         [*] End of lib/test-hooks.js
+
+                  return SockJS;
+          })();
+if ('_sockjs_onload' in window) setTimeout(_sockjs_onload, 1);
+
+// AMD compliance
+if (typeof define === 'function' && define.amd) {
+    define('sockjs', [], function(){return SockJS;});
+}
+//     [*] End of lib/index.js
+
+// [*] End of lib/all.js
+
+
+// Generated by CoffeeScript 1.7.1
+
+/*
+   Stomp Over WebSocket http://www.jmesnil.net/stomp-websocket/doc/ | Apache License V2.0
+
+   Copyright (C) 2010-2013 [Jeff Mesnil](http://jmesnil.net/)
+   Copyright (C) 2012 [FuseSource, Inc.](http://fusesource.com)
+ */
+
+(function() {
+  var Byte, Client, Frame, Stomp,
+    __hasProp = {}.hasOwnProperty,
+    __slice = [].slice;
+
+  Byte = {
+    LF: '\x0A',
+    NULL: '\x00'
+  };
+
+  Frame = (function() {
+    var unmarshallSingle;
+
+    function Frame(command, headers, body) {
+      this.command = command;
+      this.headers = headers != null ? headers : {};
+      this.body = body != null ? body : '';
+    }
+
+    Frame.prototype.toString = function() {
+      var lines, name, skipContentLength, value, _ref;
+      lines = [this.command];
+      skipContentLength = this.headers['content-length'] === false ? true : false;
+      if (skipContentLength) {
+        delete this.headers['content-length'];
+      }
+      _ref = this.headers;
+      for (name in _ref) {
+        if (!__hasProp.call(_ref, name)) continue;
+        value = _ref[name];
+        lines.push("" + name + ":" + value);
+      }
+      if (this.body && !skipContentLength) {
+        lines.push("content-length:" + (Frame.sizeOfUTF8(this.body)));
+      }
+      lines.push(Byte.LF + this.body);
+      return lines.join(Byte.LF);
+    };
+
+    Frame.sizeOfUTF8 = function(s) {
+      if (s) {
+        return encodeURI(s).match(/%..|./g).length;
+      } else {
+        return 0;
+      }
+    };
+
+    unmarshallSingle = function(data) {
+      var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _len, _ref, _ref1;
+      divider = data.search(RegExp("" + Byte.LF + Byte.LF));
+      headerLines = data.substring(0, divider).split(Byte.LF);
+      command = headerLines.shift();
+      headers = {};
+      trim = function(str) {
+        return str.replace(/^\s+|\s+$/g, '');
+      };
+      _ref = headerLines.reverse();
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        line = _ref[_i];
+        idx = line.indexOf(':');
+        headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1));
+      }
+      body = '';
+      start = divider + 2;
+      if (headers['content-length']) {
+        len = parseInt(headers['content-length']);
+        body = ('' + data).substring(start, start + len);
+      } else {
+        chr = null;
+        for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) {
+          chr = data.charAt(i);
+          if (chr === Byte.NULL) {
+            break;
+          }
+          body += chr;
+        }
+      }
+      return new Frame(command, headers, body);
+    };
+
+    Frame.unmarshall = function(datas) {
+      var frame, frames, last_frame, r;
+      frames = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*"));
+      r = {
+        frames: [],
+        partial: ''
+      };
+      r.frames = (function() {
+        var _i, _len, _ref, _results;
+        _ref = frames.slice(0, -1);
+        _results = [];
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          frame = _ref[_i];
+          _results.push(unmarshallSingle(frame));
+        }
+        return _results;
+      })();
+      last_frame = frames.slice(-1)[0];
+      if (last_frame === Byte.LF || (last_frame.search(RegExp("" + Byte.NULL + Byte.LF + "*$"))) !== -1) {
+        r.frames.push(unmarshallSingle(last_frame));
+      } else {
+        r.partial = last_frame;
+      }
+      return r;
+    };
+
+    Frame.marshall = function(command, headers, body) {
+      var frame;
+      frame = new Frame(command, headers, body);
+      return frame.toString() + Byte.NULL;
+    };
+
+    return Frame;
+
+  })();
+
+  Client = (function() {
+    var now;
+
+    function Client(ws) {
+      this.ws = ws;
+      this.ws.binaryType = "arraybuffer";
+      this.counter = 0;
+      this.connected = false;
+      this.heartbeat = {
+        outgoing: 10000,
+        incoming: 10000
+      };
+      this.maxWebSocketFrameSize = 16 * 1024;
+      this.subscriptions = {};
+      this.partialData = '';
+    }
+
+    Client.prototype.debug = function(message) {
+      var _ref;
+      return typeof window !== "undefined" && window !== null ? (_ref = window.console) != null ? _ref.log(message) : void 0 : void 0;
+    };
+
+    now = function() {
+      if (Date.now) {
+        return Date.now();
+      } else {
+        return new Date().valueOf;
+      }
+    };
+
+    Client.prototype._transmit = function(command, headers, body) {
+      var out;
+      out = Frame.marshall(command, headers, body);
+      if (typeof this.debug === "function") {
+        this.debug(">>> " + out);
+      }
+      while (true) {
+        if (out.length > this.maxWebSocketFrameSize) {
+          this.ws.send(out.substring(0, this.maxWebSocketFrameSize));
+          out = out.substring(this.maxWebSocketFrameSize);
+          if (typeof this.debug === "function") {
+            this.debug("remaining = " + out.length);
+          }
+        } else {
+          return this.ws.send(out);
+        }
+      }
+    };
+
+    Client.prototype._setupHeartbeat = function(headers) {
+      var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1;
+      if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) {
+        return;
+      }
+      _ref1 = (function() {
+        var _i, _len, _ref1, _results;
+        _ref1 = headers['heart-beat'].split(",");
+        _results = [];
+        for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+          v = _ref1[_i];
+          _results.push(parseInt(v));
+        }
+        return _results;
+      })(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1];
+      if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) {
+        ttl = Math.max(this.heartbeat.outgoing, serverIncoming);
+        if (typeof this.debug === "function") {
+          this.debug("send PING every " + ttl + "ms");
+        }
+        this.pinger = Stomp.setInterval(ttl, (function(_this) {
+          return function() {
+            _this.ws.send(Byte.LF);
+            return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0;
+          };
+        })(this));
+      }
+      if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) {
+        ttl = Math.max(this.heartbeat.incoming, serverOutgoing);
+        if (typeof this.debug === "function") {
+          this.debug("check PONG every " + ttl + "ms");
+        }
+        return this.ponger = Stomp.setInterval(ttl, (function(_this) {
+          return function() {
+            var delta;
+            delta = now() - _this.serverActivity;
+            if (delta > ttl * 2) {
+              if (typeof _this.debug === "function") {
+                _this.debug("did not receive server activity for the last " + delta + "ms");
+              }
+              return _this.ws.close();
+            }
+          };
+        })(this));
+      }
+    };
+
+    Client.prototype._parseConnect = function() {
+      var args, connectCallback, errorCallback, headers;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      headers = {};
+      switch (args.length) {
+        case 2:
+          headers = args[0], connectCallback = args[1];
+          break;
+        case 3:
+          if (args[1] instanceof Function) {
+            headers = args[0], connectCallback = args[1], errorCallback = args[2];
+          } else {
+            headers.login = args[0], headers.passcode = args[1], connectCallback = args[2];
+          }
+          break;
+        case 4:
+          headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3];
+          break;
+        default:
+          headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3], headers.host = args[4];
+      }
+      return [headers, connectCallback, errorCallback];
+    };
+
+    Client.prototype.connect = function() {
+      var args, errorCallback, headers, out;
+      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+      out = this._parseConnect.apply(this, args);
+      headers = out[0], this.connectCallback = out[1], errorCallback = out[2];
+      if (typeof this.debug === "function") {
+        this.debug("Opening Web Socket...");
+      }
+      this.ws.onmessage = (function(_this) {
+        return function(evt) {
+          var arr, c, client, data, frame, messageID, onreceive, subscription, unmarshalledData, _i, _len, _ref, _results;
+          data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() {
+            var _i, _len, _results;
+            _results = [];
+            for (_i = 0, _len = arr.length; _i < _len; _i++) {
+              c = arr[_i];
+              _results.push(String.fromCharCode(c));
+            }
+            return _results;
+          })()).join('')) : evt.data;
+          _this.serverActivity = now();
+          if (data === Byte.LF) {
+            if (typeof _this.debug === "function") {
+              _this.debug("<<< PONG");
+            }
+            return;
+          }
+          if (typeof _this.debug === "function") {
+            _this.debug("<<< " + data);
+          }
+          unmarshalledData = Frame.unmarshall(_this.partialData + data);
+          _this.partialData = unmarshalledData.partial;
+          _ref = unmarshalledData.frames;
+          _results = [];
+          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+            frame = _ref[_i];
+            switch (frame.command) {
+              case "CONNECTED":
+                if (typeof _this.debug === "function") {
+                  _this.debug("connected to server " + frame.headers.server);
+                }
+                _this.connected = true;
+                _this._setupHeartbeat(frame.headers);
+                _results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0);
+                break;
+              case "MESSAGE":
+                subscription = frame.headers.subscription;
+                onreceive = _this.subscriptions[subscription] || _this.onreceive;
+                if (onreceive) {
+                  client = _this;
+                  messageID = frame.headers["message-id"];
+                  frame.ack = function(headers) {
+                    if (headers == null) {
+                      headers = {};
+                    }
+                    return client.ack(messageID, subscription, headers);
+                  };
+                  frame.nack = function(headers) {
+                    if (headers == null) {
+                      headers = {};
+                    }
+                    return client.nack(messageID, subscription, headers);
+                  };
+                  _results.push(onreceive(frame));
+                } else {
+                  _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled received MESSAGE: " + frame) : void 0);
+                }
+                break;
+              case "RECEIPT":
+                _results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0);
+                break;
+              case "ERROR":
+                _results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0);
+                break;
+              default:
+                _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0);
+            }
+          }
+          return _results;
+        };
+      })(this);
+      this.ws.onclose = (function(_this) {
+        return function() {
+          var msg;
+          msg = "Whoops! Lost connection to " + _this.ws.url;
+          if (typeof _this.debug === "function") {
+            _this.debug(msg);
+          }
+          _this._cleanUp();
+          return typeof errorCallback === "function" ? errorCallback(msg) : void 0;
+        };
+      })(this);
+      return this.ws.onopen = (function(_this) {
+        return function() {
+          if (typeof _this.debug === "function") {
+            _this.debug('Web Socket Opened...');
+          }
+          headers["accept-version"] = Stomp.VERSIONS.supportedVersions();
+          headers["heart-beat"] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(',');
+          return _this._transmit("CONNECT", headers);
+        };
+      })(this);
+    };
+
+    Client.prototype.disconnect = function(disconnectCallback, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      this._transmit("DISCONNECT", headers);
+      this.ws.onclose = null;
+      this.ws.close();
+      this._cleanUp();
+      return typeof disconnectCallback === "function" ? disconnectCallback() : void 0;
+    };
+
+    Client.prototype._cleanUp = function() {
+      this.connected = false;
+      if (this.pinger) {
+        Stomp.clearInterval(this.pinger);
+      }
+      if (this.ponger) {
+        return Stomp.clearInterval(this.ponger);
+      }
+    };
+
+    Client.prototype.send = function(destination, headers, body) {
+      if (headers == null) {
+        headers = {};
+      }
+      if (body == null) {
+        body = '';
+      }
+      headers.destination = destination;
+      return this._transmit("SEND", headers, body);
+    };
+
+    Client.prototype.subscribe = function(destination, callback, headers) {
+      var client;
+      if (headers == null) {
+        headers = {};
+      }
+      if (!headers.id) {
+        headers.id = "sub-" + this.counter++;
+      }
+      headers.destination = destination;
+      this.subscriptions[headers.id] = callback;
+      this._transmit("SUBSCRIBE", headers);
+      client = this;
+      return {
+        id: headers.id,
+        unsubscribe: function() {
+          return client.unsubscribe(headers.id);
+        }
+      };
+    };
+
+    Client.prototype.unsubscribe = function(id) {
+      delete this.subscriptions[id];
+      return this._transmit("UNSUBSCRIBE", {
+        id: id
+      });
+    };
+
+    Client.prototype.begin = function(transaction) {
+      var client, txid;
+      txid = transaction || "tx-" + this.counter++;
+      this._transmit("BEGIN", {
+        transaction: txid
+      });
+      client = this;
+      return {
+        id: txid,
+        commit: function() {
+          return client.commit(txid);
+        },
+        abort: function() {
+          return client.abort(txid);
+        }
+      };
+    };
+
+    Client.prototype.commit = function(transaction) {
+      return this._transmit("COMMIT", {
+        transaction: transaction
+      });
+    };
+
+    Client.prototype.abort = function(transaction) {
+      return this._transmit("ABORT", {
+        transaction: transaction
+      });
+    };
+
+    Client.prototype.ack = function(messageID, subscription, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      headers["message-id"] = messageID;
+      headers.subscription = subscription;
+      return this._transmit("ACK", headers);
+    };
+
+    Client.prototype.nack = function(messageID, subscription, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      headers["message-id"] = messageID;
+      headers.subscription = subscription;
+      return this._transmit("NACK", headers);
+    };
+
+    return Client;
+
+  })();
+
+  Stomp = {
+    VERSIONS: {
+      V1_0: '1.0',
+      V1_1: '1.1',
+      V1_2: '1.2',
+      supportedVersions: function() {
+        return '1.1,1.0';
+      }
+    },
+    client: function(url, protocols) {
+      var klass, ws;
+      if (protocols == null) {
+        protocols = ['v10.stomp', 'v11.stomp'];
+      }
+      klass = Stomp.WebSocketClass || WebSocket;
+      ws = new klass(url, protocols);
+      return new Client(ws);
+    },
+    over: function(ws) {
+      return new Client(ws);
+    },
+    Frame: Frame
+  };
+
+  if (typeof exports !== "undefined" && exports !== null) {
+    exports.Stomp = Stomp;
+  }
+
+  if (typeof window !== "undefined" && window !== null) {
+    Stomp.setInterval = function(interval, f) {
+      return window.setInterval(f, interval);
+    };
+    Stomp.clearInterval = function(id) {
+      return window.clearInterval(id);
+    };
+    window.Stomp = Stomp;
+  } else if (!exports) {
+    self.Stomp = Stomp;
+  }
+
+}).call(this);
+
+//=require sockjs
+//=require stomp
diff --git a/client/apollo/json/annot.json b/client/apollo/json/annot.json
new file mode 100644
index 0000000..9b418ea
--- /dev/null
+++ b/client/apollo/json/annot.json
@@ -0,0 +1,81 @@
+{
+    "plugins" : [
+      {
+         "location" : "./plugins/WebApollo",
+         "name" : "WebApollo"
+      }
+   ],
+    "share_link" : 0,
+    "tracks" : [
+    {
+         "autocomplete" : "none",
+         "style" : {
+            "className" : "annot",
+            "renderClassName" : "annot-render",
+            "subfeatureClasses" : {
+               "non_canonical_three_prime_splice_site" : "noncanonical-splice-site",
+               "wholeCDS" : null,
+               "exon" : "container-100pct",
+               "CDS" : "annot-CDS",
+               "non_canonical_five_prime_splice_site" : "noncanonical-splice-site",
+               "UTR" : "annot-UTR"
+            },
+            "arrowheadClass" : "annot-arrowhead",
+            "alternateClasses" : {
+               "transposable_element" : {
+                  "renderClassName" : "blue-ibeam-render annot-apollo",
+                  "className" : "blue-ibeam"
+               },
+               "pseudogene" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "light-purple-80pct"
+               },
+               "snRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               },
+               "rRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               },
+               "snoRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               },
+               "repeat_region" : {
+                  "className" : "magenta-80pct"
+               },
+               "ncRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               },
+               "miRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               },
+               "tRNA" : {
+                  "renderClassName" : "gray-center-30pct annot-apollo",
+                  "className" : "brightgreen-80pct"
+               }
+            },
+            "uniqueIdField" : "id",
+            "centerSubFeature" : {
+               "non_canonical_three_prime_splice_site" : false,
+               "non_canonical_five_prime_splice_site" : false
+            }
+         },
+         "key" : "User-created Annotations",
+         "storeClass" : "WebApollo/Store/SeqFeature/ScratchPad",
+         "maxHeight" : 600,
+         "phase" : 0,
+         "compress" : 0,
+         "label" : "Annotations",
+         "type" : "WebApollo/View/Track/AnnotTrack",
+         "subfeatures" : 1
+      }
+    ],
+   "favicon" : "./plugins/WebApollo/img/webapollo_favicon.ico",
+   "alwaysOnTracks" : "DNA,Annotations",
+   "classicMenu" : 1,
+   "formatVersion" : 1
+}
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index 85f57d8..0000000
--- a/debian/changelog
+++ /dev/null
@@ -1,5 +0,0 @@
-apollo (2.0.8-1) UNRELEASED; urgency=low
-
-  * Initial release
-
- -- Andreas Tille <tille at debian.org>  Fri, 01 Dec 2017 16:31:25 +0100
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index f599e28..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-10
diff --git a/debian/control b/debian/control
deleted file mode 100644
index 527423d..0000000
--- a/debian/control
+++ /dev/null
@@ -1,27 +0,0 @@
-Source: apollo
-Maintainer: Debian Med Packaging Team <debian-med-packaging at lists.alioth.debian.org>
-Uploaders: Andreas Tille <tille at debian.org>
-Section: science
-Priority: optional
-Build-Depends: debhelper (>= 10)
-Standards-Version: 3.9.8
-Vcs-Browser: https://anonscm.debian.org/viewvc/debian-med/trunk/packages/gmod/apollo/trunk/
-Vcs-Svn: svn://anonscm.debian.org/debian-med/trunk/packages/gmod/apollo/trunk/
-Homepage: http://genomearchitect.org/
-
-Package: apollo
-Architecture: any
-Depends: ${shlibs:Depends},
-         ${misc:Depends}
-Description: genome annotation viewer and editor
- Apollo is a genome annotation viewer and editor. It was developed as a
- collaboration between the Berkeley Drosophila Genome Project (part of
- the FlyBase consortium) and The Sanger Institute in Cambridge, UK.
- Apollo allows researchers to explore genomic annotations at many levels
- of detail, and to perform expert annotation curation, all in a graphical
- environment. It was used by the FlyBase biologists to construct the
- Release 3 annotations on the finished Drosophila melanogaster genome,
- and is also a primary vehicle for sharing these annotations with the
- community. The Generic Model Organism Database (GMOD) project, which
- aims to provide a complete ready-to-use toolkit for analyzing whole
- genomes, has adopted Apollo as its annotation workbench.
diff --git a/debian/copyright b/debian/copyright
deleted file mode 100644
index d544916..0000000
--- a/debian/copyright
+++ /dev/null
@@ -1,11 +0,0 @@
-Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: Apollo
-Source: http://apollo.berkeleybop.org/current/install.html
-
-Files: *
-Copyright: © 2002-2010 <upstream>
-License: Artistic
-
-Files: debian/*
-Copyright: © 2012 Andreas Tille <tille at debian.org>
-License: Artistic
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index 6055261..0000000
--- a/debian/rules
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/make -f
-
-# DH_VERBOSE := 1
-
-%:
-	dh $@
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
deleted file mode 100644
index 97a8bed..0000000
--- a/debian/upstream/metadata
+++ /dev/null
@@ -1,12 +0,0 @@
-Reference:
-  Author: S E Lewis and S M J Searle and N Harris and M Gibson and V Lyer and J Richter and C Wiel and L Bayraktaroglir and E Birney and M A Crosby and J S Kaminker and B B Matthews and S E Prochnik and C D Smithy and J L Tupy and G M Rubin and S Misra and C J Mungall and M E Clamp
-  Title: "Apollo: a sequence annotation editor"
-  Journal: Genome Biology
-  Year: 2002
-  Volume: 3
-  Number: 12
-  Pages: research0082-0082.14
-  DOI: 10.1186/gb-2002-3-12-research0082
-  PMID: 12537571
-  URL: http://genomebiology.com/2002/3/12/research/0082
-  eprint: http://genomebiology.com/content/pdf/gb-2002-3-12-research0082.pdf
diff --git a/debian/watch b/debian/watch
deleted file mode 100644
index d07827d..0000000
--- a/debian/watch
+++ /dev/null
@@ -1,4 +0,0 @@
-version=4
-
-https://github.com/GMOD/Apollo/releases .*/archive/(\d[\d.-]+)\.(?:tar(?:\.gz|\.bz2)?|tgz)
-
diff --git a/docs/Apollo2Build.md b/docs/Apollo2Build.md
new file mode 100644
index 0000000..bb1c61e
--- /dev/null
+++ b/docs/Apollo2Build.md
@@ -0,0 +1,158 @@
+# Quick-start Developer's guide
+
+Here we will introduce how to setup Apollo on your server. In general, there are two modes of deploying Apollo.
+
+There is "development mode" where the application is launched in a temporary server (automatically) and there is
+"production mode", which will typically require an external separate database and tomcat server where you can deploy the
+generated `war` file.
+
+This guide will cover the "development mode" scenario which should be easy to start.  **To setup in a production environment, please see the [setup](Setup.md) guide.**
+
+### Java / JDK
+
+You have to install Java and the Java Development Kit (JDK) 8 or higher to run Apollo.  Both the Oracle and OpenJDK versions have been tested.
+
+### Node.js / NPM
+
+You will need to [install node.js](https://nodejs.org/en/download/), which includes NPM (the node package manager) to build Apollo.
+
+[nvm](https://github.com/creationix/nvm) is highly recommended for installing and managing multiple version of Node.
+Node v6 and up should work, but we recommend Node v8 or better.
+
+### Grails / Groovy / Gradle  (optional)
+
+Installing Grails (application framework), Groovy (development language), or Gradle (build environment) is 
+not required (they will install themselves), but it is suggested for doing development.  
+
+This is most easily done by using [SDKMAN](http://sdkman.io/) (formerly GVM) which can automatically setup
+grails for you. 
+
+1. `curl -s http://get.sdkman.io | bash`
+2. `sdk install grails 2.5.5`
+3. `sdk install gradle 2.11`
+4. `sdk install groovy`
+
+
+### Get the code
+
+To setup Apollo, you can download our [latest release](https://github.com/GMOD/Apollo/releases/latest) from our [official releases](https://github.com/GMOD/Apollo/releases/) as compressed zip or tar.gz file (link at the bottom).  
+
+Alternatively you can check it out from git directly as follows:
+
+1. `git clone https://github.com/GMOD/Apollo.git Apollo`
+2. `cd Apollo`
+3. `git checkout <XYZ>` - optional, where XYZ is the tagged version you want from here: https://github.com/GMOD/Apollo/releases
+
+### Verify install requirements
+
+We can now perform a quick-start of the application in "development mode" with this command:
+
+``` 
+./apollo run-local
+```
+
+The JBrowse and perl pre-requisites will be installed during this step, and if there is a success, then a temporary
+server will be automatically launched at `http://localhost:8080/apollo`.
+
+Note: You can also supply a port number e.g. `apollo run-local 8085` if there are conflicts on port 8080.
+
+Also note: if there are any errors at this step, check the setup.log file for errors. You can refer to the
+[troubleshooting guide](Troubleshooting.md) and often it just means the pre-requisites or perl modules failed.
+
+Also also note: the "development mode" uses an in-memory H2 database for storing data by default. The setup guide will
+show you how to configure custom database settings.
+
+### Running the code
+
+There are several distinct parts of the code.
+
+1. Apollo client plugin (JS: [dojo](https://dojotoolkit.org/documentation/), jquery, etc.) in [client directory](../client)
+1. Server ([Grails 2.5.5](http://docs.grails.org/2.5.5/): Groovy and Java) in [grails-app](../grails-app), [src](../src), [web components](../web-app) and [tests](../test).
+1. Side-panel code / wrapper code ([GWT 2.8](http://www.gwtproject.org/doc/latest/DevGuide.html): Java).  Code is java and/or XML in [src/gwt](../src/gwt).
+1. Tools / scripts in the [examples](web_services/examples) and [tools](../tools/data): Groovy, perl, bash
+1. JBrowse (JS: [dojo](https://dojotoolkit.org/documentation/), jquery, etc.)
+
+In general, the command `./apollo run-local` will build and run the client and the server code.  Subsequent runs that do not change the GWT code can use `./apollo run-app`.  Changes to domain objects or adding controller methods may make stopping and restarting the server necessary, but most other changes will compile without having to restart the server.
+
+`./apollo test` runs the grails unit and integration [tests](../test).
+
+Updating the web-service doc can be done with `./apollo create-rest-doc`
+
+
+#### Running the code for the making client plugin changes
+
+After starting the server run `./apollo watchman` which should automatically copy any files that have been modified from the [client directory](../client) to the running instance.
+
+If for some reason this is not working then make sure that your network development tab, in your browser console, has disabled caching.  You can also run the command `gradle copy-resources-dev` manually each time instead if the files don't seem to be getting copied.
+
+#### Running the code for GWT changes 
+
+To use the GWT dev server run `gradle devmode` in a separate terminal.   This will bring up a separate [GWT dev-mode code server](http://www.gwtproject.org/doc/latest/DevGuideCompilingAndDebugging.html#dev_mode) that will compile subsequent changes to the [src/gwt](../src/gwt) code after reloading the page.  
+
+If errors seem to be a little obtuse using the dev compilation, you might try running `./apollo compile` to get more detail.
+
+#### Running the code for JBrowse changes
+
+If you are testing making changes directly to JBrowse within Apollo, the following steps should work:
+
+1. `./apollo clean-all`
+1. Clone the version of jbrowse you want into a directory called `jbrowse-download` as the root level.
+1. `./apollo run-local` to run the server
+1. In a separate terminal run `gradle copy-resources-dev` to copy over your changes to the server.
+
+
+### Create server documentation
+
+Using an IDE like IntelliJ, NetBeans, Eclipse etc. is highly recommended in conjunction with [Grails 2.5.X documentation](http://docs.grails.org/2.5.x/).
+Additionally, you can generate documentation using grails:
+
+    grails doc
+    
+Server documentation (for groovy) should be available at `target/docs/all-docs.html`.
+
+## Setting up the application
+
+### Setup a production server
+
+**To setup in a production environment, please see the [setup](Setup.md) guide.**  To setup (as opposed to a development server as above), you must [properly configure a servlet container like Tomcat or Jetty](Setup.md) with [sufficient memory](Troubleshooting.md#tomcat-memory).
+
+### Adding data to Apollo
+
+After we have a server setup, we will want to add a new organism to the panel. If you are a new user, you will want to
+setup this data with the jbrowse pre-processing scripts. You can see the [data loading guide](Data_loading.md) for more
+details, but essentially, you will want to load a reference genome and an annotations file at a minimum:
+
+``` 
+bin/prepare-refseqs.pl --fasta yourgenome.fasta --out /opt/apollo/data
+
+bin/flatfile-to-json.pl --gff yourannotations.gff --type mRNA \
+        --trackLabel AnnotationsGff --out /opt/apollo/data
+```
+
+
+### Login to the web interface
+
+After you access your application at http://localhost:8080/apollo/ then you will be prompted for login information
+
+![Login first time](images/1.png)
+
+Figure 1. "Register First Admin User" screen allows you to create a new admin user.
+
+
+![Organism configuration](images/2.png)
+
+Figure 2. Navigate to the "Organism tab" and select "Create new organism". Then enter the new information for your
+organism. Importantly, the data directory refers to a directory that has been prepared with the JBrowse data loading
+scripts from the command line. See the [data loading](Data_loading.md) section for details.
+
+![Open annotator](images/3.png)
+
+Figure 3. Open up the new organism from the drop down tab on the annotator panel.
+
+
+
+## Conclusion
+
+If you completed this setup, you can then begin adding new users and performing annotations. Please continue to the
+[setup guide](Setup.md) for deploying the webapp to production or visit the [troubleshooting guide](Troubleshooting.md)
+if you encounter problems during setup.
diff --git a/docs/Architecture.md b/docs/Architecture.md
new file mode 100644
index 0000000..4f91a5f
--- /dev/null
+++ b/docs/Architecture.md
@@ -0,0 +1,288 @@
+## Architecture notes
+
+### Overview and quick-start
+
+See the [build doc](Apollo2Build.md) for the official quick-start guide.
+
+Minimally, the apollo application can be launched by running `apollo run-local`. This starts up a temporary tomcat
+server automatically. It will also simply use a in-memory H2 database if a different database configuration isn't setup
+yet.
+
+For development purposes, you can also enable automatic code reloading which helps for fast iteration.
+    
+    
+- `grails -reloading run-app` will allow changes to the server side code to be auto-reloaded. 
+- `ant devmode` will provide auto-reloading of GWT code changes
+- `scripts/copy_client.sh` will copy the plugin code to the web-apps folder to update the plugin javascript
+
+The `apollo` script automatically does several of these functions.
+
+
+Note: Changes to domain/database objects will require an application restart, but, a very cool feature of our
+application is that the whole database doesn't need reloading after a database change.
+
+If you look at the `apollo` binary, you'll see that the code for `grails run-app` and others are automatically launched
+during `apollo run-local`.
+
+Also, as always during web development, yoe will want to clear the cache to see changes ("shift-reload" on most
+browsers).
+
+### Overview
+
+![](architecture2.png)
+
+[PDF schema](https://github.com/GMOD/Apollo/blob/master/docs/schemaupdates.pdf)
+
+The main components of the Apollo 2.x application are:
+
+- [Grails 2 Server](http://grails.org) with the current version set in the [application.properties](https://github.com/GMOD/Apollo/blob/master/application.properties)
+- Datastore: configured via Hibernate / Grails whcih can use most anything supported by JDBC / hibernate (primarily,
+  Postgres, MySQL, H2)
+- JBrowse / Apollo Plugin: JS / HTML5 [JBrowse doc](http://jbrowse.org/code/JBrowse-1.11.6/docs/) and [main
+  site](http://jbrowse.org)
+- GWT client: provides the sidebar.   Can be written in another front-end language, as well.  [GWT
+  doc](http://www.gwtproject.org/)
+
+
+### Basic layout
+
+- Grails code is in normal grails directories under "grails-app"
+- GWT-only code is under "src/gwt"
+    - Code shared between the client and the server is under "src/gwt/org/bbop/apollo/gwt/shared"
+- Client code is under "client" (still)
+- Tests are under "test"
+- Old (presumably inactive code) is under "src/main/webapp"
+- New source (loaded into VM) is under "src/java" or "src/groovy" except for grails specific code.
+- Web code (not much) is either under "web-app" (and where jbrowse is copied) or under "grails-app/assets" (these are
+  compiled down).
+- GWT-specifc CSS can also be found in: "src/gwt/org/bbop/apollo/gwt/client/resources/" but it inherits the CSS on its
+  current page, as well.
+
+
+
+#### Main components
+
+The main components of the Apollo 2.x application (the four most important are 1 through 4):
+
+1. The domain classes; these are the main objects
+2. Controllers, which route those domains and provide URL routes; provides rest services
+3. Views: annotator and index and the only ones that matter for Apollo
+4. Services: very important because all of the controllers should typically have routes, then particular business logic
+  should go into the service.
+5. Configuration files: The grails-app/conf folder contains central conf files, but the apollo-config.groovy
+  file in your root directory can override these central configs (i.e. it is not necessary to edit DataSource.groovy)
+6. Grails-app/assets: all your javascript live here. efficient way to deliver this stuff
+7. Resources: web-app directory: css, images, and the jbrowse directory + WA plugin are initialized here. 
+8. Client directory: The WA plugin is copied or compiled along with jbrowse to the web-app directory
+
+### Schema/domain classes
+
+Domain classes: the most important domain class everywhere is the Feature; it is the key to everything that we do. The
+way a domain class is built: 
+
+The domain classes represent a database table. The way it works with "Feature", which is inherited by many other
+classes, is that all features are stored in the same table, the difference is that in SQL, there is a class table and
+when it pulls these tables from the database --- it queries it and then converts it into the right class.  There are a
+number of constrains you can set. 
+
+Very important: the hasMany maps the one-to-many relationship within the database. It can have many locations. the
+parentFeatureRelationships is where you map this one-to-many relationship.  You also have to have a single item
+relationship.
+
+You can add extra methods to the domain objects, but this is generally not necessary. 
+
+Note: In the DataStore configuration, setting called "auditable = true" means that a new table, a feature auditing tool,
+is keeping track of history for the specified objects
+
+#### Feature class
+
+All features inherit an ontologyId and specify a cvTerm, although CvTerms are being phased out.
+
+Subclasses of "Feature" will specify the ontologyId, but "Feature" itself is too generic, for example, so it does not
+have an ontologyId.
+
+
+#### Sequence class
+
+Sequences are the method for WA to grabs sequences used to have a cache built-in mechanism doesn't want to have that
+anymore to avoid running into memory problems.
+
+
+#### Feature locations
+
+Features such as genes all have a feature location belongs to a particular sequence. If you have a feature with
+subclasses, it can exist within many locations, and each location belongs to its own sequence.
+
+#### Feature relationship
+
+Feature relationships can define parent/child relationships as well as SO terms i.e. SO "part_of" relationships
+
+#### Feature enums
+
+The FeatureString enum: allows for mapping names for concepts, and it is useful to use these enums without worrying
+about string mappings inside the application.
+
+
+### Running the application
+
+If you go through and run this grails application when you send the URL request, then methods that are sent through the
+AnnotationEditorController  (formerly called AnnotationEditorService) dynamically calls a method using handleOperation.
+
+The AnnotatorController serves the page that the annotator is on. This doesn't map to a particular domain object.
+
+In most cases when we have these methods, it unwraps the data that is sent through into JSON object as a set of
+variables. Then it is processed into java objects and routed back to JSON to send back. 
+
+When annotator creates a transcript, it is then released to requestHandlingService and it sends it to an annotation
+event, which sends it to a WebSocket, and it's then broadcasted to everyone.
+
+#### Websockets and listeners
+
+All clients subscribe to AnnotationNotifications for new transcripts and events.
+
+If an add_transcript operation occurs, this is broadcasted via the websocket. The server side broadcasts this event, and
+then it does a JSON roundtrip to render the results and sends the return object that belongs to an AnnotationEvent.
+ 
+Procedure transcript is created --> goes to the server --> adds a transcript locally --> announces it to everyone.
+
+We used to use long polling request model for "push notifications" but now we use Spring with the SockJS, which uses
+websockets but it can fall back to long-polling.
+
+There is another component of the broadcasting called brokerMessagingTemplate is the converter to broadcast the event
+
+
+#### Controllers
+
+Grails controllers are a fairly easy concept for "routing" URLs and info to methods in the code.
+
+
+#### Services
+
+Grails services are classes that perform business logic.  (In IntelliJ, these are indicated by green buttons on the
+definitions to show that these are Injected Spring Bean classes)
+
+The word @Transactional means that every operation that is not private is handled via a transaction. In the old model
+there were a lot of files that were recreated each time, even though they did the same. Now we define a class and can
+use it again and again. And there can be transactions within transaction. I could also call other services within
+services.
+
+addTranscript generateTranscript
+
+The different services do exactly what their name implies. It may not always be clear in what particular service each
+class should be in, but it can be changed later. It is easy also to make changes to the names as well. 
+
+
+
+##### Grails views
+
+- Most of Views are under grails-app
+  - everything conforms to the MVC backend model for the Grails application. 
+- Most of java, css, html is under web-app directory
+  - Application logic for groovy, gwt, java, etc live here. we could put our old servlets there, but not recommended. 
+
+
+
+
+
+### Main configuration
+
+The central configuration files are defined in grails-app/conf/ folder, however the user normally only edits their
+personal config in apollo-config.groovy. That is because the user config file will override those in the central
+configuration. See [Configure.md](Configure.md) for details.
+
+#### Database configuration
+
+
+The "root" database configuration is specified by grails-app/conf/DataSource.groovy but it is generally over-ridden by
+the user's apollo-config.groovy
+
+
+It is recommended that the user takes sample-postgres-apollo-config.groovy or sample-mysql-apollo-config.groovy and
+copies it to apollo-config.groovy for their application.
+
+
+
+The default database driver is the h2 database, which is an "embedded" database that doesn't require installing postgres
+or mysql, but it is not generally seen as performant as postgres or mysql though. 
+
+
+Note: there are three environments that can be setup: a development environment, a test environment, and a production
+environment, and these are basically assigned automatically depending on how you deploy the app.
+
+* Development environment - "apollo run-local" or "apollo debug"
+
+* Test environment - "apollo test"
+
+* Production environment - "apollo deploy" or "apollo release"
+
+
+Note: If there are no users and no annotations, a bootstrap procedure can also automatically create some annotations and
+users to start up the app so there is something in there to begin with.
+
+
+#### UrlMapping configuration:
+
+The UrlMappings are stored in grails-app/conf/UrlMappings.groovy
+
+The UrlMappings sets up a mapping from routes to controllers
+
+Standard and customized mappings go in here. The way we route jbrowse to organism data directories is also controlled
+here.  The organismJBrowseDirectory is set for a particular session, per user. If none specified, it brings up a default
+one. 
+
+
+#### Build configuration
+
+The build configuration is stored in grails-app/conf/BuildConfig.groovy
+
+If there are libraries that are missing are are to be added, you can add them here.
+
+Additionally, the build system uses the "apollo" script and the "build.xml" to control the compilation and resource
+steps.
+
+
+#### Central config
+
+The central configuration is stored in grails-app/conf/Config.groovy
+
+
+The central Grails config contains logging, app config, and also can reference external configs. The external config can
+override settings without even touching the application code using this method
+
+In our application, we use the apollo-config.groovy then everything in there supersedes this file.
+
+The log4j area can enable logging levels. You can turn on the "debug grails.app" to output all the webapollo debug info,
+or also set the "grails.debug" environment variable for java too.
+
+There is also some Apollo configuration here, and it is mostly covered by the [configuration section](Configure.md).
+
+
+
+### GWT web-app
+
+When GWT compiles, it loads files into the web-app directory. When it loads up annotator, it goes to annotator index
+(the way things get loaded) it does an include annotator.nocache.js file, and with that, it includes all GWT stuff for
+the /annotator/index route. The src/gwt/org/bbop/apollo/gwt/ contains much code and the
+src/gwt/org/bbop/apollo/gwt/Annotator.gwt.xml is a central config file for the GWT web-app.
+
+
+
+#### User interface definitions
+
+A Bootstrap/GWT interface handles the tabs on the right for the new UI.  The annotator object is at the root of
+everything.
+
+Example definition: MainPanel.ui.xml 
+
+
+### Tests
+
+#### Unit tests
+
+Unit tests and some basic javascript tests are running on Travis-CI (see .travis.yml for example script).
+
+You can also run "apollo test" to run the tests locally. It will use the "test" database configuration automatically. 
+
+
+Also see the [testing notes](Testing_notes.md) for more details.
+
diff --git a/docs/CannedElements.md b/docs/CannedElements.md
new file mode 100644
index 0000000..fbe1932
--- /dev/null
+++ b/docs/CannedElements.md
@@ -0,0 +1,72 @@
+# Canned Elements
+
+The _Information Editor_ contains a series of boxes that curators can use to add metadata to all annotations. The _Information Editor_ for all annotation types includes a table for _Attributes_, a table for _Comments_, and radio buttons to mark one of any possible _Statuses_ for an annotation. It is possible to pre-load customized options that curators can access via drop-down menu. These pre-loaded, or 'canned' options can be configured from the _Admin_ tab in the _Annotator Panel_.
+
+This document describes how to customize and add canned elements.
+
+---
+## 1. Statuses
+
+Admins may configure a list of 'Statuses' to offer curators a list of radio buttons that mark the status of an annotation. 
+
+Follow these steps to configure a list of possible _Statuses_ and make them available to curators using the _Admin_ tab in the _Annotator Panel_.
+
+1. Navigate to the _Admin_ tab in the _Annotator Panel_
+2. Select (click) the _Statuses_ option to navigate to a new tab. 
+3. Use the _New Status_ button on the upper left corner to create a new Status. In the box 'Value' enter the new status as it will appear in the drop-down menu. 
+4. Click on the _Create_ button at the bottom of the options to save the new value (i.e. status). Use the _AvailableStatus List_ button on the upper left of this page to return to the list of statuses without creating a new one.
+
+From the list of existing statuses, you may select (click) any status to review, edit, or delete the status. 
+
+---
+## 2. Attributes Box
+  
+'Key' and 'Value' options can be customized to pre-load frequently used options and make them available to curators. The _Attributes_ box has two columns. The 'Tag' column is populated using the _Keys_ and the 'Values' column contains, the name indicates, _Values_. 
+
+### a. Canned Keys
+
+Follow these steps to configure a list of frequently used tags and make them available to curators using the _Admin_ tab in the _Annotator Panel_.
+
+1. Navigate to the _Admin_ tab in the _Annotator Panel_
+2. Select (click) the _Canned Key_ option to navigate to a new tab. 
+3. Use the _New Canned Key_ button on the upper left corner to create a new Tag. 
+  1. In the box 'Label' enter the new tag as it will appear in the drop-down menu. 
+  2. Enter any additional details about this tag in the box labeled as 'Metadata'.
+  3. Choose the types of annotations for which this tag should be available by selecting at least one of the 'Feature Types'. Hold down the 'Shift' key to select multiple types of annotations.
+4. Click on the _Create_ button at the bottom of the options to save the new tag (i.e. key). Use the _CannedKey List_ button on the upper left of this page to return to the list of keys without creating a new one.
+
+From the list of existing keys, you may select (click) any key to review, edit, or delete the key. 
+
+### b. Canned Values
+
+Configure a list of frequently used values and make them available to curators using the _Admin_ tab in the _Annotator Panel_.
+
+1. Navigate to the _Admin_ tab in the _Annotator Panel_
+2. Select (click) the _Canned Values_ option to navigate to a new tab. 
+3. Use the _New Canned Value_ button on the upper left corner to create a new Tag. 
+  1. In the box 'Label' enter the new value as it will appear in the drop-down menu. 
+  2. Enter any additional details about this value in the box labeled as 'Metadata'.
+  3. Choose the types of annotations for which this value should be available by selecting at least one of the 'Feature Types'. Hold down the 'Shift' key to select multiple types of annotations.
+4. Click on the _Create_ button at the bottom of the options to save the new value. Use the _CannedValue List_ button on the upper left of this page to return to the list of values without creating a new one.
+
+From the list of existing values, you may select (click) any value to review, edit, or delete the value. 
+
+---
+## 3. Comments Box
+
+### Canned Comments
+
+Admins may configure a list of frequently used comments and make them available for curators from the _Comments_ box in the _Information Editor_. Comments can be customized by each community according to their needs.
+
+Follow these steps to create and configure a list of comments:
+
+1. Navigate to the _Admin_ tab in the _Annotator Panel_
+2. Select (click) the _Canned Comments_ option to navigate to a new tab. 
+3. Use the _New Canned Comment_ button on the upper left corner to create a new comment. 
+  1. In the box 'Comment' enter the new comment as it will appear in the drop-down menu. 
+  2. Enter any additional details about this comment in the box labeled as 'Metadata'.
+  3. Choose the types of annotations for which this comment should be available by selecting at least one of the 'Feature Types'. Hold down the 'Shift' key to select multiple types of annotations.
+4. Click on the _Create_ button at the bottom of the options to save the new comment. Use the _CannedComment List_ button on the upper left of this page to return to the list of comments without creating a new one.
+
+From the list of existing comments, you may select (click) any comment to review, edit, or delete the comment. 
+
diff --git a/docs/ChadoExport.md b/docs/ChadoExport.md
new file mode 100644
index 0000000..4ffa5b5
--- /dev/null
+++ b/docs/ChadoExport.md
@@ -0,0 +1,73 @@
+# Chado Export Configuration
+
+Following are the steps for setting up a Chado data source that is compatible with Apollo Chado Export.
+
+### Create a Chado database
+
+First create a database in PostgreSQL for Chado.
+
+Note: Initial testing has only been done on PostgreSQL.
+
+Default name is `apollo-chado` and `apollo-production-chado` for development and production environment, respectively.
+
+### Create a Chado user
+
+Now, create a database user that has all access privileges to the newly created Chado database.
+
+### Load Chado schema and ontologies
+
+Apollo assumes that the Chado database has Chado schema v1.2 or greater and has the following ontologies loaded:
+
+1. Relations Ontology
+2. Sequence Ontology
+3. Gene Ontology
+
+
+The quickest and easiest way to do this is to use prebuilt Chado schemas.
+Apollo provides a prebuilt Chado schema with the necessary ontologies. (thanks to Eric Rasche at [Center for Phage Technology, TAMU](https://cpt.tamu.edu/computer-resources/chado-prebuilt-schema/))
+
+
+Users can load this prebuilt Chado schema as follows:
+```
+scripts/load_chado_schema.sh -u <USER> -d <CHADO_DATABASE> -h <HOST> -p <PORT> -s <CHADO_SCHEMA_SQL>
+```
+
+If there is already an existing database with the same name and if you would like to dump and create a clean database:
+```
+scripts/load_chado_schema.sh -u <USER> -d <CHADO_DATABASE> -h <HOST> -p <PORT> -s <CHADO_SCHEMA_SQL> -r
+```
+
+The '-r' flag tells the script to perform a pg_dump if `<CHADO_DATABASE>` exists.
+
+
+e.g., 
+
+```
+scripts/load_chado_schema.sh -u postgres -d apollo-chado -h localhost -p 5432 -r -s chado-schema-with-ontologies.sql.gz
+
+```
+
+The file `chado-schema-with-ontologies.sql.gz` can be found in `Apollo/scripts/` directory.
+
+The `load_chado_schema.sh` script creates log files which can be inspected to see if loading the schema was successful.
+
+Note that you will also need to do this for your testing and production instances, as well.
+
+### Configure data sources
+
+In `apollo-config.groovy`, uncomment the configuration for `datasource_chado` and specify the proper database name, database user name and database user password.
+
+### Export via UI
+
+Users can export existing annotations to the Chado database via the Annotator Panel -> Ref Sequence -> Export.
+
+### Export via web services
+
+Users can also leverage the Apollo web services API to export annotations to Chado.
+As a demonstration, a sample script, `export_annotations_to_chado.groovy` is provided.
+
+Usage for the script:
+
+```
+export_annotations_to_chado.groovy -organism ORGANISM_COMMON_NAME -username APOLLO_USERNAME -password APOLLO_PASSWORD -url http://localhost:8080/apollo
+```
diff --git a/docs/Command_line.md b/docs/Command_line.md
new file mode 100644
index 0000000..a857de5
--- /dev/null
+++ b/docs/Command_line.md
@@ -0,0 +1,84 @@
+# Command line tools
+
+The command line tools offer a number of interesting features that can be used to help setup and retrieve data from the
+application.
+
+
+## Overview
+
+
+The command line tools are located in docs/web_services/examples, and they are mostly small scripts that automate the
+usage of the the web services API.
+
+### get_gff3.groovy
+
+Example:
+
+``` 
+get_gff3.groovy -organism Amel_4.5 -username admin at webapollo.com \
+    -password admin_password -url http://localhost:8080/apollo > my output.gff3
+```
+This command can accept an -output argument to output to file, or the stdout can be redirected.
+
+The -username and -password can be specified via the command line or if omitted, the user will be prompted.
+
+### get_fasta.groovy
+
+Example:
+
+``` 
+get_fasta.groovy -organism Amel_4.5 -username admin at webapollo.com \                                                      
+    -password admin_password -seqtype cds/cdna/peptide -url http://localhost:8080/apollo > output.fa
+```
+This command can accept an -output argument to output to file, or the stdout can be redirected.   
+
+The -username and -password can be specified via the command line (similar to `get_gff3.groovy`) or if omitted, the user
+will be prompted.
+
+### add_users.groovy
+
+
+Example:
+
+``` 
+add_users.groovy -username admin at webapollo.com -password admin_password \
+    -newuser newuser at test.com -newpassword newuserpass \
+    -destinationurl http://localhost:8080/apollo
+```
+
+The -username and -password refer to the admin user, and they can also be specified via stdin instead of the command
+line if they are omitted.
+
+A list of users specified in a csv file can also be used as input.
+
+### add_organism.groovy
+
+
+Example:
+
+``` 
+add_organism.groovy -name yeast -url http://localhost:8080/apollo/ \
+    -directory /opt/apollo/yeast -username admin at webapollo.com -password admin_password
+```
+
+
+The -directory refers to the jbrowse data directory containing the output from prepare-refseqs.pl, flatfile-to-json.pl,
+etc. The -blatdb is optional, -genus, and -species are optional.
+
+The -username and -password refer to the admin user, and they can also be specified via stdin instead of the command
+line if they are omitted.
+
+
+### delete_annotations_from_organism.groovy
+
+Example:
+
+```
+docs/web_services/examples/groovy/delete_annotations_from_organism.groovy  -destinationurl http://localhost:8080/apollo\
+     -organismname honeybee2
+```
+
+This script will delete any annotations associated with a given organism.
+
+
+
diff --git a/docs/Configure.md b/docs/Configure.md
new file mode 100644
index 0000000..619694c
--- /dev/null
+++ b/docs/Configure.md
@@ -0,0 +1,558 @@
+## Apollo Configuration
+
+
+Apollo includes some basic configuration parameters that are specified in configuration files. The most important
+parameters are the database parameters in order to get Apollo up and running. Other options besides the database
+parameters can be configured via the config files, but note that many parameters can also be configured via the web
+interface.
+
+Note: Configuration options may change over time, as more configuration items are integrated into the web interface.
+
+
+### Main configuration
+
+The main configuration settings for Apollo are stored in `grails-app/conf/Config.groovy`, but you can override settings
+in your `apollo-config.groovy` file (i.e. the same file that contains your database parameters). Here are the defaults
+that are defined in the Config.groovy file:
+
+``` 
+// default apollo settings
+apollo {
+  default_minimum_intron_size = 1
+  history_size = 0
+  overlapper_class = "org.bbop.apollo.sequence.OrfOverlapper"
+  track_name_comparator = "/config/track_name_comparator.js"
+  use_cds_for_new_transcripts = true
+  user_pure_memory_store = true
+  translation_table = "/config/translation_tables/ncbi_1_translation_table.txt"
+  is_partial_translation_allowed = false // unused so far
+  get_translation_code = 1
+  only_owners_delete = false
+  sequence_search_tools = [
+    blat_nuc: [
+      search_exe: "/usr/local/bin/blat",
+      search_class: "org.bbop.apollo.sequence.search.blat.BlatCommandLineNucleotideToNucleotide",
+      name: "Blat nucleotide",
+      params: ""
+    ],
+    blat_prot: [
+      search_exe: "/usr/local/bin/blat",
+      search_class: "org.bbop.apollo.sequence.search.blat.BlatCommandLineProteinToNucleotide",
+      name: "Blat protein",
+      params: ""
+    ]
+  ]    
+      
+
+
+  splice_donor_sites = [ "GT" ]
+  splice_acceptor_sites = [ "AG"]
+  gff3.source= "." 
+  bootstrap = false
+
+  info_editor = {
+    feature_types = "default"
+    attributes = true
+    dbxrefs = true
+    pubmed_ids = true
+    go_ids = true
+    comments = true
+  }
+}
+```
+
+These settings are essentially the same familiar parameters from a config.xml file from previous Apollo versions.  The
+defaults are generally sufficient, but as noted above, you can override any particular parameter in your
+`apollo-config.groovy` file, e.g. you can add override configuration any given parameter as follows:
+
+``` 
+grails {
+  apollo.get_translation_code = 1
+  apollo {
+    use_cds_for_new_transcripts = true
+    default_minimum_intron_size = 1
+    get_translation_code = 1  // identical to the dot notation
+  }
+}
+```
+
+### JBrowse Plugins
+
+You can add / remove jbrowse plugins by copying a jbrowse section into your ```apollo-config.groovy```. 
+
+There are two sections, ```plugins``` and ```main```, which specifies the jbrowse version.
+   
+The main section can either contain a ```git``` block or a ```url``` block, both of which require ```url```.
+If a git block a ```tag``` or ```branch``` can be specified.  
+
+In the ```plugins``` section, options are ```included``` (part of the JBrowse release), ```url``` (requiring a url parameter), 
+or ```git```, which can include a ```tag``` or ```branch``` as above.  
+
+Options for ```alwaysRecheck``` and ```alwaysRepull``` always check the branch and tag and always pull respectiviely. 
+
+__Warning:__ The ```NeatHTMLFeatures``` and ```NeatCanvasFeatures``` plugins work very well in JBrowse instances.  We are still in the process of testing and improving their performance in combination with the Apollo plugin. Until we finalize this process, we strongly advise caution if enabling them for use in your Apollo instances.
+
+```
+jbrowse {
+    git {
+        url= "https://github.com/GMOD/jbrowse"
+//        tag = "1.12.1-release"
+        branch = "master"
+        alwaysPull = true
+        alwaysRecheck = true
+    }
+//    url {
+//        // always use dev for apollo
+//        url = "http://jbrowse.org/wordpress/wp-content/plugins/download-monitor/download.php?id=102"
+//        type ="zip"
+//        fileName = "JBrowse-1.12.0-dev"
+//    }
+    plugins {
+        WebApollo{
+            included = true
+        }
+        NeatHTMLFeatures{
+            included = true
+        }
+        NeatCanvasFeatures{
+            included = true
+        }
+        RegexSequenceSearch{
+            included = true
+        }
+        HideTrackLabels{
+            included = true
+        }
+//        MyVariantInfo {
+//            git = 'https://github.com/GMOD/myvariantviewer'
+//            branch = 'master'
+//            alwaysRecheck = "true"
+//            alwaysPull = "true"
+//        }
+//        SashimiPlot {
+//            git = 'https://github.com/cmdcolin/sashimiplot'
+//            branch = 'master'
+//            alwaysPull = "true"
+//        }
+    }
+}
+```
+
+
+### Translation tables
+
+
+The default translation table is [1](http://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi#SG1) 
+
+To use a different table from [this list of NCBI translation tables](http://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi) set the number in the ```apollo-config.groovy``` file as:
+
+```
+apollo {
+...
+  get_translation_code = "11"
+}
+```   
+
+You may also add a custom translation table in the ```web-app/translation_tables``` directory as follows:
+
+```
+web-app/translation_tables/ncbi_customname_translation_table.txt
+```
+
+Specify the ```customname``` in apollo-config.groovy as follows:
+
+```
+apollo {
+...
+  get_translation_code = "customname"
+}
+```
+
+As well, translation tables can be set per organism using the _'Details'_ panel located in the _'Organism'_ tab of the Annotator panel in the Apollo window: to replace the translation table (default or set by admin) for any given organism, use the field labeled as _'Non-default Translation Table'_ to enter a different table identifier as needed. 
+
+
+### Logging configuration
+
+To over-ride the default logging, you can look at the logging configurations from
+[Config.groovy](https://github.com/GMOD/Apollo/blob/master/grails-app/conf/Config.groovy) and override or modify them in
+`apollo-config.groovy`.
+
+``` 
+log4j.main = {
+    error 'org.codehaus.groovy.grails.web.servlet',  // controllers
+          'org.codehaus.groovy.grails.web.pages',    // GSP
+          'org.codehaus.groovy.grails.web.sitemesh', // layouts
+           ...
+    warn 'grails.app'
+}
+```
+
+Additional links for log4j:
+
+- Advanced log4j configuration:
+  http://blog.andresteingress.com/2012/03/22/grails-adding-more-than-one-log4j-configurations/
+- Grails log4j guide: http://grails.github.io/grails-doc/2.4.x/guide/single.html#logging
+
+
+### Canned Elements
+
+
+Canned comments, canned keys (tags), and canned values are configured using the Admin tab from the Annotator Panel on the web interface; these can no longer be created or edited using the configuration files. For more details on how to create and edit Canned Elements see [Canned Elements](CannedElements.md).
+
+View your instances page for more details. For example 
+- http://localhost:8080/apollo/cannedComment/  
+- http://localhost:8080/apollo/cannedKey/ 
+- http://localhost:8080/apollo/cannedValue/
+
+
+### Search tools
+
+Apollo can be configured to work with various sequence search tools. UCSC's BLAT tool is configured by default and you
+can customize it as follows by making modifications in the ```apollo-config.groovy``` file.  Here we replace blat with blast 
+(there is an existing wrapper for Blast).  The database for each file will be passed in via params (globally) or using the 
+```Blat database``` field in the organism tab.  For blast the database will be the root name of the blast database files 
+without the suffix.
+
+``` 
+apollo{
+	sequence_search_tools {
+        blat_nuc {
+            search_exe = "/usr/local/bin/blastn"
+            search_class = "org.bbop.apollo.sequence.search.blast.BlastCommandLine"
+            name = "Blast nucleotide"
+            params = ""
+        }
+        blat_prot {
+            search_exe = "/usr/local/bin/tblastn"
+            search_class = "org.bbop.apollo.sequence.search.blast.BlastCommandLine"
+            name = "Blast protein to translated nucleotide"
+            params = ""
+            //tmp_dir: "/opt/apollo/tmp" optional param
+        }
+        your_custom_search_tool {
+          search_exe = "/usr/local/customtool"
+          search_class = "org.your.custom.Class"
+          name: "Custom search"
+        }
+    }
+}
+
+```
+
+When you setup your organism in the web interface, you can then enter the location of the sequence search database for
+BLAT.
+
+
+Note: If the BLAT binaries reside elsewhere on your system, edit the search_exe location in the config to point to your
+BLAT executable.
+
+### Data adapters
+
+
+Data adapters for Apollo provide the methods for exporting annotation data from the application. By default, GFF3
+and FASTA adapters are supplied. They are configured to query your IOService URL e.g.
+http://localhost:8080/apollo/IOService with the customizable query
+
+``` 
+data_adapters = [[
+  permission: 1,
+  key: "GFF3",
+  data_adapters: [[
+    permission: 1,
+    key: "Only GFF3",
+    options: "output=file&format=gzip&type=GFF3&exportGff3Fasta=false"
+  ],
+  [
+    permission: 1,
+    key: "GFF3 with FASTA",
+    options: "output=file&format=gzip&type=GFF3&exportGff3Fasta=true"
+  ]]
+],
+[
+  permission: 1,
+  key : "FASTA",
+  data_adapters :[[
+    permission : 1,
+    key : "peptide",
+    options : "output=file&format=gzip&type=FASTA&seqType=peptide"
+  ],
+  [
+    permission : 1,
+    key : "cDNA",
+    options : "output=file&format=gzip&type=FASTA&seqType=cdna"
+  ],
+  [
+    permission : 1,
+    key : "CDS",
+    options : "output=file&format=gzip&type=FASTA&seqType=cds"
+  ]]
+]]
+```
+
+#### Default data adapter options
+
+The options available for the data adapters are configured as follows
+
+- type: `GFF3` or `FASTA`
+- output: can be `file` or `text`. `file` exports to a file and provides a UUID link for downloads, text just outputs to
+  stream.
+- format: can by `gzip` or `plain`. `gzip` offers gzip compression of the exports, which is the default.
+- exportSequence: `true` or `false`, which is used to include FASTA sequence at the bottom of a GFF3 export
+
+
+### Supported annotation types
+
+Many configurations will require you to define which annotation types the configuration will apply to. Apollo supports
+the following "higher level" types (from the Sequence Ontology):
+
+* sequence:gene
+* sequence:pseudogene
+* sequence:transcript
+* sequence:mRNA
+* sequence:tRNA
+* sequence:snRNA
+* sequence:snoRNA
+* sequence:ncRNA
+* sequence:rRNA
+* sequence:miRNA
+* sequence:repeat_region
+* sequence:transposable_element
+
+
+### Apache / Nginx configuration
+
+Oftentimes, admins will put use Apache or Nginx as a reverse proxy so that the requests to a main server can be
+forwarded to the tomcat server.  This setup is not necessary, but it is a very standard configuration as is making modification to iptables.  
+
+Note that we use the SockJS library, which will downgrade to long-polling if websockets are not available, but since
+websockets are preferable, it helps to take some extra steps to ensure that the websocket calls are proxied or forwarded
+in some way too.
+If you are using tomcat 7, please make sure to use the most recent stable version, which supports web sockets by default.  Using older versions (e.g. 7.0.26) websockets may not be included by default and you will need to include an additional .jar file.
+
+#### Apache Proxy 
+
+The most simple setup on apache is as follows.. Here is the most basic configuration for a reverse proxy:
+
+
+``` 
+ProxyPass  /apollo http://localhost:8080/apollo
+ProxyPassReverse  /apollo http://localhost:8080/apollo
+```
+
+Note: that a reverse proxy _does not_ use `ProxyRequests On` (which turns on forward proxying, which is dangerous)
+
+
+Also note: This setup will use downgrade to use AJAX long-polling without the websocket proxy being configured.
+
+
+To setup the proxy for websockets, you can use mod_proxy_wstunnel, first load the module
+
+``` 
+LoadModule proxy_wstunnel_module libexec/apache2/mod_proxy_wstunnel.so
+```
+
+Then add extra ProxyPass calls for the websocket "endpoint" called `/apollo/stomp`
+
+``` 
+ProxyPass /apollo/stomp  ws://localhost:8080/apollo/stomp
+ProxyPassReverse /apollo/stomp ws://localhost:8080/apollo/stomp
+```
+
+##### Debugging proxy issues
+
+Note: if your webapp is accessible but it doesn't seem like you can login, you may need to customize the
+ProxyPassReverseCookiePath
+
+For example, if you proxied to a different path, you might have something like this
+
+``` 
+ProxyPass  /testing http://localhost:8080
+ProxyPassReverse  /testing http://localhost:8080
+ProxyPassReverseCookiePath / /testing
+```
+
+Then your application might be accessible from http://localhost/testing/apollo
+
+
+#### Nginx Proxy (from version 1.4 on)
+
+Your setup may vary, but setting the upgrade headers can be used for the websocket configuration
+http://nginx.org/en/docs/http/websocket.html
+
+``` 
+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
+    
+    server {
+        # Main
+        listen   80; server_name  myserver;
+        
+        # http://nginx.org/en/docs/http/websocket.html
+        location /ApolloSever {
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection $connection_upgrade;
+            proxy_pass      http://127.0.0.1:8080;
+        }
+    }
+```
+
+### Adding extra tabs
+
+Extra tabs can be added to the side panel by over-riding the apollo configuration extraTabs:
+
+```
+    extraTabs = [
+            ['title': 'extra1', 'url': 'http://localhost:8080/apollo/annotator/report/'],
+            ['title': 'extra2', 'content': '<b>Apollo</b> documentation <a href="http://genomearchitect.org" target="_blank">linked here</a>']
+    ]
+
+```
+
+
+### Upgrading existing instances
+
+There are several scripts for migrating from older instances. See the [migration guide](Migration.md) for details.
+Particular notes:
+
+Note: Apollo does not require using the `add-webapollo-plugin.pl` because the plugin is loaded implicitly by
+including the client/apollo/json/annot.json file at run time.
+
+#### Upgrading existing JBrowse data stores
+
+It is not necessary to change your existing JBrowse data directories to use Apollo 2.x, you can just point to existing
+data directories from your previous instances.
+
+More information about [JBrowse](http://jbrowse.org/) can also be found in their [FAQ](http://gmod.org/wiki/JBrowse_FAQ).
+
+#### Adding custom CSS for track styling for JBrowse
+
+There are a variety of different ways to include new CSS into the browser, but the easiest might be the following
+
+
+Add the following statement to your trackList.json:
+
+``` 
+    "css" : "data/yourfile.css"
+```
+
+
+Then just place your CSS file in your organism's data directory.
+
+##### Adding custom CSS globally for JBrowse
+
+If you want to add CSS that is used globally for JBrowse, you can edit the CSS in the client/apollo/css folder, but
+since you need to re-deploy the app every time for updates, it is easier to just edit the data directories for your
+organisms (you do not need to re-deploy the app when you are editing organism specific data, since this is outside of
+the webapp directory and is not deployed with the WAR file)
+
+
+#### Adding custom CSS globally for the GWT app
+
+
+If you want to style the GWT sidebar, generally the bootstrap theme is used but extra CSS is also included from
+web-app/annotator/theme.css which overrides the bootstrap theme
+
+
+#### Adding / using proxies
+
+If you are https, or choose to use separate services rather than the default provided, you can setup a pass-through proxy or modify a particular URL. 
+
+This service is only available to logged-in users. 
+
+The internal proxy URL is: 
+
+```<apollo url>/proxy/request/<encoded_proxy_url>/```
+
+For example if your URL the URL we want to proxy:
+
+```http://golr.geneontology.org/solr/select```
+
+encoded:
+
+```http%3A%2F%2Fgolr.geneontology.org%2Fsolr%2Fselect```
+
+If you user is logged-in and you pass in:
+
+```http://localhost/apollo/proxy/request/http%3A%2F%2Fgolr.geneontology.org%2Fsolr%2Fselect?testkey=asdf&anotherkey=zxcv```
+
+This will get proxied to:
+
+```http://golr.geneontology.org/solr/select?testkey=asdf&anotherkey=zxcv```
+
+If you choose to use another proxy service, you can go to the "Proxy" page (as administrator). 
+Internally used proxies are provided by default. 
+The order the final URL is chosen in is 'active' and then 'fallbackOrder'.  
+
+### Register admin in configuration
+
+If you want to register your admin user in the configuration, you can add a section to your ```apollo-config.groovy``` like:
+
+    apollo{
+    // other stuff
+        admin{
+            username = "super at duperadmin.com"
+            password = System.getenv("APOLLO_ADMIN_PASSWORD")?:"demo"
+            firstName = "Super"
+            lastName = "Admin"
+        }
+    }
+    
+It should only add the user a single time.    User details can be retrieved from passed in text or from the environment depending on user preference.  
+
+Admin users will be added on system startup.  Duplicate additions will be ignored.
+
+### Other authentication strategies
+
+By default Apollo uses a username / password to authenticate users.   However, additional strategies may be used.   
+
+To configure them, add them to the ```apollo-config.groovy``` and set active to true for the ones you want to use to
+ authenticate.
+
+    apollo{
+        // other stuff
+        authentications = [
+            ["name":"Username Password Authenticator",
+             "className":"usernamePasswordAuthenticatorService",
+             "active":true,
+            ]
+            ,
+            ["name":"Remote User Authenticator",
+             "className":"remoteUserAuthenticatorService",
+             "active":false,
+            ]
+        ]
+    }
+
+### URL modifications
+
+You should be able to pass in most JBrowse URL modifications to the ```loadLink``` URL. 
+
+You should use ```tracklist=1``` to force showing the native tracklist (or use the checkbox in the Track Tab in the Annotator Panel).
+
+Use ```openAnnotatorPanel=0``` to close the Annotator Panel explicitly on startup. 
+
+### Phone Home
+
+In order to determine our usage and the current versions of Apollo being used (which helps us to provide Apollo for free), the server and the client will phone home and to google analytics.
+
+To turn off the server phone home set the configuration this way.
+    
+    apollo.phone.phoneHome = false
+    
+To add your own google analytics code set the code up this way:
+
+    google_analytics = ["UA-62921593-1","Your Google Analytics ID"]
+    
+If you don't want any reporting set:
+    
+    google_analytics = []
+
+
+### Only owners can edit
+
+Restricts deletion and reverting to original editor or admin user by setting:
+
+    apollo.only_owners_delete = true
diff --git a/docs/Contributing.md b/docs/Contributing.md
new file mode 100644
index 0000000..1afad4f
--- /dev/null
+++ b/docs/Contributing.md
@@ -0,0 +1,309 @@
+# How to contribute code to Apollo
+
+##### Audience
+These guidelines are for developers of Apollo software, whether internal or in the broader community. 
+
+## Basic principles of the Apollo-flavored [GitHub Workflow](http://guides.github.com/overviews/flow/)
+
+##### Principle 1: Work from a personal fork 
+* Prior to adopting the workflow, a developer will perform a *one-time setup* to create a personal Fork of apollo and will subsequently perform their development and testing on a task-specific branch within their forked repo. This forked repo will be associated with that developer's GitHub account, and is distinct from the shared repo managed by GMOD.
+
+##### Principle 2: Commit to personal branches of that fork
+* Changes will never be committed directly to the master branch on the shared repo. Rather, they will be composed as branches within the developer's forked repo, where the developer can iterate and refine their code prior to submitting it for review.
+
+##### Principle 3: Propose changes via pull request of personal branches
+*  Each set of changes will be developed as a task-specific *branch* in the developer's forked repo, and then create a [pull request](https://github.com/GMOD/Apollo/compare) will be created to develop and propose changes to the shared repo. This mechanism provides a way for developers to discuss, revise and ultimately merge changes from the forked repo into the shared Apollo repo.
+
+##### Principle 4: Delete or ignore stale branches, but don't recycle merged ones
+*  Once a pull request has been merged, the task-specific branch is no longer needed and may be deleted or ignored. It is bad practice to reuse an existing branch once it has been merged. Instead, a subsequent branch and pull-request cycle should begin when a developer switches to a different coding task. 
+*  You may create a pull request in order to get feedback, but if you wish to continue working on the branch, so state with "DO NOT MERGE YET".
+
+## Table of contents
+
+<!-- MarkdownTOC -->
+
+- [One Time Setup - Forking a Shared Repo](#one-time-setup---forking-a-shared-repo)
+    - [Step 1 - Backup your existing repo (optional)](#step-1---backup-your-existing-repo-optional)
+    - [Step 2 - Fork `apollo` via the Web](#step-2---fork-apollo-via-the-web)
+    - [Step 3 - Clone the Fork Locally](#step-3---clone-the-fork-locally)
+    - [Step 4 - Configure the local forked repo](#step-4---configure-the-local-forked-repo)
+    - [Step 5 - Configure  `.bashrc` to show current branch (optional)](#step-5---configure--bashrc-to-show-current-branch-optional)
+- [Typical Development Cycle](#typical-development-cycle)
+    - [Refresh and clean up local environment](#refresh-and-clean-up-local-environment)
+        - [Step 1 - Fetch remotes](#step-1---fetch-remotes)
+        - [Step 2 - Ensure that 'master' is up to date](#step-2---ensure-that-master-is-up-to-date)
+    - [Create a new branch](#create-a-new-branch)
+    - [Changes, Commits and Pushes](#changes-commits-and-pushes)
+    - [Reconcile branch with upstream changes](#reconcile-branch-with-upstream-changes)
+        - [Fetching the upstream branch](#fetching-the-upstream-branch)
+        - [Rebasing to avoid Conflicts and Merge Commits](#rebasing-to-avoid-conflicts-and-merge-commits)
+        - [Dealing with merge conflicts during rebase](#dealing-with-merge-conflicts-during-rebase)
+        - [Advanced: Interactive rebase](#advanced-interactive-rebase)
+    - [Submitting a PR (pull request)](#submitting-a-pr-pull-request)
+    - [Reviewing a pull request](#reviewing-a-pull-request)
+    - [Respond to TravisCI tests](#respond-to-travisci-tests)
+    - [Respond to peer review](#respond-to-peer-review)
+    - [Repushing to a PR branch](#repushing-to-a-pr-branch)
+    - [Merge a pull request](#merge-a-pull-request)
+    - [Celebrate and get back to work](#celebrate-and-get-back-to-work)
+- [GitHub Tricks and Tips](#github-tricks-and-tips)
+- [References and Documentation](#references-and-documentation)
+
+<!-- /MarkdownTOC -->
+
+
+
+## One Time Setup - Forking a Shared Repo
+
+The official shared ``Apollo`` repository is intended to be modified solely via pull requests that are reviewed and merged by a set of responsible 'gatekeeper' developers within the Apollo development team. These pull requests are initially created as task-specific named branches within a developer's personal forked repo.
+
+Typically, a developer will fork a shared repo once, which creates a personal copy of the repo that is associated with the developer's GitHub account. Subsequent pull requests are developed as branches within this personal forked repo. The repo need never be forked again, although each pull request will be based upon a new named branch within this forked repo.
+
+### Step 1 - Backup your existing repo (optional)
+
+The Apollo team has recently adopted the workflow described in this document. Many developers will have an existing clone of the shared repo that they have been using for development. This cloned local directory must be *moved aside* so that a proper clone of the forked repo can be used instead.
+
+*If you do not have an existing local copy of the shared repo, then skip to [Step 2](#step-2---fork-apollo-via-the-web) below.*
+
+
+### Step 2 - Fork `apollo` via the Web
+
+The easiest way to fork the `apollo` repository is via the GitHub web interface:
+
+- Ensure you are logged into GitHub as your GitHub user.
+- Navigate to the apollo shared repo at [https://github.com/GMOD/apollo](https://github.com/GMOD/apollo).
+- Notice the 'Fork' button in the upper right corner. It has a number to the right of the button.
+![](images/githubForkButton.png)
+- Click the Fork button. The resulting behavior will depend upon whether your GitHub user is a member of a GitHub organization. If not a member of an organization, then the fork operation will be performed and the forked repo will be created in the user's account.
+- If your user is a member of an organization (e.g., GMOD or acme-incorporated), then GitHub will present a dialog for the user to choose where to place the forked repo. The user should click on the icon corresponding to their username.
+![](images/githubForkTarget.png)
+- *If you accidentally click the number, you will be on the Network Graphs page and should go back.*
+
+### Step 3 - Clone the Fork Locally
+
+At this point, you will have a fork of the shared repo (e.g., apollo) stored within GitHub, but it is not yet available on your local development machine. This is done as follows:
+
+    # Assumes that directory ~/MI/ will contain your Apollo repos.
+    # Assumes that your username is MarieCurie.
+    # Adapt these instructions to suit your environment
+    > cd ~/MI
+    > git clone git at github.com:MarieCurie/apollo.git
+    > cd apollo
+
+Notice that we are using the SSH transport to clone this repo, rather than the HTTPS transport. The telltale indicator of this is the `git at github.com:MarieCurie...` rather than the alternative `https://github.com/MarieCurie...`.
+
+*Note: If you encounter difficulties with the above `git clone`, you may need to associate your local public SSH key with your GitHub account. See [Which remote URL should I use?](https://help.github.com/articles/which-remote-url-should-i-use/) for information.*
+
+### Step 4 - Configure the local forked repo
+
+The `git clone` above copied the forked repo locally, and configured the symbolic name 'origin' to point back to the *remote* GitHub fork. We will need to create an additional *remote* name to point back to the shared version of the repo (the one that we forked in Step 2). The following should work:
+
+    # Assumes that you are already in the local apollo directory
+    > git remote add upstream https://github.com/GMOD/apollo.git
+
+Verify that remotes are configured correctly by using the command `git remote -v`. The output should resemble:
+
+
+    upstream    https://github.com/GMOD/apollo.git (fetch)
+    upstream    https://github.com/GMOD/apollo.git (push)
+    origin  git at github.com:MarieCurie/apollo.git (fetch)
+    origin  git at github.com:MarieCurie/apollo.git (push)
+
+
+### Step 5 - Configure  `.bashrc` to show current branch (optional)
+
+One of the important things when using Git is to know what branch your working directory is tracking. This can be easily done with the `git status` command, but checking your branch periodically can get tedious. It is easy to configure your `bash` environment so that your current git branch is always displayed in your bash prompt.
+
+If you want to try this out, add the following to your `~/.bashrc` file:
+
+    function parse_git_branch()
+    {
+      git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ \1/'
+    }
+    LIGHT_GRAYBG="\[\033[0;47m\]"
+    LIGHT_PURPLE="\[\033[0;35m\]"
+    NO_COLOR="\[\033[0m\]"
+    export PS1="$LIGHT_PURPLE\w$LIGHT_GRAYBG\$(parse_git_branch)$NO_COLOR \$ "
+
+You will need to open up a new Terminal window (or re-login to your existing terminal) to see the effect of the above `.bashrc` changes.
+
+If you cd to a git working directory, the branch will be displayed in the prompt. For example:
+
+    ~ $
+    ~ $ # This isn't a git directory, so no branch is shown
+    ~ $
+    ~ $ cd /tmp
+    /tmp $
+    /tmp $ # This isn't a git directory, so no branch is shown
+    /tmp $
+    /tmp $ cd ~/MI/apollo/
+    ~/MI/apollo fix-feedback-button $
+    ~/MI/apollo fix-feedback-button $ # The current branch is shown
+    ~/MI/apollo fix-feedback-button $
+    ~/MI/apollo fix-feedback-button $ git status
+    On branch fix-feedback-button
+    Changes not staged for commit:
+      (use "git add <file>..." to update what will be committed)
+      (use "git checkout -- <file>..." to discard changes in working directory)
+        ... remaining output of git status elided ...
+
+---
+
+## Typical Development Cycle
+
+Once you have completed the One-time Setup above, then it will be possible to create new branches and pull requests using the instructions below. The typical development cycle will have the following phases:
+
+- Refresh and clean up local environment
+- Create a new task-specific branch
+- Perform ordinary development work, periodically committing to the branch
+- Prepare and submit a Pull Request (PR) that refers to the branch
+- Participate in PR Review, possibly making changes and pushing new commits to the branch
+- Celebrate when your PR is finally Merged into the shared repo.
+- Move onto the next task and repeat this cycle
+
+
+### Refresh and clean up local environment
+
+Git will not automatically sync your Forked repo with the original shared repo, and will not automatically update your local copy of the Forked repo. These tasks are part of the developer's normal *cycle*, and should be the first thing done prior to beginning a new development effort and creating a new branch. In addition, this
+
+#### Step 1 - Fetch remotes
+
+In the (likely) event that the *upstream* repo (the apollo shared repo) has changed since the developer last began a task, it is important to update the local copy of the upstream repo so that its changes can be incorporated into subsequent development.
+
+    > git fetch upstream        # Updates the local copy of shared repo BUT does not affect the working directory, it simply makes the upstream code available locally for subsequent Git operations. See step 2.
+
+#### Step 2 - Ensure that 'master' is up to date
+
+Assuming that new development begins with branch 'master' (a good practice), then we want to make sure our local 'master' has all the recent changes from 'upstream'. This can be done as follows:
+
+    > git checkout master
+    > git reset --hard upstream/master
+
+The above command is potentially dangerous if you are not paying attention, as it will remove any local commits to master (which you should not have) as well as any changes to local files that are also in the upstream/master version (which you should not have). In other words, the above command ensures a proper clean slate where your local master branch is identical to the upstream master branch.
+
+Some people advocate the use of `git merge upstream/master` or `git rebase upstream/master` instead of the `git reset --hard`. One risk of these options is that unintended local changes accumulate in the branch and end up in an eventual pull request. Basically, it leaves open the possibility that a developer is not really branching from upstream/master, but is branching from some developer-specific branch point.
+
+
+### Create a new branch
+
+Once you have updated the local copy of the master branch of your forked repo, you can create a named branch from this copy and begin to work on your code and pull-request. This is done with:
+
+    > git checkout -b fix-feedback-button   # This is an example name
+
+This will create a local branch called 'fix-feedback-button' and will configure your working directory to track that branch instead of 'master'.
+
+You may now freely make modifications and improvements and these changes will be accumulated into the new branch when you commit.
+
+If you followed the instructions in [Step 5 - Configure  `.bashrc` to show current branch (optional)](#step-5---configure--bashrc-to-show-current-branch-optional), your shell prompt should look something like this:
+
+    ~/MI/apollo fix-feedback-button $
+
+### Changes, Commits and Pushes
+
+Once you are in your working directory on a named branch, you make changes as normal. When you make a commit, you will be committing to the named branch by default, and not to master.
+
+You may wish to periodically `git push` your code to GitHub. Note the use of an explicit branch name that matches the branch you are on (this may not be necessary; a git expert may know better):
+
+    > git push origin fix-feedback-button   # This is an example name
+
+Note that we are pushing to 'origin', which is our forked repo. We are definitely NOT pushing to the shared 'upstream' remote, for which we may not have permission to push.
+
+
+### Reconcile branch with upstream changes
+
+If you have followed the instructions above at [Refresh and clean up local environment](#refresh-and-clean-up-local-environment), then your working directory and task-specific branch will be based on a starting point from the latest-and-greatest version of the shared repo's master branch. Depending upon how long it takes you to develop your changes, and upon how much other developer activity there is, it is possible that changes to the upstream master will conflict with changes in your branch.
+
+So it is a good practice to periodically pull down these upstream changes and reconcile your task branch with the upstream master branch. At the least, this should be performed prior to submitting a PR.
+
+#### Fetching the upstream branch
+
+The first step is to fetch the update upstream master branch down to your local development machine. Note that this command will NOT affect your working directory, but will simply make the upstream master branch available in your local Git environment.
+
+    > git fetch upstream
+
+#### Rebasing to avoid Conflicts and Merge Commits
+
+Now that you've fetched the upstream changes to your local Git environment, you will use the `git rebase` command to adjust your branch
+
+
+    > # Make that your changes are committed to your branch
+    > # before doing any rebase operations
+    > git status
+        # ... Review the git status output to ensure your changes are committed
+        # ... Also a good chance to double-check that you are on your
+        # ... task branch and not accidentally on master
+    > git rebase upstream/master
+
+The rebase command will have the effect of adjusting your commit history so that your task branch changes appear to be based upon the most recently fetched master branch, rather than the older version of master you may have used when you began your task branch.
+
+By periodically rebasing in this way, you can ensure that your changes are in sync with the rest of Apollo development and you can avoid hassles with merge conflicts during the PR process.
+
+
+#### Dealing with merge conflicts during rebase
+
+Sometimes conflicts happen where another developer has made changes and committed them to the upstream master (ideally via a successful PR) and some of those changes overlap with the code you are working on in your branch. The `git rebase` command will detect these conflicts and will give you an opportunity to fix them before continuing the rebase operation. The Git instructions during rebase should be sufficient to understand what to do, but a very verbose explanation can be found at [R [...]
+
+#### Advanced: Interactive rebase
+
+As you gain more confidence in Git and this workflow, you may want to create PRs that are easier to review and best reflect the intent of your code changes. One technique that is helpful is to use the *interactive rebase* capability of Git to help you clean up your branch prior to submitting it as a PR. This is completely optional for novice Git users, but it does produce a nicer shared commit history.
+
+See [squashing commits with rebase](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) for a good explanation.
+
+
+### Submitting a PR (pull request)
+
+Once you have developed code and are confident it is ready for review and final integration into the upstream version, you will want to do a final `git push origin ...` (see Changes, Commits and Pushes above). Then you will use the GitHub website to perform the operation of creating a Pull Request based upon the newly pushed branch.
+
+See [submitting a pull request](https://help.github.com/articles/creating-a-pull-request).
+
+
+### Reviewing a pull request
+
+The set of open PRs for the apollo can be viewed by first visiting the shared apollo GitHub page at [https://github.com/GMOD/apollo](https://github.com/GMOD/apollo).
+
+Click on the 'Pull Requests' link on the right-side of the page:
+![](images/githubPullRequest.png)
+
+Note that the Pull Request you created from your forked repo shows up in the shared repo's Pull Request list. One way to avoid confusion is to think of the shared repo's PR list as a queue of changes to be applied, pending their review and approval.
+
+### Respond to TravisCI tests
+
+The GitHub Pull Request mechanism is designed to allow review and refinement of code prior to its final merge to the shared repo. After creating your Pull Request, the TravisCI tests for apollo will be executed automatically, ensuring that the code that 'worked fine' on your development machine also works in the production-like environment provided by TravisCI. The current status of the tests can be found near the bottom of the individual PR page, to the right of the Merge Request symbol:
+![](images/githubTestProgress.png)
+![](images/githubTestStatus.png)
+
+TBD - Something should be written about developers running tests PRIOR to TravisCI and the the PR. This may already be in the README.md, but should be cited.
+
+
+### Respond to peer review
+
+The GitHub Pull Request mechanism is designed to allow review and refinement of code prior to its final merge to the shared repo. After creating your Pull Request, the TravisCI tests for apollo will be executed automatically, ensuring that the code that 'worked fine' on your development machine also works in the production-like environment provided by TravisCI. The current status of the tests can be found
+
+### Repushing to a PR branch
+
+It's likely that after created a Pull Request, you will receive useful peer review or your TravisCI tests will have failed. In either case, you will make the required changes on your development machine, retest your changes, and you can then push your new changes back to your task branch and the PR will be automatically updated. This allows a PR to evolve in response to feedback from peers. Once everyone is satisfied, the PR may be merged. (see below).
+
+
+### Merge a pull request
+
+One of the goals behind the workflow described here is to enable a large group of developers to meaningfully contribute to the Apollo codebase. The Pull Request mechanism encourages review and refinement of the proposed code changes. As a matter of informal policy, Apollo expects that a PR will not be merged by its author and that a PR will not be merged without at least one reviewer approving it (via a comment such as +1 in the PR's Comment section).
+
+### Celebrate and get back to work
+
+You have successfully gotten your code improvements into the shared repository. Congratulations! The branch you created for this PR is no longer useful, and may be deleted from your forked repo or may be kept. But in no case should the branch be further developed or reused once it has been successfully merge. Subsequent development should be on a new branch. Prepare for your next work by returning to [Refresh and clean up local environment](#refresh-and-clean-up-local-environment).
+
+---
+
+## GitHub Tricks and Tips
+
+- Add `?w=1` to a GitHub file compare URL to ignore whitespace differences.
+
+
+## References and Documentation
+
+- The instructions presented here are derived from several sources. However, a very readable and complete article is [Using the Fork-and-Branch Git Workflow](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/). Note that the article doesn't make clear that certain steps like Forking are one-time setup steps, after which Branch-PullRequest-Merge steps are used; the instructions below will attempt to clarify this.
+
+- New to GitHub? The [GitHub Guides](http://guides.github.com) are a great place to start.
+
+- Advanced GitHub users might want to check out the [GitHub Cheat Sheet](https://github.com/tiimgreen/github-cheat-sheet/blob/master/README.md)
+
diff --git a/docs/Data_loading.md b/docs/Data_loading.md
new file mode 100644
index 0000000..4c873e4
--- /dev/null
+++ b/docs/Data_loading.md
@@ -0,0 +1,264 @@
+# Data generation pipeline
+
+The data generation pipeline is based on the typical jbrowse commands such as prepare-refseqs.pl and
+flatfile-to-json.pl, and these scripts are automatically copied to a local bin/ directory when you run the setup scripts
+(e.g. `apollo run-local` or `apollo deploy` or `install_jbrowse.sh`).
+
+If you don't see a bin/ subdirectory containing these scripts after running the setup, check setup.log and check the
+[troubleshooting guide](Troubleshooting.md) for additional tips or feel free to post the error and setup.log on GitHub
+or the mailing list.
+
+### prepare-refseqs.pl
+
+The first step to setup the genome browser is to load the reference genome data. We'll use the `prepare-refseqs.pl`
+script to output to the data directory.
+
+``` 
+bin/prepare-refseqs.pl --fasta pyu_data/scf1117875582023.fa --out /opt/apollo/data
+```
+
+If you want to use an indexed FASTA genome then you can run prepare-refseqs.pl as follows:
+
+```
+bin/prepare-refseqs.pl --indexed_fasta pyu_data/scf1117875582023.fa --out /opt/apollo/data
+```
+
+The script will copy the genome FASTA and its FAI index into the output folder.
+
+Note: the output directory is used later when we load the organism into the browser with the "Create organism" form
+
+### flatfile-to-json.pl
+
+The flatfile-to-json.pl script can be used to load GFF3 files and you can customize the feature types. Here, we'll start
+off by loading data from the MAKER GFF for the Pythium ultimum data. The simplest loading command specifies a
+--trackLabel, the --type of feature to load, the --gff file and the --out directory.
+
+``` 
+bin/flatfile-to-json.pl --gff pyu_data/scf1117875582023.gff --type mRNA \
+        --trackLabel MAKER --out /opt/apollo/data
+```
+ 
+Note: you can also use the command `bin/maker2jbrowse` for loading the MAKER data.
+
+Also see the section [Customizing features](Data_loading.md#customizing-features) section for more information on
+customizing the CSS styles of the Apollo features.
+
+Note: Apollo uses features that are loaded at the "transcript" level. If your GFF3 has "gene" features with
+"transcript"/"mRNA" child features, make sure that you use the argument --type mRNA or --type transcript.
+
+
+### generate-names.pl
+
+Once data tracks have been created, you can generate a searchable index of names using the generate-names.pl script:
+
+``` 
+bin/generate-names.pl --verbose --out /opt/apollo/data
+```
+
+This is optional but useful step to index of names and features and refseq names. If you have some tracks that have
+millions of features, consider only indexing select tracks with the --tracks argument or disabling autocomplete with
+ `--completionLimit 0`.
+
+### add-bam-track.pl
+
+Apollo natively supports BAM files and the file can be read (in chunks) directly from the server with no
+preprocessing.
+
+To add a BAM track, copy the .bam and .bam.bai files to your data directory, and then use the add-bam-track.pl to add
+the file to the tracklist.
+
+``` 
+mkdir /opt/apollo/data/bam
+cp pyu_data/simulated-sorted.bam /opt/apollo/data/bam
+cp pyu_data/simulated-sorted.bam.bai /opt/apollo/data/bam
+bin/add-bam-track.pl --bam_url bam/simulated-sorted.bam \
+   --label simulated_bam --key "simulated BAM" -i /opt/apollo/data/trackList.json
+```
+
+
+Note: the `bam_url` parameter is a URL that is relative to the data directory. It is not a filepath! Also, the .bai will
+automatically be located if it is simply the .bam with .bai appended to it.
+
+### add-bw-track.pl
+
+Apollo also has native support for BigWig files (.bw), so no extra processing of these files is required either.
+
+To use this, copy the BigWig data into the jbrowse data directory and then use the add-bw-track.pl to add the file to
+the tracklist.
+
+``` 
+mkdir /opt/apollo/data/bigwig
+cp pyu_data/*.bw /opt/apollo/data/bigwig
+bin/add-bw-track.pl --bw_url bigwig/simulated-sorted.coverage.bw \
+    --label simulated_bw --key "simulated BigWig"
+```
+
+Note: the `bw_url` parameter is a URL that is relative to the data directory. It is not a filepath!
+
+### Customizing different annotation types (advanced)
+
+To change how the different annotation types look in the "User-created annotation" track, you'll need to update the
+mapping of the annotation type to the appropriate CSS class. This data resides in `client/apollo/json/annot.json`, which
+is a file containing Apollo tracks that is loaded by default. You'll need to modify the JSON entry whose label is
+`Annotations`. Of particular interest is the `alternateClasses` element. Let's look at that default element:
+
+``` 
+"alternateClasses": {
+    "pseudogene" : {
+       "className" : "light-purple-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "tRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "snRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "snoRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "ncRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "miRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "rRNA" : {
+       "className" : "brightgreen-80pct",
+       "renderClassName" : "gray-center-30pct"
+    },
+    "repeat_region" : {
+       "className" : "magenta-80pct"
+    },
+    "transposable_element" : {
+       "className" : "blue-ibeam",
+       "renderClassName" : "blue-ibeam-render"
+    }
+}
+```
+
+For each annotation type, you can override the default class mapping for both `className` and `renderClassName` to use
+another CSS class. Check out the [Customizing features](Data_loading.md#customizing-features) section for more
+information on customizing the CSS classes.
+
+### Customizing features
+
+The visual appearance of biological features in Apollo (and JBrowse) is handled by CSS stylesheets with HTMLFeatures
+tracks. Every feature and subfeature is given a default CSS "class" that matches a default CSS style in a CSS
+stylesheet. These styles are are defined in `client/apollo/css/track_styles.css` and
+`client/apollo/css/webapollo_track_styles.css`. Additional styles are also defined in these files, and can be used by
+explicitly specifying them in the --className, --subfeatureClasses, --renderClassname, or --arrowheadClass parameters to
+flatfile-to-json.pl ([see data loading section](Data_loading.md#flatfile-to-json.pl_transcripts)).
+
+Apollo differs from JBrowse in some of it's styling, largely in order to help with feature selection, edge-matching,
+and dragging. Apollo by default uses invisible container elements (with style class names like "container-16px") for
+features that have children, so that the children are fully contained within the parent feature. This is paired with
+another styled element that gets rendered *within* the feature but underneath the subfeatures, and is specified by the
+`--renderClassname` argument to `flatfile-to-json.pl`. Exons are also by default treated as special invisible
+containers, which hold styled elements for UTRs and CDS.
+
+It is relatively easy to add other stylesheets that have custom style classes that can be used as parameters to
+`flatfile-to-json.pl`. For example, you can create `/opt/apollo/data/custom_track_styles.css` which contains two new
+styles:
+
+``` 
+    .gold-90pct, 
+    .plus-gold-90pct, 
+    .minus-gold-90pct  {
+        background-color: gold;
+        height: 90%;
+        top: 5%;
+        border: 1px solid gray;
+    }
+
+    .dimgold-60pct, 
+    .plus-dimgold-60pct, 
+    .minus-dimgold-60pct  {
+        background-color: #B39700;
+        height: 60%;
+        top: 20%;
+    }
+```
+
+
+In this example, two subfeature styles are defined, and the *top* property is being set to (100%-height)/2 to assure
+that the subfeatures are centered vertically within their parent feature. When defining new styles for features, it is
+important to specify rules that apply to plus-*stylename* and minus-*stylename* in addition to *stylename*, as Apollo
+adds the "plus-" or "minus-" to the class of the feature if the the feature has a strand orientation.
+
+You need to tell Apollo where to find these styles by modifying the JBrowse config or the plugin config, e.g. by
+adding this to the trackList.json
+
+``` 
+    "css" : "data/custom_track_styles.css"
+```
+
+Then you may use these new styles using --subfeatureClasses, which uses the specified CSS classes for your features in
+the genome browser, for example:
+
+``` 
+    bin/flatfile-to-json.pl --gff MyFile.gff \
+       --type mRNA --trackLabel MyTrack      \
+       --subfeatureClasses '{"CDS":"gold-90pct","UTR": "dimgold-60pct"}'
+```
+
+### Bulk loading annotations to the user annotation track
+
+#### GFF3
+
+You can use the `tools/data/add_features_from_gff3_to_annotations.pl` script to bulk load GFF3 files with transcripts
+to the user annotation track. Let's say we want to load our `maker.gff` transcripts.
+
+``` 
+    tools/data/add_features_from_gff3_to_annotations.pl \
+        -U localhost:8080/Apollo -u web_apollo_admin -p web_apollo_admin \
+        -i scf1117875582023.gff -t mRNA -o "name of organism"
+```
+
+
+The default options should be able to handle most GFF3 files that contain genes, transcripts, and exons.
+
+You can still use this script even if the GFF3 file that you are loading does not contain transcripts and exon types.
+Let's say we want to load `match` and `match_part` features as transcripts and exons respectively. We'll use the
+`blastn.gff` file as an example.
+
+``` 
+    tools/data/add_features_from_gff3_to_annotations.pl \
+       -U localhost:8080/Apollo -u web_apollo_admin -p web_apollo_admin \
+       -i cf1117875582023.gff -t match -e match_part -o "name of organism"
+```
+
+
+You can view the add_features_from_gff3_to_annotations.pl help (`-h`) option for all available options.
+
+**Note:** Apollo makes a clear distinction between a transcript and an mRNA. Genes that have mRNA as its child feature
+are treated as protein coding annotations and Genes that have transcript as its child feature are treated as non-coding
+annotations, specifically a pseudogene.
+
+**Note:** In order to create meaningful names from your evidence when creating manual annotations, the GFF3 should 
+provide the `Name` attribute in column 9 of the [GFF3 spec](https://github.com/The-Sequence-Ontology/Specifications/blob/master/gff3.md) as shown in this example:
+
+    NC_000001.11    BestRefSeq      gene    11874   14409   .       +       .       ID=gene1;Name=DDX11L1;Dbxref=GeneID:100287102,HGNC:37102;description=DEAD%2FH %28Asp-Glu-Ala-Asp%2FHis%29 box helicase 11 like 1;gbkey=Gene;gene=DDX11L1;pseudo=true
+
+
+If you would like to look at a compatible representative GFF3, export annotations from Apollo via GFF3 export.
+
+### Disable draggable
+
+
+Apollo has a number of specific track config parameters
+
+``` 
+overrideDraggable (boolean)
+determines whether to transform the alignments tracks to draggable alignments
+
+overridePlugins (boolean)
+determines whether to transform alignments and sequence tracks
+```
+
+These can be specified on a specific track or in a global config.
diff --git a/docs/Data_loading_via_web_services.md b/docs/Data_loading_via_web_services.md
new file mode 100644
index 0000000..571a246
--- /dev/null
+++ b/docs/Data_loading_via_web_services.md
@@ -0,0 +1,159 @@
+# Data Loading via Web Services
+
+If you setup your Apollo instance on a remote server and your data is on your local machine, you can make use of Apollo 
+Web Services API to load your locally processed (and raw) data into Apollo.
+
+**Note:** Only the admin user can make use of Web Services for loading data into Apollo.
+
+
+While setting up Apollo, make sure you set `common_data_directory` in your `apollo-config.groovy`:
+
+```
+common_data_directory = "/opt/apollo"
+```
+
+By default this is `/opt/apollo` which can be changed to any other location on the file system where you would want 
+Apollo to store files uploaded via Web Services.
+
+
+
+### Adding organism with sequence
+
+You can add an organism along with its sequence data (generated by `prepare-refseqs.pl`) to Apollo.
+This is useful when an organism's data is not on the same server as the Apollo instance.
+
+For the sake of an easy example, we will demonstrate how to upload your organism sequence data to Apollo with 
+Web Services using `curl`.
+
+```
+curl http://localhost:8080/apollo/organism/addOrganismWithSequence  \
+    -F "commonName=Amel" \
+    -F "username=admin" \
+    -F "password=admin" \
+    -F "organismData=@compressed-jbrowse-data.zip"
+```
+
+`curl` will upload your `compressed-jbrowse-data.zip` file to Apollo.
+
+Once the upload is complete, Apollo will decompress the file and place it in `common_data_directory` (specified in your `apollo-config.groovy`).
+Apollo will then create an organism with the supplied common name and link to the decompressed data folder as its directory. 
+
+Currently the `organism/addOrganismWithSequence` endpoint supports Zip (\*.zip) and Tar GZip (\*.tar.gz) compressed folders.
+
+
+
+### Adding tracks to an organism
+
+You can add a track to an existing organism with the `organism/addTrackToOrganism` endpoint.
+
+Here is an example that uploads a compressed folder containing track data (generated by `flatfile-to-json.pl`) to an 
+existing organism.
+
+```
+curl http://localhost:8080/apollo/organism/addTrackToOrganism \
+    -F "organism=100001" \
+    -F "username=admin" \
+    -F "password=admin" \
+    -F "trackData=@/path/to/compressed-track-data.zip" \
+    -F "trackConfig={'label': 'track_name',type: 'FeatureTrack', 'urlTemplate': 'tracks/track_name/\{refseq\}/trackData.json'}"
+
+```
+
+The track configuration (JBrowse JSON) must be supplied via `trackData` parameter and this JSON must contain 'label'
+and 'urlTemplate'.  
+
+**Note**: The label is a unique identifier used to reference the track and can not be changed, just removed.
+
+Here is another example that uploads a BAM file along with its index to an existing organism.
+
+```
+curl http://localhost:8080/apollo/organism/addTrackToOrganism \
+    -F "organism=100001" \
+    -F "username=admin" \
+    -F "password=admin" \
+    -F "trackFile=@alignments.bam" \
+    -F "trackFileIndex=@alignments.bam.bai" \
+    -F "trackConfig={'label': 'bam_alignments', 'urlTemplate': 'bam/alignments.bam', 'key': 'BAM Alignments','type': 'JBrowse/View/Track/Alignments2'}"
+```
+
+`trackFile` parameter takes in the BAM file (or any other flat file like *.vcf, *.vcf.gz, *.bw, *.bigwig, etc.)
+
+`trackFileIndex` parameter takes in the index for the file provided by `trackFile`, and this parameter is optional since 
+not all flat files will have its associated index.
+
+You can use this endpoint to add a track to any organism, irrespective of whether it was added by `organism/addOrganismWithSequence` endpoint.
+
+In both cases, Apollo will take the uploaded data and store it in `common_data_directory`. 
+
+
+### Updating a track for an organism
+
+You can update an existing track from an organism via the `organism/updateTrackForOrganism` endpoint.
+
+As an example, lets update the previously added track with some additional configurations.
+
+```
+curl http://localhost:8080/apollo/organism/updateTrackForOrganism \
+    -F "organism=100001" \
+    -F "username=admin" \
+    -F "password=admin" \
+    -F "trackConfig={'label': 'track_name', 'urlTemplate': 'tracks/track_name/\{refseq\}/trackData.json', 'key': 'Track Name', 'type': 'JBrowse/View/Track/CanvasFeatures'}"    
+
+```
+
+This endpoint will overwrite an existing track's configuration in favor for the configuration provided by the `trackConfig` parameter.
+Thus, provide the entire track's JSON configuration with all relevant parameters.
+
+
+**Note:** This operation will be successful only for tracks added by `organism/addTrackToOrganism` endpoint.
+
+
+
+### Deleting a track from an organism
+
+You can delete a track from an organism using the `organism/deleteTrackFromOrganism` endpoint.
+
+As an example, lets remove the previously added BAM track.
+
+```
+curl  http://localhost:8080/apollo/organism/deleteTrackFromOrganism \
+    -F "organism=100001"  \
+    -F "username=admin"  \
+    -F "password=admin"  \
+    -F "trackLabel=bam_alignments" 
+
+```
+
+`trackLabel` corresponds to the `label` parameter in a track's JSON configuration and will be used to uniquely identify a track.
+
+
+**Note:** This operation will be successful only for tracks added by `organism/addTrackToOrganism` endpoint.
+
+
+
+### Delete an organism along with its data
+
+You can delete an organism along with its data directory using the `organism/deleteOrganismWithSequence` endpoint.
+
+Few things to bear in mind before sending a request to this endpoint:
+* This operation will delete both the organism and its data directory only for those organisms added via 
+`organism/addOrganismWithSequence` endpoint.
+* If you try this on an organism that was not added by `organism/addOrganismWithSequence` endpoint, Apollo will still 
+delete the organism but will only remove the data that the organism has in the `common_data_directory` (if any) while 
+leaving the original organism data directory and its contents intact. Apollo will also return a warning stating that it 
+cannot remove the organism's data directory.
+
+As an example, lets remove the organism Amel with ID `100001`:
+
+```
+curl http://localhost:8080/apollo/organism/deleteOrganismWithSequence  \
+    -F "organism=100001" \
+    -F "username=admin" \
+    -F "password=admin" \
+
+```
+
+This will remove organism with ID `100001`. 
+Since this organism was added by the `organism/addOrganismWithSequence` endpoint, Apollo will delete its data directory 
+from `common_data_directory`. 
+
diff --git a/docs/ExampleBuild.md b/docs/ExampleBuild.md
new file mode 100644
index 0000000..524ae2e
--- /dev/null
+++ b/docs/ExampleBuild.md
@@ -0,0 +1,75 @@
+
+# Example Build Script on Unix with MySQL
+
+This is an example build script.  It may **NOT** be appropriate for your environment
+but does demonstrate what a typical build process **might** look like on a 
+Unix system using MySQL.
+
+Please consult our [Setup](Setup.md) and [Configuration](Configuration.md) 
+documentation for additional information.
+
+
+```
+# Install prereqs
+apt-get install tomcat8 git ant openjdk-8-jdk nodejs
+# Upped tomcat memory per Apollo devs instructions:
+echo "export CATALINA_OPTS="-Xms512m -Xmx1g \
+              -XX:+CMSClassUnloadingEnabled \
+              -XX:+CMSPermGenSweepingEnabled \
+              -XX:+UseConcMarkSweepGC" >> /etc/default/tomcat8
+
+# Download and extract their tarball
+npm install -g bower
+wget https://github.com/GMOD/Apollo/archive/2.0.4.tar.gz
+mv 2.0.4.tar.gz Apollo-2.0.4.tar.gz
+tar xf Apollo-2.0.4.tar.gz
+# Setup apollo mysql user and database
+CREATE USER 'apollo'@'localhost' IDENTIFIED BY 'THE_PASSWORD';
+CREATE DATABASE `apollo-production`;
+GRANT ALL PRIVILEGES ON `apollo-production`.* To 'apollo'@'localhost' IDENTIFIED BY 'THE_PASSWORD';
+# Configure apollo for mysql.
+cd ~/src/Apollo-2.0.4
+# Let's store the config file outside of the source tree.
+mkdir ~/apollo.config
+# Copy the template
+cp sample-mysql-apollo-config.groovy ~/apollo.config/apollo-config.groovy 
+ln -s ~/apollo.config/apollo-config.groovy
+# For now, turn off tomcat8 so that we can see if the locally-run version works service tomcat8 stop
+# Run the local version, which verifies install reqs, and does a bunch of stuff (see below) 
+cd Apollo-2.0.4
+./apollo run-local
+
+# Some of what the Apollo installer does:
+# Clones a bunch of git submodules into apollo-2.0.4/src
+# Does a bunch of java compiling.
+# Downloads and installs grails for you here: $HOME/.grails . 
+# Installs perl modules here: $HOME/.cpanm
+# Installs java stuff here: $HOME/.java and $HOME/.m2
+
+# If a pre-installed instance: 
+rm -rf /var/lib/tomcat/webapps/apollo
+rm -f /var/lib/tomcat/webapps/apollo.war
+# Startup tomcat again
+service tomcat8 start
+
+# ... with javascript minimization: 
+./apollo release
+# ... without javascript minimization
+#  ./apollo deploy
+# Above creates this file: target/apollo-2.0.4.war
+sudo cp target/apollo-2.0.4.war /var/lib/tomcat/webapps/apollo.war
+
+# Prepare JBrowse data
+# Add the FASTA assembly
+~/src/Apollo-2.0.4/bin/prepare-refseqs.pl \
+--fasta /research/dre/assembly/assembly1.fasta.gz \
+--out ~/organisms/dre
+
+# Add annotations
+~/src/Apollo-2.0.4/bin/flatfile-to-json.pl \
+--gff /research/dre/annotation/FINAL_annotations/ssc_v4.gff \ 
+--type mRNA --trackLabel Annotations --out ~/organisms/dre
+
+# In interface point to directory ~/organisms/dre
+```
+
diff --git a/docs/Migration.md b/docs/Migration.md
new file mode 100644
index 0000000..b2f096e
--- /dev/null
+++ b/docs/Migration.md
@@ -0,0 +1,99 @@
+# Migration guide
+
+This guide explains how to prepare your Apollo 2.x instance, and to migrate data from previous Web Apollo versions
+into 2.0.
+
+In all cases you will need to follow the [guide for setting up your 2.x instance](Apollo2Build.md).
+
+
+## Migration from Evaluation to Production:
+
+If you are running your evaluation/development version using `./apollo run-local` when you setup your production
+instance, any prior annotations will use a separate database.  
+
+If you are using the same production instance you can use scripts to delete all annotations and preferences:
+
+`scripts/delete_all_features.sh`
+
+or just the annotations:
+
+`scripts/delete_only_features.sh`
+
+If you want to start from scratch (including reloading organisms and users), you can just drop the database (when the
+server is not running) and the proper tables will be recreated on startup.
+
+## Migration from 2.0.X to 2.0.Y on production:
+
+### Installation from a downloaded release
+
+- [Download the desired Apollo release](https://github.com/GMOD/Apollo/releases/) from the bottom of each release.   Official releases will be tagged as "Release" and have a green label.
+- Expand the archive. 
+- Copy your existing apollo-config.groovy file into the directory. 
+- Always backup your database!
+- Create a new war file as below: ```./apollo deploy```.
+- Turn off tomcat and remove the old apollo directory and ```.war``` file in the webapps folder.
+- Copy in new .war file with the same name.
+- Restart tomcat and you are ready to go.
+
+Note if you you choose to have two different versions of Apollo running, though need to point to different database instances or you will experience problems.
+
+### Installation from a checked out github
+
+If you want bleeding and only moderately tested code (not recommended unless you feel you know what you're doing), you can clone Apollo directly from our source page https://github.com/GMOD/Apollo/
+
+Any upgrading can be taken care of during a pull.  Please note that as we sometimes change the version of JBrowse, so you should do:
+
+```./apollo clean-all```
+
+before building a target for production.
+
+You can the follow the directions for deploying a downloaded release, above.
+
+
+## Migration from 1.0 to 2.0:
+
+We provide examples in the form of [migration scripts](https://github.com/gmod/apollo/tree/master/docs/web_services/
+examples) in the docs/web_services/examples directory. These tools are also described in the [command line tools 
+section](Command_line.md).
+
+We have written many of the [command line tools](Command_line.md) examples using the groovy language, but mostly any
+language will work (Perl, shell/curl, Python, etc.).
+
+
+### Migrate Annotations
+
+We provide a [migration script](https://github.com/gmod/apollo/tree/master/docs/web_services/examples/groovy/
+migrate_annotations1to2.groovy) that connects to a single Web Apollo 1 instance and populates the annotations for an
+organism for a set of sequences / (confusingly called tracks as well).  It would be best to develop your script on a
+development instance of Apollo2 for restricted sequences.
+
+To get the scripts working properly, you'll need to provide the list of sequences (or tracks) to migrate for each
+organism.  You can get the list of tracks by either using the database (`select * from tracks ;`) or looking in the Web
+Apollo annotations directory
+
+``` 
+ls -1 /opt/apollo/annotations/ | grep Annotations | grep -v history | paste -s -d"," -
+```
+
+
+
+### Migrate Users
+
+You have to add users de novo using something like the [add_users.groovy
+script](https://github.com/gmod/apollo/tree/master/docs/web_services/examples/groovy/add_users.groovy). In this case you
+create a csv file with the email, name, password, and role ('user' or 'admin'). This is passed into the add_users.groovy
+script and users are added.  
+
+From Web Apollo 1, you should be able to pull user names out of the database `select * from users ;`, but there is not
+much overlap between users in Web Apollo1.x and Apollo2.x.
+
+If you have only a few users, however, just adding them manually on the users will likely be easier. 
+
+### Add Organisms
+
+If possible adding organisms on the organisms tab is the easiest option if you only have a handful of organisms.  
+
+The [add_organism.groovy script](https://github.com/gmod/apollo/tree/master/docs/web_services/examples/groovy/
+add_organism.groovy) can help automate this process if you have a large number of migrations to handle.
+
+
diff --git a/docs/NCBI_checks/Additional_Checks.txt b/docs/NCBI_checks/Additional_Checks.txt
new file mode 100644
index 0000000..783cdaa
--- /dev/null
+++ b/docs/NCBI_checks/Additional_Checks.txt
@@ -0,0 +1,26 @@
+
+There were a variety of issues we dealt with the exported GFF from Apollo, and just ended up writing a set of post-apollo-export cleanup scripts that formatted everything back how it should be. Most of these are part of my biocode collection of scripts (https://github.com/jorvis/biocode).
+
+I think there might have been a few extras manually done to take care of a few other things (like those where the coordinates were negative) but what I'm pasting below represented the bulk of them:
+
+$ cat /home/jorvis/bin/process_webapollo_gff3.bash
+#!/bin/bash
+
+#1. Replace all instances of 'transcript' in the third column with 'mRNA'
+#2. Removes any orphaned features (which have no children or parent)
+#3. Finds RNAs which have no parent gene and adds them
+#4. Checks for any exon's which don't point to the right parent, corrects them
+#5. Collapse each gene coordinates to the widest child RNA
+#6. Removes any duplicate features based on the key string: molecule_id, parent_id, feature_type, start, stop
+
+/home/jorvis/git/biocode/gff/replace_gff_type_column_value.py -i $1 -it transcript -o $1.mRNA -ot mRNA
+
+/home/jorvis/git/biocode/gff/remove_orphaned_features.py -i $1.mRNA -o $1.mRNA.noorphans
+
+/home/jorvis/git/biocode/sandbox/jorvis/correct_RNAs_missing_genes.py -i $1.mRNA.noorphans -o $1.mRNA.noorphans.nomissinggenes
+
+/home/jorvis/git/biocode/sandbox/jorvis/correct_gff3_exon_parentage.py -i $1.mRNA.noorphans.nomissinggenes -o $1.mRNA.noorphans.nomissinggenes.fixedparents
+
+/home/jorvis/git/biocode/sandbox/jorvis/collapse_gene_coordinates_to_mRNA_range.py -i $1.mRNA.noorphans.nomissinggenes.fixedparents -o $1.mRNA.noorphans.nomissinggenes.fixedparents.collapsedgenes
+
+/home/jorvis/git/biocode/gff/remove_duplicate_features.py -i $1.mRNA.noorphans.nomissinggenes.fixedparents.collapsedgenes -o $1.mRNA.noorphans.nomissinggenes.fixedparents.collapsedgenes.nodupes
diff --git a/docs/NCBI_checks/Common_Eukaryotic_Annotation_Errors_to_Avoid.docx b/docs/NCBI_checks/Common_Eukaryotic_Annotation_Errors_to_Avoid.docx
new file mode 100644
index 0000000..c918a07
Binary files /dev/null and b/docs/NCBI_checks/Common_Eukaryotic_Annotation_Errors_to_Avoid.docx differ
diff --git a/docs/Permissions.md b/docs/Permissions.md
new file mode 100644
index 0000000..8a1b48f
--- /dev/null
+++ b/docs/Permissions.md
@@ -0,0 +1,59 @@
+# Permissions guide
+
+### Global
+
+* **admin**: access to everything
+* **user**: only guarantees a login with permissions configured on organism basis
+
+
+### Organism
+
+Can only view things related to that organism.
+
+* **read**: view / search only, no annotation
+
+``` 
+    Annotations: lock detail / coding
+    RefSeq: hide export
+    Organism: hide
+    User: hide 
+    Group: hide 
+    Preferences: hide 
+    JBrowse: disable UcA track 
+```
+* **export**: same as read, but can use the export screen
+
+``` 
+    RefSeq: show export 
+```
+
+* **write**: same as above, but can add / edit annotations
+
+``` 
+    Annotations: allow editing
+    JBrowse: enable UcA track 
+```
+
+* **admin**: access to everything for that organism
+
+``` 
+    Organism: show
+    User: show 
+    Group: show
+    Preferences: (still hide)
+```
+
+
+Table of permissions:
+
+``` 
+| Permission | Annotator          | Users/groups  | Annotations         | Organism                  |
+|------------|--------------------|---------------|---------------------|---------------------------|
+| READ       | visible / locked   | hide          | visible / no export | visible                   |
+| EXPORT     | visible / locked   | hide          | visible / export    | visible                   |
+| WRITE      | visible + editable | hide          | visible / export    | visible                   |
+| ADMIN      | visible + editable | visible       | visible /export     | visible + admin functions |
+| NONE       | not available      | not available | not available       | not visible               |
+```
+
+The Preference panel is available only for GLOBAL admin.
diff --git a/docs/PostgreSQLSetup.md b/docs/PostgreSQLSetup.md
new file mode 100644
index 0000000..284485d
--- /dev/null
+++ b/docs/PostgreSQLSetup.md
@@ -0,0 +1,37 @@
+# PostgreSQL Setup guide
+
+There are a couple ways to setup a PostgreSQL.  One would be as a trusted user (e.g. postgres):
+
+## Setup as a non-trusted user "database_user" with a secure password for a production database named "apollo-production"
+
+- On debian/ubuntu/redhat/centOS,requires postgres user to execute command, hence "sudo su postgres"
+```
+sudo su postgres -c "createuser -RDIElPS database_user"
+sudo su postgres -c "createdb -E UTF-8 -O database_user apollo-production"
+```
+- On macOSX/homebrew, not necessary to login to postgres user
+
+```
+createuser -RDIElPS database_user 
+createdb -O database_user apollo-production
+```
+
+
+- In ```apollo-config.groovy``` your username will be the name of the user and you should provide the password.
+
+## Setup as a trusted postgres user with a database named "apollo-production"
+
+- On debian/ubuntu/redhat/centOS,requires postgres user to execute command, hence "sudo su postgres"
+```
+sudo su postgres -c "createuser -RDIElPS $PGUSER"
+sudo su postgres -c "createdb -E UTF-8 -O $PGUSER apollo-production"
+```
+- On macOSX/homebrew, not necessary to login to postgres user
+```
+createuser -RDIElPS $PGUSER
+createdb -O $PGUSER apollo-production
+```
+
+- In ```apollo-config.groovy``` your username will be postgres and you should comment out the password line.
+
+Note: Using a tool like [pgtune](http://pgtune.leopard.in.ua/) might help to tune PostgreSQL settings.  
diff --git a/docs/Prerequisites.md b/docs/Prerequisites.md
new file mode 100644
index 0000000..264bcf7
--- /dev/null
+++ b/docs/Prerequisites.md
@@ -0,0 +1,57 @@
+## Pre-requisites
+
+
+### Client pre-requisites
+
+Apollo is a web-based application, so the only client side
+requirement is a web browser. Apollo has been tested on Chrome, Firefox, and Safari
+and matches the web browser requirements for JBrowse (see [jbrowse.org](http://jbrowse.org) for details).
+
+### Server-side pre-requisites
+
+Note: see the [Apollo 2.x quick-start](Apollo2Build.md) for the
+quickest way to take care of pre-requisites.
+
+-   System pre-requisites (see quick-start guide for simple setup)
+    -   Any Unix like system (e.g., Unix, Linux, Mac OS X).
+    -   Servlet container (must support servlet spec 3.0+) such as tomcat 8 for production (not needed for development).
+    -   Java 8+  OpenJDK or Oracle should work.
+    -   [npm 2.X or better / node.js](https://nodejs.org/en/download/package-manager/)
+    -   Grails (optional, but good for development).   The easiest way to install is using sdkman, see [Apollo 2.x quick-start](Apollo2Build.md) for this step).
+    -   Ant 1.8+ (most package managers will have this).
+    -   A database (RDMS) system. Sample configurations for PostgreSQL and MySQL are available. H2 configuration does not require any manual installation.
+    -   Basic tools like Git, Curl, a text editor, etc.
+-   Data generation pipeline pre-requisites (for full list see http://gmod.org/wiki/JBrowse_Configuration_Guide)
+    -   System packages:
+        -   libpng12-0 (optional, for JBrowse imagetrack)
+        -   libpng12-dev (optional, for JBrowse imagetrack)
+        -   zlib1g (Debian/Ubuntu)
+        -   zlib1g-dev (Debian/Ubuntu)
+        -   zlib (RedHat/CentOS)
+        -   zlib-devel (RedHat/CentOS)
+        -   libexpat1-dev (Debian/Ubuntu)
+        -   expat-dev (RedHat/CentOS)
+-   Perl pre-requisites:
+    -   Apollo will automatically try to install all perl-pre-requisites.
+    -   If you are building Apollo in "release" mode, perl 5.10 or up will be required
+-   Sequence search (optional).
+    -   Blat (download [Linux](http://hgdownload.cse.ucsc.edu/admin/exe/linux.x86_64/) or [OSX](http://hgdownload.cse.ucsc.edu/admin/exe/macOSX.x86_64/) binaries).
+
+#### Package manager commands
+
+To install system pre-requisites, you can try the following commands
+
+
+##### Debian/Ubuntu 16
+
+`sudo apt-get install openjdk-8-jdk curl libexpat1-dev postgresql postgresql-server-dev-all tomcat8 git`
+
+##### CentOS/RedHat
+
+`sudo yum install postgresql postgresql-server postgresql-devel expat-devel tomcat git curl`
+
+##### MacOSX/Homebrew
+
+`brew install postgresql tomcat git`
+
+
diff --git a/docs/Release.md b/docs/Release.md
new file mode 100644
index 0000000..0718503
--- /dev/null
+++ b/docs/Release.md
@@ -0,0 +1,18 @@
+## Rough steps for Release of X.Y.Z
+
+- Update version in ```application.properties```.
+- Update version in ```docs/conf.py```.
+- Search doc for specific cases related to ```X.Y.Z```.
+- Confirm ChangeLog.md.
+- Commit and push.
+- In GH Create / Tag a Release reflecting ChangeLog.md with summary.
+
+- Update servers / images:
+    - Update demo, download new release and build
+    - Update docker images (https://github.com/GMOD/docker-apollo), retag for docker hub, and create a release
+    - Update AWS images and resubmit.
+    - Update AWS  training image.
+    
+- Update genomearchitect to reflect zenodo and version.
+- Generate and update link on master to reflect zenodo link if appropriate.
+- Do announcement on gmod-ajax, apollo, and other lists.
diff --git a/docs/Setup.md b/docs/Setup.md
new file mode 100644
index 0000000..c152040
--- /dev/null
+++ b/docs/Setup.md
@@ -0,0 +1,248 @@
+# Setup guide
+
+
+The quick-start guide showed how to quickly launch a temporary instance of Apollo, but deploying the application to
+production normally involves some extra steps.
+
+
+The general idea behind your deployment is to create a `apollo-config.groovy` file from some existing sample files which
+have sample settings for various database engines.
+
+
+## Production pre-requisites
+
+You will minimally need to have Java 8 or greater, [Grails](https://grails.org/), [git](https://git-scm.com/),
+[ant](http://ant.apache.org/), a servlet container e.g. [tomcat7+](http://tomcat.apache.org/), jetty, or resin. An
+external database such as PostgreSQL or MySQL is generally used for production, but instructions for the H2 database is
+also provided.
+
+**Important note**:  The default memory for Tomcat and Jetty is insufficient to run Apollo (and most other web apps).   
+You should [increase the memory according to these instructions](Troubleshooting.md#tomcat-memory).
+
+Other possible [build settings for JBrowse](http://gmod.org/wiki/JBrowse_Configuration_Guide) (based on an Ubuntu 16 install):
+
+     sudo apt-get update && sudo apt-get install zlib1g-dev libpng-dev libgd2-noxpm-dev build-essential git python-software-properties 
+     curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
+     sudo apt-get install nodejs 
+     
+NOTE: npm (installed with nodejs) must be version 6 or better.  If not installed from the above instructions, most [stable versions of node.js](https://nodejs.org/en/download/package-manager/) will supply this.  [nvm](https://github.com/creationix/nvm) may also be useful.
+
+NOTE: you must link nodejs to to node if your system installs it as a ```nodejs``` binary instead of a node one.  E.g., 
+
+    sudo ln -s /usr/bin/nodejs /usr/bin/node
+     
+Build settings for Apollo specifically.  Recent versions of tomcat7 will work, though tomcat8 is preferred.  If it does not install automatically there are a number of ways to [build tomcat on linux](https://www.digitalocean.com/community/tutorials/how-to-install-java-with-apt-get-on-ubuntu-16-04):
+     
+    sudo apt-get install ant openjdk-8-jdk 
+    export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/  # or set in .bashrc / .project
+
+Download Apollo from the [latest release](https://github.com/GMOD/Apollo/releases/latest/) under source-code and unzip.  Test installation by running ```./apollo run-local``` and see that the web-server starts up on http://localhost:8080/apollo/.  To setup for production continue onto configuration below after install . 
+
+If you get an ```Unsupported major.minor error``` or similar, please confirm that the version of java that tomcat is running ```ps -ef | grep java``` is the same as the one you used to build.  Setting JAVA_HOME to the Java 8 JDK should fix most problems.
+
+
+#### JSON in the URL with newer versions of Tomcat
+
+When JSON is added to the URL string (e.g., `addStores` and `addTracks`) you may get this error with newer patched versions of Tomcat 7.0.73, 8.0.39, 8.5.7:
+
+     java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
+
+To fix these, the best solution we've come up with (and there may be many) is to explicitly allow these characters, which you can do starting with Tomcat versions: 7.0.76, 8.0.42, 8.5.12.
+This is done by adding the following line to `$CATALINA_HOME/conf/catalina.properties`:
+
+    tomcat.util.http.parser.HttpParser.requestTargetAllow=|{}
+
+### Database configuration
+
+Apollo supports several database backends, and you can choose sample configurations from using H2, Postgres, or
+MySQL by default.
+
+Each has a file called `sample-h2-apollo-config.groovy` or `sample-postgres-apollo-config.groovy` that is designed to be
+renamed to apollo-config.groovy before running `apollo deploy`. Additionally there is a
+`sample-docker-apollo-config.groovy` which allows control of the configuration via environment variables.
+
+Furthermore, the `apollo-config.groovy` has different groovy environments for test, development, and production modes.
+The environment will be selected automatically selected depending on how it is run, e.g:
+
+* `apollo deploy` or `apollo release` use the production environment (i.e. when you copy the war file to your production
+server `apollo run-local` or `apollo debug` use the development environment (i.e. when you are running it locally)
+* `apollo test` uses the test environment (i.e. only when running unit tests)
+
+
+
+#### Configure for H2:
+- H2 is an embedded database engine, so no external setups are needed. Simply copy sample-h2-apollo-config.groovy to
+  apollo-config.groovy.
+    - The default dev environment (`apollo run-local` or `apollo run-app`) is in memory so you will have to change that to file.
+- If you use H2 with tomcat or jetty in production you have to set the permissions for the file path in production correctly (e.g. `jdbc:h2:/mypath/prodDb`, `chown -u tomcat:tomcat /mypath/prodDb.*.db`).
+    - If you use the local relative path `jdbc:h2:./prodDb` and tomcat8 the path will likely be: `/usr/share/tomcat8/prodDb*db`
+
+#### Configure for PostgreSQL:
+- Create a new database with postgres and add a user for production mode.  Here are [a few ways to do this in PostgreSQL](PostgreSQLSetup.md).
+- Copy the sample-postgres-apollo-config.groovy to apollo-config.groovy. 
+
+
+
+#### Configure for MySQL:
+- Create a new MySQL database for production mode (i.e. run ``create database `apollo-production``` in the mysql
+  console) and copy the sample-postgres-apollo-config.groovy to apollo-config.groovy.
+
+
+#### Configure for Docker:
+- Set up and export all of the environment variables you wish to configure. At bare minimum you will likely wish to set
+  `WEBAPOLLO_DB_USERNAME`, `WEBAPOLLO_DB_PASSWORD`, `WEBAPOLLO_DB_DRIVER`, `WEBAPOLLO_DB_DIALECT`, and
+`WEBAPOLLO_DB_URI`
+- Create a new database in your chosen database backend and copy the sample-docker-apollo-config.groovy to
+  apollo-config.groovy.
+- [Instructions and a script for launching docker with apollo and PostgreSQL](https://github.com/GMOD/docker-apollo).
+
+
+#### Apollo in Galaxy
+Apollo can always be used externally from Galaxy, but there are a few integrations available as well.
+
+- [Using Docker compose](https://github.com/GMOD/docker-compose-galaxy-annotation).
+- [From the Test Toolshed](https://testtoolshed.g2.bx.psu.edu/view/eric-rasche/apollo/df7a90763b3c).
+- [Using the Galaxy Genome Annotation Toolsuite](https://github.com/galaxy-genome-annotation/docker-galaxy-genome-annotation).
+
+### Database schema
+
+After you startup the application, the database schema (tables, etc.) is automatically setup. You don't have to
+initialize any database schemas yourself.
+
+## Deploy the application
+
+The `apollo run-local` command only launches a temporary server and should really not be used in production, so to
+deploy to production, we build a new WAR file with the `apollo deploy` command. After you have setup your
+`apollo-config.groovy` file, and it has the appropriate username, password, and JDBC URL in it, then we can run the
+command:
+
+``` 
+./apollo deploy
+```
+
+
+This command will package the application and it will download any missing pre-requisites (jbrowse) into a WAR file in
+the "target/" subfolder. After it completes, you can then copy the WAR file (e.g. ```apollo-2.0.4.war```) from the target folder
+to the ```web-app``` folder of your [web container](https://en.wikipedia.org/wiki/Web_container#open_source_Web_containers) installation.
+If you name the file ```apollo.war``` in your webapps folder, then you can access your app at "http://localhost:8080/apollo"
+
+We test primarily on [Apache Tomcat (7.0.62+ and 8)](http://tomcat.apache.org/).  **Make sure to [set your Tomcat memory](https://github.com/GMOD/Apollo/blob/master/docs/Troubleshooting.md#tomcat-memory) to an appropriate size or Apollo will run slow / crash.**
+
+
+
+Alternatively, as we alluded to previously, you can also launch a temporary instance of the server which is useful for
+testing
+
+``` 
+./apollo run-local 8085
+```
+
+This temporary server will be accessible at "http://localhost:8085/apollo"
+
+### Tomcat configuration
+
+If you have tracks that have deep nested features that will result in a feature JSON larger than 10MB or if you have a client
+ that sends requests to the Apollo server as JSON of size larger than 10MB then you will have to modify `src/war/templates/web.xml`.
+
+Specifically the following block in `web.xml`:
+```
+    <context-param>
+        <param-name>org.apache.tomcat.websocket.textBufferSize</param-name>
+        <param-value>10000000</param-value>
+    </context-param>
+    <context-param>
+        <param-name>org.apache.tomcat.websocket.binaryBufferSize</param-name>
+        <param-value>10000000</param-value>
+    </context-param>
+```
+
+Note: The `<param-value>` is in bytes.
+
+### Memory configuration
+
+Changing the memory used by Apollo in production must be [configured within Tomcat directly](Troubleshooting#tomcat-memory).
+
+The default memory assigned to Apollo to run commands in Apollo is 2048 MB. This can be changed in your
+`apollo-config.groovy` by uncommenting the memory configuration block:
+
+```
+// Uncomment to change the default memory configurations
+grails.project.fork = [
+        test   : false,
+        // configure settings for the run-app JVM
+        run    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+        // configure settings for the run-war JVM
+        war    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+        // configure settings for the Console UI JVM
+        console: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+]
+```
+
+### Note on database settings
+
+If you use the `apollo run-local` command, then the "development" section of the apollo-config.groovy is used (or an
+temporary in-memory H2 database is used if no apollo-config.groovy exists).
+
+If you use the WAR file generated by the `apollo deploy` command on your own webserver, then the "production" section of
+the apollo-config.groovy is used.
+
+## Detailed build instructions
+
+
+While the shortcut `apollo deploy` takes care of basic application deployment, understanding the full build process of
+Apollo can help you to optimize and improve your deployed instances.
+
+To learn more about the architecture of webapollo, view the [architecture guide](Architecture.md) but the main idea here
+is to learn how to use `apollo release` to construct a build that includes javascript minimization
+
+
+### Pre-requisites for Javascript minimization
+
+In addition to the system [pre-requisites](Prerequisites.md), the javascript compilation will use nodejs, which can be
+installed from a package manager on many platforms. Recommended setup for different platforms:
+
+
+``` 
+sudo apt-get install nodejs
+sudo yum install epel-release npm
+brew install node
+```
+
+#### Install extra perl modules
+
+Building apollo in release mode also requires some extra Perl modules, namely Text::Markdown and DateTime. One way to
+install them:
+
+``` 
+bin/cpanm -l extlib DateTime Text::Markdown
+```
+
+### Performing the javascript minimization
+
+To build a Apollo release with Javascript minimization, you can use the command
+
+``` 
+./apollo release
+```
+
+This will compile JBrowse and Apollo javascript code into minimized files so that the number of HTTP requests that the
+client needs to make are reduced.
+
+In all other respects, `apollo release` is exactly the same as `apollo deploy` though.
+
+
+### Performing active development
+
+To perform active development of the codebase, use
+
+``` 
+./apollo debug
+```
+
+This will launch a temporary instance of Apollo by running `grails run-app` and `ant devmode` at the same time,
+which means that any changes to the Java files will be picked up, allowing fast iteration.
+
+If you modify the javascript files (i.e. the client directory), you can run `scripts/copy_client.sh` and these will be
+picked up on-the-fly too.
+
+
diff --git a/docs/TestScript.md b/docs/TestScript.md
new file mode 100644
index 0000000..2fe5fb4
--- /dev/null
+++ b/docs/TestScript.md
@@ -0,0 +1,260 @@
+# Apollo Testing Script
+
+Note: The following steps are meant for testing purposes only, not for training manual annotators.
+
+## Apollo General Information
+- The Apollo website:
+http://GenomeArchitect.org
+- The article describing Apollo can be found at:  
+http://genomebiology.com/2013/14/8/R93/abstract
+- The public Apollo honey bee (_Apis mellifera_) demonstration site is available at: 
+http://genomearchitect.org/demo/
+- You may find our user guide at:
+http://genomearchitect.org/users-guide/
+- You may find a few slide presentations on the 'How Tos' of Apollo at:
+http://www.slideshare.net/MonicaMunozTorres/
+- Apollo at GMOD page: 
+http://www.gmod.org/wiki/WebApollo 
+- Apollo installation and configuration guide
+http://genomearchitect.readthedocs.io/en/latest/
+
+
+## Testing an Apollo Instance
+
+### A) Testing functions in the main window
+
+1) Switch between organisms:
+
+1.1) Check that you can change organisms from the upper left-corner drop-down menu. 
+
+1.2) Check that you can switch organisms from the _Organism_ tab in the _Annotator Panel_
+
+1.3) Check that the organism and sequence preference is preserved only on a single browser tab.  Every tab / window opened should allow a different sequence and organism.  The last organism + reference sequence (e.g. scaffold, chromosome) should be loaded into each, but each browser tab will be independent.
+
+1.3.1) The last "movement" of any browser for a particular organism should become the new preference (location + scaffold) for that organism.  
+
+1.3.2) The last "movement" should become the new "default" organism. 
+
+1.3.3) Switching back and forth between sequences should remember the last location for each. 
+
+1.3.4) Switching back and forth between organisms should remember the last location (scaffold and location) for each. 
+
+1.4) Switch between organisms after creating annotations on this reference sequence (e.g. scaffold, chromosome), if the two reference sequences have the same name in two (or more) different organisms.  
+
+1.4.1) Use the JBrowse drop-down or search menu to navigate between the two scaffolds on different organisms at least twice and confirm that annotations that belong to an organism on one scaffold are only seen on that organism and not the other.  
+
+1.4.2) Use the _Sequence Panel_ to confirm that scaffolds on a particular organism only belong to that organism.  i.e., every scaffold listed should be part of the current organism.
+
+1.4.3) Use the _Sequence Dropdown_ to confirm that scaffolds on a particular organism only belong to that organism.
+
+1.4.4) Use the _Annotator Panel_ "Go To Annotation" button to confirm that scaffolds listed for a particular organism and scaffold only belong to that organism.
+
+2) Test top-level menu options:
+
+2.1) Login / Logout
+   
+   Test that you are able to logout using the options on the upper right corner of the main window by clicking on your user ID and choosing to 'logout'. Then, test that you are able to log back in. 
+   
+   Test that all browsers log out for a set user. 
+   
+   When logged out you should still be able to view "public" organisms and browse public genomes from the link on the login screen.
+
+2.2) File 
+
+   /Open (Test that data can be loaded locally using URLs (File / Open / Remote URLs)).
+
+   /Add Combination Track: test that the arithmetic combination of quantitative tracks is possible by dragging two of them into a 'combination track'. Test different operations (addition, substraction) and arrangements (left and right positions for each track) as appropriate.
+   
+   /Add Sequence Search Track and perform a search test.
+
+2.3) View
+   
+   Check the ability to set and clear highlights, show plus/minus strands, show track label, resize quantitative tracks, color by CDS, and changing the color scheme (dark or light).
+
+2.4) Tools
+
+   From _Tools_ menu, query the genome with BLAT, using an amino acid or nucleotide sequence. For example: Housekeeping gene Calpain small subunit 1 CPNS1, CAPNS1, CAPN4, CAPNS (UniProt).
+
+   >sp|P04632|CPNS1_HUMAN Calpain small subunit 1 MFLVNSFLKGGGGGGGGGGGLGGGLGNVLGGLISGAGGGGGGGGGGGGGGGGGGGGTAMRILGGVISAISEAAAQYNPEPPPPRTHYSNIEANESEEVRQFRRLFAQLAGDDMEVSATELMNILNKVVTRHPDLKTDGFGIDTCRSMVAVMDSDTTGKLGFEEFKYLWNNIKRWQAIYKQFDTDRSGTICSSELPGAFEAAGFHLNEHLYNMIIRRYSDESGNMDFDNFISCLVRLDAMFRAFKSLDKDGTGQIQVNIQEWLQLTMYS
+   
+   Clear the highlighted region using the option from the _View_ menu.
+
+2.5) Help
+
+   Check that all links go to a new screen.
+
+   
+
+3) Test the Navigation Bar
+
+3.1) Search for an indexed gene (e.g. in honey bee demo CSN2_DANRE (it's on Group1.37:152689..155265)) by typing the gene name on the search box in the middle of the navigation bar in the main window. 
+
+4) Drag and drop a gene onto the “User-created Annotations” (U-cA) area.
+
+5) Zoom in (double click) to inspect last exon (5'-3') of the displayed gene and:
+
+5.1) Change intron/exon boundary (dragging)
+
+5.2) Check the recalculated ORF
+
+5.3) Color by CDS using the corresponding option from the _View_ top-level menu
+
+6) 'Zoom to Base Level' to reveal DNA Track and test sequence annotation alterations: 
+
+6.1) Insertions 
+
+6.2) Deletions 
+
+6.3) Substitutions
+
+7) 'Zoom back out', then reveal right-click menu. 
+
+7.1) Test: 
+
+7.1.1) Get Sequence, Get GFF3
+
+7.1.2) Delete, Merge, Split, Duplicate, Make Intron, Move to Opposite Strand.
+
+7.1.3) Set Translation Start, Set Translation End, Set Longest ORF, Set Readthrough Stop Codon.
+
+7.1.4) Set to Downstream Splice Donor, Set to Upstream Splice Donor, Set to Downstream Splice Acceptor, Set to Upstream Splice Acceptor.
+
+7.1.5) Check the _Undo_ and _Redo_ operations
+
+7.1.6) Show _History_ from the right click menu, and test the ability to revert to any of the previous versions of the feature by clicking on the arrow buttons to the right of each version.
+
+7.1.7) Annotation Information Editor: Name, Symbol, DBXRefs, Comments, Gene Ontology IDs, and PubMed IDs.
+
+7.1.8) Use both the genomic feature you are currently annotating and a genomic feature from one of the evidence tracks to modify the exon and UTR boundaries for the annotation in the _User-created Annotations_ area using the following operations from the right-click menu: _Set as 3' end_, _Set as 5' end_, _Set both ends_.
+
+7.1.9) Set exon boundary to create and remove an isoform, and use _History_ to conduct _Undo_ / _Redo_ operations on this isoform.
+
+7.1.10) Change the annotation type from the right-click menu and check _Undo_ / _Redo_ operations on this annotation.
+
+8) Check that the URL can be used for sharing work (on a different browser) for both logged in and logged out (JBrowse only) mode: bring up different browser window and paste the shared URL. Check real-time update by dragging and dropping another exon to the model on the left (same strand); check that “non-canonical boundaries” warning sign appears as appropriate. Last, delete an exon, Redo/Undo to test.  
+
+9) Check that you are able to export data from the _User-created Annotations_ track using the drop down menu option (from the track label) and choosing the 'Save track data' option. Here check both GFF3 (with and without FASTA) and FASTA files (CDS, cDNA, peptide, and highlighted region (note: you must first highlight a region to test this)).
+
+
+### B) Testing the _Annotator Panel_
+
+10) Check that you can switch between organisms using both the drop-down menu in the upper left corner of the _Annotator Panel_ and the options in the and the _Organism_ tab.
+
+11) Test that you are able to logout by clicking on the 'logout' arrow located in the upper right corner of the _Annotator Panel_. Then, test that you are able to log back in. 
+
+12) Test changing the user's password using the options available when clicking on the user ID button in the upper right corner of the _Annotator Panel_.
+
+13) Test functionality for each of the tabs in the panel
+
+13.1) Annotations
+
+13.1.1) Check that you can navigate to an annotation by clicking on them from the list in the panel. Annotations for gene elements that produce transcripts will require one click on the name of the annotation, then double clicking on the transcript.
+
+13.1.2) Navigate To Annotation Details
+
+13.1.2.1) After clicking on an annotation, click on the _Details_ tab at the bottom of the _Annotator Panel_ to display metadata for each annotation. Then, click on the _Coding_ tab to reveal a list of exons, and click on one of the exons to reveal options to modify its 
+boundary using the arrows in the display.  Modify a number explicitly and click outside the box.  Confirm that a change was made for a legitimate value.
+
+13.1.2.2) Repeat for pseudogenes and non-coding RNAs.
+
+13.1.2.3) Reveal the _Details_ for Repeat Region and Transposable Element to display metadate for each annotation.  
+
+13.1.3) Find an annotation using the _Annotation Name_ search box, and use the filters from the drop down menus. 
+
+13.2) Tracks
+
+13.2.1) Check the display of evidence available on all tracks by clicking to "check" and "uncheck" from the list of available tracks.
+
+13.2.2) Search for a track using the search box.
+
+13.2.3) Check that clicking on the show JBrowse tracks selector icons properly toggles the JBrowse tracks.
+
+13.2.3.1) Click on the track panel and confirm that selecting and unselecting the JBrowse track view and the main panel toggle icon.  
+
+13.2.3.2) Click on the main panel toggle button and confirm that doing and undoing the toggle switch toggles the JBrowse track view and switches the track panel toggle as well.
+
+13.2.3.3) Confirm that reload in either case saves the prior preference.
+
+13.2.3.4) Test as Admin and non-Admin for one case to confirm layout.
+
+13.2.3.5) Test a set of track categories can handle opening and closing, searching, and select / unselect all.
+
+13.3) Ref Sequence
+
+13.3.1) Use the search box to find a scaffold / chromosome and navigate to it by double clicking on one of them. 
+
+13.3.2) Test that you can export GFF3, FASTA, and CHADO files for one or more selected scaffolds at a time. 
+
+13.4) Organism
+
+13.4.1) Check that you can alter the metadata or source file for existing organisms, add new organisms, and delete current organisms. 
+
+13.4.2) Test that you can switch between organisms by double clicking on one of them. 
+
+13.5) Users 
+
+13.5.1) Create a new user and grant read, write, and publish permissions.
+
+13.5.2) Test altering information and permissions and group membership for users. 
+
+13.6) Groups
+
+13.6.1) Test that you can add and delete groups, as well as assign users to these groups. 
+
+13.7) Admin
+
+13.7.1) Click and corroborate that each item listed under the _Admin_ tab sends you to a new page. And test that you can add and delete fields and data as needed for each page. 
+
+13.8) Public link
+
+13.8.1) Confirm that clicking on a public link option in the main window when _logged in_ will forward you to the Annotator Panel view.
+
+13.8.2) Confirm that clicking on a public link option in the main window when _logged out_ will give you the regular "JBrowse view".
+
+   Check that this link opens up a new window showing only the browser and JBrowse track menu, without the annotator panel. 
+   When logging in, it should redirect to another login menu and then finally end up at the _Annotator Panel_ view with the same tracks selected.
+  
+13.9) Logged in link
+   
+13.9.1) Confirm that clicking on a logged in link option in the main window when _logged in_ will forward you to the _Annotator Panel_ view directly.
+
+13.9.2) Confirm that clicking on a logged in link option in the main window when _logged out_ will take you to the login screen and then redirect you to the proper _Annotator Panel_ view when approved with the same selections in-tact.
+
+### C) Testing Security Linking
+
+14) Test security redirect 
+
+14.1) Test redirect by clicking on a JBrowse (public URL) link and then clicking login.  
+
+14.2) Test through a proxy (nginx, apache, etc.)
+
+14.3) Test copy and pasting a logged in URL (goes to loadLink)
+
+14.4) When logged out and logging back in should come back to the same place.
+
+14.4.1) Should work even if on different browser.
+
+### D) Testing Web Services
+
+15.1) Make sure that you can add organisms using the ```add_organism.groovy``` script.  ```groovy add_organism.groovy -directory /apollo_data_directory/web_apollo_demo_yeast -genus bread -species yeast -name SampleOrganism -url http://testserver.gov/Apollo-staging -username adminuser at admin.gov -password demo -public```
+
+15.1.1) Verify that it also works for the shell (curl) script as well.
+
+15.2) Verify that you can use the ```add_users.groovy``` script. ```groovy add_users.groovy -newuser "SampleUser" -username adminuser at admin.gov -password adminpassword -destinationurl http://testserver.gov/Apollo-staging -newrole admin```
+ 
+15.2.1) Verify that it also works for the shell (curl) script as well.
+
+15.3) Make sure that you can add annotations using the ```add_features_from_gff3_to_annotations.pl``` script from exported annotations.
+
+15.3.1) Create annotations for new user on new organism.
+
+15.3.2) Download annotations as GFF3.
+
+15.3.3) Run ```add_features_from_gff3_to_annotations.pl``` against the same organism and user and confirm that it works. ```./add_features_from_gff3_to_annotations.pl -U http://testserver.gov/Apollo-staging -u adminuser at admin.gov -p adminpassword -i Annotations-chrI.gff3  --organism SampleOrganism```
+
+15.3.4) Run ```delete_annotations_from_organism.groovy``` and confirm that annotations from this organism have been removed.  `groovy delete_annotations_from_organism.groovy -adminusername adminuser at admin.gov -adminpassword adminpassword -destinationurl http://testserver.gov/Apollo-staging -organismname SampleOrganism`
+
+
+
+
+
diff --git a/docs/Testing_notes.md b/docs/Testing_notes.md
new file mode 100644
index 0000000..ca71b89
--- /dev/null
+++ b/docs/Testing_notes.md
@@ -0,0 +1,74 @@
+
+## Automated testing architecture
+
+The Apollo unit testing framework uses the grails testing guidelines extensively, which can be reviewed here:
+http://grails.github.io/grails-doc/2.4.3/guide/testing.html
+
+
+Our basic methodology is to run the full test suite with the apollo command:
+
+``` 
+apollo test
+```
+
+
+More specific tests can also be run for example by running specific commands for `grails test-app`
+
+``` 
+grails test-app :unit-test
+```
+
+This runs ALL of the tests in “test/unit”. If you want to test a specific function then write it something like this:
+
+``` 
+grails test-app org.bbop.apollo.FeatureService :unit
+```
+
+
+
+### Notes about the test suites:
+
+1. @Mock includes any domain objects you’ll use.  Unit tests don’t use the database.
+2. The setup() function is run for each test
+3. The test is composed of blocks of code with `when:` and `then:`. You have to have both or it is not a test. 
+
+
+Example test:
+
+``` 
+ at TestFor(FeatureService)
+ at Mock([Sequence,FeatureLocation,Feature])
+  class FeatureServiceSpec extends Specification {
+  void setup(){}
+  void "convert JSON to Feature Location"(){
+
+  when: "We have a valid json object"
+  JSONObject jsonObject = new JSONObject()
+  Sequence sequence = new Sequence(name: "Chr3",
+    seqChunkSize: 20, start:1, end:100, length:99).save(failOnError: true)
+  jsonObject.put(FeatureStringEnum.FMIN.value,73)
+  jsonObject.put(FeatureStringEnum.FMAX.value,113)
+  jsonObject.put(FeatureStringEnum.STRAND.value, Strand.POSITIVE.value)
+
+
+  then: "We should return a valid FeatureLocation"
+  FeatureLocation featureLocation = 
+    service.convertJSONToFeatureLocation(jsonObject,sequence)
+  assert featureLocation.sequence.name == "Chr3"
+  assert featureLocation.fmin == 73
+  assert featureLocation.fmax == 113
+  assert featureLocation.strand ==Strand.POSITIVE.value
+} }
+```
+
+There are 3 "special" types of things to test, which are all important and reflect the grails special functions:
+Domains, Controllers, Services.  They will all be in the “test” directory and all be suffixed with “Spec” for a Spock
+test.
+
+
+### Chado
+
+If you test with the [chado export](ChadoExport.md) you will need to make sure you load ontologies into your chado database or integration steps will fail.  If you don't specify chado in your apollo-config.groovy then no further action would be necessary.
+
+    ./scripts/load_chado_schema.sh -u nathandunn -d apollo-chado-test -s chado-schema-with-ontologies.sql.gz -r
+
diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md
new file mode 100644
index 0000000..0327267
--- /dev/null
+++ b/docs/Troubleshooting.md
@@ -0,0 +1,271 @@
+# Troubleshooting guide
+
+### Tomcat memory
+
+
+Typically, the default memory allowance for the Java Virtual Machine (JVM) is too low. The memory requirements for Web
+Apollo will depend on many variables, but in general, we recommend at least 1g for the heap size and 256m for the
+PermGen size as a starting point.
+
+#### Suggested Tomcat memory settings
+
+``` 
+export CATALINA_OPTS="-Xms512m -Xmx1g \
+        -XX:+CMSClassUnloadingEnabled \
+        -XX:+CMSPermGenSweepingEnabled \
+        -XX:+UseConcMarkSweepGC"
+```
+
+
+In cases where the assembled genome is highly fragmented, additional tuning of memory requirements and garbage
+collection will be necessary to maintain the system stable. Below is an example from a research group that maintains
+over 40 Apollo instances with assemblies that range from 1,000 to 150,000 scaffolds (reference sequences):  
+
+``` 
+export CATALINA_OPTS="-Xmx12288m -Xms8192m \
+        -XX:ReservedCodeCacheSize=64m \
+        -XX:+UseG1GC \
+        -XX:+CMSClassUnloadingEnabled \
+        -Xloggc:$CATALINA_HOME/logs/gc.log \
+        -XX:+PrintHeapAtGC \
+        -XX:+PrintGCDetails \
+        -XX:+PrintGCTimeStamps"
+```
+
+To change your settings, you can *usually* edit the setenv.sh script in `$TOMCAT_BIN_DIR/setenv.sh` where
+`$TOMCAT_BIN_DIR` is  the directory where the Tomcat binaries reside.  It is possible that this file doesn't exist by default, but it will be picked up when Tomcat restarts.  Make sure that tomcat can read the file.  
+
+In most cases, creating the setenv.sh should be sufficient but you may have to edit a catalina.sh or another file directly depending on your system and tomcat setup.  For example, on Ubuntu, the file /etc/default/tomcat7 often contains these settings. 
+
+#### Confirm your settings
+
+Your CATALINA_OPTS settings from setenv.sh can be confirmed with a tool like jvisualvm or via the command line with the
+`ps` tool.  e.g. `ps -ef | grep java`  should yield something like the following allowing you to confirm that your memory settings have been picked up.
+
+```
+root      9848     1  0 Oct22 ?        00:36:44 /usr/lib/jvm/java-7-openjdk-amd64/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/current/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1g -Xmx2g -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled -XX:+UseConcMarkSweepGC -Dj
+```
+
+#### Re-install after changing settings
+
+If you start seeing memory leaks (`java.lang.OutOfMemoryError: Java heap space`) after doing an update, you might try
+re-installing, as the live re-deploy itself can cause memory leaks or an inconsistent software state. 
+
+If you have named your web application named `Apollo.war` then you can remove all of these files from your webapps
+directory and re-deploy.
+
+- Run `apollo deploy`  (or `apollo release` for javascript-minimization)
+- Undeploy any existing Apollo instances
+- Stop tomcat
+- Copy the war file to the webapps folder
+- Start tomcat 
+
+### Tomcat permissions
+
+Preferably, when running Apollo or any webserver, you should not run Tomcat as root. Therefore, when deploying your
+war file to tomcat or another web application server, you may need to tune your file permissions to make sure Tomcat is
+able to access your files.
+
+On many production systems, tomcat will typically belong to a user and group called something like 'tomcat'. Make sure
+that the 'tomcat' user can read your "webapps" directory (where you placed your war file) and write into the annotations
+and any other relevant directory (e.g. tomcat/logs).   As such, it is sometimes helpful to add the user you logged-in as
+to the same group as your tomcat user and set group write permissions for both.
+
+Consider using a package manager to install Tomcat so that proper security settings are installed, or to use the jsvc
+http://tomcat.apache.org/tomcat-7.0-doc/security-howto.html#Non-Tomcat_settings
+
+
+### Errors with JBrowse
+
+#### JBrowse tools don't show up in ```bin``` directory (or install at all) after install or typing ```install_jbrowse.sh```
+
+If the ```bin``` directory with JBrowse tools doesn't show up after calling ```install_jbrowse.sh``` JBrowse is having trouble installing itself for a few possible reasons.   If these do not work, please observe the [JBrowse troubleshooting](http://jbrowse.org/code/JBrowse-1.12.1/docs/tutorial/#Troubleshooting) and [JBrowse install](https://jbrowse.org/install/) pages, as well and the ```setup.log``` file created during the installation process. 
+
+##### cpanm or other components are not installed
+
+Make sure the [appropriate JBrowse libraries](http://gmod.org/wiki/JBrowse_Configuration_Guide#Making_a_New_JBrowse) are installed on your system.
+
+If you see ```chmod: cannot access `web-app/jbrowse/bin/cpanm': No such file or directory ``` make sure to install [cpanm](http://search.cpan.org/~miyagawa/App-cpanminus-1.7040/lib/App/cpanminus.pm).
+
+##### Git tool is too old
+
+Git expects to clone a single branch which is supported in git 1.7.10 and greater.  The output when that fails looks something like this:
+
+```
+Buildfile: build.xml
+
+copy.apollo.plugin.webapp:
+
+setup-jbrowse:
+
+git.clone:
+[exec] Result: 129
+```
+
+The solution is to upgrade git to 1.7.10 or greater or remove the line with the ```--single-branch``` option in ```build.xml```. 
+
+##### Accessing git behind a firewall. 
+
+If you are behind a firewall, checking out code using the ```git://``` protocol may not be allowed, but that is the default.    The output will look something like this:
+
+```
+setup-jbrowse:
+
+git.clone:
+     [exec] Submodule 'src/FileSaver' (git://github.com/dkasenberg/FileSaver.js.git) registered for path 'src/FileSaver'
+     [exec] Submodule 'src/dbind' (git://github.com/rbuels/dbind.git) registered for path 'src/dbind'
+    . . . .
+     [exec] Submodule 'src/xstyle' (git://github.com/kriszyp/xstyle.git) registered for path 'src/xstyle'
+     [exec] Result: 1
+```
+
+with possibly more output below. 
+
+Type:
+
+```git config --global url."https://".insteadOf git://``` 
+
+in the command-line and then re-install using ```./apollo clean-all``` ```./apollo run-local``` (or deploy or release).
+
+
+
+#### e.g. "Can't locate Hash/Merge.pm in @INC" or "Can't locate JBlibs.pm in @INC"
+
+If you are trying to run the jbrowse binaries but get these sorts of errors, try running `install_jbrowse.sh` which will
+initialize as many pre-requisites as possible including JBLibs and other JBrowse dependencies. 
+
+#### Rebuilding JBrowse
+
+You can manually clear jbrowse files from web-app/jbrowse and re-run `apollo deploy` to rebuild JBrowse.
+
+#### RequestError: Unable to load ... Apollo2/jbrowse/data/trackList.json status: 500
+
+Apollo2 does fairly strict JSON validation so make sure your trackList.json file is valid JSON
+
+If you still get this error after validating please forward the issue to our github issue tracker.
+
+
+### Complaints about 8080 being in use
+
+Please check that you don't already have a tomcat running `netstat -tan | grep 8080`. Sometimes tomcat does not exit
+properly.  `ps -ef | grep java` and then `kill -9` the offending processing.
+
+Note that you can also configure tomcat to run on different ports, or you can launch a temporary instance of apollo with
+`apollo run-local 8085` for example to avoid the port conflict.
+
+### Unable to open the h2 / default database for writing
+
+If you receive an error similar to this:
+
+``` 
+SEVERE: Unable to create initial connections of pool.
+org.h2.jdbc.JdbcSQLException: Error opening database: 
+    "Could not save properties /var/lib/tomcat7/prodDb.lock.db" [8000-176]
+```
+
+Then this is due to the production server trying to write an h2 instance in an area it doesn't have permissions to.  If
+you use H2 (which is great for testing or single-user user, but not for full-blown production) make sure that:
+
+You can modify the specified data directory for the H2 database in the apollo-config.groovy. For example, using the
+/tmp/ directory, or some other directory:
+
+``` 
+url = "jdbc:h2:/tmp/prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+```
+
+This will write a H2 db file to `/tmp/prodDB.db`.  If you don't specify an absolute path it will try to write in the
+same directory that tomcat is running in e.g., `/var/lib/tomcat7/` which can have permission issues.
+
+
+More detail on database configuration when specifying the `apollo-config.groovy` file is available in the
+[setup guide](Setup.md).
+
+
+### Grails cache errors
+
+In some instances you can't write to the default cache location on disk.  Part of an example config log:
+
+``` 
+2015-07-03 14:37:39,675 [main] ERROR context.GrailsContextLoaderListener  - Error initializing the application: null
+java.lang.NullPointerException
+        at grails.plugin.cache.ehcache.GrailsEhCacheManagerFactoryBean$ReloadableCacheManager.rebuild(GrailsEhCacheManagerFactoryBean.java:171)
+        at grails.plugin.cache.ehcache.EhcacheConfigLoader.reload(EhcacheConfigLoader.groovy:63)
+        at grails.plugin.cache.ConfigLoader.reload(ConfigLoader.groovy:42)
+```
+
+There are several solutions to this, but all involve updating the `apollo-config.groovy` file to override the caching 
+defined in the [Config.groovy](https://github.com/GMOD/Apollo/blob/master/grails-app/conf/Config.groovy#L103).
+
+#### Disabling the cache:
+
+``` 
+    grails.cache.config = {
+        cache {
+            enabled = false
+            name 'globalcache'
+        }
+    }
+```
+
+This can also be done by removing the plugin.  In [```grails-app/conf/BuildConfig```](https://github.com/GMOD/Apollo/blob/master/grails-app/conf/BuildConfig.groovy) remove / comment out the line and re-building:
+
+     compile ':cache-ehcache:1.0.5'
+
+
+#### Disallow writing overflow to disk
+
+Can be used for small instances
+
+``` 
+    grails.cache.config = {
+        // avoid ehcache naming conflict to run multiple WA instances
+        provider {
+            name "ehcache-apollo-"+(new Date().format("yyyyMMddHHmmss"))
+        }
+        cache {
+            enabled = true
+            name 'globalcache'
+            eternal false
+            overflowToDisk false   // THIS IS THE IMPORTANT LINE
+            maxElementsInMemory 100000
+        }
+    }
+```
+
+#### Specify the overflow directory
+
+Best for high load servers, which will need the cache.  Make sure your tomcat /
+web-server user can write to that directory:
+
+``` 
+    // copy from Config.groovy except where noted
+    grails.cache.config = {
+    ... 
+        cache {
+        ...  
+            maxElementsOnDisk 10000000
+            // this is the important part, below!
+            diskStore{
+                path '/opt/apollo/cache-directory'
+            }
+        }
+        ...
+    }
+```
+
+
+Information on the [grails ehcache plugin](http://grails-plugins.github.io/grails-cache-ehcache/guide/usage.html) (see
+"Overriding values") and [ehcache itself](http://ehcache.org/documentation/2.8/integrations/grails).
+
+### Mysql invalid TimeStamp error
+
+For certain version of MySQL we might get errors of this nature:
+
+> SQLException occurred when processing request: [GET] /apollo/annotator/getAppState
+Value '0000-00-00 00:00:00' can not be represented as java.sql.Timestamp. Stacktrace follows:
+java.sql.SQLException: Value '0000-00-00 00:00:00' can not be represented as java.sql.Timestamp
+
+The fix is to set the ```zeroDateTimeBehavior=convertToNull``` to the url connect screen.  Originally [identified here](https://github.com/GMOD/Apollo/issues/1170).  Here is an example URL:
+
+
+    jdbc:mysql://localhost/apollo_production?zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=UTF-8&characterSetResults=UTF-8
diff --git a/docs/Web_services.md b/docs/Web_services.md
new file mode 100644
index 0000000..a53a591
--- /dev/null
+++ b/docs/Web_services.md
@@ -0,0 +1,137 @@
+# Web Service API
+
+
+The Apollo Web Service API is a JSON-based REST API to interact with the annotations and other services of Apollo.
+Both the request and response JSON objects can contain feature information that are based on the Chado schema.  We use 
+the web services API [scripting examples](https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/)
+and we also use them in the Apollo JBrowse plugin.
+
+
+The most up to date Web Service API documentation is deployed from the source code rest-api-doc annotations. 
+
+See [http://demo.genomearchitect.org/Apollo2/jbrowse/web_services/api](http://demo.genomearchitect.org/Apollo2/jbrowse/web_services/api/) for details
+
+### Warning
+
+If you are sending password you care about over the wire (even if not using web services) it is *highly recommended*
+that you use https (which adds encryption ssl) instead of http.
+
+
+### Examples
+
+We provide an [examples directory](docs/web_services/examples/).
+
+``` 
+curl -b cookies.txt -c cookies.txt -e "http://localhost:8080" \
+    -H "Content-Type:application/json" \
+    -d "{'username': 'demo', 'password': 'demo'}" \
+    "http://localhost:8080/apollo/Login?operation=login"
+```
+
+
+Login expects two parameters: <code>username</code> and <code>password</code>, and optionally rememberMe for a
+persistent cookie.
+
+A successful login returns a empty JSON object
+
+### Python Client
+
+A [python client](https://github.com/galaxy-genome-annotation/python-apollo) has been provided over many of the
+Apollo web services, which is easy to setup:
+
+```
+pip install apollo
+arrow init # provide Apollo credentials
+arrow -h
+## have fun
+arrow groups get_groups
+```
+
+[Documentation on commands](http://python-apollo.readthedocs.io/en/latest/commands.html) and [some examples](http://python-apollo.readthedocs.io/en/latest/arrow.html) working with [jq](https://stedolan.github.io/jq/tutorial/): 
+
+
+## What is the Web Service API?
+
+For a given Apollo server url (e.g., `https://localhost:8080/apollo` or any other Apollo site on the web), the
+Web Service API allows us to make requests to the various "controllers" of the application and perform operations.
+
+The controllers that are available for Apollo include the AnnotationEditorController, the OrganismController, the
+IOServiceController for downloads of data, and the UserController for user management.
+
+
+Most API requests will take:
+
+- The proper url (e.g., to get features from the AnnotationEditorController, we can send requests to
+  (e.g `http://localhost/apollo/annotationEditor/getFeatures`)
+- username - an authorized user (also uses session if none specified)
+- password - password (also uses session if none specified)
+- organism - (if applicable) the "common name" of the organism for the operation -- will also pull from the "user
+  preferences" if none is specified.
+- track/sequence - (if applicable) reference sequence name (shown in sequence panel / genomic
+  browse)
+- uniquename - (if applicable) the uniquename is a [UUID](https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html)
+ used to guarantee a unique ID
+
+### Errors If an error has occurred, a proper HTTP error code (most likely 400 or 500) and an error message.  is
+returned, in JSON format:
+
+``` 
+{ "error": "error message" }
+```
+
+
+### Cookies
+
+The Apollo Login creates a JSESSIONID cookie and rememberMe cookie (if applicable) and these can be used in
+downstream API requests (for example, by setting -b cookies.txt in curl will preserve the cookie in the request).
+
+You can also pass username/password to individual API requests and these will authenticate each individual request. 
+
+
+
+### Representing features in JSON
+
+Most requests and responses will contain an array of `feature` JSON objects named `features`.  The `feature` object is
+based on the Chado `feature`, `featureloc`, `cv`, and `cvterm` tables.
+
+``` 
+{
+    "residues": "$residues",
+    "type": {
+        "cv": {
+            "name": "$cv_name"
+        },
+        "name": "$cv_term"
+    },
+    "location": {
+        "fmax": $rightmost_intrabase_coordinate_of_feature,
+        "fmin": $leftmost_intrabase_coordinate_of_feature,
+        "strand": $strand
+    },
+    "uniquename": "$feature_unique_name"
+    "children": [$array_of_child_features]
+    "properties": [$array_of_properties]
+}
+```
+where:
+
+* `residues` - A sequence of alphabetic characters representing biological residues (nucleic acids, amino acids)
+ [string]
+* `type.cv.name` - The name of the ontology [string] `type.name` - The name for the cvterm [string]
+* `location.fmax` - The rightmost/maximal intrabase boundary in the linear range [integer]
+* `location.fmin` - The leftmost/minimal intrabase boundary in the linear range [integer]
+*  `strand` - The orientation/directionality of the location. Should be 0, -1 or +1 [integer]
+* `uniquename` - The unique name for a feature [string]
+* `children` - Array of child feature objects [array]
+* `properties` - Array of properties (including frameshifts for transcripts) [array]
+
+Note that different operations will require different fields to be set (which will be elaborated upon in each operation
+section).
+
+
+## Web Services API
+
+The most up to date Web Service API documentation is deployed from the source code rest-api-doc annotations
+
+
+See [http://demo.genomearchitect.org/Apollo2/jbrowse/web_services/api](http://demo.genomearchitect.org/Apollo2/jbrowse/web_services/api) for details
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..c50ff38
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-merlot
\ No newline at end of file
diff --git a/docs/apollo-stress-tests/suite1/README.docx b/docs/apollo-stress-tests/suite1/README.docx
new file mode 100644
index 0000000..92cda7a
Binary files /dev/null and b/docs/apollo-stress-tests/suite1/README.docx differ
diff --git a/docs/apollo-stress-tests/suite1/README.pdf b/docs/apollo-stress-tests/suite1/README.pdf
new file mode 100644
index 0000000..b402a68
Binary files /dev/null and b/docs/apollo-stress-tests/suite1/README.pdf differ
diff --git a/docs/apollo-stress-tests/suite1/apollo-stress.jmx b/docs/apollo-stress-tests/suite1/apollo-stress.jmx
new file mode 100644
index 0000000..b48c28b
--- /dev/null
+++ b/docs/apollo-stress-tests/suite1/apollo-stress.jmx
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.6" jmeter="2.11 r1554548">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">${__P(server, apollo-stage.nal.usda.gov)}</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">${__P(loop, 1)}</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">${__P(users, 1)}</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">${__P(users, 1)}</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': '${username}', 'password': '${password}'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${__P(organism, blager)}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">3</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add feature" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">${operation}</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${__P(organism, blager)}/AnnotationEditorService</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id 1" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">${matchno}</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${__P(organism, blager)}/AnnotationEditorService</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+          <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="CSV Data Set Config" enabled="true">
+            <stringProp name="filename">testdata/${__P(organism, blager)}/operations.csv</stringProp>
+            <stringProp name="fileEncoding"></stringProp>
+            <stringProp name="variableNames">track,operation,matchno</stringProp>
+            <stringProp name="delimiter">\t</stringProp>
+            <boolProp name="quotedData">false</boolProp>
+            <boolProp name="recycle">true</boolProp>
+            <boolProp name="stopThread">false</boolProp>
+            <stringProp name="shareMode">shareMode.thread</stringProp>
+          </CSVDataSet>
+          <hashTree/>
+          <UniformRandomTimer guiclass="UniformRandomTimerGui" testclass="UniformRandomTimer" testname="Uniform Random Timer" enabled="true">
+            <stringProp name="ConstantTimer.delay">0</stringProp>
+            <stringProp name="RandomTimer.range">3000.0</stringProp>
+          </UniformRandomTimer>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value"></stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${__P(organism, blager)}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="View Results in Table" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true">
+          <stringProp name="ConstantTimer.delay">1000</stringProp>
+        </ConstantTimer>
+        <hashTree/>
+        <CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="CSV Data Set Config" enabled="true">
+          <stringProp name="filename">testdata/${__P(organism, blager)}/usernames.csv</stringProp>
+          <stringProp name="fileEncoding"></stringProp>
+          <stringProp name="variableNames">username,password</stringProp>
+          <stringProp name="delimiter">,</stringProp>
+          <boolProp name="quotedData">false</boolProp>
+          <boolProp name="recycle">true</boolProp>
+          <boolProp name="stopThread">false</boolProp>
+          <stringProp name="shareMode">shareMode.group</stringProp>
+        </CSVDataSet>
+        <hashTree/>
+      </hashTree>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite1/testdata/cercap/operations.csv b/docs/apollo-stress-tests/suite1/testdata/cercap/operations.csv
new file mode 100644
index 0000000..f9043b7
--- /dev/null
+++ b/docs/apollo-stress-tests/suite1/testdata/cercap/operations.csv
@@ -0,0 +1,3 @@
+NW_004522772.1	{ "track": "NW_004522772.1", "features": [{"location":{"fmin":2960,"fmax":3244,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"108609_t","children":[{"location":{"fmin":2960,"fmax":2967,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":3227,"fmax":3244,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":2960,"fmax":3244,"strand":1},"type":{"cv":{"name":"sequence"},"name":"CDS"}}]}], "operation" [...]
+NW_004523814.1	{ "track": "NW_004523814.1", "features": [{"location":{"fmin":846128,"fmax":856388,"strand":-1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"NW_004523814.1.g30.t1","children":[{"location":{"fmin":856086,"fmax":856388,"strand":-1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":855850,"fmax":856388,"strand":-1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":854860,"fmax":854929,"strand":-1},"type":{"cv":{"name":"sequence [...]
+NW_004523690.1	{ "track": "NW_004523690.1", "features": [{"location":{"fmin":169785,"fmax":182097,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"SNAP pred NW_004523690.1-snap.36","children":[{"location":{"fmin":169785,"fmax":169818,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":181941,"fmax":182097,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":169785,"fmax":182097,"strand":1},"type":{"cv":{"name":"s [...]
diff --git a/docs/apollo-stress-tests/suite1/testdata/cercap/usernames.csv b/docs/apollo-stress-tests/suite1/testdata/cercap/usernames.csv
new file mode 100644
index 0000000..d8bc9ae
--- /dev/null
+++ b/docs/apollo-stress-tests/suite1/testdata/cercap/usernames.csv
@@ -0,0 +1 @@
+username,password
\ No newline at end of file
diff --git a/docs/apollo-stress-tests/suite2/example-users-for-stress-test.csv b/docs/apollo-stress-tests/suite2/example-users-for-stress-test.csv
new file mode 100644
index 0000000..b27fb67
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/example-users-for-stress-test.csv
@@ -0,0 +1,17 @@
+user.one at example.com,User,One,userone,user
+user.two at example.com,User,Two,usertwo,user
+user.three at example.com,User,Three,userthree,user
+user.four at example.com,User,Four,userfour,user
+user.five at example.com,User,Five,userfive,user
+user.six at example.com,User,Six,usersix,user
+user.seven at example.com,User,Seven,userseven,user
+user.eight at example.com,User,Eight,usereight,user
+user.nine at example.com,User,Nine,usernine,user
+user.ten at example.com,User,Ten,userten,user
+user.eleven at example.com,User,Eleven,usereleven,user
+user.twelve at example.com,User,Twelve,usertwelve,user
+user.thirteen at example.com,User,Thirteen,userthirteen,user
+user.fourteen at example.com,User,Fourteen,userfourteen,user
+user.fifteen at example.com,User,Fifteen,userfifteen,user
+user.sixteen at example.com,User,Sixteen,usersixteen,user
+user.seventeen at example.com,User,Seventeen,userseventeen,user
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-blast-test-4.jmx b/docs/apollo-stress-tests/suite2/jmeter-dev-blast-test-4.jmx
new file mode 100644
index 0000000..ce80ca1
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-blast-test-4.jmx
@@ -0,0 +1,3208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.8" jmeter="2.13 r1665067">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">localhost</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="instance_name" elementType="Argument">
+            <stringProp name="Argument.name">instance_name</stringProp>
+            <stringProp name="Argument.value">apollo</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="get_loop_count" elementType="Argument">
+            <stringProp name="Argument.name">get_loop_count</stringProp>
+            <stringProp name="Argument.value">5</stringProp>
+            <stringProp name="Argument.desc">number of times to get features inbetween adding</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="loops_per_count" elementType="Argument">
+            <stringProp name="Argument.name">loops_per_count</stringProp>
+            <stringProp name="Argument.value">5</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="throughput_per_minute" elementType="Argument">
+            <stringProp name="Argument.name">throughput_per_minute</stringProp>
+            <stringProp name="Argument.value">2</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 1" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.one at example.com', 'password': 'userone'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.one at example.com","features":[{"location":{"fmin":898823,"strand":-1,"fmax":927500},"name":"GB42150-RA","children":[{"location":{"fmin":898823,"strand":-1,"fmax":901952},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L1" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 2" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.two at example.com', 'password': 'usertwo'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.two at example.com","features":[{"location":{"fmin":874076,"strand":-1,"fmax":874361},"name":"GB40738-RA","children":[{"location":{"fmin":874076,"strand":-1,"fmax":874091},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L2" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 3" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.three at example.com', 'password': 'userthree'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.three at example.com","features":[{"location":{"fmin":418378,"strand":1,"fmax":516127},"name":"GB43015-RA","children":[{"location":{"fmin":512448,"strand":1,"fmax":516127},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L3" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 4" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.four at example.com', 'password': 'userfour'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.four at example.com","features":[{"location":{"fmin":383802,"strand":-1,"fmax":501601},"name":"GB42156-RA","children":[{"location":{"fmin":383802,"strand":-1,"fmax":386863},"type":{"name":"exon","cv&qu [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L4" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 5" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.five at example.com', 'password': 'userfive'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.five at example.com","features":[{"location":{"fmin":976735,"strand":1,"fmax":995721},"name":"GB42183-RA","children":[{"location":{"fmin":995216,"strand":1,"fmax":995721},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L5" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 6" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.six at example.com', 'password': 'usersix'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.six at example.com","features":[{"location":{"fmin":238521,"strand":1,"fmax":241316},"name":"GB40030-RA","children":[{"location":{"fmin":238521,"strand":1,"fmax":238636},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L6" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 7" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.seven at example.com', 'password': 'userseven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.seven at example.com","features":[{"location":{"fmin":386401,"strand":-1,"fmax":426902},"name":"GB40010-RA","children":[{"location":{"fmin":386401,"strand":-1,"fmax":386621},"type":{"name":"exon","cv&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L7" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 8" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eight at example.com', 'password': 'usereight'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eight at example.com","features":[{"location":{"fmin":223558,"strand":1,"fmax":238576},"name":"GB40808-RA","children":[{"location":{"fmin":223558,"strand":1,"fmax":223806},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L8" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 9" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.nine at example.com', 'password': 'usernine'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.nine at example.com","features":[{"location":{"fmin":107997,"strand":1,"fmax":120164},"name":"GB48942-RA","children":[{"location":{"fmin":107997,"strand":1,"fmax":108043},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L9" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 10" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.ten at example.com', 'password': 'userten'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.ten at example.com","features":[{"location":{"fmin":300388,"strand":1,"fmax":328828},"name":"GB42167-RA","children":[{"location":{"fmin":325348,"strand":1,"fmax":328828},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L10" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 11" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eleven at example.com', 'password': 'usereleven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eleven at example.com","features":[{"location":{"fmin":355088,"strand":-1,"fmax":356284},"name":"GB42157-RA","children":[{"location":{"fmin":355088,"strand":-1,"fmax":355214},"type":{"name":"exon","cv& [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L11" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 12" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.twelve at example.com', 'password': 'usertwelve'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.twelve at example.com","features":[{"location":{"fmin":66900638,"strand":1,"fmax":66956304},"name":"NM_001046507.2","children":[{"location":{"fmin":66900638,"strand":1,"fmax":66900789},"type":{"name":"exon" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L12" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 13" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.thirteen at example.com', 'password': 'userthirteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.thirteen at example.com","features":[{"location":{"fmin":117545733,"strand":-1,"fmax":117953888},"name":"XM_010801489.1","children":[{"location":{"fmin":117953802,"strand":-1,"fmax":117953888},"type":{"name":"ex [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L13" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 14" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fourteen at example.com', 'password': 'userfourteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fourteen at example.com","features":[{"location":{"fmin":64486958,"strand":-1,"fmax":64750899},"name":"NM_001205725.1","children":[{"location":{"fmin":64749947,"strand":-1,"fmax":64750899},"type":{"name":"exon&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L14" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 15" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fifteen at example.com', 'password': 'userfifteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fifteen at example.com","features":[{"location":{"fmin":36284953,"strand":1,"fmax":36432436},"name":"XM_005207756.2","children":[{"location":{"fmin":36284953,"strand":1,"fmax":36285264},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L15" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 16" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.sixteen at example.com', 'password': 'usersixteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.sixteen at example.com","features":[{"location":{"fmin":87287872,"strand":1,"fmax":87932060},"name":"XM_010821869.1","children":[{"location":{"fmin":87287872,"strand":1,"fmax":87288538},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L16" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>false</assertions>
+            <subresults>false</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>true</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <url>true</url>
+            <threadCounts>true</threadCounts>
+            <sampleCount>true</sampleCount>
+          </value>
+        </objProp>
+        <stringProp name="filename">/Users/nathandunn/repositories/Apollo/docs/apollo-stress-tests/suite2/results.csv</stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="StatGraphVisualizer" testclass="ResultCollector" testname="Aggregate Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="MonitorHealthVisualizer" testclass="ResultCollector" testname="Monitor Results" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="RespTimeGraphVisualizer" testclass="ResultCollector" testname="Response Time Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+        <intProp name="RespTimeGraph.linestrockwidth">1</intProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="DistributionGraphVisualizer" testclass="ResultCollector" testname="Distribution Graph (alpha)" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true">
+        <stringProp name="ConstantTimer.delay">300</stringProp>
+        <stringProp name="RandomTimer.range">100.0</stringProp>
+      </GaussianRandomTimer>
+      <hashTree/>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-1.jmx b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-1.jmx
new file mode 100644
index 0000000..376f353
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-1.jmx
@@ -0,0 +1,1900 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.8" jmeter="2.13 r1665067">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">localhost</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="instance_name" elementType="Argument">
+            <stringProp name="Argument.name">instance_name</stringProp>
+            <stringProp name="Argument.value">apollo</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 1" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.one at example.com', 'password': 'userone'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.one at example.com","features":[{"location":{"fmin":898823,"strand":-1,"fmax":927500},"name":"GB42150-RA","children":[{"location":{"fmin":898823,"strand":-1,"fmax":901952},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 2" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.two at example.com', 'password': 'usertwo'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.two at example.com","features":[{"location":{"fmin":874076,"strand":-1,"fmax":874361},"name":"GB40738-RA","children":[{"location":{"fmin":874076,"strand":-1,"fmax":874091},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 3" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.three at example.com', 'password': 'userthree'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.three at example.com","features":[{"location":{"fmin":418378,"strand":1,"fmax":516127},"name":"GB43015-RA","children":[{"location":{"fmin":512448,"strand":1,"fmax":516127},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 4" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.four at example.com', 'password': 'userfour'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.four at example.com","features":[{"location":{"fmin":383802,"strand":-1,"fmax":501601},"name":"GB42156-RA","children":[{"location":{"fmin":383802,"strand":-1,"fmax":386863},"type":{"name":"exon","cv&qu [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 5" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.five at example.com', 'password': 'userfive'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.five at example.com","features":[{"location":{"fmin":976735,"strand":1,"fmax":995721},"name":"GB42183-RA","children":[{"location":{"fmin":995216,"strand":1,"fmax":995721},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 6" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.six at example.com', 'password': 'usersix'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.six at example.com","features":[{"location":{"fmin":238521,"strand":1,"fmax":241316},"name":"GB40030-RA","children":[{"location":{"fmin":238521,"strand":1,"fmax":238636},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 7" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.seven at example.com', 'password': 'userseven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.seven at example.com","features":[{"location":{"fmin":386401,"strand":-1,"fmax":426902},"name":"GB40010-RA","children":[{"location":{"fmin":386401,"strand":-1,"fmax":386621},"type":{"name":"exon","cv&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 8" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eight at example.com', 'password': 'usereight'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eight at example.com","features":[{"location":{"fmin":223558,"strand":1,"fmax":238576},"name":"GB40808-RA","children":[{"location":{"fmin":223558,"strand":1,"fmax":223806},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 9" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.nine at example.com', 'password': 'usernine'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.nine at example.com","features":[{"location":{"fmin":107997,"strand":1,"fmax":120164},"name":"GB48942-RA","children":[{"location":{"fmin":107997,"strand":1,"fmax":108043},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 10" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.ten at example.com', 'password': 'userten'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.ten at example.com","features":[{"location":{"fmin":300388,"strand":1,"fmax":328828},"name":"GB42167-RA","children":[{"location":{"fmin":325348,"strand":1,"fmax":328828},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 11" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eleven at example.com', 'password': 'usereleven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eleven at example.com","features":[{"location":{"fmin":355088,"strand":-1,"fmax":356284},"name":"GB42157-RA","children":[{"location":{"fmin":355088,"strand":-1,"fmax":355214},"type":{"name":"exon","cv& [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="StatGraphVisualizer" testclass="ResultCollector" testname="Aggregate Graph" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="MonitorHealthVisualizer" testclass="ResultCollector" testname="Monitor Results" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="RespTimeGraphVisualizer" testclass="ResultCollector" testname="Response Time Graph" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+        <intProp name="RespTimeGraph.linestrockwidth">1</intProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="DistributionGraphVisualizer" testclass="ResultCollector" testname="Distribution Graph (alpha)" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true">
+        <stringProp name="ConstantTimer.delay">300</stringProp>
+        <stringProp name="RandomTimer.range">100.0</stringProp>
+      </GaussianRandomTimer>
+      <hashTree/>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-2.jmx b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-2.jmx
new file mode 100644
index 0000000..440b1c3
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-2.jmx
@@ -0,0 +1,2645 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.8" jmeter="2.13 r1665067">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">localhost</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="instance_name" elementType="Argument">
+            <stringProp name="Argument.name">instance_name</stringProp>
+            <stringProp name="Argument.value">apollo</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 1" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.one at example.com', 'password': 'userone'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.one at example.com","features":[{"location":{"fmin":898823,"strand":-1,"fmax":927500},"name":"GB42150-RA","children":[{"location":{"fmin":898823,"strand":-1,"fmax":901952},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 2" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.two at example.com', 'password': 'usertwo'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.two at example.com","features":[{"location":{"fmin":874076,"strand":-1,"fmax":874361},"name":"GB40738-RA","children":[{"location":{"fmin":874076,"strand":-1,"fmax":874091},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 3" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.three at example.com', 'password': 'userthree'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.three at example.com","features":[{"location":{"fmin":418378,"strand":1,"fmax":516127},"name":"GB43015-RA","children":[{"location":{"fmin":512448,"strand":1,"fmax":516127},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 4" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.four at example.com', 'password': 'userfour'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.four at example.com","features":[{"location":{"fmin":383802,"strand":-1,"fmax":501601},"name":"GB42156-RA","children":[{"location":{"fmin":383802,"strand":-1,"fmax":386863},"type":{"name":"exon","cv&qu [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 5" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.five at example.com', 'password': 'userfive'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.five at example.com","features":[{"location":{"fmin":976735,"strand":1,"fmax":995721},"name":"GB42183-RA","children":[{"location":{"fmin":995216,"strand":1,"fmax":995721},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 6" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.six at example.com', 'password': 'usersix'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.six at example.com","features":[{"location":{"fmin":238521,"strand":1,"fmax":241316},"name":"GB40030-RA","children":[{"location":{"fmin":238521,"strand":1,"fmax":238636},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 7" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.seven at example.com', 'password': 'userseven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.seven at example.com","features":[{"location":{"fmin":386401,"strand":-1,"fmax":426902},"name":"GB40010-RA","children":[{"location":{"fmin":386401,"strand":-1,"fmax":386621},"type":{"name":"exon","cv&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 8" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eight at example.com', 'password': 'usereight'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eight at example.com","features":[{"location":{"fmin":223558,"strand":1,"fmax":238576},"name":"GB40808-RA","children":[{"location":{"fmin":223558,"strand":1,"fmax":223806},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 9" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.nine at example.com', 'password': 'usernine'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.nine at example.com","features":[{"location":{"fmin":107997,"strand":1,"fmax":120164},"name":"GB48942-RA","children":[{"location":{"fmin":107997,"strand":1,"fmax":108043},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 10" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.ten at example.com', 'password': 'userten'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.ten at example.com","features":[{"location":{"fmin":300388,"strand":1,"fmax":328828},"name":"GB42167-RA","children":[{"location":{"fmin":325348,"strand":1,"fmax":328828},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 11" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eleven at example.com', 'password': 'usereleven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eleven at example.com","features":[{"location":{"fmin":355088,"strand":-1,"fmax":356284},"name":"GB42157-RA","children":[{"location":{"fmin":355088,"strand":-1,"fmax":355214},"type":{"name":"exon","cv& [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 12" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.twelve at example.com', 'password': 'usertwelve'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.twelve at example.com","features":[{"location":{"fmin":66900638,"strand":1,"fmax":66956304},"name":"NM_001046507.2","children":[{"location":{"fmin":66900638,"strand":1,"fmax":66900789},"type":{"name":"exon" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 13" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.thirteen at example.com', 'password': 'userthirteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.thirteen at example.com","features":[{"location":{"fmin":117545733,"strand":-1,"fmax":117953888},"name":"XM_010801489.1","children":[{"location":{"fmin":117953802,"strand":-1,"fmax":117953888},"type":{"name":"ex [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 14" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fourteen at example.com', 'password': 'userfourteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fourteen at example.com","features":[{"location":{"fmin":64486958,"strand":-1,"fmax":64750899},"name":"NM_001205725.1","children":[{"location":{"fmin":64749947,"strand":-1,"fmax":64750899},"type":{"name":"exon&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 15" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fifteen at example.com', 'password': 'userfifteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fifteen at example.com","features":[{"location":{"fmin":36284953,"strand":1,"fmax":36432436},"name":"XM_005207756.2","children":[{"location":{"fmin":36284953,"strand":1,"fmax":36285264},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 16" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.sixteen at example.com', 'password': 'usersixteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.sixteen at example.com","features":[{"location":{"fmin":87287872,"strand":1,"fmax":87932060},"name":"XM_010821869.1","children":[{"location":{"fmin":87287872,"strand":1,"fmax":87288538},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+      </hashTree>
+      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="StatGraphVisualizer" testclass="ResultCollector" testname="Aggregate Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="MonitorHealthVisualizer" testclass="ResultCollector" testname="Monitor Results" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="RespTimeGraphVisualizer" testclass="ResultCollector" testname="Response Time Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+        <intProp name="RespTimeGraph.linestrockwidth">1</intProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="DistributionGraphVisualizer" testclass="ResultCollector" testname="Distribution Graph (alpha)" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true">
+        <stringProp name="ConstantTimer.delay">300</stringProp>
+        <stringProp name="RandomTimer.range">100.0</stringProp>
+      </GaussianRandomTimer>
+      <hashTree/>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-3.jmx b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-3.jmx
new file mode 100644
index 0000000..76331a4
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-3.jmx
@@ -0,0 +1,3352 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.8" jmeter="2.13 r1665067">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">localhost</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="instance_name" elementType="Argument">
+            <stringProp name="Argument.name">instance_name</stringProp>
+            <stringProp name="Argument.value">apollo</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="get_loop_count" elementType="Argument">
+            <stringProp name="Argument.name">get_loop_count</stringProp>
+            <stringProp name="Argument.value">5</stringProp>
+            <stringProp name="Argument.desc">number of times to get features inbetween adding</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="loops_per_count" elementType="Argument">
+            <stringProp name="Argument.name">loops_per_count</stringProp>
+            <stringProp name="Argument.value">5</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="throughput_per_minute" elementType="Argument">
+            <stringProp name="Argument.name">throughput_per_minute</stringProp>
+            <stringProp name="Argument.value">2</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 1" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">2</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.one at example.com', 'password': 'userone'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.one at example.com","features":[{"location":{"fmin":898823,"strand":-1,"fmax":927500},"name":"GB42150-RA","children":[{"location":{"fmin":898823,"strand":-1,"fmax":901952},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L1" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L1" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 2" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">4</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.two at example.com', 'password': 'usertwo'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.two at example.com","features":[{"location":{"fmin":874076,"strand":-1,"fmax":874361},"name":"GB40738-RA","children":[{"location":{"fmin":874076,"strand":-1,"fmax":874091},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L2" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L2" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 3" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">6</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.three at example.com', 'password': 'userthree'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.three at example.com","features":[{"location":{"fmin":418378,"strand":1,"fmax":516127},"name":"GB43015-RA","children":[{"location":{"fmin":512448,"strand":1,"fmax":516127},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L3" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L3" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 4" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">8</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.four at example.com', 'password': 'userfour'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.four at example.com","features":[{"location":{"fmin":383802,"strand":-1,"fmax":501601},"name":"GB42156-RA","children":[{"location":{"fmin":383802,"strand":-1,"fmax":386863},"type":{"name":"exon","cv&qu [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L4" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L4" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 5" enabled="false">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.five at example.com', 'password': 'userfive'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.five at example.com","features":[{"location":{"fmin":976735,"strand":1,"fmax":995721},"name":"GB42183-RA","children":[{"location":{"fmin":995216,"strand":1,"fmax":995721},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L5" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L5" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>2.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 6" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">12</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.six at example.com', 'password': 'usersix'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.six at example.com","features":[{"location":{"fmin":238521,"strand":1,"fmax":241316},"name":"GB40030-RA","children":[{"location":{"fmin":238521,"strand":1,"fmax":238636},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L6" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L6" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 7" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">14</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.seven at example.com', 'password': 'userseven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.seven at example.com","features":[{"location":{"fmin":386401,"strand":-1,"fmax":426902},"name":"GB40010-RA","children":[{"location":{"fmin":386401,"strand":-1,"fmax":386621},"type":{"name":"exon","cv&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L7" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L7" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 8" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">16</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eight at example.com', 'password': 'usereight'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eight at example.com","features":[{"location":{"fmin":223558,"strand":1,"fmax":238576},"name":"GB40808-RA","children":[{"location":{"fmin":223558,"strand":1,"fmax":223806},"type":{"name":"exon","cv&quo [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L8" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L8" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 9" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">18</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.nine at example.com', 'password': 'usernine'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.nine at example.com","features":[{"location":{"fmin":107997,"strand":1,"fmax":120164},"name":"GB48942-RA","children":[{"location":{"fmin":107997,"strand":1,"fmax":108043},"type":{"name":"exon","cv&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L9" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L9" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 10" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">20</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.ten at example.com', 'password': 'userten'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.ten at example.com","features":[{"location":{"fmin":300388,"strand":1,"fmax":328828},"name":"GB42167-RA","children":[{"location":{"fmin":325348,"strand":1,"fmax":328828},"type":{"name":"exon","cv" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L10" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L10" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 11" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">22</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.eleven at example.com', 'password': 'usereleven'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.eleven at example.com","features":[{"location":{"fmin":355088,"strand":-1,"fmax":356284},"name":"GB42157-RA","children":[{"location":{"fmin":355088,"strand":-1,"fmax":355214},"type":{"name":"exon","cv& [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L11" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L11" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 12" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">24</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.twelve at example.com', 'password': 'usertwelve'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.twelve at example.com","features":[{"location":{"fmin":66900638,"strand":1,"fmax":66956304},"name":"NM_001046507.2","children":[{"location":{"fmin":66900638,"strand":1,"fmax":66900789},"type":{"name":"exon" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L12" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L12" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 13" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">26</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.thirteen at example.com', 'password': 'userthirteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.thirteen at example.com","features":[{"location":{"fmin":117545733,"strand":-1,"fmax":117953888},"name":"XM_010801489.1","children":[{"location":{"fmin":117953802,"strand":-1,"fmax":117953888},"type":{"name":"ex [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L13" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L13" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 14" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">28</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fourteen at example.com', 'password': 'userfourteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fourteen at example.com","features":[{"location":{"fmin":64486958,"strand":-1,"fmax":64750899},"name":"NM_001205725.1","children":[{"location":{"fmin":64749947,"strand":-1,"fmax":64750899},"type":{"name":"exon&q [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L14" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L14" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 15" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">33</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.fifteen at example.com', 'password': 'userfifteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.fifteen at example.com","features":[{"location":{"fmin":36284953,"strand":1,"fmax":36432436},"name":"XM_005207756.2","children":[{"location":{"fmin":36284953,"strand":1,"fmax":36285264},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L15" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L15" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group 16" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <intProp name="LoopController.loops">-1</intProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">1</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">37</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+        <boolProp name="ThreadGroup.delayedStart">true</boolProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'user.sixteen at example.com', 'password': 'usersixteen'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{"operation":"add_transcript","username":"user.sixteen at example.com","features":[{"location":{"fmin":87287872,"strand":1,"fmax":87932060},"name":"XM_010821869.1","children":[{"location":{"fmin":87287872,"strand":1,"fmax":87288538},"type":{"name":"exon&quot [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract track" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">track</stringProp>
+              <stringProp name="RegexExtractor.regex">"sequence":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Get Feature Loop Controller" enabled="true">
+            <boolProp name="LoopController.continue_forever">true</boolProp>
+            <stringProp name="LoopController.loops">5</stringProp>
+          </LoopController>
+          <hashTree>
+            <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Feature L16" enabled="true">
+              <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+              <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+                <collectionProp name="Arguments.arguments">
+                  <elementProp name="" elementType="HTTPArgument">
+                    <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                    <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "get_features" }</stringProp>
+                    <stringProp name="Argument.metadata">=</stringProp>
+                  </elementProp>
+                </collectionProp>
+              </elementProp>
+              <stringProp name="HTTPSampler.domain">${server}</stringProp>
+              <stringProp name="HTTPSampler.port">8080</stringProp>
+              <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+              <stringProp name="HTTPSampler.response_timeout"></stringProp>
+              <stringProp name="HTTPSampler.protocol"></stringProp>
+              <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+              <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/getFeatures</stringProp>
+              <stringProp name="HTTPSampler.method">POST</stringProp>
+              <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+              <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+              <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+              <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+              <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+              <boolProp name="HTTPSampler.monitor">false</boolProp>
+              <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+            </HTTPSamplerProxy>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature L16" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${instance_name}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${instance_name}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ConstantThroughputTimer guiclass="TestBeanGUI" testclass="ConstantThroughputTimer" testname="Constant Throughput Timer" enabled="true">
+          <intProp name="calcMode">0</intProp>
+          <doubleProp>
+            <name>throughput</name>
+            <value>14.0</value>
+            <savedValue>0.0</savedValue>
+          </doubleProp>
+        </ConstantThroughputTimer>
+        <hashTree/>
+      </hashTree>
+      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>false</assertions>
+            <subresults>false</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>true</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <url>true</url>
+            <threadCounts>true</threadCounts>
+            <sampleCount>true</sampleCount>
+          </value>
+        </objProp>
+        <stringProp name="filename">/Users/nathandunn/repositories/Apollo/docs/apollo-stress-tests/suite2/results.csv</stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="StatGraphVisualizer" testclass="ResultCollector" testname="Aggregate Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="MonitorHealthVisualizer" testclass="ResultCollector" testname="Monitor Results" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="RespTimeGraphVisualizer" testclass="ResultCollector" testname="Response Time Graph" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+        <intProp name="RespTimeGraph.linestrockwidth">1</intProp>
+      </ResultCollector>
+      <hashTree/>
+      <ResultCollector guiclass="DistributionGraphVisualizer" testclass="ResultCollector" testname="Distribution Graph (alpha)" enabled="false">
+        <boolProp name="ResultCollector.error_logging">false</boolProp>
+        <objProp>
+          <name>saveConfig</name>
+          <value class="SampleSaveConfiguration">
+            <time>true</time>
+            <latency>true</latency>
+            <timestamp>true</timestamp>
+            <success>true</success>
+            <label>true</label>
+            <code>true</code>
+            <message>true</message>
+            <threadName>true</threadName>
+            <dataType>true</dataType>
+            <encoding>false</encoding>
+            <assertions>true</assertions>
+            <subresults>true</subresults>
+            <responseData>false</responseData>
+            <samplerData>false</samplerData>
+            <xml>false</xml>
+            <fieldNames>false</fieldNames>
+            <responseHeaders>false</responseHeaders>
+            <requestHeaders>false</requestHeaders>
+            <responseDataOnError>false</responseDataOnError>
+            <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+            <assertionsResultsToSave>0</assertionsResultsToSave>
+            <bytes>true</bytes>
+            <threadCounts>true</threadCounts>
+          </value>
+        </objProp>
+        <stringProp name="filename"></stringProp>
+      </ResultCollector>
+      <hashTree/>
+      <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true">
+        <stringProp name="ConstantTimer.delay">300</stringProp>
+        <stringProp name="RandomTimer.range">100.0</stringProp>
+      </GaussianRandomTimer>
+      <hashTree/>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-notes.md b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-notes.md
new file mode 100644
index 0000000..8d740e0
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test-notes.md
@@ -0,0 +1,40 @@
+Apollo stress test using Apache JMeter
+======================================
+
+### jmeter-dev-stress-test-1.jmx
+The script has two user-defined variables:
+
+* server (`localhost`)
+* instance_name ( `apollo`)
+
+The test is performed on data from *Apis mellifera*.
+
+Datasets:
+
+1. [*A. mellifera* reference genome](http://hymenopteragenome.org/beebase/sites/hymenopteragenome.org.beebase/files/data/Amel_4.5_scaffolds.fa.gz)
+2. [*A. mellifera* Official Gene Set v3.2](http://hymenopteragenome.org/beebase/sites/hymenopteragenome.org.beebase/files/data/consortium_data/amel_OGSv3.2.gff3.gz)
+
+The datasets can be processed as described [here](http://genomearchitect.readthedocs.io/en/latest/Data_loading/#data-generation-pipeline).
+
+### jmeter-dev-stress-test-2.jmx
+
+The script has two user-defined variables:
+* server (`localhost`)
+* instance_name ( `apollo`)
+
+The test is performed on data from *Apis mellifera* as well as *Bos taurus*.
+
+Datasets:
+
+1. [*Bos taurus* reference genome](http://128.206.12.216/drupal/sites/bovinegenome.org/files/data/umd3.1/UMD3.1_chromosomes.fa.gz)
+2. [*Bos taurus* RefSeq Annotations for protein coding genes](http://128.206.12.216/drupal/sites/bovinegenome.org/files/data/umd3.1/RefSeq_UMD3.1.1_protein_coding.gff3.gz)
+
+The datasets can be processed as described [here](http://genomearchitect.readthedocs.io/en/latest/Data_loading/#data-generation-pipeline).
+
+###Users
+Each of the test script utilizes several user profiles. To add the same user profiles as described in the script, make use of ```add_users.groovy``` with ```example-users-for-stress-test.csv``` as input.
+```
+add_users.groovy -inputfile example-users-for-stress-test.csv --username <admin username> --password <admin password> --destinationurl <URL for apollo>
+```
+
+Note: After this step, make sure to grant organism permissions to each user. This can be done via the Users tab in Annotator Panel of Apollo.
diff --git a/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test.jmx b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test.jmx
new file mode 100644
index 0000000..a35f06a
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-dev-stress-test.jmx
@@ -0,0 +1,324 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.8" jmeter="2.13 r1665067">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">localhost</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="species" elementType="Argument">
+            <stringProp name="Argument.name">species</stringProp>
+            <stringProp name="Argument.value">apollo</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value">Group1.1</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">8</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">10</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">5</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': 'demo', 'password': 'demo'}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${species}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">10</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "Group1.7", "features": [{"location":{"fmin":398013,"fmax":412787,"strand":-1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"GB53205-RA","children":[{"location":{"fmin":398013,"fmax":398033,"strand":-1},"type" [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${species}/annotationEditor/addTranscript</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${species}/annotationEditor/deleteFeature</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${species}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <name>saveConfig</name>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>false</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <threadCounts>true</threadCounts>
+            </value>
+          </objProp>
+          <objProp>
+            <name></name>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+              <threadCounts>true</threadCounts>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="View Results in Table" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <name>saveConfig</name>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>false</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <threadCounts>true</threadCounts>
+            </value>
+          </objProp>
+          <objProp>
+            <name></name>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+              <threadCounts>true</threadCounts>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true">
+          <stringProp name="ConstantTimer.delay">400</stringProp>
+        </ConstantTimer>
+        <hashTree/>
+      </hashTree>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-stress-test-template.jmx b/docs/apollo-stress-tests/suite2/jmeter-stress-test-template.jmx
new file mode 100644
index 0000000..1a5e8f1
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/jmeter-stress-test-template.jmx
@@ -0,0 +1,318 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.6" jmeter="2.11 r1554548">
+  <hashTree>
+    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+      <stringProp name="TestPlan.comments"></stringProp>
+      <boolProp name="TestPlan.functional_mode">false</boolProp>
+      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments"/>
+      </elementProp>
+      <stringProp name="TestPlan.user_define_classpath"></stringProp>
+    </TestPlan>
+    <hashTree>
+      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+        <collectionProp name="Arguments.arguments">
+          <elementProp name="server" elementType="Argument">
+            <stringProp name="Argument.name">server</stringProp>
+            <stringProp name="Argument.value">yourhost.com</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="species" elementType="Argument">
+            <stringProp name="Argument.name">species</stringProp>
+            <stringProp name="Argument.value">your_species</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="track" elementType="Argument">
+            <stringProp name="Argument.name">track</stringProp>
+            <stringProp name="Argument.value">Scaffold1</stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+          <elementProp name="feature_id" elementType="Argument">
+            <stringProp name="Argument.name">feature_id</stringProp>
+            <stringProp name="Argument.value"></stringProp>
+            <stringProp name="Argument.metadata">=</stringProp>
+          </elementProp>
+        </collectionProp>
+      </Arguments>
+      <hashTree/>
+      <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP Cookie Manager" enabled="true">
+        <collectionProp name="CookieManager.cookies"/>
+        <boolProp name="CookieManager.clearEachIteration">true</boolProp>
+        <stringProp name="CookieManager.policy">default</stringProp>
+        <stringProp name="CookieManager.implementation">org.apache.jmeter.protocol.http.control.HC4CookieHandler</stringProp>
+      </CookieManager>
+      <hashTree/>
+      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+        <collectionProp name="HeaderManager.headers">
+          <elementProp name="" elementType="Header">
+            <stringProp name="Header.name">Content-Type</stringProp>
+            <stringProp name="Header.value">application/json</stringProp>
+          </elementProp>
+        </collectionProp>
+      </HeaderManager>
+      <hashTree/>
+      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">false</boolProp>
+          <stringProp name="LoopController.loops">1</stringProp>
+        </elementProp>
+        <stringProp name="ThreadGroup.num_threads">5</stringProp>
+        <stringProp name="ThreadGroup.ramp_time">5</stringProp>
+        <longProp name="ThreadGroup.start_time">1404149611000</longProp>
+        <longProp name="ThreadGroup.end_time">1404149611000</longProp>
+        <boolProp name="ThreadGroup.scheduler">false</boolProp>
+        <stringProp name="ThreadGroup.duration"></stringProp>
+        <stringProp name="ThreadGroup.delay"></stringProp>
+      </ThreadGroup>
+      <hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
+          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+            <collectionProp name="Arguments.arguments">
+              <elementProp name="" elementType="HTTPArgument">
+                <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                <stringProp name="Argument.value">{'username': '', 'password': ''}</stringProp>
+                <stringProp name="Argument.metadata">=</stringProp>
+              </elementProp>
+            </collectionProp>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${species}/Login?operation=login</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+          <boolProp name="LoopController.continue_forever">true</boolProp>
+          <stringProp name="LoopController.loops">2</stringProp>
+        </LoopController>
+        <hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add transcript" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "Scaffold1", "features": [{"location":{"fmin":1477397,"fmax":1477640,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"BgerTmpM000008-RA","children":[{"location":{"fmin":1477397,"fmax":1477640,"strand":1}," [...]
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${species}/AnnotationEditorService</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract feature_id" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">feature_id</stringProp>
+              <stringProp name="RegexExtractor.regex">"uniquename":"(.+?)"</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default"></stringProp>
+              <stringProp name="RegexExtractor.match_number">3</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Delete feature" enabled="true">
+            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+              <collectionProp name="Arguments.arguments">
+                <elementProp name="" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">{ "track": "${track}", "features": [ { "uniquename": "${feature_id}" } ], "operation": "delete_feature" }</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                </elementProp>
+              </collectionProp>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain">${server}</stringProp>
+            <stringProp name="HTTPSampler.port">8080</stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">/${species}/AnnotationEditorService</stringProp>
+            <stringProp name="HTTPSampler.method">POST</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree/>
+        </hashTree>
+        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Logout" enabled="true">
+          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+            <collectionProp name="Arguments.arguments"/>
+          </elementProp>
+          <stringProp name="HTTPSampler.domain">${server}</stringProp>
+          <stringProp name="HTTPSampler.port">8080</stringProp>
+          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+          <stringProp name="HTTPSampler.response_timeout"></stringProp>
+          <stringProp name="HTTPSampler.protocol">http</stringProp>
+          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+          <stringProp name="HTTPSampler.path">/${species}/Login?operation=logout</stringProp>
+          <stringProp name="HTTPSampler.method">POST</stringProp>
+          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
+          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
+          <boolProp name="HTTPSampler.monitor">false</boolProp>
+          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+        </HTTPSamplerProxy>
+        <hashTree/>
+        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="View Results in Table" enabled="true">
+          <boolProp name="ResultCollector.error_logging">false</boolProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <objProp>
+            <value class="SampleSaveConfiguration">
+              <time>true</time>
+              <latency>true</latency>
+              <timestamp>true</timestamp>
+              <success>true</success>
+              <label>true</label>
+              <code>true</code>
+              <message>true</message>
+              <threadName>true</threadName>
+              <dataType>true</dataType>
+              <encoding>false</encoding>
+              <assertions>true</assertions>
+              <subresults>true</subresults>
+              <responseData>false</responseData>
+              <samplerData>false</samplerData>
+              <xml>false</xml>
+              <fieldNames>false</fieldNames>
+              <responseHeaders>false</responseHeaders>
+              <requestHeaders>true</requestHeaders>
+              <responseDataOnError>false</responseDataOnError>
+              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
+              <assertionsResultsToSave>0</assertionsResultsToSave>
+              <bytes>true</bytes>
+              <url>true</url>
+            </value>
+          </objProp>
+          <stringProp name="filename"></stringProp>
+        </ResultCollector>
+        <hashTree/>
+        <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true">
+          <stringProp name="ConstantTimer.delay">1000</stringProp>
+        </ConstantTimer>
+        <hashTree/>
+      </hashTree>
+    </hashTree>
+  </hashTree>
+</jmeterTestPlan>
diff --git a/docs/apollo-stress-tests/suite2/jmeter-stress-test.pdf b/docs/apollo-stress-tests/suite2/jmeter-stress-test.pdf
new file mode 100644
index 0000000..d9244de
Binary files /dev/null and b/docs/apollo-stress-tests/suite2/jmeter-stress-test.pdf differ
diff --git a/docs/apollo-stress-tests/suite2/summary-rc2-2015-07-26.csv b/docs/apollo-stress-tests/suite2/summary-rc2-2015-07-26.csv
new file mode 100644
index 0000000..4fd8c41
--- /dev/null
+++ b/docs/apollo-stress-tests/suite2/summary-rc2-2015-07-26.csv
@@ -0,0 +1,49 @@
+Label,# Samples,Average,Min,Max,Std. Dev.,Error %,Throughput,KB/sec,Avg. Bytes
+Login,351,8,4,110,13.46,0.00%,.0,0.02,348.0
+Add transcript L2,250,943,112,2287,582.81,0.00%,.0,0.09,2940.2
+Add transcript L12,185,331,93,2322,235.88,0.00%,.0,0.11,4599.0
+Add transcript L10,250,273,117,852,116.13,0.00%,.0,0.14,4541.2
+Add transcript L15,250,264,94,685,140.55,0.00%,.0,0.15,4601.2
+Add transcript L3,249,703,196,2089,311.76,0.00%,.0,0.30,9402.2
+Add transcript L11,249,1593,22,4031,804.14,0.40%,.0,0.08,2404.4
+Add transcript L9,250,350,97,1191,227.64,0.00%,.0,0.20,6165.2
+Add transcript L6,250,519,150,1766,312.13,0.00%,.0,0.13,4010.2
+Add transcript L1,256,708,133,1665,256.90,0.00%,.0,0.17,5088.1
+Add transcript L14,250,1594,151,4703,646.30,0.40%,.0,0.08,2436.1
+Add transcript L4,250,884,308,1797,234.58,0.00%,.0,0.22,6819.2
+Add transcript L7,250,1143,370,2219,381.88,0.00%,.0,0.35,11151.8
+Add transcript L16,247,961,350,3987,502.96,0.00%,.0,0.49,15588.2
+Add transcript L8,250,945,392,1921,326.10,0.00%,.0,0.29,8979.2
+Add transcript L13,185,2963,1159,10333,1382.26,0.00%,.0,0.63,26611.0
+Get Feature L3,1240,1105,49,5690,1008.68,0.00%,.2,18.20,115242.8
+Get Feature L15,1242,1066,157,3866,596.33,0.00%,.2,13.63,86301.1
+Get Feature L14,1242,1071,120,4078,605.97,0.00%,.2,13.63,86317.2
+Get Feature L13,915,5002,75,21933,4435.38,0.00%,.1,68.31,586287.0
+Get Feature L12,920,5143,74,23944,4492.44,0.00%,.1,68.41,584952.4
+Get Feature L10,1240,2071,487,5450,762.60,0.00%,.2,26.14,165449.1
+Get Feature L9,1242,643,34,3125,668.91,0.00%,.2,7.95,50353.8
+Get Feature L4,1240,2084,452,6233,761.37,0.00%,.2,26.10,165251.8
+Get Feature L16,1229,1484,75,7310,1395.79,0.00%,.2,25.19,161227.6
+Get Feature L6,1242,1472,87,4691,876.90,0.00%,.2,19.69,124720.7
+Get Feature L7,1242,1452,90,4642,879.86,0.00%,.2,19.69,124720.7
+Get Feature L8,1240,1392,144,5177,878.01,0.00%,.2,17.59,111292.8
+Get Feature L1,1275,1906,74,6155,776.61,0.00%,.2,26.47,163208.8
+Get Feature L11,1240,2061,595,6287,763.42,0.00%,.2,26.15,165578.9
+Get Feature L2,1241,1277,155,5211,885.48,0.00%,.2,17.56,111322.6
+Delete feature L11,248,123,30,549,74.18,0.00%,.0,0.01,175.2
+Delete feature L2,248,127,33,618,77.83,0.00%,.0,0.01,175.2
+Delete feature L14,248,130,31,1022,98.30,0.00%,.0,0.01,175.2
+Delete feature L15,248,262,62,2427,215.30,0.00%,.0,0.01,175.2
+Delete feature L10,248,247,46,1398,180.28,0.00%,.0,0.01,175.2
+Delete feature L12,184,291,51,1227,183.33,0.00%,.0,0.00,175.3
+Delete feature L6,248,206,50,1312,155.35,0.00%,.0,0.01,175.2
+Delete feature L1,255,378,93,2584,298.14,0.00%,.0,0.01,175.1
+Delete feature L3,248,503,150,7553,635.05,0.00%,.0,0.01,175.2
+Delete feature L9,248,311,87,2291,283.73,0.00%,.0,0.01,175.2
+Delete feature L8,248,492,163,3364,390.25,0.00%,.0,0.01,175.2
+Delete feature L4,248,464,56,1870,233.17,0.00%,.0,0.01,175.2
+Delete feature L16,245,921,276,10036,1162.42,0.00%,.0,0.01,175.2
+Delete feature L13,183,2385,821,31914,2665.60,0.00%,.0,0.00,175.3
+Delete feature L7,248,649,96,3393,429.82,0.00%,.0,0.01,175.2
+Logout,321,8,1,492,33.08,0.00%,.0,0.01,266.0
+TOTAL,25878,1471,1,31914,1829.68,0.01%,3.4,397.32,120938.4
diff --git a/docs/architecture2.png b/docs/architecture2.png
new file mode 100644
index 0000000..68e2b91
Binary files /dev/null and b/docs/architecture2.png differ
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..8fc2f77
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+#
+# Apollo documentation build configuration file, created by
+# sphinx-quickstart on Mon Mar  9 10:54:48 2015.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+sys.path.append(os.path.abspath('exts'))
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['markdowntransform']
+
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Apollo'
+copyright = u'2017, Apollo'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '2.0.8'
+# The full version, including alpha/beta/rc tags.
+release = '2.0.8'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Apollodoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+  ('index', 'Apollo.tex', u'Apollo Documentation',
+   u'Apollo', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'apollo', u'Apollo Documentation',
+     [u'apollo'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'Apollo', u'Apollo Documentation',
+   u'apollo', 'Apollo', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+# The suffix of source filenames.
+from recommonmark.parser import CommonMarkParser
+
+# The suffix of source filenames.
+source_suffix = ['.rst', '.md']
+
+source_parsers = {
+    '.md': CommonMarkParser,
+}
diff --git a/docs/exts/markdowntransform.py b/docs/exts/markdowntransform.py
new file mode 100644
index 0000000..0b3116f
--- /dev/null
+++ b/docs/exts/markdowntransform.py
@@ -0,0 +1,10 @@
+import re
+
+def processLink(app, docname, source):
+    original = source[0]
+    subbed = re.sub(r"\.md", r"\.html", original)
+    source[0] = subbed
+
+def setup(app):
+    app.connect('source-read', processLink)
+                                               
diff --git a/docs/exts/markdowntransform.pyc b/docs/exts/markdowntransform.pyc
new file mode 100644
index 0000000..92fe7c4
Binary files /dev/null and b/docs/exts/markdowntransform.pyc differ
diff --git a/docs/images/1.png b/docs/images/1.png
new file mode 100644
index 0000000..fb2a9bb
Binary files /dev/null and b/docs/images/1.png differ
diff --git a/docs/images/2.png b/docs/images/2.png
new file mode 100644
index 0000000..5dc0fcb
Binary files /dev/null and b/docs/images/2.png differ
diff --git a/docs/images/3.png b/docs/images/3.png
new file mode 100644
index 0000000..5e6f140
Binary files /dev/null and b/docs/images/3.png differ
diff --git a/docs/images/download.png b/docs/images/download.png
new file mode 100644
index 0000000..301fb99
Binary files /dev/null and b/docs/images/download.png differ
diff --git a/docs/images/download_small.png b/docs/images/download_small.png
new file mode 100644
index 0000000..1efc599
Binary files /dev/null and b/docs/images/download_small.png differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..b89ad44
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,48 @@
+Apollo
+============
+
+Apollo - An instantaneous, collaborative, genome annotation editor.
+
+The application's technology stack includes a Grails-based Java web application with flexible database backends and a
+Javascript client that runs in a web browser as a JBrowse plugin.  
+
+You can find the latest release here: https://github.com/GMOD/Apollo/releases/latest and our setup guide: http://genomearchitect.readthedocs.io/en/latest/Setup.html
+
+- Apollo general documentation: `https://genomearchitect.github.io/ <https://genomearchitect.github.io>`__
+
+- JBrowse general documentation: `http://jbrowse.org <http://jbrowse.org>`__
+
+.. image:: https://travis-ci.org/GMOD/Apollo.png?branch=master
+
+Note: This documentation covers release versions 2.x of Apollo. For the 1.0.4 installation please refer to
+the installation guide found at `http://genomearchitect.readthedocs.io/en/1.0.4/ <http://genomearchitect.readthedocs.io/en/1.0.4/>`__
+
+
+.. A PDF version of this documentation is also available for download.
+
+.. Link `https://media.readthedocs.org/pdf/webapollo/latest/webapollo.pdf <https://media.readthedocs.org/pdf/webapollo/latest/webapollo.pdf>`__
+
+
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+   :glob:
+
+   Prerequisites
+   Apollo2Build
+   Setup
+   Configure
+   ChadoExport
+   Data_loading
+   Data_Loading_via_web_services
+   Contributing
+   Troubleshooting
+   Migration
+   Permissions
+   Testing_notes
+   Architecture
+   Command_line
+   Web_services
+   ExampleBuild
diff --git a/docs/schemaupdates.graffle/data.plist b/docs/schemaupdates.graffle/data.plist
new file mode 100644
index 0000000..bd742d1
Binary files /dev/null and b/docs/schemaupdates.graffle/data.plist differ
diff --git a/docs/schemaupdates.graffle/image1.tiff b/docs/schemaupdates.graffle/image1.tiff
new file mode 100644
index 0000000..7d66042
Binary files /dev/null and b/docs/schemaupdates.graffle/image1.tiff differ
diff --git a/docs/schemaupdates.graffle/image10.tiff b/docs/schemaupdates.graffle/image10.tiff
new file mode 100644
index 0000000..5ff6a30
Binary files /dev/null and b/docs/schemaupdates.graffle/image10.tiff differ
diff --git a/docs/schemaupdates.graffle/image2.png b/docs/schemaupdates.graffle/image2.png
new file mode 100644
index 0000000..7a21391
Binary files /dev/null and b/docs/schemaupdates.graffle/image2.png differ
diff --git a/docs/schemaupdates.graffle/image3.png b/docs/schemaupdates.graffle/image3.png
new file mode 100644
index 0000000..8569e93
Binary files /dev/null and b/docs/schemaupdates.graffle/image3.png differ
diff --git a/docs/schemaupdates.graffle/image4.png b/docs/schemaupdates.graffle/image4.png
new file mode 100644
index 0000000..dabcdef
Binary files /dev/null and b/docs/schemaupdates.graffle/image4.png differ
diff --git a/docs/schemaupdates.graffle/image5.png b/docs/schemaupdates.graffle/image5.png
new file mode 100644
index 0000000..0cb8f27
Binary files /dev/null and b/docs/schemaupdates.graffle/image5.png differ
diff --git a/docs/schemaupdates.graffle/image7.tiff b/docs/schemaupdates.graffle/image7.tiff
new file mode 100644
index 0000000..2ac739a
Binary files /dev/null and b/docs/schemaupdates.graffle/image7.tiff differ
diff --git a/docs/schemaupdates.graffle/image8.tiff b/docs/schemaupdates.graffle/image8.tiff
new file mode 100644
index 0000000..41927ce
Binary files /dev/null and b/docs/schemaupdates.graffle/image8.tiff differ
diff --git a/docs/schemaupdates.graffle/image9.tiff b/docs/schemaupdates.graffle/image9.tiff
new file mode 100644
index 0000000..2ac739a
Binary files /dev/null and b/docs/schemaupdates.graffle/image9.tiff differ
diff --git a/docs/schemaupdates.pdf b/docs/schemaupdates.pdf
new file mode 100644
index 0000000..b7adcbd
Binary files /dev/null and b/docs/schemaupdates.pdf differ
diff --git a/docs/undoredotranscript.graffle b/docs/undoredotranscript.graffle
new file mode 100644
index 0000000..62cad99
Binary files /dev/null and b/docs/undoredotranscript.graffle differ
diff --git a/docs/web_services/examples/groovy/Apollo1Operations.groovy b/docs/web_services/examples/groovy/Apollo1Operations.groovy
new file mode 100644
index 0000000..c760237
--- /dev/null
+++ b/docs/web_services/examples/groovy/Apollo1Operations.groovy
@@ -0,0 +1,55 @@
+
+import groovy.json.JsonSlurper
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovy.sql.Sql
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+//@Grab(group='postgresql', module='postgresql', version='8.3-603.jdbc4')
+//@Grab('org.postgresql:postgresql:9.3-1101-jdbc41')
+ at Grab(group='org.postgresql', module='postgresql', version='9.4-1201-jdbc41')
+ at GrabConfig(systemClassLoader = true)
+
+
+
+
+static def getFeature(url,track,cookieFile,ignorePrefix){
+
+    String prefix = ignorePrefix ? "" : "Annotations-"
+
+    String json = "{ 'operation': 'get_features', 'track': '${prefix}${track}'}"
+    def process = ["curl","-b",cookieFile,"-c",cookieFile,"-e",url,"--data",json,"${url}/AnnotationEditorService"].execute()
+    def response = process.text
+    if(process.exitValue()!=0){
+        println process.errorStream.text
+    }
+    def jsonResponse = new JsonSlurper().parseText(response)
+    return jsonResponse
+
+}
+
+static def doLogin(url, username, password,cookieFile) {
+    String json = "{'username': '${username}', 'password': '${password}'}"
+    def process = ["curl", "-c", cookieFile, "-H", "Content-Type:application/json", "-d", json, "${url}/Login?operation=login"].execute()
+    def response = process.text
+    if (process.exitValue() != 0) {
+        println process.errorStream.text
+    }
+    def jsonResponse = new JsonSlurper().parseText(response)
+    return jsonResponse
+}
+
+static def getUsers(username,password,url){
+    JSONArray usersArray = new JSONArray()
+    Class.forName("org.postgresql.Driver");
+
+//    Sql sql = groovy.sql.Sql.newInstance( "jdbc:postgresql://localhost/web_apollo_users",username,password, "org.postgresql.Driver")
+    Sql sql = groovy.sql.Sql.newInstance( "jdbc:postgresql://${url}",username,password, "org.postgresql.Driver")
+    sql.eachRow('select * from users') { row ->
+        JSONObject userObject = new JSONObject()
+        userObject.username=row[1]
+        usersArray.add(userObject)
+    }
+
+    return usersArray
+}
diff --git a/docs/web_services/examples/groovy/Apollo2Operations.groovy b/docs/web_services/examples/groovy/Apollo2Operations.groovy
new file mode 100644
index 0000000..4345ac1
--- /dev/null
+++ b/docs/web_services/examples/groovy/Apollo2Operations.groovy
@@ -0,0 +1,94 @@
+
+import groovyx.net.http.RESTClient
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+static JSONArray assignNewUniqueName(JSONArray inputArray,Map uniqueNamesMap) {
+    JSONArray returnArray = new JSONArray()
+    String oldUniqueName, newUniqueName
+    int idx = 0
+    for (def eachEntity : inputArray) {
+        oldUniqueName = eachEntity.uniquename
+        newUniqueName = generateUniqueName()
+        eachEntity.uniquename = newUniqueName
+        returnArray.add(idx, eachEntity)
+        uniqueNamesMap.put(oldUniqueName, newUniqueName)
+        idx += 1
+    }
+    return returnArray
+}
+
+static JSONObject triggerAddFeature(String destinationurl, String username, String password, String organism, String sequenceName, JSONArray featuresArray, Boolean ignoressl = false) {
+    URL url = new URL(destinationurl)
+    String fullPath = "${url.path}/annotationEditor/addFeature"
+    fullPath = fullPath.replaceAll("//","/")
+    def addFeatureClient = new RESTClient(url)
+    if (ignoressl) { addFeatureClient.ignoreSSLIssues() }
+    def addFeatureResponse = addFeatureClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: [  'username' : username, 'password' : password, 'track' : sequenceName, 'organism' : organism, 'features' : featuresArray ]
+    )
+
+    assert addFeatureResponse.status == 200
+    if (addFeatureResponse.getData().size() == 0) {
+        println "Error: Server did not respond properly while trying to call /addFeature"
+        return
+    }
+    else {
+        return addFeatureResponse.getData()
+
+    }
+}
+
+static JSONObject triggerAddTranscript(String destinationurl, String username, String password, String organism, String sequenceName, JSONArray featuresArray, Boolean ignoressl = false) {
+    URL url = new URL(destinationurl)
+    String fullPath = "${url.path}/annotationEditor/addTranscript"
+    fullPath = fullPath.replaceAll("//","/")
+    def addTranscriptClient = new RESTClient(url)
+    if (ignoressl) { addTranscriptClient.ignoreSSLIssues() }
+    def addTranscriptResponse = addTranscriptClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: [  'username' : username, 'password' : password, 'track' : sequenceName, 'organism' : organism, 'features' : featuresArray ]
+    )
+
+    assert addTranscriptResponse.status == 200
+    if (addTranscriptResponse.getData().size() == 0) {
+        println "Error: Server did not respond properly while trying to call /addTranscript"
+        return
+    }
+    else {
+        return addTranscriptResponse.getData()
+    }
+}
+
+static JSONObject triggerRemoveTranscript(String destinationurl, String username, String password, String organism, String sequenceName, JSONArray featuresArray, Boolean ignoressl = false) {
+    URL url = new URL(destinationurl)
+    String fullPath = "${url.path}/annotationEditor/deleteFeature"
+    fullPath = fullPath.replaceAll("//","/")
+    def removeTranscriptClient = new RESTClient(url)
+    if (ignoressl) { removeTranscriptClient.ignoreSSLIssues() }
+    def removeTranscriptResponse = removeTranscriptClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: [  'username' : username, 'password' : password, 'track' : sequenceName, 'organism' : organism, 'features' : featuresArray]
+    )
+
+    assert removeTranscriptResponse.status == 200
+    if (removeTranscriptResponse.getData().size() == 0) {
+        println "Error: Server did not respond properly while trying to call /deleteTranscript"
+        return
+    }
+    else {
+        return removeTranscriptResponse.getData()
+    }
+}
+
+static String generateUniqueName() {
+    return UUID.randomUUID().toString()
+
+}
diff --git a/docs/web_services/examples/groovy/SampleFeatures.groovy b/docs/web_services/examples/groovy/SampleFeatures.groovy
new file mode 100644
index 0000000..3eb39d1
--- /dev/null
+++ b/docs/web_services/examples/groovy/SampleFeatures.groovy
@@ -0,0 +1,30 @@
+import groovy.json.JsonSlurper
+
+ at Grab(group='org.codehaus.groovy', module='groovy-json', version='2.4.7')
+
+
+
+
+static Object getSampleFeatures(){
+
+    String jsonArrayString = "[{\"location\":{\"fmin\":697781,\"fmax\":709269,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40823-RA\",\"children\":[{\"location\":{\"fmin\":708824,\"fmax\":709269,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":697781,\"fmax\":697848,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":698001,\"fmax\":698129,\"strand\": [...]
+            ",{\"location\":{\"fmin\":713033,\"fmax\":718259,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40824-RA\",\"children\":[{\"location\":{\"fmin\":713033,\"fmax\":713117,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":713645,\"fmax\":713724,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":713941,\"fmax\":714142,\"strand\":1},\"type\":{\"cv [...]
+            ",{\"location\":{\"fmin\":751352,\"fmax\":762365,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40830-RA\",\"children\":[{\"location\":{\"fmin\":751352,\"fmax\":751543,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":751994,\"fmax\":752145,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":757029,\"fmax\":757287,\"strand\":1},\"type\":{\"cv [...]
+            ",{\"location\":{\"fmin\":787022,\"fmax\":836988,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40740-RA\",\"children\":[{\"location\":{\"fmin\":787022,\"fmax\":787740,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":787022,\"fmax\":788349,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":789768,\"fmax\":790242,\"strand\":-1},\"type\":{ [...]
+//            ",{\"location\":{\"fmin\":787022,\"fmax\":836988,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40740-RA\",\"children\":[{\"location\":{\"fmin\":787022,\"fmax\":787740,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":787022,\"fmax\":788349,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":789768,\"fmax\":790242,\"strand\":-1},\"type\" [...]
+            ",{\"location\":{\"fmin\":1304894,\"fmax\":1347482,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40714-RA\",\"children\":[{\"location\":{\"fmin\":1304894,\"fmax\":1304979,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1347453,\"fmax\":1347482,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1304894,\"fmax\":1347482,\"strand\":-1},\" [...]
+            ",{\"location\":{\"fmin\":1077584,\"fmax\":1077828,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40842-RA\",\"children\":[{\"location\":{\"fmin\":1077584,\"fmax\":1077828,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}" +
+            ",{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1224676,\"fmax\":1224823,\"strand\":1},\"type [...]
+            ",{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1224676,\"fmax\":1224823,\"strand\":1},\"type [...]
+            ",{\"location\":{\"fmin\":524298,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40818-RA\",\"children\":[{\"location\":{\"fmin\":524298,\"fmax\":524303,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":572204,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":524298,\"fmax\":525125,\"strand\":1},\"type\":{\"cv [...]
+            ",{\"location\":{\"fmin\":524298,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40818-RA\",\"children\":[{\"location\":{\"fmin\":524298,\"fmax\":524303,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":572204,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":524298,\"fmax\":525125,\"strand\":1},\"type\":{\"cv [...]
+
+//    String jsonArrayString = "[{\"location\":{\"fmin\":1077584,\"fmax\":1077828,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40842-RA\",\"children\":[{\"location\":{\"fmin\":1077584,\"fmax\":1077828,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}]"
+//    String jsonArrayString = "[{\"location\":{\"fmin\":524298,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40818-RA\",\"children\":[{\"location\":{\"fmin\":524298,\"fmax\":524303,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":572204,\"fmax\":572691,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":524298,\"fmax\":525125,\"strand\ [...]
+
+    def jsonArray = new JsonSlurper().parseText(jsonArrayString)
+//	def jsonArray = new JsonSlurper().parseText("[{\"name\":\"asdf\"}]")
+
+    return jsonArray
+}
+
diff --git a/docs/web_services/examples/groovy/add_organism.groovy b/docs/web_services/examples/groovy/add_organism.groovy
new file mode 100755
index 0000000..60dd23a
--- /dev/null
+++ b/docs/web_services/examples/groovy/add_organism.groovy
@@ -0,0 +1,85 @@
+#!/usr/bin/env groovy
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+import groovyx.net.http.RESTClient
+import org.json.JSONObject
+
+
+String usageString = "add_organism.groovy <options>\n" +
+        "Example: " +
+        "./add_organism.groovy -name yeast -url http://localhost:8080/apollo/ -directory /opt/apollo/yeast -public\n"+
+        "which would prompt for user/pass\n"+
+        "-or-\n"+
+        "./add_organism.groovy -name yeast -url http://localhost:8080/apollo/ -directory /opt/apollo/yeast -username user at site.com -password secret -public"
+
+def cli = new CliBuilder(usage: usageString)
+cli.setStopAtNonOption(true)
+cli.url('URL to Apollo instance', required: true, args: 1)
+cli.name('organism common name', required: true, args: 1)
+cli.directory('jbrowse data directory', required: true, args: 1)
+cli.blatdb('blatdb directory', args: 1)
+cli.genus('genus', args: 1)
+cli.public('public', args: 0)
+cli.species('species', args: 1)
+cli.username('username', required: false, args: 1)
+cli.password('password', required: false, args: 1)
+cli.returnAllOrganisms('returnAllOrganisms (default true)', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+def admin_username
+def admin_password 
+try {
+    options = cli.parse(args)
+
+    if (!(options?.url && options?.name && options?.directory)) {
+        return
+    }
+
+    def cons = System.console()
+    if (!(admin_username=options?.username)) {
+        admin_username = new String(cons.readPassword('Enter admin username: ') )
+    }
+    if (!(admin_password=options?.password)) {
+        admin_password = new String(cons.readPassword('Enter admin password: ') )
+    }
+
+} catch (e) {
+    println(e)
+    return
+}
+
+
+def s=options.url
+if (s.endsWith("/")) {
+    s = s.substring(0, s.length() - 1);
+}
+
+URL url = new URL(s)
+
+
+def argumentsArray = [
+        commonName: options.name,
+        directory : options.directory,
+        username  : admin_username,
+        password  : admin_password,
+        blatdb    : options.blatdb ?: null,
+        genus     : options.genus ?: null,
+        species   : options.species ?: null,
+        publicMode: options.public,
+        returnAllOrganisms : options.returnAllOrganisms ?: true 
+]
+
+def client = new RESTClient(options.url)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String fullPath = "${url.path}/organism/addOrganism"
+
+def resp = client.post(
+        contentType: 'text/javascript',
+        path: fullPath,
+        body: argumentsArray
+)
+
+assert resp.status == 200  // HTTP response code; 404 means not found, etc.
+println resp.getData()
diff --git a/docs/web_services/examples/groovy/add_users.groovy b/docs/web_services/examples/groovy/add_users.groovy
new file mode 100755
index 0000000..4ead1e8
--- /dev/null
+++ b/docs/web_services/examples/groovy/add_users.groovy
@@ -0,0 +1,117 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+evaluate(new File("${scriptDir}/Apollo2Operations.groovy"))
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+ at Grab(group = 'org.apache.commons', module = 'commons-lang3', version = '3.0')
+
+
+String usageString = "add_users.groovy <options>\n" +
+        "Example: \n" +
+        "./add_users.groovy -inputfile somefile.csv -destinationurl http://localhost:8080/Apollo/\n" +
+        "./add_users.groovy -firstName New -lastName User -newuser newuser at test.com -newpassword newuserpass -destinationurl http://localhost:8080/Apollo/\n"
+
+def cli = new CliBuilder(usage: 'add_users.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.inputfile('CSV file with format <email>,<firstname>,<lastname>,<password>,<role>', required: false, args: 1)
+cli.username('Admin password', required: false, args: 1)
+cli.password('Admin username', required: false, args: 1)
+cli.firstName('firstName', required: false, args: 1)
+cli.lastName('lastName', required: false, args: 1)
+cli.newuser('New user name (if not from csv)',required: false, args: 1)
+cli.newpassword('New user password (if not from csv)',required: false, args: 1)
+cli.newrole('New user role (if not from csv)',required: false, args: 1)
+cli.destinationurl('Apollo URL', required: true, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+
+def admin_username
+def admin_password
+try {
+    options = cli.parse(args)
+
+    if (!(options?.destinationurl)) {
+        println "Requires destination URL\n" + usageString
+        return
+    }
+
+    if(!options?.inputfile && !options?.newuser) {
+        println "Requires CSV or user on command line\n" + usageString
+        return
+    }
+
+    def cons = System.console()
+    if (!(admin_username=options?.username)) {
+        admin_username = new String(cons.readPassword('Enter admin username: ') )
+    }
+    if (!(admin_password=options?.password)) {
+        admin_password = new String(cons.readPassword('Enter admin password: ') )
+    }
+
+} catch (e) {
+    println(e)
+    return
+}
+
+
+JSONArray usersArray = new JSONArray()
+
+if(options.inputfile) {
+    new File(options.inputfile).splitEachLine(",") { fields ->
+        JSONObject user = new JSONObject()
+        user.email = fields[0]
+        user.firstName = fields[1]
+        user.lastName = fields[2]
+        user.password = fields[3] ?: 'default'
+        user.role = fields[4] ?: 'user'
+        usersArray.add(user)
+    }
+}
+else {
+    JSONObject user = new JSONObject()
+    user.email = options?.newuser
+    user.password = options?.newpassword ?: 'default'
+    user.role = options?.newrole ?: 'user'
+    user.firstName = options?.firstName ?: 'N/A'
+    user.lastName = options?.lastName ?: 'N/A'
+    usersArray.add(user)
+}
+
+
+def s=options.destinationurl
+if (s.endsWith("/")) {
+    s = s.substring(0, s.length() - 1);
+}
+
+URL url = new URL(s)
+
+def client = new RESTClient(options.destinationurl)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String fullPath = "${url.path}/user/createUser"
+
+for (user in usersArray) {
+    def userArgument = [
+        email    : user.email,
+        firstName: user.firstName,
+        lastName : user.lastName,
+        role     : user.role,
+        username : admin_username,
+        password : admin_password,
+        newPassword: user.password
+    ]
+
+    def resp = client.post(
+        contentType: 'text/javascript',
+        path: fullPath,
+        body: userArgument
+    )
+
+    if(resp.data.error) println user.email+": "+resp.data.error
+    assert resp.status == 200  // HTTP response code; 404 means not found, etc.
+}
+
+
diff --git a/docs/web_services/examples/groovy/alter_group_permissions.groovy b/docs/web_services/examples/groovy/alter_group_permissions.groovy
new file mode 100755
index 0000000..d31f09e
--- /dev/null
+++ b/docs/web_services/examples/groovy/alter_group_permissions.groovy
@@ -0,0 +1,164 @@
+#!/usr/bin/env groovy
+import groovyjarjarcommonscli.Option
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+ at Grab(group = 'org.apache.commons', module = 'commons-lang3', version = '3.0')
+
+String usageString = "\nUSAGE: alter_group_permissions.groovy <options>\n" +
+        "Permissions: ADMINISTRATE,WRITE,EXPORT,READ (lower permissions are inherited)\n" +
+        "Example:\n" +
+        "./alter_group_permissions.groovy -inputfile group_to_permissions.csv -destinationurl http://localhost:8080/Apollo\n" +
+        "./alter_group_permissions.groovy -groupname group1 -organism organism_name -permission READ:WRITE -destinationurl http://localhost:8080/Apollo"
+
+def cli = new CliBuilder(usage: 'alter_group_permissions.groovy')
+cli.setStopAtNonOption(true)
+cli.inputfile('CSV file with format <groupname>,<organism>,<permissions>', required: false, args: 1)
+cli.organism('Organism for which the current group should be granted permissions', required: false, args: 1)
+cli.permission('Permission(s) to be granted for group on organism, separated by \':\'', required: false, args: 1)
+cli.groupname('groupName for a group', required: false, args: 1)
+cli.destinationurl('Apollo URL', required: true, args: 1)
+cli.adminusername('Admin username', required: false, args: 1)
+cli.adminpassword('Admin password', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+
+def admin_username
+def admin_password
+def groupPermissionMap = [:]
+
+try {
+    options = cli.parse(args)
+    if (!(options?.destinationurl)) {
+        println "NOTE: Requires destination URL\n" + usageString
+        return
+    }
+    if (!options?.inputfile && !options?.groupname) {
+        println "NOTE: Requires a CSV as inputfile   OR   groupname, organism and permissions as arguments\n" + usageString
+        return
+    }
+    if (options?.inputfile && options?.groupname) {
+        println "NOTE: Requires a CSV as inputfile   OR   groupname, organism and permissions as arguments\n" + usageString
+        return
+    }
+    if (options?.permission && !options?.organism) {
+        println "NOTE: Need organism as argument for updating permissions\n" + usageString
+        return
+    }
+
+    def sysConsole = System.console()
+    if (!(admin_username=options?.adminusername)) {
+        admin_username = new String(sysConsole.readLine('Enter admin username: '))
+    }
+    if (!(admin_password=options?.adminpassword)) {
+        admin_password = new String(sysConsole.readPassword('Enter admin password: '))
+    }
+} catch(e) {
+    println(e)
+    return
+}
+
+if (options.inputfile) {
+    groupPermissionMap = parseInputFile(options.inputfile)
+}
+else {
+    JSONObject groupObject = new JSONObject()
+    groupObject.groupname = options.groupname
+    groupObject.organism = options.organism
+    groupObject.ADMINISTRATE = false
+    groupObject.EXPORT = false
+    groupObject.READ = false
+    groupObject.WRITE = false
+    def permissionArray = options?.permission ? options.permission.split(':') : []
+    permissionArray.each {
+        if (it == 'ADMINISTRATE') {groupObject.ADMINISTRATE = true}
+        else if (it == 'EXPORT') {groupObject.EXPORT = true}
+        else if (it == 'READ') {groupObject.READ = true}
+        else if (it == 'WRITE') {groupObject.WRITE = true}
+    }
+    groupPermissionMap.put(options.groupname, groupObject)
+}
+
+def s=options.destinationurl
+if (s.endsWith("/")) {
+    s = s.substring(0, s.length() - 1)
+}
+
+URL url = new URL(s)
+def client = new RESTClient(options.destinationurl)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String path = "${url.path}/group/updateOrganismPermission"
+
+for (String groupName in groupPermissionMap.keySet()) {
+    println "Processing group: ${groupName}"
+    JSONObject groupObject = groupPermissionMap.get(groupName) as JSONObject
+    if ((options?.permission && !options?.inputfile) || (!options?.permission && options?.inputfile)) {
+        def userArgument = [
+                name: groupObject.groupname,
+                organism: groupObject.organism,
+                ADMINISTRATE: groupObject.ADMINISTRATE,
+                EXPORT: groupObject.EXPORT,
+                READ: groupObject.READ,
+                WRITE: groupObject.WRITE,
+                username: admin_username,
+                password: admin_password
+        ]
+        def response = client.post(
+                contentType: 'text/javascript',
+                path: path,
+                body: userArgument
+        )
+        if (response.data.error) {
+            println "Error while altering permissions for group: ${groupName}\n${response.data.error}"
+        }
+        assert response.status == 200
+    }
+}
+
+def parseInputFile(String inputFile) {
+    def permissionMap = [:]
+    new File(inputFile).splitEachLine(',') { fields ->
+        if (fields.size() != 3) {
+            println "ERROR: Improper formatting in ${inputFile} at line:\n${fields.join(',')}"
+            return
+        }
+        JSONObject groupPermissionObject = new JSONObject()
+        groupPermissionObject.ADMINISTRATE = false
+        groupPermissionObject.EXPORT = false
+        groupPermissionObject.READ = false
+        groupPermissionObject.WRITE = false
+        String groupName  = fields[0]
+        if (permissionMap.containsKey(groupName)) {
+            println "Duplicate entries for groupName: ${groupName}"
+        }
+        else {
+            permissionMap.put(groupName, groupPermissionObject)
+        }
+        permissionMap.get(groupName).groupname = groupName
+        String organism = fields[1]
+        def permissionArray = fields[2].split(':')
+        permissionArray.each {
+            if (it == 'ADMINISTRATE') {
+                permissionMap.get(groupName).ADMINISTRATE = true
+            }
+            else if (it == 'EXPORT') {
+                permissionMap.get(groupName).EXPORT = true
+            }
+            else if (it == 'READ') {
+                permissionMap.get(groupName).READ = true
+            }
+            else if (it == 'WRITE') {
+                permissionMap.get(groupName).WRITE = true
+            }
+            else {
+                println "Unrecognized permission found for groupName: ${groupName}"
+                System.exit(1)
+            }
+        }
+        permissionMap.get(groupName).organism = organism
+    }
+    return permissionMap
+}
\ No newline at end of file
diff --git a/docs/web_services/examples/groovy/alter_user_groups_and_permissions.groovy b/docs/web_services/examples/groovy/alter_user_groups_and_permissions.groovy
new file mode 100755
index 0000000..777c761
--- /dev/null
+++ b/docs/web_services/examples/groovy/alter_user_groups_and_permissions.groovy
@@ -0,0 +1,228 @@
+#!/usr/bin/env groovy
+import groovyjarjarcommonscli.Option
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+ at Grab(group = 'org.apache.commons', module = 'commons-lang3', version = '3.0')
+
+String usageString = "\nUSAGE: alter_user_groups_and_permissions.groovy <options>\n" +
+        "Example:\n" +
+        "./alter_user_groups_and_permissions.groovy -inputfile user_to_permissions.csv -destinationurl http://localhost:8080/Apollo/\n" +
+        "./alter_user_groups_and_permissions.groovy -user test at admin.gov -organism organism_name -permission READ:WRITE -destinationurl http://localhost:8080/Apollo/\n" +
+        "./alter_user_groups_and_permissions.groovy -user test at admin.gov -addToGroup group1 -removeFromGroup group2:group3 -destinationurl http://localhost:8080/Apollo/\n" +
+        "./alter_user_groups_and_permissions.groovy -user test at admin.gov -organism organism_name -permission READ:WRITE -addToGroup group1 -removeFromGroup group2 -destinationurl http://localhost:8080/Apollo/"
+
+def cli = new CliBuilder(usage: 'alter_user_groups_and_permissions.groovy')
+cli.setStopAtNonOption(true)
+cli.inputfile('CSV file with format <username/email>,<organism>,<permissions>,<groups to add user to>,<groups to remove user from>', required: false, args: 1)
+cli.user('email/username for a user', required: false, args: 1)
+cli.organism('Organism for which permissions have to be altered for username', required: false, args: 1)
+cli.permission('Permission(s) to be granted for username on organism, separated by \':\'.  They can be READ, WRITE, EXPORT, ADMINISTRATE', required: false, args: 1)
+cli.addToGroup('Add user to group(s), separated by \':\'', required: false, args: 1)
+cli.removeFromGroup('Remove user from group(s), separated by \':\'', required: false, args: 1)
+cli.destinationurl('Apollo URL', required: true, args: 1)
+cli.adminusername('Admin username', required: false, args: 1)
+cli.adminpassword('Admin password', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+
+def admin_username
+def admin_password
+def userPermissionMap = [:]
+
+try {
+    options = cli.parse(args)
+    if (!(options?.destinationurl)) {
+        println "NOTE: Requires destination URL\n" + usageString
+        return
+    }
+    if (!options?.inputfile && !options?.user) {
+        println "NOTE: Requires a CSV as inputfile\nOR\n user, organism and permission as arguments\nOR\n user, addToGroup, removeFromGroup\n" + usageString
+        return
+    }
+    if (options?.inputfile && options?.user) {
+        println "NOTE: Requires a CSV as inputfile\nOR\n user, organism and permission as arguments\nOR\n user, addToGroup, removeFromGroup\n" + usageString
+        return
+    }
+    if (options?.permission && !options?.organism) {
+        println "NOTE: Need organism as argument for updating permissions\n" + usageString
+        return
+    }
+    def sysConsole = System.console()
+    if (!(admin_username=options?.adminusername)) {
+        admin_username = new String(sysConsole.readLine('Enter admin username: '))
+    }
+    if (!(admin_password=options?.adminpassword)) {
+        admin_password = new String(sysConsole.readPassword('Enter admin password: '))
+    }
+    
+} catch(e) {
+    println(e)
+    return
+}
+
+if (options.inputfile) {
+    userPermissionMap = parseInputFile(options.inputfile)
+}
+else {
+    JSONObject userObject = new JSONObject()
+    userObject.organism = options.organism
+    userObject.ADMINISTRATE = false
+    userObject.EXPORT = false
+    userObject.READ = false
+    userObject.WRITE = false
+    userObject.addtogroup = options?.addToGroup ? options.addToGroup.split(':') : []
+    userObject.removefromgroup = options?.removeFromGroup ? options.removeFromGroup.split(':') : []
+    def permissionArray = options?.permission ? options.permission.split(':') : []
+    permissionArray.each {
+        if (it == 'ADMINISTRATE') {userObject.ADMINISTRATE = true}
+        else if (it == 'EXPORT') {userObject.EXPORT = true}
+        else if (it == 'READ') {userObject.READ = true}
+        else if (it == 'WRITE') {userObject.WRITE = true}
+    }
+    userPermissionMap.put(options.user, userObject)
+}
+
+def s=options.destinationurl
+if (s.endsWith("/")) {
+    s = s.substring(0, s.length() - 1)
+}
+
+URL url = new URL(s)
+def client = new RESTClient(options.destinationurl)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String updateOrganismPermissionPath = "${url.path}/user/updateOrganismPermission"
+String addUserToGroupPath = "${url.path}/user/addUserToGroup"
+String removeUserFromGroupPath = "${url.path}/user/removeUserFromGroup"
+
+for (user in userPermissionMap.keySet()) {
+    println "Processing user: ${user}"
+    JSONObject userObject = userPermissionMap.get(user) as JSONObject
+    if ((options?.permission && !options?.inputfile) || (!options?.permission && options?.inputfile)) {
+        updateOrganismPermission(user, userObject, updateOrganismPermissionPath, client, admin_username, admin_password)
+    }
+    if ((options?.addToGroup && !options?.inputfile) || (!options?.addToGroup && options?.inputfile)) {
+        addUserToGroup(user, userObject, addUserToGroupPath, client, admin_username, admin_password)
+    }
+    if ((options?.removeFromGroup && !options?.inputfile) || (!options?.removeFromGroup && options?.inputfile)) {
+        removeUserFromGroup(user, userObject, removeUserFromGroupPath, client, admin_username, admin_password)
+    }
+}
+
+def updateOrganismPermission(String user, JSONObject userObject, String path, RESTClient client, String username, String password) {
+    def userArgument = [
+            user: user,
+            organism: userObject.organism,
+            ADMINISTRATE: userObject.ADMINISTRATE,
+            EXPORT: userObject.EXPORT,
+            READ: userObject.READ,
+            WRITE: userObject.WRITE,
+            username: username,
+            password: password
+    ]
+    def response = client.post(
+            contentType: 'text/javascript',
+            path: path,
+            body: userArgument
+    )
+    
+    if (response.data.error) {
+        println "Error while altering permissions for user: ${user}\n${response.data.error}"
+    }
+    assert response.status == 200
+}
+
+def addUserToGroup(String user, JSONObject userObject, String path, RESTClient client, String username, String password) {
+    for (String group : userObject.addtogroup) {
+        if (group == '' || group == null) {continue}
+        def userArgument = [
+                user: user,
+                group: group,
+                username: username,
+                password: password
+        ]
+
+        def response = client.post(
+                contentType: 'text/javascript',
+                path: path,
+                body: userArgument
+        )
+        
+        if (response.data.error) {
+            println "Error while adding user ${user.user} to group ${group}\n${response.data.error}"
+        }
+        assert response.status == 200
+    }
+}
+
+def removeUserFromGroup(String user, JSONObject userObject, String path, RESTClient client, String username, String password) {
+    for (String group : userObject.removefromgroup) {
+        if (group == '' || group == null) {continue}
+        def userArgument = [
+                user: user,
+                group: group,
+                username: username,
+                password: password
+        ]
+
+        def response = client.post(
+                contentType: 'text/javascript',
+                path: path,
+                body: userArgument
+        )
+
+        if (response.data.error) {
+            println "Error while removing user ${user.user} from group ${group}\n${response.data.error}"
+        }
+        assert response.status == 200
+    }
+}
+
+def parseInputFile(String inputFile) {
+    def permissionMap = [:]
+    new File(inputFile).splitEachLine(',') { fields ->
+        if (fields.size() != 5) {
+            println "ERROR: Improper formatting in ${inputFile} at line:\n${fields.join(',')}"
+            return
+        }
+        JSONObject userPermissionObject = new JSONObject()
+        userPermissionObject.ADMINISTRATE = false
+        userPermissionObject.EXPORT = false
+        userPermissionObject.READ = false
+        userPermissionObject.WRITE = false
+        String userName = fields[0]
+        if (permissionMap.containsKey(userName)) {
+            println "Duplicate entries for user: ${userName}"
+        }
+        else {
+            permissionMap.put(userName, userPermissionObject)
+        }
+        String organism = fields[1]
+        def permissionArray = fields[2].split(':')
+        permissionArray.each { 
+            if (it == 'ADMINISTRATE') {
+                permissionMap.get(userName).ADMINISTRATE = true
+            }
+            else if (it == 'EXPORT') {
+                permissionMap.get(userName).EXPORT = true
+            }
+            else if (it == 'READ') {
+                permissionMap.get(userName).READ = true
+            }
+            else if (it == 'WRITE') {
+                permissionMap.get(userName).WRITE = true
+            }
+            else {
+                println "Unrecognized permission found for user: ${userName}"
+                System.exit(1)
+            }
+        }
+        permissionMap.get(userName).organism = organism
+        permissionMap.get(userName).addtogroup = fields[3].split(':')
+        permissionMap.get(userName).removefromgroup = fields[4].split(':')
+    }
+    return permissionMap
+}
\ No newline at end of file
diff --git a/docs/web_services/examples/groovy/delete_annotations_from_organism.groovy b/docs/web_services/examples/groovy/delete_annotations_from_organism.groovy
new file mode 100755
index 0000000..da8eff2
--- /dev/null
+++ b/docs/web_services/examples/groovy/delete_annotations_from_organism.groovy
@@ -0,0 +1,78 @@
+#!/usr/bin/env groovy
+import groovyjarjarcommonscli.Option
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+//@Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.1')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.5.2')
+ at Grab(group = 'org.apache.commons', module = 'commons-lang3', version = '3.0')
+
+String usageString = "\nUSAGE: delete_annotations_from_organism.groovy <options>\n" +
+        "Example (will prompt if 'adminusername' and 'adminpassword' are not provided):\n" +
+        "./delete_annotations_from_organism.groovy -organismname organism_name -destinationurl http://localhost:8080/Apollo\n" +
+        "./delete_annotations_from_organism.groovy -organismid 123 -destinationurl http://localhost:8080/Apollo"
+        "./delete_annotations_from_organism.groovy -organismid 123 -destinationurl http://localhost:8080/Apollo -adminusername bob at gov.com -adminpassword demo"
+
+def cli = new CliBuilder(usage: 'delete_annotations_from_organism.groovy')
+cli.setStopAtNonOption(true)
+cli.organismid('Organism Id corresponding to organism', required: false, args: 1)
+cli.organismname('Common name for the organism (if organismid is not provided)', required: false, args:1)
+cli.destinationurl('Apollo URL', required: true, args: 1)
+cli.adminusername('Admin username', required: false, args: 1)
+cli.adminpassword('Admin password', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+
+def admin_username
+def admin_password
+
+try {
+    options = cli.parse(args)
+    if (!options?.destinationurl) {
+        println "NOTE: Requires destination URL\n" + usageString
+        return
+    }
+    if (!options?.organismid && !options?.organismname) {
+        println "NOTE: Requires organismid or organismname as an argument\n" + usageString
+        return
+    }
+    def sysConsole = System.console()
+    if (!(admin_username=options?.adminusername)) {
+        admin_username = new String(sysConsole.readLine('Enter admin username: '))
+    }
+    if (!(admin_password=options?.adminpassword)) {
+        admin_password = new String(sysConsole.readPassword('Enter admin password: '))
+    }
+} catch(e) {
+    println(e)
+    return
+}
+
+def s=options.destinationurl
+if (s.endsWith("/")) {
+    s = s.substring(0, s.length() - 1)
+}
+
+URL url = new URL(s)
+def client = new RESTClient(options.destinationurl)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String path = "${url.path}/organism/deleteOrganismFeatures"
+
+def userArgument = [
+        organism: options.organismid ? options.organismid : options.organismname, 
+        username: admin_username,
+        password: admin_password
+]
+
+def response = client.post(
+        contentType: 'text/javascript',
+        path: path,
+        body: userArgument
+)
+
+if (response.data.error) {
+    println "Error while deleting features for organism\n${response.data.error}"
+}
+assert response.status == 200
diff --git a/docs/web_services/examples/groovy/export_annotations_to_chado.groovy b/docs/web_services/examples/groovy/export_annotations_to_chado.groovy
new file mode 100755
index 0000000..91a462d
--- /dev/null
+++ b/docs/web_services/examples/groovy/export_annotations_to_chado.groovy
@@ -0,0 +1,79 @@
+#!/usr/bin/env groovy
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+import groovyx.net.http.RESTClient
+import org.json.JSONObject
+
+String usageString = "Example: " +
+        "./export_annotations_to_chado.groovy -organism Amel -url http://localhost:8080/apollo\n" +
+        "which would prompt username and password\n"
+        "-or-\n" +
+        "./export_annotations_to_chado.groovy -username user at site.com -password secret -organism Amel -url http://localhost:8080/apollo\n"
+
+def cli = new CliBuilder(usage: "export_annotations_to_chado.groovy <options>")
+cli.setStopAtNonOption(true)
+cli.organism('Organism common name', required: true, args: 1)
+cli.url('URL to Apollo instance', required: true, args: 1)
+cli.username('username', required: false, args: 1)
+cli.password('password', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+
+OptionAccessor options
+def admin_username
+def admin_password
+
+try {
+        options = cli.parse(args)
+        if (!(options.url && options.organism)) {
+                println usageString
+                return
+        }
+
+        def cons = System.console()
+        if (!(admin_username = options?.username)) {
+                admin_username = new String(cons.readPassword('Enter admin username: '))
+        }
+        if (!(admin_password = options?.password)) {
+                admin_password = new String(cons.readPassword('Enter admin password: '))
+        }
+} catch (e) {
+        println(e)
+        return
+}
+
+def urlString = options.url
+if (urlString.endsWith('/')) {
+        urlString = urlString.substring(0, urlString.length() - 1)
+}
+
+URL url = new URL(urlString)
+
+def sequencesList = []
+
+def argumentsArray = [
+        username: admin_username,
+        password: admin_password,
+        organism: options.organism,
+        type: 'CHADO',
+        seqType: '',
+        exportGff3Fasta: '',
+        output: '',
+        format: '',
+        sequences: [],
+        exportAllSequences: true
+]
+
+def client = new RESTClient(options.url)
+if (options.ignoressl) { client.ignoreSSLIssues() }
+String fullPath = "${url.path}/IOService/write"
+
+def response = client.post(
+        contentType: 'text/javascript',
+        path: fullPath,
+        body: argumentsArray
+)
+
+assert response.status == 200
+println response.getData()
\ No newline at end of file
diff --git a/docs/web_services/examples/groovy/get_fasta.groovy b/docs/web_services/examples/groovy/get_fasta.groovy
new file mode 100755
index 0000000..59c4873
--- /dev/null
+++ b/docs/web_services/examples/groovy/get_fasta.groovy
@@ -0,0 +1,99 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+String usageString = "get_fasta.groovy <options>" +
+        "Example: \n" +
+        "./get_fasta.groovy -username admin -password somepass -url http://localhost/apollo "
+
+def cli = new CliBuilder(usage: 'get_fasta.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.url('URL of Apollo from which FASTA is to be fetched', required: true, args: 1)
+cli.username('username', required: false, args: 1)
+cli.password('password', required: false, args: 1)
+cli.password('url', required: false, args: 1)
+cli.seqtype('seqtype', required: true, args: 1)
+cli.output('output file', required: false, args: 1)
+cli.organism('organism', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+def admin_username
+def admin_password
+try {
+    options = cli.parse(args)
+
+    if (!(options?.url)) {
+        println "Requires destination URL\n" + usageString
+        return
+    }
+
+    def cons = System.console()
+    if(cons) {
+        if (!(admin_username=options?.username)) {
+            admin_username = new String(cons.readPassword('Username: ') )
+        }
+        if (!(admin_password=options?.password)) {
+            admin_password = new String(cons.readPassword('Password: ') )
+        }
+    }
+    else if(!options.username||!options.password){
+        System.err.println("Error: missing -username and -password and can't read them when using redirect");
+        if(!options.output) throw "Require output file"  
+    }
+    else {
+        admin_username=options.username
+        admin_password=options.password
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+// just get data
+def client = new RESTClient(options.url,'text/plain')
+if (options.ignoressl) { client.ignoreSSLIssues() }
+def post = [
+    username: admin_username,
+    password: admin_password,
+    format: 'plain',
+    seqType: options.seqtype,
+    type: 'FASTA',
+    exportSequence: false,
+    exportAllSequences: true,
+    organism: options.organism,
+    output:'text'
+]
+def response = client.post(path:options.url+'/IOService/write',body: post)
+
+assert response.status == 200
+
+
+StringBuilder builder = new StringBuilder();                                                                        
+int charsRead = -1;                                                                                                 
+char[] chars = new char[100];                                                                                       
+charsRead = response.data.read(chars,0,chars.length);                                                               
+while(charsRead>0){                                                                                                 
+    //if we have valid chars, append them to end of string.                                                         
+    builder.append(chars,0,charsRead);                                                                              
+    charsRead = response.data.read(chars,0,chars.length);                                                           
+} 
+
+if(options.output) {
+    def file=new File(options.output)
+    def writer = new PrintWriter(file)
+    writer.println builder.toString();  
+    writer.close()
+}
+else {
+    print builder.toString();
+}
+
+
+
diff --git a/docs/web_services/examples/groovy/get_gff3.groovy b/docs/web_services/examples/groovy/get_gff3.groovy
new file mode 100755
index 0000000..82f2958
--- /dev/null
+++ b/docs/web_services/examples/groovy/get_gff3.groovy
@@ -0,0 +1,82 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+
+import groovyx.net.http.HTTPBuilder
+
+
+String usageString = "get_gff3.groovy <options>" +
+        "Example: \n" +
+        "./get_gff3.groovy -username admin -password somepass -organism honeybee -url http://localhost/apollo "
+
+def cli = new CliBuilder(usage: 'get_gff3.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.url('URL of Apollo from which GFF3 is to be fetched', required: true, args: 1)
+cli.username('username', required: false, args: 1)
+cli.password('password', required: false, args: 1)
+cli.output('output file', required: false, args: 1)
+cli.organism('organism', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+def admin_username
+def admin_password
+try {
+    options = cli.parse(args)
+
+    if (!(options?.url)) {
+        println "Requires destination URL\n" + usageString
+        return
+    }
+
+    def cons = System.console()
+    if(cons) {
+        if (!(admin_username=options?.username)) {
+            admin_username = new String(cons.readPassword('Username: ') )
+        }
+        if (!(admin_password=options?.password)) {
+            admin_password = new String(cons.readPassword('Password: ') )
+        }
+    }
+    else if(!options?.username||!options?.password) {
+        System.err.println("Error: missing -username and -password and can't read them when using redirect");
+        if(!options.output) throw "Require output file"
+    }
+    else {
+        admin_password=options.password
+        admin_username=options.username
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+
+def http = new HTTPBuilder(options.url)
+
+
+// just get data
+def post=[
+    username: admin_username,
+    password: admin_password,
+    format: 'plain',
+    type: 'GFF3',
+    exportSequence: false,
+    exportAllSequences: true,
+    organism: options.organism,
+    output:'text'
+]
+
+http.get(path: options.url+'/IOService/write/',query: post) { resp, reader ->
+  if(options.output) {
+      def file=new File(options.output)
+      def writer = new PrintWriter(file)
+      writer << reader
+      writer.close()
+  }
+  else
+      System.out << reader
+}
+
diff --git a/docs/web_services/examples/groovy/get_new_features.groovy b/docs/web_services/examples/groovy/get_new_features.groovy
new file mode 100755
index 0000000..78988a3
--- /dev/null
+++ b/docs/web_services/examples/groovy/get_new_features.groovy
@@ -0,0 +1,93 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+String usageString = "get_new_features.groovy <options>" +
+        "Example: \n" +
+        "./get_new_features.groovy -username admin -password somepass -url http://localhost/apollo -date 2014-05-17:00:00:00 -afterDate=true -output thisFile.txt"
+
+def cli = new CliBuilder(usage: 'get_fasta.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.username('username', required: false, args: 1)
+cli.password('password', required: false, args: 1)
+cli.url('url', required: true, args: 1)
+cli.date('date', required: true, args: 1)
+cli.afterDate('afterDate', required: false, args: 1)
+cli.output('output', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+def admin_username
+def admin_password
+try {
+    options = cli.parse(args)
+
+    if (!(options?.url)) {
+        println "Requires destination URL\n" + usageString
+        return
+    }
+
+    def cons = System.console()
+    if(cons) {
+        if (!(admin_username=options?.username)) {
+            admin_username = new String(cons.readPassword('Username: ') )
+        }
+        if (!(admin_password=options?.password)) {
+            admin_password = new String(cons.readPassword('Password: ') )
+        }
+    }
+    else if(!options.username||!options.password){
+        System.err.println("Error: missing -username and -password and can't read them when using redirect");
+        if(!options.output) throw "Require output file"  
+    }
+    else {
+        admin_username=options.username
+        admin_password=options.password
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+// just get data
+def client = new RESTClient(options.url,'text/plain')
+if (options.ignoressl) { client.ignoreSSLIssues() }
+def post = [
+    username: admin_username,
+    password: admin_password,
+    date: options.date,
+    afterDate: options.afterDate ?: true
+]
+def response = client.post(path:options.url+'/featureEvent/findChanges',body: post)
+
+assert response.status == 200
+
+
+StringBuilder builder = new StringBuilder();                                                                        
+int charsRead = -1;                                                                                                 
+char[] chars = new char[100];                                                                                       
+charsRead = response.data.read(chars,0,chars.length);                                                               
+while(charsRead>0){                                                                                                 
+    //if we have valid chars, append them to end of string.                                                         
+    builder.append(chars,0,charsRead);                                                                              
+    charsRead = response.data.read(chars,0,chars.length);                                                           
+} 
+
+if(options.output) {
+    def file=new File(options.output)
+    def writer = new PrintWriter(file)
+    writer.println builder.toString();  
+    writer.close()
+}
+else {
+    print builder.toString();
+}
+
+
+
diff --git a/docs/web_services/examples/groovy/get_usersinwa1.groovy b/docs/web_services/examples/groovy/get_usersinwa1.groovy
new file mode 100755
index 0000000..c23fd9c
--- /dev/null
+++ b/docs/web_services/examples/groovy/get_usersinwa1.groovy
@@ -0,0 +1,44 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+evaluate(new File("${scriptDir}/Apollo1Operations.groovy"))
+
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+import groovyx.net.http.RESTClient
+
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
+
+String usageString = "get_usersinwa1.groovy <options>" +
+        "Example: \n" +
+        "./get_usersinwa1.groovy -username web_apollo_users_admin -password somepass -databaseurl localhost/web_apollo_users "
+
+def cli = new CliBuilder(usage: 'get_usersinwa1.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.databaseurl('URL of Apollo 1.0.x database from which users are fetched', required: true, args: 1)
+cli.username('username', required: true, args: 1)
+cli.password('password', required: false, args: 1)
+OptionAccessor options
+
+try {
+    options = cli.parse(args)
+
+    if (!(options?.databaseurl && options?.username)) {
+        println "\n" + usageString
+        return
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+// just get data
+def users = Apollo1Operations.getUsers(options.username, options?.password ?: "", options.databaseurl)
+if (users == null) {
+    println "Could not communicate with ${options.databaseurl}"
+    return
+}
+for (user in users) {
+    println user.username
+}
diff --git a/docs/web_services/examples/groovy/migrate_annotations1to2.groovy b/docs/web_services/examples/groovy/migrate_annotations1to2.groovy
new file mode 100755
index 0000000..ca43796
--- /dev/null
+++ b/docs/web_services/examples/groovy/migrate_annotations1to2.groovy
@@ -0,0 +1,104 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+evaluate(new File("${scriptDir}/Apollo1Operations.groovy"))
+evaluate(new File("${scriptDir}/Apollo2Operations.groovy"))
+
+
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
+
+String usageString = "migrate_annotations1to2.groovy <options>" +
+        "Example: \n" +
+        "./migrate_annotations1to2.groovy -username1 demo -password1 demo -username2 ndunn at me.com -password2 demo  -sourceurl http://localhost:8080/Apollo1Instance/ -organism Honey2 -destinationurl http://localhost:8080/Apollo2/ -sequence_names Group1.26,Group1.3"
+
+def cli = new CliBuilder(usage: 'migrate_annotations.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.sourceurl('URL of Apollo 1.0.x instance from which annotations are fetched', required: true, args: 1)
+cli.destinationurl('URL of Apollo 2.0.x instance to which annotations are to be loaded', required: true, args: 1)
+cli.organism('organism common name', required: true, args: 1)
+cli.username1('username1', required: true, args: 1)
+cli.password1('password1', required: true, args: 1)
+cli.username2('username2', required: true, args: 1)
+cli.password2('password2', required: true, args: 1)
+cli.sequence_names('sequence_names', required: true, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+cli.ignore_prefix('Use this flag to NOT add the "Annotations-" prefix when pulling in Sequences', required: false)
+OptionAccessor options
+
+try {
+    options = cli.parse(args)
+
+    if (!(options?.sourceurl && options?.destinationurl && options?.organism && options?.username2 && options?.password2 && options?.username1 && options?.password1 && options?.sequence_names)) {
+        println "\n"+usageString
+        return
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+String cookieFile = "${options.username2}_cookies.txt"
+
+def responseArray = Apollo1Operations.doLogin(options.sourceurl, options.username1, options.password1,cookieFile)
+if (responseArray == null) {
+    println "Could not communicate with ${options.sourceurl}"
+    return
+}
+
+uniqueNamesMap = [:]
+featuresMap = [:]
+
+JSONObject newArray = new JSONObject()
+JSONArray addFeaturesArray = new JSONArray()
+JSONArray addTranscriptArray = new JSONArray()
+
+
+sequenceArray = options.sequence_names.tokenize(',')
+for (String sequence in sequenceArray) {
+    String sequenceName = sequence
+    def featuresResponse = Apollo1Operations.getFeature(options.sourceurl,sequenceName,cookieFile,options.ignore_prefix)
+    def featuresFromSource  = featuresResponse.features // contains list of mRNAs; Size == number of annotations on chromosome
+
+    for (def entity : featuresFromSource) {
+        JSONObject entityJSONObject = entity as JSONObject
+        newArray.location = entityJSONObject.location
+        newArray.type = entityJSONObject.type
+        newArray.name = entityJSONObject.name
+        //tmp.name = entityJSONObject.name.tokenize('-')[0]
+        newArray.children = Apollo2Operations.assignNewUniqueName(entityJSONObject.children,uniqueNamesMap)
+        if (entityJSONObject.type.name == 'repeat_region' || entityJSONObject.type.name == 'transposable_element') {
+            addFeaturesArray.add(0, newArray)
+        }
+        else {
+            addTranscriptArray.add(0, newArray)
+        }
+    }
+
+    if (addFeaturesArray.size() > 0) {
+        def response = Apollo2Operations.triggerAddFeature(options.destinationurl, options.username2, options.password2, options.organism, sequenceName, addFeaturesArray, options.ignoressl)
+        if (response == null) { return }
+        println "Migrate ${response.size()} features for ${sequence}"
+    }
+    if (addTranscriptArray.size() > 0) {
+        //println "ADDTRANSCRIPTARRAY: ${addTranscriptArray.toString()}"
+        def response = Apollo2Operations.triggerAddTranscript(options.destinationurl, options.username2, options.password2, options.organism, sequenceName, addTranscriptArray, options.ignoressl)
+        if (response == null) { return }
+        println "Migrate ${response.size()} transcripts for ${sequence}"
+    }
+
+    // keep stats
+    featuresMap.put(sequenceName, (addFeaturesArray.size() + addTranscriptArray.size()))
+    addFeaturesArray.clear()
+    addTranscriptArray.clear()
+}
+
+for(f in featuresMap){
+    println f.value + " found in " + f.key
+}
+
+
+
diff --git a/docs/web_services/examples/groovy/stress_suite.sh b/docs/web_services/examples/groovy/stress_suite.sh
new file mode 100755
index 0000000..3317b4d
--- /dev/null
+++ b/docs/web_services/examples/groovy/stress_suite.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+
+echo "Do one run just to cache the data"
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 5 -load 1 -showHeader
+echo "Starting testing"
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 5 -load 1 -showHeader
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 5 -load 2
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 5 -load 4
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 5 -load 8
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 2 -iter 5 -load 1
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 2 -iter 5 -load 2
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 2 -iter 5 -load 4
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 2 -iter 5 -load 8
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 4 -iter 5 -load 1
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 4 -iter 5 -load 2
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 4 -iter 5 -load 4
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 4 -iter 5 -load 8
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 10 -load 1
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 10 -load 2
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 10 -load 4
+./stress_test.groovy -organism Honey1 -username ndunn at me.com -password demo -destinationurl http://localhost:8080/apollo -concurrency 1 -iter 10 -load 8
diff --git a/docs/web_services/examples/groovy/stress_test.groovy b/docs/web_services/examples/groovy/stress_test.groovy
new file mode 100755
index 0000000..31ea172
--- /dev/null
+++ b/docs/web_services/examples/groovy/stress_test.groovy
@@ -0,0 +1,122 @@
+#!/usr/bin/env groovy
+scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
+evaluate(new File("${scriptDir}/SampleFeatures.groovy"))
+evaluate(new File("${scriptDir}/Apollo2Operations.groovy"))
+
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
+
+String usageString = "stress_test.groovy <options>" +
+        "Example: \n" +
+        "./stress_test.groovy -iter 100 -concurrency 3 -username ndunn at me.com -password demo  -organism Honey2 -destinationurl http://localhost:8080/Apollo2/ -load 10 -showHeader"
+
+def cli = new CliBuilder(usage: 'stress_test.groovy <options>')
+cli.setStopAtNonOption(true)
+cli.destinationurl('URL of Apollo instance to which annotations are to be loaded', required: true, args: 1)
+cli.organism('organism common name', required: true, args: 1)
+cli.username('username', required: true, args: 1)
+cli.password('password', required: true, args: 1)
+cli.concurrency('concurrent transaction', required: false, args: 1)
+cli.iter('iter', required: false, args: 1)
+cli.load('load level (0-10)', required: false, args: 1)
+cli.showHeader('Show the output header file', required: false, args: 0)
+
+OptionAccessor options
+
+try {
+    options = cli.parse(args)
+
+    if (!(options?.destinationurl && options?.organism && options?.username && options?.password)) {
+        println "\n" + usageString
+        return
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+String sequenceName = "Group1.10"
+int concurrency = options.concurrency ? Integer.parseInt(options.concurrency) : 1
+int iter = options.iter ? Integer.parseInt(options.iter) : 1
+int load = options.load ? Integer.parseInt(options.load) : 1
+Boolean showHeader = options.showHeader ? true : false
+load = load < 1 ? 1 : load
+load = load > 10 ? 10 : load
+
+
+
+def inputArray = SampleFeatures.getSampleFeatures()
+def sampleFeaturesArray = new JSONArray()
+for(int i = 0 ; i < load ; i++){
+    sampleFeaturesArray.add(inputArray[i])
+}
+int sampleFeaturesSize = sampleFeaturesArray.size()
+
+List<Long> timingsArray = new ArrayList<>()
+
+
+for (int i = 0; i < (int) iter; i++) {
+    long startTime = System.currentTimeMillis()
+
+    def threads = []
+    for (int j = 0; j < (int) concurrency; j++) {
+
+        def thread = new Thread({
+            JSONArray deleteArray = new JSONArray()
+            def response = Apollo2Operations.triggerAddTranscript(options.destinationurl, options.username, options.password, options.organism, sequenceName, sampleFeaturesArray)
+//            println "response ${response.features.collect { it.uniquename }}"
+            response.features.collect { it.uniquename }.each() { uniquename ->
+                JSONObject jsonObject = new JSONObject()
+                jsonObject.put("uniquename", uniquename)
+                deleteArray.add(jsonObject)
+            }
+            Apollo2Operations.triggerRemoveTranscript(options.destinationurl, options.username, options.password, options.organism, sequenceName, deleteArray)
+        })
+        threads << thread
+    }
+    threads.each { it.start() }
+
+    boolean threadsRunning = true
+    while(threadsRunning){
+        threadsRunning = false
+        threads.each { Thread it ->
+            if(it?.alive){
+                threadsRunning = true
+            }
+        }
+        sleep(50l)
+    }
+    long iterTime = System.currentTimeMillis() - startTime
+//    System.out.println("i: "+i+ " time: "+iterTime)
+    timingsArray.add(iterTime)
+}
+
+long totalTime = (long) timingsArray.sum()
+float meanIterTime = totalTime / (float) iter
+float variance = timingsArray.sum(){
+    Math.pow((it - meanIterTime),2)
+}
+int totalFeatures = iter * concurrency * load
+int totalTransactions = iter * concurrency
+
+//println "concurrency: ${concurrency}"
+//println "iter: ${iter}"
+//println "load: ${load}"
+if(showHeader){
+    println "concurrency,iter,load,total,mean-iter,stdv,per_iter,per_trans,per_feature"
+}
+println "${concurrency},${iter},${load},${totalTime/1000f},${meanIterTime/1000f},${Math.sqrt(variance)/1000f},${totalTime/(iter*1000f)},${totalTime/(totalTransactions*1000f)},${totalTime/(totalFeatures*1000f)}"
+
+//println("total: "+totalTime/1000f)
+//println("mean per iteration : "+meanIterTime / 1000f)
+//println("stddev: "+Math.sqrt(variance)/1000f)
+//println("per iteration : "+totalTime/(iter*1000f))
+//println("per transaction: "+totalTime/(totalTransactions*1000f))
+//println("per feature: "+totalTime/(totalFeatures*1000f))
+
+
+
diff --git a/docs/web_services/examples/groovy/test_user_file.csv b/docs/web_services/examples/groovy/test_user_file.csv
new file mode 100644
index 0000000..0ef9fa1
--- /dev/null
+++ b/docs/web_services/examples/groovy/test_user_file.csv
@@ -0,0 +1,4 @@
+bob.jones at org.gov,Bob,Jones,supersecret
+terri.smith at org.gov,Terry,Smith
+billy.bob at org.gov,Billy,Bob,demo,user
+newadmin at newadmin.com,new,admin,newadmin,admin
diff --git a/docs/web_services/examples/groovy/transfer_annotations2to2.groovy b/docs/web_services/examples/groovy/transfer_annotations2to2.groovy
new file mode 100755
index 0000000..db80651
--- /dev/null
+++ b/docs/web_services/examples/groovy/transfer_annotations2to2.groovy
@@ -0,0 +1,222 @@
+#!/usr/bin/env groovy
+import groovyx.net.http.RESTClient
+import net.sf.json.JSONArray
+import net.sf.json.JSONObject
+
+/**
+ *
+ */
+
+ at Grab(group = 'org.json', module = 'json', version = '20140107')
+ at Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7.2')
+
+String usageString = "transfer_annotations2to2.groovy <options>" +
+        "Example: \n" +
+        "./transfer_annotations2to2.groovy -username ndunn at me.com -password demo -sourceurl http://localhost:8080/apollo -source_organism amel -destinationurl http://localhost:8080/apollo2 -destination_organism amel2 -sequence_names Group1.1,Group1.10,Group1.2 "
+
+def cli = new CliBuilder(usage: 'transfer_annotations.groovyy <options>')
+cli.setStopAtNonOption(true)
+cli.sourceurl('URL of source Apollo instance from which annotations are fetched', required: true, args: 1)
+cli.destinationurl('URL of destination Apollo instance to which annotations are to be loaded', required: true, args: 1)
+cli.source_organism('source organism common name', required: true, args: 1)
+cli.destination_organism('destination organism common name', required: true, args: 1)
+cli.username('username', required: true, args: 1)
+cli.password('password', required: true, args: 1)
+cli.sequence_names('sequence_names', required: false, args: 1)
+cli.ignoressl('Use this flag to ignore SSL issues', required: false)
+OptionAccessor options
+
+try {
+    options = cli.parse(args)
+
+    if (!(options?.sourceurl && options?.destinationurl && options?.source_organism && options?.destination_organism && options?.username && options?.password)) {
+        println "\n"+usageString
+        return
+    }
+} catch (e) {
+    println(e)
+    return
+}
+
+uniqueNamesMap = [:]
+featuresMap = [:]
+JSONObject newArray = new JSONObject()
+ArrayList sequenceArray = new JSONArray()
+JSONArray addFeaturesArray = new JSONArray()
+JSONArray addTranscriptArray = new JSONArray()
+
+if (!options.sequence_names) {
+    // fetching sequences that are accessible by user
+    sequenceArray = getAllSequencesForUsername(options.sourceurl, options.username, options.password, options.source_organism, options.ignoressl)
+    if (sequenceArray == null) {
+        println "Error: Could not fetch sequences for organism ${options.source_organism}."
+        return
+    }
+}
+else {
+    sequenceArray = options.sequence_names.tokenize(',')
+}
+
+// For each sequence, fetching annotations from sourceurl
+for (String sequence in sequenceArray) {
+    String sequenceName = sequence
+    URL url = new URL(options.sourceurl)
+    String fullPath = "${url.path}/annotationEditor/getFeatures"
+    def getFeaturesClient = new RESTClient(options.sourceurl)
+    if (options.ignoressl) { getFeaturesClient.ignoreSSLIssues() }
+    def getFeaturesResponse = getFeaturesClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: ['username': options.username, 'password': options.password, 'track': sequenceName, 'organism': options.source_organism]
+    )
+
+    if (getFeaturesResponse.status != 200) {
+        println "Error: Source ${options.sourceurl} responded with ${getFeaturesResponse.status} status"
+        return
+    }
+    //println getFeaturesResponse.getData().toString()
+    if (getFeaturesResponse.getData().features == null) {
+        println "Sequence ${sequenceName} does not exist for ${options.source_organism} at source URL ${options.sourceurl}"
+        continue
+    }
+    else if (getFeaturesResponse.getData().features.size() == 0) {
+        println "Request to fetch features from Sequence ${sequenceName} for ${options.source_organism} at source URL ${options.sourceurl} did not return any features"
+        featuresMap.put(sequenceName, 0)
+        continue
+    }
+
+    def featuresFromSource  = getFeaturesResponse.getData().features // contains list of mRNAs; Size == number of annotations on chromosome
+    
+    for (def entity : featuresFromSource) {
+        JSONObject entityJSONObject = entity as JSONObject
+        newArray.location = entityJSONObject.location
+        newArray.type = entityJSONObject.type
+        newArray.name = entityJSONObject.name
+        //tmp.name = entityJSONObject.name.tokenize('-')[0]
+        newArray.children = assignNewUniqueName(entityJSONObject.children)
+        if (entityJSONObject.type.name == 'repeat_region' || entityJSONObject.type.name == 'transposable_element') {
+            addFeaturesArray.add(0, newArray)
+        }
+        else {
+            addTranscriptArray.add(0, newArray)
+        }
+    }
+    
+    if (addFeaturesArray.size() > 0) {
+        //println "ADDFEATURESARRAY: ${addFeaturesArray.toString()}"
+        def response = triggerAddFeature(options.destinationurl, options.username, options.password, options.destination_organism, sequenceName, addFeaturesArray, options.ignoressl)
+        if (response == null) { return }
+        println "addFeature response size: ${response.size()}"
+    }
+    if (addTranscriptArray.size() > 0) {
+        //println "ADDTRANSCRIPTARRAY: ${addTranscriptArray.toString()}"
+        def response = triggerAddTranscript(options.destinationurl, options.username, options.password, options.destination_organism, sequenceName, addTranscriptArray, options.ignoressl)
+        if (response == null) { return }
+        println "addTranscript response size: ${response.size()}"
+    }
+
+    featuresMap.put(sequenceName, (addFeaturesArray.size() + addTranscriptArray.size()))
+}
+
+println "\n::: STATISTICS :::\nSource URL :${options.sourceurl}\nSource Organism: ${options.source_organism}"
+println "Destination URL :${options.destinationurl}\nDestination Organism: ${options.destination_organism}"
+
+
+if (featuresMap.size() > 0) {
+    println "\nFeatures Exported from each sequence:"
+    featuresMap.each{ println "${it.key} : ${it.value}" }
+}
+
+if (uniqueNamesMap.size() > 0) {
+    println "\nold uniquename : new uniquename"
+    uniqueNamesMap.each{ println "${it.key} : ${it.value}" }
+}
+
+
+String generateUniqueName() {
+    return UUID.randomUUID().toString()
+
+}
+
+JSONArray assignNewUniqueName(JSONArray inputArray) {
+    JSONArray returnArray = new JSONArray()
+    String oldUniqueName, newUniqueName
+    idx = 0
+    for (def eachEntity : inputArray) {
+        oldUniqueName = eachEntity.uniquename
+        newUniqueName = generateUniqueName()
+        eachEntity.uniquename = newUniqueName
+        returnArray.add(idx, eachEntity)
+        uniqueNamesMap.put(oldUniqueName, newUniqueName)
+        idx += 1
+    }
+    return returnArray
+}
+
+JSONObject triggerAddFeature(String destinationurl, String username, String password, String organism, String sequenceName, JSONArray featuresArray, Boolean ignoressl = false) {
+    URL url = new URL(destinationurl)
+    String fullPath = "${url.path}/annotationEditor/addFeature"
+    def addFeatureClient = new RESTClient(url)
+    if (ignoressl) { addFeatureClient.ignoreSSLIssues() }
+    def addFeatureResponse = addFeatureClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: [  'username' : username, 'password' : password, 'track' : sequenceName, 'organism' : organism, 'features' : featuresArray ]
+    )
+
+    assert addFeatureResponse.status == 200
+    if (addFeatureResponse.getData().size() == 0) {
+        println "Error: Server did not respond properly while trying to call /addFeature"
+        return
+    }
+    else {
+        return addFeatureResponse.getData()
+        
+    }
+}
+
+JSONObject triggerAddTranscript(String destinationurl, String username, String password, String organism, String sequenceName, JSONArray featuresArray, Boolean ignoressl = false) {
+    URL url = new URL(destinationurl)
+    String fullPath = "${url.path}/annotationEditor/addTranscript"
+    def addTranscriptClient = new RESTClient(url)
+    if (ignoressl) { addTranscriptClient.ignoreSSLIssues() }
+    def addTranscriptResponse = addTranscriptClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: [  'username' : username, 'password' : password, 'track' : sequenceName, 'organism' : organism, 'features' : featuresArray ]
+    )
+
+    assert addTranscriptResponse.status == 200
+    if (addTranscriptResponse.getData().size() == 0) {
+        println "Error: Server did not respond properly while trying to call /addTranscript"
+        return
+    }
+    else {
+        return addTranscriptResponse.getData()
+    }
+}
+
+ArrayList getAllSequencesForUsername(String sourceurl, String username, String password, String organism, Boolean ignoressl = false) {
+    URL url = new URL(sourceurl)
+    String fullPath = "${url.path}/organism/getSequencesForOrganism"
+    def argumentsArray = [
+            username : username,
+            password : password,
+            organism : organism
+    ]
+    def getSequencesForOrganismClient = new RESTClient(sourceurl)
+    if (ignoressl) { getSequencesForOrganismClient.ignoreSSLIssues() }
+    def getSequencesForOrganismResponse = getSequencesForOrganismClient.post(
+            contentType: 'text/javascript',
+            path: fullPath,
+            body: argumentsArray
+    )
+    
+    assert getSequencesForOrganismResponse.status == 200
+    if (getSequencesForOrganismResponse.getData().sequences) {
+        return getSequencesForOrganismResponse.getData().sequences as ArrayList
+    } else {
+        println getSequencesForOrganismResponse.getData().error
+        return
+    }
+}
\ No newline at end of file
diff --git a/docs/web_services/examples/perl/test_add_feature.pl b/docs/web_services/examples/perl/test_add_feature.pl
new file mode 100755
index 0000000..9e063c5
--- /dev/null
+++ b/docs/web_services/examples/perl/test_add_feature.pl
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use File::Basename;
+use lib dirname($0);
+
+use Getopt::Long qw(:config no_ignore_case bundling);
+use LWP::UserAgent;
+use JSON;
+
+
+my $annotation_track_prefix = "";
+my $username;
+my $password;
+my $url;
+my $session_id;
+my $help;
+my $trackname;
+my $feature_data;
+
+$| = 1;
+
+parse_options();
+
+sub parse_options {
+    GetOptions(
+           "username|u=s"       => \$username,
+           "password|p=s"       => \$password,
+           "url|U=s"            => \$url,
+           "track|t=s"            => \$trackname,
+           "prefix|P=s"           => \$annotation_track_prefix,
+           "featuredata|F=s"           => \$feature_data,
+           "help|h"             => \$help);
+    print_usage() if $help;
+    die "Missing required parameter: username\n" if !$username;
+    die "Missing required parameter: password\n" if !$password;
+    die "Missing required parameter: url\n" if !$url;
+    die "Missing required parameter: track\n" if !$trackname;
+    die "Missing required parameter: featuredata\n" if !$feature_data;
+    $url = "http://$url" if $url !~ m%http://%;
+}
+
+sub print_usage {
+    my $progname = basename($0);
+    die << "END";
+usage: $progname
+    --url|-U <URL to Apollo instance>
+    --username|-u <username>
+    --password|-p <password>
+    --track|-t <trackname>
+    [--prefix|-P <trackname>]
+
+    U: URL to Apollo instance
+    u: username to access Apollo
+    p: password to access Apollo
+    t: trackname to delete tracks on
+    P: annotation track prefix [default: ]
+    F: feature data
+END
+}
+
+my $track=$annotation_track_prefix.$trackname;
+
+
+my $login_result=`curl -b cookies.txt -c cookies.txt -H "Content-Type:application/json" -d "{'username': '$username', 'password': '$password'}" "$url/Login?operation=login" 2> /dev/null`;
+
+
+print $login_result."\n";
+#[{"location":{"fmin":559153,"fmax":559540,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"GB42178-RA","children":[{"location":{"fmin":559153,"fmax":559540,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}}]}]
+
+my $feature_result=`curl -b cookies.txt -c cookies.txt --data '{ "username": '$username', "track": "$track", "features": $feature_data, "operation": "add_transcript" }' $url/AnnotationEditorService 2> /dev/null`;
+
+print $feature_result . "\n";
+
+
+
diff --git a/docs/web_services/examples/perl/test_remove_features.pl b/docs/web_services/examples/perl/test_remove_features.pl
new file mode 100644
index 0000000..dfc8704
--- /dev/null
+++ b/docs/web_services/examples/perl/test_remove_features.pl
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use File::Basename;
+use lib dirname($0);
+
+use Getopt::Long qw(:config no_ignore_case bundling);
+use LWP::UserAgent;
+use JSON;
+
+
+my $username;
+my $password;
+my $url;
+my $session_id;
+my $help;
+my $trackname;
+
+
+$| = 1;
+
+parse_options();
+
+sub parse_options {
+    GetOptions(
+           "username|u=s"       => \$username,
+           "password|p=s"       => \$password,
+           "url|U=s"            => \$url,
+           "track|t=s"            => \$trackname,
+           "help|h"             => \$help);
+    print_usage() if $help;
+    die "Missing required parameter: username\n" if !$username;
+    die "Missing required parameter: password\n" if !$password;
+    die "Missing required parameter: url\n" if !$url;
+    die "Missing required parameter: track\n" if !$trackname;
+    $url = "http://$url" if $url !~ m%http://%;
+}
+
+
+
+sub print_usage {
+    my $progname = basename($0);
+    die << "END";
+usage: $progname
+    --url|-U <URL to Apollo instance>
+    --username|-u <username>
+    --password|-p <password>
+    --track|-t <trackname>
+    [--prefix|-P <trackname>]
+
+    U: URL to Apollo instance
+    u: username to access Apollo
+    p: password to access Apollo
+    t: trackname to delete tracks on
+END
+}
+
+print $trackname."\n";
+my $track=$trackname;
+
+print $track."\n";
+my $login_result=`curl -b cookies.txt -c cookies.txt -H "Content-Type:application/json" -d "{'username': '$username', 'password': '$password'}" "$url/Login?operation=login" 2> /dev/null`;
+
+my $features_to_delete=`curl -b cookies.txt -c cookies.txt --data '{ "track": "$track", "operation": "get_features" }' $url/AnnotationEditorService 2> /dev/null`;
+
+my $result=decode_json $features_to_delete;
+
+my @features;
+if($result->{features}) {
+    foreach my $feature (@{$result->{features}}) {
+        my %uniquename=("uniquename"=>$feature->{uniquename});
+        push (@features, \%uniquename);
+    }
+}
+my $str=encode_json \@features;
+
+
+my $output=`curl -b cookies.txt -c cookies.txt --data '{ "track": "$track", "operation": "delete_feature","features": $str }' $url/AnnotationEditorService 2> /dev/null`;
+if($output eq '{"features":[]}') {
+    print "Success\n";
+}
+else {
+    print "Error: $output";
+}
diff --git a/docs/web_services/examples/shell/addAttribute.sh b/docs/web_services/examples/shell/addAttribute.sh
new file mode 100755
index 0000000..6a2d978
--- /dev/null
+++ b/docs/web_services/examples/shell/addAttribute.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+organism=$4
+trackName=$5
+uniqueName=$6
+attributePair=$7
+
+usage() {
+    echo "Sample script for adding attribute to a feature via web services"
+    echo "Usage:    ./addAttribute.sh <complete_apollo_URL> <username> <password> <organism_common_name> <track> <unique_name_for_feature> <attribute=value>"
+    echo "Example:  ./addAttribute.sh http://localhost:8080/apollo demo demo Amel Group1.10 f5f9fb2d-5b40-48fb-bf02-b67a87cfb82a isPseudo=False"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$organism" || ! -n "$trackName" || ! -n "$uniqueName" || ! -n "$attributePair" ]]; then
+    usage
+    exit
+fi
+
+attrArray=(${attributePair//=/ })
+echo curl -i -H \"Content-type: application/json\" -X POST ${url}/annotationEditor/addAttribute -d \"{\"username\":\"${username}\", \"password\":\"${password}\", \"features\":[{\"non_reserved_properties\":[{\"tag\":\"${attrArray[0]}\",\"value\":\"${attrArray[1]}\"}], \"uniquename\":\"${uniqueName}\"}],\"track\":\"${trackName}\", \"organism\":\"${organism}\"}\"
+curl -i -H "Content-type: application/json" -X POST ${url}/annotationEditor/addAttribute -d "{"username":"${username}", "password":"${password}", "features":[{"non_reserved_properties":[{"tag":"${attrArray[0]}","value":"${attrArray[1]}"}], "uniquename":"${uniqueName}"}],"track":"${trackName}", "organism":"${organism}"}"
\ No newline at end of file
diff --git a/docs/web_services/examples/shell/addComment.sh b/docs/web_services/examples/shell/addComment.sh
new file mode 100755
index 0000000..4866420
--- /dev/null
+++ b/docs/web_services/examples/shell/addComment.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+organism=$4
+trackName=$5
+uniqueName=$6
+comment=$7
+
+usage() {
+    echo "Sample script for adding comment to a feature via web services"
+    echo "Usage:    ./addComment.sh <complete_apollo_URL> <username> <password> <organism_common_name> <track> <unique_name_for_feature> <comment>"
+    echo "Example:  ./addComment.sh http://localhost:8080/apollo demo demo Amel Group1.10 f5f9fb2d-5b40-48fb-bf02-b67a87cfb82a 'This annotation is complete'"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$organism" || ! -n "$trackName" || ! -n "$uniqueName" || ! -n "$comment" ]]; then
+    usage
+    exit
+fi
+
+echo curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/addComments -d "{'operation': 'add_comments', 'username': '${username}', 'password': '${password}', 'features':[{'uniquename': '${uniqueName}','comments':['${comment}']}], 'track':'${trackName}', 'organism': '${organism}'}"
+curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/addComments -d "{'operation': 'add_comments', 'username': '${username}', 'password': '${password}', 'features':[{'uniquename': '${uniqueName}','comments':['${comment}']}], 'track':'${trackName}', 'organism': '${organism}'}"
\ No newline at end of file
diff --git a/docs/web_services/examples/shell/deleteFeature.sh b/docs/web_services/examples/shell/deleteFeature.sh
new file mode 100755
index 0000000..56b5434
--- /dev/null
+++ b/docs/web_services/examples/shell/deleteFeature.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+uniqueName=$4
+
+usage() {
+    echo "Sample script for deleting a feature via web services"
+    echo "Usage:    ./deleteFeature.sh <complete_apollo_URL> <username> <password> <unique_name_for_feature>"
+    echo "Example:  ./deleteFeature.sh http://localhost:8080/apollo demo at demo.com demo f5f9fb2d-5b40-48fb-bf02-b67a87cfb82a"
+}
+
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$uniqueName"  ]]; then
+    usage
+    exit
+fi
+
+echo curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/deleteFeature -d "{'username':'${username}', 'password':'${password}', 'features':[{'uniquename':'${uniqueName}'}], 'track':'${trackName}', 'organism':'${organism}'}"
+curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/deleteFeature -d "{'username':'${username}', 'password':'${password}', 'features':[{'uniquename':'${uniqueName}'}], 'track':'${trackName}', 'organism':'${organism}'}"
diff --git a/docs/web_services/examples/shell/doLogin.sh b/docs/web_services/examples/shell/doLogin.sh
new file mode 100755
index 0000000..c9678c3
--- /dev/null
+++ b/docs/web_services/examples/shell/doLogin.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+
+usage() {
+    echo "Sample script to login to Apollo via web services"
+    echo "Usage:    ./doLogin.sh <complete_apollo_URL> <username> <password>"
+    echo "Example:  ./doLogin.sh http://localhost:8080/apollo demo demo"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" ]]; then
+    usage
+    exit
+fi
+
+echo curl -c ${username}_cookies.txt -H 'Content-Type:application/json' -d "{'username': '${username}', 'password': '${password}'}" "${url}/Login?operation=login" 2> /dev/null
+curl -c ${username}_cookies.txt -H 'Content-Type:application/json' -d "{'username': '${username}', 'password': '${password}'}" "${url}/Login?operation=login" 2> /dev/null
\ No newline at end of file
diff --git a/docs/web_services/examples/shell/exportFeatures.sh b/docs/web_services/examples/shell/exportFeatures.sh
new file mode 100755
index 0000000..3c84a20
--- /dev/null
+++ b/docs/web_services/examples/shell/exportFeatures.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+url=$1
+username=$2
+password=$3
+organism=$4
+export_type=$5
+
+usage() {
+    echo "Sample script for exporting features from organism as GFF3 via web services"
+    echo "Usage:    ./exportFeatures.sh <complete_apollo_URL> <username> <password> <export_type>"
+    echo "Example:  ./exportFeatures.sh http://localhost:8080/apollo demo demo peptide"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$organism" || ! -n "$export_type" ]]; then
+    usage
+    exit
+fi
+
+echo curl "${url}/IOService/write" -H 'Content-Type: application/json' --data "{'type': 'GFF3', 'seqType':'${export_type}', 'exportAllSequences': 'false', 'exportGff3Fasta': 'true', 'output': 'text', 'format': 'text', 'sequences':[], 'username': '${username}', 'password': '${password}', 'organism': '${organism}'}"
+curl "${url}/IOService/write" -H 'Content-Type: application/json' --data "{'type': 'GFF3', 'seqType':'${export_type}', 'exportAllSequences': 'false', 'exportGff3Fasta': 'true', 'output': 'text', 'format': 'text', 'sequences':[], 'username': '${username}', 'password': '${password}', 'organism': '${organism}'}"
+
diff --git a/docs/web_services/examples/shell/findAllOrganisms.sh b/docs/web_services/examples/shell/findAllOrganisms.sh
new file mode 100755
index 0000000..4d7c55c
--- /dev/null
+++ b/docs/web_services/examples/shell/findAllOrganisms.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+
+usage() {
+    echo "Sample script for finding all available organisms in an Apollo instance via web services"
+    echo "Usage:    ./findAllOrganisms.sh <complete_apollo_URL> <username> <password>"
+    echo "Example:  ./findAllOrganisms.sh http://localhost:8080/apollo demo demo"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" ]]; then
+    usage
+    exit
+fi
+
+echo curl  --header "Content-type: application/json" --request POST --data "{'username': '${username}', 'password': '${password}'}" "${url}/organism/findAllOrganisms"
+curl  --header "Content-type: application/json" --request POST --data "{'username': '${username}', 'password': '${password}'}" "${url}/organism/findAllOrganisms"
+
diff --git a/docs/web_services/examples/shell/getFeatures.sh b/docs/web_services/examples/shell/getFeatures.sh
new file mode 100755
index 0000000..9183f1f
--- /dev/null
+++ b/docs/web_services/examples/shell/getFeatures.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+organism=$4
+track=$5
+
+usage() {
+    echo "Sample script to get all features from a sequence of an organism via web services"
+    echo "Usage:    ./getFeatures.sh <complete_apollo_URL> <username> <password> <organism> <sequence_name>"
+    echo "Example:  ./getFeatures.sh http://localhost:8080/apollo demo demo Amel Group1.10"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$organism" || ! -n "$track" ]]; then
+    usage
+    exit
+fi
+
+# First login to Apollo
+echo curl -b login_cookies.txt -c login_cookies.txt -H "Content-Type:application/json" -d "{'username': '${username}', 'password': '${password}'}" ${url}/Login?operation=login
+curl -b login_cookies.txt -c login_cookies.txt -H "Content-Type:application/json" -d "{'username': '${username}', 'password': '${password}'}" ${url}/Login?operation=login
+
+# Then request for features
+echo curl -b login_cookies.txt -c login_cookies.txt -e "${url}" --data "{ 'organism':'${organism}','operation': 'get_features', 'track': '${track}'}" ${url}/annotator/AnnotationEditorService
+curl -b login_cookies.txt -c login_cookies.txt -e "${url}" --data "{ 'organism':'${organism}','operation': 'get_features', 'track': '${track}'}" ${url}/annotator/AnnotationEditorService
diff --git a/docs/web_services/examples/shell/listStatus.sh b/docs/web_services/examples/shell/listStatus.sh
new file mode 100755
index 0000000..633fb8b
--- /dev/null
+++ b/docs/web_services/examples/shell/listStatus.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+organism=$4
+trackName=$5
+uniqueName=$6
+status=$7
+
+usage() {
+    echo "Sample script for listing statuses via web services"
+    echo "Usage:    ./listStatus.sh <complete_apollo_URL> <username> <password> "
+    echo "Example:  ./listStatus.sh http://localhost:8080/apollo demo demopass"
+    echo ""
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password"  ]]; then
+    usage
+    exit
+fi
+
+echo curl -i -H 'Content-type: application/json' -X POST ${url}/availableStatus/showStatus -d "{'username': '${username}', 'password': '${password}'}"
+curl -i -H 'Content-type: application/json' -X POST ${url}/availableStatus/showStatus -d "{'username': '${username}', 'password': '${password}'}"
diff --git a/docs/web_services/examples/shell/loadGroups.sh b/docs/web_services/examples/shell/loadGroups.sh
new file mode 100755
index 0000000..0c23cc2
--- /dev/null
+++ b/docs/web_services/examples/shell/loadGroups.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+
+url=$1
+username=$2
+password=$3
+groupid=$4
+
+usage() {
+    echo "Sample script to get all features from a sequence of an organism via web services"
+    echo "Usage:    ./loadGroups.sh <complete_apollo_URL> <username> <password> <group_id>"
+    echo "Example:  ./loadGroups.sh http://localhost:8080/apollo demo demo 123"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" ]]; then
+    usage
+    exit
+fi
+
+echo "Load all groups"
+curl  -X POST -H 'Content-Type: application/json' -d "{'username': '${username}', 'password': '${password}'}" "${url}/group/loadGroups"
+
+
+echo "Load one group with a given userId"
+curl  -X POST -H 'Content-Type: application/json' -d "{'username': '${username}', 'password': '${password}', 'groupId': ${groupid}}" "${url}/group/loadGroups"
+
+  
diff --git a/docs/web_services/examples/shell/loadUsers.sh b/docs/web_services/examples/shell/loadUsers.sh
new file mode 100755
index 0000000..c44f531
--- /dev/null
+++ b/docs/web_services/examples/shell/loadUsers.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+userid=$4
+
+usage() {
+    echo "Sample script to get all features from a sequence of an organism via web services"
+    echo "Usage:    ./loadUsers.sh <complete_apollo_URL> <username> <password> <user_id>"
+    echo "Example:  ./loadUsers.sh http://localhost:8080/apollo demo demo 12345"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" ]]; then
+    usage
+    exit
+fi
+
+if [ ! -n "$userid" ]; then
+    echo "Load all users"
+    curl  -i -X POST -H 'Content-Type: application/json' -d "{'omitEmptyOrganisms':true,'username': '${username}', 'password': '${password}'}" "${url}/user/loadUsers"
+else
+    echo "Load one users with a given userId: ${userid}"
+    curl  -i -X POST -H 'Content-Type: application/json' -d "{'username': '${username}', 'password': '${password}','userId': ${userid}}" ${url}/user/loadUsers
+fi
diff --git a/docs/web_services/examples/shell/sample_rest.sh b/docs/web_services/examples/shell/sample_rest.sh
new file mode 100755
index 0000000..b1f039b
--- /dev/null
+++ b/docs/web_services/examples/shell/sample_rest.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+ curl -b cookies.txt -c cookies.txt -e  "http://localhost:8080" -H "Content-Type:application/json" -d "{'username': 'demo', 'password': 'demo'}" "http://localhost:8080/apollo/Login?operation=login"
+ curl -b cookies.txt -c cookies.txt -e  "http://localhost:8080" --data '{ operation: "write", adapter: "GFF3", tracks: ["scf1117875582023"], options: "output=file&format=gzip" }' http://localhost:8080/apollo/IOService
+ # look for url 
diff --git a/docs/web_services/examples/shell/setStatus.sh b/docs/web_services/examples/shell/setStatus.sh
new file mode 100755
index 0000000..3da00d4
--- /dev/null
+++ b/docs/web_services/examples/shell/setStatus.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+url=$1
+username=$2
+password=$3
+organism=$4
+trackName=$5
+uniqueName=$6
+status=$7
+
+usage() {
+    echo "Sample script for adding status to a feature via web services"
+    echo "Usage:    ./setStatus.sh <complete_apollo_URL> <username> <password> <organism_common_name> <track> <unique_name_for_feature> <status>"
+    echo "Example:  ./setStatus.sh http://localhost:8080/apollo demo demo Amel Group1.10 f5f9fb2d-5b40-48fb-bf02-b67a87cfb82a 'VerificationNeeded'"
+    echo ""
+    echo "Note: Be sure to create the status beforehand in Apollo from the Admin tab in Annotator Panel"
+}
+
+if [[ ! -n "$url" || ! -n "$username" || ! -n "$password" || ! -n "$organism" || ! -n "$trackName" || ! -n "$uniqueName" || ! -n "$status" ]]; then
+    usage
+    exit
+fi
+
+echo curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/setStatus -d "{'username': '${username}', 'password': '${password}', 'features': [{'uniquename': '${uniqueName}', 'status': '${status}'}], 'track': '${trackName}', 'organism': '${organism}'}"
+curl -i -H 'Content-type: application/json' -X POST ${url}/annotationEditor/setStatus -d "{'username': '${username}', 'password': '${password}', 'features': [{'uniquename': '${uniqueName}', 'status': '${status}'}], 'track': '${trackName}', 'organism': '${organism}'}"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..5ccda13
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b18d973
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Mar 14 22:13:59 PDT 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..72d362d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+ at if "%DEBUG%" == "" @echo off
+ at rem ##########################################################################
+ at rem
+ at rem  Gradle startup script for Windows
+ at rem
+ at rem ##########################################################################
+
+ at rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+ at rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+ at rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+ at rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+ at rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+ at rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+ at rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+ at rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+ at rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/grails-app/assets/images/ApolloLogo-letter-2-small_0.png b/grails-app/assets/images/ApolloLogo-letter-2-small_0.png
new file mode 100644
index 0000000..ba07df7
Binary files /dev/null and b/grails-app/assets/images/ApolloLogo-letter-2-small_0.png differ
diff --git a/grails-app/assets/images/ApolloLogo_100x36.png b/grails-app/assets/images/ApolloLogo_100x36.png
new file mode 100644
index 0000000..aa04f44
Binary files /dev/null and b/grails-app/assets/images/ApolloLogo_100x36.png differ
diff --git a/grails-app/assets/images/apple-touch-icon-retina.png b/grails-app/assets/images/apple-touch-icon-retina.png
new file mode 100644
index 0000000..5cc83ed
Binary files /dev/null and b/grails-app/assets/images/apple-touch-icon-retina.png differ
diff --git a/grails-app/assets/images/apple-touch-icon.png b/grails-app/assets/images/apple-touch-icon.png
new file mode 100644
index 0000000..aba337f
Binary files /dev/null and b/grails-app/assets/images/apple-touch-icon.png differ
diff --git a/grails-app/assets/images/favicon.ico b/grails-app/assets/images/favicon.ico
new file mode 100644
index 0000000..3dfcb92
Binary files /dev/null and b/grails-app/assets/images/favicon.ico differ
diff --git a/grails-app/assets/images/grails_logo.png b/grails-app/assets/images/grails_logo.png
new file mode 100644
index 0000000..9836b93
Binary files /dev/null and b/grails-app/assets/images/grails_logo.png differ
diff --git a/grails-app/assets/images/skin/database_add.png b/grails-app/assets/images/skin/database_add.png
new file mode 100644
index 0000000..802bd6c
Binary files /dev/null and b/grails-app/assets/images/skin/database_add.png differ
diff --git a/grails-app/assets/images/skin/database_delete.png b/grails-app/assets/images/skin/database_delete.png
new file mode 100644
index 0000000..cce652e
Binary files /dev/null and b/grails-app/assets/images/skin/database_delete.png differ
diff --git a/grails-app/assets/images/skin/database_edit.png b/grails-app/assets/images/skin/database_edit.png
new file mode 100644
index 0000000..e501b66
Binary files /dev/null and b/grails-app/assets/images/skin/database_edit.png differ
diff --git a/grails-app/assets/images/skin/database_save.png b/grails-app/assets/images/skin/database_save.png
new file mode 100644
index 0000000..44c06dd
Binary files /dev/null and b/grails-app/assets/images/skin/database_save.png differ
diff --git a/grails-app/assets/images/skin/database_table.png b/grails-app/assets/images/skin/database_table.png
new file mode 100644
index 0000000..693709c
Binary files /dev/null and b/grails-app/assets/images/skin/database_table.png differ
diff --git a/grails-app/assets/images/skin/exclamation.png b/grails-app/assets/images/skin/exclamation.png
new file mode 100644
index 0000000..c37bd06
Binary files /dev/null and b/grails-app/assets/images/skin/exclamation.png differ
diff --git a/grails-app/assets/images/skin/house.png b/grails-app/assets/images/skin/house.png
new file mode 100644
index 0000000..fed6221
Binary files /dev/null and b/grails-app/assets/images/skin/house.png differ
diff --git a/grails-app/assets/images/skin/information.png b/grails-app/assets/images/skin/information.png
new file mode 100644
index 0000000..12cd1ae
Binary files /dev/null and b/grails-app/assets/images/skin/information.png differ
diff --git a/grails-app/assets/images/skin/shadow.jpg b/grails-app/assets/images/skin/shadow.jpg
new file mode 100644
index 0000000..b7ed44f
Binary files /dev/null and b/grails-app/assets/images/skin/shadow.jpg differ
diff --git a/grails-app/assets/images/skin/sorted_asc.gif b/grails-app/assets/images/skin/sorted_asc.gif
new file mode 100644
index 0000000..6b179c1
Binary files /dev/null and b/grails-app/assets/images/skin/sorted_asc.gif differ
diff --git a/grails-app/assets/images/skin/sorted_desc.gif b/grails-app/assets/images/skin/sorted_desc.gif
new file mode 100644
index 0000000..38b3a01
Binary files /dev/null and b/grails-app/assets/images/skin/sorted_desc.gif differ
diff --git a/grails-app/assets/images/spinner.gif b/grails-app/assets/images/spinner.gif
new file mode 100644
index 0000000..1ed786f
Binary files /dev/null and b/grails-app/assets/images/spinner.gif differ
diff --git a/grails-app/assets/images/springsource.png b/grails-app/assets/images/springsource.png
new file mode 100644
index 0000000..e806d00
Binary files /dev/null and b/grails-app/assets/images/springsource.png differ
diff --git a/grails-app/assets/images/webapollo_favicon.ico b/grails-app/assets/images/webapollo_favicon.ico
new file mode 100644
index 0000000..96cd853
Binary files /dev/null and b/grails-app/assets/images/webapollo_favicon.ico differ
diff --git a/grails-app/assets/javascripts/WebServicesController.js b/grails-app/assets/javascripts/WebServicesController.js
new file mode 100644
index 0000000..7f02fbf
--- /dev/null
+++ b/grails-app/assets/javascripts/WebServicesController.js
@@ -0,0 +1,15 @@
+var app = angular.module('WebServicesApp', ['ui.bootstrap']);
+app.controller('WebServicesController', function ($scope, $http, $attrs) {
+    var ctrl = this;
+    ctrl.rootUrl = $attrs.rootUrl;
+    ctrl.expandAll = false;
+
+    ctrl.load = function () {
+
+        $http.get(ctrl.rootUrl + "/js/restapidoc/restapidoc.json").success(function (data, status, headers, config) {
+            ctrl.apis = data.apis;
+        });
+    };
+    ctrl.load();
+
+});
diff --git a/grails-app/assets/javascripts/annotator/app.js b/grails-app/assets/javascripts/annotator/app.js
new file mode 100644
index 0000000..279e721
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/app.js
@@ -0,0 +1,35 @@
+//= require jquery
+//= require js/jquery-ui-1.10.3.custom
+//= require bootstrap
+//= require vendor/angular.min.js
+//= require vendor/ui-bootstrap-tpls-0.12.0.min.js
+//= require vendor/ui-layout.min.js
+//= require_self
+//= require_tree controllers
+//= require_tree services
+var as = angular.module('AnnotatorApplication', ['ui.bootstrap','ui.layout']);
+
+//$(function ()
+//{
+//    $(".resizable1").resizable(
+//        {
+//            autoHide: true,
+//            handles: 'e',
+//            resize: function(e, ui)
+//            {
+//                var parent = ui.element.parent();
+//                var remainingSpace = parent.width() - ui.element.outerWidth(),
+//                    divTwo = ui.element.next(),
+//                    divTwoWidth = (remainingSpace - (divTwo.outerWidth() - divTwo.width()))/parent.width()*100+"%";
+//                divTwo.width(divTwoWidth);
+//            },
+//            stop: function(e, ui)
+//            {
+//                var parent = ui.element.parent();
+//                ui.element.css(
+//                    {
+//                        width: ui.element.width()/parent.width()*100+"%",
+//                    });
+//            }
+//        });
+//});
diff --git a/grails-app/assets/javascripts/annotator/controllers/Annotator.js b/grails-app/assets/javascripts/annotator/controllers/Annotator.js
new file mode 100644
index 0000000..ec92e30
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/controllers/Annotator.js
@@ -0,0 +1,17 @@
+//= require annotator/app
+
+angular.module('AnnotatorApplication').controller('AnnotatorController', function ($scope, $rootScope, $http, $location) {
+
+    $scope.data = "asdfg";
+
+    $scope.tabs = [
+        { title: 'A',active:'true',disabled:'false',content:'dude' }
+        ,{ title: 'B',active:'true',disabled:'false',content:'what?' }
+    ];
+
+    $scope.pingSelf = function(){
+  console.log('ping');
+    };
+
+
+});
diff --git a/grails-app/assets/javascripts/annotator/directives/Directive.js b/grails-app/assets/javascripts/annotator/directives/Directive.js
new file mode 100644
index 0000000..9d0207d
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/directives/Directive.js
@@ -0,0 +1 @@
+console.log('directive loaded');
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/annotator/filters/filter1.js b/grails-app/assets/javascripts/annotator/filters/filter1.js
new file mode 100644
index 0000000..fc166cc
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/filters/filter1.js
@@ -0,0 +1 @@
+console.log('filters loaded');
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/annotator/models/Feature.js b/grails-app/assets/javascripts/annotator/models/Feature.js
new file mode 100644
index 0000000..9436081
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/models/Feature.js
@@ -0,0 +1 @@
+console.log('feature loaded');
diff --git a/grails-app/assets/javascripts/annotator/partials/Form.html b/grails-app/assets/javascripts/annotator/partials/Form.html
new file mode 100644
index 0000000..e69de29
diff --git a/grails-app/assets/javascripts/annotator/services/Service.js b/grails-app/assets/javascripts/annotator/services/Service.js
new file mode 100644
index 0000000..265b78b
--- /dev/null
+++ b/grails-app/assets/javascripts/annotator/services/Service.js
@@ -0,0 +1 @@
+console.log('Service loaded');
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/application.js b/grails-app/assets/javascripts/application.js
new file mode 100644
index 0000000..2ba4e85
--- /dev/null
+++ b/grails-app/assets/javascripts/application.js
@@ -0,0 +1,22 @@
+// This is a manifest file that'll be compiled into application.js.
+//
+// Any JavaScript file within this directory can be referenced here using a relative path.
+//
+// You're free to add application-wide JavaScript to this file, but it's generally better 
+// to create separate JavaScript files as needed.
+//
+//= require jquery
+//= require bootstrap
+//= require vendor/bootstrap-datepicker.js
+//= require_self
+
+
+if (typeof jQuery !== 'undefined') {
+	(function($) {
+		$('#spinner').ajaxStart(function() {
+			$(this).fadeIn();
+		}).ajaxStop(function() {
+			$(this).fadeOut();
+		});
+	})(jQuery);
+}
diff --git a/grails-app/assets/javascripts/oldlook.js b/grails-app/assets/javascripts/oldlook.js
new file mode 100644
index 0000000..7b380d6
--- /dev/null
+++ b/grails-app/assets/javascripts/oldlook.js
@@ -0,0 +1,21 @@
+// This is a manifest file that'll be compiled into application.js.
+//
+// Any JavaScript file within this directory can be referenced here using a relative path.
+//
+// You're free to add application-wide JavaScript to this file, but it's generally better 
+// to create separate JavaScript files as needed.
+//
+//= require jquery
+//= require bootstrap
+//= require_self
+
+
+if (typeof jQuery !== 'undefined') {
+	(function($) {
+		$('#spinner').ajaxStart(function() {
+			$(this).fadeIn();
+		}).ajaxStop(function() {
+			$(this).fadeOut();
+		});
+	})(jQuery);
+}
diff --git a/grails-app/assets/javascripts/selectTracks.js b/grails-app/assets/javascripts/selectTracks.js
new file mode 100644
index 0000000..0683f05
--- /dev/null
+++ b/grails-app/assets/javascripts/selectTracks.js
@@ -0,0 +1,48 @@
+//= require self
+function update_checked(checked) {
+    $("#checkbox_option").prop("checked", checked);
+    table.$(".track_select").prop("checked", checked);
+};
+
+function login() {
+    var $login = $("#login_dialog");
+    $login.dialog("option", "closeOnEscape", false);
+    $login.dialog("option", "dialogClass", "login_dialog");
+    $(".ui-dialog-titlebar-close", this.parentNode).hide();
+    $login.load("Login");
+    $login.dialog("open");
+    $login.dialog("option", "width", "auto");
+};
+
+function logout() {
+    $.ajax({
+        type: "post",
+        url: "Login?operation=logout",
+        headers: {
+            "Content-Type":"application/x-www-form-urlencoded",
+        },
+        success: function(data, textStatus, jqXHR) {
+        },
+        error: function(qXHR, textStatus, errorThrown) {
+        }
+    });
+};
+
+function open_user_manager_dialog() {
+    var $userManager = $("<div id='user_manager_dialog' title='Manage users'></div>");
+    $userManager.dialog( {
+        draggable: false,
+        modal: true,
+        autoOpen: true,
+        resizable: false,
+        closeOnEscape: true,
+        close: function() {
+            $(this).dialog("destroy").remove();
+        },
+        width: "70%"
+    } );
+    $userManager.load("userPermissions.jsp", null, function() {
+        $userManager.dialog('option', 'position', 'center');
+    });
+    //$userManager.dialog("open");
+}
diff --git a/grails-app/assets/javascripts/vendor/angular-1.3.17.min.js b/grails-app/assets/javascripts/vendor/angular-1.3.17.min.js
new file mode 100644
index 0000000..78fad60
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular-1.3.17.min.js
@@ -0,0 +1,253 @@
+/*
+ AngularJS v1.3.17
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(T,V,s){'use strict';function E(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.17/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){c=c+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,e;e=arguments[a];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;c+=d(e)}return Error(c)}}function Sa(b){if(null==b||Ta(b))return!1;var a="length"in Object( [...]
+return b.nodeType===ma&&a?!0:K(b)||A(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function q(b,a,c){var d,e;if(b)if(F(b))for(d in b)"prototype"==d||"length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d)||a.call(c,b[d],d,b);else if(A(b)||Sa(b)){var f="object"!==typeof b;d=0;for(e=b.length;d<e;d++)(f||d in b)&&a.call(c,b[d],d,b)}else if(b.forEach&&b.forEach!==q)b.forEach(a,c,b);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d,b);return b}function Jd(b,a,c){for(var d=Object.key [...]
+e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function pc(b){return function(a,c){b(c,a)}}function Kd(){return++rb}function qc(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function y(b){for(var a=b.$$hashKey,c=1,d=arguments.length;c<d;c++){var e=arguments[c];if(e)for(var f=Object.keys(e),g=0,h=f.length;g<h;g++){var l=f[g];b[l]=e[l]}}qc(b,a);return b}function aa(b){return parseInt(b,10)}function Pb(b,a){return y(Object.create(b),a)}function D(){}function na(b){return b}function ca(b){retu [...]
+function z(b){return"undefined"===typeof b}function B(b){return"undefined"!==typeof b}function J(b){return null!==b&&"object"===typeof b}function K(b){return"string"===typeof b}function U(b){return"number"===typeof b}function ea(b){return"[object Date]"===Aa.call(b)}function F(b){return"function"===typeof b}function Ua(b){return"[object RegExp]"===Aa.call(b)}function Ta(b){return b&&b.window===b}function Va(b){return b&&b.$evalAsync&&b.$watch}function Wa(b){return"boolean"===typeof b}fun [...]
+!(b.nodeName||b.prop&&b.attr&&b.find))}function Ld(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function sa(b){return O(b.nodeName||b[0]&&b[0].nodeName)}function Xa(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return a}function Ba(b,a,c,d){if(Ta(b)||Va(b))throw Ja("cpws");if(a){if(b===a)throw Ja("cpi");c=c||[];d=d||[];if(J(b)){var e=c.indexOf(b);if(-1!==e)return d[e];c.push(b);d.push(a)}if(A(b))for(var f=a.length=0;f<b.length;f++)e=Ba(b[f],null,c,d),J(b[f])& [...]
+d.push(e)),a.push(e);else{var g=a.$$hashKey;A(a)?a.length=0:q(a,function(b,c){delete a[c]});for(f in b)b.hasOwnProperty(f)&&(e=Ba(b[f],null,c,d),J(b[f])&&(c.push(b[f]),d.push(e)),a[f]=e);qc(a,g)}}else if(a=b)A(b)?a=Ba(b,[],c,d):ea(b)?a=new Date(b.getTime()):Ua(b)?(a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex):J(b)&&(e=Object.create(Object.getPrototypeOf(b)),a=Ba(b,e,c,d));return a}function oa(b,a){if(A(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c [...]
+a||{},b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function fa(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&&"object"==c)if(A(b)){if(!A(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!fa(b[d],a[d]))return!1;return!0}}else{if(ea(b))return ea(a)?fa(b.getTime(),a.getTime()):!1;if(Ua(b))return Ua(a)?b.toString()==a.toString():!1;if(Va(b)||Va(a)||Ta(b)||Ta(a)||A(a)||ea(a)||Ua(a))return!1;c={};for(d [...]
+d.charAt(0)&&!F(b[d])){if(!fa(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==s&&!F(a[d]))return!1;return!0}return!1}function Ya(b,a,c){return b.concat(Za.call(a,c))}function sc(b,a){var c=2<arguments.length?Za.call(arguments,2):[];return!F(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,Ya(c,arguments,0)):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Md(b,a){var c=a;"str [...]
+"$"===b.charAt(0)&&"$"===b.charAt(1)?c=s:Ta(a)?c="$WINDOW":a&&V===a?c="$DOCUMENT":Va(a)&&(c="$SCOPE");return c}function $a(b,a){if("undefined"===typeof b)return s;U(a)||(a=a?2:null);return JSON.stringify(b,Md,a)}function tc(b){return K(b)?JSON.parse(b):b}function ta(b){b=G(b).clone();try{b.empty()}catch(a){}var c=G("<div>").append(b).html();try{return b[0].nodeType===ab?O(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+O(b)})}catch(d){return O(c)}}function uc(b) [...]
+function vc(b){var a={},c,d;q((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=uc(c[0]),B(d)&&(b=B(c[1])?uc(c[1]):!0,wc.call(a,d)?A(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Qb(b){var a=[];q(b,function(b,d){A(b)?q(b,function(b){a.push(Ca(d,!0)+(!0===b?"":"="+Ca(b,!0)))}):a.push(Ca(d,!0)+(!0===b?"":"="+Ca(b,!0)))});return a.length?a.join("&"):""}function sb(b){return Ca(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}functio [...]
+"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function Nd(b,a){var c,d,e=tb.length;b=G(b);for(d=0;d<e;++d)if(c=tb[d]+a,K(c=b.attr(c)))return c;return null}function Od(b,a){var c,d,e={};q(tb,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))});q(tb,function(a){a+="app";var e;!c&&(e=b.querySelector("["+a.replace(":","\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==Nd(c,"s [...]
+a(c,d?[d]:[],e))}function xc(b,a,c){J(c)||(c={});c=y({strictDi:!1},c);var d=function(){b=G(b);if(b.injector()){var d=b[0]===V?"document":ta(b);throw Ja("btstrpd",d.replace(/</,"<").replace(/>/,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);a.unshift("ng");d=bb(a,c.strictDi);d.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(functio [...]
+d);c(b)(a)})}]);return d},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;T&&e.test(T.name)&&(c.debugInfoEnabled=!0,T.name=T.name.replace(e,""));if(T&&!f.test(T.name))return d();T.name=T.name.replace(f,"");ba.resumeBootstrap=function(b){q(b,function(b){a.push(b)});return d()};F(ba.resumeDeferredBootstrap)&&ba.resumeDeferredBootstrap()}function Pd(){T.name="NG_ENABLE_DEBUG_INFO!"+T.name;T.location.reload()}function Qd(b){b=ba.element(b).injector();if(!b)throw Ja("test");return b.get(" [...]
+function yc(b,a){a=a||"_";return b.replace(Rd,function(b,d){return(d?a:"")+b.toLowerCase()})}function Sd(){var b;zc||((pa=T.jQuery)&&pa.fn.on?(G=pa,y(pa.fn,{scope:Ka.scope,isolateScope:Ka.isolateScope,controller:Ka.controller,injector:Ka.injector,inheritedData:Ka.inheritedData}),b=pa.cleanData,pa.cleanData=function(a){var c;if(Rb)Rb=!1;else for(var d=0,e;null!=(e=a[d]);d++)(c=pa._data(e,"events"))&&c.$destroy&&pa(e).triggerHandler("$destroy");b(a)}):G=P,ba.element=G,zc=!0)}function Sb(b, [...]
+a||"?",c||"required");return b}function La(b,a,c){c&&A(b)&&(b=b[b.length-1]);Sb(F(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ma(b,a){if("hasOwnProperty"===b)throw Ja("badname",a);}function Ac(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,g=0;g<f;g++)d=a[g],b&&(b=(e=b)[d]);return!c&&F(b)?sc(e,b):b}function ub(b){var a=b[0];b=b[b.length-1];var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);retur [...]
+function Td(b){function a(a,b,c){return a[b]||(a[b]=c())}var c=E("$injector"),d=E("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||E;return a(b,"module",function(){var b={};return function(f,g,h){if("hasOwnProperty"===f)throw d("badname","module");g&&b.hasOwnProperty(f)&&(b[f]=null);return a(b,f,function(){function a(c,d,e,f){f||(f=b);return function(){f[e||"push"]([c,d,arguments]);return v}}if(!g)throw c("nomod",f);var b=[],d=[],e=[],n=a("$injector","invoke","push",d),v={_invokeQueu [...]
+_runBlocks:e,requires:g,name:f,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),animation:a("$animateProvider","register"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:n,run:function(a){e.push(a);return this}};h&&n(h);return v})}})}function Ud(b){y(b,{bootstrap:xc,copy:Ba,extend [...]
+element:G,forEach:q,injector:bb,noop:D,bind:sc,toJson:$a,fromJson:tc,identity:na,isUndefined:z,isDefined:B,isString:K,isFunction:F,isObject:J,isNumber:U,isElement:rc,isArray:A,version:Vd,isDate:ea,lowercase:O,uppercase:vb,callbacks:{counter:0},getTestability:Qd,$$minErr:E,$$csp:cb,reloadWithDebugInfo:Pd});db=Td(T);try{db("ngLocale")}catch(a){db("ngLocale",[]).provider("$locale",Wd)}db("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:Xd});a.provider("$compile",Bc).direc [...]
+input:Cc,textarea:Cc,form:Zd,script:$d,select:ae,style:be,option:ce,ngBind:de,ngBindHtml:ee,ngBindTemplate:fe,ngClass:ge,ngClassEven:he,ngClassOdd:ie,ngCloak:je,ngController:ke,ngForm:le,ngHide:me,ngIf:ne,ngInclude:oe,ngInit:pe,ngNonBindable:qe,ngPluralize:re,ngRepeat:se,ngShow:te,ngStyle:ue,ngSwitch:ve,ngSwitchWhen:we,ngSwitchDefault:xe,ngOptions:ye,ngTransclude:ze,ngModel:Ae,ngList:Be,ngChange:Ce,pattern:Dc,ngPattern:Dc,required:Ec,ngRequired:Ec,minlength:Fc,ngMinlength:Fc,maxlength:Gc [...]
+ngValue:De,ngModelOptions:Ee}).directive({ngInclude:Fe}).directive(wb).directive(Hc);a.provider({$anchorScroll:Ge,$animate:He,$browser:Ie,$cacheFactory:Je,$controller:Ke,$document:Le,$exceptionHandler:Me,$filter:Ic,$interpolate:Ne,$interval:Oe,$http:Pe,$httpBackend:Qe,$location:Re,$log:Se,$parse:Te,$rootScope:Ue,$q:Ve,$$q:We,$sce:Xe,$sceDelegate:Ye,$sniffer:Ze,$templateCache:$e,$templateRequest:af,$$testability:bf,$timeout:cf,$window:df,$$rAF:ef,$$asyncCallback:ff,$$jqLite:gf})}])}functi [...]
+function(a,b,d,e){return e?d.toUpperCase():d}).replace(jf,"Moz$1")}function Jc(b){b=b.nodeType;return b===ma||!b||9===b}function Kc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Tb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(kf.exec(b)||["",""])[1].toLowerCase();d=ha[d]||ha._default;c.innerHTML=d[1]+b.replace(lf,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=Ya(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML= [...]
+return e}function P(b){if(b instanceof P)return b;var a;K(b)&&(b=N(b),a=!0);if(!(this instanceof P)){if(a&&"<"!=b.charAt(0))throw Ub("nosel");return new P(b)}if(a){a=V;var c;b=(c=mf.exec(b))?[a.createElement(c[1])]:(c=Kc(b,a))?c.childNodes:[]}Lc(this,b)}function Vb(b){return b.cloneNode(!0)}function xb(b,a){a||yb(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)yb(c[d])}function Mc(b,a,c,d){if(B(d))throw Ub("offargs");var e=(d=zb(b))&&d.events,f=d&&d.hand [...]
+function(a){if(B(c)){var d=e[a];Xa(d||[],c);if(d&&0<d.length)return}b.removeEventListener(a,f,!1);delete e[a]});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,f,!1),delete e[a]}function yb(b,a){var c=b.ng339,d=c&&Ab[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),Mc(b)),delete Ab[c],b.ng339=s))}function zb(b,a){var c=b.ng339,c=c&&Ab[c];a&&!c&&(b.ng339=c=++nf,c=Ab[c]={events:{},data:{},handle:s});return c}function Wb(b,a,c){if(Jc(b)){var d=B(c),e= [...]
+f=!a;b=(b=zb(b,!e))&&b.data;if(d)b[a]=c;else{if(f)return b;if(e)return b&&b[a];y(b,a)}}}function Bb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function Cb(b,a){a&&b.setAttribute&&q(a.split(" "),function(a){b.setAttribute("class",N((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+N(a)+" "," ")))})}function Db(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\ [...]
+q(a.split(" "),function(a){a=N(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",N(c))}}function Lc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=a.length;if("number"===typeof c&&a.window!==a){if(c)for(var d=0;d<c;d++)b[b.length++]=a[d]}else b[b.length++]=a}}function Nc(b,a){return Eb(b,"$"+(a||"ngController")+"Controller")}function Eb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=A(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if((c=G.data(b,a[d]))!==s)return c;b= [...]
+11===b.nodeType&&b.host}}function Oc(b){for(xb(b,!0);b.firstChild;)b.removeChild(b.firstChild)}function Pc(b,a){a||xb(b);var c=b.parentNode;c&&c.removeChild(b)}function of(b,a){a=a||T;if("complete"===a.document.readyState)a.setTimeout(b);else G(a).on("load",b)}function Qc(b,a){var c=Fb[a.toLowerCase()];return c&&Rc[sa(b)]&&c}function pf(b,a){var c=b.nodeName;return("INPUT"===c||"TEXTAREA"===c)&&Sc[a]}function qf(b,a){var c=function(c,e){c.isDefaultPrevented=function(){return c.defaultPre [...]
+a[e||c.type],g=f?f.length:0;if(g){if(z(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=!0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};1<g&&(f=oa(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||f[l].call(b,c)}};c.elem=b;return c}function gf(){this.$get=function(){return y(P,{hasClass:function(b,a){b.at [...]
+return Bb(b,a)},addClass:function(b,a){b.attr&&(b=b[0]);return Db(b,a)},removeClass:function(b,a){b.attr&&(b=b[0]);return Cb(b,a)}})}}function Na(b,a){var c=b&&b.$$hashKey;if(c)return"function"===typeof c&&(c=b.$$hashKey()),c;c=typeof b;return c="function"==c||"object"==c&&null!==b?b.$$hashKey=c+":"+(a||Kd)():c+":"+b}function fb(b,a){if(a){var c=0;this.nextUid=function(){return++c}}q(b,this.put,this)}function rf(b){return(b=b.toString().replace(Tc,"").match(Uc))?"function("+(b[1]||"").re [...]
+" ")+")":"fn"}function bb(b,a){function c(a){return function(b,c){if(J(b))q(b,pc(a));else return a(b,c)}}function d(a,b){Ma(a,"service");if(F(b)||A(b))b=n.instantiate(b);if(!b.$get)throw Da("pget",a);return r[a+"Provider"]=b}function e(a,b){return function(){var c=t.invoke(b,this);if(z(c))throw Da("undef",a);return c}}function f(a,b,c){return d(a,{$get:!1!==c?e(a,b):b})}function g(a){var b=[],c;q(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=n.get(e[0]);f[e [...]
+e[2])}}if(!m.get(a)){m.put(a,!0);try{K(a)?(c=db(a),b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):F(a)?b.push(n.invoke(a)):A(a)?b.push(n.invoke(a)):La(a,"module")}catch(e){throw A(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Da("modulerr",a,e.stack||e.message||e);}}});return b}function h(b,c){function d(a,e){if(b.hasOwnProperty(a)){if(b[a]===l)throw Da("cdep",a+" <- "+k.join(" <- "));return b[a] [...]
+b[a]=l,b[a]=c(a,e)}catch(f){throw b[a]===l&&delete b[a],f;}finally{k.shift()}}function e(b,c,f,g){"string"===typeof f&&(g=f,f=null);var k=[],l=bb.$$annotate(b,a,g),h,n,m;n=0;for(h=l.length;n<h;n++){m=l[n];if("string"!==typeof m)throw Da("itkn",m);k.push(f&&f.hasOwnProperty(m)?f[m]:d(m,g))}A(b)&&(b=b[h]);return b.apply(c,k)}return{invoke:e,instantiate:function(a,b,c){var d=Object.create((A(a)?a[a.length-1]:a).prototype||null);a=e(a,d,b,c);return J(a)||F(a)?a:d},get:d,annotate:bb.$$annotat [...]
+"Provider")||b.hasOwnProperty(a)}}}a=!0===a;var l={},k=[],m=new fb([],!0),r={$provide:{provider:c(d),factory:c(f),service:c(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return f(a,ca(b),!1)}),constant:c(function(a,b){Ma(a,"constant");r[a]=b;v[a]=b}),decorator:function(a,b){var c=n.get(a+"Provider"),d=c.$get;c.$get=function(){var a=t.invoke(d,c);return t.invoke(b,null,{$delegate:a})}}}},n=r.$injector=h(r,function(a,b){ba.isString(b)& [...]
+throw Da("unpr",k.join(" <- "));}),v={},t=v.$injector=h(v,function(a,b){var c=n.get(a+"Provider",b);return t.invoke(c.$get,c,s,a)});q(g(b),function(a){t.invoke(a||D)});return t}function Ge(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===sa(a))return b=a,!0});return b}function f(b){if(b){b.scrollIntoView();var c;c=g.yOffset;F(c)?c=c():rc(c)?(c=c[0], [...]
+a.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):U(c)||(c=0);c&&(b=b.getBoundingClientRect().top,a.scrollBy(0,b-c))}else a.scrollTo(0,0)}function g(){var a=c.hash(),b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=a.document;b&&d.$watch(function(){return c.hash()},function(a,b){a===b&&""===a||of(function(){d.$evalAsync(g)})});return g}]}function ff(){this.$get=["$$rAF","$timeout",function(b,a){return b.supported?functi [...]
+function(b){return a(b,0,!1)}}]}function sf(b,a,c,d){function e(a){try{a.apply(null,Za.call(arguments,1))}finally{if(p--,0===p)for(;I.length;)try{I.pop()()}catch(b){c.error(b)}}}function f(a,b){(function Xb(){q(R,function(a){a()});x=b(Xb,a)})()}function g(){h();l()}function h(){a:{try{M=v.state;break a}catch(a){}M=void 0}M=z(M)?null:M;fa(M,S)&&(M=S);S=M}function l(){if(H!==m.url()||C!==M)H=m.url(),C=M,q($,function(a){a(m.url(),M)})}function k(a){try{return decodeURIComponent(a)}catch(b){ [...]
+var m=this,r=a[0],n=b.location,v=b.history,t=b.setTimeout,w=b.clearTimeout,u={};m.isMock=!1;var p=0,I=[];m.$$completeOutstandingRequest=e;m.$$incOutstandingRequestCount=function(){p++};m.notifyWhenNoOutstandingRequests=function(a){q(R,function(a){a()});0===p?a():I.push(a)};var R=[],x;m.addPollFn=function(a){z(x)&&f(100,t);R.push(a);return a};var M,C,H=n.href,Q=a.find("base"),X=null;h();C=M;m.url=function(a,c,e){z(e)&&(e=null);n!==b.location&&(n=b.location);v!==b.history&&(v=b.history);if [...]
+C===e;if(H===a&&(!d.history||f))return m;var g=H&&Ea(H)===Ea(a);H=a;C=e;if(!d.history||g&&f){if(!g||X)X=a;c?n.replace(a):g?(c=n,e=a.indexOf("#"),a=-1===e?"":a.substr(e),c.hash=a):n.href=a}else v[c?"replaceState":"pushState"](e,"",a),h(),C=M;return m}return X||n.href.replace(/%27/g,"'")};m.state=function(){return M};var $=[],W=!1,S=null;m.onUrlChange=function(a){if(!W){if(d.history)G(b).on("popstate",g);G(b).on("hashchange",g);W=!0}$.push(a);return a};m.$$checkUrlChange=l;m.baseHref=funct [...]
+Q.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};var Fa={},B="",hb=m.baseHref();m.cookies=function(a,b){var d,e,f,g;if(a)b===s?r.cookie=encodeURIComponent(a)+"=;path="+hb+";expires=Thu, 01 Jan 1970 00:00:00 GMT":K(b)&&(d=(r.cookie=encodeURIComponent(a)+"="+encodeURIComponent(b)+";path="+hb).length+1,4096<d&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(r.cookie!==B)for(B=r.cookie,d=B.split("; "),Fa={},f=0 [...]
+d[f],g=e.indexOf("="),0<g&&(a=k(e.substring(0,g)),Fa[a]===s&&(Fa[a]=k(e.substring(g+1))));return Fa}};m.defer=function(a,b){var c;p++;c=t(function(){delete u[c];e(a)},b||0);u[c]=!0;return c};m.defer.cancel=function(a){return u[a]?(delete u[a],w(a),e(D),!0):!1}}function Ie(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new sf(b,d,a,c)}]}function Je(){this.$get=function(){function b(b,d){function e(a){a!=r&&(n?n==a&&(n=a.n):n=a,f(a.n,a.p),f(a,r),r=a,r.n=null) [...]
+b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw E("$cacheFactory")("iid",b);var g=0,h=y({},d,{id:b}),l={},k=d&&d.capacity||Number.MAX_VALUE,m={},r=null,n=null;return a[b]={put:function(a,b){if(k<Number.MAX_VALUE){var c=m[a]||(m[a]={key:a});e(c)}if(!z(b))return a in l||g++,l[a]=b,g>k&&this.remove(n.key),b},get:function(a){if(k<Number.MAX_VALUE){var b=m[a];if(!b)return;e(b)}return l[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=m[a];if(!b)return;b==r&&(r=b.p);b==n&&(n=b.n);f(b.n,b.p [...]
+g--},removeAll:function(){l={};g=0;m={};r=n=null},destroy:function(){m=h=l=null;delete a[b]},info:function(){return y({},h,{size:g})}}}var a={};b.info=function(){var b={};q(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function $e(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Bc(b,a){function c(a,b){var c=/^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/,d={};q(a,function(a,e){var f=a.match(c);if(!f)throw da("iscp",b,e,a);d[e]={mode:f[1 [...]
+f[2],optional:"?"===f[3],attrName:f[4]||e}});return d}function d(a){var b=a.charAt(0);if(!b||b!==O(b))throw da("baddir",a);return a}var e={},f=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,g=/(([\w\-]+)(?:\:([^;]+))?;?)/,h=Ld("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,k=/^(on[a-z]+|formaction)$/;this.directive=function n(a,f){Ma(a,"directive");K(a)?(d(a),Sb(f,"directiveFactory"),e.hasOwnProperty(a)||(e[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,d) [...]
+q(e[a],function(e,g){try{var h=b.invoke(e);F(h)?h={compile:ca(h)}:!h.compile&&h.link&&(h.compile=ca(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";J(h.scope)&&(h.$$isolateBindings=c(h.scope,h.name));f.push(h)}catch(l){d(l)}});return f}])),e[a].push(f)):q(a,pc(n));return this};this.aHrefSanitizationWhitelist=function(b){return B(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this [...]
+function(b){return B(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};var m=!0;this.debugInfoEnabled=function(a){return B(a)?(m=a,this):m};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,b,c,d,u,p,I,R,x,M,C){function H(a,b){try{a.addClass(b)}catch(c){}}function Q(a,b,c,d,e){a instanceof G||(a=G(a));q(a,function(b,c){b.nodeType==ab&&b.nodeVal [...]
+(a[c]=G(b).wrap("<span></span>").parent()[0])});var f=X(a,b,a,c,d,e);Q.$$addScopeClass(a);var g=null;return function(b,c,d){Sb(b,"scope");d=d||{};var e=d.parentBoundTranscludeFn,h=d.transcludeControllers;d=d.futureParentElement;e&&e.$$boundTransclude&&(e=e.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==sa(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?G(T(g,G("<div>").append(a).html())):c?Ka.clone.call(a):a;if(h)for(var l in h)d.data("$"+l+"Controller",h[l].insta [...]
+b);c&&c(d,b);f&&f(b,d,d,e);return d}}function X(a,b,c,d,e,f){function g(a,c,d,e){var f,l,k,n,m,t,w;if(p)for(w=Array(c.length),n=0;n<h.length;n+=3)f=h[n],w[f]=c[f];else w=c;n=0;for(m=h.length;n<m;)l=w[h[n++]],c=h[n++],f=h[n++],c?(c.scope?(k=a.$new(),Q.$$addScopeInfo(G(l),k)):k=a,t=c.transcludeOnThisElement?$(a,c.transclude,e,c.elementTranscludeOnThisElement):!c.templateOnThisElement&&e?e:!e&&b?$(a,b):null,c(f,k,l,d,t)):f&&f(a,l.childNodes,s,e)}for(var h=[],l,k,n,m,p,t=0;t<a.length;t++){l= [...]
+W(a[t],[],l,0===t?d:s,e);(f=k.length?B(k,a[t],l,b,c,null,[],[],f):null)&&f.scope&&Q.$$addScopeClass(l.$$element);l=f&&f.terminal||!(n=a[t].childNodes)||!n.length?null:X(n,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||l)h.push(t,f,l),m=!0,p=p||f;f=null}return m?g:null}function $(a,b,c,d){return function(d,e,f,g,h){d||(d=a.$new(!1,h),d.$$transcluded=!0);return b(d,e,{parentBoundTranscludeFn:c,transcludeControllers:f,futureParentElement:g})}}function W(a,b,c [...]
+c.$attr,l;switch(a.nodeType){case ma:z(b,va(sa(a)),"E",d,e);for(var k,n,m,p=a.attributes,t=0,w=p&&p.length;t<w;t++){var v=!1,M=!1;k=p[t];l=k.name;n=N(k.value);k=va(l);if(m=ac.test(k))l=l.replace(Wc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});var x=k.replace(/(Start|End)$/,"");E(x)&&k===x+"Start"&&(v=l,M=l.substr(0,l.length-5)+"end",l=l.substr(0,l.length-6));k=va(l.toLowerCase());h[k]=l;if(m||!c.hasOwnProperty(k))c[k]=n,Qc(a,k)&&(c[k]=!0);P(a,b,n,k,m);z(b,k,"A",d, [...]
+a.className;J(a)&&(a=a.animVal);if(K(a)&&""!==a)for(;l=g.exec(a);)k=va(l[2]),z(b,k,"C",d,e)&&(c[k]=N(l[3])),a=a.substr(l.index+l[0].length);break;case ab:ia(b,a.nodeValue);break;case 8:try{if(l=f.exec(a.nodeValue))k=va(l[1]),z(b,k,"M",d,e)&&(c[k]=N(l[2]))}catch(u){}}b.sort(xa);return b}function S(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw da("uterdir",b,c);a.nodeType==ma&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e) [...]
+return G(d)}function Fa(a,b,c){return function(d,e,f,g,h){e=S(e[0],b,c);return a(d,e,f,g,h)}}function B(a,d,e,f,g,h,k,n,m){function w(a,b,c,d){if(a){c&&(a=Fa(a,c,d));a.require=L.require;a.directiveName=ia;if(R===L||L.$$isolateScope)a=Y(a,{isolateScope:!0});k.push(a)}if(b){c&&(b=Fa(b,c,d));b.require=L.require;b.directiveName=ia;if(R===L||L.$$isolateScope)b=Y(b,{isolateScope:!0});n.push(b)}}function M(a,b,c,d){var e,f="data",g=!1,h=c,k;if(K(b)){k=b.match(l);b=b.substring(k[0].length);k[3]& [...]
+null:k[1]=k[3]);"^"===k[1]?f="inheritedData":"^^"===k[1]&&(f="inheritedData",h=c.parent());"?"===k[2]&&(g=!0);e=null;d&&"data"===f&&(e=d[b])&&(e=e.instance);e=e||h[f]("$"+b+"Controller");if(!e&&!g)throw da("ctreq",b,a);return e||null}A(b)&&(e=[],q(b,function(b){e.push(M(a,b,c,d))}));return e}function x(a,c,f,g,h){function l(a,b,c){var d;Va(a)||(c=b,b=a,a=s);D&&(d=H);c||(c=D?S.parent():S);return h(a,b,d,c,Zb)}var m,t,w,C,H,ib,S,W;d===f?(W=e,S=e.$$element):(S=G(f),W=new $b(S,e));R&&(C=c.$n [...]
+(ib=l,ib.$$boundTransclude=h);I&&($={},H={},q(I,function(a){var b={$scope:a===R||a.$$isolateScope?C:c,$element:S,$attrs:W,$transclude:ib};w=a.controller;"@"==w&&(w=W[a.name]);b=p(w,b,!0,a.controllerAs);H[a.name]=b;D||S.data("$"+a.name+"Controller",b.instance);$[a.name]=b}));if(R){Q.$$addScopeInfo(S,C,!0,!(X&&(X===R||X===R.$$originalDirective)));Q.$$addScopeClass(S,!0);g=$&&$[R.name];var ua=C;g&&g.identifier&&!0===R.bindToController&&(ua=g.instance);q(C.$$isolateBindings=R.$$isolateBindin [...]
+d){var e=a.attrName,f=a.optional,g,h,l,k;switch(a.mode){case "@":W.$observe(e,function(a){ua[d]=a});W.$$observers[e].$$scope=c;W[e]&&(ua[d]=b(W[e])(c));break;case "=":if(f&&!W[e])break;h=u(W[e]);k=h.literal?fa:function(a,b){return a===b||a!==a&&b!==b};l=h.assign||function(){g=ua[d]=h(c);throw da("nonassign",W[e],R.name);};g=ua[d]=h(c);f=function(a){k(a,ua[d])||(k(a,g)?l(c,a=ua[d]):ua[d]=a);return g=a};f.$stateful=!0;f=a.collection?c.$watchCollection(W[e],f):c.$watch(u(W[e],f),null,h.lite [...]
+f);break;case "&":h=u(W[e]),ua[d]=function(a){return h(c,a)}}})}$&&(q($,function(a){a()}),$=null);g=0;for(m=k.length;g<m;g++)t=k[g],Xc(t,t.isolateScope?C:c,S,W,t.require&&M(t.directiveName,t.require,S,H),ib);var Zb=c;R&&(R.template||null===R.templateUrl)&&(Zb=C);a&&a(Zb,f.childNodes,s,h);for(g=n.length-1;0<=g;g--)t=n[g],Xc(t,t.isolateScope?C:c,S,W,t.require&&M(t.directiveName,t.require,S,H),ib)}m=m||{};for(var C=-Number.MAX_VALUE,H,I=m.controllerDirectives,$,R=m.newIsolateScopeDirective, [...]
+Ga=m.nonTlbTranscludeDirective,z=!1,Yb=!1,D=m.hasElementTranscludeDirective,y=e.$$element=G(d),L,ia,E,gb=f,xa,qa=0,O=a.length;qa<O;qa++){L=a[qa];var P=L.$$start,jb=L.$$end;P&&(y=S(d,P,jb));E=s;if(C>L.priority)break;if(E=L.scope)L.templateUrl||(J(E)?(Oa("new/isolated scope",R||H,L,y),R=L):Oa("new/isolated scope",R,L,y)),H=H||L;ia=L.name;!L.templateUrl&&L.controller&&(E=L.controller,I=I||{},Oa("'"+ia+"' controller",I[ia],L,y),I[ia]=L);if(E=L.transclude)z=!0,L.$$tlb||(Oa("transclusion",Ga,L [...]
+"element"==E?(D=!0,C=L.priority,E=y,y=e.$$element=G(V.createComment(" "+ia+": "+e[ia]+" ")),d=y[0],U(g,Za.call(E,0),d),gb=Q(E,f,C,h&&h.name,{nonTlbTranscludeDirective:Ga})):(E=G(Vb(d)).contents(),y.empty(),gb=Q(E,f));if(L.template)if(Yb=!0,Oa("template",X,L,y),X=L,E=F(L.template)?L.template(y,e):L.template,E=Yc(E),L.replace){h=L;E=Tb.test(E)?Zc(T(L.templateNamespace,N(E))):[];d=E[0];if(1!=E.length||d.nodeType!==ma)throw da("tplrt",ia,"");U(g,y,d);O={$attr:{}};E=W(d,[],O);var tf=a.splice( [...]
+(qa+1));R&&hb(E);a=a.concat(E).concat(tf);Vc(e,O);O=a.length}else y.html(E);if(L.templateUrl)Yb=!0,Oa("template",X,L,y),X=L,L.replace&&(h=L),x=Xb(a.splice(qa,a.length-qa),y,e,g,z&&gb,k,n,{controllerDirectives:I,newIsolateScopeDirective:R,templateDirective:X,nonTlbTranscludeDirective:Ga}),O=a.length;else if(L.compile)try{xa=L.compile(y,e,gb),F(xa)?w(null,xa,P,jb):xa&&w(xa.pre,xa.post,P,jb)}catch(ac){c(ac,ta(y))}L.terminal&&(x.terminal=!0,C=Math.max(C,L.priority))}x.scope=H&&!0===H.scope;x [...]
+z;x.elementTranscludeOnThisElement=D;x.templateOnThisElement=Yb;x.transclude=gb;m.hasElementTranscludeDirective=D;return x}function hb(a){for(var b=0,c=a.length;b<c;b++)a[b]=Pb(a[b],{$$isolateScope:!0})}function z(b,d,f,g,h,k,l){if(d===h)return null;h=null;if(e.hasOwnProperty(d)){var m;d=a.get(d+"Directive");for(var p=0,w=d.length;p<w;p++)try{m=d[p],(g===s||g>m.priority)&&-1!=m.restrict.indexOf(f)&&(k&&(m=Pb(m,{$$start:k,$$end:l})),b.push(m),h=m)}catch(x){c(x)}}return h}function E(b){if( [...]
+a.get(b+"Directive"),d=0,f=c.length;d<f;d++)if(b=c[d],b.multiElement)return!0;return!1}function Vc(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,f){"class"==f?(H(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function Xb(a, [...]
+h,k){var l=[],n,m,p=b[0],t=a.shift(),x=Pb(t,{templateUrl:null,transclude:null,replace:null,$$originalDirective:t}),M=F(t.templateUrl)?t.templateUrl(b,c):t.templateUrl,v=t.templateNamespace;b.empty();d(M).then(function(d){var w,C;d=Yc(d);if(t.replace){d=Tb.test(d)?Zc(T(v,N(d))):[];w=d[0];if(1!=d.length||w.nodeType!==ma)throw da("tplrt",t.name,M);d={$attr:{}};U(e,b,w);var u=W(w,[],d);J(t.scope)&&hb(u);a=u.concat(a);Vc(c,d)}else w=p,b.html(d);a.unshift(x);n=B(a,w,c,f,b,t,g,h,k);q(e,function [...]
+w&&(e[c]=b[0])});for(m=X(b[0].childNodes,f);l.length;){d=l.shift();C=l.shift();var I=l.shift(),Q=l.shift(),u=b[0];if(!d.$$destroyed){if(C!==p){var R=C.className;k.hasElementTranscludeDirective&&t.replace||(u=Vb(w));U(I,G(C),u);H(G(u),R)}C=n.transcludeOnThisElement?$(d,n.transclude,Q):Q;n(m,d,u,e,C)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(n.transcludeOnThisElement&&(a=$(b,n.transclude,e)),n(m,b,c,d,a)))}}function xa(a,b){var c=b.priority-a.priority;retu [...]
+c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function Oa(a,b,c,d){if(b)throw da("multidir",b.name,c.name,a,ta(d));}function ia(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=a.parent();var b=!!a.length;b&&Q.$$addBindingClass(a);return function(a,c){var e=c.parent();b||Q.$$addBindingClass(e);Q.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function T(a,b){a=O(a||"html");switch(a){case "svg":case "math":var c=V.createElement("div");c [...]
+"<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function jb(a,b){if("srcdoc"==b)return x.HTML;var c=sa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||"ngSrc"==b))return x.RESOURCE_URL}function P(a,c,d,e,f){var g=jb(a,e);f=h[e]||f;var l=b(d,!0,g,f);if(l){if("multiple"===e&&"select"===sa(a))throw da("selmulti",ta(a));c.push({priority:100,compile:function(){return{pre:function(a,c,h){c=h.$$observers||(h.$$observers={});if(k.test(e))throw da("no [...]
+var n=h[e];n!==d&&(l=n&&b(n,!0,g,f),d=n);l&&(h[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(h.$$observers&&h.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!=b?h.$updateClass(a,b):h.$set(e,a)}))}}}})}}function U(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var l=a.length;g<l;g++,h++)h<l?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=V.createDocumentFragment();a.a [...]
+G(c).data(G(d).data());pa?(Rb=!0,pa.cleanData([d])):delete G.cache[d[G.expando]];d=1;for(e=b.length;d<e;d++)f=b[d],G(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function Y(a,b){return y(function(){return a.apply(null,arguments)},a,b)}function Xc(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,ta(d))}}var $b=function(a,b){if(b){var c=Object.keys(b),d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};$b.prototype={$normalize:va,$addClass:func [...]
+0<a.length&&M.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&M.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=$c(a,b);c&&c.length&&M.addClass(this.$$element,c);(c=$c(b,a))&&c.length&&M.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=this.$$element[0],g=Qc(f,a),h=pf(f,a),f=a;g?(this.$$element.prop(a,b),e=g):h&&(this[h]=b,f=h);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=yc(a,"-"));g=sa(this.$$element);if("a"===g&&"href"= [...]
+g&&"src"===a)this[a]=b=C(b,"src"===a);else if("img"===g&&"srcset"===a){for(var g="",h=N(b),l=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,l=/\s/.test(h)?l:/(,)/,h=h.split(l),l=Math.floor(h.length/2),k=0;k<l;k++)var n=2*k,g=g+C(N(h[n]),!0),g=g+(" "+N(h[n+1]));h=N(h[2*k]).split(/\s/);g+=C(N(h[0]),!0);2===h.length&&(g+=" "+N(h[1]));this[a]=b=g}!1!==d&&(null===b||b===s?this.$$element.removeAttr(e):this.$$element.attr(e,b));(a=this.$$observers)&&q(a[f],function(a){try{a(b)}catch(d){c(d)}})},$observe [...]
+b){var c=this,d=c.$$observers||(c.$$observers=ga()),e=d[a]||(d[a]=[]);e.push(b);I.$evalAsync(function(){!e.$$inter&&c.hasOwnProperty(a)&&b(c[a])});return function(){Xa(e,b)}}};var Ga=b.startSymbol(),qa=b.endSymbol(),Yc="{{"==Ga||"}}"==qa?na:function(a){return a.replace(/\{\{/g,Ga).replace(/}}/g,qa)},ac=/^ngAttr[A-Z]/;Q.$$addBindingInfo=m?function(a,b){var c=a.data("$binding")||[];A(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:D;Q.$$addBindingClass=m?function(a){H(a,"ng-binding")}:D;Q [...]
+m?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:D;Q.$$addScopeClass=m?function(a,b){H(a,b?"ng-isolate-scope":"ng-scope")}:D;return Q}]}function va(b){return eb(b.replace(Wc,""))}function $c(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),f=0;a:for(;f<d.length;f++){for(var g=d[f],h=0;h<e.length;h++)if(g==e[h])continue a;c+=(0<c.length?" ":"")+g}return c}function Zc(b){b=G(b);var a=b.length;if(1>=a)return b;for(;a--;)8===b[a].nodeType&&uf.call(b,a,1);r [...]
+{},a=!1,c=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,c){Ma(a,"controller");J(a)?y(b,a):b[a]=c};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(d,e){function f(a,b,c,d){if(!a||!J(a.$scope))throw E("$controller")("noscp",d,b);a.$scope[b]=c}return function(g,h,l,k){var m,r,n;l=!0===l;k&&K(k)&&(n=k);if(K(g)){k=g.match(c);if(!k)throw vf("ctrlfmt",g);r=k[1];n=n||k[3];g=b.hasOwnProperty(r)?b[r]:Ac(h.$scope,r,!0)||(a?Ac(e,r,!0):s);La(g,r,!0)}if(l)return l=(A( [...]
+1]:g).prototype,m=Object.create(l||null),n&&f(h,n,m,r||g.name),y(function(){d.invoke(g,m,h,r);return m},{instance:m,identifier:n});m=d.instantiate(g,h,r);n&&f(h,n,m,r||g.name);return m}}]}function Le(){this.$get=["$window",function(b){return G(b.document)}]}function Me(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function bc(b,a){if(K(b)){var c=b.replace(wf,"").trim();if(c){var d=a("Content-Type");(d=d&&0===d.indexOf(ad))||(d=(d=c.match(xf))&&yf[d[0] [...]
+d&&(b=tc(c))}}return b}function bd(b){var a=ga(),c,d,e;if(!b)return a;q(b.split("\n"),function(b){e=b.indexOf(":");c=O(N(b.substr(0,e)));d=N(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function cd(b){var a=J(b)?b:s;return function(c){a||(a=bd(b));return c?(c=a[O(c)],void 0===c&&(c=null),c):a}}function dd(b,a,c,d){if(F(d))return d(b,a,c);q(d,function(d){b=d(b,a,c)});return b}function Pe(){var b=this.defaults={transformResponse:[bc],transformRequest:[function(a){return J(a)&&"[o [...]
+Aa.call(a)&&"[object Blob]"!==Aa.call(a)&&"[object FormData]"!==Aa.call(a)?$a(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:oa(cc),put:oa(cc),patch:oa(cc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},a=!1;this.useApplyAsync=function(b){return B(b)?(a=!!b,this):a};var c=this.interceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(d,e,f,g,h,l){function k(a){function c(a){var b=y({},a);b.data=a.dat [...]
+a.headers,a.status,e.transformResponse):a.data;a=a.status;return 200<=a&&300>a?b:h.reject(b)}function d(a){var b,c={};q(a,function(a,d){F(a)?(b=a(),null!=b&&(c[d]=b)):c[d]=a});return c}if(!ba.isObject(a))throw E("$http")("badreq",a);var e=y({method:"get",transformRequest:b.transformRequest,transformResponse:b.transformResponse},a);e.headers=function(a){var c=b.headers,e=y({},a.headers),f,g,c=y({},c.common,c[O(a.method)]);a:for(f in c){a=O(f);for(g in e)if(O(g)===a)continue a;e[f]=c[f]}re [...]
+e.method=vb(e.method);var f=[function(a){var d=a.headers,e=dd(a.data,cd(d),s,a.transformRequest);z(e)&&q(d,function(a,b){"content-type"===O(b)&&delete d[b]});z(a.withCredentials)&&!z(b.withCredentials)&&(a.withCredentials=b.withCredentials);return m(a,e).then(c,c)},s],g=h.when(e);for(q(v,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var l=f.shift(),g=g.then(a,l)}g.suc [...]
+"fn");g.then(function(b){a(b.data,b.status,b.headers,e)});return g};g.error=function(a){La(a,"fn");g.then(null,function(b){a(b.data,b.status,b.headers,e)});return g};return g}function m(c,f){function l(b,c,d,e){function f(){m(c,b,d,e)}C&&(200<=b&&300>b?C.put(q,[b,c,bd(d),e]):C.remove(q));a?g.$applyAsync(f):(f(),g.$$phase||g.$apply())}function m(a,b,d,e){b=Math.max(b,0);(200<=b&&300>b?x.resolve:x.reject)({data:a,status:b,headers:cd(d),config:c,statusText:e})}function v(a){m(a.data,a.statu [...]
+a.statusText)}function R(){var a=k.pendingRequests.indexOf(c);-1!==a&&k.pendingRequests.splice(a,1)}var x=h.defer(),M=x.promise,C,H,Q=c.headers,q=r(c.url,c.params);k.pendingRequests.push(c);M.then(R,R);!c.cache&&!b.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(C=J(c.cache)?c.cache:J(b.cache)?b.cache:n);C&&(H=C.get(q),B(H)?H&&F(H.then)?H.then(v,v):A(H)?m(H[1],H[0],oa(H[2]),H[3]):m(H,200,{},"OK"):C.put(q,M));z(H)&&((H=ed(c.url)?e.cookies()[c.xsrfCookieName||b.xsrfCookieName]: [...]
+b.xsrfHeaderName]=H),d(c.method,q,f,l,Q,c.timeout,c.withCredentials,c.responseType));return M}function r(a,b){if(!b)return a;var c=[];Jd(b,function(a,b){null===a||z(a)||(A(a)||(a=[a]),q(a,function(a){J(a)&&(a=ea(a)?a.toISOString():$a(a));c.push(Ca(b)+"="+Ca(a))}))});0<c.length&&(a+=(-1==a.indexOf("?")?"?":"&")+c.join("&"));return a}var n=f("$http"),v=[];q(c,function(a){v.unshift(K(a)?l.get(a):l.invoke(a))});k.pendingRequests=[];(function(a){q(arguments,function(a){k[a]=function(b,c){retu [...]
+{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){k[a]=function(b,c,d){return k(y(d||{},{method:a,url:b,data:c}))}})})("post","put","patch");k.defaults=b;return k}]}function zf(){return new T.XMLHttpRequest}function Qe(){this.$get=["$browser","$window","$document",function(b,a,c){return Af(b,zf,b.defer,a.angular.callbacks,c[0])}]}function Af(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),m=null;f.type="text/javascript";f.src=a;f. [...]
+m=function(a){f.removeEventListener("load",m,!1);f.removeEventListener("error",m,!1);e.body.removeChild(f);f=null;var g=-1,v="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),v=a.type,g="error"===a.type?404:200);c&&c(g,v)};f.addEventListener("load",m,!1);f.addEventListener("error",m,!1);e.body.appendChild(f);return m}return function(e,h,l,k,m,r,n,v){function t(){p&&p();I&&I.abort()}function w(a,d,e,f,g){x!==s&&c.cancel(x);p=I=null;a(d,e,f,g);b.$$completeOutstandingRequest(D) [...]
+h=h||b.url();if("jsonp"==O(e)){var u="_"+(d.counter++).toString(36);d[u]=function(a){d[u].data=a;d[u].called=!0};var p=f(h.replace("JSON_CALLBACK","angular.callbacks."+u),u,function(a,b){w(k,a,d[u].data,"",b);d[u]=D})}else{var I=a();I.open(e,h,!0);q(m,function(a,b){B(a)&&I.setRequestHeader(b,a)});I.onload=function(){var a=I.statusText||"",b="response"in I?I.response:I.responseText,c=1223===I.status?204:I.status;0===c&&(c=b?200:"file"==ya(h).protocol?404:0);w(k,c,b,I.getAllResponseHeaders [...]
+function(){w(k,-1,null,null,"")};I.onerror=e;I.onabort=e;n&&(I.withCredentials=!0);if(v)try{I.responseType=v}catch(R){if("json"!==v)throw R;}I.send(l||null)}if(0<r)var x=c(t,r);else r&&F(r.then)&&r.then(t)}}function Ne(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function f(a){return"\\\\\\"+a}function g(f,g,v,t){function w(c){return c.replace(k,b).r [...]
+a)}function u(a){try{var b=a;a=v?e.getTrusted(v,b):e.valueOf(b);var c;if(t&&!B(a))c=a;else if(null==a)c="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=$a(a)}c=a}return c}catch(g){c=dc("interr",f,g.toString()),d(c)}}t=!!t;for(var p,I,q=0,x=[],M=[],C=f.length,H=[],Q=[];q<C;)if(-1!=(p=f.indexOf(b,q))&&-1!=(I=f.indexOf(a,p+h)))q!==p&&H.push(w(f.substring(q,p))),q=f.substring(p+h,I),x.push(q),M.push(c(q,u)),q=I+l,Q.push(H.length),H.push("");else{q!==C&&H.pus [...]
+break}if(v&&1<H.length)throw dc("noconcat",f);if(!g||x.length){var X=function(a){for(var b=0,c=x.length;b<c;b++){if(t&&z(a[b]))return;H[Q[b]]=a[b]}return H.join("")};return y(function(a){var b=0,c=x.length,e=Array(c);try{for(;b<c;b++)e[b]=M[b](a);return X(e)}catch(g){a=dc("interr",f,g.toString()),d(a)}},{exp:f,expressions:x,$$watchDelegate:function(a,b,c){var d;return a.$watchGroup(M,function(c,e){var f=X(c);F(b)&&b.call(this,f,c!==e?d:f,a);d=f},c)}})}}var h=b.length,l=a.length,k=new Reg [...]
+f),"g"),m=new RegExp(a.replace(/./g,f),"g");g.startSymbol=function(){return b};g.endSymbol=function(){return a};return g}]}function Oe(){this.$get=["$rootScope","$window","$q","$$q",function(b,a,c,d){function e(e,h,l,k){var m=a.setInterval,r=a.clearInterval,n=0,v=B(k)&&!k,t=(v?d:c).defer(),w=t.promise;l=B(l)?l:0;w.then(null,null,e);w.$$intervalId=m(function(){t.notify(n++);0<l&&n>=l&&(t.resolve(n),r(w.$$intervalId),delete f[w.$$intervalId]);v||b.$apply()},h);f[w.$$intervalId]=t;return w} [...]
+e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete f[b.$$intervalId],!0):!1};return e}]}function Wd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME [...]
+SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a",ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"]},pluralCat:function(b){return 1===b?" [...]
+function ec(b){b=b.split("/");for(var a=b.length;a--;)b[a]=sb(b[a]);return b.join("/")}function fd(b,a){var c=ya(b);a.$$protocol=c.protocol;a.$$host=c.hostname;a.$$port=aa(c.port)||Bf[c.protocol]||null}function gd(b,a){var c="/"!==b.charAt(0);c&&(b="/"+b);var d=ya(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search=vc(d.search);a.$$hash=decodeURIComponent(d.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function wa [...]
+a.indexOf(b))return a.substr(b.length)}function Ea(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Gb(b){return b.replace(/(#.+)|#$/,"$1")}function fc(b){return b.substr(0,Ea(b).lastIndexOf("/")+1)}function gc(b,a){this.$$html5=!0;a=a||"";var c=fc(b);fd(b,this);this.$$parse=function(a){var b=wa(c,a);if(!K(b))throw Hb("ipthprfx",a,c);gd(b,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Qb(this.$$search),b=this.$$hash?"#"+sb(this.$$hash): [...]
+ec(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;(f=wa(b,d))!==s?(g=f,g=(f=wa(a,f))!==s?c+(wa("/",f)||f):b+g):(f=wa(c,d))!==s?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function hc(b,a){var c=fc(b);fd(b,this);this.$$parse=function(d){var e=wa(b,d)||wa(c,d),f;z(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",z(e)&&(b=d,this.replace())):(f=wa(a,e),z(f)&&(f=e));gd(f,this);d=t [...]
+var e=b,g=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&(f=f.replace(e,""));g.exec(f)||(d=(f=g.exec(d))?f[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=Qb(this.$$search),e=this.$$hash?"#"+sb(this.$$hash):"";this.$$url=ec(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ea(b)==Ea(a)?(this.$$parse(a),!0):!1}}function hd(b,a){this.$$html5=!0;hc.apply(this,arguments);var c=fc(b);this.$$parseLinkUrl=function(d,e){ [...]
+e[0])return this.hash(e.slice(1)),!0;var f,g;b==Ea(d)?f=d:(g=wa(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=Qb(this.$$search),e=this.$$hash?"#"+sb(this.$$hash):"";this.$$url=ec(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function Ib(b){return function(){return this[b]}}function id(b,a){return function(c){if(z(c))return this[b];this[b]=a(c);this.$$compose();return this}}function Re(){var b="",a={enabled:!1,requireBase:!0,rewri [...]
+this.hashPrefix=function(a){return B(a)?(b=a,this):b};this.html5Mode=function(b){return Wa(b)?(a.enabled=b,this):J(b)?(Wa(b.enabled)&&(a.enabled=b.enabled),Wa(b.requireBase)&&(a.requireBase=b.requireBase),Wa(b.rewriteLinks)&&(a.rewriteLinks=b.rewriteLinks),this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(c,d,e,f,g){function h(a,b,c){var e=k.url(),f=k.$$state;try{d.url(a,b,c),k.$$state=d.state()}catch(g){throw k.url(e),k.$$state=f,g;}}function l(a,b [...]
+k.absUrl(),a,k.$$state,b)}var k,m;m=d.baseHref();var r=d.url(),n;if(a.enabled){if(!m&&a.requireBase)throw Hb("nobase");n=r.substring(0,r.indexOf("/",r.indexOf("//")+2))+(m||"/");m=e.history?gc:hd}else n=Ea(r),m=hc;k=new m(n,"#"+b);k.$$parseLinkUrl(r,r);k.$$state=d.state();var v=/^\s*(javascript|mailto):/i;f.on("click",function(b){if(a.rewriteLinks&&!b.ctrlKey&&!b.metaKey&&!b.shiftKey&&2!=b.which&&2!=b.button){for(var e=G(b.target);"a"!==sa(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return [...]
+l=e.attr("href")||e.attr("xlink:href");J(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=ya(h.animVal).href);v.test(h)||!h||e.attr("target")||b.isDefaultPrevented()||!k.$$parseLinkUrl(h,l)||(b.preventDefault(),k.absUrl()!=d.url()&&(c.$apply(),g.angular["ff-684208-preventDefault"]=!0))}});Gb(k.absUrl())!=Gb(r)&&d.url(k.absUrl(),!0);var t=!0;d.onUrlChange(function(a,b){c.$evalAsync(function(){var d=k.absUrl(),e=k.$$state,f;k.$$parse(a);k.$$state=b;f=c.$broadcast("$locationChangeStart", [...]
+k.absUrl()===a&&(f?(k.$$parse(d),k.$$state=e,h(d,!1,e)):(t=!1,l(d,e)))});c.$$phase||c.$digest()});c.$watch(function(){var a=Gb(d.url()),b=Gb(k.absUrl()),f=d.state(),g=k.$$replace,n=a!==b||k.$$html5&&e.history&&f!==k.$$state;if(t||n)t=!1,c.$evalAsync(function(){var b=k.absUrl(),d=c.$broadcast("$locationChangeStart",b,a,k.$$state,f).defaultPrevented;k.absUrl()===b&&(d?(k.$$parse(a),k.$$state=f):(n&&h(b,g,f===k.$$state?null:k.$$state),l(a,f)))});k.$$replace=!1});return k}]}function Se(){var [...]
+this.debugEnabled=function(a){return B(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||D;a=!1;try{a=!!e.apply}catch(l){}return a?function(){var a=[];q(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}retur [...]
+info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function ra(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw ja("isecfld",a);return b}function ka(b,a){if(b){if(b.constructor===b)throw ja("isecfn",a);if(b.window===b)throw ja("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw ja("isecdom",a);if(b===Object)throw [...]
+a);}return b}function ic(b){return b.constant}function kb(b,a,c,d,e){ka(b,e);ka(a,e);c=c.split(".");for(var f,g=0;1<c.length;g++){f=ra(c.shift(),e);var h=0===g&&a&&a[f]||b[f];h||(h={},b[f]=h);b=ka(h,e)}f=ra(c.shift(),e);ka(b[f],e);return b[f]=d}function Pa(b){return"constructor"==b}function jd(b,a,c,d,e,f,g){ra(b,f);ra(a,f);ra(c,f);ra(d,f);ra(e,f);var h=function(a){return ka(a,f)},l=g||Pa(b)?h:na,k=g||Pa(a)?h:na,m=g||Pa(c)?h:na,r=g||Pa(d)?h:na,n=g||Pa(e)?h:na;return function(f,g){var h=g [...]
+g:f;if(null==h)return h;h=l(h[b]);if(!a)return h;if(null==h)return s;h=k(h[a]);if(!c)return h;if(null==h)return s;h=m(h[c]);if(!d)return h;if(null==h)return s;h=r(h[d]);return e?null==h?s:h=n(h[e]):h}}function Cf(b,a){return function(c,d){return b(c,d,ka,a)}}function Df(b,a,c){var d=a.expensiveChecks,e=d?Ef:Ff,f=e[b];if(f)return f;var g=b.split("."),h=g.length;if(a.csp)f=6>h?jd(g[0],g[1],g[2],g[3],g[4],c,d):function(a,b){var e=0,f;do f=jd(g[e++],g[e++],g[e++],g[e++],g[e++],c,d)(a,b),b=s, [...]
+h);return f};else{var l="";d&&(l+="s = eso(s, fe);\nl = eso(l, fe);\n");var k=d;q(g,function(a,b){ra(a,c);var e=(b?"s":'((l&&l.hasOwnProperty("'+a+'"))?l:s)')+"."+a;if(d||Pa(a))e="eso("+e+", fe)",k=!0;l+="if(s == null) return undefined;\ns="+e+";\n"});l+="return s;";a=new Function("s","l","eso","fe",l);a.toString=ca(l);k&&(a=Cf(a,c));f=a}f.sharedGetter=!0;f.assign=function(a,c,d){return kb(a,d,b,c,b)};return e[b]=f}function jc(b){return F(b.valueOf)?b.valueOf():Gf.call(b)}function Te(){v [...]
+a=ga();this.$get=["$filter","$sniffer",function(c,d){function e(a){var b=a;a.sharedGetter&&(b=function(b,c){return a(b,c)},b.literal=a.literal,b.constant=a.constant,b.assign=a.assign);return b}function f(a,b){for(var c=0,d=a.length;c<d;c++){var e=a[c];e.constant||(e.inputs?f(e.inputs,b):-1===b.indexOf(e)&&b.push(e))}return b}function g(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=jc(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function h(a,b,c,d){var e=d.$$inputs||(d.$$in [...]
+[])),h;if(1===e.length){var l=g,e=e[0];return a.$watch(function(a){var b=e(a);g(b,l)||(h=d(a),l=b&&jc(b));return h},b,c)}for(var k=[],n=0,m=e.length;n<m;n++)k[n]=g;return a.$watch(function(a){for(var b=!1,c=0,f=e.length;c<f;c++){var l=e[c](a);if(b||(b=!g(l,k[c])))k[c]=l&&jc(l)}b&&(h=d(a));return h},b,c)}function l(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;F(b)&&b.apply(this,arguments);B(a)&&d.$$postDigest(function(){B(f)&&e()})},c)}function k(a,b,c,d [...]
+!0;q(a,function(a){B(a)||(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,c,d){g=a;F(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function m(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){F(b)&&b.apply(this,arguments);e()},c)}function r(a,b){if(!b)return a;var c=a.$$watchDelegate,c=c!==k&&c!==l?function(c,d){var e=a(c,d);return b(e,c,d)}:function(c,d){var e=a(c,d),f=b(e,c,d);return B(e)?f:e};a.$$watchDele [...]
+h?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=h,c.inputs=[a]);return c}var n={csp:d.csp,expensiveChecks:!1},v={csp:d.csp,expensiveChecks:!0};return function(d,f,g){var p,q,s;switch(typeof d){case "string":s=d=d.trim();var x=g?a:b;p=x[s];p||(":"===d.charAt(0)&&":"===d.charAt(1)&&(q=!0,d=d.substring(2)),g=g?v:n,p=new kc(g),p=(new lb(p,c,g)).parse(d),p.constant?p.$$watchDelegate=m:q?(p=e(p),p.$$watchDelegate=p.literal?k:l):p.inputs&&(p.$$watchDelegate=h),x[s]=p);retu [...]
+case "function":return r(d,f);default:return r(D,f)}}}]}function Ve(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return kd(function(a){b.$evalAsync(a)},a)}]}function We(){this.$get=["$browser","$exceptionHandler",function(b,a){return kd(function(a){b.defer(a)},a)}]}function kd(b,a){function c(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function d(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function [...]
+c.pending&&(c.processScheduled=!0,b(function(){var b,d,e;e=c.pending;c.processScheduled=!1;c.pending=s;for(var f=0,g=e.length;f<g;++f){d=e[f][0];b=e[f][c.status];try{F(b)?d.resolve(b(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),a(h)}}}))}function g(){this.promise=new d;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var h=E("$q",TypeError);d.prototype={then:function(a,b,c){var d=new g;this.$$state.pendi [...]
+[];this.$$state.pending.push([d,a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return k(b,!0,a)},function(b){return k(b,!1,a)},b)}};g.prototype={resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(h("qcycle",a)):this.$$resolve(a))},$$resolve:function(b){var d,e;e=c(this,this.$$resolve,this.$$reject);try{if(J(b)||F(b))d=b&&b.then;F(d)?(this.prom [...]
+-1,d.call(b,e[0],e[1],this.notify)):(this.promise.$$state.value=b,this.promise.$$state.status=1,f(this.promise.$$state))}catch(g){e[1](g),a(g)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,g=d.length;f<g;f++){e=d[f][0];b=d[f][3];try{e.no [...]
+b(c):c)}catch(h){a(h)}}})}};var l=function(a,b){var c=new g;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{F(c)&&(d=c())}catch(e){return l(e,!1)}return d&&F(d.then)?d.then(function(){return l(a,b)},function(a){return l(a,!1)}):l(a,b)},m=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},r=function v(a){if(!F(a))throw h("norslvr",a);if(!(this instanceof v))return new v(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)}) [...]
+r.defer=function(){return new g};r.reject=function(a){var b=new g;b.reject(a);return b.promise};r.when=m;r.all=function(a){var b=new g,c=0,d=A(a)?[]:{};q(a,function(a,e){c++;m(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return r}function ef(){this.$get=["$window","$timeout",function(b,a){function c(){for(var a=0;a<m.length;a++){var b=m[a];b&&(m[a]=null,b())}k=m.length=0}functio [...]
+m.length;k++;m.push(a);0===b&&(l=h(c));return function(){0<=b&&(b=m[b]=null,0===--k&&l&&(l(),l=null,m.length=0))}}var e=b.requestAnimationFrame||b.webkitRequestAnimationFrame,f=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,g=!!e,h=g?function(a){var b=e(a);return function(){f(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};d.supported=g;var l,k=0,m=[];return d}]}function Ue(){function b(a){function b(){this.$$watchers=this.$ [...]
+this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$id=++rb;this.$$ChildScope=null}b.prototype=a;return b}var a=10,c=E("$rootScope"),d=null,e=null;this.digestTtl=function(b){arguments.length&&(a=b);return a};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(f,g,h,l){function k(a){a.currentScope.$$destroyed=!0}function m(){this.$id=++rb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHea [...]
+null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$isolateBindings=null}function r(a){if(u.$$phase)throw c("inprog",u.$$phase);u.$$phase=a}function n(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function v(){}function t(){for(;s.length;)try{s.shift()()}catch(a){g(a)}e=null}function w(){null===e&&(e=l.defer(function(){u.$apply(t)}))}m.prototype={constructor:m,$new:function(a,c){var d;c=c [...]
+(d=new m,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=b(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(a||c!=this)&&d.$on("$destroy",k);return d},$watch:function(a,b,c){var e=h(a);if(e.$$watchDelegate)return e.$$watchDelegate(this,b,c,e);var f=this.$$watchers,g={fn:b,last:v,get:e,exp:a,eq:!!c};d=null;F(b)||(g.fn=D);f||(f=this.$$watchers=[]);f.unshift(g);re [...]
+g);d=null}},$watchGroup:function(a,b){function c(){h=!1;l?(l=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,l=!0;if(!a.length){var k=!0;g.$evalAsync(function(){k&&b(e,e,g)});return function(){k=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});q(a,function(a,b){var l=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(l)});return function(){for(;f.length;)f.shift()()}},$watchCollection:functi [...]
+a;var b,d,g,h;if(!z(e)){if(J(e))if(Sa(e))for(f!==n&&(f=n,v=f.length=0,k++),a=e.length,v!==a&&(k++,f.length=v=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(k++,f[b]=g);else{f!==r&&(f=r={},v=0,k++);a=0;for(b in e)e.hasOwnProperty(b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(k++,f[b]=g)):(v++,f[b]=g,k++));if(v>a)for(b in k++,f)e.hasOwnProperty(b)||(v--,delete f[b])}else f!==e&&(f=e,k++);return k}}c.$stateful=!0;var d=this,e,f,g,l=1<b.length,k=0,m=h(a,c),n=[],r={},p=!0,v [...]
+function(){p?(p=!1,b(e,e,d)):b(e,g,d);if(l)if(J(e))if(Sa(e)){g=Array(e.length);for(var a=0;a<e.length;a++)g[a]=e[a]}else for(a in g={},e)wc.call(e,a)&&(g[a]=e[a]);else g=e})},$digest:function(){var b,f,h,k,m,n,q=a,w,S=[],s,B;r("$digest");l.$$checkUrlChange();this===u&&null!==e&&(l.defer.cancel(e),t());d=null;do{n=!1;for(w=this;p.length;){try{B=p.shift(),B.scope.$eval(B.expression,B.locals)}catch(R){g(R)}d=null}a:do{if(k=w.$$watchers)for(m=k.length;m--;)try{if(b=k[m])if((f=b.get(w))!==(h= [...]
+!(b.eq?fa(f,h):"number"===typeof f&&"number"===typeof h&&isNaN(f)&&isNaN(h)))n=!0,d=b,b.last=b.eq?Ba(f,null):f,b.fn(f,h===v?f:h,w),5>q&&(s=4-q,S[s]||(S[s]=[]),S[s].push({msg:F(b.exp)?"fn: "+(b.exp.name||b.exp.toString()):b.exp,newVal:f,oldVal:h}));else if(b===d){n=!1;break a}}catch(z){g(z)}if(!(k=w.$$childHead||w!==this&&w.$$nextSibling))for(;w!==this&&!(k=w.$$nextSibling);)w=w.$parent}while(w=k);if((n||p.length)&&!q--)throw u.$$phase=null,c("infdig",a,S);}while(n||p.length);for(u.$$phas [...]
+$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;if(this!==u){for(var b in this.$$listenerCount)n(this,this.$$listenerCount[b],b);a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=thi [...]
+this.$applyAsync=D;this.$on=this.$watch=this.$watchGroup=function(){return D};this.$$listeners={};this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=this.$$watchers=null}}},$eval:function(a,b){return h(a)(this,b)},$evalAsync:function(a,b){u.$$phase||p.length||l.defer(function(){p.length&&u.$digest()});p.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){I.push(a)},$apply:function(a){try{return r("$apply"),this.$eval(a)}catch(b) [...]
+null;try{u.$digest()}catch(c){throw g(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&s.push(b);w()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,n(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,f=!1,h={name:a,targetScope:e,stopPropagation:function(){f=!0},pr [...]
+!0},defaultPrevented:!1},l=Ya([h],arguments,1),k,m;do{d=e.$$listeners[a]||c;h.currentScope=e;k=0;for(m=d.length;k<m;k++)if(d[k])try{d[k].apply(null,l)}catch(n){g(n)}else d.splice(k,1),k--,m--;if(f)return h.currentScope=null,h;e=e.$parent}while(e);h.currentScope=null;return h},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var f=Ya([e],arguments,1),h,l;c=d; [...]
+c;d=c.$$listeners[a]||[];h=0;for(l=d.length;h<l;h++)if(d[h])try{d[h].apply(null,f)}catch(k){g(k)}else d.splice(h,1),h--,l--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var u=new m,p=u.$$asyncQueue=[],I=u.$$postDigestQueue=[],s=u.$$applyAsyncQueue=[];return u}]}function Xd(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizatio [...]
+function(a){return B(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;f=ya(c).href;return""===f||f.match(e)?c:"unsafe:"+f}}}function Hf(b){if("self"===b)return b;if(K(b)){if(-1<b.indexOf("***"))throw za("iwcard",b);b=ld(b).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(Ua(b))return new RegExp("^"+b.source+"$");throw za("imatcher");}function md(b){var a=[];B(b [...]
+return a}function Ye(){this.SCE_CONTEXTS=la;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=md(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=md(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?ed(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toS [...]
+return b}var f=function(a){throw za("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var g=e(),h={};h[la.HTML]=e(g);h[la.CSS]=e(g);h[la.URL]=e(g);h[la.JS]=e(g);h[la.RESOURCE_URL]=e(h[la.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw za("icontext",a,b);if(null===b||b===s||""===b)return b;if("string"!==typeof b)throw za("itype",a);return new c(b)},getTrusted:function(c,e){if(null===e||e===s||""===e)return e;var g=h.hasOwnProperty(c)?h[c]:null;if(g& [...]
+g)return e.$$unwrapTrustedValue();if(c===la.RESOURCE_URL){var g=ya(e.toString()),r,n,v=!1;r=0;for(n=b.length;r<n;r++)if(d(b[r],g)){v=!0;break}if(v)for(r=0,n=a.length;r<n;r++)if(d(a[r],g)){v=!1;break}if(v)return e;throw za("insecurl",e.toString());}if(c===la.HTML)return f(e);throw za("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function Xe(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sceDelegate",func [...]
+8>Qa)throw za("iequirks");var d=oa(la);d.isEnabled=function(){return b};d.trustAs=c.trustAs;d.getTrusted=c.getTrusted;d.valueOf=c.valueOf;b||(d.trustAs=d.getTrusted=function(a,b){return b},d.valueOf=na);d.parseAs=function(b,c){var e=a(c);return e.literal&&e.constant?e:a(c,function(a){return d.getTrusted(b,a)})};var e=d.parseAs,f=d.getTrusted,g=d.trustAs;q(la,function(a,b){var c=O(b);d[eb("parse_as_"+c)]=function(b){return e(a,b)};d[eb("get_trusted_"+c)]=function(b){return f(a,b)};d[eb("t [...]
+c)]=function(b){return g(a,b)}});return d}]}function Ze(){this.$get=["$window","$document",function(b,a){var c={},d=aa((/android (\d+)/.exec(O((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,l=f.body&&f.body.style,k=!1,m=!1;if(l){for(var r in l)if(k=h.exec(r)){g=k[0];g=g.substr(0,1).toUpperCase()+g.substr(1);break}g||(g="WebkitOpacity"in l&&"webkit");k=!!("transition"in l||g+"Transition"in l);m=!!("animation"i [...]
+l);!d||k&&m||(k=K(f.body.style.webkitTransition),m=K(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hasEvent:function(a){if("input"===a&&11>=Qa)return!1;if(z(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:cb(),vendorPrefix:g,transitions:k,animations:m,android:d}}]}function af(){this.$get=["$templateCache","$http","$q","$sce",function(b,a,c,d){function e(f,g){e.totalPendingRequests++;K(f)&&b.get(f)||(f=d.getTrustedResourceU [...]
+a.defaults&&a.defaults.transformResponse;A(h)?h=h.filter(function(a){return a!==bc}):h===bc&&(h=null);return a.get(f,{cache:b,transformResponse:h})["finally"](function(){e.totalPendingRequests--}).then(function(a){return a.data},function(a){if(!g)throw da("tpload",f);return c.reject(a)})}e.totalPendingRequests=0;return e}]}function bf(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var g=[];q(a, [...]
+ba.element(a).data("$binding");d&&q(d,function(d){c?(new RegExp("(^|\\s)"+ld(b)+"(\\s|\\||$)")).test(d)&&g.push(a):-1!=d.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,c){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var l=a.querySelectorAll("["+g[h]+"model"+(c?"=":"*=")+'"'+b+'"]');if(l.length)return l}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}} [...]
+["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b,a,c,d,e){function f(f,l,k){var m=B(k)&&!k,r=(m?d:c).defer(),n=r.promise;l=a.defer(function(){try{r.resolve(f())}catch(a){r.reject(a),e(a)}finally{delete g[n.$$timeoutId]}m||b.$apply()},l);n.$$timeoutId=l;g[l]=r;return n}var g={};f.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),delete g[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return f}]}function ya(b){Qa&&(Y.setAttribute("hre [...]
+Y.setAttribute("href",b);return{href:Y.href,protocol:Y.protocol?Y.protocol.replace(/:$/,""):"",host:Y.host,search:Y.search?Y.search.replace(/^\?/,""):"",hash:Y.hash?Y.hash.replace(/^#/,""):"",hostname:Y.hostname,port:Y.port,pathname:"/"===Y.pathname.charAt(0)?Y.pathname:"/"+Y.pathname}}function ed(b){b=K(b)?ya(b):b;return b.protocol===nd.protocol&&b.host===nd.host}function df(){this.$get=ca(T)}function Ic(b){function a(c,d){if(J(c)){var e={};q(c,function(b,c){e[c]=a(c,b)});return e}retur [...]
+"Filter",d)}this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];a("currency",od);a("date",pd);a("filter",If);a("json",Jf);a("limitTo",Kf);a("lowercase",Lf);a("number",qd);a("orderBy",rd);a("uppercase",Mf)}function If(){return function(b,a,c){if(!A(b))return b;var d;switch(null!==a?typeof a:"null"){case "function":break;case "boolean":case "null":case "number":case "string":d=!0;case "object":a=Nf(a,c,d);break;default:return b}return b.filter( [...]
+a,c){var d=J(b)&&"$"in b;!0===a?a=fa:F(a)||(a=function(a,b){if(z(a))return!1;if(null===a||null===b)return a===b;if(J(a)||J(b))return!1;a=O(""+a);b=O(""+b);return-1!==a.indexOf(b)});return function(e){return d&&!J(e)?Ha(e,b.$,a,!1):Ha(e,b,a,c)}}function Ha(b,a,c,d,e){var f=null!==b?typeof b:"null",g=null!==a?typeof a:"null";if("string"===g&&"!"===a.charAt(0))return!Ha(b,a.substring(1),c,d);if(A(b))return b.some(function(b){return Ha(b,a,c,d)});switch(f){case "object":var h;if(d){for(h in  [...]
+h.charAt(0)&&Ha(b[h],a,c,!0))return!0;return e?!1:Ha(b,a,c,!1)}if("object"===g){for(h in a)if(e=a[h],!F(e)&&!z(e)&&(f="$"===h,!Ha(f?b:b[h],e,c,f,f)))return!1;return!0}return c(b,a);case "function":return!1;default:return c(b,a)}}function od(b){var a=b.NUMBER_FORMATS;return function(b,d,e){z(d)&&(d=a.CURRENCY_SYM);z(e)&&(e=a.PATTERNS[1].maxFrac);return null==b?b:sd(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,e).replace(/\u00A4/g,d)}}function qd(b){var a=b.NUMBER_FORMATS;return function(b,d) [...]
+b?b:sd(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function sd(b,a,c,d,e){if(!isFinite(b)||J(b))return"";var f=0>b;b=Math.abs(b);var g=b+"",h="",l=[],k=!1;if(-1!==g.indexOf("e")){var m=g.match(/([\d\.]+)e(-?)(\d+)/);m&&"-"==m[2]&&m[3]>e+1?b=0:(h=g,k=!0)}if(k)0<e&&1>b&&(h=b.toFixed(e),b=parseFloat(h));else{g=(g.split(td)[1]||"").length;z(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);var g=(""+b).split(td),k=g[0],g=g[1]||"",r=0 [...]
+v=a.gSize;if(k.length>=n+v)for(r=k.length-n,m=0;m<r;m++)0===(r-m)%v&&0!==m&&(h+=c),h+=k.charAt(m);for(m=r;m<k.length;m++)0===(k.length-m)%n&&0!==m&&(h+=c),h+=k.charAt(m);for(;g.length<e;)g+="0";e&&"0"!==e&&(h+=d+g.substr(0,e))}0===b&&(f=!1);l.push(f?a.negPre:a.posPre,h,f?a.negSuf:a.posSuf);return l.join("")}function Jb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function Z(b,a,c,d){c=c||0;return function(e){e=e["get"+b]();if(0<c [...]
+c;0===e&&-12==c&&(e=12);return Jb(e,a,d)}}function Kb(b,a){return function(c,d){var e=c["get"+b](),f=vb(a?"SHORT"+b:b);return d[f][e]}}function ud(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function vd(b){return function(a){var c=ud(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return Jb(a,b)}}function lc(b,a){return 0>=b.getFullYear()?a.ERAS[0]:a.ERAS[1]}function pd(b){function a(a){var b;if( [...]
+new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear,l=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=aa(b[9]+b[10]),g=aa(b[9]+b[11]));h.call(a,aa(b[1]),aa(b[2])-1,aa(b[3]));f=aa(b[4]||0)-f;g=aa(b[5]||0)-g;h=aa(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));l.call(a,f,g,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var g="",h=[],l,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;K(c) [...]
+aa(c):a(c));U(c)&&(c=new Date(c));if(!ea(c))return c;for(;e;)(k=Pf.exec(e))?(h=Ya(h,k,1),e=h.pop()):(h.push(e),e=null);f&&"UTC"===f&&(c=new Date(c.getTime()),c.setMinutes(c.getMinutes()+c.getTimezoneOffset()));q(h,function(a){l=Qf[a];g+=l?l(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Jf(){return function(b,a){z(a)&&(a=2);return $a(b,a)}}function Kf(){return function(b,a){U(b)&&(b=b.toString());return A(b)||K(b)?(a=Infinity===Math.abs(Number(a))? [...]
+aa(a))?0<a?b.slice(0,a):b.slice(a):K(b)?"":[]:b}}function rd(b){return function(a,c,d){function e(a,b){return b?function(b,c){return a(c,b)}:a}function f(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function g(a){return null===a?"null":"function"===typeof a.valueOf&&(a=a.valueOf(),f(a))||"function"===typeof a.toString&&(a=a.toString(),f(a))?a:""}function h(a,b){var c=typeof a,d=typeof b;c===d&&"object"===c&&(a=g(a),b=g(b));return c===d?("strin [...]
+a.toLowerCase(),b=b.toLowerCase()),a===b?0:a<b?-1:1):c<d?-1:1}if(!Sa(a))return a;c=A(c)?c:[c];0===c.length&&(c=["+"]);c=c.map(function(a){var c=!1,d=a||na;if(K(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c="-"==a.charAt(0),a=a.substring(1);if(""===a)return e(h,c);d=b(a);if(d.constant){var f=d();return e(function(a,b){return h(a[f],b[f])},c)}}return e(function(a,b){return h(d(a),d(b))},c)});return Za.call(a).sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e} [...]
+d))}}function Ia(b){F(b)&&(b={link:b});b.restrict=b.restrict||"AC";return ca(b)}function wd(b,a,c,d,e){var f=this,g=[],h=f.$$parentForm=b.parent().controller("form")||Lb;f.$error={};f.$$success={};f.$pending=s;f.$name=e(a.name||a.ngForm||"")(c);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;h.$addControl(f);f.$rollbackViewValue=function(){q(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){q(g,function(a){a.$commitViewValue()})};f.$addControl=fun [...]
+"input");g.push(a);a.$name&&(f[a.$name]=a)};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];q(f.$pending,function(b,c){f.$setValidity(c,null,a)});q(f.$error,function(b,c){f.$setValidity(c,null,a)});q(f.$$success,function(b,c){f.$setValidity(c,null,a)});Xa(g,a)};xd({ctrl:this,$element:b,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){v [...]
+d&&(Xa(d,c),0===d.length&&delete a[b])},parentForm:h,$animate:d});f.$setDirty=function(){d.removeClass(b,Ra);d.addClass(b,Mb);f.$dirty=!0;f.$pristine=!1;h.$setDirty()};f.$setPristine=function(){d.setClass(b,Ra,Mb+" ng-submitted");f.$dirty=!1;f.$pristine=!0;f.$submitted=!1;q(g,function(a){a.$setPristine()})};f.$setUntouched=function(){q(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){d.addClass(b,"ng-submitted");f.$submitted=!0;h.$setSubmitted()}}function mc(b){b.$formatters [...]
+a:a.toString()})}function mb(b,a,c,d,e,f){var g=O(a[0].type);if(!e.android){var h=!1;a.on("compositionstart",function(a){h=!0});a.on("compositionend",function(){h=!1;l()})}var l=function(b){k&&(f.defer.cancel(k),k=null);if(!h){var e=a.val();b=b&&b.type;"password"===g||c.ngTrim&&"false"===c.ngTrim||(e=N(e));(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,b)}};if(e.hasEvent("input"))a.on("input",l);else{var k,m=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.val [...]
+a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))a.on("paste cut",m)}a.on("change",l);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)}}function Nb(b,a){return function(c,d){var e,f;if(ea(c))return c;if(K(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-1)&&(c=c.substring(1,c.length-1));if(Rf.test(c))return new Date(c);b.lastIndex=0;if(e=b.exec(c))return e.shift(),f=d?{yyyy:d.getFullYear(),MM:d.getM [...]
+dd:d.getDate(),HH:d.getHours(),mm:d.getMinutes(),ss:d.getSeconds(),sss:d.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(b,c){c<a.length&&(f[a[c]]=+b)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function nb(b,a,c,d){return function(e,f,g,h,l,k,m){function r(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function n(a){return B(a)?ea(a)?a:c(a):s}yd(e,f,g,h);mb(e,f,g,h,l,k);var v=h&&h.$options&&h.$options.timezone,t;h.$$pars [...]
+h.$parsers.push(function(b){return h.$isEmpty(b)?null:a.test(b)?(b=c(b,t),"UTC"===v&&b.setMinutes(b.getMinutes()-b.getTimezoneOffset()),b):s});h.$formatters.push(function(a){if(a&&!ea(a))throw Ob("datefmt",a);if(r(a)){if((t=a)&&"UTC"===v){var b=6E4*t.getTimezoneOffset();t=new Date(t.getTime()+b)}return m("date")(a,d,v)}t=null;return""});if(B(g.min)||g.ngMin){var q;h.$validators.min=function(a){return!r(a)||z(q)||c(a)>=q};g.$observe("min",function(a){q=n(a);h.$validate()})}if(B(g.max)||g. [...]
+h.$validators.max=function(a){return!r(a)||z(u)||c(a)<=u};g.$observe("max",function(a){u=n(a);h.$validate()})}}}function yd(b,a,c,d){(d.$$hasNativeValidators=J(a[0].validity))&&d.$parsers.push(function(b){var c=a.prop("validity")||{};return c.badInput&&!c.typeMismatch?s:b})}function zd(b,a,c,d,e){if(B(d)){b=b(d);if(!b.constant)throw E("ngModel")("constexpr",c,d);return b(a)}return e}function nc(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;d<a.lengt [...]
+a[d],m=0;m<b.length;m++)if(e==b[m])continue a;c.push(e)}return c}function e(a){if(!A(a)){if(K(a))return a.split(" ");if(J(a)){var b=[];q(a,function(a,c){a&&(b=b.concat(c.split(" ")))});return b}}return a}return{restrict:"AC",link:function(f,g,h){function l(a,b){var c=g.data("$classCounts")||{},d=[];q(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||f.$index%2===a){var k=e(b||[]);if(!m){var v=l(k,1) [...]
+m)){var q=e(m),v=d(k,q),k=d(q,k),v=l(v,1),k=l(k,-1);v&&v.length&&c.addClass(g,v);k&&k.length&&c.removeClass(g,k)}}m=oa(b)}var m;f.$watch(h[b],k,!0);h.$observe("class",function(a){k(f.$eval(h[b]))});"ngClass"!==b&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var k=e(f.$eval(h[b]));g===a?(g=l(k,1),h.$addClass(g)):(g=l(k,-1),h.$removeClass(g))}})}}}]}function xd(b){function a(a,b){b&&!f[a]?(k.addClass(e,a),f[a]=!0):!b&&f[a]&&(k.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+yc [...]
+a(ob+b,!0===c);a(Ad+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,l=b.parentForm,k=b.$animate;f[Ad]=!(f[ob]=e.hasClass(ob));d.$setValidity=function(b,e,f){e===s?(d.$pending||(d.$pending={}),g(d.$pending,b,f)):(d.$pending&&h(d.$pending,b,f),Bd(d.$pending)&&(d.$pending=s));Wa(e)?e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f)):(h(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(Cd,!0),d.$valid=d.$invalid=s,c("",null)):(a(Cd,!1),d.$valid=Bd(d.$error),d [...]
+!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?s:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);l.$setValidity(b,e,d)}}function Bd(b){if(b)for(var a in b)return!1;return!0}var Sf=/^\/(.+)\/([a-z]*)$/,O=function(b){return K(b)?b.toLowerCase():b},wc=Object.prototype.hasOwnProperty,vb=function(b){return K(b)?b.toUpperCase():b},Qa,G,pa,Za=[].slice,uf=[].splice,Tf=[].push,Aa=Object.prototype.toString,Ja=E("ng"),ba=T.angular||(T.angular={}),db,rb=0;Qa=V.documentMode;D.$inject=[];na.$injec [...]
+Array.isArray,N=function(b){return K(b)?b.trim():b},ld=function(b){return b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},cb=function(){if(B(cb.isActive_))return cb.isActive_;var b=!(!V.querySelector("[ng-csp]")&&!V.querySelector("[data-ng-csp]"));if(!b)try{new Function("")}catch(a){b=!0}return cb.isActive_=b},tb=["ng-","data-ng-","ng:","x-ng-"],Rd=/[A-Z]/g,zc=!1,Rb,ma=1,ab=3,Vd={full:"1.3.17",major:1,minor:3,dot:17,codeName:"tsktskskly-euouae"};P.expando="ng3 [...]
+P.cache={},nf=1;P._data=function(b){return this.cache[b[this.expando]]||{}};var hf=/([\:\-\_]+(.))/g,jf=/^moz([A-Z])/,Uf={mouseleave:"mouseout",mouseenter:"mouseover"},Ub=E("jqLite"),mf=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Tb=/<|&#?\w+;/,kf=/<([\w:]+)/,lf=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ha={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody [...]
+td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var Ka=P.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===V.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),P(T).on("load",a))},toString:function(){var b=[];q(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?G(this[b]):G(this[this.length+b])},length:0,push:Tf,s [...]
+splice:[].splice},Fb={};q("multiple selected checked disabled readOnly required open".split(" "),function(b){Fb[O(b)]=b});var Rc={};q("input select option textarea button form details".split(" "),function(b){Rc[b]=!0});var Sc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};q({data:Wb,removeData:yb},function(b,a){P[a]=b});q({data:Wb,inheritedData:Eb,scope:function(b){return G.data(b,"$scope")||Eb(b.parentNode||b,["$isolateScope","$scope"])},is [...]
+"$isolateScope")||G.data(b,"$isolateScopeNoTemplate")},controller:Nc,injector:function(b){return Eb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Bb,css:function(b,a,c){a=eb(a);if(B(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=b.nodeType;if(d!==ab&&2!==d&&8!==d)if(d=O(a),Fb[d])if(B(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||D).specified?d:s;else if(B(c))b.setAttribute(a [...]
+b.getAttribute(a,2),null===b?s:b},prop:function(b,a,c){if(B(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(z(b)){var d=a.nodeType;return d===ma||d===ab?a.textContent:""}a.textContent=b}b.$dv="";return b}(),val:function(b,a){if(z(a)){if(b.multiple&&"select"===sa(b)){var c=[];q(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(z(a))return b.innerHTML;xb(b,!0);b.innerHTML=a},empty:Oc},functi [...]
+function(a,d){var e,f,g=this.length;if(b!==Oc&&(2==b.length&&b!==Bb&&b!==Nc?a:d)===s){if(J(a)){for(e=0;e<g;e++)if(b===Wb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;g=e===s?Math.min(g,1):g;for(f=0;f<g;f++){var h=b(this[f],a,d);e=e?e+h:h}return e}for(e=0;e<g;e++)b(this[e],a,d);return this}});q({removeData:yb,on:function a(c,d,e,f){if(B(f))throw Ub("onargs");if(Jc(c)){var g=zb(c,!0);f=g.events;var h=g.handle;h||(h=g.handle=qf(c,f));for(var g=0<=d.indexOf(" ")?d.split [...]
+l=g.length;l--;){d=g[l];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===d?a(c,Uf[d],function(a){var c=a.relatedTarget;c&&(c===this||this.contains(c))||h(a,d)}):"$destroy"!==d&&c.addEventListener(d,h,!1),k=f[d]);k.push(e)}}},off:Mc,one:function(a,c,d){a=G(a);a.on(c,function f(){a.off(c,d);a.off(c,f)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;xb(a);q(new P(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];q(a. [...]
+function(a){a.nodeType===ma&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(d===ma||11===d){c=new P(c);for(var d=0,e=c.length;d<e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(a.nodeType===ma){var d=a.firstChild;q(new P(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=G(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:Pc,detach:function(a){Pc(a,!0)},after [...]
+c){var d=a,e=a.parentNode;c=new P(c);for(var f=0,g=c.length;f<g;f++){var h=c[f];e.insertBefore(h,d.nextSibling);d=h}},addClass:Db,removeClass:Cb,toggleClass:function(a,c,d){c&&q(c.split(" "),function(c){var f=d;z(f)&&(f=!Bb(a,c));(f?Db:Cb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Vb,triggerHandler:function(a,c,d){var e,f,g=c. [...]
+zb(a);if(h=(h=h&&h.events)&&h[g])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:D,type:g,target:a},c.type&&(e=y(e,c)),c=oa(h),f=d?[e].concat(d):[e],q(c,function(c){e.isImmediatePropagationStopped()||c.apply(a,f)})}},function(a,c){P.prototype[c]= [...]
+e,f){for(var g,h=0,l=this.length;h<l;h++)z(g)?(g=a(this[h],c,e,f),B(g)&&(g=G(g))):Lc(g,a(this[h],c,e,f));return B(g)?g:this};P.prototype.bind=P.prototype.on;P.prototype.unbind=P.prototype.off});fb.prototype={put:function(a,c){this[Na(a,this.nextUid)]=c},get:function(a){return this[Na(a,this.nextUid)]},remove:function(a){var c=this[a=Na(a,this.nextUid)];delete this[a];return c}};var Uc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,Vf=/,/,Wf=/^\s*(_?)(\S+?)\1\s*$/,Tc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/)) [...]
+bb.$$annotate=function(a,c,d){var e;if("function"===typeof a){if(!(e=a.$inject)){e=[];if(a.length){if(c)throw K(d)&&d||(d=a.name||rf(a)),Da("strictdi",d);c=a.toString().replace(Tc,"");c=c.match(Uc);q(c[1].split(Vf),function(a){a.replace(Wf,function(a,c,d){e.push(d)})})}a.$inject=e}}else A(a)?(c=a.length-1,La(a[c],"fn"),e=a.slice(0,c)):La(a,"fn",!0);return e};var Xf=E("$animate"),He=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.c [...]
+c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.classNameFilter=function(a){1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null);return this.$$classNameFilter};this.$get=["$$q","$$asyncCallback","$rootScope",function(a,d,e){function f(d){var f,g=a.defer();g.promise.$$cancelFn=function(){f&&f()};e.$$postDigest(function(){f=d(function(){g.resolve()})});return g.promise}function g(a,c){var d=[],e=[],f=ga();q((a.attr("class")||"").split(/\s+/),function(a){f[a]=!0 [...]
+c){var g=f[c];!1===a&&g?e.push(c):!0!==a||g||d.push(c)});return 0<d.length+e.length&&[d.length?d:null,e.length?e:null]}function h(a,c,d){for(var e=0,f=c.length;e<f;++e)a[c[e]]=d}function l(){m||(m=a.defer(),d(function(){m.resolve();m=null}));return m.promise}function k(a,c){if(ba.isObject(c)){var d=y(c.from||{},c.to||{});a.css(d)}}var m;return{animate:function(a,c,d){k(a,{from:c,to:d});return l()},enter:function(a,c,d,e){k(a,e);d?d.after(a):c.prepend(a);return l()},leave:function(a,c){k( [...]
+return l()},move:function(a,c,d,e){return this.enter(a,c,d,e)},addClass:function(a,c,d){return this.setClass(a,c,[],d)},$$addClassImmediately:function(a,c,d){a=G(a);c=K(c)?c:A(c)?c.join(" "):"";q(a,function(a){Db(a,c)});k(a,d);return l()},removeClass:function(a,c,d){return this.setClass(a,[],c,d)},$$removeClassImmediately:function(a,c,d){a=G(a);c=K(c)?c:A(c)?c.join(" "):"";q(a,function(a){Cb(a,c)});k(a,d);return l()},setClass:function(a,c,d,e){var k=this,l=!1;a=G(a);var m=a.data("$$anima [...]
+m?e&&m.options&&(m.options=ba.extend(m.options||{},e)):(m={classes:{},options:e},l=!0);e=m.classes;c=A(c)?c:c.split(" ");d=A(d)?d:d.split(" ");h(e,c,!0);h(e,d,!1);l&&(m.promise=f(function(c){var d=a.data("$$animateClasses");a.removeData("$$animateClasses");if(d){var e=g(a,d.classes);e&&k.$$setClassImmediately(a,e[0],e[1],d.options)}c()}),a.data("$$animateClasses",m));return m.promise},$$setClassImmediately:function(a,c,d,e){c&&this.$$addClassImmediately(a,c);d&&this.$$removeClassImmediat [...]
+k(a,e);return l()},enabled:D,cancel:D}}]}],da=E("$compile");Bc.$inject=["$provide","$$sanitizeUriProvider"];var Wc=/^((?:x|data)[\:\-_])/i,vf=E("$controller"),ad="application/json",cc={"Content-Type":ad+";charset=utf-8"},xf=/^\[|^\{(?!\{)/,yf={"[":/]$/,"{":/}$/},wf=/^\)\]\}',?\n/,dc=E("$interpolate"),Yf=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,Bf={http:80,https:443,ftp:21},Hb=E("$location"),Zf={$$html5:!1,$$replace:!1,absUrl:Ib("$$absUrl"),url:function(a){if(z(a))return this.$$url;var c=Yf.exec [...]
+""===a)&&this.path(decodeURIComponent(c[1]));(c[2]||c[1]||""===a)&&this.search(c[3]||"");this.hash(c[5]||"");return this},protocol:Ib("$$protocol"),host:Ib("$$host"),port:Ib("$$port"),path:id("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(K(a)||U(a))a=a.toString(),this.$$search=vc(a);else if(J(a))a=Ba(a,{}),q(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else thr [...]
+break;default:z(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:id("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};q([hd,hc,gc],function(a){a.prototype=Object.create(Zf);a.prototype.state=function(c){if(!arguments.length)return this.$$state;if(a!==gc||!this.$$html5)throw Hb("nostate");this.$$state=z(c)?null:c;return this}});var ja=E("$parse"),$f=Function.prototype.call,ag=Function.prototy [...]
+bg=Function.prototype.bind,pb=ga();q({"null":function(){return null},"true":function(){return!0},"false":function(){return!1},undefined:function(){}},function(a,c){a.constant=a.literal=a.sharedGetter=!0;pb[c]=a});pb["this"]=function(a){return a};pb["this"].sharedGetter=!0;var qb=y(ga(),{"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return B(d)?B(e)?d+e:d:B(e)?e:s},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(B(d)?d:0)-(B(e)?e:0)},"*":function(a,c,d,e){return d(a,c)*e(a,c)},"/":function(a,c,d [...]
+c)/e(a,c)},"%":function(a,c,d,e){return d(a,c)%e(a,c)},"===":function(a,c,d,e){return d(a,c)===e(a,c)},"!==":function(a,c,d,e){return d(a,c)!==e(a,c)},"==":function(a,c,d,e){return d(a,c)==e(a,c)},"!=":function(a,c,d,e){return d(a,c)!=e(a,c)},"<":function(a,c,d,e){return d(a,c)<e(a,c)},">":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){ [...]
+c)||e(a,c)},"!":function(a,c,d){return!d(a,c)},"=":!0,"|":!0}),cg={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},kc=function(a){this.options=a};kc.prototype={constructor:kc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?")) [...]
+text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var c=a+this.peek(),d=c+this.peek(2),e=qb[c],f=qb[d];qb[a]||e||f?(a=f?d:e?c:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,c){return-1!==c.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9"> [...]
+typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=B(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw ja("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=O(this.text.c [...]
+if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&&e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:c,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=this.index;this.index<this.text.length;){var c=this.text.charAt(this.index);if(!t [...]
+!this.isNumber(c))break;this.index++}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;this.index<this.text.length;){var g=this.text.charAt(this.index),e=e+g;if(f)"u"===g?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=4,d+=String.fromCharCode(parseInt(f,16))):d+=cg[g]||g,f=!1;else if("\\"===g)f=! [...]
+a){this.index++;this.tokens.push({index:c,text:e,constant:!0,value:d});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var lb=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d};lb.ZERO=y(function(){return 0},{sharedGetter:!0,constant:!0});lb.prototype={constructor:lb,parse:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.statements();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);a.literal=!!a.literal;a.constant=!! [...]
+return a},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.peek().identifier&&this.peek().text in pb?a=pb[this.consume().text]:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var c,d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):"["===c.text?(d=a,a=this. [...]
+"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw ja("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw ja("ueoe",this.text);return this.tokens[0]},peek:function(a,c,d,e){return this.peekAhead(0,a,c,d,e)},peekAhead:function(a,c,d,e,f){if(this.tokens.length>a){a=this.tokens[a];var g=a.text;if(g===c||g===d||g===e||g===f||!(c||d||e||f))return a}return!1},expe [...]
+c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},consume:function(a){if(0===this.tokens.length)throw ja("ueoe",this.text);var c=this.expect(a);c||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return c},unaryFn:function(a,c){var d=qb[a];return y(function(a,f){return d(a,f,c)},{constant:c.constant,inputs:[c]})},binaryFn:function(a,c,d,e){var f=qb[c];return y(function(c,e){return f(c,e,a,d)},{constant:a.constant&&d.constant,inputs:!e&&[a,d]})},identifier:fun [...]
+this.consume().text;this.peek(".")&&this.peekAhead(1).identifier&&!this.peekAhead(2,"(");)a+=this.consume().text+this.consume().text;return Df(a,this.options,this.text)},constant:function(){var a=this.consume().value;return y(function(){return a},{constant:!0,literal:!0})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,d){for(var e,f=0,g=a.length;f<g;f++)e=a[f](c,d [...]
+filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},filter:function(a){var c=this.$filter(this.consume().text),d,e;if(this.peek(":"))for(d=[],e=[];this.expect(":");)d.push(this.expression());var f=[a].concat(d||[]);return y(function(f,h){var l=a(f,h);if(e){e[0]=l;for(l=d.length;l--;)e[l+1]=d[l](f,h);return c.apply(s,e)}return c(l)},{constant:!c.$stateful&&f.every(ic),inputs:!c.$stateful&&f})},expression:function(){return this.assignment()},assi [...]
+this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),y(function(d,f){return a.assign(d,c(d,f),f)},{inputs:[a,c]})):a},ternary:function(){var a=this.logicalOR(),c;if(this.expect("?")&&(c=this.assignment(),this.consume(":"))){var d=this.assignment();return y(function(e,f){return a(e,f)?c(e,f):d(e,f)},{constant:a.constant&&c.constant&&d.constant})}return a},logicalOR: [...]
+this.logicalAND(),c;c=this.expect("||");)a=this.binaryFn(a,c.text,this.logicalAND(),!0);return a},logicalAND:function(){for(var a=this.equality(),c;c=this.expect("&&");)a=this.binaryFn(a,c.text,this.equality(),!0);return a},equality:function(){for(var a=this.relational(),c;c=this.expect("==","!=","===","!==");)a=this.binaryFn(a,c.text,this.relational());return a},relational:function(){for(var a=this.additive(),c;c=this.expect("<",">","<=",">=");)a=this.binaryFn(a,c.text,this.additive()); [...]
+additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.text,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.text,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(lb.ZERO,a.text,this.unary()):(a=this.expect("!"))?this.unaryFn(a.text,this.unary()):this.primary()},fieldAccess:function(a){var c=t [...]
+return y(function(d,e,f){d=f||a(d,e);return null==d?s:c(d)},{assign:function(d,e,f){var g=a(d,f);g||a.assign(d,g={},f);return c.assign(g,e)}})},objectIndex:function(a){var c=this.text,d=this.expression();this.consume("]");return y(function(e,f){var g=a(e,f),h=d(e,f);ra(h,c);return g?ka(g[h],c):s},{assign:function(e,f,g){var h=ra(d(e,g),c),l=ka(a(e,g),c);l||a.assign(e,l={},g);return l[h]=f}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression()) [...]
+}this.consume(")");var e=this.text,f=d.length?[]:null;return function(g,h){var l=c?c(g,h):B(c)?s:g,k=a(g,h,l)||D;if(f)for(var m=d.length;m--;)f[m]=ka(d[m](g,h),e);ka(l,e);if(k){if(k.constructor===k)throw ja("isecfn",e);if(k===$f||k===ag||k===bg)throw ja("isecff",e);}l=k.apply?k.apply(l,f):k(f[0],f[1],f[2],f[3],f[4]);f&&(f.length=0);return ka(l,e)}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(" [...]
+return y(function(c,d){for(var e=[],f=0,g=a.length;f<g;f++)e.push(a[f](c,d));return e},{literal:!0,constant:a.every(ic),inputs:a})},object:function(){var a=[],c=[];if("}"!==this.peekToken().text){do{if(this.peek("}"))break;var d=this.consume();d.constant?a.push(d.value):d.identifier?a.push(d.text):this.throwError("invalid key",d);this.consume(":");c.push(this.expression())}while(this.expect(","))}this.consume("}");return y(function(d,f){for(var g={},h=0,l=c.length;h<l;h++)g[a[h]]=c[h](d, [...]
+{literal:!0,constant:c.every(ic),inputs:c})}};var Ff=ga(),Ef=ga(),Gf=Object.prototype.valueOf,za=E("$sce"),la={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},da=E("$compile"),Y=V.createElement("a"),nd=ya(T.location.href);Ic.$inject=["$provide"];od.$inject=["$locale"];qd.$inject=["$locale"];var td=".",Qf={yyyy:Z("FullYear",4),yy:Z("FullYear",2,0,!0),y:Z("FullYear",1),MMMM:Kb("Month"),MMM:Kb("Month",!0),MM:Z("Month",2,1),M:Z("Month",1,1),dd:Z("Date",2),d:Z("Date",1),HH [...]
+2),H:Z("Hours",1),hh:Z("Hours",2,-12),h:Z("Hours",1,-12),mm:Z("Minutes",2),m:Z("Minutes",1),ss:Z("Seconds",2),s:Z("Seconds",1),sss:Z("Milliseconds",3),EEEE:Kb("Day"),EEE:Kb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(Jb(Math[0<a?"floor":"ceil"](a/60),2)+Jb(Math.abs(a%60),2))},ww:vd(2),w:vd(1),G:lc,GG:lc,GGG:lc,GGGG:function(a,c){return 0>=a.getFullYear()?c.ERANAMES[0]:c.ERANAMES[1]}},Pf=/((?:[^yM [...]
+Of=/^\-?\d+$/;pd.$inject=["$locale"];var Lf=ca(O),Mf=ca(vb);rd.$inject=["$parse"];var Yd=ca({restrict:"E",compile:function(a,c){if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){if("a"===c[0].nodeName.toLowerCase()){var f="[object SVGAnimatedString]"===Aa.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}}),wb={};q(Fb,function(a,c){if("multiple"!=a){var d=va("ng-"+c);wb[d]=function(){return{restrict:"A",priority:100,link:function(a,f [...]
+function(a){g.$set(c,!!a)})}}}}});q(Sc,function(a,c){wb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPattern.match(Sf))){f.$set("ngPattern",new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});q(["src","srcset","href"],function(a){var c=va("ng-"+a);wb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===Aa.call(e.prop("href"))&&(h="xlinkHref",f.$a [...]
+g=null);f.$observe(c,function(c){c?(f.$set(h,c),Qa&&g&&e.prop(g,f[h])):"href"===a&&f.$set(h,null)})}}}});var Lb={$addControl:D,$$renameControl:function(a,c){a.$name=c},$removeControl:D,$setValidity:D,$setDirty:D,$setPristine:D,$setSubmitted:D};wd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Dd=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:wd,compile:function(d,e){d.addClass(Ra).addClass(ob);var f=e.name?"name":a&&e.ng [...]
+!1;return{pre:function(a,d,e,k){if(!("action"in e)){var m=function(c){a.$apply(function(){k.$commitViewValue();k.$setSubmitted()});c.preventDefault()};d[0].addEventListener("submit",m,!1);d.on("$destroy",function(){c(function(){d[0].removeEventListener("submit",m,!1)},0,!1)})}var r=k.$$parentForm;f&&(kb(a,null,k.$name,k,k.$name),e.$observe(f,function(c){k.$name!==c&&(kb(a,null,k.$name,s,k.$name),r.$$renameControl(k,c),kb(a,null,k.$name,k,k.$name))}));d.on("$destroy",function(){r.$removeC [...]
+f&&kb(a,null,e[f],s,k.$name);y(k,Lb)})}}}}}]},Zd=Dd(),le=Dd(!0),Rf=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,dg=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,eg=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,fg=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Ed=/^(\d{4})-(\d{2})-(\d{2})$/,Fd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,oc=/^(\d{4})-W(\d\d) [...]
+Hd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Id={text:function(a,c,d,e,f,g){mb(a,c,d,e,f,g);mc(e)},date:nb("date",Ed,Nb(Ed,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":nb("datetimelocal",Fd,Nb(Fd,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:nb("time",Hd,Nb(Hd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:nb("week",oc,function(a,c){if(ea(a))return a;if(K(a)){oc.lastIndex=0;var d=oc.exec(a);if(d){var e=+d[1],f=+d[2],g=d=0,h=0,l=0,k=ud(e),f=7*(f-1);c&&(d=c.getH [...]
+c.getMinutes(),h=c.getSeconds(),l=c.getMilliseconds());return new Date(e,0,k.getDate()+f,d,g,h,l)}}return NaN},"yyyy-Www"),month:nb("month",Gd,Nb(Gd,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){yd(a,c,d,e);mb(a,c,d,e,f,g);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:fg.test(a)?parseFloat(a):s});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!U(a))throw Ob("numfmt",a);a=a.toString()}return a});if(B(d.min)||d.ngMin){var h;e.$validators.min=f [...]
+z(h)||a>=h};d.$observe("min",function(a){B(a)&&!U(a)&&(a=parseFloat(a,10));h=U(a)&&!isNaN(a)?a:s;e.$validate()})}if(B(d.max)||d.ngMax){var l;e.$validators.max=function(a){return e.$isEmpty(a)||z(l)||a<=l};d.$observe("max",function(a){B(a)&&!U(a)&&(a=parseFloat(a,10));l=U(a)&&!isNaN(a)?a:s;e.$validate()})}},url:function(a,c,d,e,f,g){mb(a,c,d,e,f,g);mc(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||dg.test(d)}},email:function(a,c,d,e,f,g){mb(a,c,d, [...]
+e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||eg.test(d)}},radio:function(a,c,d,e){z(d.name)&&c.attr("name",++rb);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,g,h,l){var k=zd(l,a,"ngTrueValue",d.ngTrueValue,!0),m=zd(l,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].c [...]
+a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return!1===a};e.$formatters.push(function(a){return fa(a,k)});e.$parsers.push(function(a){return a?k:m})},hidden:D,button:D,submit:D,reset:D,file:D},Cc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:{pre:function(f,g,h,l){l[0]&&(Id[O(h.type)]||Id.text)(f,g,h,l[0],c,a,d,e)}}}}],gg=/^(true|false|\d+)$/,De=function(){return{restrict:"A",priority:100,com [...]
+c){return gg.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,function(a){f.$set("value",a)})}}}},de=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);e=e[0];c.$watch(f.ngBind,function(a){e.textContent=a===s?"":a})}}}}],fe=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.att [...]
+c.$$addBindingInfo(f,d.expressions);f=f[0];g.$observe("ngBindTemplate",function(a){f.textContent=a===s?"":a})}}}}],ee=["$sce","$parse","$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var g=c(f.ngBindHtml),h=c(f.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(h,function(){e.html(a.getTrustedHtml(g(c))||"")})}}}}],Ce=ca({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$v [...]
+ge=nc("",!0),ie=nc("Odd",0),he=nc("Even",1),je=Ia({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),ke=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Hc={},hg={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=va("ng-"+a);Hc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f, [...]
+d(g[c],null,!0);return function(c,d){d.on(a,function(d){var f=function(){h(c,{$event:d})};hg[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var ne=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var h,l,k;c.$watch(e.ngIf,function(c){c?l||g(function(c,f){l=f;c[c.length++]=V.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),l&&(l.$destroy(),l=n [...]
+ub(h.clone),a.leave(k).then(function(){k=null}),h=null))})}}}],oe=["$templateRequest","$anchorScroll","$animate",function(a,c,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ba.noop,compile:function(e,f){var g=f.ngInclude||f.src,h=f.onload||"",l=f.autoscroll;return function(e,f,q,n,v){var t=0,w,s,p,I=function(){s&&(s.remove(),s=null);w&&(w.$destroy(),w=null);p&&(d.leave(p).then(function(){s=null}),s=p,p=null)};e.$watch(g,function(g){var q=function(){!B(l [...]
+c()},r=++t;g?(a(g,!0).then(function(a){if(r===t){var c=e.$new();n.template=a;a=v(c,function(a){I();d.enter(a,null,f).then(q)});w=c;p=a;w.$emit("$includeContentLoaded",g);e.$eval(h)}},function(){r===t&&(I(),e.$emit("$includeContentError",g))}),e.$emit("$includeContentRequested",g)):(I(),n.template=null)})}}}}],Fe=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(Kc(f.template,V).childNodes)(c,fun [...]
+{futureParentElement:d})):(d.html(f.template),a(d.contents())(c))}}}],pe=Ia({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Be=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",g="false"!==d.ngTrim,h=g?N(f):f;e.$parsers.push(function(a){if(!z(a)){var c=[];a&&q(a.split(h),function(a){a&&c.push(g?N(a):a)});return c}});e.$formatters.push(function(a){return A(a)?a.join(f):s});e.$isEmpty=f [...]
+!a.length}}}},ob="ng-valid",Ad="ng-invalid",Ra="ng-pristine",Mb="ng-dirty",Cd="ng-pending",Ob=new E("ngModel"),ig=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,c,d,e,f,g,h,l,k,m){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=s;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;th [...]
+!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=s;this.$name=m(d.name||"",!1)(a);var r=f(d.ngModel),n=r.assign,v=r,t=n,w=null,u,p=this;this.$$setOptions=function(a){if((p.$options=a)&&a.getterSetter){var c=f(d.ngModel+"()"),g=f(d.ngModel+"($$$p)");v=function(a){var d=r(a);F(d)&&(d=c(a));return d};t=function(a,c){F(r(a))?g(a,{$$$p:p.$modelValue}):n(a,p.$modelValue)}}else if(!r.assign)throw Ob("nonassign",d.ngModel,ta(e));};this.$render=D;this.$isEmpty=fun [...]
+""===a||null===a||a!==a};var I=e.inheritedData("$formController")||Lb,E=0;xd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:I,$animate:g});this.$setPristine=function(){p.$dirty=!1;p.$pristine=!0;g.removeClass(e,Mb);g.addClass(e,Ra)};this.$setDirty=function(){p.$dirty=!0;p.$pristine=!1;g.removeClass(e,Ra);g.addClass(e,Mb);I.$setDirty()};this.$setUntouched=function(){p.$touched=!1;p.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this. [...]
+function(){p.$touched=!0;p.$untouched=!1;g.setClass(e,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){h.cancel(w);p.$viewValue=p.$$lastCommittedViewValue;p.$render()};this.$validate=function(){if(!U(p.$modelValue)||!isNaN(p.$modelValue)){var a=p.$$rawModelValue,c=p.$valid,d=p.$modelValue,e=p.$options&&p.$options.allowInvalid;p.$$runValidators(a,p.$$lastCommittedViewValue,function(f){e||c===f||(p.$modelValue=f?a:s,p.$modelValue!==d&&p.$$writeModelToScope())})}};this.$$run [...]
+function(a,c,d){function e(){var d=!0;q(p.$validators,function(e,f){var h=e(a,c);d=d&&h;g(f,h)});return d?!0:(q(p.$asyncValidators,function(a,c){g(c,null)}),!1)}function f(){var d=[],e=!0;q(p.$asyncValidators,function(f,h){var k=f(a,c);if(!k||!F(k.then))throw Ob("$asyncValidators",k);g(h,s);d.push(k.then(function(){g(h,!0)},function(a){e=!1;g(h,!1)}))});d.length?k.all(d).then(function(){h(e)},D):h(!0)}function g(a,c){l===E&&p.$setValidity(a,c)}function h(a){l===E&&d(a)}E++;var l=E;(funct [...]
+p.$$parserName||"parse";if(u===s)g(a,null);else return u||(q(p.$validators,function(a,c){g(c,null)}),q(p.$asyncValidators,function(a,c){g(c,null)})),g(a,u),u;return!0})()?e()?f():h(!1):h(!1)};this.$commitViewValue=function(){var a=p.$viewValue;h.cancel(w);if(p.$$lastCommittedViewValue!==a||""===a&&p.$$hasNativeValidators)p.$$lastCommittedViewValue=a,p.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var c=p.$$lastCommittedViewValue;if(u=z(c)?s:!0) [...]
+0;d<p.$parsers.length;d++)if(c=p.$parsers[d](c),z(c)){u=!1;break}U(p.$modelValue)&&isNaN(p.$modelValue)&&(p.$modelValue=v(a));var e=p.$modelValue,f=p.$options&&p.$options.allowInvalid;p.$$rawModelValue=c;f&&(p.$modelValue=c,p.$modelValue!==e&&p.$$writeModelToScope());p.$$runValidators(c,p.$$lastCommittedViewValue,function(a){f||(p.$modelValue=a?c:s,p.$modelValue!==e&&p.$$writeModelToScope())})};this.$$writeModelToScope=function(){t(a,p.$modelValue);q(p.$viewChangeListeners,function(a){tr [...]
+this.$setViewValue=function(a,c){p.$viewValue=a;p.$options&&!p.$options.updateOnDefault||p.$$debounceViewValueCommit(c)};this.$$debounceViewValueCommit=function(c){var d=0,e=p.$options;e&&B(e.debounce)&&(e=e.debounce,U(e)?d=e:U(e[c])?d=e[c]:U(e["default"])&&(d=e["default"]));h.cancel(w);d?w=h(function(){p.$commitViewValue()},d):l.$$phase?p.$commitViewValue():a.$apply(function(){p.$commitViewValue()})};a.$watch(function(){var c=v(a);if(c!==p.$modelValue&&(p.$modelValue===p.$modelValue||c= [...]
+p.$$rawModelValue=c;u=s;for(var d=p.$formatters,e=d.length,f=c;e--;)f=d[e](f);p.$viewValue!==f&&(p.$viewValue=p.$$lastCommittedViewValue=f,p.$render(),p.$$runValidators(c,f,D))}return c})}],Ae=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:ig,priority:1,compile:function(c){c.addClass(Ra).addClass("ng-untouched").addClass(ob);return{pre:function(a,c,f,g){var h=g[0],l=g[1]||Lb;h.$$setOptions(g[2]&&g[2].$options);l.$addControl(h);f.$ [...]
+function(a){h.$name!==a&&l.$$renameControl(h,a)});a.$on("$destroy",function(){l.$removeControl(h)})},post:function(c,e,f,g){var h=g[0];if(h.$options&&h.$options.updateOn)e.on(h.$options.updateOn,function(a){h.$$debounceViewValueCommit(a&&a.type)});e.on("blur",function(e){h.$touched||(a.$$phase?c.$evalAsync(h.$setTouched):c.$apply(h.$setTouched))})}}}}}],jg=/(\s+|^)default(\s+|$)/,Ee=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=a.$eva [...]
+this.$options.updateOn!==s?(this.$options.updateOnDefault=!1,this.$options.updateOn=N(this.$options.updateOn.replace(jg,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},qe=Ia({terminal:!0,priority:1E3}),re=["$locale","$interpolate",function(a,c){var d=/{}/g,e=/^when(Minus)?(.+)$/;return{restrict:"EA",link:function(f,g,h){function l(a){g.text(a||"")}var k=h.count,m=h.$attr.when&&g.attr(h.$attr.when),r=h.offset||0,n=f.$eval(m)||{},v={},m=c.startSy [...]
+c.endSymbol(),s=m+k+"-"+r+t,u=ba.noop,p;q(h,function(a,c){var d=e.exec(c);d&&(d=(d[1]?"-":"")+O(d[2]),n[d]=g.attr(h.$attr[c]))});q(n,function(a,e){v[e]=c(a.replace(d,s))});f.$watch(k,function(c){c=parseFloat(c);var d=isNaN(c);d||c in n||(c=a.pluralCat(c-r));c===p||d&&isNaN(p)||(u(),u=f.$watch(v[c],l),p=c)})}}}],se=["$parse","$animate",function(a,c){var d=E("ngRepeat"),e=function(a,c,d,e,k,m,q){a[d]=e;k&&(a[k]=m);a.$index=c;a.$first=0===c;a.$last=c===q-1;a.$middle=!(a.$first||a.$last);a.$ [...]
+0===(c&1))};return{restrict:"A",multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,g){var h=g.ngRepeat,l=V.createComment(" end ngRepeat: "+h+" "),k=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!k)throw d("iexp",h);var m=k[1],r=k[2],n=k[3],v=k[4],k=m.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!k)throw d("iidexp",m);var t=k[3]||k[1],w=k[2];if(n&&(!/^[$a-zA-Z_][$a-zA-Z0 [...]
+/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(n)))throw d("badident",n);var u,p,B,E,z={$id:Na};v?u=a(v):(B=function(a,c){return Na(c)},E=function(a){return a});return function(a,f,g,k,m){u&&(p=function(c,d,e){w&&(z[w]=c);z[t]=d;z.$index=e;return u(a,z)});var v=ga();a.$watchCollection(r,function(g){var k,r,u=f[0],H,z=ga(),y,D,Q,x,F,A,J;n&&(a[n]=g);if(Sa(g))F=g,r=p||B;else{r=p||E;F=[];for(J in g)g.hasOwnProperty(J)&&"$"!=J.charAt(0)&&F.pus [...]
+F.length;J=Array(y);for(k=0;k<y;k++)if(D=g===F?k:F[k],Q=g[D],x=r(D,Q,k),v[x])A=v[x],delete v[x],z[x]=A,J[k]=A;else{if(z[x])throw q(J,function(a){a&&a.scope&&(v[a.id]=a)}),d("dupes",h,x,Q);J[k]={id:x,scope:s,clone:s};z[x]=!0}for(H in v){A=v[H];x=ub(A.clone);c.leave(x);if(x[0].parentNode)for(k=0,r=x.length;k<r;k++)x[k].$$NG_REMOVED=!0;A.scope.$destroy()}for(k=0;k<y;k++)if(D=g===F?k:F[k],Q=g[D],A=J[k],A.scope){H=u;do H=H.nextSibling;while(H&&H.$$NG_REMOVED);A.clone[0]!=H&&c.move(ub(A.clone) [...]
+u=A.clone[A.clone.length-1];e(A.scope,k,t,Q,w,D,y)}else m(function(a,d){A.scope=d;var f=l.cloneNode(!1);a[a.length++]=f;c.enter(a,null,G(u));u=f;A.clone=a;z[A.id]=A;e(A.scope,k,t,Q,w,D,y)});v=z})}}}}],te=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngShow,function(c){a[c?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],me=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.n [...]
+"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],ue=Ia(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),ve=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var g=[],h=[],l=[],k=[],m=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=l.length;d<e;++d)a.cancel(l[d]) [...]
+0;for(e=k.length;d<e;++d){var t=ub(h[d].clone);k[d].$destroy();(l[d]=a.leave(t)).then(m(l,d))}h.length=0;k.length=0;(g=f.cases["!"+c]||f.cases["?"])&&q(g,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=V.createComment(" end ngSwitchWhen: ");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],we=Ia({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cas [...]
+element:c})}}),xe=Ia({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:f,element:c})}}),ze=Ia({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw E("ngTransclude")("orphan",ta(c));f(function(a){c.empty();c.append(a)})}}),$d=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],kg=E("ngOptions"),ye=c [...]
+terminal:!0}),ae=["$compile","$parse",function(a,c){var d=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,e={$setViewValue:D};return{restrict:"E",require:["select","?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var l=this,k={},m=e,q;l.databound=d.ngModel;l.init=function(a,c,d){m=a;q=d};l.addOption=function(c,d){Ma(c [...]
+k[c]=!0;m.$viewValue==c&&(a.val(c),q.parent()&&q.remove());d&&d[0].hasAttribute("selected")&&(d[0].selected=!0)};l.removeOption=function(a){this.hasOption(a)&&(delete k[a],m.$viewValue===a&&this.renderUnknownOption(a))};l.renderUnknownOption=function(c){c="? "+Na(c)+" ?";q.val(c);a.prepend(q);a.val(c);q.prop("selected",!0)};l.hasOption=function(a){return k.hasOwnProperty(a)};c.$on("$destroy",function(){l.renderUnknownOption=D})}],link:function(e,g,h,l){function k(a,c,d,e){d.$render=funct [...]
+d.$viewValue;e.hasOption(a)?(y.parent()&&y.remove(),c.val(a),""===a&&u.prop("selected",!0)):null==a&&u?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){y.parent()&&y.remove();d.$setViewValue(c.val())})})}function m(a,c,d){var e;d.$render=function(){var a=new fb(d.$viewValue);q(c.find("option"),function(c){c.selected=B(a.get(c.value))})};a.$watch(function(){fa(e,d.$viewValue)||(e=oa(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){v [...]
+function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function r(e,f,g){function h(a,c,d){T[D]=d;G&&(T[G]=c);return a(e,T)}function k(a){var c;if(v)if(K&&A(a)){c=new fb([]);for(var d=0;d<a.length;d++)c.put(h(K,null,a[d]),!0)}else c=new fb(a);else K&&(a=h(K,null,a));return function(d,e){var f;f=K?K:x?x:C;return v?B(c.remove(h(f,d,e))):a===h(f,d,e)}}function l(){p||(e.$$postDigest(r),p=!0)}function m(a,c,d){a[c]=a[c]||0;a[c]+=d?1:-1}function r(){p=!1;var a={"":[]},c=[""],d,l,t,s [...]
+s=M(e)||[];var D=G?Object.keys(s).sort():s,x,A,F,C,S={};u=k(t);var N=!1,U,V;O={};for(C=0;F=D.length,C<F;C++){x=C;if(G&&(x=D[C],"$"===x.charAt(0)))continue;A=s[x];d=h(J,x,A)||"";(l=a[d])||(l=a[d]=[],c.push(d));d=u(x,A);N=N||d;A=h(y,x,A);A=B(A)?A:"";V=K?K(e,T):G?D[C]:C;K&&(O[V]=x);l.push({id:V,label:A,selected:d})}v||(w||null===t?a[""].unshift({id:"",label:"",selected:!N}):N||a[""].unshift({id:"?",label:"",selected:!0}));x=0;for(D=c.length;x<D;x++){d=c[x];l=a[d];P.length<=x?(t={element:E.c [...]
+d),label:l.label},s=[t],P.push(s),f.append(t.element)):(s=P[x],t=s[0],t.label!=d&&t.element.attr("label",t.label=d));N=null;C=0;for(F=l.length;C<F;C++)d=l[C],(u=s[C+1])?(N=u.element,u.label!==d.label&&(m(S,u.label,!1),m(S,d.label,!0),N.text(u.label=d.label),N.prop("label",u.label)),u.id!==d.id&&N.val(u.id=d.id),N[0].selected!==d.selected&&(N.prop("selected",u.selected=d.selected),Qa&&N.prop("selected",u.selected))):(""===d.id&&w?U=w:(U=z.clone()).val(d.id).prop("selected",d.selected).att [...]
+d.selected).prop("label",d.label).text(d.label),s.push(u={element:U,label:d.label,id:d.id,selected:d.selected}),m(S,d.label,!0),N?N.after(U):t.element.append(U),N=U);for(C++;s.length>C;)d=s.pop(),m(S,d.label,!1),d.element.remove()}for(;P.length>x;){l=P.pop();for(C=1;C<l.length;++C)m(S,l[C].label,!1);l[0].element.remove()}q(S,function(a,c){0<a?n.addOption(c):0>a&&n.removeOption(c)})}var u;if(!(u=t.match(d)))throw kg("iexp",t,ta(f));var y=c(u[2]||u[1]),D=u[4]||u[6],F=/ as /.test(u[0])&&u[1 [...]
+null,G=u[5],J=c(u[3]||""),C=c(u[2]?u[1]:D),M=c(u[7]),K=u[8]?c(u[8]):null,O={},P=[[{element:f,label:""}]],T={};w&&(a(w)(e),w.removeClass("ng-scope"),w.remove());f.empty();f.on("change",function(){e.$apply(function(){var a=M(e)||[],c;if(v)c=[],q(f.val(),function(d){d=K?O[d]:d;c.push("?"===d?s:""===d?null:h(x?x:C,d,a[d]))});else{var d=K?O[f.val()]:f.val();c="?"===d?s:""===d?null:h(x?x:C,d,a[d])}g.$setViewValue(c);r()})});g.$render=r;e.$watchCollection(M,l);e.$watchCollection(function(){var  [...]
+if(a&&A(a)){c=Array(a.length);for(var d=0,f=a.length;d<f;d++)c[d]=h(y,d,a[d])}else if(a)for(d in c={},a)a.hasOwnProperty(d)&&(c[d]=h(y,d,a[d]));return c},l);v&&e.$watchCollection(function(){return g.$modelValue},l)}if(l[1]){var n=l[0];l=l[1];var v=h.multiple,t=h.ngOptions,w=!1,u,p=!1,z=G(V.createElement("option")),E=G(V.createElement("optgroup")),y=z.clone();h=0;for(var D=g.children(),F=D.length;h<F;h++)if(""===D[h].value){u=w=D.eq(h);break}n.init(l,w,y);v&&(l.$isEmpty=function(a){return [...]
+t?r(e,g,l):v?m(e,g,l):k(e,g,l,n)}}}}],ce=["$interpolate",function(a){var c={addOption:D,removeOption:D};return{restrict:"E",priority:100,compile:function(d,e){if(z(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,d,e){var k=d.parent(),m=k.data("$selectController")||k.parent().data("$selectController");m&&m.databound||(m=c);f?a.$watch(f,function(a,c){e.$set("value",a);c!==a&&m.removeOption(c);m.addOption(a,d)}):m.addOption(e.value,d);d.on("$destroy",function(){ [...]
+be=ca({restrict:"E",terminal:!1}),Ec=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){e&&(d.required=!0,e.$validators.required=function(a,c){return!d.required||!e.$isEmpty(c)},d.$observe("required",function(){e.$validate()}))}}},Dc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f,g=d.ngPattern||d.pattern;d.$observe("pattern",function(a){K(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw E("ngPattern")("noregexp",g,a [...]
+a||s;e.$validate()});e.$validators.pattern=function(a){return e.$isEmpty(a)||z(f)||f.test(a)}}}}},Gc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=-1;d.$observe("maxlength",function(a){a=aa(a);f=isNaN(a)?-1:a;e.$validate()});e.$validators.maxlength=function(a,c){return 0>f||e.$isEmpty(c)||c.length<=f}}}}},Fc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",function(a){f=aa(a)||0;e.$validat [...]
+function(a,c){return e.$isEmpty(c)||c.length>=f}}}}};T.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(Sd(),Ud(ba),G(V).ready(function(){Od(V,xc)}))})(window,document);!window.angular.$$csp()&&window.angular.element(document).find("head").prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}</style>');
+//# sourceMappingURL=angular.min.js.map
diff --git a/grails-app/assets/javascripts/vendor/angular-route.js b/grails-app/assets/javascripts/vendor/angular-route.js
new file mode 100644
index 0000000..55d3a9b
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular-route.js
@@ -0,0 +1,920 @@
+/**
+ * @license AngularJS v1.2.9
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/**
+ * @ngdoc overview
+ * @name ngRoute
+ * @description
+ *
+ * # ngRoute
+ *
+ * The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
+ *
+ * ## Example
+ * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
+ * 
+ * {@installModule route}
+ *
+ * <div doc-module-components="ngRoute"></div>
+ */
+ /* global -ngRouteModule */
+var ngRouteModule = angular.module('ngRoute', ['ng']).
+                        provider('$route', $RouteProvider);
+
+/**
+ * @ngdoc object
+ * @name ngRoute.$routeProvider
+ * @function
+ *
+ * @description
+ *
+ * Used for configuring routes.
+ * 
+ * ## Example
+ * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
+ *
+ * ## Dependencies
+ * Requires the {@link ngRoute `ngRoute`} module to be installed.
+ */
+function $RouteProvider(){
+  function inherit(parent, extra) {
+    return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
+  }
+
+  var routes = {};
+
+  /**
+   * @ngdoc method
+   * @name ngRoute.$routeProvider#when
+   * @methodOf ngRoute.$routeProvider
+   *
+   * @param {string} path Route path (matched against `$location.path`). If `$location.path`
+   *    contains redundant trailing slash or is missing one, the route will still match and the
+   *    `$location.path` will be updated to add or drop the trailing slash to exactly match the
+   *    route definition.
+   *
+   *      * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up
+   *        to the next slash are matched and stored in `$routeParams` under the given `name`
+   *        when the route matches.
+   *      * `path` can contain named groups starting with a colon and ending with a star:
+   *        e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name`
+   *        when the route matches.
+   *      * `path` can contain optional named groups with a question mark: e.g.`:name?`.
+   *
+   *    For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
+   *    `/color/brown/largecode/code/with/slashs/edit` and extract:
+   *
+   *      * `color: brown`
+   *      * `largecode: code/with/slashs`.
+   *
+   *
+   * @param {Object} route Mapping information to be assigned to `$route.current` on route
+   *    match.
+   *
+   *    Object properties:
+   *
+   *    - `controller` – `{(string|function()=}` – Controller fn that should be associated with
+   *      newly created scope or the name of a {@link angular.Module#controller registered
+   *      controller} if passed as a string.
+   *    - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
+   *      published to scope under the `controllerAs` name.
+   *    - `template` – `{string=|function()=}` – html template as a string or a function that
+   *      returns an html template as a string which should be used by {@link
+   *      ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
+   *      This property takes precedence over `templateUrl`.
+   *
+   *      If `template` is a function, it will be called with the following parameters:
+   *
+   *      - `{Array.<Object>}` - route parameters extracted from the current
+   *        `$location.path()` by applying the current route
+   *
+   *    - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
+   *      template that should be used by {@link ngRoute.directive:ngView ngView}.
+   *
+   *      If `templateUrl` is a function, it will be called with the following parameters:
+   *
+   *      - `{Array.<Object>}` - route parameters extracted from the current
+   *        `$location.path()` by applying the current route
+   *
+   *    - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
+   *      be injected into the controller. If any of these dependencies are promises, the router
+   *      will wait for them all to be resolved or one to be rejected before the controller is
+   *      instantiated.
+   *      If all the promises are resolved successfully, the values of the resolved promises are
+   *      injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
+   *      fired. If any of the promises are rejected the
+   *      {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
+   *      is:
+   *
+   *      - `key` – `{string}`: a name of a dependency to be injected into the controller.
+   *      - `factory` - `{string|function}`: If `string` then it is an alias for a service.
+   *        Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected}
+   *        and the return value is treated as the dependency. If the result is a promise, it is
+   *        resolved before its value is injected into the controller. Be aware that
+   *        `ngRoute.$routeParams` will still refer to the previous route within these resolve
+   *        functions.  Use `$route.current.params` to access the new route parameters, instead.
+   *
+   *    - `redirectTo` – {(string|function())=} – value to update
+   *      {@link ng.$location $location} path with and trigger route redirection.
+   *
+   *      If `redirectTo` is a function, it will be called with the following parameters:
+   *
+   *      - `{Object.<string>}` - route parameters extracted from the current
+   *        `$location.path()` by applying the current route templateUrl.
+   *      - `{string}` - current `$location.path()`
+   *      - `{Object}` - current `$location.search()`
+   *
+   *      The custom `redirectTo` function is expected to return a string which will be used
+   *      to update `$location.path()` and `$location.search()`.
+   *
+   *    - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
+   *      or `$location.hash()` changes.
+   *
+   *      If the option is set to `false` and url in the browser changes, then
+   *      `$routeUpdate` event is broadcasted on the root scope.
+   *
+   *    - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
+   *
+   *      If the option is set to `true`, then the particular route can be matched without being
+   *      case sensitive
+   *
+   * @returns {Object} self
+   *
+   * @description
+   * Adds a new route definition to the `$route` service.
+   */
+  this.when = function(path, route) {
+    routes[path] = angular.extend(
+      {reloadOnSearch: true},
+      route,
+      path && pathRegExp(path, route)
+    );
+
+    // create redirection for trailing slashes
+    if (path) {
+      var redirectPath = (path[path.length-1] == '/')
+            ? path.substr(0, path.length-1)
+            : path +'/';
+
+      routes[redirectPath] = angular.extend(
+        {redirectTo: path},
+        pathRegExp(redirectPath, route)
+      );
+    }
+
+    return this;
+  };
+
+   /**
+    * @param path {string} path
+    * @param opts {Object} options
+    * @return {?Object}
+    *
+    * @description
+    * Normalizes the given path, returning a regular expression
+    * and the original path.
+    *
+    * Inspired by pathRexp in visionmedia/express/lib/utils.js.
+    */
+  function pathRegExp(path, opts) {
+    var insensitive = opts.caseInsensitiveMatch,
+        ret = {
+          originalPath: path,
+          regexp: path
+        },
+        keys = ret.keys = [];
+
+    path = path
+      .replace(/([().])/g, '\\$1')
+      .replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){
+        var optional = option === '?' ? option : null;
+        var star = option === '*' ? option : null;
+        keys.push({ name: key, optional: !!optional });
+        slash = slash || '';
+        return ''
+          + (optional ? '' : slash)
+          + '(?:'
+          + (optional ? slash : '')
+          + (star && '(.+?)' || '([^/]+)')
+          + (optional || '')
+          + ')'
+          + (optional || '');
+      })
+      .replace(/([\/$\*])/g, '\\$1');
+
+    ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
+    return ret;
+  }
+
+  /**
+   * @ngdoc method
+   * @name ngRoute.$routeProvider#otherwise
+   * @methodOf ngRoute.$routeProvider
+   *
+   * @description
+   * Sets route definition that will be used on route change when no other route definition
+   * is matched.
+   *
+   * @param {Object} params Mapping information to be assigned to `$route.current`.
+   * @returns {Object} self
+   */
+  this.otherwise = function(params) {
+    this.when(null, params);
+    return this;
+  };
+
+
+  this.$get = ['$rootScope',
+               '$location',
+               '$routeParams',
+               '$q',
+               '$injector',
+               '$http',
+               '$templateCache',
+               '$sce',
+      function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
+
+    /**
+     * @ngdoc object
+     * @name ngRoute.$route
+     * @requires $location
+     * @requires $routeParams
+     *
+     * @property {Object} current Reference to the current route definition.
+     * The route definition contains:
+     *
+     *   - `controller`: The controller constructor as define in route definition.
+     *   - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
+     *     controller instantiation. The `locals` contain
+     *     the resolved values of the `resolve` map. Additionally the `locals` also contain:
+     *
+     *     - `$scope` - The current route scope.
+     *     - `$template` - The current route template HTML.
+     *
+     * @property {Array.<Object>} routes Array of all configured routes.
+     *
+     * @description
+     * `$route` is used for deep-linking URLs to controllers and views (HTML partials).
+     * It watches `$location.url()` and tries to map the path to an existing route definition.
+     *
+     * Requires the {@link ngRoute `ngRoute`} module to be installed.
+     *
+     * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
+     *
+     * The `$route` service is typically used in conjunction with the
+     * {@link ngRoute.directive:ngView `ngView`} directive and the
+     * {@link ngRoute.$routeParams `$routeParams`} service.
+     *
+     * @example
+       This example shows how changing the URL hash causes the `$route` to match a route against the
+       URL, and the `ngView` pulls in the partial.
+
+       Note that this example is using {@link ng.directive:script inlined templates}
+       to get it working on jsfiddle as well.
+
+     <example module="ngViewExample" deps="angular-route.js">
+       <file name="index.html">
+         <div ng-controller="MainCntl">
+           Choose:
+           <a href="Book/Moby">Moby</a> |
+           <a href="Book/Moby/ch/1">Moby: Ch1</a> |
+           <a href="Book/Gatsby">Gatsby</a> |
+           <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
+           <a href="Book/Scarlet">Scarlet Letter</a><br/>
+
+           <div ng-view></div>
+           <hr />
+
+           <pre>$location.path() = {{$location.path()}}</pre>
+           <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
+           <pre>$route.current.params = {{$route.current.params}}</pre>
+           <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
+           <pre>$routeParams = {{$routeParams}}</pre>
+         </div>
+       </file>
+
+       <file name="book.html">
+         controller: {{name}}<br />
+         Book Id: {{params.bookId}}<br />
+       </file>
+
+       <file name="chapter.html">
+         controller: {{name}}<br />
+         Book Id: {{params.bookId}}<br />
+         Chapter Id: {{params.chapterId}}
+       </file>
+
+       <file name="script.js">
+         angular.module('ngViewExample', ['ngRoute'])
+
+         .config(function($routeProvider, $locationProvider) {
+           $routeProvider.when('/Book/:bookId', {
+             templateUrl: 'book.html',
+             controller: BookCntl,
+             resolve: {
+               // I will cause a 1 second delay
+               delay: function($q, $timeout) {
+                 var delay = $q.defer();
+                 $timeout(delay.resolve, 1000);
+                 return delay.promise;
+               }
+             }
+           });
+           $routeProvider.when('/Book/:bookId/ch/:chapterId', {
+             templateUrl: 'chapter.html',
+             controller: ChapterCntl
+           });
+
+           // configure html5 to get links working on jsfiddle
+           $locationProvider.html5Mode(true);
+         });
+
+         function MainCntl($scope, $route, $routeParams, $location) {
+           $scope.$route = $route;
+           $scope.$location = $location;
+           $scope.$routeParams = $routeParams;
+         }
+
+         function BookCntl($scope, $routeParams) {
+           $scope.name = "BookCntl";
+           $scope.params = $routeParams;
+         }
+
+         function ChapterCntl($scope, $routeParams) {
+           $scope.name = "ChapterCntl";
+           $scope.params = $routeParams;
+         }
+       </file>
+
+       <file name="scenario.js">
+         it('should load and compile correct template', function() {
+           element('a:contains("Moby: Ch1")').click();
+           var content = element('.doc-example-live [ng-view]').text();
+           expect(content).toMatch(/controller\: ChapterCntl/);
+           expect(content).toMatch(/Book Id\: Moby/);
+           expect(content).toMatch(/Chapter Id\: 1/);
+
+           element('a:contains("Scarlet")').click();
+           sleep(2); // promises are not part of scenario waiting
+           content = element('.doc-example-live [ng-view]').text();
+           expect(content).toMatch(/controller\: BookCntl/);
+           expect(content).toMatch(/Book Id\: Scarlet/);
+         });
+       </file>
+     </example>
+     */
+
+    /**
+     * @ngdoc event
+     * @name ngRoute.$route#$routeChangeStart
+     * @eventOf ngRoute.$route
+     * @eventType broadcast on root scope
+     * @description
+     * Broadcasted before a route change. At this  point the route services starts
+     * resolving all of the dependencies needed for the route change to occurs.
+     * Typically this involves fetching the view template as well as any dependencies
+     * defined in `resolve` route property. Once  all of the dependencies are resolved
+     * `$routeChangeSuccess` is fired.
+     *
+     * @param {Object} angularEvent Synthetic event object.
+     * @param {Route} next Future route information.
+     * @param {Route} current Current route information.
+     */
+
+    /**
+     * @ngdoc event
+     * @name ngRoute.$route#$routeChangeSuccess
+     * @eventOf ngRoute.$route
+     * @eventType broadcast on root scope
+     * @description
+     * Broadcasted after a route dependencies are resolved.
+     * {@link ngRoute.directive:ngView ngView} listens for the directive
+     * to instantiate the controller and render the view.
+     *
+     * @param {Object} angularEvent Synthetic event object.
+     * @param {Route} current Current route information.
+     * @param {Route|Undefined} previous Previous route information, or undefined if current is
+     * first route entered.
+     */
+
+    /**
+     * @ngdoc event
+     * @name ngRoute.$route#$routeChangeError
+     * @eventOf ngRoute.$route
+     * @eventType broadcast on root scope
+     * @description
+     * Broadcasted if any of the resolve promises are rejected.
+     *
+     * @param {Object} angularEvent Synthetic event object
+     * @param {Route} current Current route information.
+     * @param {Route} previous Previous route information.
+     * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
+     */
+
+    /**
+     * @ngdoc event
+     * @name ngRoute.$route#$routeUpdate
+     * @eventOf ngRoute.$route
+     * @eventType broadcast on root scope
+     * @description
+     *
+     * The `reloadOnSearch` property has been set to false, and we are reusing the same
+     * instance of the Controller.
+     */
+
+    var forceReload = false,
+        $route = {
+          routes: routes,
+
+          /**
+           * @ngdoc method
+           * @name ngRoute.$route#reload
+           * @methodOf ngRoute.$route
+           *
+           * @description
+           * Causes `$route` service to reload the current route even if
+           * {@link ng.$location $location} hasn't changed.
+           *
+           * As a result of that, {@link ngRoute.directive:ngView ngView}
+           * creates new scope, reinstantiates the controller.
+           */
+          reload: function() {
+            forceReload = true;
+            $rootScope.$evalAsync(updateRoute);
+          }
+        };
+
+    $rootScope.$on('$locationChangeSuccess', updateRoute);
+
+    return $route;
+
+    /////////////////////////////////////////////////////
+
+    /**
+     * @param on {string} current url
+     * @param route {Object} route regexp to match the url against
+     * @return {?Object}
+     *
+     * @description
+     * Check if the route matches the current url.
+     *
+     * Inspired by match in
+     * visionmedia/express/lib/router/router.js.
+     */
+    function switchRouteMatcher(on, route) {
+      var keys = route.keys,
+          params = {};
+
+      if (!route.regexp) return null;
+
+      var m = route.regexp.exec(on);
+      if (!m) return null;
+
+      for (var i = 1, len = m.length; i < len; ++i) {
+        var key = keys[i - 1];
+
+        var val = 'string' == typeof m[i]
+              ? decodeURIComponent(m[i])
+              : m[i];
+
+        if (key && val) {
+          params[key.name] = val;
+        }
+      }
+      return params;
+    }
+
+    function updateRoute() {
+      var next = parseRoute(),
+          last = $route.current;
+
+      if (next && last && next.$$route === last.$$route
+          && angular.equals(next.pathParams, last.pathParams)
+          && !next.reloadOnSearch && !forceReload) {
+        last.params = next.params;
+        angular.copy(last.params, $routeParams);
+        $rootScope.$broadcast('$routeUpdate', last);
+      } else if (next || last) {
+        forceReload = false;
+        $rootScope.$broadcast('$routeChangeStart', next, last);
+        $route.current = next;
+        if (next) {
+          if (next.redirectTo) {
+            if (angular.isString(next.redirectTo)) {
+              $location.path(interpolate(next.redirectTo, next.params)).search(next.params)
+                       .replace();
+            } else {
+              $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
+                       .replace();
+            }
+          }
+        }
+
+        $q.when(next).
+          then(function() {
+            if (next) {
+              var locals = angular.extend({}, next.resolve),
+                  template, templateUrl;
+
+              angular.forEach(locals, function(value, key) {
+                locals[key] = angular.isString(value) ?
+                    $injector.get(value) : $injector.invoke(value);
+              });
+
+              if (angular.isDefined(template = next.template)) {
+                if (angular.isFunction(template)) {
+                  template = template(next.params);
+                }
+              } else if (angular.isDefined(templateUrl = next.templateUrl)) {
+                if (angular.isFunction(templateUrl)) {
+                  templateUrl = templateUrl(next.params);
+                }
+                templateUrl = $sce.getTrustedResourceUrl(templateUrl);
+                if (angular.isDefined(templateUrl)) {
+                  next.loadedTemplateUrl = templateUrl;
+                  template = $http.get(templateUrl, {cache: $templateCache}).
+                      then(function(response) { return response.data; });
+                }
+              }
+              if (angular.isDefined(template)) {
+                locals['$template'] = template;
+              }
+              return $q.all(locals);
+            }
+          }).
+          // after route change
+          then(function(locals) {
+            if (next == $route.current) {
+              if (next) {
+                next.locals = locals;
+                angular.copy(next.params, $routeParams);
+              }
+              $rootScope.$broadcast('$routeChangeSuccess', next, last);
+            }
+          }, function(error) {
+            if (next == $route.current) {
+              $rootScope.$broadcast('$routeChangeError', next, last, error);
+            }
+          });
+      }
+    }
+
+
+    /**
+     * @returns the current active route, by matching it against the URL
+     */
+    function parseRoute() {
+      // Match a route
+      var params, match;
+      angular.forEach(routes, function(route, path) {
+        if (!match && (params = switchRouteMatcher($location.path(), route))) {
+          match = inherit(route, {
+            params: angular.extend({}, $location.search(), params),
+            pathParams: params});
+          match.$$route = route;
+        }
+      });
+      // No route matched; fallback to "otherwise" route
+      return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
+    }
+
+    /**
+     * @returns interpolation of the redirect path with the parameters
+     */
+    function interpolate(string, params) {
+      var result = [];
+      angular.forEach((string||'').split(':'), function(segment, i) {
+        if (i === 0) {
+          result.push(segment);
+        } else {
+          var segmentMatch = segment.match(/(\w+)(.*)/);
+          var key = segmentMatch[1];
+          result.push(params[key]);
+          result.push(segmentMatch[2] || '');
+          delete params[key];
+        }
+      });
+      return result.join('');
+    }
+  }];
+}
+
+ngRouteModule.provider('$routeParams', $RouteParamsProvider);
+
+
+/**
+ * @ngdoc object
+ * @name ngRoute.$routeParams
+ * @requires $route
+ *
+ * @description
+ * The `$routeParams` service allows you to retrieve the current set of route parameters.
+ *
+ * Requires the {@link ngRoute `ngRoute`} module to be installed.
+ *
+ * The route parameters are a combination of {@link ng.$location `$location`}'s
+ * {@link ng.$location#methods_search `search()`} and {@link ng.$location#methods_path `path()`}.
+ * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
+ *
+ * In case of parameter name collision, `path` params take precedence over `search` params.
+ *
+ * The service guarantees that the identity of the `$routeParams` object will remain unchanged
+ * (but its properties will likely change) even when a route change occurs.
+ *
+ * Note that the `$routeParams` are only updated *after* a route change completes successfully.
+ * This means that you cannot rely on `$routeParams` being correct in route resolve functions.
+ * Instead you can use `$route.current.params` to access the new route's parameters.
+ *
+ * @example
+ * <pre>
+ *  // Given:
+ *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
+ *  // Route: /Chapter/:chapterId/Section/:sectionId
+ *  //
+ *  // Then
+ *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
+ * </pre>
+ */
+function $RouteParamsProvider() {
+  this.$get = function() { return {}; };
+}
+
+ngRouteModule.directive('ngView', ngViewFactory);
+ngRouteModule.directive('ngView', ngViewFillContentFactory);
+
+
+/**
+ * @ngdoc directive
+ * @name ngRoute.directive:ngView
+ * @restrict ECA
+ *
+ * @description
+ * # Overview
+ * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
+ * including the rendered template of the current route into the main layout (`index.html`) file.
+ * Every time the current route changes, the included view changes with it according to the
+ * configuration of the `$route` service.
+ *
+ * Requires the {@link ngRoute `ngRoute`} module to be installed.
+ *
+ * @animations
+ * enter - animation is used to bring new content into the browser.
+ * leave - animation is used to animate existing content away.
+ *
+ * The enter and leave animation occur concurrently.
+ *
+ * @scope
+ * @priority 400
+ * @param {string=} onload Expression to evaluate whenever the view updates.
+ *
+ * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll
+ *                  $anchorScroll} to scroll the viewport after the view is updated.
+ *
+ *                  - If the attribute is not set, disable scrolling.
+ *                  - If the attribute is set without value, enable scrolling.
+ *                  - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated
+ *                    as an expression yields a truthy value.
+ * @example
+    <example module="ngViewExample" deps="angular-route.js" animations="true">
+      <file name="index.html">
+        <div ng-controller="MainCntl as main">
+          Choose:
+          <a href="Book/Moby">Moby</a> |
+          <a href="Book/Moby/ch/1">Moby: Ch1</a> |
+          <a href="Book/Gatsby">Gatsby</a> |
+          <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
+          <a href="Book/Scarlet">Scarlet Letter</a><br/>
+
+          <div class="view-animate-container">
+            <div ng-view class="view-animate"></div>
+          </div>
+          <hr />
+
+          <pre>$location.path() = {{main.$location.path()}}</pre>
+          <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
+          <pre>$route.current.params = {{main.$route.current.params}}</pre>
+          <pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
+          <pre>$routeParams = {{main.$routeParams}}</pre>
+        </div>
+      </file>
+
+      <file name="book.html">
+        <div>
+          controller: {{book.name}}<br />
+          Book Id: {{book.params.bookId}}<br />
+        </div>
+      </file>
+
+      <file name="chapter.html">
+        <div>
+          controller: {{chapter.name}}<br />
+          Book Id: {{chapter.params.bookId}}<br />
+          Chapter Id: {{chapter.params.chapterId}}
+        </div>
+      </file>
+
+      <file name="animations.css">
+        .view-animate-container {
+          position:relative;
+          height:100px!important;
+          position:relative;
+          background:white;
+          border:1px solid black;
+          height:40px;
+          overflow:hidden;
+        }
+
+        .view-animate {
+          padding:10px;
+        }
+
+        .view-animate.ng-enter, .view-animate.ng-leave {
+          -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
+          transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
+
+          display:block;
+          width:100%;
+          border-left:1px solid black;
+
+          position:absolute;
+          top:0;
+          left:0;
+          right:0;
+          bottom:0;
+          padding:10px;
+        }
+
+        .view-animate.ng-enter {
+          left:100%;
+        }
+        .view-animate.ng-enter.ng-enter-active {
+          left:0;
+        }
+        .view-animate.ng-leave.ng-leave-active {
+          left:-100%;
+        }
+      </file>
+
+      <file name="script.js">
+        angular.module('ngViewExample', ['ngRoute', 'ngAnimate'],
+          function($routeProvider, $locationProvider) {
+            $routeProvider.when('/Book/:bookId', {
+              templateUrl: 'book.html',
+              controller: BookCntl,
+              controllerAs: 'book'
+            });
+            $routeProvider.when('/Book/:bookId/ch/:chapterId', {
+              templateUrl: 'chapter.html',
+              controller: ChapterCntl,
+              controllerAs: 'chapter'
+            });
+
+            // configure html5 to get links working on jsfiddle
+            $locationProvider.html5Mode(true);
+        });
+
+        function MainCntl($route, $routeParams, $location) {
+          this.$route = $route;
+          this.$location = $location;
+          this.$routeParams = $routeParams;
+        }
+
+        function BookCntl($routeParams) {
+          this.name = "BookCntl";
+          this.params = $routeParams;
+        }
+
+        function ChapterCntl($routeParams) {
+          this.name = "ChapterCntl";
+          this.params = $routeParams;
+        }
+      </file>
+
+      <file name="scenario.js">
+        it('should load and compile correct template', function() {
+          element('a:contains("Moby: Ch1")').click();
+          var content = element('.doc-example-live [ng-view]').text();
+          expect(content).toMatch(/controller\: ChapterCntl/);
+          expect(content).toMatch(/Book Id\: Moby/);
+          expect(content).toMatch(/Chapter Id\: 1/);
+
+          element('a:contains("Scarlet")').click();
+          content = element('.doc-example-live [ng-view]').text();
+          expect(content).toMatch(/controller\: BookCntl/);
+          expect(content).toMatch(/Book Id\: Scarlet/);
+        });
+      </file>
+    </example>
+ */
+
+
+/**
+ * @ngdoc event
+ * @name ngRoute.directive:ngView#$viewContentLoaded
+ * @eventOf ngRoute.directive:ngView
+ * @eventType emit on the current ngView scope
+ * @description
+ * Emitted every time the ngView content is reloaded.
+ */
+ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate'];
+function ngViewFactory(   $route,   $anchorScroll,   $animate) {
+  return {
+    restrict: 'ECA',
+    terminal: true,
+    priority: 400,
+    transclude: 'element',
+    link: function(scope, $element, attr, ctrl, $transclude) {
+        var currentScope,
+            currentElement,
+            autoScrollExp = attr.autoscroll,
+            onloadExp = attr.onload || '';
+
+        scope.$on('$routeChangeSuccess', update);
+        update();
+
+        function cleanupLastView() {
+          if (currentScope) {
+            currentScope.$destroy();
+            currentScope = null;
+          }
+          if(currentElement) {
+            $animate.leave(currentElement);
+            currentElement = null;
+          }
+        }
+
+        function update() {
+          var locals = $route.current && $route.current.locals,
+              template = locals && locals.$template;
+
+          if (angular.isDefined(template)) {
+            var newScope = scope.$new();
+            var current = $route.current;
+
+            // Note: This will also link all children of ng-view that were contained in the original
+            // html. If that content contains controllers, ... they could pollute/change the scope.
+            // However, using ng-view on an element with additional content does not make sense...
+            // Note: We can't remove them in the cloneAttchFn of $transclude as that
+            // function is called before linking the content, which would apply child
+            // directives to non existing elements.
+            var clone = $transclude(newScope, function(clone) {
+              $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
+                if (angular.isDefined(autoScrollExp)
+                  && (!autoScrollExp || scope.$eval(autoScrollExp))) {
+                  $anchorScroll();
+                }
+              });
+              cleanupLastView();
+            });
+
+            currentElement = clone;
+            currentScope = current.scope = newScope;
+            currentScope.$emit('$viewContentLoaded');
+            currentScope.$eval(onloadExp);
+          } else {
+            cleanupLastView();
+          }
+        }
+    }
+  };
+}
+
+// This directive is called during the $transclude call of the first `ngView` directive.
+// It will replace and compile the content of the element with the loaded template.
+// We need this directive so that the element content is already filled when
+// the link function of another directive on the same element as ngView
+// is called.
+ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route'];
+function ngViewFillContentFactory($compile, $controller, $route) {
+  return {
+    restrict: 'ECA',
+    priority: -400,
+    link: function(scope, $element) {
+      var current = $route.current,
+          locals = current.locals;
+
+      $element.html(locals.$template);
+
+      var link = $compile($element.contents());
+
+      if (current.controller) {
+        locals.$scope = scope;
+        var controller = $controller(current.controller, locals);
+        if (current.controllerAs) {
+          scope[current.controllerAs] = controller;
+        }
+        $element.data('$ngControllerController', controller);
+        $element.children().data('$ngControllerController', controller);
+      }
+
+      link(scope);
+    }
+  };
+}
+
+
+})(window, window.angular);
diff --git a/grails-app/assets/javascripts/vendor/angular-strap.min.js b/grails-app/assets/javascripts/vendor/angular-strap.min.js
new file mode 100644
index 0000000..a3ee851
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular-strap.min.js
@@ -0,0 +1,10 @@
+/**
+ * angular-strap
+ * @version v2.1.2 - 2014-10-19
+ * @link http://mgcrea.github.io/angular-strap
+ * @author Olivier Louvignes (olivier at mg-crea.com)
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+!function(e,t){"use strict";angular.module("mgcrea.ngStrap",["mgcrea.ngStrap.modal","mgcrea.ngStrap.aside","mgcrea.ngStrap.alert","mgcrea.ngStrap.button","mgcrea.ngStrap.select","mgcrea.ngStrap.datepicker","mgcrea.ngStrap.timepicker","mgcrea.ngStrap.navbar","mgcrea.ngStrap.tooltip","mgcrea.ngStrap.popover","mgcrea.ngStrap.dropdown","mgcrea.ngStrap.typeahead","mgcrea.ngStrap.scrollspy","mgcrea.ngStrap.affix","mgcrea.ngStrap.tab","mgcrea.ngStrap.collapse"]),angular.module("mgcrea.ngStrap.a [...]
+var a=l.enter(w,e,t,n);a&&a.then&&a.then(n),d.$isShown=!0,d.$$phase||d.$root&&d.$root.$$phase||d.$digest();var o=w[0];$(function(){o.focus()}),h.addClass(u.prefixClass+"-open"),u.animation&&h.addClass(u.prefixClass+"-with-"+u.animation),u.backdrop&&(w.on("click",r),b.on("click",r)),u.keyboard&&w.on("keyup",s.$onKeyUp)}},s.hide=function(){if(d.$isShown&&!d.$emit(u.prefixEvent+".hide.before",s).defaultPrevented){var e=l.leave(w,i);e&&e.then&&e.then(i),u.backdrop&&l.leave(b),d.$isShown=!1,d [...]
+//# sourceMappingURL=angular-strap.min.js.map
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/angular-strap.tpl.min.js b/grails-app/assets/javascripts/vendor/angular-strap.tpl.min.js
new file mode 100644
index 0000000..1d909ee
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular-strap.tpl.min.js
@@ -0,0 +1,8 @@
+/**
+ * angular-strap
+ * @version v2.1.2 - 2014-10-19
+ * @link http://mgcrea.github.io/angular-strap
+ * @author Olivier Louvignes (olivier at mg-crea.com)
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+!function(){"use strict";angular.module("mgcrea.ngStrap.alert").run(["$templateCache",function(t){t.put("alert/alert.tpl.html",'<div class="alert" ng-class="[type ? \'alert-\' + type : null]"><button type="button" class="close" ng-if="dismissable" ng-click="$hide()">×</button> <strong ng-bind="title"></strong> <span ng-bind-html="content"></span></div>')}]),angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(t){t.put("aside/aside.tpl.html",'<div class="aside" [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/angular.min.js b/grails-app/assets/javascripts/vendor/angular.min.js
new file mode 100644
index 0000000..272101e
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular.min.js
@@ -0,0 +1,294 @@
+/*
+ AngularJS v1.4.7
+ (c) 2010-2015 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(Q,X,w){'use strict';function I(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.4.7/"+(b?b+"/":"")+a;for(a=1;a<arguments.length;a++){c=c+(1==a?"?":"&")+"p"+(a-1)+"=";var d=encodeURIComponent,e;e=arguments[a];e="function"==typeof e?e.toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof e?"undefined":"string"!=typeof e?JSON.stringify(e):e;c+=d(e)}return Error(c)}}function Da(b){if(null==b||Za(b))return!1;var a="length"in Object(b [...]
+return b.nodeType===pa&&a?!0:G(b)||J(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function m(b,a,c){var d,e;if(b)if(x(b))for(d in b)"prototype"==d||"length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d)||a.call(c,b[d],d,b);else if(J(b)||Da(b)){var f="object"!==typeof b;d=0;for(e=b.length;d<e;d++)(f||d in b)&&a.call(c,b[d],d,b)}else if(b.forEach&&b.forEach!==m)b.forEach(a,c,b);else if(mc(b))for(d in b)a.call(c,b[d],d,b);else if("function"===typeof b.hasOwnProperty)for(d in b)b.ha [...]
+a.call(c,b[d],d,b);else for(d in b)ta.call(b,d)&&a.call(c,b[d],d,b);return b}function nc(b,a,c){for(var d=Object.keys(b).sort(),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function oc(b){return function(a,c){b(c,a)}}function Ud(){return++nb}function pc(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function Mb(b,a,c){for(var d=b.$$hashKey,e=0,f=a.length;e<f;++e){var h=a[e];if(C(h)||x(h))for(var g=Object.keys(h),l=0,k=g.length;l<k;l++){var n=g[l],p=h[n];c&&C(p)?ea(p)?b[n]=new Date(p.value [...]
+b[n]=new RegExp(p):(C(b[n])||(b[n]=J(p)?[]:{}),Mb(b[n],[p],!0)):b[n]=p}}pc(b,d);return b}function P(b){return Mb(b,ua.call(arguments,1),!1)}function Vd(b){return Mb(b,ua.call(arguments,1),!0)}function Y(b){return parseInt(b,10)}function Nb(b,a){return P(Object.create(b),a)}function y(){}function $a(b){return b}function qa(b){return function(){return b}}function qc(b){return x(b.toString)&&b.toString!==Object.prototype.toString}function v(b){return"undefined"===typeof b}function A(b){retu [...]
+typeof b}function C(b){return null!==b&&"object"===typeof b}function mc(b){return null!==b&&"object"===typeof b&&!rc(b)}function G(b){return"string"===typeof b}function V(b){return"number"===typeof b}function ea(b){return"[object Date]"===va.call(b)}function x(b){return"function"===typeof b}function Oa(b){return"[object RegExp]"===va.call(b)}function Za(b){return b&&b.window===b}function ab(b){return b&&b.$evalAsync&&b.$watch}function bb(b){return"boolean"===typeof b}function sc(b){retur [...]
+b.prop&&b.attr&&b.find))}function Wd(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function wa(b){return F(b.nodeName||b[0]&&b[0].nodeName)}function cb(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return c}function ha(b,a,c,d){if(Za(b)||ab(b))throw Ea("cpws");if(tc.test(va.call(a)))throw Ea("cpta");if(a){if(b===a)throw Ea("cpi");c=c||[];d=d||[];C(b)&&(c.push(b),d.push(a));var e;if(J(b))for(e=a.length=0;e<b.length;e++)a.push(ha(b[e],null,c,d));else{var f=a.$$h [...]
+a.length=0:m(a,function(b,c){delete a[c]});if(mc(b))for(e in b)a[e]=ha(b[e],null,c,d);else if(b&&"function"===typeof b.hasOwnProperty)for(e in b)b.hasOwnProperty(e)&&(a[e]=ha(b[e],null,c,d));else for(e in b)ta.call(b,e)&&(a[e]=ha(b[e],null,c,d));pc(a,f)}}else if(a=b,C(b)){if(c&&-1!==(f=c.indexOf(b)))return d[f];if(J(b))return ha(b,[],c,d);if(tc.test(va.call(b)))a=new b.constructor(b);else if(ea(b))a=new Date(b.getTime());else if(Oa(b))a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0 [...]
+b.lastIndex;else if(x(b.cloneNode))a=b.cloneNode(!0);else return e=Object.create(rc(b)),ha(b,e,c,d);d&&(c.push(b),d.push(a))}return a}function ja(b,a){if(J(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c]}else if(C(b))for(c in a=a||{},b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function ka(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;var c=typeof b,d;if(c==typeof a&&"object"==c)if(J(b)){if(!J(a))return!1;if((c=b.length)==a.length) [...]
+c;d++)if(!ka(b[d],a[d]))return!1;return!0}}else{if(ea(b))return ea(a)?ka(b.getTime(),a.getTime()):!1;if(Oa(b))return Oa(a)?b.toString()==a.toString():!1;if(ab(b)||ab(a)||Za(b)||Za(a)||J(a)||ea(a)||Oa(a))return!1;c=fa();for(d in b)if("$"!==d.charAt(0)&&!x(b[d])){if(!ka(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!(d in c)&&"$"!==d.charAt(0)&&A(a[d])&&!x(a[d]))return!1;return!0}return!1}function db(b,a,c){return b.concat(ua.call(a,c))}function uc(b,a){var c=2<arguments.length?ua.call(argumen [...]
+return!x(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,db(c,arguments,0)):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Xd(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)&&"$"===b.charAt(1)?c=w:Za(a)?c="$WINDOW":a&&X===a?c="$DOCUMENT":ab(a)&&(c="$SCOPE");return c}function eb(b,a){if("undefined"===typeof b)return w;V(a)||(a=a?2:null);return JSON.stringify(b,Xd,a)}function vc(b){return G(b)?JSON.parse(b):b}fu [...]
+a){var c=Date.parse("Jan 01, 1970 00:00:00 "+b)/6E4;return isNaN(c)?a:c}function Ob(b,a,c){c=c?-1:1;var d=wc(a,b.getTimezoneOffset());a=b;b=c*(d-b.getTimezoneOffset());a=new Date(a.getTime());a.setMinutes(a.getMinutes()+b);return a}function xa(b){b=B(b).clone();try{b.empty()}catch(a){}var c=B("<div>").append(b).html();try{return b[0].nodeType===Pa?F(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+F(b)})}catch(d){return F(c)}}function xc(b){try{return decodeURICo [...]
+function yc(b){var a={};m((b||"").split("&"),function(b){var d,e,f;b&&(e=b=b.replace(/\+/g,"%20"),d=b.indexOf("="),-1!==d&&(e=b.substring(0,d),f=b.substring(d+1)),e=xc(e),A(e)&&(f=A(f)?xc(f):!0,ta.call(a,e)?J(a[e])?a[e].push(f):a[e]=[a[e],f]:a[e]=f))});return a}function Pb(b){var a=[];m(b,function(b,d){J(b)?m(b,function(b){a.push(la(d,!0)+(!0===b?"":"="+la(b,!0)))}):a.push(la(d,!0)+(!0===b?"":"="+la(b,!0)))});return a.length?a.join("&"):""}function ob(b){return la(b,!0).replace(/%26/gi," [...]
+"=").replace(/%2B/gi,"+")}function la(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function Yd(b,a){var c,d,e=Qa.length;for(d=0;d<e;++d)if(c=Qa[d]+a,G(c=b.getAttribute(c)))return c;return null}function Zd(b,a){var c,d,e={};m(Qa,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))});m(Qa,function(a){a+="app";var e;!c&&(e=b.querySelector( [...]
+"\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==Yd(c,"strict-di"),a(c,d?[d]:[],e))}function zc(b,a,c){C(c)||(c={});c=P({strictDi:!1},c);var d=function(){b=B(b);if(b.injector()){var d=b[0]===X?"document":xa(b);throw Ea("btstrpd",d.replace(/</,"<").replace(/>/,">"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);c.debugInfoEnabled&&a.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);a.unshift("ng");d=fb(a,c.strictDi);d.invoke(["$r [...]
+"$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return d},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;Q&&e.test(Q.name)&&(c.debugInfoEnabled=!0,Q.name=Q.name.replace(e,""));if(Q&&!f.test(Q.name))return d();Q.name=Q.name.replace(f,"");da.resumeBootstrap=function(b){m(b,function(b){a.push(b)});return d()};x(da.resumeDeferredBootstrap)&&da.resumeDeferredBootstrap()}function $d(){Q.name="NG_ENABLE_DEBUG_INFO!"+Q.name;Q.lo [...]
+function ae(b){b=da.element(b).injector();if(!b)throw Ea("test");return b.get("$$testability")}function Ac(b,a){a=a||"_";return b.replace(be,function(b,d){return(d?a:"")+b.toLowerCase()})}function ce(){var b;if(!Bc){var a=pb();(ra=v(a)?Q.jQuery:a?Q[a]:w)&&ra.fn.on?(B=ra,P(ra.fn,{scope:Ra.scope,isolateScope:Ra.isolateScope,controller:Ra.controller,injector:Ra.injector,inheritedData:Ra.inheritedData}),b=ra.cleanData,ra.cleanData=function(a){var d;if(Qb)Qb=!1;else for(var e=0,f;null!=(f=a[e [...]
+ra._data(f,"events"))&&d.$destroy&&ra(f).triggerHandler("$destroy");b(a)}):B=R;da.element=B;Bc=!0}}function qb(b,a,c){if(!b)throw Ea("areq",a||"?",c||"required");return b}function Sa(b,a,c){c&&J(b)&&(b=b[b.length-1]);qb(x(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ta(b,a){if("hasOwnProperty"===b)throw Ea("badname",a);}function Cc(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,f=a.length,h=0;h<f;h++)d=a[h],b&&(b=(e=b)[ [...]
+x(b)?uc(e,b):b}function rb(b){for(var a=b[0],c=b[b.length-1],d,e=1;a!==c&&(a=a.nextSibling);e++)if(d||b[e]!==a)d||(d=B(ua.call(b,0,e))),d.push(a);return d||b}function fa(){return Object.create(null)}function de(b){function a(a,b,c){return a[b]||(a[b]=c())}var c=I("$injector"),d=I("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||I;return a(b,"module",function(){var b={};return function(f,h,g){if("hasOwnProperty"===f)throw d("badname","module");h&&b.hasOwnProperty(f)&&(b[f]=null);retur [...]
+c,e,f){f||(f=d);return function(){f[e||"push"]([b,c,arguments]);return E}}function b(a,c){return function(b,e){e&&x(e)&&(e.$$moduleName=f);d.push([a,c,arguments]);return E}}if(!h)throw c("nomod",f);var d=[],e=[],r=[],t=a("$injector","invoke","push",e),E={_invokeQueue:d,_configBlocks:e,_runBlocks:r,requires:h,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decor [...]
+"decorator"),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),config:t,run:function(a){r.push(a);return this}};g&&t(g);return E})}})}function ee(b){P(b,{bootstrap:zc,copy:ha,extend:P,merge:Vd,equals:ka,element:B,forEach:m,injector:fb,noop:y,bind:uc,toJson:eb,fromJson:vc,identity:$a,isUndefined:v,isDefined:A,isString:G,isFunction:x,isObject:C,isNumber:V,isElement:sc, [...]
+version:fe,isDate:ea,lowercase:F,uppercase:sb,callbacks:{counter:0},getTestability:ae,$$minErr:I,$$csp:Fa,reloadWithDebugInfo:$d});Rb=de(Q);Rb("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:ge});a.provider("$compile",Dc).directive({a:he,input:Ec,textarea:Ec,form:ie,script:je,select:ke,style:le,option:me,ngBind:ne,ngBindHtml:oe,ngBindTemplate:pe,ngClass:qe,ngClassEven:re,ngClassOdd:se,ngCloak:te,ngController:ue,ngForm:ve,ngHide:we,ngIf:xe,ngInclude:ye,ngInit:ze,ngNonB [...]
+ngPluralize:Be,ngRepeat:Ce,ngShow:De,ngStyle:Ee,ngSwitch:Fe,ngSwitchWhen:Ge,ngSwitchDefault:He,ngOptions:Ie,ngTransclude:Je,ngModel:Ke,ngList:Le,ngChange:Me,pattern:Fc,ngPattern:Fc,required:Gc,ngRequired:Gc,minlength:Hc,ngMinlength:Hc,maxlength:Ic,ngMaxlength:Ic,ngValue:Ne,ngModelOptions:Oe}).directive({ngInclude:Pe}).directive(tb).directive(Jc);a.provider({$anchorScroll:Qe,$animate:Re,$animateCss:Se,$$animateQueue:Te,$$AnimateRunner:Ue,$browser:Ve,$cacheFactory:We,$controller:Xe,$docume [...]
+$filter:Kc,$$forceReflow:$e,$interpolate:af,$interval:bf,$http:cf,$httpParamSerializer:df,$httpParamSerializerJQLike:ef,$httpBackend:ff,$xhrFactory:gf,$location:hf,$log:jf,$parse:kf,$rootScope:lf,$q:mf,$$q:nf,$sce:of,$sceDelegate:pf,$sniffer:qf,$templateCache:rf,$templateRequest:sf,$$testability:tf,$timeout:uf,$window:vf,$$rAF:wf,$$jqLite:xf,$$HashMap:yf,$$cookieReader:zf})}])}function gb(b){return b.replace(Af,function(a,b,d,e){return e?d.toUpperCase():d}).replace(Bf,"Moz$1")}function L [...]
+return b===pa||!b||9===b}function Mc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Sb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(Cf.exec(b)||["",""])[1].toLowerCase();d=ma[d]||ma._default;c.innerHTML=d[1]+b.replace(Df,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=db(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";m(f,function(a){e.appendChild(a)});return e}function R(b){if(b instanceof R)return b;var a;G [...]
+a=!0);if(!(this instanceof R)){if(a&&"<"!=b.charAt(0))throw Tb("nosel");return new R(b)}if(a){a=X;var c;b=(c=Ef.exec(b))?[a.createElement(c[1])]:(c=Mc(b,a))?c.childNodes:[]}Nc(this,b)}function Ub(b){return b.cloneNode(!0)}function ub(b,a){a||vb(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)vb(c[d])}function Oc(b,a,c,d){if(A(d))throw Tb("offargs");var e=(d=wb(b))&&d.events,f=d&&d.handle;if(f)if(a)m(a.split(" "),function(a){if(A(c)){var d=e[a];cb(d||[],c [...]
+d.length)return}b.removeEventListener(a,f,!1);delete e[a]});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,f,!1),delete e[a]}function vb(b,a){var c=b.ng339,d=c&&hb[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),Oc(b)),delete hb[c],b.ng339=w))}function wb(b,a){var c=b.ng339,c=c&&hb[c];a&&!c&&(b.ng339=c=++Ff,c=hb[c]={events:{},data:{},handle:w});return c}function Vb(b,a,c){if(Lc(b)){var d=A(c),e=!d&&a&&!C(a),f=!a;b=(b=wb(b,!e))&&b.data;if(d)b[a]=c [...]
+if(e)return b&&b[a];P(b,a)}}}function xb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function yb(b,a){a&&b.setAttribute&&m(a.split(" "),function(a){b.setAttribute("class",T((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+T(a)+" "," ")))})}function zb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");m(a.split(" "),function(a){a=T(a);-1===c.indexO [...]
+(c+=a+" ")});b.setAttribute("class",T(c))}}function Nc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=a.length;if("number"===typeof c&&a.window!==a){if(c)for(var d=0;d<c;d++)b[b.length++]=a[d]}else b[b.length++]=a}}function Pc(b,a){return Ab(b,"$"+(a||"ngController")+"Controller")}function Ab(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=J(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if(A(c=B.data(b,a[d])))return c;b=b.parentNode||11===b.nodeType&&b.host}}function Qc(b){for(ub(b,! [...]
+function Wb(b,a){a||ub(b);var c=b.parentNode;c&&c.removeChild(b)}function Gf(b,a){a=a||Q;if("complete"===a.document.readyState)a.setTimeout(b);else B(a).on("load",b)}function Rc(b,a){var c=Bb[a.toLowerCase()];return c&&Sc[wa(b)]&&c}function Hf(b,a){var c=function(c,e){c.isDefaultPrevented=function(){return c.defaultPrevented};var f=a[e||c.type],h=f?f.length:0;if(h){if(v(c.immediatePropagationStopped)){var g=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropa [...]
+!0;c.stopPropagation&&c.stopPropagation();g&&g.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};1<h&&(f=ja(f));for(var l=0;l<h;l++)c.isImmediatePropagationStopped()||f[l].call(b,c)}};c.elem=b;return c}function xf(){this.$get=function(){return P(R,{hasClass:function(b,a){b.attr&&(b=b[0]);return xb(b,a)},addClass:function(b,a){b.attr&&(b=b[0]);return zb(b,a)},removeClass:function(b,a){b.attr&&(b=b[0]);return yb(b,a)}})}}function Ga(b,a){var c=b& [...]
+if(c)return"function"===typeof c&&(c=b.$$hashKey()),c;c=typeof b;return c="function"==c||"object"==c&&null!==b?b.$$hashKey=c+":"+(a||Ud)():c+":"+b}function Ua(b,a){if(a){var c=0;this.nextUid=function(){return++c}}m(b,this.put,this)}function If(b){return(b=b.toString().replace(Tc,"").match(Uc))?"function("+(b[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function fb(b,a){function c(a){return function(b,c){if(C(b))m(b,oc(a));else return a(b,c)}}function d(a,b){Ta(a,"service");if(x(b)||J(b))b=r [...]
+if(!b.$get)throw Ha("pget",a);return p[a+"Provider"]=b}function e(a,b){return function(){var c=E.invoke(b,this);if(v(c))throw Ha("undef",a);return c}}function f(a,b,c){return d(a,{$get:!1!==c?e(a,b):b})}function h(a){qb(v(a)||J(a),"modulesToLoad","not an array");var b=[],c;m(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=r.get(e[0]);f[e[1]].apply(f,e[2])}}if(!n.get(a)){n.put(a,!0);try{G(a)?(c=Rb(a),b=b.concat(h(c.requires)).concat(c._runBlocks),d(c._invokeQu [...]
+x(a)?b.push(r.invoke(a)):J(a)?b.push(r.invoke(a)):Sa(a,"module")}catch(e){throw J(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ha("modulerr",a,e.stack||e.message||e);}}});return b}function g(b,c){function d(a,e){if(b.hasOwnProperty(a)){if(b[a]===l)throw Ha("cdep",a+" <- "+k.join(" <- "));return b[a]}try{return k.unshift(a),b[a]=l,b[a]=c(a,e)}catch(f){throw b[a]===l&&delete b[a],f;}finally{k.shift()}}function e(b,c,f,g){"string"===ty [...]
+f,f=null);var h=[],k=fb.$$annotate(b,a,g),l,r,p;r=0;for(l=k.length;r<l;r++){p=k[r];if("string"!==typeof p)throw Ha("itkn",p);h.push(f&&f.hasOwnProperty(p)?f[p]:d(p,g))}J(b)&&(b=b[l]);return b.apply(c,h)}return{invoke:e,instantiate:function(a,b,c){var d=Object.create((J(a)?a[a.length-1]:a).prototype||null);a=e(a,d,b,c);return C(a)||x(a)?a:d},get:d,annotate:fb.$$annotate,has:function(a){return p.hasOwnProperty(a+"Provider")||b.hasOwnProperty(a)}}}a=!0===a;var l={},k=[],n=new Ua([],!0),p={$ [...]
+factory:c(f),service:c(function(a,b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return f(a,qa(b),!1)}),constant:c(function(a,b){Ta(a,"constant");p[a]=b;t[a]=b}),decorator:function(a,b){var c=r.get(a+"Provider"),d=c.$get;c.$get=function(){var a=E.invoke(d,c);return E.invoke(b,null,{$delegate:a})}}}},r=p.$injector=g(p,function(a,b){da.isString(b)&&k.push(b);throw Ha("unpr",k.join(" <- "));}),t={},E=t.$injector=g(t,function(a,b){var c=r.get(a+"Pro [...]
+return E.invoke(c.$get,c,w,a)});m(h(b),function(a){a&&E.invoke(a)});return E}function Qe(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===wa(a))return b=a,!0});return b}function f(b){if(b){b.scrollIntoView();var c;c=h.yOffset;x(c)?c=c():sc(c)?(c=c[0],c="fixed"!==a.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):V(c)||(c=0);c&&(b=b.g [...]
+a.scrollBy(0,b-c))}else a.scrollTo(0,0)}function h(a){a=G(a)?a:c.hash();var b;a?(b=g.getElementById(a))?f(b):(b=e(g.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var g=a.document;b&&d.$watch(function(){return c.hash()},function(a,b){a===b&&""===a||Gf(function(){d.$evalAsync(h)})});return h}]}function ib(b,a){if(!b&&!a)return"";if(!b)return a;if(!a)return b;J(b)&&(b=b.join(" "));J(a)&&(a=a.join(" "));return b+" "+a}function Jf(b){G(b)&&(b=b.split(" "));var a=fa();m(b,function(b){ [...]
+(a[b]=!0)});return a}function Ia(b){return C(b)?b:{}}function Kf(b,a,c,d){function e(a){try{a.apply(null,ua.call(arguments,1))}finally{if(E--,0===E)for(;K.length;)try{K.pop()()}catch(b){c.error(b)}}}function f(){ia=null;h();g()}function h(){a:{try{u=n.state;break a}catch(a){}u=void 0}u=v(u)?null:u;ka(u,L)&&(u=L);L=u}function g(){if(z!==l.url()||q!==u)z=l.url(),q=u,m(O,function(a){a(l.url(),u)})}var l=this,k=b.location,n=b.history,p=b.setTimeout,r=b.clearTimeout,t={};l.isMock=!1;var E=0,K [...]
+e;l.$$incOutstandingRequestCount=function(){E++};l.notifyWhenNoOutstandingRequests=function(a){0===E?a():K.push(a)};var u,q,z=k.href,N=a.find("base"),ia=null;h();q=u;l.url=function(a,c,e){v(e)&&(e=null);k!==b.location&&(k=b.location);n!==b.history&&(n=b.history);if(a){var f=q===e;if(z===a&&(!d.history||f))return l;var g=z&&Ja(z)===Ja(a);z=a;q=e;if(!d.history||g&&f){if(!g||ia)ia=a;c?k.replace(a):g?(c=k,e=a.indexOf("#"),e=-1===e?"":a.substr(e),c.hash=e):k.href=a;k.href!==a&&(ia=a)}else n[c [...]
+"pushState"](e,"",a),h(),q=u;return l}return ia||k.href.replace(/%27/g,"'")};l.state=function(){return u};var O=[],H=!1,L=null;l.onUrlChange=function(a){if(!H){if(d.history)B(b).on("popstate",f);B(b).on("hashchange",f);H=!0}O.push(a);return a};l.$$applicationDestroyed=function(){B(b).off("hashchange popstate",f)};l.$$checkUrlChange=g;l.baseHref=function(){var a=N.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};l.defer=function(a,b){var c;E++;c=p(function(){delete t[c];e( [...]
+t[c]=!0;return c};l.defer.cancel=function(a){return t[a]?(delete t[a],r(a),e(y),!0):!1}}function Ve(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new Kf(b,d,a,c)}]}function We(){this.$get=function(){function b(b,d){function e(a){a!=p&&(r?r==a&&(r=a.n):r=a,f(a.n,a.p),f(a,p),p=a,p.n=null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw I("$cacheFactory")("iid",b);var h=0,g=P({},d,{id:b}),l={},k=d&&d.capacity||Number.MAX_VALUE,n={},p=null,r=null [...]
+{put:function(a,b){if(!v(b)){if(k<Number.MAX_VALUE){var c=n[a]||(n[a]={key:a});e(c)}a in l||h++;l[a]=b;h>k&&this.remove(r.key);return b}},get:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;e(b)}return l[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;b==p&&(p=b.p);b==r&&(r=b.n);f(b.n,b.p);delete n[a]}delete l[a];h--},removeAll:function(){l={};h=0;n={};p=r=null},destroy:function(){n=g=l=null;delete a[b]},info:function(){return P({},g,{size:h})}}}var a={}; [...]
+{};m(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function rf(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Dc(b,a){function c(a,b,c){var d=/^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/,e={};m(a,function(a,f){var g=a.match(d);if(!g)throw ga("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f}});return e}function d(a){var b=a.charAt [...]
+b!==F(b))throw ga("baddir",a);if(a!==a.trim())throw ga("baddir",a);}var e={},f=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,h=/(([\w\-]+)(?:\:([^;]+))?;?)/,g=Wd("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,k=/^(on[a-z]+|formaction)$/;this.directive=function r(a,f){Ta(a,"directive");G(a)?(d(a),qb(f,"directiveFactory"),e.hasOwnProperty(a)||(e[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,d){var f=[];m(e[a],function(e,g){try{var h=b.invoke(e);x(h)?h={com [...]
+!h.compile&&h.link&&(h.compile=qa(h.link));h.priority=h.priority||0;h.index=g;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";var k=h,l=h,r=h.name,n={isolateScope:null,bindToController:null};C(l.scope)&&(!0===l.bindToController?(n.bindToController=c(l.scope,r,!0),n.isolateScope={}):n.isolateScope=c(l.scope,r,!1));C(l.bindToController)&&(n.bindToController=c(l.bindToController,r,!0));if(C(n.bindToController)){var S=l.controller,E=l.controllerAs;if(!S [...]
+r);var ca;a:if(E&&G(E))ca=E;else{if(G(S)){var m=Vc.exec(S);if(m){ca=m[3];break a}}ca=void 0}if(!ca)throw ga("noident",r);}var s=k.$$bindings=n;C(s.isolateScope)&&(h.$$isolateBindings=s.isolateScope);h.$$moduleName=e.$$moduleName;f.push(h)}catch(w){d(w)}});return f}])),e[a].push(f)):m(a,oc(r));return this};this.aHrefSanitizationWhitelist=function(b){return A(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return A(b)?( [...]
+this):a.imgSrcSanitizationWhitelist()};var n=!0;this.debugInfoEnabled=function(a){return A(a)?(n=a,this):n};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,b,c,d,u,q,z,N,ia,O,H){function L(a,b){try{a.addClass(b)}catch(c){}}function W(a,b,c,d,e){a instanceof B||(a=B(a));m(a,function(b,c){b.nodeType==Pa&&b.nodeValue.match(/\S+/)&&(a[c]=B(b).wrap("<span></span>").parent( [...]
+S(a,b,a,c,d,e);W.$$addScopeClass(a);var g=null;return function(b,c,d){qb(b,"scope");d=d||{};var e=d.parentBoundTranscludeFn,h=d.transcludeControllers;d=d.futureParentElement;e&&e.$$boundTransclude&&(e=e.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==wa(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?B(Xb(g,B("<div>").append(a).html())):c?Ra.clone.call(a):a;if(h)for(var k in h)d.data("$"+k+"Controller",h[k].instance);W.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,e);re [...]
+b,c,d,e,f){function g(a,c,d,e){var f,k,l,r,n,t,O;if(q)for(O=Array(c.length),r=0;r<h.length;r+=3)f=h[r],O[f]=c[f];else O=c;r=0;for(n=h.length;r<n;)if(k=O[h[r++]],c=h[r++],f=h[r++],c){if(c.scope){if(l=a.$new(),W.$$addScopeInfo(B(k),l),t=c.$$destroyBindings)c.$$destroyBindings=null,l.$on("$destroyed",t)}else l=a;t=c.transcludeOnThisElement?ba(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?ba(a,b):null;c(f,l,k,d,t,c)}else f&&f(a,k.childNodes,w,e)}for(var h=[],k,l,r,n,q,t=0;t<a.length; [...]
+l=ca(a[t],[],k,0===t?d:w,e);(f=l.length?D(l,a[t],k,b,c,null,[],[],f):null)&&f.scope&&W.$$addScopeClass(k.$$element);k=f&&f.terminal||!(r=a[t].childNodes)||!r.length?null:S(r,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)h.push(t,f,k),n=!0,q=q||f;f=null}return n?g:null}function ba(a,b,c){return function(d,e,f,g,h){d||(d=a.$new(!1,h),d.$$transcluded=!0);return b(d,e,{parentBoundTranscludeFn:c,transcludeControllers:f,futureParentElement:g})}}function ca(a, [...]
+c.$attr,k;switch(a.nodeType){case pa:na(b,ya(wa(a)),"E",d,e);for(var l,r,n,q=a.attributes,t=0,O=q&&q.length;t<O;t++){var K=!1,H=!1;l=q[t];k=l.name;r=T(l.value);l=ya(k);if(n=ja.test(l))k=k.replace(Wc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});var S=l.replace(/(Start|End)$/,"");I(S)&&l===S+"Start"&&(K=k,H=k.substr(0,k.length-5)+"end",k=k.substr(0,k.length-6));l=ya(k.toLowerCase());g[l]=k;if(n||!c.hasOwnProperty(l))c[l]=r,Rc(a,l)&&(c[l]=!0);V(a,b,r,l,n);na(b,l,"A", [...]
+a.className;C(a)&&(a=a.animVal);if(G(a)&&""!==a)for(;k=h.exec(a);)l=ya(k[2]),na(b,l,"C",d,e)&&(c[l]=T(k[3])),a=a.substr(k.index+k[0].length);break;case Pa:if(11===Wa)for(;a.parentNode&&a.nextSibling&&a.nextSibling.nodeType===Pa;)a.nodeValue+=a.nextSibling.nodeValue,a.parentNode.removeChild(a.nextSibling);Ka(b,a.nodeValue);break;case 8:try{if(k=f.exec(a.nodeValue))l=ya(k[1]),na(b,l,"M",d,e)&&(c[l]=T(k[2]))}catch(E){}}b.sort(M);return b}function za(a,b,c){var d=[],e=0;if(b&&a.hasAttribute& [...]
+b,c);a.nodeType==pa&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return B(d)}function s(a,b,c){return function(d,e,f,g,h){e=za(e[0],b,c);return a(d,e,f,g,h)}}function D(a,b,d,e,f,g,h,k,r){function n(a,b,c,d){if(a){c&&(a=s(a,c,d));a.require=D.require;a.directiveName=y;if(u===D||D.$$isolateScope)a=$(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=s(b,c,d));b.require=D.require;b.directiveName=y;if(u===D||D.$$isolateScope)b=$(b,{isolateScope [...]
+function t(a,b,c,d){var e;if(G(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;e||(d="$"+b+"Controller",e=g?c.inheritedData(d):c.data(d));if(!e&&!f)throw ga("ctreq",b,a);}else if(J(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=t(a,b[g],c,d);return e||null}function O(a,b,c,d,e,f){var g=fa(),h;for(h in d){var k=d[h],l={$scope:k===u||k.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},r=k.controller;"@"==r&&(r=b[k.n [...]
+l,!0,k.controllerAs);g[k.name]=l;ia||a.data("$"+k.name+"Controller",l.instance)}return g}function K(a,c,e,f,g,l){function r(a,b,c){var d;ab(a)||(c=b,b=a,a=w);ia&&(d=ca);c||(c=ia?N.parent():N);return g(a,b,d,c,za)}var n,q,H,E,ca,z,N;b===e?(f=d,N=d.$$element):(N=B(e),f=new Z(N,d));u&&(E=c.$new(!0));g&&(z=r,z.$$boundTransclude=g);ba&&(ca=O(N,f,z,ba,E,c));u&&(W.$$addScopeInfo(N,E,!0,!(L&&(L===u||L===u.$$originalDirective))),W.$$addScopeClass(N,!0),E.$$isolateBindings=u.$$isolateBindings,Y(c, [...]
+u,E));if(ca){var Va=u||S,m;Va&&ca[Va.name]&&(q=Va.$$bindings.bindToController,(H=ca[Va.name])&&H.identifier&&q&&(m=H,l.$$destroyBindings=Y(c,f,H.instance,q,Va)));for(n in ca){H=ca[n];var D=H();D!==H.instance&&(H.instance=D,N.data("$"+n+"Controller",D),H===m&&(l.$$destroyBindings(),l.$$destroyBindings=Y(c,f,D,q,Va)))}}n=0;for(l=h.length;n<l;n++)q=h[n],aa(q,q.isolateScope?E:c,N,f,q.require&&t(q.directiveName,q.require,N,ca),z);var za=c;u&&(u.template||null===u.templateUrl)&&(za=E);a&&a(za, [...]
+w,g);for(n=k.length-1;0<=n;n--)q=k[n],aa(q,q.isolateScope?E:c,N,f,q.require&&t(q.directiveName,q.require,N,ca),z)}r=r||{};for(var H=-Number.MAX_VALUE,S=r.newScopeDirective,ba=r.controllerDirectives,u=r.newIsolateScopeDirective,L=r.templateDirective,z=r.nonTlbTranscludeDirective,N=!1,m=!1,ia=r.hasElementTranscludeDirective,v=d.$$element=B(b),D,y,M,Ka=e,na,I=0,F=a.length;I<F;I++){D=a[I];var P=D.$$start,R=D.$$end;P&&(v=za(b,P,R));M=w;if(H>D.priority)break;if(M=D.scope)D.templateUrl||(C(M)?( [...]
+u||S,D,v),u=D):Q("new/isolated scope",u,D,v)),S=S||D;y=D.name;!D.templateUrl&&D.controller&&(M=D.controller,ba=ba||fa(),Q("'"+y+"' controller",ba[y],D,v),ba[y]=D);if(M=D.transclude)N=!0,D.$$tlb||(Q("transclusion",z,D,v),z=D),"element"==M?(ia=!0,H=D.priority,M=v,v=d.$$element=B(X.createComment(" "+y+": "+d[y]+" ")),b=v[0],U(f,ua.call(M,0),b),Ka=W(M,e,H,g&&g.name,{nonTlbTranscludeDirective:z})):(M=B(Ub(b)).contents(),v.empty(),Ka=W(M,e));if(D.template)if(m=!0,Q("template",L,D,v),L=D,M=x(D. [...]
+D.template(v,d):D.template,M=ha(M),D.replace){g=D;M=Sb.test(M)?Xc(Xb(D.templateNamespace,T(M))):[];b=M[0];if(1!=M.length||b.nodeType!==pa)throw ga("tplrt",y,"");U(f,v,b);F={$attr:{}};M=ca(b,[],F);var Lf=a.splice(I+1,a.length-(I+1));u&&A(M);a=a.concat(M).concat(Lf);Yc(d,F);F=a.length}else v.html(M);if(D.templateUrl)m=!0,Q("template",L,D,v),L=D,D.replace&&(g=D),K=Mf(a.splice(I,a.length-I),v,d,f,N&&Ka,h,k,{controllerDirectives:ba,newScopeDirective:S!==D&&S,newIsolateScopeDirective:u,templat [...]
+nonTlbTranscludeDirective:z}),F=a.length;else if(D.compile)try{na=D.compile(v,d,Ka),x(na)?n(null,na,P,R):na&&n(na.pre,na.post,P,R)}catch(V){c(V,xa(v))}D.terminal&&(K.terminal=!0,H=Math.max(H,D.priority))}K.scope=S&&!0===S.scope;K.transcludeOnThisElement=N;K.templateOnThisElement=m;K.transclude=Ka;r.hasElementTranscludeDirective=ia;return K}function A(a){for(var b=0,c=a.length;b<c;b++)a[b]=Nb(a[b],{$$isolateScope:!0})}function na(b,d,f,g,h,k,l){if(d===h)return null;h=null;if(e.hasOwnPrope [...]
+d=a.get(d+"Directive");for(var q=0,t=d.length;q<t;q++)try{n=d[q],(v(g)||g>n.priority)&&-1!=n.restrict.indexOf(f)&&(k&&(n=Nb(n,{$$start:k,$$end:l})),b.push(n),h=n)}catch(H){c(H)}}return h}function I(b){if(e.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,f=c.length;d<f;d++)if(b=c[d],b.multiElement)return!0;return!1}function Yc(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,func [...]
+f?(L(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==f?(e.attr("style",e.attr("style")+";"+b),a.style=(a.style?a.style+";":"")+b):"$"==f.charAt(0)||a.hasOwnProperty(f)||(a[f]=b,d[f]=c[f])})}function Mf(a,b,c,e,f,g,h,k){var l=[],r,n,q=b[0],t=a.shift(),H=Nb(t,{templateUrl:null,transclude:null,replace:null,$$originalDirective:t}),O=x(t.templateUrl)?t.templateUrl(b,c):t.templateUrl,E=t.templateNamespace;b.empty();d(O).then(function(d){var K,u;d=ha(d);if(t.replace){d=Sb.test(d)?Xc( [...]
+[];K=d[0];if(1!=d.length||K.nodeType!==pa)throw ga("tplrt",t.name,O);d={$attr:{}};U(e,b,K);var z=ca(K,[],d);C(t.scope)&&A(z);a=z.concat(a);Yc(c,d)}else K=q,b.html(d);a.unshift(H);r=D(a,K,c,f,b,t,g,h,k);m(e,function(a,c){a==K&&(e[c]=b[0])});for(n=S(b[0].childNodes,f);l.length;){d=l.shift();u=l.shift();var N=l.shift(),W=l.shift(),z=b[0];if(!d.$$destroyed){if(u!==q){var za=u.className;k.hasElementTranscludeDirective&&t.replace||(z=Ub(K));U(N,B(u),z);L(B(z),za)}u=r.transcludeOnThisElement?ba [...]
+W):W;r(n,d,z,e,u,r)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(r.transcludeOnThisElement&&(a=ba(b,r.transclude,e)),r(n,b,c,d,a,r)))}}function M(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.name<b.name?-1:1:a.index-b.index}function Q(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw ga("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,xa(d));}function Ka(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:funct [...]
+a.parent();var b=!!a.length;b&&W.$$addBindingClass(a);return function(a,c){var e=c.parent();b||W.$$addBindingClass(e);W.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Xb(a,b){a=F(a||"html");switch(a){case "svg":case "math":var c=X.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function R(a,b){if("srcdoc"==b)return ia.HTML;var c=wa(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&& [...]
+"ngSrc"==b))return ia.RESOURCE_URL}function V(a,c,d,e,f){var h=R(a,e);f=g[e]||f;var l=b(d,!0,h,f);if(l){if("multiple"===e&&"select"===wa(a))throw ga("selmulti",xa(a));c.push({priority:100,compile:function(){return{pre:function(a,c,g){c=g.$$observers||(g.$$observers=fa());if(k.test(e))throw ga("nodomevents");var r=g[e];r!==d&&(l=r&&b(r,!0,h,f),d=r);l&&(g[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(g.$$observers&&g.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!=b?g.$updateClas [...]
+a)}))}}}})}}function U(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=X.createDocumentFragment();a.appendChild(d);B.hasData(d)&&(B(c).data(B(d).data()),ra?(Qb=!0,ra.cleanData([d])):delete B.cache[d[B.expando]]);d=1;for(e=b.length;d<e;d++)f=b[d],B(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.le [...]
+b){return P(function(){return a.apply(null,arguments)},a,b)}function aa(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,xa(d))}}function Y(a,c,d,e,f,g){var h;m(e,function(e,g){var k=e.attrName,l=e.optional,r,n,q,K;switch(e.mode){case "@":l||ta.call(c,k)||(d[g]=c[k]=void 0);c.$observe(k,function(a){G(a)&&(d[g]=a)});c.$$observers[k].$$scope=a;G(c[k])&&(d[g]=b(c[k])(a));break;case "=":if(!ta.call(c,k)){if(l)break;c[k]=void 0}if(l&&!c[k])break;n=u(c[k]);K=n.literal?ka:function(a,b){return a===b|| [...]
+b};q=n.assign||function(){r=d[g]=n(a);throw ga("nonassign",c[k],f.name);};r=d[g]=n(a);l=function(b){K(b,d[g])||(K(b,r)?q(a,b=d[g]):d[g]=b);return r=b};l.$stateful=!0;l=e.collection?a.$watchCollection(c[k],l):a.$watch(u(c[k],l),null,n.literal);h=h||[];h.push(l);break;case "&":n=c.hasOwnProperty(k)?u(c[k]):y;if(n===y&&l)break;d[g]=function(b){return n(a,b)}}});e=h?function(){for(var a=0,b=h.length;a<b;++a)h[a]()}:y;return g&&e!==y?(g.$on("$destroy",e),y):e}var Z=function(a,b){if(b){var c=O [...]
+d,e,f;d=0;for(e=c.length;d<e;d++)f=c[d],this[f]=b[f]}else this.$attr={};this.$$element=a};Z.prototype={$normalize:ya,$addClass:function(a){a&&0<a.length&&O.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&O.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=Zc(a,b);c&&c.length&&O.addClass(this.$$element,c);(c=Zc(b,a))&&c.length&&O.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=Rc(this.$$element[0],a),g=$c[a],h=a;f?(this.$$element.prop(a,b),e=f [...]
+b,h=g);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Ac(a,"-"));f=wa(this.$$element);if("a"===f&&"href"===a||"img"===f&&"src"===a)this[a]=b=H(b,"src"===a);else if("img"===f&&"srcset"===a){for(var f="",g=T(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(g)?k:/(,)/,g=g.split(k),k=Math.floor(g.length/2),l=0;l<k;l++)var r=2*l,f=f+H(T(g[r]),!0),f=f+(" "+T(g[r+1]));g=T(g[2*l]).split(/\s/);f+=H(T(g[0]),!0);2===g.length&&(f+=" "+T(g[1]));this[a]=b=f}!1!==d&&(null===b||v( [...]
+this.$$element.attr(e,b));(a=this.$$observers)&&m(a[h],function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=fa()),e=d[a]||(d[a]=[]);e.push(b);z.$evalAsync(function(){e.$$inter||!c.hasOwnProperty(a)||v(c[a])||b(c[a])});return function(){cb(e,b)}}};var da=b.startSymbol(),ea=b.endSymbol(),ha="{{"==da||"}}"==ea?$a:function(a){return a.replace(/\{\{/g,da).replace(/}}/g,ea)},ja=/^ngAttr[A-Z]/;W.$$addBindingInfo=n?function(a,b){var c=a.data("$ [...]
+[];J(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:y;W.$$addBindingClass=n?function(a){L(a,"ng-binding")}:y;W.$$addScopeInfo=n?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:y;W.$$addScopeClass=n?function(a,b){L(a,b?"ng-isolate-scope":"ng-scope")}:y;return W}]}function ya(b){return gb(b.replace(Wc,""))}function Zc(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),f=0;a:for(;f<d.length;f++){for(var h=d[f],g=0;g<e.length;g++)if(h==e[g])continue a;c+=(0 [...]
+" ":"")+h}return c}function Xc(b){b=B(b);var a=b.length;if(1>=a)return b;for(;a--;)8===b[a].nodeType&&Nf.call(b,a,1);return b}function Xe(){var b={},a=!1;this.register=function(a,d){Ta(a,"controller");C(a)?P(b,a):b[a]=d};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(c,d){function e(a,b,c,d){if(!a||!C(a.$scope))throw I("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,h,g,l){var k,n,p;g=!0===g;l&&G(l)&&(p=l);if(G(f)){l=f.match(Vc);if(!l)throw Of("c [...]
+n=l[1];p=p||l[3];f=b.hasOwnProperty(n)?b[n]:Cc(h.$scope,n,!0)||(a?Cc(d,n,!0):w);Sa(f,n,!0)}if(g)return g=(J(f)?f[f.length-1]:f).prototype,k=Object.create(g||null),p&&e(h,p,k,n||f.name),P(function(){var a=c.invoke(f,k,h,n);a!==k&&(C(a)||x(a))&&(k=a,p&&e(h,p,k,n||f.name));return k},{instance:k,identifier:p});k=c.instantiate(f,h,n);p&&e(h,p,k,n||f.name);return k}}]}function Ye(){this.$get=["$window",function(b){return B(b.document)}]}function Ze(){this.$get=["$log",function(b){return functi [...]
+arguments)}}]}function Yb(b){return C(b)?ea(b)?b.toISOString():eb(b):b}function df(){this.$get=function(){return function(b){if(!b)return"";var a=[];nc(b,function(b,d){null===b||v(b)||(J(b)?m(b,function(b,c){a.push(la(d)+"="+la(Yb(b)))}):a.push(la(d)+"="+la(Yb(b))))});return a.join("&")}}}function ef(){this.$get=function(){return function(b){function a(b,e,f){null===b||v(b)||(J(b)?m(b,function(b,c){a(b,e+"["+(C(b)?c:"")+"]")}):C(b)&&!ea(b)?nc(b,function(b,c){a(b,e+(f?"":"[")+c+(f?"":"]") [...]
+"="+la(Yb(b))))}if(!b)return"";var c=[];a(b,"",!0);return c.join("&")}}}function Zb(b,a){if(G(b)){var c=b.replace(Pf,"").trim();if(c){var d=a("Content-Type");(d=d&&0===d.indexOf(ad))||(d=(d=c.match(Qf))&&Rf[d[0]].test(c));d&&(b=vc(c))}}return b}function bd(b){var a=fa(),c;G(b)?m(b.split("\n"),function(b){c=b.indexOf(":");var e=F(T(b.substr(0,c)));b=T(b.substr(c+1));e&&(a[e]=a[e]?a[e]+", "+b:b)}):C(b)&&m(b,function(b,c){var f=F(c),h=T(b);f&&(a[f]=a[f]?a[f]+", "+h:h)});return a}function cd [...]
+return function(c){a||(a=bd(b));return c?(c=a[F(c)],void 0===c&&(c=null),c):a}}function dd(b,a,c,d){if(x(d))return d(b,a,c);m(d,function(d){b=d(b,a,c)});return b}function cf(){var b=this.defaults={transformResponse:[Zb],transformRequest:[function(a){return C(a)&&"[object File]"!==va.call(a)&&"[object Blob]"!==va.call(a)&&"[object FormData]"!==va.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ja($b),put:ja($b),patch:ja($b)},xsrfCookieName:"XSRF-TOKEN", [...]
+paramSerializer:"$httpParamSerializer"},a=!1;this.useApplyAsync=function(b){return A(b)?(a=!!b,this):a};var c=!0;this.useLegacyPromiseExtensions=function(a){return A(a)?(c=!!a,this):c};var d=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector",function(e,f,h,g,l,k){function n(a){function d(a){var b=P({},a);b.data=a.data?dd(a.data,a.headers,a.status,f.transformResponse):a.data;a=a.status;return 200<=a&&300>a?b:l.reject(b)}function e [...]
+d={};m(a,function(a,e){x(a)?(c=a(b),null!=c&&(d[e]=c)):d[e]=a});return d}if(!da.isObject(a))throw I("$http")("badreq",a);var f=P({method:"get",transformRequest:b.transformRequest,transformResponse:b.transformResponse,paramSerializer:b.paramSerializer},a);f.headers=function(a){var c=b.headers,d=P({},a.headers),f,g,h,c=P({},c.common,c[F(a.method)]);a:for(f in c){g=F(f);for(h in d)if(F(h)===g)continue a;d[f]=c[f]}return e(d,ja(a))}(a);f.method=sb(f.method);f.paramSerializer=G(f.paramSeriali [...]
+f.paramSerializer;var g=[function(a){var c=a.headers,e=dd(a.data,cd(c),w,a.transformRequest);v(e)&&m(c,function(a,b){"content-type"===F(b)&&delete c[b]});v(a.withCredentials)&&!v(b.withCredentials)&&(a.withCredentials=b.withCredentials);return p(a,e).then(d,d)},w],h=l.when(f);for(m(E,function(a){(a.request||a.requestError)&&g.unshift(a.request,a.requestError);(a.response||a.responseError)&&g.push(a.response,a.responseError)});g.length;){a=g.shift();var r=g.shift(),h=h.then(a,r)}c?(h.succ [...]
+"fn");h.then(function(b){a(b.data,b.status,b.headers,f)});return h},h.error=function(a){Sa(a,"fn");h.then(null,function(b){a(b.data,b.status,b.headers,f)});return h}):(h.success=ed("success"),h.error=ed("error"));return h}function p(c,d){function h(b,c,d,e){function f(){k(c,b,d,e)}L&&(200<=b&&300>b?L.put(ba,[b,c,bd(d),e]):L.remove(ba));a?g.$applyAsync(f):(f(),g.$$phase||g.$apply())}function k(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?O.resolve:O.reject)({data:a,status:b,headers:cd(d),config:c, [...]
+function p(a){k(a.data,a.status,ja(a.headers()),a.statusText)}function E(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var O=l.defer(),H=O.promise,L,m,S=c.headers,ba=r(c.url,c.paramSerializer(c.params));n.pendingRequests.push(c);H.then(E,E);!c.cache&&!b.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(L=C(c.cache)?c.cache:C(b.cache)?b.cache:t);L&&(m=L.get(ba),A(m)?m&&x(m.then)?m.then(p,p):J(m)?k(m[1],m[0],ja(m[2]),m[3]):k(m,200,{},"OK"):L.put(ba,H) [...]
+fd(c.url)?f()[c.xsrfCookieName||b.xsrfCookieName]:w)&&(S[c.xsrfHeaderName||b.xsrfHeaderName]=m),e(c.method,ba,d,h,S,c.timeout,c.withCredentials,c.responseType));return H}function r(a,b){0<b.length&&(a+=(-1==a.indexOf("?")?"?":"&")+b);return a}var t=h("$http");b.paramSerializer=G(b.paramSerializer)?k.get(b.paramSerializer):b.paramSerializer;var E=[];m(d,function(a){E.unshift(G(a)?k.get(a):k.invoke(a))});n.pendingRequests=[];(function(a){m(arguments,function(a){n[a]=function(b,c){return n( [...]
+{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){n[a]=function(b,c,d){return n(P({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");n.defaults=b;return n}]}function gf(){this.$get=function(){return function(){return new Q.XMLHttpRequest}}}function ff(){this.$get=["$browser","$window","$document","$xhrFactory",function(b,a,c,d){return Sf(b,d,b.defer,a.angular.callbacks,c[0])}]}function Sf(b,a,c,d,e){function f(a,b,c){var f=e.createElem [...]
+n=null;f.type="text/javascript";f.src=a;f.async=!0;n=function(a){f.removeEventListener("load",n,!1);f.removeEventListener("error",n,!1);e.body.removeChild(f);f=null;var h=-1,t="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),t=a.type,h="error"===a.type?404:200);c&&c(h,t)};f.addEventListener("load",n,!1);f.addEventListener("error",n,!1);e.body.appendChild(f);return n}return function(e,g,l,k,n,p,r,t){function E(){q&&q();z&&z.abort()}function K(a,d,e,f,g){A(s)&&c.cancel(s);q=z [...]
+e,f,g);b.$$completeOutstandingRequest(y)}b.$$incOutstandingRequestCount();g=g||b.url();if("jsonp"==F(e)){var u="_"+(d.counter++).toString(36);d[u]=function(a){d[u].data=a;d[u].called=!0};var q=f(g.replace("JSON_CALLBACK","angular.callbacks."+u),u,function(a,b){K(k,a,d[u].data,"",b);d[u]=y})}else{var z=a(e,g);z.open(e,g,!0);m(n,function(a,b){A(a)&&z.setRequestHeader(b,a)});z.onload=function(){var a=z.statusText||"",b="response"in z?z.response:z.responseText,c=1223===z.status?204:z.status; [...]
+b?200:"file"==Aa(g).protocol?404:0);K(k,c,b,z.getAllResponseHeaders(),a)};e=function(){K(k,-1,null,null,"")};z.onerror=e;z.onabort=e;r&&(z.withCredentials=!0);if(t)try{z.responseType=t}catch(N){if("json"!==t)throw N;}z.send(v(l)?null:l)}if(0<p)var s=c(E,p);else p&&x(p.then)&&p.then(E)}}function af(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function [...]
+a}function h(c){return c.replace(n,b).replace(p,a)}function g(f,g,n,p){function u(a){try{var b=a;a=n?e.getTrusted(n,b):e.valueOf(b);var c;if(p&&!A(a))c=a;else if(null==a)c="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=eb(a)}c=a}return c}catch(g){d(La.interr(f,g))}}p=!!p;for(var q,m,N=0,s=[],O=[],H=f.length,L=[],W=[];N<H;)if(-1!=(q=f.indexOf(b,N))&&-1!=(m=f.indexOf(a,q+l)))N!==q&&L.push(h(f.substring(N,q))),N=f.substring(q+l,m),s.push(N),O.push(c(N,u)), [...]
+L.push("");else{N!==H&&L.push(h(f.substring(N)));break}n&&1<L.length&&La.throwNoconcat(f);if(!g||s.length){var S=function(a){for(var b=0,c=s.length;b<c;b++){if(p&&v(a[b]))return;L[W[b]]=a[b]}return L.join("")};return P(function(a){var b=0,c=s.length,e=Array(c);try{for(;b<c;b++)e[b]=O[b](a);return S(e)}catch(g){d(La.interr(f,g))}},{exp:f,expressions:s,$$watchDelegate:function(a,b){var c;return a.$watchGroup(O,function(d,e){var f=S(d);x(b)&&b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=b.leng [...]
+n=new RegExp(b.replace(/./g,f),"g"),p=new RegExp(a.replace(/./g,f),"g");g.startSymbol=function(){return b};g.endSymbol=function(){return a};return g}]}function bf(){this.$get=["$rootScope","$window","$q","$$q",function(b,a,c,d){function e(e,g,l,k){var n=4<arguments.length,p=n?ua.call(arguments,4):[],r=a.setInterval,t=a.clearInterval,E=0,K=A(k)&&!k,u=(K?d:c).defer(),q=u.promise;l=A(l)?l:0;q.then(null,null,n?function(){e.apply(null,p)}:e);q.$$intervalId=r(function(){u.notify(E++);0<l&&E>=l [...]
+t(q.$$intervalId),delete f[q.$$intervalId]);K||b.$apply()},g);f[q.$$intervalId]=u;return q}var f={};e.cancel=function(b){return b&&b.$$intervalId in f?(f[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete f[b.$$intervalId],!0):!1};return e}]}function ac(b){b=b.split("/");for(var a=b.length;a--;)b[a]=ob(b[a]);return b.join("/")}function gd(b,a){var c=Aa(b);a.$$protocol=c.protocol;a.$$host=c.hostname;a.$$port=Y(c.port)||Tf[c.protocol]||null}function hd(b,a){var c="/" [...]
+c&&(b="/"+b);var d=Aa(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search=yc(d.search);a.$$hash=decodeURIComponent(d.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function sa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ja(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Cb(b){return b.replace(/(#.+)|#$/,"$1")}function bc(b,a,c){this.$$html5=!0;c=c||"";gd(b,this);this.$$parse=function(b) [...]
+b);if(!G(c))throw Db("ipthprfx",b,a);hd(c,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var b=Pb(this.$$search),c=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=ac(this.$$path)+(b?"?"+b:"")+c;this.$$absUrl=a+this.$$url.substr(1)};this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,h;A(f=sa(b,d))?(h=f,h=A(f=sa(c,f))?a+(sa("/",f)||f):b+h):A(f=sa(a,d))?h=a+f:a==d+"/"&&(h=a);h&&this.$$parse(h);return!!h}}function cc(b,a,c [...]
+this.$$parse=function(d){var e=sa(b,d)||sa(a,d),f;v(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",v(e)&&(b=d,this.replace())):(f=sa(c,e),v(f)&&(f=e));hd(f,this);d=this.$$path;var e=b,h=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&(f=f.replace(e,""));h.exec(f)||(d=(f=h.exec(d))?f[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var a=Pb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=ac(this.$$path)+(a?"?"+a:"")+e;this.$$absUrl=b+(this.$$url?c+this.$$url:"")};this.$$pa [...]
+function(a,c){return Ja(b)==Ja(a)?(this.$$parse(a),!0):!1}}function id(b,a,c){this.$$html5=!0;cc.apply(this,arguments);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,h;b==Ja(d)?f=d:(h=sa(a,d))?f=b+c+h:a===d+"/"&&(f=a);f&&this.$$parse(f);return!!f};this.$$compose=function(){var a=Pb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=ac(this.$$path)+(a?"?"+a:"")+e;this.$$absUrl=b+c+this.$$url}}function Eb(b){return function(){return thi [...]
+a){return function(c){if(v(c))return this[b];this[b]=a(c);this.$$compose();return this}}function hf(){var b="",a={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(a){return A(a)?(b=a,this):b};this.html5Mode=function(b){return bb(b)?(a.enabled=b,this):C(b)?(bb(b.enabled)&&(a.enabled=b.enabled),bb(b.requireBase)&&(a.requireBase=b.requireBase),bb(b.rewriteLinks)&&(a.rewriteLinks=b.rewriteLinks),this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window [...]
+d,e,f,h){function g(a,b,c){var e=k.url(),f=k.$$state;try{d.url(a,b,c),k.$$state=d.state()}catch(g){throw k.url(e),k.$$state=f,g;}}function l(a,b){c.$broadcast("$locationChangeSuccess",k.absUrl(),a,k.$$state,b)}var k,n;n=d.baseHref();var p=d.url(),r;if(a.enabled){if(!n&&a.requireBase)throw Db("nobase");r=p.substring(0,p.indexOf("/",p.indexOf("//")+2))+(n||"/");n=e.history?bc:id}else r=Ja(p),n=cc;var t=r.substr(0,Ja(r).lastIndexOf("/")+1);k=new n(r,t,"#"+b);k.$$parseLinkUrl(p,p);k.$$state= [...]
+var E=/^\s*(javascript|mailto):/i;f.on("click",function(b){if(a.rewriteLinks&&!b.ctrlKey&&!b.metaKey&&!b.shiftKey&&2!=b.which&&2!=b.button){for(var e=B(b.target);"a"!==wa(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;var g=e.prop("href"),l=e.attr("href")||e.attr("xlink:href");C(g)&&"[object SVGAnimatedString]"===g.toString()&&(g=Aa(g.animVal).href);E.test(g)||!g||e.attr("target")||b.isDefaultPrevented()||!k.$$parseLinkUrl(g,l)||(b.preventDefault(),k.absUrl()!=d.url()&&(c.$apply(),h.an [...]
+!0))}});Cb(k.absUrl())!=Cb(p)&&d.url(k.absUrl(),!0);var K=!0;d.onUrlChange(function(a,b){v(sa(t,a))?h.location.href=a:(c.$evalAsync(function(){var d=k.absUrl(),e=k.$$state,f;k.$$parse(a);k.$$state=b;f=c.$broadcast("$locationChangeStart",a,d,b,e).defaultPrevented;k.absUrl()===a&&(f?(k.$$parse(d),k.$$state=e,g(d,!1,e)):(K=!1,l(d,e)))}),c.$$phase||c.$digest())});c.$watch(function(){var a=Cb(d.url()),b=Cb(k.absUrl()),f=d.state(),h=k.$$replace,r=a!==b||k.$$html5&&e.history&&f!==k.$$state;if(K [...]
+c.$evalAsync(function(){var b=k.absUrl(),d=c.$broadcast("$locationChangeStart",b,a,k.$$state,f).defaultPrevented;k.absUrl()===b&&(d?(k.$$parse(a),k.$$state=f):(r&&g(b,h,f===k.$$state?null:k.$$state),l(a,f)))});k.$$replace=!1});return k}]}function jf(){var b=!0,a=this;this.debugEnabled=function(a){return A(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sour [...]
+a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||y;a=!1;try{a=!!e.apply}catch(l){}return a?function(){var a=[];m(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function Xa(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"=== [...]
+b||"__proto__"===b)throw Z("isecfld",a);return b}function kd(b,a){b+="";if(!G(b))throw Z("iseccst",a);return b}function Ba(b,a){if(b){if(b.constructor===b)throw Z("isecfn",a);if(b.window===b)throw Z("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw Z("isecdom",a);if(b===Object)throw Z("isecobj",a);}return b}function ld(b,a){if(b){if(b.constructor===b)throw Z("isecfn",a);if(b===Uf||b===Vf||b===Wf)throw Z("isecff",a);}}function md(b,a){if(b&&(b===(0).constructor||b= [...]
+b==="".constructor||b==={}.constructor||b===[].constructor||b===Function.constructor))throw Z("isecaf",a);}function Xf(b,a){return"undefined"!==typeof b?b:a}function nd(b,a){return"undefined"===typeof b?a:"undefined"===typeof a?b:b+a}function U(b,a){var c,d;switch(b.type){case s.Program:c=!0;m(b.body,function(b){U(b.expression,a);c=c&&b.expression.constant});b.constant=c;break;case s.Literal:b.constant=!0;b.toWatch=[];break;case s.UnaryExpression:U(b.argument,a);b.constant=b.argument.con [...]
+b.argument.toWatch;break;case s.BinaryExpression:U(b.left,a);U(b.right,a);b.constant=b.left.constant&&b.right.constant;b.toWatch=b.left.toWatch.concat(b.right.toWatch);break;case s.LogicalExpression:U(b.left,a);U(b.right,a);b.constant=b.left.constant&&b.right.constant;b.toWatch=b.constant?[]:[b];break;case s.ConditionalExpression:U(b.test,a);U(b.alternate,a);U(b.consequent,a);b.constant=b.test.constant&&b.alternate.constant&&b.consequent.constant;b.toWatch=b.constant?[]:[b];break;case s. [...]
+!1;b.toWatch=[b];break;case s.MemberExpression:U(b.object,a);b.computed&&U(b.property,a);b.constant=b.object.constant&&(!b.computed||b.property.constant);b.toWatch=[b];break;case s.CallExpression:c=b.filter?!a(b.callee.name).$stateful:!1;d=[];m(b.arguments,function(b){U(b,a);c=c&&b.constant;b.constant||d.push.apply(d,b.toWatch)});b.constant=c;b.toWatch=b.filter&&!a(b.callee.name).$stateful?d:[b];break;case s.AssignmentExpression:U(b.left,a);U(b.right,a);b.constant=b.left.constant&&b.righ [...]
+b.toWatch=[b];break;case s.ArrayExpression:c=!0;d=[];m(b.elements,function(b){U(b,a);c=c&&b.constant;b.constant||d.push.apply(d,b.toWatch)});b.constant=c;b.toWatch=d;break;case s.ObjectExpression:c=!0;d=[];m(b.properties,function(b){U(b.value,a);c=c&&b.value.constant;b.value.constant||d.push.apply(d,b.value.toWatch)});b.constant=c;b.toWatch=d;break;case s.ThisExpression:b.constant=!1,b.toWatch=[]}}function od(b){if(1==b.length){b=b[0].expression;var a=b.toWatch;return 1!==a.length?a:a[0] [...]
+function pd(b){return b.type===s.Identifier||b.type===s.MemberExpression}function qd(b){if(1===b.body.length&&pd(b.body[0].expression))return{type:s.AssignmentExpression,left:b.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function rd(b){return 0===b.body.length||1===b.body.length&&(b.body[0].expression.type===s.Literal||b.body[0].expression.type===s.ArrayExpression||b.body[0].expression.type===s.ObjectExpression)}function sd(b,a){this.astBuilder=b;this.$filter=a}funct [...]
+a){this.astBuilder=b;this.$filter=a}function Fb(b){return"constructor"==b}function dc(b){return x(b.valueOf)?b.valueOf():Yf.call(b)}function kf(){var b=fa(),a=fa();this.$get=["$filter",function(c){function d(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=dc(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function e(a,b,c,e,f){var g=e.inputs,h;if(1===g.length){var k=d,g=g[0];return a.$watch(function(a){var b=g(a);d(b,k)||(h=e(a,w,w,[b]),k=b&&dc(b));return h},b,c,f)}for(var l=[] [...]
+m=g.length;p<m;p++)l[p]=d,n[p]=null;return a.$watch(function(a){for(var b=!1,c=0,f=g.length;c<f;c++){var k=g[c](a);if(b||(b=!d(k,l[c])))n[c]=k,l[c]=k&&dc(k)}b&&(h=e(a,w,w,n));return h},b,c,f)}function f(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;x(b)&&b.apply(this,arguments);A(a)&&d.$$postDigest(function(){A(f)&&e()})},c)}function h(a,b,c,d){function e(a){var b=!0;m(a,function(a){A(a)||(b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a [...]
+c,d){g=a;x(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function g(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){x(b)&&b.apply(this,arguments);e()},c)}function l(a,b){if(!b)return a;var c=a.$$watchDelegate,c=c!==h&&c!==f?function(c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}:function(c,d,e,f){e=a(c,d,e,f);c=b(e,c,d);return A(e)?c:e};a.$$watchDelegate&&a.$$watchDelegate!==e?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=e [...]
+a.inputs?a.inputs:[a]);return c}var k=Fa().noUnsafeEval,n={csp:k,expensiveChecks:!1},p={csp:k,expensiveChecks:!0};return function(d,k,E){var m,u,q;switch(typeof d){case "string":q=d=d.trim();var s=E?a:b;m=s[q];m||(":"===d.charAt(0)&&":"===d.charAt(1)&&(u=!0,d=d.substring(2)),E=E?p:n,m=new ec(E),m=(new fc(m,c,E)).parse(d),m.constant?m.$$watchDelegate=g:u?m.$$watchDelegate=m.literal?h:f:m.inputs&&(m.$$watchDelegate=e),s[q]=m);return l(m,k);case "function":return l(d,k);default:return y}}}] [...]
+["$rootScope","$exceptionHandler",function(b,a){return ud(function(a){b.$evalAsync(a)},a)}]}function nf(){this.$get=["$browser","$exceptionHandler",function(b,a){return ud(function(a){b.defer(a)},a)}]}function ud(b,a){function c(a,b,c){function d(b){return function(c){e||(e=!0,b.call(a,c))}}var e=!1;return[d(b),d(c)]}function d(){this.$$state={status:0}}function e(a,b){return function(c){b.call(a,c)}}function f(c){!c.processScheduled&&c.pending&&(c.processScheduled=!0,b(function(){var b, [...]
+c.processScheduled=!1;c.pending=w;for(var f=0,g=e.length;f<g;++f){d=e[f][0];b=e[f][c.status];try{x(b)?d.resolve(b(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),a(h)}}}))}function h(){this.promise=new d;this.resolve=e(this,this.resolve);this.reject=e(this,this.reject);this.notify=e(this,this.notify)}var g=I("$q",TypeError);P(d.prototype,{then:function(a,b,c){if(v(a)&&v(b)&&v(c))return this;var d=new h;this.$$state.pending=this.$$state.pending||[];this.$$ [...]
+a,b,c]);0<this.$$state.status&&f(this.$$state);return d.promise},"catch":function(a){return this.then(null,a)},"finally":function(a,b){return this.then(function(b){return k(b,!0,a)},function(b){return k(b,!1,a)},b)}});P(h.prototype,{resolve:function(a){this.promise.$$state.status||(a===this.promise?this.$$reject(g("qcycle",a)):this.$$resolve(a))},$$resolve:function(b){var d,e;e=c(this,this.$$resolve,this.$$reject);try{if(C(b)||x(b))d=b&&b.then;x(d)?(this.promise.$$state.status=-1,d.call( [...]
+this.notify)):(this.promise.$$state.value=b,this.promise.$$state.status=1,f(this.promise.$$state))}catch(g){e[1](g),a(g)}},reject:function(a){this.promise.$$state.status||this.$$reject(a)},$$reject:function(a){this.promise.$$state.value=a;this.promise.$$state.status=2;f(this.promise.$$state)},notify:function(c){var d=this.promise.$$state.pending;0>=this.promise.$$state.status&&d&&d.length&&b(function(){for(var b,e,f=0,g=d.length;f<g;f++){e=d[f][0];b=d[f][3];try{e.notify(x(b)?b(c):c)}catc [...]
+var l=function(a,b){var c=new h;b?c.resolve(a):c.reject(a);return c.promise},k=function(a,b,c){var d=null;try{x(c)&&(d=c())}catch(e){return l(e,!1)}return d&&x(d.then)?d.then(function(){return l(a,b)},function(a){return l(a,!1)}):l(a,b)},n=function(a,b,c,d){var e=new h;e.resolve(a);return e.promise.then(b,c,d)},p=function t(a){if(!x(a))throw g("norslvr",a);if(!(this instanceof t))return new t(a);var b=new h;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};p.defer=f [...]
+p.reject=function(a){var b=new h;b.reject(a);return b.promise};p.when=n;p.resolve=n;p.all=function(a){var b=new h,c=0,d=J(a)?[]:{};m(a,function(a,e){c++;n(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise};return p}function wf(){this.$get=["$window","$timeout",function(b,a){var c=b.requestAnimationFrame||b.webkitRequestAnimationFrame,d=b.cancelAnimationFrame||b.webkitCancelAnimationFr [...]
+e=!!c,f=e?function(a){var b=c(a);return function(){d(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};f.supported=e;return f}]}function lf(){function b(a){function b(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++nb;this.$$ChildScope=null}b.prototype=a;return b}var a=10,c=I("$rootScope"),d=null,e=null;this.digestTtl=function(b){arguments.length&&(a=b);return a};t [...]
+["$injector","$exceptionHandler","$parse","$browser",function(f,h,g,l){function k(a){a.currentScope.$$destroyed=!0}function n(){this.$id=++nb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function p(a){if(q.$$phase)throw c("inprog",q.$$phase);q.$$phase=a}function r(a,b){do a.$$watchersCo [...]
+a.$parent)}function t(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function E(){}function s(){for(;w.length;)try{w.shift()()}catch(a){h(a)}e=null}function u(){null===e&&(e=l.defer(function(){q.$apply(s)}))}n.prototype={constructor:n,$new:function(a,c){var d;c=c||this;a?(d=new n,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=b(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c [...]
+d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(a||c!=this)&&d.$on("$destroy",k);return d},$watch:function(a,b,c,e){var f=g(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,c,f,a);var h=this,k=h.$$watchers,l={fn:b,last:E,get:f,exp:e||a,eq:!!c};d=null;x(b)||(l.fn=y);k||(k=h.$$watchers=[]);k.unshift(l);r(this,1);return function(){0<=cb(k,l)&&r(h,-1);d=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h [...]
+if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!1}}if(1===a.length)return this.$watch(a[0],function(a,c,f){e[0]=a;d[0]=c;b(e,a===c?e:d,f)});m(a,function(a,b){var k=g.$watch(a,function(a,f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!v(e)){if(C(e))if(Da(e))for(f!==p&&(f=p,t=f.length=0,l++),a=e.length,t!==a&&(l++,f.length=t=a),b=0;b<a;b [...]
+g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==r&&(f=r={},t=0,l++);a=0;for(b in e)ta.call(e,b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(t++,f[b]=g,l++));if(t>a)for(b in l++,f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1<b.length,l=0,n=g(a,c),p=[],r={},q=!0,t=0;return this.$watch(n,function(){q?(q=!1,b(e,e,d)):b(e,h,d);if(k)if(C(e))if(Da(e)){h=Array(e.length);for(var a=0;a<e.length;a++)h[a]=e[a]}else  [...]
+{},e)ta.call(e,a)&&(h[a]=e[a]);else h=e})},$digest:function(){var b,f,g,k,n,r,t=a,m,u=[],D,v;p("$digest");l.$$checkUrlChange();this===q&&null!==e&&(l.defer.cancel(e),s());d=null;do{r=!1;for(m=this;z.length;){try{v=z.shift(),v.scope.$eval(v.expression,v.locals)}catch(w){h(w)}d=null}a:do{if(k=m.$$watchers)for(n=k.length;n--;)try{if(b=k[n])if((f=b.get(m))!==(g=b.last)&&!(b.eq?ka(f,g):"number"===typeof f&&"number"===typeof g&&isNaN(f)&&isNaN(g)))r=!0,d=b,b.last=b.eq?ha(f,null):f,b.fn(f,g===E [...]
+t&&(D=4-t,u[D]||(u[D]=[]),u[D].push({msg:x(b.exp)?"fn: "+(b.exp.name||b.exp.toString()):b.exp,newVal:f,oldVal:g}));else if(b===d){r=!1;break a}}catch(y){h(y)}if(!(k=m.$$watchersCount&&m.$$childHead||m!==this&&m.$$nextSibling))for(;m!==this&&!(k=m.$$nextSibling);)m=m.$parent}while(m=k);if((r||z.length)&&!t--)throw q.$$phase=null,c("infdig",a,u);}while(r||z.length);for(q.$$phase=null;N.length;)try{N.shift()()}catch(A){h(A)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this [...]
+this.$$destroyed=!0;this===q&&l.$$applicationDestroyed();r(this,-this.$$watchersCount);for(var b in this.$$listenerCount)t(this,this.$$listenerCount[b],b);a&&a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync [...]
+this.$watch=this.$watchGroup=function(){return y};this.$$listeners={};this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=this.$$watchers=null}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a,b){q.$$phase||z.length||l.defer(function(){z.length&&q.$digest()});z.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){N.push(a)},$apply:function(a){try{p("$apply");try{return this.$eval(a)}finally{q.$$phase=null}}catch(b){ [...]
+c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&w.push(b);u()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,t(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,f=!1,g={name:a,targetScope:e,stopPropagation:function(){f=!0},preventDefault:function(){g.defaultPrevente [...]
+k=db([g],arguments,1),l,n;do{d=e.$$listeners[a]||c;g.currentScope=e;l=0;for(n=d.length;l<n;l++)if(d[l])try{d[l].apply(null,k)}catch(p){h(p)}else d.splice(l,1),l--,n--;if(f)return g.currentScope=null,g;e=e.$parent}while(e);g.currentScope=null;return g},$broadcast:function(a,b){var c=this,d=this,e={name:a,targetScope:this,preventDefault:function(){e.defaultPrevented=!0},defaultPrevented:!1};if(!this.$$listenerCount[a])return e;for(var f=db([e],arguments,1),g,k;c=d;){e.currentScope=c;d=c.$$ [...]
+[];g=0;for(k=d.length;g<k;g++)if(d[g])try{d[g].apply(null,f)}catch(l){h(l)}else d.splice(g,1),g--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var q=new n,z=q.$$asyncQueue=[],N=q.$$postDigestQueue=[],w=q.$$applyAsyncQueue=[];return q}]}function ge(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(a) [...]
+(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return A(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;f=Aa(c).href;return""===f||f.match(e)?c:"unsafe:"+f}}}function Zf(b){if("self"===b)return b;if(G(b)){if(-1<b.indexOf("***"))throw Ca("iwcard",b);b=vd(b).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(Oa(b))return new RegExp("^"+b.source+"$");throw Ca("imatcher");}function wd(b){var a=[];A(b)&&m(b,function(b){a.pus [...]
+return a}function pf(){this.SCE_CONTEXTS=oa;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=wd(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=wd(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?fd(b):!!a.exec(b.href)}function e(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=function(){return this.$$unwrapTrustedValue()};b.prototype.toS [...]
+return b}var f=function(a){throw Ca("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var h=e(),g={};g[oa.HTML]=e(h);g[oa.CSS]=e(h);g[oa.URL]=e(h);g[oa.JS]=e(h);g[oa.RESOURCE_URL]=e(g[oa.URL]);return{trustAs:function(a,b){var c=g.hasOwnProperty(a)?g[a]:null;if(!c)throw Ca("icontext",a,b);if(null===b||v(b)||""===b)return b;if("string"!==typeof b)throw Ca("itype",a);return new c(b)},getTrusted:function(c,e){if(null===e||v(e)||""===e)return e;var h=g.hasOwnProperty(c)?g[c]:null;if(h&&e [...]
+h)return e.$$unwrapTrustedValue();if(c===oa.RESOURCE_URL){var h=Aa(e.toString()),p,r,t=!1;p=0;for(r=b.length;p<r;p++)if(d(b[p],h)){t=!0;break}if(t)for(p=0,r=a.length;p<r;p++)if(d(a[p],h)){t=!1;break}if(t)return e;throw Ca("insecurl",e.toString());}if(c===oa.HTML)return f(e);throw Ca("unsafe");},valueOf:function(a){return a instanceof h?a.$$unwrapTrustedValue():a}}}]}function of(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sceDelegate",func [...]
+8>Wa)throw Ca("iequirks");var d=ja(oa);d.isEnabled=function(){return b};d.trustAs=c.trustAs;d.getTrusted=c.getTrusted;d.valueOf=c.valueOf;b||(d.trustAs=d.getTrusted=function(a,b){return b},d.valueOf=$a);d.parseAs=function(b,c){var e=a(c);return e.literal&&e.constant?e:a(c,function(a){return d.getTrusted(b,a)})};var e=d.parseAs,f=d.getTrusted,h=d.trustAs;m(oa,function(a,b){var c=F(b);d[gb("parse_as_"+c)]=function(b){return e(a,b)};d[gb("get_trusted_"+c)]=function(b){return f(a,b)};d[gb("t [...]
+c)]=function(b){return h(a,b)}});return d}]}function qf(){this.$get=["$window","$document",function(b,a){var c={},d=Y((/android (\d+)/.exec(F((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},h,g=/^(Moz|webkit|ms)(?=[A-Z])/,l=f.body&&f.body.style,k=!1,n=!1;if(l){for(var p in l)if(k=g.exec(p)){h=k[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in l&&"webkit");k=!!("transition"in l||h+"Transition"in l);n=!!("animation"in [...]
+l);!d||k&&n||(k=G(l.webkitTransition),n=G(l.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hasEvent:function(a){if("input"===a&&11>=Wa)return!1;if(v(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:Fa(),vendorPrefix:h,transitions:k,animations:n,android:d}}]}function sf(){this.$get=["$templateCache","$http","$q","$sce",function(b,a,c,d){function e(f,h){e.totalPendingRequests++;G(f)&&b.get(f)||(f=d.getTrustedResourceUrl(f));var g=a.default [...]
+J(g)?g=g.filter(function(a){return a!==Zb}):g===Zb&&(g=null);return a.get(f,{cache:b,transformResponse:g})["finally"](function(){e.totalPendingRequests--}).then(function(a){b.put(f,a.data);return a.data},function(a){if(!h)throw ga("tpload",f,a.status,a.statusText);return c.reject(a)})}e.totalPendingRequests=0;return e}]}function tf(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var h=[];m(a,fun [...]
+da.element(a).data("$binding");d&&m(d,function(d){c?(new RegExp("(^|\\s)"+vd(b)+"(\\s|\\||$)")).test(d)&&h.push(a):-1!=d.indexOf(b)&&h.push(a)})});return h},findModels:function(a,b,c){for(var h=["ng-","data-ng-","ng\\:"],g=0;g<h.length;++g){var l=a.querySelectorAll("["+h[g]+"model"+(c?"=":"*=")+'"'+b+'"]');if(l.length)return l}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}} [...]
+["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b,a,c,d,e){function f(f,l,k){x(f)||(k=l,l=f,f=y);var n=ua.call(arguments,3),p=A(k)&&!k,r=(p?d:c).defer(),t=r.promise,m;m=a.defer(function(){try{r.resolve(f.apply(null,n))}catch(a){r.reject(a),e(a)}finally{delete h[t.$$timeoutId]}p||b.$apply()},l);t.$$timeoutId=m;h[m]=r;return t}var h={};f.cancel=function(b){return b&&b.$$timeoutId in h?(h[b.$$timeoutId].reject("canceled"),delete h[b.$$timeoutId],a.defer.cancel(b.$$timeoutId [...]
+function Aa(b){Wa&&($.setAttribute("href",b),b=$.href);$.setAttribute("href",b);return{href:$.href,protocol:$.protocol?$.protocol.replace(/:$/,""):"",host:$.host,search:$.search?$.search.replace(/^\?/,""):"",hash:$.hash?$.hash.replace(/^#/,""):"",hostname:$.hostname,port:$.port,pathname:"/"===$.pathname.charAt(0)?$.pathname:"/"+$.pathname}}function fd(b){b=G(b)?Aa(b):b;return b.protocol===xd.protocol&&b.host===xd.host}function vf(){this.$get=qa(Q)}function yd(b){function a(a){try{return  [...]
+var c=b[0]||{},d={},e="";return function(){var b,h,g,l,k;b=c.cookie||"";if(b!==e)for(e=b,b=e.split("; "),d={},g=0;g<b.length;g++)h=b[g],l=h.indexOf("="),0<l&&(k=a(h.substring(0,l)),v(d[k])&&(d[k]=a(h.substring(l+1))));return d}}function zf(){this.$get=yd}function Kc(b){function a(c,d){if(C(c)){var e={};m(c,function(b,c){e[c]=a(c,b)});return e}return b.factory(c+"Filter",d)}this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+"Filter")}}];a("currency",zd);a [...]
+a("filter",$f);a("json",ag);a("limitTo",bg);a("lowercase",cg);a("number",Bd);a("orderBy",Cd);a("uppercase",dg)}function $f(){return function(b,a,c){if(!Da(b)){if(null==b)return b;throw I("filter")("notarray",b);}var d;switch(gc(a)){case "function":break;case "boolean":case "null":case "number":case "string":d=!0;case "object":a=eg(a,c,d);break;default:return b}return Array.prototype.filter.call(b,a)}}function eg(b,a,c){var d=C(b)&&"$"in b;!0===a?a=ka:x(a)||(a=function(a,b){if(v(a))return [...]
+a||null===b)return a===b;if(C(b)||C(a)&&!qc(a))return!1;a=F(""+a);b=F(""+b);return-1!==a.indexOf(b)});return function(e){return d&&!C(e)?Ma(e,b.$,a,!1):Ma(e,b,a,c)}}function Ma(b,a,c,d,e){var f=gc(b),h=gc(a);if("string"===h&&"!"===a.charAt(0))return!Ma(b,a.substring(1),c,d);if(J(b))return b.some(function(b){return Ma(b,a,c,d)});switch(f){case "object":var g;if(d){for(g in b)if("$"!==g.charAt(0)&&Ma(b[g],a,c,!0))return!0;return e?!1:Ma(b,a,c,!1)}if("object"===h){for(g in a)if(e=a[g],!x(e) [...]
+(f="$"===g,!Ma(f?b:b[g],e,c,f,f)))return!1;return!0}return c(b,a);case "function":return!1;default:return c(b,a)}}function gc(b){return null===b?"null":typeof b}function zd(b){var a=b.NUMBER_FORMATS;return function(b,d,e){v(d)&&(d=a.CURRENCY_SYM);v(e)&&(e=a.PATTERNS[1].maxFrac);return null==b?b:Dd(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,e).replace(/\u00A4/g,d)}}function Bd(b){var a=b.NUMBER_FORMATS;return function(b,d){return null==b?b:Dd(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}f [...]
+a,c,d,e){if(C(b))return"";var f=0>b;b=Math.abs(b);var h=Infinity===b;if(!h&&!isFinite(b))return"";var g=b+"",l="",k=!1,n=[];h&&(l="\u221e");if(!h&&-1!==g.indexOf("e")){var p=g.match(/([\d\.]+)e(-?)(\d+)/);p&&"-"==p[2]&&p[3]>e+1?b=0:(l=g,k=!0)}if(h||k)0<e&&1>b&&(l=b.toFixed(e),b=parseFloat(l),l=l.replace(hc,d));else{h=(g.split(hc)[1]||"").length;v(e)&&(e=Math.min(Math.max(a.minFrac,h),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);var h=(""+b).split(hc),g=h[0],h=h[1] [...]
+r=a.lgSize,t=a.gSize;if(g.length>=r+t)for(p=g.length-r,k=0;k<p;k++)0===(p-k)%t&&0!==k&&(l+=c),l+=g.charAt(k);for(k=p;k<g.length;k++)0===(g.length-k)%r&&0!==k&&(l+=c),l+=g.charAt(k);for(;h.length<e;)h+="0";e&&"0"!==e&&(l+=d+h.substr(0,e))}0===b&&(f=!1);n.push(f?a.negPre:a.posPre,l,f?a.negSuf:a.posSuf);return n.join("")}function Gb(b,a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function aa(b,a,c,d){c=c||0;return function(e){e=e["get" [...]
+c||e>-c)e+=c;0===e&&-12==c&&(e=12);return Gb(e,a,d)}}function Hb(b,a){return function(c,d){var e=c["get"+b](),f=sb(a?"SHORT"+b:b);return d[f][e]}}function Ed(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function Fd(b){return function(a){var c=Ed(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return Gb(a,b)}}function ic(b,a){return 0>=b.getFullYear()?a.ERAS[0]:a.ERAS[1]}function Ad(b){function a(a [...]
+a.match(c)){a=new Date(0);var f=0,h=0,g=b[8]?a.setUTCFullYear:a.setFullYear,l=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Y(b[9]+b[10]),h=Y(b[9]+b[11]));g.call(a,Y(b[1]),Y(b[2])-1,Y(b[3]));f=Y(b[4]||0)-f;h=Y(b[5]||0)-h;g=Y(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));l.call(a,f,h,g,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,e,f){var h="",g=[],l,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]|| [...]
+fg.test(c)?Y(c):a(c));V(c)&&(c=new Date(c));if(!ea(c)||!isFinite(c.getTime()))return c;for(;e;)(k=gg.exec(e))?(g=db(g,k,1),e=g.pop()):(g.push(e),e=null);var n=c.getTimezoneOffset();f&&(n=wc(f,c.getTimezoneOffset()),c=Ob(c,f,!0));m(g,function(a){l=hg[a];h+=l?l(c,b.DATETIME_FORMATS,n):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return h}}function ag(){return function(b,a){v(a)&&(a=2);return eb(b,a)}}function bg(){return function(b,a,c){a=Infinity===Math.abs(Number(a))?Number(a):Y(a);if(i [...]
+V(b)&&(b=b.toString());if(!J(b)&&!G(b))return b;c=!c||isNaN(c)?0:Y(c);c=0>c&&c>=-b.length?b.length+c:c;return 0<=a?b.slice(c,c+a):0===c?b.slice(a,b.length):b.slice(Math.max(0,c+a),c)}}function Cd(b){function a(a,c){c=c?-1:1;return a.map(function(a){var d=1,g=$a;if(x(a))g=a;else if(G(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))d="-"==a.charAt(0)?-1:1,a=a.substring(1);if(""!==a&&(g=b(a),g.constant))var l=g(),g=function(a){return a[l]}}return{get:g,descending:d*c}})}function c(a){switch(typeo [...]
+default:return!1}}return function(b,e,f){if(!Da(b))return b;J(e)||(e=[e]);0===e.length&&(e=["+"]);var h=a(e,f);h.push({get:function(){return{}},descending:f?-1:1});b=Array.prototype.map.call(b,function(a,b){return{value:a,predicateValues:h.map(function(d){var e=d.get(a);d=typeof e;if(null===e)d="string",e="null";else if("string"===d)e=e.toLowerCase();else if("object"===d)a:{if("function"===typeof e.valueOf&&(e=e.valueOf(),c(e)))break a;if(qc(e)&&(e=e.toString(),c(e)))break a;e=b}return{v [...]
+b.sort(function(a,b){for(var c=0,d=0,e=h.length;d<e;++d){var c=a.predicateValues[d],f=b.predicateValues[d],t=0;c.type===f.type?c.value!==f.value&&(t=c.value<f.value?-1:1):t=c.type<f.type?-1:1;if(c=t*h[d].descending)break}return c});return b=b.map(function(a){return a.value})}}function Na(b){x(b)&&(b={link:b});b.restrict=b.restrict||"AC";return qa(b)}function Gd(b,a,c,d,e){var f=this,h=[];f.$error={};f.$$success={};f.$pending=w;f.$name=e(a.name||a.ngForm||"")(c);f.$dirty=!1;f.$pristine=!0 [...]
+!0;f.$invalid=!1;f.$submitted=!1;f.$$parentForm=Ib;f.$rollbackViewValue=function(){m(h,function(a){a.$rollbackViewValue()})};f.$commitViewValue=function(){m(h,function(a){a.$commitViewValue()})};f.$addControl=function(a){Ta(a.$name,"input");h.push(a);a.$name&&(f[a.$name]=a);a.$$parentForm=f};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];m(f.$pending,function(b,c){f.$setValidity [...]
+m(f.$error,function(b,c){f.$setValidity(c,null,a)});m(f.$$success,function(b,c){f.$setValidity(c,null,a)});cb(h,a);a.$$parentForm=Ib};Hd({ctrl:this,$element:b,set:function(a,b,c){var d=a[b];d?-1===d.indexOf(c)&&d.push(c):a[b]=[c]},unset:function(a,b,c){var d=a[b];d&&(cb(d,c),0===d.length&&delete a[b])},$animate:d});f.$setDirty=function(){d.removeClass(b,Ya);d.addClass(b,Jb);f.$dirty=!0;f.$pristine=!1;f.$$parentForm.$setDirty()};f.$setPristine=function(){d.setClass(b,Ya,Jb+" ng-submitted" [...]
+!1;f.$pristine=!0;f.$submitted=!1;m(h,function(a){a.$setPristine()})};f.$setUntouched=function(){m(h,function(a){a.$setUntouched()})};f.$setSubmitted=function(){d.addClass(b,"ng-submitted");f.$submitted=!0;f.$$parentForm.$setSubmitted()}}function jc(b){b.$formatters.push(function(a){return b.$isEmpty(a)?a:a.toString()})}function jb(b,a,c,d,e,f){var h=F(a[0].type);if(!e.android){var g=!1;a.on("compositionstart",function(a){g=!0});a.on("compositionend",function(){g=!1;l()})}var l=function( [...]
+k=null);if(!g){var e=a.val();b=b&&b.type;"password"===h||c.ngTrim&&"false"===c.ngTrim||(e=T(e));(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,b)}};if(e.hasEvent("input"))a.on("input",l);else{var k,n=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||n(a,this,this.value)});if(e.hasEvent("paste"))a.on("paste cut",n)}a.on("change",l);d.$render=function(){var b=d.$isEmp [...]
+"":d.$viewValue;a.val()!==b&&a.val(b)}}function Kb(b,a){return function(c,d){var e,f;if(ea(c))return c;if(G(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-1)&&(c=c.substring(1,c.length-1));if(ig.test(c))return new Date(c);b.lastIndex=0;if(e=b.exec(c))return e.shift(),f=d?{yyyy:d.getFullYear(),MM:d.getMonth()+1,dd:d.getDate(),HH:d.getHours(),mm:d.getMinutes(),ss:d.getSeconds(),sss:d.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},m(e,function(b,c){c<a.length&&(f[a[c]]=+b)} [...]
+f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function kb(b,a,c,d){return function(e,f,h,g,l,k,n){function p(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function r(a){return A(a)&&!ea(a)?c(a)||w:a}Id(e,f,h,g);jb(e,f,h,g,l,k);var t=g&&g.$options&&g.$options.timezone,m;g.$$parserName=b;g.$parsers.push(function(b){return g.$isEmpty(b)?null:a.test(b)?(b=c(b,m),t&&(b=Ob(b,t)),b):w});g.$formatters.push(function(a){if(a&&!ea(a))throw lb("datefmt",a);if(p(a))return(m=a)&&t&&(m [...]
+n("date")(a,d,t);m=null;return""});if(A(h.min)||h.ngMin){var s;g.$validators.min=function(a){return!p(a)||v(s)||c(a)>=s};h.$observe("min",function(a){s=r(a);g.$validate()})}if(A(h.max)||h.ngMax){var u;g.$validators.max=function(a){return!p(a)||v(u)||c(a)<=u};h.$observe("max",function(a){u=r(a);g.$validate()})}}}function Id(b,a,c,d){(d.$$hasNativeValidators=C(a[0].validity))&&d.$parsers.push(function(b){var c=a.prop("validity")||{};return c.badInput&&!c.typeMismatch?w:b})}function Jd(b,a, [...]
+b(d);if(!b.constant)throw lb("constexpr",c,d);return b(a)}return e}function kc(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;d<a.length;d++){for(var e=a[d],n=0;n<b.length;n++)if(e==b[n])continue a;c.push(e)}return c}function e(a){var b=[];return J(a)?(m(a,function(a){b=b.concat(e(a))}),b):G(a)?a.split(" "):C(a)?(m(a,function(a,c){a&&(b=b.concat(c.split(" ")))}),b):a}return{restrict:"AC",link:function(f,h,g){function l(a,b){var c=h.data("$classCounts [...]
+d=[];m(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});h.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||f.$index%2===a){var k=e(b||[]);if(!n){var m=l(k,1);g.$addClass(m)}else if(!ka(b,n)){var s=e(n),m=d(k,s),k=d(s,k),m=l(m,1),k=l(k,-1);m&&m.length&&c.addClass(h,m);k&&k.length&&c.removeClass(h,k)}}n=ja(b)}var n;f.$watch(g[b],k,!0);g.$observe("class",function(a){k(f.$eval(g[b]))});"ngClass"!==b&&f.$watch("$index",function(c,d){var h=c&1;if(h!==(d& [...]
+e(f.$eval(g[b]));h===a?(h=l(k,1),g.$addClass(h)):(h=l(k,-1),g.$removeClass(h))}})}}}]}function Hd(b){function a(a,b){b&&!f[a]?(l.addClass(e,a),f[a]=!0):!b&&f[a]&&(l.removeClass(e,a),f[a]=!1)}function c(b,c){b=b?"-"+Ac(b,"-"):"";a(mb+b,!0===c);a(Kd+b,!1===c)}var d=b.ctrl,e=b.$element,f={},h=b.set,g=b.unset,l=b.$animate;f[Kd]=!(f[mb]=e.hasClass(mb));d.$setValidity=function(b,e,f){v(e)?(d.$pending||(d.$pending={}),h(d.$pending,b,f)):(d.$pending&&g(d.$pending,b,f),Ld(d.$pending)&&(d.$pending [...]
+e?(g(d.$error,b,f),h(d.$$success,b,f)):(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),g(d.$$success,b,f));d.$pending?(a(Md,!0),d.$valid=d.$invalid=w,c("",null)):(a(Md,!1),d.$valid=Ld(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?w:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);d.$$parentForm.$setValidity(b,e,d)}}function Ld(b){if(b)for(var a in b)if(b.hasOwnProperty(a))return!1;return!0}var jg=/^\/(.+)\/([a-z]*)$/,F=function(b){return G(b)?b.toLowerCase( [...]
+sb=function(b){return G(b)?b.toUpperCase():b},Wa,B,ra,ua=[].slice,Nf=[].splice,kg=[].push,va=Object.prototype.toString,rc=Object.getPrototypeOf,Ea=I("ng"),da=Q.angular||(Q.angular={}),Rb,nb=0;Wa=X.documentMode;y.$inject=[];$a.$inject=[];var J=Array.isArray,tc=/^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/,T=function(b){return G(b)?b.trim():b},vd=function(b){return b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")}, [...]
+X.querySelector("[ng-csp]")||X.querySelector("[data-ng-csp]");if(b){var a=b.getAttribute("ng-csp")||b.getAttribute("data-ng-csp");Fa.rules={noUnsafeEval:!a||-1!==a.indexOf("no-unsafe-eval"),noInlineStyle:!a||-1!==a.indexOf("no-inline-style")}}else{b=Fa;try{new Function(""),a=!1}catch(c){a=!0}b.rules={noUnsafeEval:a,noInlineStyle:!1}}}return Fa.rules},pb=function(){if(A(pb.name_))return pb.name_;var b,a,c=Qa.length,d,e;for(a=0;a<c;++a)if(d=Qa[a],b=X.querySelector("["+d.replace(":","\\:")+ [...]
+b.getAttribute(d+"jq");break}return pb.name_=e},Qa=["ng-","data-ng-","ng:","x-ng-"],be=/[A-Z]/g,Bc=!1,Qb,pa=1,Pa=3,fe={full:"1.4.7",major:1,minor:4,dot:7,codeName:"dark-luminescence"};R.expando="ng339";var hb=R.cache={},Ff=1;R._data=function(b){return this.cache[b[this.expando]]||{}};var Af=/([\:\-\_]+(.))/g,Bf=/^moz([A-Z])/,lg={mouseleave:"mouseout",mouseenter:"mouseover"},Tb=I("jqLite"),Ef=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,Sb=/<|&#?\w+;/,Cf=/<([\w:-]+)/,Df=/<(?!area|br|col|embed|hr|img|i [...]
+ma={option:[1,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ma.optgroup=ma.option;ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead;ma.th=ma.td;var Ra=R.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===X.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),R(Q).on(" [...]
+toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?B(this[b]):B(this[this.length+b])},length:0,push:kg,sort:[].sort,splice:[].splice},Bb={};m("multiple selected checked disabled readOnly required open".split(" "),function(b){Bb[F(b)]=b});var Sc={};m("input select option textarea button form details".split(" "),function(b){Sc[b]=!0});var $c={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern: [...]
+m({data:Vb,removeData:vb,hasData:function(b){for(var a in hb[b.ng339])return!0;return!1}},function(b,a){R[a]=b});m({data:Vb,inheritedData:Ab,scope:function(b){return B.data(b,"$scope")||Ab(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return B.data(b,"$isolateScope")||B.data(b,"$isolateScopeNoTemplate")},controller:Pc,injector:function(b){return Ab(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:xb,css:function(b,a,c){a=gb(a);if(A(c))b.styl [...]
+attr:function(b,a,c){var d=b.nodeType;if(d!==Pa&&2!==d&&8!==d)if(d=F(a),Bb[d])if(A(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||y).specified?d:w;else if(A(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?w:b},prop:function(b,a,c){if(A(c))b[a]=c;else return b[a]},text:function(){function b(a,b){if(v(b)){var d=a.nodeType;return d===pa||d===Pa?a.textContent:""}a.textContent=b}b.$dv="";re [...]
+val:function(b,a){if(v(a)){if(b.multiple&&"select"===wa(b)){var c=[];m(b.options,function(a){a.selected&&c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(v(a))return b.innerHTML;ub(b,!0);b.innerHTML=a},empty:Qc},function(b,a){R.prototype[a]=function(a,d){var e,f,h=this.length;if(b!==Qc&&v(2==b.length&&b!==xb&&b!==Pc?a:d)){if(C(a)){for(e=0;e<h;e++)if(b===Vb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;h=v(e)?Math.mi [...]
+for(f=0;f<h;f++){var g=b(this[f],a,d);e=e?e+g:g}return e}for(e=0;e<h;e++)b(this[e],a,d);return this}});m({removeData:vb,on:function a(c,d,e,f){if(A(f))throw Tb("onargs");if(Lc(c)){var h=wb(c,!0);f=h.events;var g=h.handle;g||(g=h.handle=Hf(c,f));for(var h=0<=d.indexOf(" ")?d.split(" "):[d],l=h.length;l--;){d=h[l];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===d?a(c,lg[d],function(a){var c=a.relatedTarget;c&&(c===this||this.contains(c))||g(a,d)}):"$destroy"!==d&&c.addEventListener [...]
+k=f[d]);k.push(e)}}},off:Oc,one:function(a,c,d){a=B(a);a.on(c,function f(){a.off(c,d);a.off(c,f)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;ub(a);m(new R(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];m(a.childNodes,function(a){a.nodeType===pa&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(d===pa||11===d){c=new R(c);for(var d=0,e=c. [...]
+e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(a.nodeType===pa){var d=a.firstChild;m(new R(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=B(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:Wb,detach:function(a){Wb(a,!0)},after:function(a,c){var d=a,e=a.parentNode;c=new R(c);for(var f=0,h=c.length;f<h;f++){var g=c[f];e.insertBefore(g,d.nextSibling);d=g}},addClass:zb,removeClass:yb,toggleClass:function(a,c,d){c&&m(c.split(" "),functi [...]
+d;v(f)&&(f=!xb(a,c));(f?zb:yb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){return a.nextElementSibling},find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Ub,triggerHandler:function(a,c,d){var e,f,h=c.type||c,g=wb(a);if(g=(g=g&&g.events)&&g[h])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopImmediatePropagation:function(){this.immedi [...]
+!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:y,type:h,target:a},c.type&&(e=P(e,c)),c=ja(g),f=d?[e].concat(d):[e],m(c,function(c){e.isImmediatePropagationStopped()||c.apply(a,f)})}},function(a,c){R.prototype[c]=function(c,e,f){for(var h,g=0,l=this.length;g<l;g++)v(h)?(h=a(this[g],c,e,f),A(h)&&(h=B(h))):Nc(h,a(this[g],c,e,f));return A(h)?h:this};R.prototype.bind=R.prototype.on;R.prototype.unbind=R.prototype.off});Ua.prototype={pu [...]
+c){this[Ga(a,this.nextUid)]=c},get:function(a){return this[Ga(a,this.nextUid)]},remove:function(a){var c=this[a=Ga(a,this.nextUid)];delete this[a];return c}};var yf=[function(){this.$get=[function(){return Ua}]}],Uc=/^[^\(]*\(\s*([^\)]*)\)/m,mg=/,/,ng=/^\s*(_?)(\S+?)\1\s*$/,Tc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ha=I("$injector");fb.$$annotate=function(a,c,d){var e;if("function"===typeof a){if(!(e=a.$inject)){e=[];if(a.length){if(c)throw G(d)&&d||(d=a.name||If(a)),Ha("strictdi",d);c=a.toS [...]
+"");c=c.match(Uc);m(c[1].split(mg),function(a){a.replace(ng,function(a,c,d){e.push(d)})})}a.$inject=e}}else J(a)?(c=a.length-1,Sa(a[c],"fn"),e=a.slice(0,c)):Sa(a,"fn",!0);return e};var Nd=I("$animate"),Ue=function(){this.$get=["$q","$$rAF",function(a,c){function d(){}d.all=y;d.chain=y;d.prototype={end:y,cancel:y,resume:y,pause:y,complete:y,then:function(d,f){return a(function(a){c(function(){a()})}).then(d,f)}};return d}]},Te=function(){var a=new Ua,c=[];this.$get=["$$AnimateRunner","$ro [...]
+function(d,e){function f(a,c,d){var e=!1;c&&(c=G(c)?c.split(" "):J(c)?c:[],m(c,function(c){c&&(e=!0,a[c]=d)}));return e}function h(){m(c,function(c){var d=a.get(c);if(d){var e=Jf(c.attr("class")),f="",h="";m(d,function(a,c){a!==!!e[c]&&(a?f+=(f.length?" ":"")+c:h+=(h.length?" ":"")+c)});m(c,function(a){f&&zb(a,f);h&&yb(a,h)});a.remove(c)}});c.length=0}return{enabled:y,on:y,off:y,pin:y,push:function(g,l,k,n){n&&n();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeCla [...]
+n=k.removeClass,k=a.get(g)||{},l=f(k,l,!0),n=f(k,n,!1),l||n)a.put(g,k),c.push(g),1===c.length&&e.$$postDigest(h);return new d}}}]},Re=["$provide",function(a){var c=this;this.$$registeredAnimations=Object.create(null);this.register=function(d,e){if(d&&"."!==d.charAt(0))throw Nd("notcsel",d);var f=d+"-animation";c.$$registeredAnimations[d.substr(1)]=f;a.factory(f,e)};this.classNameFilter=function(a){if(1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?a:null)&&/(\s+|\/)ng-a [...]
+"ng-animate");return this.$$classNameFilter};this.$get=["$$animateQueue",function(a){function c(a,d,e){if(e){var l;a:{for(l=0;l<e.length;l++){var k=e[l];if(1===k.nodeType){l=k;break a}}l=void 0}!l||l.parentNode||l.previousElementSibling||(e=null)}e?e.after(a):d.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&a.end()},enter:function(f,h,g,l){h=h&&B(h);g=g&&B(g);h=h||g.parent();c(f,h,g);return a.push(f,"enter",Ia(l))},move:function(f,h,g,l){h=h&&B( [...]
+h=h||g.parent();c(f,h,g);return a.push(f,"move",Ia(l))},leave:function(c,e){return a.push(c,"leave",Ia(e),function(){c.remove()})},addClass:function(c,e,g){g=Ia(g);g.addClass=ib(g.addclass,e);return a.push(c,"addClass",g)},removeClass:function(c,e,g){g=Ia(g);g.removeClass=ib(g.removeClass,e);return a.push(c,"removeClass",g)},setClass:function(c,e,g,l){l=Ia(l);l.addClass=ib(l.addClass,e);l.removeClass=ib(l.removeClass,g);return a.push(c,"setClass",l)},animate:function(c,e,g,l,k){k=Ia(k);k [...]
+P(k.from,e):e;k.to=k.to?P(k.to,g):g;k.tempClasses=ib(k.tempClasses,l||"ng-inline-animate");return a.push(c,"animate",k)}}}]}],Se=function(){this.$get=["$$rAF","$q",function(a,c){var d=function(){};d.prototype={done:function(a){this.defer&&this.defer[!0===a?"reject":"resolve"]()},end:function(){this.done()},cancel:function(){this.done(!0)},getPromise:function(){this.defer||(this.defer=c.defer());return this.defer.promise},then:function(a,c){return this.getPromise().then(a,c)},"catch":func [...]
+"finally":function(a){return this.getPromise()["finally"](a)}};return function(c,f){function h(){a(function(){f.addClass&&(c.addClass(f.addClass),f.addClass=null);f.removeClass&&(c.removeClass(f.removeClass),f.removeClass=null);f.to&&(c.css(f.to),f.to=null);g||l.done();g=!0});return l}f.cleanupStyles&&(f.from=f.to=null);f.from&&(c.css(f.from),f.from=null);var g,l=new d;return{start:h,end:h}}}]},ga=I("$compile");Dc.$inject=["$provide","$$sanitizeUriProvider"];var Wc=/^((?:x|data)[\:\-_])/ [...]
+Vc=/^(\S+)(\s+as\s+(\w+))?$/,$e=function(){this.$get=["$document",function(a){return function(c){c?!c.nodeType&&c instanceof B&&(c=c[0]):c=a[0].body;return c.offsetWidth+1}}]},ad="application/json",$b={"Content-Type":ad+";charset=utf-8"},Qf=/^\[|^\{(?!\{)/,Rf={"[":/]$/,"{":/}$/},Pf=/^\)\]\}',?\n/,og=I("$http"),ed=function(a){return function(){throw og("legacy",a);}},La=da.$interpolateMinErr=I("$interpolate");La.throwNoconcat=function(a){throw La("noconcat",a);};La.interr=function(a,c){re [...]
+a,c.toString())};var pg=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,Tf={http:80,https:443,ftp:21},Db=I("$location"),qg={$$html5:!1,$$replace:!1,absUrl:Eb("$$absUrl"),url:function(a){if(v(a))return this.$$url;var c=pg.exec(a);(c[1]||""===a)&&this.path(decodeURIComponent(c[1]));(c[2]||c[1]||""===a)&&this.search(c[3]||"");this.hash(c[5]||"");return this},protocol:Eb("$$protocol"),host:Eb("$$host"),port:Eb("$$port"),path:jd("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a [...]
+c){switch(arguments.length){case 0:return this.$$search;case 1:if(G(a)||V(a))a=a.toString(),this.$$search=yc(a);else if(C(a))a=ha(a,{}),m(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw Db("isrcharg");break;default:v(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:jd("$$hash",function(a){return null!==a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};m([id,cc,bc],function(a){a.prototype=Object.create(qg);a. [...]
+function(c){if(!arguments.length)return this.$$state;if(a!==bc||!this.$$html5)throw Db("nostate");this.$$state=v(c)?null:c;return this}});var Z=I("$parse"),Uf=Function.prototype.call,Vf=Function.prototype.apply,Wf=Function.prototype.bind,Lb=fa();m("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Lb[a]=!0});var rg={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},ec=function(a){this.options=a};ec.prototype={constructor:ec,lex:function(a){this.text=a;this.index=0;f [...]
+[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;else{var c=a+this.peek(),d=c+this.peek(2),e=Lb[c],f=Lb[d];Lb[a]||e||f?(a=f?d:e?c:a,this.tokens.push({index:this.index,text:a,operator:!0} [...]
+a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,c){return-1!==c.indexOf(a)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:functi [...]
+a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=A(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw Z("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=F(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=d;else if(this.isExpOperator(d)&&e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOper [...]
+e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}this.tokens.push({index:c,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=this.index;this.index<this.text.length;){var c=this.text.charAt(this.index);if(!this.isIdent(c)&&!this.isNumber(c))break;this.index++}this.tokens.push({index:a,text:this.text.slice(a,this.index),identifier:!0})},readString:function(a){var c=this.index;this.index++;for(var d="",e=a,f=!1;thi [...]
+this.text.charAt(this.index),e=e+h;if(f)"u"===h?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=4,d+=String.fromCharCode(parseInt(f,16))):d+=rg[h]||h,f=!1;else if("\\"===h)f=!0;else{if(h===a){this.index++;this.tokens.push({index:c,text:e,constant:!0,value:d});return}d+=h}this.index++}this.throwError("Unterminated quote",c)}};var s=function(a,c){this.lexer=a;this.options=c};s.Program="Program";s.Exp [...]
+"ExpressionStatement";s.AssignmentExpression="AssignmentExpression";s.ConditionalExpression="ConditionalExpression";s.LogicalExpression="LogicalExpression";s.BinaryExpression="BinaryExpression";s.UnaryExpression="UnaryExpression";s.CallExpression="CallExpression";s.MemberExpression="MemberExpression";s.Identifier="Identifier";s.Literal="Literal";s.ArrayExpression="ArrayExpression";s.Property="Property";s.ObjectExpression="ObjectExpression";s.ThisExpression="ThisExpression";s.NGValueParam [...]
+s.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:s.Program,body:a}},expressionStatement:function(){return{type:s.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expres [...]
+this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();this.expect("=")&&(a={type:s.AssignmentExpression,left:a,right:this.assignment(),operator:"="});return a},ternary:function(){var a=this.logicalOR(),c,d;return this.expect("?")&&(c=this.expression(),this.consume(":"))?(d=this.expression(),{type:s.ConditionalExpression,test:a,alternate:c,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={ [...]
+operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:s.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),c;c=this.expect("==","!=","===","!==");)a={type:s.BinaryExpression,operator:c.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),c;c=this.expect("<",">","<=",">=");)a={type:s.BinaryExpression,ope [...]
+left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a={type:s.BinaryExpression,operator:c.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:c.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unar [...]
+primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.constants.hasOwnProperty(this.peek().text)?a=ha(this.constants[this.consume().text]):this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var c;c=this.expect("(","[",".");)"("===c.text?(a={type:s.CallExpression,callee:a,arguments:this.pars [...]
+this.consume(")")):"["===c.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===c.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var c={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return c},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.pus [...]
+while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],c;if("}"!= [...]
+c={type:s.Property,kind:"init"};this.peek().constant?c.key=this.constant():this.peek().identifier?c.key=this.identifier():this.throwError("invalid key",this.peek());this.consume(":");c.value=this.expression();a.push(c)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},throwError:function(a,c){throw Z("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},consume:function(a){if(0===this.tokens.length)throw Z("ueoe",this.text);var c=this [...]
+c||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return c},peekToken:function(){if(0===this.tokens.length)throw Z("ueoe",this.text);return this.tokens[0]},peek:function(a,c,d,e){return this.peekAhead(0,a,c,d,e)},peekAhead:function(a,c,d,e,f){if(this.tokens.length>a){a=this.tokens[a];var h=a.text;if(h===c||h===d||h===e||h===f||!(c||d||e||f))return a}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},constants:{"true":{type:s.Liter [...]
+"false":{type:s.Literal,value:!1},"null":{type:s.Literal,value:null},undefined:{type:s.Literal,value:w},"this":{type:s.ThisExpression}}};sd.prototype={compile:function(a,c){var d=this,e=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:c,fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};U(e,d.$filter);var f="",h;this.stage="assign";if(h=qd(e))this.state.computing="assign",f=this.nextId(),this.recurse(h,f),this.return_(f),f="fn.assign="+this.gener [...]
+"s,v,l");h=od(e.body);d.stage="inputs";m(h,function(a,c){var e="fn"+c;d.state[e]={vars:[],body:[],own:{}};d.state.computing=e;var f=d.nextId();d.recurse(a,f);d.return_(f);d.state.inputs.push(e);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(e);f='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+f+this.watchFns()+"return fn;";f=(new Function("$filter","ensureSafeMemberName","ensureSafeObject","ensureSafeFunction [...]
+"ensureSafeAssignContext","ifDefined","plus","text",f))(this.$filter,Xa,Ba,ld,kd,md,Xf,nd,a);this.state=this.stage=w;f.literal=rd(e);f.constant=e.constant;return f},USE:"use",STRICT:"strict",watchFns:function(){var a=[],c=this.state.inputs,d=this;m(c,function(c){a.push("var "+c+"="+d.generateFunction(c,"s"))});c.length&&a.push("fn.inputs=["+c.join(",")+"];");return a.join("")},generateFunction:function(a,c){return"function("+c+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:funct [...]
+[],c=this;m(this.state.filters,function(d,e){a.push(d+"=$filter("+c.escape(e)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,c,d,e,f,h){var g,l,k=this,n,p;e=e||y;if(!h&&A(a.watchId))c=c||this.nextId(),this.if_("i",this.lazyAssign(c,this.computedMember("i",a.watchId)),this.lazyRecurse(a,c,d,e,f,!0));else switch(a.typ [...]
+function(c,d){k.recurse(c.expression,w,w,function(a){l=a});d!==a.body.length-1?k.current().body.push(l,";"):k.return_(l)});break;case s.Literal:p=this.escape(a.value);this.assign(c,p);e(p);break;case s.UnaryExpression:this.recurse(a.argument,w,w,function(a){l=a});p=a.operator+"("+this.ifDefined(l,0)+")";this.assign(c,p);e(p);break;case s.BinaryExpression:this.recurse(a.left,w,w,function(a){g=a});this.recurse(a.right,w,w,function(a){l=a});p="+"===a.operator?this.plus(g,l):"-"===a.operator [...]
+0)+a.operator+this.ifDefined(l,0):"("+g+")"+a.operator+"("+l+")";this.assign(c,p);e(p);break;case s.LogicalExpression:c=c||this.nextId();k.recurse(a.left,c);k.if_("&&"===a.operator?c:k.not(c),k.lazyRecurse(a.right,c));e(c);break;case s.ConditionalExpression:c=c||this.nextId();k.recurse(a.test,c);k.if_(c,k.lazyRecurse(a.alternate,c),k.lazyRecurse(a.consequent,c));e(c);break;case s.Identifier:c=c||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnPr [...]
+a.name)+"?l:s"),d.computed=!1,d.name=a.name);Xa(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){f&&1!==f&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(c,k.nonComputedMember("s",a.name))})},c&&k.lazyAssign(c,k.nonComputedMember("l",a.name)));(k.state.expensiveChecks||Fb(a.name))&&k.addEnsureSafeObject(c);e(c);break;case s.MemberExpression:g=d&&(d.contex [...]
+this.nextId();c=c||this.nextId();k.recurse(a.object,g,w,function(){k.if_(k.notNull(g),function(){if(a.computed)l=k.nextId(),k.recurse(a.property,l),k.getStringValue(l),k.addEnsureSafeMemberName(l),f&&1!==f&&k.if_(k.not(k.computedMember(g,l)),k.lazyAssign(k.computedMember(g,l),"{}")),p=k.ensureSafeObject(k.computedMember(g,l)),k.assign(c,p),d&&(d.computed=!0,d.name=l);else{Xa(a.property.name);f&&1!==f&&k.if_(k.not(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g, [...]
+"{}"));p=k.nonComputedMember(g,a.property.name);if(k.state.expensiveChecks||Fb(a.property.name))p=k.ensureSafeObject(p);k.assign(c,p);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(c,"undefined")});e(c)},!!f);break;case s.CallExpression:c=c||this.nextId();a.filter?(l=k.filter(a.callee.name),n=[],m(a.arguments,function(a){var c=k.nextId();k.recurse(a,c);n.push(c)}),p=l+"("+n.join(",")+")",k.assign(c,p),e(c)):(l=k.nextId(),g={},n=[],k.recurse(a.callee,l,g,function(){k.if_( [...]
+function(){k.addEnsureSafeFunction(l);m(a.arguments,function(a){k.recurse(a,k.nextId(),w,function(a){n.push(k.ensureSafeObject(a))})});g.name?(k.state.expensiveChecks||k.addEnsureSafeObject(g.context),p=k.member(g.context,g.name,g.computed)+"("+n.join(",")+")"):p=l+"("+n.join(",")+")";p=k.ensureSafeObject(p);k.assign(c,p)},function(){k.assign(c,"undefined")});e(c)}));break;case s.AssignmentExpression:l=this.nextId();g={};if(!pd(a.left))throw Z("lval");this.recurse(a.left,w,g,function(){k [...]
+function(){k.recurse(a.right,l);k.addEnsureSafeObject(k.member(g.context,g.name,g.computed));k.addEnsureSafeAssignContext(g.context);p=k.member(g.context,g.name,g.computed)+a.operator+l;k.assign(c,p);e(c||p)})},1);break;case s.ArrayExpression:n=[];m(a.elements,function(a){k.recurse(a,k.nextId(),w,function(a){n.push(a)})});p="["+n.join(",")+"]";this.assign(c,p);e(p);break;case s.ObjectExpression:n=[];m(a.properties,function(a){k.recurse(a.value,k.nextId(),w,function(c){n.push(k.escape(a.k [...]
+s.Identifier?a.key.name:""+a.key.value)+":"+c)})});p="{"+n.join(",")+"}";this.assign(c,p);e(p);break;case s.ThisExpression:this.assign(c,"s");e("s");break;case s.NGValueParameter:this.assign(c,"v"),e("v")}},getHasOwnProperty:function(a,c){var d=a+"."+c,e=this.current().own;e.hasOwnProperty(d)||(e[d]=this.nextId(!1,a+"&&("+this.escape(c)+" in "+a+")"));return e[d]},assign:function(a,c){if(a)return this.current().body.push(a,"=",c,";"),a},filter:function(a){this.state.filters.hasOwnPropert [...]
+this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,c){return"ifDefined("+a+","+this.escape(c)+")"},plus:function(a,c){return"plus("+a+","+c+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,c,d){if(!0===a)c();else{var e=this.current().body;e.push("if(",a,"){");c();e.push("}");d&&(e.push("else{"),d(),e.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,c){return a+"."+c},compu [...]
+c){return a+"["+c+"]"},member:function(a,c,d){return d?this.computedMember(a,c):this.nonComputedMember(a,c)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),";")},addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),";")},addEnsureSafeAssignContext:function(a){this.current().body.push(this.ensureSafeAssignContext(a),";")},ensur [...]
+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},getStringValue:function(a){this.assign(a,"getStringValue("+a+",text)")},ensureSafeAssignContext:function(a){return"ensureSafeAssignContext("+a+",text)"},lazyRecurse:function(a,c,d,e,f,h){var g=this;return function(){g.recurse(a,c,d,e,f,h)}},lazyAssign:function(a,c){var d=this;return function(){d.assign(a,c)}},stringEscapeRegex:/[^ a [...]
+stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(G(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(V(a))return a.toString();if(!0===a)return"true";if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Z("esc");},nextId:function(a,c){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(c?"="+c:""));return d},current:function(){return this.state[this.s [...]
+td.prototype={compile:function(a,c){var d=this,e=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=c;U(e,d.$filter);var f,h;if(f=qd(e))h=this.recurse(f);f=od(e.body);var g;f&&(g=[],m(f,function(a,c){var e=d.recurse(a);a.input=e;g.push(e);a.watchId=c}));var l=[];m(e.body,function(a){l.push(d.recurse(a.expression))});f=0===e.body.length?function(){}:1===e.body.length?l[0]:function(a,c){var d;m(l,function(e){d=e(a,c)});return d};h&&(f.assign=function(a,c,d){return h(a,d,c)});g&& [...]
+g);f.literal=rd(e);f.constant=e.constant;return f},recurse:function(a,c,d){var e,f,h=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,c);case s.UnaryExpression:return f=this.recurse(a.argument),this["unary"+a.operator](f,c);case s.BinaryExpression:return e=this.recurse(a.left),f=this.recurse(a.right),this["binary"+a.operator](e,f,c);case s.LogicalExpression:return e=this.recurse(a.left),f=this.recurse(a.right),this["binary"+a [...]
+f,c);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),c);case s.Identifier:return Xa(a.name,h.expression),h.identifier(a.name,h.expensiveChecks||Fb(a.name),c,d,h.expression);case s.MemberExpression:return e=this.recurse(a.object,!1,!!d),a.computed||(Xa(a.property.name,h.expression),f=a.property.name),a.computed&&(f=this.recurse(a.property)),a.computed?this.computedMember(e,f,c,d,h.expression):this.nonComputedM [...]
+h.expensiveChecks,c,d,h.expression);case s.CallExpression:return g=[],m(a.arguments,function(a){g.push(h.recurse(a))}),a.filter&&(f=this.$filter(a.callee.name)),a.filter||(f=this.recurse(a.callee,!0)),a.filter?function(a,d,e,h){for(var r=[],m=0;m<g.length;++m)r.push(g[m](a,d,e,h));a=f.apply(w,r,h);return c?{context:w,name:w,value:a}:a}:function(a,d,e,p){var r=f(a,d,e,p),m;if(null!=r.value){Ba(r.context,h.expression);ld(r.value,h.expression);m=[];for(var s=0;s<g.length;++s)m.push(Ba(g[s]( [...]
+h.expression));m=Ba(r.value.apply(r.context,m),h.expression)}return c?{value:m}:m};case s.AssignmentExpression:return e=this.recurse(a.left,!0,1),f=this.recurse(a.right),function(a,d,g,p){var r=e(a,d,g,p);a=f(a,d,g,p);Ba(r.value,h.expression);md(r.context);r.context[r.name]=a;return c?{value:a}:a};case s.ArrayExpression:return g=[],m(a.elements,function(a){g.push(h.recurse(a))}),function(a,d,e,f){for(var h=[],m=0;m<g.length;++m)h.push(g[m](a,d,e,f));return c?{value:h}:h};case s.ObjectExp [...]
+[],m(a.properties,function(a){g.push({key:a.key.type===s.Identifier?a.key.name:""+a.key.value,value:h.recurse(a.value)})}),function(a,d,e,f){for(var h={},m=0;m<g.length;++m)h[g[m].key]=g[m].value(a,d,e,f);return c?{value:h}:h};case s.ThisExpression:return function(a){return c?{value:a}:a};case s.NGValueParameter:return function(a,d,e,f){return c?{value:e}:e}}},"unary+":function(a,c){return function(d,e,f,h){d=a(d,e,f,h);d=A(d)?+d:0;return c?{value:d}:d}},"unary-":function(a,c){return fun [...]
+f,h){d=a(d,e,f,h);d=A(d)?-d:0;return c?{value:d}:d}},"unary!":function(a,c){return function(d,e,f,h){d=!a(d,e,f,h);return c?{value:d}:d}},"binary+":function(a,c,d){return function(e,f,h,g){var l=a(e,f,h,g);e=c(e,f,h,g);l=nd(l,e);return d?{value:l}:l}},"binary-":function(a,c,d){return function(e,f,h,g){var l=a(e,f,h,g);e=c(e,f,h,g);l=(A(l)?l:0)-(A(e)?e:0);return d?{value:l}:l}},"binary*":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)*c(e,f,h,g);return d?{value:e}:e}},"binary/":func [...]
+d){return function(e,f,h,g){e=a(e,f,h,g)/c(e,f,h,g);return d?{value:e}:e}},"binary%":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)%c(e,f,h,g);return d?{value:e}:e}},"binary===":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)===c(e,f,h,g);return d?{value:e}:e}},"binary!==":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)!==c(e,f,h,g);return d?{value:e}:e}},"binary==":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)==c(e,f,h,g);return d?{value:e}:e}},"binary!=":func [...]
+d){return function(e,f,h,g){e=a(e,f,h,g)!=c(e,f,h,g);return d?{value:e}:e}},"binary<":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)<c(e,f,h,g);return d?{value:e}:e}},"binary>":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)>c(e,f,h,g);return d?{value:e}:e}},"binary<=":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)<=c(e,f,h,g);return d?{value:e}:e}},"binary>=":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)>=c(e,f,h,g);return d?{value:e}:e}},"binary&&":function( [...]
+f,h,g){e=a(e,f,h,g)&&c(e,f,h,g);return d?{value:e}:e}},"binary||":function(a,c,d){return function(e,f,h,g){e=a(e,f,h,g)||c(e,f,h,g);return d?{value:e}:e}},"ternary?:":function(a,c,d,e){return function(f,h,g,l){f=a(f,h,g,l)?c(f,h,g,l):d(f,h,g,l);return e?{value:f}:f}},value:function(a,c){return function(){return c?{context:w,name:w,value:a}:a}},identifier:function(a,c,d,e,f){return function(h,g,l,k){h=g&&a in g?g:h;e&&1!==e&&h&&!h[a]&&(h[a]={});g=h?h[a]:w;c&&Ba(g,f);return d?{context:h,na [...]
+g}},computedMember:function(a,c,d,e,f){return function(h,g,l,k){var n=a(h,g,l,k),p,m;null!=n&&(p=c(h,g,l,k),p=kd(p),Xa(p,f),e&&1!==e&&n&&!n[p]&&(n[p]={}),m=n[p],Ba(m,f));return d?{context:n,name:p,value:m}:m}},nonComputedMember:function(a,c,d,e,f,h){return function(g,l,k,n){g=a(g,l,k,n);f&&1!==f&&g&&!g[c]&&(g[c]={});l=null!=g?g[c]:w;(d||Fb(c))&&Ba(l,h);return e?{context:g,name:c,value:l}:l}},inputs:function(a,c){return function(d,e,f,h){return h?h[c]:a(d,e,f)}}};var fc=function(a,c,d){th [...]
+a;this.$filter=c;this.options=d;this.ast=new s(this.lexer);this.astCompiler=d.csp?new td(this.ast,c):new sd(this.ast,c)};fc.prototype={constructor:fc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};fa();fa();var Yf=Object.prototype.valueOf,Ca=I("$sce"),oa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},ga=I("$compile"),$=X.createElement("a"),xd=Aa(Q.location.href);yd.$inject=["$document"];Kc.$inject=["$provide"];zd.$inject=["$local [...]
+["$locale"];var hc=".",hg={yyyy:aa("FullYear",4),yy:aa("FullYear",2,0,!0),y:aa("FullYear",1),MMMM:Hb("Month"),MMM:Hb("Month",!0),MM:aa("Month",2,1),M:aa("Month",1,1),dd:aa("Date",2),d:aa("Date",1),HH:aa("Hours",2),H:aa("Hours",1),hh:aa("Hours",2,-12),h:aa("Hours",1,-12),mm:aa("Minutes",2),m:aa("Minutes",1),ss:aa("Seconds",2),s:aa("Seconds",1),sss:aa("Milliseconds",3),EEEE:Hb("Day"),EEE:Hb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a,c,d){a=-1*d;ret [...]
+a?"+":"")+(Gb(Math[0<a?"floor":"ceil"](a/60),2)+Gb(Math.abs(a%60),2))},ww:Fd(2),w:Fd(1),G:ic,GG:ic,GGG:ic,GGGG:function(a,c){return 0>=a.getFullYear()?c.ERANAMES[0]:c.ERANAMES[1]}},gg=/((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,fg=/^\-?\d+$/;Ad.$inject=["$locale"];var cg=qa(F),dg=qa(sb);Cd.$inject=["$parse"];var he=qa({restrict:"E",compile:function(a,c){if(!c.href&&!c.xlinkHref)return function(a,c){if("a"===c[0].nodeName.toLowerCase()){var f="[o [...]
+va.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}}),tb={};m(Bb,function(a,c){function d(a,d,f){a.$watch(f[e],function(a){f.$set(c,!!a)})}if("multiple"!=a){var e=ya("ng-"+c),f=d;"checked"===a&&(f=function(a,c,f){f.ngModel!==f[e]&&d(a,c,f)});tb[e]=function(){return{restrict:"A",priority:100,link:f}}}});m($c,function(a,c){tb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPatt [...]
+new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});m(["src","srcset","href"],function(a){var c=ya("ng-"+a);tb[c]=function(){return{priority:99,link:function(d,e,f){var h=a,g=a;"href"===a&&"[object SVGAnimatedString]"===va.call(e.prop("href"))&&(g="xlinkHref",f.$attr[g]="xlink:href",h=null);f.$observe(c,function(c){c?(f.$set(g,c),Wa&&h&&e.prop(h,f[g])):"href"===a&&f.$set(g,null)})}}}});var Ib={$addControl:y,$$renameControl:function(a,c){a.$name=c},$removeControl:y, [...]
+$setDirty:y,$setPristine:y,$setSubmitted:y};Gd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Od=function(a){return["$timeout","$parse",function(c,d){function e(a){return""===a?d('this[""]').assign:d(a).assign||y}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Gd,compile:function(d,h){d.addClass(Ya).addClass(mb);var g=h.name?"name":a&&h.ngForm?"ngForm":!1;return{pre:function(a,d,f,h){var m=h[0];if(!("action"in f)){var t=function(c){a.$a [...]
+m.$setSubmitted()});c.preventDefault()};d[0].addEventListener("submit",t,!1);d.on("$destroy",function(){c(function(){d[0].removeEventListener("submit",t,!1)},0,!1)})}(h[1]||m.$$parentForm).$addControl(m);var s=g?e(m.$name):y;g&&(s(a,m),f.$observe(g,function(c){m.$name!==c&&(s(a,w),m.$$parentForm.$$renameControl(m,c),s=e(m.$name),s(a,m))}));d.on("$destroy",function(){m.$$parentForm.$removeControl(m);s(a,w);P(m,Ib)})}}}}}]},ie=Od(),ve=Od(!0),ig=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d [...]
+sg=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,tg=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,ug=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Pd=/^(\d{4})-(\d{2})-(\d{2})$/,Qd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,lc=/^(\d{4})-W(\d\d)$/,Rd=/^(\d{4})-(\d\d)$/,Sd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Td={text:function(a,c,d,e,f,h){jb(a,c,d,e,f,h);jc(e)},dat [...]
+Pd,Kb(Pd,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":kb("datetimelocal",Qd,Kb(Qd,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:kb("time",Sd,Kb(Sd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:kb("week",lc,function(a,c){if(ea(a))return a;if(G(a)){lc.lastIndex=0;var d=lc.exec(a);if(d){var e=+d[1],f=+d[2],h=d=0,g=0,l=0,k=Ed(e),f=7*(f-1);c&&(d=c.getHours(),h=c.getMinutes(),g=c.getSeconds(),l=c.getMilliseconds());return new Date(e,0,k.getDate()+f,d,h,g,l)}}return [...]
+month:kb("month",Rd,Kb(Rd,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,h){Id(a,c,d,e);jb(a,c,d,e,f,h);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:ug.test(a)?parseFloat(a):w});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!V(a))throw lb("numfmt",a);a=a.toString()}return a});if(A(d.min)||d.ngMin){var g;e.$validators.min=function(a){return e.$isEmpty(a)||v(g)||a>=g};d.$observe("min",function(a){A(a)&&!V(a)&&(a=parseFloat(a,10));g=V(a)&&!isNaN( [...]
+d.ngMax){var l;e.$validators.max=function(a){return e.$isEmpty(a)||v(l)||a<=l};d.$observe("max",function(a){A(a)&&!V(a)&&(a=parseFloat(a,10));l=V(a)&&!isNaN(a)?a:w;e.$validate()})}},url:function(a,c,d,e,f,h){jb(a,c,d,e,f,h);jc(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||sg.test(d)}},email:function(a,c,d,e,f,h){jb(a,c,d,e,f,h);jc(e);e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||tg.test(d)}},radio:func [...]
+d,e){v(d.name)&&c.attr("name",++nb);c.on("click",function(a){c[0].checked&&e.$setViewValue(d.value,a&&a.type)});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e,f,h,g,l){var k=Jd(l,a,"ngTrueValue",d.ngTrueValue,!0),n=Jd(l,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return!1===a};e.$formatters.pus [...]
+k)});e.$parsers.push(function(a){return a?k:n})},hidden:y,button:y,submit:y,reset:y,file:y},Ec=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:{pre:function(f,h,g,l){l[0]&&(Td[F(g.type)]||Td.text)(f,h,g,l[0],c,a,d,e)}}}}],vg=/^(true|false|\d+)$/,Ne=function(){return{restrict:"A",priority:100,compile:function(a,c){return vg.test(c.ngValue)?function(a,c,f){f.$set("value",a.$eval(f.ngValue))}:function(a,c,f){a.$watch(f.ngValue,functi [...]
+a)})}}}},ne=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);e=e[0];c.$watch(f.ngBind,function(a){e.textContent=v(a)?"":a})}}}}],pe=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,h){d=a(f.attr(h.$attr.ngBindTemplate));c.$$addBindingInfo(f,d.expressions);f=f[0];h.$observe("ngBindTemplate",function(a){f.textContent=v(a)?"":a})}}}}],oe= [...]
+"$compile",function(a,c,d){return{restrict:"A",compile:function(e,f){var h=c(f.ngBindHtml),g=c(f.ngBindHtml,function(a){return(a||"").toString()});d.$$addBindingClass(e);return function(c,e,f){d.$$addBindingInfo(e,f.ngBindHtml);c.$watch(g,function(){e.html(a.getTrustedHtml(h(c))||"")})}}}}],Me=qa({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),qe=kc("",!0),se=kc("Odd",0),re=kc("Even",1),te=Na({compile:function(a,c){c. [...]
+w);a.removeClass("ng-cloak")}}),ue=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Jc={},wg={blur:!0,focus:!0};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=ya("ng-"+a);Jc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,h){var g=d(h[c],null,!0);return function(c,d){d.on(a,function(d){var f=function(){g(c,{$ev [...]
+wg[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var xe=["$animate",function(a){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,h){var g,l,k;c.$watch(e.ngIf,function(c){c?l||h(function(c,f){l=f;c[c.length++]=X.createComment(" end ngIf: "+e.ngIf+" ");g={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),l&&(l.$destroy(),l=null),g&&(k=rb(g.clone),a.leave(k).then(function(){k=null}),g=null))})}}}],ye=["$templateRe [...]
+"$animate",function(a,c,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:da.noop,compile:function(e,f){var h=f.ngInclude||f.src,g=f.onload||"",l=f.autoscroll;return function(e,f,m,r,t){var s=0,v,u,q,z=function(){u&&(u.remove(),u=null);v&&(v.$destroy(),v=null);q&&(d.leave(q).then(function(){u=null}),u=q,q=null)};e.$watch(h,function(h){var m=function(){!A(l)||l&&!e.$eval(l)||c()},p=++s;h?(a(h,!0).then(function(a){if(p===s){var c=e.$new();r.template=a;a=t(c, [...]
+d.enter(a,null,f).then(m)});v=c;q=a;v.$emit("$includeContentLoaded",h);e.$eval(g)}},function(){p===s&&(z(),e.$emit("$includeContentError",h))}),e.$emit("$includeContentRequested",h)):(z(),r.template=null)})}}}}],Pe=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(Mc(f.template,X).childNodes)(c,function(a){d.append(a)},{futureParentElement:d})):(d.html(f.template),a(d.contents())(c))}}}],ze=Na({ [...]
+compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Le=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,c,d,e){var f=c.attr(d.$attr.ngList)||", ",h="false"!==d.ngTrim,g=h?T(f):f;e.$parsers.push(function(a){if(!v(a)){var c=[];a&&m(a.split(g),function(a){a&&c.push(h?T(a):a)});return c}});e.$formatters.push(function(a){return J(a)?a.join(f):w});e.$isEmpty=function(a){return!a||!a.length}}}},mb="ng-valid",Kd="ng-invalid",Ya="ng-pristine",Jb="ng-d [...]
+"ng-pending",lb=I("ngModel"),xg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,c,d,e,f,h,g,l,k,n){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=w;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this [...]
+w;this.$name=n(d.name||"",!1)(a);this.$$parentForm=Ib;var p=f(d.ngModel),r=p.assign,t=p,s=r,K=null,u,q=this;this.$$setOptions=function(a){if((q.$options=a)&&a.getterSetter){var c=f(d.ngModel+"()"),g=f(d.ngModel+"($$$p)");t=function(a){var d=p(a);x(d)&&(d=c(a));return d};s=function(a,c){x(p(a))?g(a,{$$$p:q.$modelValue}):r(a,q.$modelValue)}}else if(!p.assign)throw lb("nonassign",d.ngModel,xa(e));};this.$render=y;this.$isEmpty=function(a){return v(a)||""===a||null===a||a!==a};var z=0;Hd({ct [...]
+set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},$animate:h});this.$setPristine=function(){q.$dirty=!1;q.$pristine=!0;h.removeClass(e,Jb);h.addClass(e,Ya)};this.$setDirty=function(){q.$dirty=!0;q.$pristine=!1;h.removeClass(e,Ya);h.addClass(e,Jb);q.$$parentForm.$setDirty()};this.$setUntouched=function(){q.$touched=!1;q.$untouched=!0;h.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=function(){q.$touched=!0;q.$untouched=!1;h.setClass(e,"ng-touched","ng-untouched")};thi [...]
+function(){g.cancel(K);q.$viewValue=q.$$lastCommittedViewValue;q.$render()};this.$validate=function(){if(!V(q.$modelValue)||!isNaN(q.$modelValue)){var a=q.$$rawModelValue,c=q.$valid,d=q.$modelValue,e=q.$options&&q.$options.allowInvalid;q.$$runValidators(a,q.$$lastCommittedViewValue,function(f){e||c===f||(q.$modelValue=f?a:w,q.$modelValue!==d&&q.$$writeModelToScope())})}};this.$$runValidators=function(a,c,d){function e(){var d=!0;m(q.$validators,function(e,f){var h=e(a,c);d=d&&h;g(f,h)}); [...]
+!0:(m(q.$asyncValidators,function(a,c){g(c,null)}),!1)}function f(){var d=[],e=!0;m(q.$asyncValidators,function(f,h){var k=f(a,c);if(!k||!x(k.then))throw lb("$asyncValidators",k);g(h,w);d.push(k.then(function(){g(h,!0)},function(a){e=!1;g(h,!1)}))});d.length?k.all(d).then(function(){h(e)},y):h(!0)}function g(a,c){l===z&&q.$setValidity(a,c)}function h(a){l===z&&d(a)}z++;var l=z;(function(){var a=q.$$parserName||"parse";if(v(u))g(a,null);else return u||(m(q.$validators,function(a,c){g(c,nu [...]
+function(a,c){g(c,null)})),g(a,u),u;return!0})()?e()?f():h(!1):h(!1)};this.$commitViewValue=function(){var a=q.$viewValue;g.cancel(K);if(q.$$lastCommittedViewValue!==a||""===a&&q.$$hasNativeValidators)q.$$lastCommittedViewValue=a,q.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var c=q.$$lastCommittedViewValue;if(u=v(c)?w:!0)for(var d=0;d<q.$parsers.length;d++)if(c=q.$parsers[d](c),v(c)){u=!1;break}V(q.$modelValue)&&isNaN(q.$modelValue)&&(q.$mod [...]
+var e=q.$modelValue,f=q.$options&&q.$options.allowInvalid;q.$$rawModelValue=c;f&&(q.$modelValue=c,q.$modelValue!==e&&q.$$writeModelToScope());q.$$runValidators(c,q.$$lastCommittedViewValue,function(a){f||(q.$modelValue=a?c:w,q.$modelValue!==e&&q.$$writeModelToScope())})};this.$$writeModelToScope=function(){s(a,q.$modelValue);m(q.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};this.$setViewValue=function(a,c){q.$viewValue=a;q.$options&&!q.$options.updateOnDefault||q.$$debounceV [...]
+this.$$debounceViewValueCommit=function(c){var d=0,e=q.$options;e&&A(e.debounce)&&(e=e.debounce,V(e)?d=e:V(e[c])?d=e[c]:V(e["default"])&&(d=e["default"]));g.cancel(K);d?K=g(function(){q.$commitViewValue()},d):l.$$phase?q.$commitViewValue():a.$apply(function(){q.$commitViewValue()})};a.$watch(function(){var c=t(a);if(c!==q.$modelValue&&(q.$modelValue===q.$modelValue||c===c)){q.$modelValue=q.$$rawModelValue=c;u=w;for(var d=q.$formatters,e=d.length,f=c;e--;)f=d[e](f);q.$viewValue!==f&&(q.$v [...]
+q.$$lastCommittedViewValue=f,q.$render(),q.$$runValidators(c,f,y))}return c})}],Ke=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:xg,priority:1,compile:function(c){c.addClass(Ya).addClass("ng-untouched").addClass(mb);return{pre:function(a,c,f,h){var g=h[0];c=h[1]||g.$$parentForm;g.$$setOptions(h[2]&&h[2].$options);c.$addControl(g);f.$observe("name",function(a){g.$name!==a&&g.$$parentForm.$$renameControl(g,a)});a.$on("$destroy",fun [...]
+post:function(c,e,f,h){var g=h[0];if(g.$options&&g.$options.updateOn)e.on(g.$options.updateOn,function(a){g.$$debounceViewValueCommit(a&&a.type)});e.on("blur",function(e){g.$touched||(a.$$phase?c.$evalAsync(g.$setTouched):c.$apply(g.$setTouched))})}}}}}],yg=/(\s+|^)default(\s+|$)/,Oe=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=ha(a.$eval(c.ngModelOptions));A(this.$options.updateOn)?(this.$options.updateOnDefault=!1,this.$options.upd [...]
+function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},Ae=Na({terminal:!0,priority:1E3}),zg=I("ngOptions"),Ag=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,Ie=["$compile","$parse",function(a,c){function d(a,d,e){function f(a,c,d,e,g){this.selectValue=a;this.viewVal [...]
+d;this.group=e;this.disabled=g}function n(a){var c;if(!s&&Da(a))c=a;else{c=[];for(var d in a)a.hasOwnProperty(d)&&"$"!==d.charAt(0)&&c.push(d)}return c}var m=a.match(Ag);if(!m)throw zg("iexp",a,xa(d));var r=m[5]||m[7],s=m[6];a=/ as /.test(m[0])&&m[1];var v=m[9];d=c(m[2]?m[1]:r);var w=a&&c(a)||d,u=v&&c(v),q=v?function(a,c){return u(e,c)}:function(a){return Ga(a)},z=function(a,c){return q(a,x(a,c))},y=c(m[2]||m[1]),A=c(m[3]||""),O=c(m[4]||""),H=c(m[8]),B={},x=s?function(a,c){B[s]=c;B[r]=a; [...]
+function(a){B[r]=a;return B};return{trackBy:v,getTrackByValue:z,getWatchables:c(H,function(a){var c=[];a=a||[];for(var d=n(a),f=d.length,g=0;g<f;g++){var h=a===d?g:d[g],k=x(a[h],h),h=q(a[h],k);c.push(h);if(m[2]||m[1])h=y(e,k),c.push(h);m[4]&&(k=O(e,k),c.push(k))}return c}),getOptions:function(){for(var a=[],c={},d=H(e)||[],g=n(d),h=g.length,m=0;m<h;m++){var p=d===g?m:g[m],r=x(d[p],p),s=w(e,r),p=q(s,r),t=y(e,r),u=A(e,r),r=O(e,r),s=new f(p,s,t,u,r);a.push(s);c[p]=s}return{items:a,selectVal [...]
+getViewValueFromOption:function(a){return v?da.copy(a.viewValue):a.viewValue}}}}}var e=X.createElement("option"),f=X.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","?ngModel"],link:function(c,g,l,k){function n(a,c){a.element=c;c.disabled=a.disabled;a.label!==c.label&&(c.label=a.label,c.textContent=a.label);a.value!==c.value&&(c.value=a.selectValue)}function p(a,c,d,e){c&&F(c.nodeName)===d?d=c:(d=e.cloneNode(!1),c?a.insertBefore(d,c):a.appendChild(d));return d [...]
+a.nextSibling,Wb(a),a=c}function s(a){var c=q&&q[0],d=H&&H[0];if(c||d)for(;a&&(a===c||a===d||c&&8===c.nodeType);)a=a.nextSibling;return a}function v(){var a=x&&u.readValue();x=C.getOptions();var c={},d=g[0].firstChild;O&&g.prepend(q);d=s(d);x.items.forEach(function(a){var h,k;a.group?(h=c[a.group],h||(h=p(g[0],d,"optgroup",f),d=h.nextSibling,h.label=a.group,h=c[a.group]={groupElement:h,currentOptionElement:h.firstChild}),k=p(h.groupElement,h.currentOptionElement,"option",e),n(a,k),h.curr [...]
+k.nextSibling):(k=p(g[0],d,"option",e),n(a,k),d=k.nextSibling)});Object.keys(c).forEach(function(a){r(c[a].currentOptionElement)});r(d);w.$render();if(!w.$isEmpty(a)){var h=u.readValue();(C.trackBy?ka(a,h):a===h)||(w.$setViewValue(h),w.$render())}}var w=k[1];if(w){var u=k[0];k=l.multiple;for(var q,z=0,y=g.children(),A=y.length;z<A;z++)if(""===y[z].value){q=y.eq(z);break}var O=!!q,H=B(e.cloneNode(!1));H.val("?");var x,C=d(l.ngOptions,g,c);k?(w.$isEmpty=function(a){return!a||0===a.length}, [...]
+function(a){x.items.forEach(function(a){a.element.selected=!1});a&&a.forEach(function(a){(a=x.getOptionFromViewValue(a))&&!a.disabled&&(a.element.selected=!0)})},u.readValue=function(){var a=g.val()||[],c=[];m(a,function(a){(a=x.selectValueMap[a])&&!a.disabled&&c.push(x.getViewValueFromOption(a))});return c},C.trackBy&&c.$watchCollection(function(){if(J(w.$viewValue))return w.$viewValue.map(function(a){return C.getTrackByValue(a)})},function(){w.$render()})):(u.writeValue=function(a){var [...]
+c&&!c.disabled?g[0].value!==c.selectValue&&(H.remove(),O||q.remove(),g[0].value=c.selectValue,c.element.selected=!0,c.element.setAttribute("selected","selected")):null===a||O?(H.remove(),O||g.prepend(q),g.val(""),q.prop("selected",!0),q.attr("selected",!0)):(O||q.remove(),g.prepend(H),g.val("?"),H.prop("selected",!0),H.attr("selected",!0))},u.readValue=function(){var a=x.selectValueMap[g.val()];return a&&!a.disabled?(O||q.remove(),H.remove(),x.getViewValueFromOption(a)):null},C.trackBy&& [...]
+function(){w.$render()}));O?(q.remove(),a(q)(c),q.removeClass("ng-scope")):q=B(e.cloneNode(!1));v();c.$watchCollection(C.getWatchables,v)}}}}],Be=["$locale","$interpolate","$log",function(a,c,d){var e=/{}/g,f=/^when(Minus)?(.+)$/;return{link:function(h,g,l){function k(a){g.text(a||"")}var n=l.count,p=l.$attr.when&&g.attr(l.$attr.when),r=l.offset||0,s=h.$eval(p)||{},w={},A=c.startSymbol(),u=c.endSymbol(),q=A+n+"-"+r+u,z=da.noop,x;m(l,function(a,c){var d=f.exec(c);d&&(d=(d[1]?"-":"")+F(d[2 [...]
+m(s,function(a,d){w[d]=c(a.replace(e,q))});h.$watch(n,function(c){var e=parseFloat(c),f=isNaN(e);f||e in s||(e=a.pluralCat(e-r));e===x||f&&V(x)&&isNaN(x)||(z(),f=w[e],v(f)?(null!=c&&d.debug("ngPluralize: no rule defined for '"+e+"' in "+p),z=y,k()):z=h.$watch(f,k),x=e)})}}}],Ce=["$parse","$animate",function(a,c){var d=I("ngRepeat"),e=function(a,c,d,e,k,m,p){a[d]=e;k&&(a[k]=m);a.$index=c;a.$first=0===c;a.$last=c===p-1;a.$middle=!(a.$first||a.$last);a.$odd=!(a.$even=0===(c&1))};return{rest [...]
+multiElement:!0,transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,compile:function(f,h){var g=h.ngRepeat,l=X.createComment(" end ngRepeat: "+g+" "),k=g.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!k)throw d("iexp",g);var n=k[1],p=k[2],r=k[3],s=k[4],k=n.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);if(!k)throw d("iidexp",n);var v=k[3]||k[1],y=k[2];if(r&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(r)||/^(null|undefin [...]
+r);var u,q,z,A,x={$id:Ga};s?u=a(s):(z=function(a,c){return Ga(c)},A=function(a){return a});return function(a,f,h,k,n){u&&(q=function(c,d,e){y&&(x[y]=c);x[v]=d;x.$index=e;return u(a,x)});var s=fa();a.$watchCollection(p,function(h){var k,p,t=f[0],u,x=fa(),C,G,J,M,I,F,L;r&&(a[r]=h);if(Da(h))I=h,p=q||z;else for(L in p=q||A,I=[],h)ta.call(h,L)&&"$"!==L.charAt(0)&&I.push(L);C=I.length;L=Array(C);for(k=0;k<C;k++)if(G=h===I?k:I[k],J=h[G],M=p(G,J,k),s[M])F=s[M],delete s[M],x[M]=F,L[k]=F;else{if(x [...]
+function(a){a&&a.scope&&(s[a.id]=a)}),d("dupes",g,M,J);L[k]={id:M,scope:w,clone:w};x[M]=!0}for(u in s){F=s[u];M=rb(F.clone);c.leave(M);if(M[0].parentNode)for(k=0,p=M.length;k<p;k++)M[k].$$NG_REMOVED=!0;F.scope.$destroy()}for(k=0;k<C;k++)if(G=h===I?k:I[k],J=h[G],F=L[k],F.scope){u=t;do u=u.nextSibling;while(u&&u.$$NG_REMOVED);F.clone[0]!=u&&c.move(rb(F.clone),null,B(t));t=F.clone[F.clone.length-1];e(F.scope,k,v,J,y,G,C)}else n(function(a,d){F.scope=d;var f=l.cloneNode(!1);a[a.length++]=f;c [...]
+null,B(t));t=f;F.clone=a;x[F.id]=F;e(F.scope,k,v,J,y,G,C)});s=x})}}}}],De=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngShow,function(c){a[c?"removeClass":"addClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],we=["$animate",function(a){return{restrict:"A",multiElement:!0,link:function(c,d,e){c.$watch(e.ngHide,function(c){a[c?"addClass":"removeClass"](d,"ng-hide",{tempClasses:"ng-hide-animate"})})}}}],Ee=Na(function(a,c,d){a.$watch( [...]
+function(a,d){d&&a!==d&&m(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),Fe=["$animate",function(a){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var h=[],g=[],l=[],k=[],n=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||e.on,function(c){var d,e;d=0;for(e=l.length;d<e;++d)a.cancel(l[d]);d=l.length=0;for(e=k.length;d<e;++d){var s=rb(g[d].clone);k[d].$destroy();(l[d]=a.leave(s)).then(n(l,d))}g.length=0;k.length=0;(h=f [...]
+c]||f.cases["?"])&&m(h,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=X.createComment(" end ngSwitchWhen: ");g.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],Ge=Na({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),He=Na({transclude:"element",priority:1200,require:"^ngSwitch",multiElement [...]
+c,d,e,f){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:f,element:c})}}),Je=Na({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw I("ngTransclude")("orphan",xa(c));f(function(a){c.empty();c.append(a)})}}),je=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],Bg={$setViewValue:y,$render:y},Cg=["$element","$scope","$attrs",function(a,c,d){var e=this,f=new Ua;e.ngModelCtrl=Bg;e.unknownOptio [...]
+e.renderUnknownOption=function(c){c="? "+Ga(c)+" ?";e.unknownOption.val(c);a.prepend(e.unknownOption);a.val(c)};c.$on("$destroy",function(){e.renderUnknownOption=y});e.removeUnknownOption=function(){e.unknownOption.parent()&&e.unknownOption.remove()};e.readValue=function(){e.removeUnknownOption();return a.val()};e.writeValue=function(c){e.hasOption(c)?(e.removeUnknownOption(),a.val(c),""===c&&e.emptyOption.prop("selected",!0)):null==c&&e.emptyOption?(e.removeUnknownOption(),a.val("")):e. [...]
+e.addOption=function(a,c){Ta(a,'"option value"');""===a&&(e.emptyOption=c);var d=f.get(a)||0;f.put(a,d+1)};e.removeOption=function(a){var c=f.get(a);c&&(1===c?(f.remove(a),""===a&&(e.emptyOption=w)):f.put(a,c-1))};e.hasOption=function(a){return!!f.get(a)}}],ke=function(){return{restrict:"E",require:["select","?ngModel"],controller:Cg,link:function(a,c,d,e){var f=e[1];if(f){var h=e[0];h.ngModelCtrl=f;f.$render=function(){h.writeValue(f.$viewValue)};c.on("change",function(){a.$apply(functi [...]
+if(d.multiple){h.readValue=function(){var a=[];m(c.find("option"),function(c){c.selected&&a.push(c.value)});return a};h.writeValue=function(a){var d=new Ua(a);m(c.find("option"),function(a){a.selected=A(d.get(a.value))})};var g,l=NaN;a.$watch(function(){l!==f.$viewValue||ka(g,f.$viewValue)||(g=ja(f.$viewValue),f.$render());l=f.$viewValue});f.$isEmpty=function(a){return!a||0===a.length}}}}}},me=["$interpolate",function(a){return{restrict:"E",priority:100,compile:function(c,d){if(A(d.value [...]
+!0);else{var f=a(c.text(),!0);f||d.$set("value",c.text())}return function(a,c,d){function k(a){p.addOption(a,c);p.ngModelCtrl.$render();c[0].hasAttribute("selected")&&(c[0].selected=!0)}var m=c.parent(),p=m.data("$selectController")||m.parent().data("$selectController");if(p&&p.ngModelCtrl){if(e){var r;d.$observe("value",function(a){A(r)&&p.removeOption(r);r=a;k(a)})}else f?a.$watch(f,function(a,c){d.$set("value",a);c!==a&&p.removeOption(c);k(a)}):k(d.value);c.on("$destroy",function(){p. [...]
+p.ngModelCtrl.$render()})}}}}}],le=qa({restrict:"E",terminal:!1}),Gc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){e&&(d.required=!0,e.$validators.required=function(a,c){return!d.required||!e.$isEmpty(c)},d.$observe("required",function(){e.$validate()}))}}},Fc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f,h=d.ngPattern||d.pattern;d.$observe("pattern",function(a){G(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)thr [...]
+h,a,xa(c));f=a||w;e.$validate()});e.$validators.pattern=function(a,c){return e.$isEmpty(c)||v(f)||f.test(c)}}}}},Ic=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=-1;d.$observe("maxlength",function(a){a=Y(a);f=isNaN(a)?-1:a;e.$validate()});e.$validators.maxlength=function(a,c){return 0>f||e.$isEmpty(c)||c.length<=f}}}}},Hc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",function(a){f=Y(a)| [...]
+e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}};Q.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(ce(),ee(da),da.module("ngLocale",[],["$provide",function(a){function c(a){a+="";var c=a.indexOf(".");return-1==c?0:a.length-c-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6, [...]
+SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSiz [...]
+maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",pluralCat:function(a,e){var f=a|0,h=e;w===h&&(h=Math.min(c(a),3));Math.pow(10,h);return 1==f&&0==h?"one":"other"}})}]),B(X).ready(function(){Zd(X,zc)}))})(window,document);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-a [...]
+//# sourceMappingURL=angular.min.js.map
diff --git a/grails-app/assets/javascripts/vendor/angular.min.js.map b/grails-app/assets/javascripts/vendor/angular.min.js.map
new file mode 100644
index 0000000..0dddf2a
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/angular.min.js.map
@@ -0,0 +1,8 @@
+{
+"version":3,
+"file":"angular.min.js",
+"lineCount":209,
+"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAmBC,CAAnB,CAA8B,CA8BvCC,QAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,MAAAA,SAAAA,EAAAA,CAAAA,IAAAA,EAAAA,SAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,EAAAA,GAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,GAAAA,CAAAA,EAAAA,EAAAA,CAAAA,CAAAA,uCAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,GAAAA,CAAAA,EAAAA,EAAAA,CAAAA,KAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,SAAAA,OAAAA,CAAAA,CAAAA,EAAAA,CAAAA,CAAAA,CAAAA,CAAAA,EAAAA,CAAAA,EAAAA,CAAAA,CAAAA,GAAAA,CAAAA,GAAAA,EAAAA,GAAAA, [...]
+"sources":["angular.js"],
+"names":["window","document","undefined","minErr","isArrayLike","obj","isWindow","length","nodeType","isString","isArray","forEach","iterator","context","key","isFunction","hasOwnProperty","call","sortedKeys","keys","push","sort","forEachSorted","i","reverseParams","iteratorFn","value","nextUid","index","uid","digit","charCodeAt","join","String","fromCharCode","unshift","setHashKey","h","$$hashKey","extend","dst","arguments","int","str","parseInt","inherit","parent","extra","noop","ident [...]
+}
diff --git a/grails-app/assets/javascripts/vendor/jquery-1.11.1.min.js b/grails-app/assets/javascripts/vendor/jquery-1.11.1.min.js
new file mode 100644
index 0000000..ab28a24
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-1.11.1.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b [...]
+if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&& [...]
+},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.prop [...]
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/external/jquery/jquery.js b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/external/jquery/jquery.js
new file mode 100644
index 0000000..c5c6482
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/external/jquery/jquery.js
@@ -0,0 +1,9789 @@
+/*!
+ * jQuery JavaScript Library v1.10.2
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2013-07-03T13:48Z
+ */
+(function( window, undefined ) {
+
+// Can't do this because several apps including ASP.NET trace
+// the stack via arguments.caller.callee and Firefox dies if
+// you try to trace through "use strict" call chains. (#13335)
+// Support: Firefox 18+
+//"use strict";
+var
+	// The deferred used on DOM ready
+	readyList,
+
+	// A central reference to the root jQuery(document)
+	rootjQuery,
+
+	// Support: IE<10
+	// For `typeof xmlNode.method` instead of `xmlNode.method !== undefined`
+	core_strundefined = typeof undefined,
+
+	// Use the correct document accordingly with window argument (sandbox)
+	location = window.location,
+	document = window.document,
+	docElem = document.documentElement,
+
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	// [[Class]] -> type pairs
+	class2type = {},
+
+	// List of deleted data cache ids, so we can reuse them
+	core_deletedIds = [],
+
+	core_version = "1.10.2",
+
+	// Save a reference to some core methods
+	core_concat = core_deletedIds.concat,
+	core_push = core_deletedIds.push,
+	core_slice = core_deletedIds.slice,
+	core_indexOf = core_deletedIds.indexOf,
+	core_toString = class2type.toString,
+	core_hasOwn = class2type.hasOwnProperty,
+	core_trim = core_version.trim,
+
+	// Define a local copy of jQuery
+	jQuery = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context, rootjQuery );
+	},
+
+	// Used for matching numbers
+	core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,
+
+	// Used for splitting on whitespace
+	core_rnotwhite = /\S+/g,
+
+	// Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+	// A simple way to check for HTML strings
+	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+	// Strict HTML recognition (#11290: must start with <)
+	rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+
+	// Match a standalone tag
+	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+	// JSON RegExp
+	rvalidchars = /^[\],:{}\s]*$/,
+	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+	rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+	rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,
+
+	// Matches dashed string for camelizing
+	rmsPrefix = /^-ms-/,
+	rdashAlpha = /-([\da-z])/gi,
+
+	// Used by jQuery.camelCase as callback to replace()
+	fcamelCase = function( all, letter ) {
+		return letter.toUpperCase();
+	},
+
+	// The ready event handler
+	completed = function( event ) {
+
+		// readyState === "complete" is good enough for us to call the dom ready in oldIE
+		if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) {
+			detach();
+			jQuery.ready();
+		}
+	},
+	// Clean-up method for dom ready events
+	detach = function() {
+		if ( document.addEventListener ) {
+			document.removeEventListener( "DOMContentLoaded", completed, false );
+			window.removeEventListener( "load", completed, false );
+
+		} else {
+			document.detachEvent( "onreadystatechange", completed );
+			window.detachEvent( "onload", completed );
+		}
+	};
+
+jQuery.fn = jQuery.prototype = {
+	// The current version of jQuery being used
+	jquery: core_version,
+
+	constructor: jQuery,
+	init: function( selector, context, rootjQuery ) {
+		var match, elem;
+
+		// HANDLE: $(""), $(null), $(undefined), $(false)
+		if ( !selector ) {
+			return this;
+		}
+
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+				// Assume that strings that start and end with <> are HTML and skip the regex check
+				match = [ null, selector, null ];
+
+			} else {
+				match = rquickExpr.exec( selector );
+			}
+
+			// Match html or make sure no context is specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] ) {
+					context = context instanceof jQuery ? context[0] : context;
+
+					// scripts is true for back-compat
+					jQuery.merge( this, jQuery.parseHTML(
+						match[1],
+						context && context.nodeType ? context.ownerDocument || context : document,
+						true
+					) );
+
+					// HANDLE: $(html, props)
+					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+						for ( match in context ) {
+							// Properties of context are called as methods if possible
+							if ( jQuery.isFunction( this[ match ] ) ) {
+								this[ match ]( context[ match ] );
+
+							// ...and otherwise set as attributes
+							} else {
+								this.attr( match, context[ match ] );
+							}
+						}
+					}
+
+					return this;
+
+				// HANDLE: $(#id)
+				} else {
+					elem = document.getElementById( match[2] );
+
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE and Opera return items
+						// by name instead of ID
+						if ( elem.id !== match[2] ) {
+							return rootjQuery.find( selector );
+						}
+
+						// Otherwise, we inject the element directly into the jQuery object
+						this.length = 1;
+						this[0] = elem;
+					}
+
+					this.context = document;
+					this.selector = selector;
+					return this;
+				}
+
+			// HANDLE: $(expr, $(...))
+			} else if ( !context || context.jquery ) {
+				return ( context || rootjQuery ).find( selector );
+
+			// HANDLE: $(expr, context)
+			// (which is just equivalent to: $(context).find(expr)
+			} else {
+				return this.constructor( context ).find( selector );
+			}
+
+		// HANDLE: $(DOMElement)
+		} else if ( selector.nodeType ) {
+			this.context = this[0] = selector;
+			this.length = 1;
+			return this;
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) ) {
+			return rootjQuery.ready( selector );
+		}
+
+		if ( selector.selector !== undefined ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return jQuery.makeArray( selector, this );
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The default length of a jQuery object is 0
+	length: 0,
+
+	toArray: function() {
+		return core_slice.call( this );
+	},
+
+	// Get the Nth element in the matched element set OR
+	// Get the whole matched element set as a clean array
+	get: function( num ) {
+		return num == null ?
+
+			// Return a 'clean' array
+			this.toArray() :
+
+			// Return just the object
+			( num < 0 ? this[ this.length + num ] : this[ num ] );
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems ) {
+
+		// Build a new jQuery matched element set
+		var ret = jQuery.merge( this.constructor(), elems );
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+		ret.context = this.context;
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Execute a callback for every element in the matched set.
+	// (You can seed the arguments with an array of args, but this is
+	// only used internally.)
+	each: function( callback, args ) {
+		return jQuery.each( this, callback, args );
+	},
+
+	ready: function( fn ) {
+		// Add the callback
+		jQuery.ready.promise().done( fn );
+
+		return this;
+	},
+
+	slice: function() {
+		return this.pushStack( core_slice.apply( this, arguments ) );
+	},
+
+	first: function() {
+		return this.eq( 0 );
+	},
+
+	last: function() {
+		return this.eq( -1 );
+	},
+
+	eq: function( i ) {
+		var len = this.length,
+			j = +i + ( i < 0 ? len : 0 );
+		return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function( elem, i ) {
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	end: function() {
+		return this.prevObject || this.constructor(null);
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: core_push,
+	sort: [].sort,
+	splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+	var src, copyIsArray, copy, name, options, clone,
+		target = arguments[0] || {},
+		i = 1,
+		length = arguments.length,
+		deep = false;
+
+	// Handle a deep copy situation
+	if ( typeof target === "boolean" ) {
+		deep = target;
+		target = arguments[1] || {};
+		// skip the boolean and the target
+		i = 2;
+	}
+
+	// Handle case when target is a string or something (possible in deep copy)
+	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+		target = {};
+	}
+
+	// extend jQuery itself if only one argument is passed
+	if ( length === i ) {
+		target = this;
+		--i;
+	}
+
+	for ( ; i < length; i++ ) {
+		// Only deal with non-null/undefined values
+		if ( (options = arguments[ i ]) != null ) {
+			// Extend the base object
+			for ( name in options ) {
+				src = target[ name ];
+				copy = options[ name ];
+
+				// Prevent never-ending loop
+				if ( target === copy ) {
+					continue;
+				}
+
+				// Recurse if we're merging plain objects or arrays
+				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+					if ( copyIsArray ) {
+						copyIsArray = false;
+						clone = src && jQuery.isArray(src) ? src : [];
+
+					} else {
+						clone = src && jQuery.isPlainObject(src) ? src : {};
+					}
+
+					// Never move original objects, clone them
+					target[ name ] = jQuery.extend( deep, clone, copy );
+
+				// Don't bring in undefined values
+				} else if ( copy !== undefined ) {
+					target[ name ] = copy;
+				}
+			}
+		}
+	}
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	// Unique for each copy of jQuery on the page
+	// Non-digits removed to match rinlinejQuery
+	expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
+
+	noConflict: function( deep ) {
+		if ( window.$ === jQuery ) {
+			window.$ = _$;
+		}
+
+		if ( deep && window.jQuery === jQuery ) {
+			window.jQuery = _jQuery;
+		}
+
+		return jQuery;
+	},
+
+	// Is the DOM ready to be used? Set to true once it occurs.
+	isReady: false,
+
+	// A counter to track how many items to wait for before
+	// the ready event fires. See #6781
+	readyWait: 1,
+
+	// Hold (or release) the ready event
+	holdReady: function( hold ) {
+		if ( hold ) {
+			jQuery.readyWait++;
+		} else {
+			jQuery.ready( true );
+		}
+	},
+
+	// Handle when the DOM is ready
+	ready: function( wait ) {
+
+		// Abort if there are pending holds or we're already ready
+		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+			return;
+		}
+
+		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+		if ( !document.body ) {
+			return setTimeout( jQuery.ready );
+		}
+
+		// Remember that the DOM is ready
+		jQuery.isReady = true;
+
+		// If a normal DOM Ready event fired, decrement, and wait if need be
+		if ( wait !== true && --jQuery.readyWait > 0 ) {
+			return;
+		}
+
+		// If there are functions bound, to execute
+		readyList.resolveWith( document, [ jQuery ] );
+
+		// Trigger any bound ready events
+		if ( jQuery.fn.trigger ) {
+			jQuery( document ).trigger("ready").off("ready");
+		}
+	},
+
+	// See test/unit/core.js for details concerning isFunction.
+	// Since version 1.3, DOM methods and functions like alert
+	// aren't supported. They return false on IE (#2968).
+	isFunction: function( obj ) {
+		return jQuery.type(obj) === "function";
+	},
+
+	isArray: Array.isArray || function( obj ) {
+		return jQuery.type(obj) === "array";
+	},
+
+	isWindow: function( obj ) {
+		/* jshint eqeqeq: false */
+		return obj != null && obj == obj.window;
+	},
+
+	isNumeric: function( obj ) {
+		return !isNaN( parseFloat(obj) ) && isFinite( obj );
+	},
+
+	type: function( obj ) {
+		if ( obj == null ) {
+			return String( obj );
+		}
+		return typeof obj === "object" || typeof obj === "function" ?
+			class2type[ core_toString.call(obj) ] || "object" :
+			typeof obj;
+	},
+
+	isPlainObject: function( obj ) {
+		var key;
+
+		// Must be an Object.
+		// Because of IE, we also have to check the presence of the constructor property.
+		// Make sure that DOM nodes and window objects don't pass through, as well
+		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+			return false;
+		}
+
+		try {
+			// Not own constructor property must be Object
+			if ( obj.constructor &&
+				!core_hasOwn.call(obj, "constructor") &&
+				!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+				return false;
+			}
+		} catch ( e ) {
+			// IE8,9 Will throw exceptions on certain host objects #9897
+			return false;
+		}
+
+		// Support: IE<9
+		// Handle iteration over inherited properties before own properties.
+		if ( jQuery.support.ownLast ) {
+			for ( key in obj ) {
+				return core_hasOwn.call( obj, key );
+			}
+		}
+
+		// Own properties are enumerated firstly, so to speed up,
+		// if last one is own, then all properties are own.
+		for ( key in obj ) {}
+
+		return key === undefined || core_hasOwn.call( obj, key );
+	},
+
+	isEmptyObject: function( obj ) {
+		var name;
+		for ( name in obj ) {
+			return false;
+		}
+		return true;
+	},
+
+	error: function( msg ) {
+		throw new Error( msg );
+	},
+
+	// data: string of html
+	// context (optional): If specified, the fragment will be created in this context, defaults to document
+	// keepScripts (optional): If true, will include scripts passed in the html string
+	parseHTML: function( data, context, keepScripts ) {
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		if ( typeof context === "boolean" ) {
+			keepScripts = context;
+			context = false;
+		}
+		context = context || document;
+
+		var parsed = rsingleTag.exec( data ),
+			scripts = !keepScripts && [];
+
+		// Single tag
+		if ( parsed ) {
+			return [ context.createElement( parsed[1] ) ];
+		}
+
+		parsed = jQuery.buildFragment( [ data ], context, scripts );
+		if ( scripts ) {
+			jQuery( scripts ).remove();
+		}
+		return jQuery.merge( [], parsed.childNodes );
+	},
+
+	parseJSON: function( data ) {
+		// Attempt to parse using the native JSON parser first
+		if ( window.JSON && window.JSON.parse ) {
+			return window.JSON.parse( data );
+		}
+
+		if ( data === null ) {
+			return data;
+		}
+
+		if ( typeof data === "string" ) {
+
+			// Make sure leading/trailing whitespace is removed (IE can't handle it)
+			data = jQuery.trim( data );
+
+			if ( data ) {
+				// Make sure the incoming data is actual JSON
+				// Logic borrowed from http://json.org/json2.js
+				if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+					.replace( rvalidtokens, "]" )
+					.replace( rvalidbraces, "")) ) {
+
+					return ( new Function( "return " + data ) )();
+				}
+			}
+		}
+
+		jQuery.error( "Invalid JSON: " + data );
+	},
+
+	// Cross-browser xml parsing
+	parseXML: function( data ) {
+		var xml, tmp;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		try {
+			if ( window.DOMParser ) { // Standard
+				tmp = new DOMParser();
+				xml = tmp.parseFromString( data , "text/xml" );
+			} else { // IE
+				xml = new ActiveXObject( "Microsoft.XMLDOM" );
+				xml.async = "false";
+				xml.loadXML( data );
+			}
+		} catch( e ) {
+			xml = undefined;
+		}
+		if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+			jQuery.error( "Invalid XML: " + data );
+		}
+		return xml;
+	},
+
+	noop: function() {},
+
+	// Evaluates a script in a global context
+	// Workarounds based on findings by Jim Driscoll
+	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+	globalEval: function( data ) {
+		if ( data && jQuery.trim( data ) ) {
+			// We use execScript on Internet Explorer
+			// We use an anonymous function so that context is window
+			// rather than jQuery in Firefox
+			( window.execScript || function( data ) {
+				window[ "eval" ].call( window, data );
+			} )( data );
+		}
+	},
+
+	// Convert dashed to camelCase; used by the css and data modules
+	// Microsoft forgot to hump their vendor prefix (#9572)
+	camelCase: function( string ) {
+		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+	},
+
+	// args is for internal usage only
+	each: function( obj, callback, args ) {
+		var value,
+			i = 0,
+			length = obj.length,
+			isArray = isArraylike( obj );
+
+		if ( args ) {
+			if ( isArray ) {
+				for ( ; i < length; i++ ) {
+					value = callback.apply( obj[ i ], args );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( i in obj ) {
+					value = callback.apply( obj[ i ], args );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			}
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( isArray ) {
+				for ( ; i < length; i++ ) {
+					value = callback.call( obj[ i ], i, obj[ i ] );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( i in obj ) {
+					value = callback.call( obj[ i ], i, obj[ i ] );
+
+					if ( value === false ) {
+						break;
+					}
+				}
+			}
+		}
+
+		return obj;
+	},
+
+	// Use native String.trim function wherever possible
+	trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+		function( text ) {
+			return text == null ?
+				"" :
+				core_trim.call( text );
+		} :
+
+		// Otherwise use our own trimming functionality
+		function( text ) {
+			return text == null ?
+				"" :
+				( text + "" ).replace( rtrim, "" );
+		},
+
+	// results is for internal usage only
+	makeArray: function( arr, results ) {
+		var ret = results || [];
+
+		if ( arr != null ) {
+			if ( isArraylike( Object(arr) ) ) {
+				jQuery.merge( ret,
+					typeof arr === "string" ?
+					[ arr ] : arr
+				);
+			} else {
+				core_push.call( ret, arr );
+			}
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, arr, i ) {
+		var len;
+
+		if ( arr ) {
+			if ( core_indexOf ) {
+				return core_indexOf.call( arr, elem, i );
+			}
+
+			len = arr.length;
+			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+			for ( ; i < len; i++ ) {
+				// Skip accessing in sparse arrays
+				if ( i in arr && arr[ i ] === elem ) {
+					return i;
+				}
+			}
+		}
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		var l = second.length,
+			i = first.length,
+			j = 0;
+
+		if ( typeof l === "number" ) {
+			for ( ; j < l; j++ ) {
+				first[ i++ ] = second[ j ];
+			}
+		} else {
+			while ( second[j] !== undefined ) {
+				first[ i++ ] = second[ j++ ];
+			}
+		}
+
+		first.length = i;
+
+		return first;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var retVal,
+			ret = [],
+			i = 0,
+			length = elems.length;
+		inv = !!inv;
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( ; i < length; i++ ) {
+			retVal = !!callback( elems[ i ], i );
+			if ( inv !== retVal ) {
+				ret.push( elems[ i ] );
+			}
+		}
+
+		return ret;
+	},
+
+	// arg is for internal usage only
+	map: function( elems, callback, arg ) {
+		var value,
+			i = 0,
+			length = elems.length,
+			isArray = isArraylike( elems ),
+			ret = [];
+
+		// Go through the array, translating each of the items to their
+		if ( isArray ) {
+			for ( ; i < length; i++ ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+
+		// Go through every key on the object,
+		} else {
+			for ( i in elems ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+		}
+
+		// Flatten any nested arrays
+		return core_concat.apply( [], ret );
+	},
+
+	// A global GUID counter for objects
+	guid: 1,
+
+	// Bind a function to a context, optionally partially applying any
+	// arguments.
+	proxy: function( fn, context ) {
+		var args, proxy, tmp;
+
+		if ( typeof context === "string" ) {
+			tmp = fn[ context ];
+			context = fn;
+			fn = tmp;
+		}
+
+		// Quick check to determine if target is callable, in the spec
+		// this throws a TypeError, but we will just return undefined.
+		if ( !jQuery.isFunction( fn ) ) {
+			return undefined;
+		}
+
+		// Simulated bind
+		args = core_slice.call( arguments, 2 );
+		proxy = function() {
+			return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
+		};
+
+		// Set the guid of unique handler to the same of original handler, so it can be removed
+		proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+		return proxy;
+	},
+
+	// Multifunctional method to get and set values of a collection
+	// The value/s can optionally be executed if it's a function
+	access: function( elems, fn, key, value, chainable, emptyGet, raw ) {
+		var i = 0,
+			length = elems.length,
+			bulk = key == null;
+
+		// Sets many values
+		if ( jQuery.type( key ) === "object" ) {
+			chainable = true;
+			for ( i in key ) {
+				jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
+			}
+
+		// Sets one value
+		} else if ( value !== undefined ) {
+			chainable = true;
+
+			if ( !jQuery.isFunction( value ) ) {
+				raw = true;
+			}
+
+			if ( bulk ) {
+				// Bulk operations run against the entire set
+				if ( raw ) {
+					fn.call( elems, value );
+					fn = null;
+
+				// ...except when executing function values
+				} else {
+					bulk = fn;
+					fn = function( elem, key, value ) {
+						return bulk.call( jQuery( elem ), value );
+					};
+				}
+			}
+
+			if ( fn ) {
+				for ( ; i < length; i++ ) {
+					fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
+				}
+			}
+		}
+
+		return chainable ?
+			elems :
+
+			// Gets
+			bulk ?
+				fn.call( elems ) :
+				length ? fn( elems[0], key ) : emptyGet;
+	},
+
+	now: function() {
+		return ( new Date() ).getTime();
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations.
+	// Note: this method belongs to the css module but it's needed here for the support module.
+	// If support gets modularized, this method should be moved back to the css module.
+	swap: function( elem, options, callback, args ) {
+		var ret, name,
+			old = {};
+
+		// Remember the old values, and insert the new ones
+		for ( name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		ret = callback.apply( elem, args || [] );
+
+		// Revert the old values
+		for ( name in options ) {
+			elem.style[ name ] = old[ name ];
+		}
+
+		return ret;
+	}
+});
+
+jQuery.ready.promise = function( obj ) {
+	if ( !readyList ) {
+
+		readyList = jQuery.Deferred();
+
+		// Catch cases where $(document).ready() is called after the browser event has already occurred.
+		// we once tried to use readyState "interactive" here, but it caused issues like the one
+		// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+		if ( document.readyState === "complete" ) {
+			// Handle it asynchronously to allow scripts the opportunity to delay ready
+			setTimeout( jQuery.ready );
+
+		// Standards-based browsers support DOMContentLoaded
+		} else if ( document.addEventListener ) {
+			// Use the handy event callback
+			document.addEventListener( "DOMContentLoaded", completed, false );
+
+			// A fallback to window.onload, that will always work
+			window.addEventListener( "load", completed, false );
+
+		// If IE event model is used
+		} else {
+			// Ensure firing before onload, maybe late but safe also for iframes
+			document.attachEvent( "onreadystatechange", completed );
+
+			// A fallback to window.onload, that will always work
+			window.attachEvent( "onload", completed );
+
+			// If IE and not a frame
+			// continually check to see if the document is ready
+			var top = false;
+
+			try {
+				top = window.frameElement == null && document.documentElement;
+			} catch(e) {}
+
+			if ( top && top.doScroll ) {
+				(function doScrollCheck() {
+					if ( !jQuery.isReady ) {
+
+						try {
+							// Use the trick by Diego Perini
+							// http://javascript.nwbox.com/IEContentLoaded/
+							top.doScroll("left");
+						} catch(e) {
+							return setTimeout( doScrollCheck, 50 );
+						}
+
+						// detach all dom ready events
+						detach();
+
+						// and execute any waiting functions
+						jQuery.ready();
+					}
+				})();
+			}
+		}
+	}
+	return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
+	class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+function isArraylike( obj ) {
+	var length = obj.length,
+		type = jQuery.type( obj );
+
+	if ( jQuery.isWindow( obj ) ) {
+		return false;
+	}
+
+	if ( obj.nodeType === 1 && length ) {
+		return true;
+	}
+
+	return type === "array" || type !== "function" &&
+		( length === 0 ||
+		typeof length === "number" && length > 0 && ( length - 1 ) in obj );
+}
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+/*!
+ * Sizzle CSS Selector Engine v1.10.2
+ * http://sizzlejs.com/
+ *
+ * Copyright 2013 jQuery Foundation, Inc. and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2013-07-03
+ */
+(function( window, undefined ) {
+
+var i,
+	support,
+	cachedruns,
+	Expr,
+	getText,
+	isXML,
+	compile,
+	outermostContext,
+	sortInput,
+
+	// Local document vars
+	setDocument,
+	document,
+	docElem,
+	documentIsHTML,
+	rbuggyQSA,
+	rbuggyMatches,
+	matches,
+	contains,
+
+	// Instance-specific data
+	expando = "sizzle" + -(new Date()),
+	preferredDoc = window.document,
+	dirruns = 0,
+	done = 0,
+	classCache = createCache(),
+	tokenCache = createCache(),
+	compilerCache = createCache(),
+	hasDuplicate = false,
+	sortOrder = function( a, b ) {
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+		return 0;
+	},
+
+	// General-purpose constants
+	strundefined = typeof undefined,
+	MAX_NEGATIVE = 1 << 31,
+
+	// Instance methods
+	hasOwn = ({}).hasOwnProperty,
+	arr = [],
+	pop = arr.pop,
+	push_native = arr.push,
+	push = arr.push,
+	slice = arr.slice,
+	// Use a stripped-down indexOf if we can't use a native one
+	indexOf = arr.indexOf || function( elem ) {
+		var i = 0,
+			len = this.length;
+		for ( ; i < len; i++ ) {
+			if ( this[i] === elem ) {
+				return i;
+			}
+		}
+		return -1;
+	},
+
+	booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
+
+	// Regular expressions
+
+	// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+	whitespace = "[\\x20\\t\\r\\n\\f]",
+	// http://www.w3.org/TR/css3-syntax/#characters
+	characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+	// Loosely modeled on CSS identifier characters
+	// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
+	// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+	identifier = characterEncoding.replace( "w", "w#" ),
+
+	// Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+	attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+		"*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+	// Prefer arguments quoted,
+	//   then not containing pseudos/brackets,
+	//   then attribute selectors/non-parenthetical expressions,
+	//   then anything else
+	// These preferences are here to reduce the number of selectors
+	//   needing tokenize in the PSEUDO preFilter
+	pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)",
+
+	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+	rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
+
+	rsibling = new RegExp( whitespace + "*[+~]" ),
+	rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ),
+
+	rpseudo = new RegExp( pseudos ),
+	ridentifier = new RegExp( "^" + identifier + "$" ),
+
+	matchExpr = {
+		"ID": new RegExp( "^#(" + characterEncoding + ")" ),
+		"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+		"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+		"ATTR": new RegExp( "^" + attributes ),
+		"PSEUDO": new RegExp( "^" + pseudos ),
+		"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+		"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+		// For use in libraries implementing .is()
+		// We use this for POS matching in `select`
+		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+			whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+	},
+
+	rnative = /^[^{]+\{\s*\[native \w/,
+
+	// Easily-parseable/retrievable ID or TAG or CLASS selectors
+	rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+	rinputs = /^(?:input|select|textarea|button)$/i,
+	rheader = /^h\d$/i,
+
+	rescape = /'|\\/g,
+
+	// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+	runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
+	funescape = function( _, escaped, escapedWhitespace ) {
+		var high = "0x" + escaped - 0x10000;
+		// NaN means non-codepoint
+		// Support: Firefox
+		// Workaround erroneous numeric interpretation of +"0x"
+		return high !== high || escapedWhitespace ?
+			escaped :
+			// BMP codepoint
+			high < 0 ?
+				String.fromCharCode( high + 0x10000 ) :
+				// Supplemental Plane codepoint (surrogate pair)
+				String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+	};
+
+// Optimize for push.apply( _, NodeList )
+try {
+	push.apply(
+		(arr = slice.call( preferredDoc.childNodes )),
+		preferredDoc.childNodes
+	);
+	// Support: Android<4.0
+	// Detect silently failing push.apply
+	arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+	push = { apply: arr.length ?
+
+		// Leverage slice if possible
+		function( target, els ) {
+			push_native.apply( target, slice.call(els) );
+		} :
+
+		// Support: IE<9
+		// Otherwise append directly
+		function( target, els ) {
+			var j = target.length,
+				i = 0;
+			// Can't trust NodeList.length
+			while ( (target[j++] = els[i++]) ) {}
+			target.length = j - 1;
+		}
+	};
+}
+
+function Sizzle( selector, context, results, seed ) {
+	var match, elem, m, nodeType,
+		// QSA vars
+		i, groups, old, nid, newContext, newSelector;
+
+	if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+		setDocument( context );
+	}
+
+	context = context || document;
+	results = results || [];
+
+	if ( !selector || typeof selector !== "string" ) {
+		return results;
+	}
+
+	if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
+		return [];
+	}
+
+	if ( documentIsHTML && !seed ) {
+
+		// Shortcuts
+		if ( (match = rquickExpr.exec( selector )) ) {
+			// Speed-up: Sizzle("#ID")
+			if ( (m = match[1]) ) {
+				if ( nodeType === 9 ) {
+					elem = context.getElementById( m );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE, Opera, and Webkit return items
+						// by name instead of ID
+						if ( elem.id === m ) {
+							results.push( elem );
+							return results;
+						}
+					} else {
+						return results;
+					}
+				} else {
+					// Context is not a document
+					if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+						contains( context, elem ) && elem.id === m ) {
+						results.push( elem );
+						return results;
+					}
+				}
+
+			// Speed-up: Sizzle("TAG")
+			} else if ( match[2] ) {
+				push.apply( results, context.getElementsByTagName( selector ) );
+				return results;
+
+			// Speed-up: Sizzle(".CLASS")
+			} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
+				push.apply( results, context.getElementsByClassName( m ) );
+				return results;
+			}
+		}
+
+		// QSA path
+		if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+			nid = old = expando;
+			newContext = context;
+			newSelector = nodeType === 9 && selector;
+
+			// qSA works strangely on Element-rooted queries
+			// We can work around this by specifying an extra ID on the root
+			// and working up from there (Thanks to Andrew Dupont for the technique)
+			// IE 8 doesn't work on object elements
+			if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+				groups = tokenize( selector );
+
+				if ( (old = context.getAttribute("id")) ) {
+					nid = old.replace( rescape, "\\$&" );
+				} else {
+					context.setAttribute( "id", nid );
+				}
+				nid = "[id='" + nid + "'] ";
+
+				i = groups.length;
+				while ( i-- ) {
+					groups[i] = nid + toSelector( groups[i] );
+				}
+				newContext = rsibling.test( selector ) && context.parentNode || context;
+				newSelector = groups.join(",");
+			}
+
+			if ( newSelector ) {
+				try {
+					push.apply( results,
+						newContext.querySelectorAll( newSelector )
+					);
+					return results;
+				} catch(qsaError) {
+				} finally {
+					if ( !old ) {
+						context.removeAttribute("id");
+					}
+				}
+			}
+		}
+	}
+
+	// All others
+	return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
+ *	property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ *	deleting the oldest entry
+ */
+function createCache() {
+	var keys = [];
+
+	function cache( key, value ) {
+		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+		if ( keys.push( key += " " ) > Expr.cacheLength ) {
+			// Only keep the most recent entries
+			delete cache[ keys.shift() ];
+		}
+		return (cache[ key ] = value);
+	}
+	return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+	fn[ expando ] = true;
+	return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+	var div = document.createElement("div");
+
+	try {
+		return !!fn( div );
+	} catch (e) {
+		return false;
+	} finally {
+		// Remove from its parent by default
+		if ( div.parentNode ) {
+			div.parentNode.removeChild( div );
+		}
+		// release memory in IE
+		div = null;
+	}
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+	var arr = attrs.split("|"),
+		i = attrs.length;
+
+	while ( i-- ) {
+		Expr.attrHandle[ arr[i] ] = handler;
+	}
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+	var cur = b && a,
+		diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+			( ~b.sourceIndex || MAX_NEGATIVE ) -
+			( ~a.sourceIndex || MAX_NEGATIVE );
+
+	// Use IE sourceIndex if available on both nodes
+	if ( diff ) {
+		return diff;
+	}
+
+	// Check if b follows a
+	if ( cur ) {
+		while ( (cur = cur.nextSibling) ) {
+			if ( cur === b ) {
+				return -1;
+			}
+		}
+	}
+
+	return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return name === "input" && elem.type === type;
+	};
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return (name === "input" || name === "button") && elem.type === type;
+	};
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+	return markFunction(function( argument ) {
+		argument = +argument;
+		return markFunction(function( seed, matches ) {
+			var j,
+				matchIndexes = fn( [], seed.length, argument ),
+				i = matchIndexes.length;
+
+			// Match elements found at the specified indexes
+			while ( i-- ) {
+				if ( seed[ (j = matchIndexes[i]) ] ) {
+					seed[j] = !(matches[j] = seed[j]);
+				}
+			}
+		});
+	});
+}
+
+/**
+ * Detect xml
+ * @param {Element|Object} elem An element or a document
+ */
+isXML = Sizzle.isXML = function( elem ) {
+	// documentElement is verified for cases where it doesn't yet exist
+	// (such as loading iframes in IE - #4833)
+	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+	return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+	var doc = node ? node.ownerDocument || node : preferredDoc,
+		parent = doc.defaultView;
+
+	// If no document and documentElement is available, return
+	if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+		return document;
+	}
+
+	// Set our document
+	document = doc;
+	docElem = doc.documentElement;
+
+	// Support tests
+	documentIsHTML = !isXML( doc );
+
+	// Support: IE>8
+	// If iframe document is assigned to "document" variable and if iframe has been reloaded,
+	// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
+	// IE6-8 do not support the defaultView property so parent will be undefined
+	if ( parent && parent.attachEvent && parent !== parent.top ) {
+		parent.attachEvent( "onbeforeunload", function() {
+			setDocument();
+		});
+	}
+
+	/* Attributes
+	---------------------------------------------------------------------- */
+
+	// Support: IE<8
+	// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
+	support.attributes = assert(function( div ) {
+		div.className = "i";
+		return !div.getAttribute("className");
+	});
+
+	/* getElement(s)By*
+	---------------------------------------------------------------------- */
+
+	// Check if getElementsByTagName("*") returns only elements
+	support.getElementsByTagName = assert(function( div ) {
+		div.appendChild( doc.createComment("") );
+		return !div.getElementsByTagName("*").length;
+	});
+
+	// Check if getElementsByClassName can be trusted
+	support.getElementsByClassName = assert(function( div ) {
+		div.innerHTML = "<div class='a'></div><div class='a i'></div>";
+
+		// Support: Safari<4
+		// Catch class over-caching
+		div.firstChild.className = "i";
+		// Support: Opera<10
+		// Catch gEBCN failure to find non-leading classes
+		return div.getElementsByClassName("i").length === 2;
+	});
+
+	// Support: IE<10
+	// Check if getElementById returns elements by name
+	// The broken getElementById methods don't pick up programatically-set names,
+	// so use a roundabout getElementsByName test
+	support.getById = assert(function( div ) {
+		docElem.appendChild( div ).id = expando;
+		return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
+	});
+
+	// ID find and filter
+	if ( support.getById ) {
+		Expr.find["ID"] = function( id, context ) {
+			if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+				var m = context.getElementById( id );
+				// Check parentNode to catch when Blackberry 4.6 returns
+				// nodes that are no longer in the document #6963
+				return m && m.parentNode ? [m] : [];
+			}
+		};
+		Expr.filter["ID"] = function( id ) {
+			var attrId = id.replace( runescape, funescape );
+			return function( elem ) {
+				return elem.getAttribute("id") === attrId;
+			};
+		};
+	} else {
+		// Support: IE6/7
+		// getElementById is not reliable as a find shortcut
+		delete Expr.find["ID"];
+
+		Expr.filter["ID"] =  function( id ) {
+			var attrId = id.replace( runescape, funescape );
+			return function( elem ) {
+				var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+				return node && node.value === attrId;
+			};
+		};
+	}
+
+	// Tag
+	Expr.find["TAG"] = support.getElementsByTagName ?
+		function( tag, context ) {
+			if ( typeof context.getElementsByTagName !== strundefined ) {
+				return context.getElementsByTagName( tag );
+			}
+		} :
+		function( tag, context ) {
+			var elem,
+				tmp = [],
+				i = 0,
+				results = context.getElementsByTagName( tag );
+
+			// Filter out possible comments
+			if ( tag === "*" ) {
+				while ( (elem = results[i++]) ) {
+					if ( elem.nodeType === 1 ) {
+						tmp.push( elem );
+					}
+				}
+
+				return tmp;
+			}
+			return results;
+		};
+
+	// Class
+	Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+		if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
+			return context.getElementsByClassName( className );
+		}
+	};
+
+	/* QSA/matchesSelector
+	---------------------------------------------------------------------- */
+
+	// QSA and matchesSelector support
+
+	// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+	rbuggyMatches = [];
+
+	// qSa(:focus) reports false when true (Chrome 21)
+	// We allow this because of a bug in IE8/9 that throws an error
+	// whenever `document.activeElement` is accessed on an iframe
+	// So, we allow :focus to pass through QSA all the time to avoid the IE error
+	// See http://bugs.jquery.com/ticket/13378
+	rbuggyQSA = [];
+
+	if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
+		// Build QSA regex
+		// Regex strategy adopted from Diego Perini
+		assert(function( div ) {
+			// Select is set to empty string on purpose
+			// This is to test IE's treatment of not explicitly
+			// setting a boolean content attribute,
+			// since its presence should be enough
+			// http://bugs.jquery.com/ticket/12359
+			div.innerHTML = "<select><option selected=''></option></select>";
+
+			// Support: IE8
+			// Boolean attributes and "value" are not treated correctly
+			if ( !div.querySelectorAll("[selected]").length ) {
+				rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+			}
+
+			// Webkit/Opera - :checked should return selected option elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			// IE8 throws error here and will not see later tests
+			if ( !div.querySelectorAll(":checked").length ) {
+				rbuggyQSA.push(":checked");
+			}
+		});
+
+		assert(function( div ) {
+
+			// Support: Opera 10-12/IE8
+			// ^= $= *= and empty values
+			// Should not select anything
+			// Support: Windows 8 Native Apps
+			// The type attribute is restricted during .innerHTML assignment
+			var input = doc.createElement("input");
+			input.setAttribute( "type", "hidden" );
+			div.appendChild( input ).setAttribute( "t", "" );
+
+			if ( div.querySelectorAll("[t^='']").length ) {
+				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+			}
+
+			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+			// IE8 throws error here and will not see later tests
+			if ( !div.querySelectorAll(":enabled").length ) {
+				rbuggyQSA.push( ":enabled", ":disabled" );
+			}
+
+			// Opera 10-11 does not throw on post-comma invalid pseudos
+			div.querySelectorAll("*,:x");
+			rbuggyQSA.push(",.*:");
+		});
+	}
+
+	if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector ||
+		docElem.mozMatchesSelector ||
+		docElem.oMatchesSelector ||
+		docElem.msMatchesSelector) )) ) {
+
+		assert(function( div ) {
+			// Check to see if it's possible to do matchesSelector
+			// on a disconnected node (IE 9)
+			support.disconnectedMatch = matches.call( div, "div" );
+
+			// This should fail with an exception
+			// Gecko does not error, returns false instead
+			matches.call( div, "[s!='']:x" );
+			rbuggyMatches.push( "!=", pseudos );
+		});
+	}
+
+	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
+	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+	/* Contains
+	---------------------------------------------------------------------- */
+
+	// Element contains another
+	// Purposefully does not implement inclusive descendent
+	// As in, an element does not contain itself
+	contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ?
+		function( a, b ) {
+			var adown = a.nodeType === 9 ? a.documentElement : a,
+				bup = b && b.parentNode;
+			return a === bup || !!( bup && bup.nodeType === 1 && (
+				adown.contains ?
+					adown.contains( bup ) :
+					a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+			));
+		} :
+		function( a, b ) {
+			if ( b ) {
+				while ( (b = b.parentNode) ) {
+					if ( b === a ) {
+						return true;
+					}
+				}
+			}
+			return false;
+		};
+
+	/* Sorting
+	---------------------------------------------------------------------- */
+
+	// Document order sorting
+	sortOrder = docElem.compareDocumentPosition ?
+	function( a, b ) {
+
+		// Flag for duplicate removal
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+
+		var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b );
+
+		if ( compare ) {
+			// Disconnected nodes
+			if ( compare & 1 ||
+				(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
+
+				// Choose the first element that is related to our preferred document
+				if ( a === doc || contains(preferredDoc, a) ) {
+					return -1;
+				}
+				if ( b === doc || contains(preferredDoc, b) ) {
+					return 1;
+				}
+
+				// Maintain original order
+				return sortInput ?
+					( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+					0;
+			}
+
+			return compare & 4 ? -1 : 1;
+		}
+
+		// Not directly comparable, sort on existence of method
+		return a.compareDocumentPosition ? -1 : 1;
+	} :
+	function( a, b ) {
+		var cur,
+			i = 0,
+			aup = a.parentNode,
+			bup = b.parentNode,
+			ap = [ a ],
+			bp = [ b ];
+
+		// Exit early if the nodes are identical
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+
+		// Parentless nodes are either documents or disconnected
+		} else if ( !aup || !bup ) {
+			return a === doc ? -1 :
+				b === doc ? 1 :
+				aup ? -1 :
+				bup ? 1 :
+				sortInput ?
+				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+				0;
+
+		// If the nodes are siblings, we can do a quick check
+		} else if ( aup === bup ) {
+			return siblingCheck( a, b );
+		}
+
+		// Otherwise we need full lists of their ancestors for comparison
+		cur = a;
+		while ( (cur = cur.parentNode) ) {
+			ap.unshift( cur );
+		}
+		cur = b;
+		while ( (cur = cur.parentNode) ) {
+			bp.unshift( cur );
+		}
+
+		// Walk down the tree looking for a discrepancy
+		while ( ap[i] === bp[i] ) {
+			i++;
+		}
+
+		return i ?
+			// Do a sibling check if the nodes have a common ancestor
+			siblingCheck( ap[i], bp[i] ) :
+
+			// Otherwise nodes in our document sort first
+			ap[i] === preferredDoc ? -1 :
+			bp[i] === preferredDoc ? 1 :
+			0;
+	};
+
+	return doc;
+};
+
+Sizzle.matches = function( expr, elements ) {
+	return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+	// Set document vars if needed
+	if ( ( elem.ownerDocument || elem ) !== document ) {
+		setDocument( elem );
+	}
+
+	// Make sure that attribute selectors are quoted
+	expr = expr.replace( rattributeQuotes, "='$1']" );
+
+	if ( support.matchesSelector && documentIsHTML &&
+		( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+		( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
+
+		try {
+			var ret = matches.call( elem, expr );
+
+			// IE 9's matchesSelector returns false on disconnected nodes
+			if ( ret || support.disconnectedMatch ||
+					// As well, disconnected nodes are said to be in a document
+					// fragment in IE 9
+					elem.document && elem.document.nodeType !== 11 ) {
+				return ret;
+			}
+		} catch(e) {}
+	}
+
+	return Sizzle( expr, document, null, [elem] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+	// Set document vars if needed
+	if ( ( context.ownerDocument || context ) !== document ) {
+		setDocument( context );
+	}
+	return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+	// Set document vars if needed
+	if ( ( elem.ownerDocument || elem ) !== document ) {
+		setDocument( elem );
+	}
+
+	var fn = Expr.attrHandle[ name.toLowerCase() ],
+		// Don't get fooled by Object.prototype properties (jQuery #13807)
+		val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+			fn( elem, name, !documentIsHTML ) :
+			undefined;
+
+	return val === undefined ?
+		support.attributes || !documentIsHTML ?
+			elem.getAttribute( name ) :
+			(val = elem.getAttributeNode(name)) && val.specified ?
+				val.value :
+				null :
+		val;
+};
+
+Sizzle.error = function( msg ) {
+	throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+	var elem,
+		duplicates = [],
+		j = 0,
+		i = 0;
+
+	// Unless we *know* we can detect duplicates, assume their presence
+	hasDuplicate = !support.detectDuplicates;
+	sortInput = !support.sortStable && results.slice( 0 );
+	results.sort( sortOrder );
+
+	if ( hasDuplicate ) {
+		while ( (elem = results[i++]) ) {
+			if ( elem === results[ i ] ) {
+				j = duplicates.push( i );
+			}
+		}
+		while ( j-- ) {
+			results.splice( duplicates[ j ], 1 );
+		}
+	}
+
+	return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+	var node,
+		ret = "",
+		i = 0,
+		nodeType = elem.nodeType;
+
+	if ( !nodeType ) {
+		// If no nodeType, this is expected to be an array
+		for ( ; (node = elem[i]); i++ ) {
+			// Do not traverse comment nodes
+			ret += getText( node );
+		}
+	} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+		// Use textContent for elements
+		// innerText usage removed for consistency of new lines (see #11153)
+		if ( typeof elem.textContent === "string" ) {
+			return elem.textContent;
+		} else {
+			// Traverse its children
+			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+				ret += getText( elem );
+			}
+		}
+	} else if ( nodeType === 3 || nodeType === 4 ) {
+		return elem.nodeValue;
+	}
+	// Do not include comment or processing instruction nodes
+
+	return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+	// Can be adjusted by the user
+	cacheLength: 50,
+
+	createPseudo: markFunction,
+
+	match: matchExpr,
+
+	attrHandle: {},
+
+	find: {},
+
+	relative: {
+		">": { dir: "parentNode", first: true },
+		" ": { dir: "parentNode" },
+		"+": { dir: "previousSibling", first: true },
+		"~": { dir: "previousSibling" }
+	},
+
+	preFilter: {
+		"ATTR": function( match ) {
+			match[1] = match[1].replace( runescape, funescape );
+
+			// Move the given value to match[3] whether quoted or unquoted
+			match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape );
+
+			if ( match[2] === "~=" ) {
+				match[3] = " " + match[3] + " ";
+			}
+
+			return match.slice( 0, 4 );
+		},
+
+		"CHILD": function( match ) {
+			/* matches from matchExpr["CHILD"]
+				1 type (only|nth|...)
+				2 what (child|of-type)
+				3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+				4 xn-component of xn+y argument ([+-]?\d*n|)
+				5 sign of xn-component
+				6 x of xn-component
+				7 sign of y-component
+				8 y of y-component
+			*/
+			match[1] = match[1].toLowerCase();
+
+			if ( match[1].slice( 0, 3 ) === "nth" ) {
+				// nth-* requires argument
+				if ( !match[3] ) {
+					Sizzle.error( match[0] );
+				}
+
+				// numeric x and y parameters for Expr.filter.CHILD
+				// remember that false/true cast respectively to 0/1
+				match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+				match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+			// other types prohibit arguments
+			} else if ( match[3] ) {
+				Sizzle.error( match[0] );
+			}
+
+			return match;
+		},
+
+		"PSEUDO": function( match ) {
+			var excess,
+				unquoted = !match[5] && match[2];
+
+			if ( matchExpr["CHILD"].test( match[0] ) ) {
+				return null;
+			}
+
+			// Accept quoted arguments as-is
+			if ( match[3] && match[4] !== undefined ) {
+				match[2] = match[4];
+
+			// Strip excess characters from unquoted arguments
+			} else if ( unquoted && rpseudo.test( unquoted ) &&
+				// Get excess from tokenize (recursively)
+				(excess = tokenize( unquoted, true )) &&
+				// advance to the next closing parenthesis
+				(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+				// excess is a negative index
+				match[0] = match[0].slice( 0, excess );
+				match[2] = unquoted.slice( 0, excess );
+			}
+
+			// Return only captures needed by the pseudo filter method (type and argument)
+			return match.slice( 0, 3 );
+		}
+	},
+
+	filter: {
+
+		"TAG": function( nodeNameSelector ) {
+			var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+			return nodeNameSelector === "*" ?
+				function() { return true; } :
+				function( elem ) {
+					return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+				};
+		},
+
+		"CLASS": function( className ) {
+			var pattern = classCache[ className + " " ];
+
+			return pattern ||
+				(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+				classCache( className, function( elem ) {
+					return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
+				});
+		},
+
+		"ATTR": function( name, operator, check ) {
+			return function( elem ) {
+				var result = Sizzle.attr( elem, name );
+
+				if ( result == null ) {
+					return operator === "!=";
+				}
+				if ( !operator ) {
+					return true;
+				}
+
+				result += "";
+
+				return operator === "=" ? result === check :
+					operator === "!=" ? result !== check :
+					operator === "^=" ? check && result.indexOf( check ) === 0 :
+					operator === "*=" ? check && result.indexOf( check ) > -1 :
+					operator === "$=" ? check && result.slice( -check.length ) === check :
+					operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+					operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+					false;
+			};
+		},
+
+		"CHILD": function( type, what, argument, first, last ) {
+			var simple = type.slice( 0, 3 ) !== "nth",
+				forward = type.slice( -4 ) !== "last",
+				ofType = what === "of-type";
+
+			return first === 1 && last === 0 ?
+
+				// Shortcut for :nth-*(n)
+				function( elem ) {
+					return !!elem.parentNode;
+				} :
+
+				function( elem, context, xml ) {
+					var cache, outerCache, node, diff, nodeIndex, start,
+						dir = simple !== forward ? "nextSibling" : "previousSibling",
+						parent = elem.parentNode,
+						name = ofType && elem.nodeName.toLowerCase(),
+						useCache = !xml && !ofType;
+
+					if ( parent ) {
+
+						// :(first|last|only)-(child|of-type)
+						if ( simple ) {
+							while ( dir ) {
+								node = elem;
+								while ( (node = node[ dir ]) ) {
+									if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
+										return false;
+									}
+								}
+								// Reverse direction for :only-* (if we haven't yet done so)
+								start = dir = type === "only" && !start && "nextSibling";
+							}
+							return true;
+						}
+
+						start = [ forward ? parent.firstChild : parent.lastChild ];
+
+						// non-xml :nth-child(...) stores cache data on `parent`
+						if ( forward && useCache ) {
+							// Seek `elem` from a previously-cached index
+							outerCache = parent[ expando ] || (parent[ expando ] = {});
+							cache = outerCache[ type ] || [];
+							nodeIndex = cache[0] === dirruns && cache[1];
+							diff = cache[0] === dirruns && cache[2];
+							node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+							while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+								// Fallback to seeking `elem` from the start
+								(diff = nodeIndex = 0) || start.pop()) ) {
+
+								// When found, cache indexes on `parent` and break
+								if ( node.nodeType === 1 && ++diff && node === elem ) {
+									outerCache[ type ] = [ dirruns, nodeIndex, diff ];
+									break;
+								}
+							}
+
+						// Use previously-cached element index if available
+						} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
+							diff = cache[1];
+
+						// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
+						} else {
+							// Use the same loop as above to seek `elem` from the start
+							while ( (node = ++nodeIndex && node && node[ dir ] ||
+								(diff = nodeIndex = 0) || start.pop()) ) {
+
+								if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
+									// Cache the index of each encountered element
+									if ( useCache ) {
+										(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
+									}
+
+									if ( node === elem ) {
+										break;
+									}
+								}
+							}
+						}
+
+						// Incorporate the offset, then check against cycle size
+						diff -= last;
+						return diff === first || ( diff % first === 0 && diff / first >= 0 );
+					}
+				};
+		},
+
+		"PSEUDO": function( pseudo, argument ) {
+			// pseudo-class names are case-insensitive
+			// http://www.w3.org/TR/selectors/#pseudo-classes
+			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+			// Remember that setFilters inherits from pseudos
+			var args,
+				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+					Sizzle.error( "unsupported pseudo: " + pseudo );
+
+			// The user may use createPseudo to indicate that
+			// arguments are needed to create the filter function
+			// just as Sizzle does
+			if ( fn[ expando ] ) {
+				return fn( argument );
+			}
+
+			// But maintain support for old signatures
+			if ( fn.length > 1 ) {
+				args = [ pseudo, pseudo, "", argument ];
+				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+					markFunction(function( seed, matches ) {
+						var idx,
+							matched = fn( seed, argument ),
+							i = matched.length;
+						while ( i-- ) {
+							idx = indexOf.call( seed, matched[i] );
+							seed[ idx ] = !( matches[ idx ] = matched[i] );
+						}
+					}) :
+					function( elem ) {
+						return fn( elem, 0, args );
+					};
+			}
+
+			return fn;
+		}
+	},
+
+	pseudos: {
+		// Potentially complex pseudos
+		"not": markFunction(function( selector ) {
+			// Trim the selector passed to compile
+			// to avoid treating leading and trailing
+			// spaces as combinators
+			var input = [],
+				results = [],
+				matcher = compile( selector.replace( rtrim, "$1" ) );
+
+			return matcher[ expando ] ?
+				markFunction(function( seed, matches, context, xml ) {
+					var elem,
+						unmatched = matcher( seed, null, xml, [] ),
+						i = seed.length;
+
+					// Match elements unmatched by `matcher`
+					while ( i-- ) {
+						if ( (elem = unmatched[i]) ) {
+							seed[i] = !(matches[i] = elem);
+						}
+					}
+				}) :
+				function( elem, context, xml ) {
+					input[0] = elem;
+					matcher( input, null, xml, results );
+					return !results.pop();
+				};
+		}),
+
+		"has": markFunction(function( selector ) {
+			return function( elem ) {
+				return Sizzle( selector, elem ).length > 0;
+			};
+		}),
+
+		"contains": markFunction(function( text ) {
+			return function( elem ) {
+				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+			};
+		}),
+
+		// "Whether an element is represented by a :lang() selector
+		// is based solely on the element's language value
+		// being equal to the identifier C,
+		// or beginning with the identifier C immediately followed by "-".
+		// The matching of C against the element's language value is performed case-insensitively.
+		// The identifier C does not have to be a valid language name."
+		// http://www.w3.org/TR/selectors/#lang-pseudo
+		"lang": markFunction( function( lang ) {
+			// lang value must be a valid identifier
+			if ( !ridentifier.test(lang || "") ) {
+				Sizzle.error( "unsupported lang: " + lang );
+			}
+			lang = lang.replace( runescape, funescape ).toLowerCase();
+			return function( elem ) {
+				var elemLang;
+				do {
+					if ( (elemLang = documentIsHTML ?
+						elem.lang :
+						elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+						elemLang = elemLang.toLowerCase();
+						return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+					}
+				} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+				return false;
+			};
+		}),
+
+		// Miscellaneous
+		"target": function( elem ) {
+			var hash = window.location && window.location.hash;
+			return hash && hash.slice( 1 ) === elem.id;
+		},
+
+		"root": function( elem ) {
+			return elem === docElem;
+		},
+
+		"focus": function( elem ) {
+			return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+		},
+
+		// Boolean properties
+		"enabled": function( elem ) {
+			return elem.disabled === false;
+		},
+
+		"disabled": function( elem ) {
+			return elem.disabled === true;
+		},
+
+		"checked": function( elem ) {
+			// In CSS3, :checked should return both checked and selected elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			var nodeName = elem.nodeName.toLowerCase();
+			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+		},
+
+		"selected": function( elem ) {
+			// Accessing this property makes selected-by-default
+			// options in Safari work properly
+			if ( elem.parentNode ) {
+				elem.parentNode.selectedIndex;
+			}
+
+			return elem.selected === true;
+		},
+
+		// Contents
+		"empty": function( elem ) {
+			// http://www.w3.org/TR/selectors/#empty-pseudo
+			// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+			//   not comment, processing instructions, or others
+			// Thanks to Diego Perini for the nodeName shortcut
+			//   Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+				if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) {
+					return false;
+				}
+			}
+			return true;
+		},
+
+		"parent": function( elem ) {
+			return !Expr.pseudos["empty"]( elem );
+		},
+
+		// Element/input types
+		"header": function( elem ) {
+			return rheader.test( elem.nodeName );
+		},
+
+		"input": function( elem ) {
+			return rinputs.test( elem.nodeName );
+		},
+
+		"button": function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return name === "input" && elem.type === "button" || name === "button";
+		},
+
+		"text": function( elem ) {
+			var attr;
+			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+			// use getAttribute instead to test this case
+			return elem.nodeName.toLowerCase() === "input" &&
+				elem.type === "text" &&
+				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type );
+		},
+
+		// Position-in-collection
+		"first": createPositionalPseudo(function() {
+			return [ 0 ];
+		}),
+
+		"last": createPositionalPseudo(function( matchIndexes, length ) {
+			return [ length - 1 ];
+		}),
+
+		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ argument < 0 ? argument + length : argument ];
+		}),
+
+		"even": createPositionalPseudo(function( matchIndexes, length ) {
+			var i = 0;
+			for ( ; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"odd": createPositionalPseudo(function( matchIndexes, length ) {
+			var i = 1;
+			for ( ; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			var i = argument < 0 ? argument + length : argument;
+			for ( ; --i >= 0; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			var i = argument < 0 ? argument + length : argument;
+			for ( ; ++i < length; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		})
+	}
+};
+
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+	Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+	Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+function tokenize( selector, parseOnly ) {
+	var matched, match, tokens, type,
+		soFar, groups, preFilters,
+		cached = tokenCache[ selector + " " ];
+
+	if ( cached ) {
+		return parseOnly ? 0 : cached.slice( 0 );
+	}
+
+	soFar = selector;
+	groups = [];
+	preFilters = Expr.preFilter;
+
+	while ( soFar ) {
+
+		// Comma and first run
+		if ( !matched || (match = rcomma.exec( soFar )) ) {
+			if ( match ) {
+				// Don't consume trailing commas as valid
+				soFar = soFar.slice( match[0].length ) || soFar;
+			}
+			groups.push( tokens = [] );
+		}
+
+		matched = false;
+
+		// Combinators
+		if ( (match = rcombinators.exec( soFar )) ) {
+			matched = match.shift();
+			tokens.push({
+				value: matched,
+				// Cast descendant combinators to space
+				type: match[0].replace( rtrim, " " )
+			});
+			soFar = soFar.slice( matched.length );
+		}
+
+		// Filters
+		for ( type in Expr.filter ) {
+			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+				(match = preFilters[ type ]( match ))) ) {
+				matched = match.shift();
+				tokens.push({
+					value: matched,
+					type: type,
+					matches: match
+				});
+				soFar = soFar.slice( matched.length );
+			}
+		}
+
+		if ( !matched ) {
+			break;
+		}
+	}
+
+	// Return the length of the invalid excess
+	// if we're just parsing
+	// Otherwise, throw an error or return tokens
+	return parseOnly ?
+		soFar.length :
+		soFar ?
+			Sizzle.error( selector ) :
+			// Cache the tokens
+			tokenCache( selector, groups ).slice( 0 );
+}
+
+function toSelector( tokens ) {
+	var i = 0,
+		len = tokens.length,
+		selector = "";
+	for ( ; i < len; i++ ) {
+		selector += tokens[i].value;
+	}
+	return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+	var dir = combinator.dir,
+		checkNonElements = base && dir === "parentNode",
+		doneName = done++;
+
+	return combinator.first ?
+		// Check against closest ancestor/preceding element
+		function( elem, context, xml ) {
+			while ( (elem = elem[ dir ]) ) {
+				if ( elem.nodeType === 1 || checkNonElements ) {
+					return matcher( elem, context, xml );
+				}
+			}
+		} :
+
+		// Check against all ancestor/preceding elements
+		function( elem, context, xml ) {
+			var data, cache, outerCache,
+				dirkey = dirruns + " " + doneName;
+
+			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+			if ( xml ) {
+				while ( (elem = elem[ dir ]) ) {
+					if ( elem.nodeType === 1 || checkNonElements ) {
+						if ( matcher( elem, context, xml ) ) {
+							return true;
+						}
+					}
+				}
+			} else {
+				while ( (elem = elem[ dir ]) ) {
+					if ( elem.nodeType === 1 || checkNonElements ) {
+						outerCache = elem[ expando ] || (elem[ expando ] = {});
+						if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {
+							if ( (data = cache[1]) === true || data === cachedruns ) {
+								return data === true;
+							}
+						} else {
+							cache = outerCache[ dir ] = [ dirkey ];
+							cache[1] = matcher( elem, context, xml ) || cachedruns;
+							if ( cache[1] === true ) {
+								return true;
+							}
+						}
+					}
+				}
+			}
+		};
+}
+
+function elementMatcher( matchers ) {
+	return matchers.length > 1 ?
+		function( elem, context, xml ) {
+			var i = matchers.length;
+			while ( i-- ) {
+				if ( !matchers[i]( elem, context, xml ) ) {
+					return false;
+				}
+			}
+			return true;
+		} :
+		matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+	var elem,
+		newUnmatched = [],
+		i = 0,
+		len = unmatched.length,
+		mapped = map != null;
+
+	for ( ; i < len; i++ ) {
+		if ( (elem = unmatched[i]) ) {
+			if ( !filter || filter( elem, context, xml ) ) {
+				newUnmatched.push( elem );
+				if ( mapped ) {
+					map.push( i );
+				}
+			}
+		}
+	}
+
+	return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+	if ( postFilter && !postFilter[ expando ] ) {
+		postFilter = setMatcher( postFilter );
+	}
+	if ( postFinder && !postFinder[ expando ] ) {
+		postFinder = setMatcher( postFinder, postSelector );
+	}
+	return markFunction(function( seed, results, context, xml ) {
+		var temp, i, elem,
+			preMap = [],
+			postMap = [],
+			preexisting = results.length,
+
+			// Get initial elements from seed or context
+			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+			// Prefilter to get matcher input, preserving a map for seed-results synchronization
+			matcherIn = preFilter && ( seed || !selector ) ?
+				condense( elems, preMap, preFilter, context, xml ) :
+				elems,
+
+			matcherOut = matcher ?
+				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+					// ...intermediate processing is necessary
+					[] :
+
+					// ...otherwise use results directly
+					results :
+				matcherIn;
+
+		// Find primary matches
+		if ( matcher ) {
+			matcher( matcherIn, matcherOut, context, xml );
+		}
+
+		// Apply postFilter
+		if ( postFilter ) {
+			temp = condense( matcherOut, postMap );
+			postFilter( temp, [], context, xml );
+
+			// Un-match failing elements by moving them back to matcherIn
+			i = temp.length;
+			while ( i-- ) {
+				if ( (elem = temp[i]) ) {
+					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+				}
+			}
+		}
+
+		if ( seed ) {
+			if ( postFinder || preFilter ) {
+				if ( postFinder ) {
+					// Get the final matcherOut by condensing this intermediate into postFinder contexts
+					temp = [];
+					i = matcherOut.length;
+					while ( i-- ) {
+						if ( (elem = matcherOut[i]) ) {
+							// Restore matcherIn since elem is not yet a final match
+							temp.push( (matcherIn[i] = elem) );
+						}
+					}
+					postFinder( null, (matcherOut = []), temp, xml );
+				}
+
+				// Move matched elements from seed to results to keep them synchronized
+				i = matcherOut.length;
+				while ( i-- ) {
+					if ( (elem = matcherOut[i]) &&
+						(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+						seed[temp] = !(results[temp] = elem);
+					}
+				}
+			}
+
+		// Add elements to results, through postFinder if defined
+		} else {
+			matcherOut = condense(
+				matcherOut === results ?
+					matcherOut.splice( preexisting, matcherOut.length ) :
+					matcherOut
+			);
+			if ( postFinder ) {
+				postFinder( null, results, matcherOut, xml );
+			} else {
+				push.apply( results, matcherOut );
+			}
+		}
+	});
+}
+
+function matcherFromTokens( tokens ) {
+	var checkContext, matcher, j,
+		len = tokens.length,
+		leadingRelative = Expr.relative[ tokens[0].type ],
+		implicitRelative = leadingRelative || Expr.relative[" "],
+		i = leadingRelative ? 1 : 0,
+
+		// The foundational matcher ensures that elements are reachable from top-level context(s)
+		matchContext = addCombinator( function( elem ) {
+			return elem === checkContext;
+		}, implicitRelative, true ),
+		matchAnyContext = addCombinator( function( elem ) {
+			return indexOf.call( checkContext, elem ) > -1;
+		}, implicitRelative, true ),
+		matchers = [ function( elem, context, xml ) {
+			return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+				(checkContext = context).nodeType ?
+					matchContext( elem, context, xml ) :
+					matchAnyContext( elem, context, xml ) );
+		} ];
+
+	for ( ; i < len; i++ ) {
+		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+			matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+		} else {
+			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+			// Return special upon seeing a positional matcher
+			if ( matcher[ expando ] ) {
+				// Find the next relative operator (if any) for proper handling
+				j = ++i;
+				for ( ; j < len; j++ ) {
+					if ( Expr.relative[ tokens[j].type ] ) {
+						break;
+					}
+				}
+				return setMatcher(
+					i > 1 && elementMatcher( matchers ),
+					i > 1 && toSelector(
+						// If the preceding token was a descendant combinator, insert an implicit any-element `*`
+						tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
+					).replace( rtrim, "$1" ),
+					matcher,
+					i < j && matcherFromTokens( tokens.slice( i, j ) ),
+					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+					j < len && toSelector( tokens )
+				);
+			}
+			matchers.push( matcher );
+		}
+	}
+
+	return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+	// A counter to specify which element is currently being matched
+	var matcherCachedRuns = 0,
+		bySet = setMatchers.length > 0,
+		byElement = elementMatchers.length > 0,
+		superMatcher = function( seed, context, xml, results, expandContext ) {
+			var elem, j, matcher,
+				setMatched = [],
+				matchedCount = 0,
+				i = "0",
+				unmatched = seed && [],
+				outermost = expandContext != null,
+				contextBackup = outermostContext,
+				// We must always have either seed elements or context
+				elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+				// Use integer dirruns iff this is the outermost matcher
+				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);
+
+			if ( outermost ) {
+				outermostContext = context !== document && context;
+				cachedruns = matcherCachedRuns;
+			}
+
+			// Add elements passing elementMatchers directly to results
+			// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
+			for ( ; (elem = elems[i]) != null; i++ ) {
+				if ( byElement && elem ) {
+					j = 0;
+					while ( (matcher = elementMatchers[j++]) ) {
+						if ( matcher( elem, context, xml ) ) {
+							results.push( elem );
+							break;
+						}
+					}
+					if ( outermost ) {
+						dirruns = dirrunsUnique;
+						cachedruns = ++matcherCachedRuns;
+					}
+				}
+
+				// Track unmatched elements for set filters
+				if ( bySet ) {
+					// They will have gone through all possible matchers
+					if ( (elem = !matcher && elem) ) {
+						matchedCount--;
+					}
+
+					// Lengthen the array for every element, matched or not
+					if ( seed ) {
+						unmatched.push( elem );
+					}
+				}
+			}
+
+			// Apply set filters to unmatched elements
+			matchedCount += i;
+			if ( bySet && i !== matchedCount ) {
+				j = 0;
+				while ( (matcher = setMatchers[j++]) ) {
+					matcher( unmatched, setMatched, context, xml );
+				}
+
+				if ( seed ) {
+					// Reintegrate element matches to eliminate the need for sorting
+					if ( matchedCount > 0 ) {
+						while ( i-- ) {
+							if ( !(unmatched[i] || setMatched[i]) ) {
+								setMatched[i] = pop.call( results );
+							}
+						}
+					}
+
+					// Discard index placeholder values to get only actual matches
+					setMatched = condense( setMatched );
+				}
+
+				// Add matches to results
+				push.apply( results, setMatched );
+
+				// Seedless set matches succeeding multiple successful matchers stipulate sorting
+				if ( outermost && !seed && setMatched.length > 0 &&
+					( matchedCount + setMatchers.length ) > 1 ) {
+
+					Sizzle.uniqueSort( results );
+				}
+			}
+
+			// Override manipulation of globals by nested matchers
+			if ( outermost ) {
+				dirruns = dirrunsUnique;
+				outermostContext = contextBackup;
+			}
+
+			return unmatched;
+		};
+
+	return bySet ?
+		markFunction( superMatcher ) :
+		superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+	var i,
+		setMatchers = [],
+		elementMatchers = [],
+		cached = compilerCache[ selector + " " ];
+
+	if ( !cached ) {
+		// Generate a function of recursive functions that can be used to check each element
+		if ( !group ) {
+			group = tokenize( selector );
+		}
+		i = group.length;
+		while ( i-- ) {
+			cached = matcherFromTokens( group[i] );
+			if ( cached[ expando ] ) {
+				setMatchers.push( cached );
+			} else {
+				elementMatchers.push( cached );
+			}
+		}
+
+		// Cache the compiled function
+		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+	}
+	return cached;
+};
+
+function multipleContexts( selector, contexts, results ) {
+	var i = 0,
+		len = contexts.length;
+	for ( ; i < len; i++ ) {
+		Sizzle( selector, contexts[i], results );
+	}
+	return results;
+}
+
+function select( selector, context, results, seed ) {
+	var i, tokens, token, type, find,
+		match = tokenize( selector );
+
+	if ( !seed ) {
+		// Try to minimize operations if there is only one group
+		if ( match.length === 1 ) {
+
+			// Take a shortcut and set the context if the root selector is an ID
+			tokens = match[0] = match[0].slice( 0 );
+			if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+					support.getById && context.nodeType === 9 && documentIsHTML &&
+					Expr.relative[ tokens[1].type ] ) {
+
+				context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+				if ( !context ) {
+					return results;
+				}
+				selector = selector.slice( tokens.shift().value.length );
+			}
+
+			// Fetch a seed set for right-to-left matching
+			i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+			while ( i-- ) {
+				token = tokens[i];
+
+				// Abort if we hit a combinator
+				if ( Expr.relative[ (type = token.type) ] ) {
+					break;
+				}
+				if ( (find = Expr.find[ type ]) ) {
+					// Search, expanding context for leading sibling combinators
+					if ( (seed = find(
+						token.matches[0].replace( runescape, funescape ),
+						rsibling.test( tokens[0].type ) && context.parentNode || context
+					)) ) {
+
+						// If seed is empty or no tokens remain, we can return early
+						tokens.splice( i, 1 );
+						selector = seed.length && toSelector( tokens );
+						if ( !selector ) {
+							push.apply( results, seed );
+							return results;
+						}
+
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// Compile and execute a filtering function
+	// Provide `match` to avoid retokenization if we modified the selector above
+	compile( selector, match )(
+		seed,
+		context,
+		!documentIsHTML,
+		results,
+		rsibling.test( selector )
+	);
+	return results;
+}
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Support: Chrome<14
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert(function( div1 ) {
+	// Should return 1, but returns 4 (following)
+	return div1.compareDocumentPosition( document.createElement("div") ) & 1;
+});
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert(function( div ) {
+	div.innerHTML = "<a href='#'></a>";
+	return div.firstChild.getAttribute("href") === "#" ;
+}) ) {
+	addHandle( "type|href|height|width", function( elem, name, isXML ) {
+		if ( !isXML ) {
+			return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+		}
+	});
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert(function( div ) {
+	div.innerHTML = "<input/>";
+	div.firstChild.setAttribute( "value", "" );
+	return div.firstChild.getAttribute( "value" ) === "";
+}) ) {
+	addHandle( "value", function( elem, name, isXML ) {
+		if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+			return elem.defaultValue;
+		}
+	});
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert(function( div ) {
+	return div.getAttribute("disabled") == null;
+}) ) {
+	addHandle( booleans, function( elem, name, isXML ) {
+		var val;
+		if ( !isXML ) {
+			return (val = elem.getAttributeNode( name )) && val.specified ?
+				val.value :
+				elem[ name ] === true ? name.toLowerCase() : null;
+		}
+	});
+}
+
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})( window );
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+	var object = optionsCache[ options ] = {};
+	jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {
+		object[ flag ] = true;
+	});
+	return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *	options: an optional list of space-separated options that will change how
+ *			the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *	once:			will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *	memory:			will keep track of previous values and will call any callback added
+ *					after the list has been fired right away with the latest "memorized"
+ *					values (like a Deferred)
+ *
+ *	unique:			will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *	stopOnFalse:	interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+	// Convert options from String-formatted to Object-formatted if needed
+	// (we check in cache first)
+	options = typeof options === "string" ?
+		( optionsCache[ options ] || createOptions( options ) ) :
+		jQuery.extend( {}, options );
+
+	var // Flag to know if list is currently firing
+		firing,
+		// Last fire value (for non-forgettable lists)
+		memory,
+		// Flag to know if list was already fired
+		fired,
+		// End of the loop when firing
+		firingLength,
+		// Index of currently firing callback (modified by remove if needed)
+		firingIndex,
+		// First callback to fire (used internally by add and fireWith)
+		firingStart,
+		// Actual callback list
+		list = [],
+		// Stack of fire calls for repeatable lists
+		stack = !options.once && [],
+		// Fire callbacks
+		fire = function( data ) {
+			memory = options.memory && data;
+			fired = true;
+			firingIndex = firingStart || 0;
+			firingStart = 0;
+			firingLength = list.length;
+			firing = true;
+			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+					memory = false; // To prevent further calls using add
+					break;
+				}
+			}
+			firing = false;
+			if ( list ) {
+				if ( stack ) {
+					if ( stack.length ) {
+						fire( stack.shift() );
+					}
+				} else if ( memory ) {
+					list = [];
+				} else {
+					self.disable();
+				}
+			}
+		},
+		// Actual Callbacks object
+		self = {
+			// Add a callback or a collection of callbacks to the list
+			add: function() {
+				if ( list ) {
+					// First, we save the current length
+					var start = list.length;
+					(function add( args ) {
+						jQuery.each( args, function( _, arg ) {
+							var type = jQuery.type( arg );
+							if ( type === "function" ) {
+								if ( !options.unique || !self.has( arg ) ) {
+									list.push( arg );
+								}
+							} else if ( arg && arg.length && type !== "string" ) {
+								// Inspect recursively
+								add( arg );
+							}
+						});
+					})( arguments );
+					// Do we need to add the callbacks to the
+					// current firing batch?
+					if ( firing ) {
+						firingLength = list.length;
+					// With memory, if we're not firing then
+					// we should call right away
+					} else if ( memory ) {
+						firingStart = start;
+						fire( memory );
+					}
+				}
+				return this;
+			},
+			// Remove a callback from the list
+			remove: function() {
+				if ( list ) {
+					jQuery.each( arguments, function( _, arg ) {
+						var index;
+						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+							list.splice( index, 1 );
+							// Handle firing indexes
+							if ( firing ) {
+								if ( index <= firingLength ) {
+									firingLength--;
+								}
+								if ( index <= firingIndex ) {
+									firingIndex--;
+								}
+							}
+						}
+					});
+				}
+				return this;
+			},
+			// Check if a given callback is in the list.
+			// If no argument is given, return whether or not list has callbacks attached.
+			has: function( fn ) {
+				return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
+			},
+			// Remove all callbacks from the list
+			empty: function() {
+				list = [];
+				firingLength = 0;
+				return this;
+			},
+			// Have the list do nothing anymore
+			disable: function() {
+				list = stack = memory = undefined;
+				return this;
+			},
+			// Is it disabled?
+			disabled: function() {
+				return !list;
+			},
+			// Lock the list in its current state
+			lock: function() {
+				stack = undefined;
+				if ( !memory ) {
+					self.disable();
+				}
+				return this;
+			},
+			// Is it locked?
+			locked: function() {
+				return !stack;
+			},
+			// Call all callbacks with the given context and arguments
+			fireWith: function( context, args ) {
+				if ( list && ( !fired || stack ) ) {
+					args = args || [];
+					args = [ context, args.slice ? args.slice() : args ];
+					if ( firing ) {
+						stack.push( args );
+					} else {
+						fire( args );
+					}
+				}
+				return this;
+			},
+			// Call all the callbacks with the given arguments
+			fire: function() {
+				self.fireWith( this, arguments );
+				return this;
+			},
+			// To know if the callbacks have already been called at least once
+			fired: function() {
+				return !!fired;
+			}
+		};
+
+	return self;
+};
+jQuery.extend({
+
+	Deferred: function( func ) {
+		var tuples = [
+				// action, add listener, listener list, final state
+				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+				[ "notify", "progress", jQuery.Callbacks("memory") ]
+			],
+			state = "pending",
+			promise = {
+				state: function() {
+					return state;
+				},
+				always: function() {
+					deferred.done( arguments ).fail( arguments );
+					return this;
+				},
+				then: function( /* fnDone, fnFail, fnProgress */ ) {
+					var fns = arguments;
+					return jQuery.Deferred(function( newDefer ) {
+						jQuery.each( tuples, function( i, tuple ) {
+							var action = tuple[ 0 ],
+								fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
+							// deferred[ done | fail | progress ] for forwarding actions to newDefer
+							deferred[ tuple[1] ](function() {
+								var returned = fn && fn.apply( this, arguments );
+								if ( returned && jQuery.isFunction( returned.promise ) ) {
+									returned.promise()
+										.done( newDefer.resolve )
+										.fail( newDefer.reject )
+										.progress( newDefer.notify );
+								} else {
+									newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
+								}
+							});
+						});
+						fns = null;
+					}).promise();
+				},
+				// Get a promise for this deferred
+				// If obj is provided, the promise aspect is added to the object
+				promise: function( obj ) {
+					return obj != null ? jQuery.extend( obj, promise ) : promise;
+				}
+			},
+			deferred = {};
+
+		// Keep pipe for back-compat
+		promise.pipe = promise.then;
+
+		// Add list-specific methods
+		jQuery.each( tuples, function( i, tuple ) {
+			var list = tuple[ 2 ],
+				stateString = tuple[ 3 ];
+
+			// promise[ done | fail | progress ] = list.add
+			promise[ tuple[1] ] = list.add;
+
+			// Handle state
+			if ( stateString ) {
+				list.add(function() {
+					// state = [ resolved | rejected ]
+					state = stateString;
+
+				// [ reject_list | resolve_list ].disable; progress_list.lock
+				}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+			}
+
+			// deferred[ resolve | reject | notify ]
+			deferred[ tuple[0] ] = function() {
+				deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
+				return this;
+			};
+			deferred[ tuple[0] + "With" ] = list.fireWith;
+		});
+
+		// Make the deferred a promise
+		promise.promise( deferred );
+
+		// Call given func if any
+		if ( func ) {
+			func.call( deferred, deferred );
+		}
+
+		// All done!
+		return deferred;
+	},
+
+	// Deferred helper
+	when: function( subordinate /* , ..., subordinateN */ ) {
+		var i = 0,
+			resolveValues = core_slice.call( arguments ),
+			length = resolveValues.length,
+
+			// the count of uncompleted subordinates
+			remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+			// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+			deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+			// Update function for both resolve and progress values
+			updateFunc = function( i, contexts, values ) {
+				return function( value ) {
+					contexts[ i ] = this;
+					values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+					if( values === progressValues ) {
+						deferred.notifyWith( contexts, values );
+					} else if ( !( --remaining ) ) {
+						deferred.resolveWith( contexts, values );
+					}
+				};
+			},
+
+			progressValues, progressContexts, resolveContexts;
+
+		// add listeners to Deferred subordinates; treat others as resolved
+		if ( length > 1 ) {
+			progressValues = new Array( length );
+			progressContexts = new Array( length );
+			resolveContexts = new Array( length );
+			for ( ; i < length; i++ ) {
+				if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+					resolveValues[ i ].promise()
+						.done( updateFunc( i, resolveContexts, resolveValues ) )
+						.fail( deferred.reject )
+						.progress( updateFunc( i, progressContexts, progressValues ) );
+				} else {
+					--remaining;
+				}
+			}
+		}
+
+		// if we're not waiting on anything, resolve the master
+		if ( !remaining ) {
+			deferred.resolveWith( resolveContexts, resolveValues );
+		}
+
+		return deferred.promise();
+	}
+});
+jQuery.support = (function( support ) {
+
+	var all, a, input, select, fragment, opt, eventName, isSupported, i,
+		div = document.createElement("div");
+
+	// Setup
+	div.setAttribute( "className", "t" );
+	div.innerHTML = "  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+	// Finish early in limited (non-browser) environments
+	all = div.getElementsByTagName("*") || [];
+	a = div.getElementsByTagName("a")[ 0 ];
+	if ( !a || !a.style || !all.length ) {
+		return support;
+	}
+
+	// First batch of tests
+	select = document.createElement("select");
+	opt = select.appendChild( document.createElement("option") );
+	input = div.getElementsByTagName("input")[ 0 ];
+
+	a.style.cssText = "top:1px;float:left;opacity:.5";
+
+	// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+	support.getSetAttribute = div.className !== "t";
+
+	// IE strips leading whitespace when .innerHTML is used
+	support.leadingWhitespace = div.firstChild.nodeType === 3;
+
+	// Make sure that tbody elements aren't automatically inserted
+	// IE will insert them into empty tables
+	support.tbody = !div.getElementsByTagName("tbody").length;
+
+	// Make sure that link elements get serialized correctly by innerHTML
+	// This requires a wrapper element in IE
+	support.htmlSerialize = !!div.getElementsByTagName("link").length;
+
+	// Get the style information from getAttribute
+	// (IE uses .cssText instead)
+	support.style = /top/.test( a.getAttribute("style") );
+
+	// Make sure that URLs aren't manipulated
+	// (IE normalizes it by default)
+	support.hrefNormalized = a.getAttribute("href") === "/a";
+
+	// Make sure that element opacity exists
+	// (IE uses filter instead)
+	// Use a regex to work around a WebKit issue. See #5145
+	support.opacity = /^0.5/.test( a.style.opacity );
+
+	// Verify style float existence
+	// (IE uses styleFloat instead of cssFloat)
+	support.cssFloat = !!a.style.cssFloat;
+
+	// Check the default checkbox/radio value ("" on WebKit; "on" elsewhere)
+	support.checkOn = !!input.value;
+
+	// Make sure that a selected-by-default option has a working selected property.
+	// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+	support.optSelected = opt.selected;
+
+	// Tests for enctype support on a form (#6743)
+	support.enctype = !!document.createElement("form").enctype;
+
+	// Makes sure cloning an html5 element does not cause problems
+	// Where outerHTML is undefined, this still works
+	support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>";
+
+	// Will be defined later
+	support.inlineBlockNeedsLayout = false;
+	support.shrinkWrapBlocks = false;
+	support.pixelPosition = false;
+	support.deleteExpando = true;
+	support.noCloneEvent = true;
+	support.reliableMarginRight = true;
+	support.boxSizingReliable = true;
+
+	// Make sure checked status is properly cloned
+	input.checked = true;
+	support.noCloneChecked = input.cloneNode( true ).checked;
+
+	// Make sure that the options inside disabled selects aren't marked as disabled
+	// (WebKit marks them as disabled)
+	select.disabled = true;
+	support.optDisabled = !opt.disabled;
+
+	// Support: IE<9
+	try {
+		delete div.test;
+	} catch( e ) {
+		support.deleteExpando = false;
+	}
+
+	// Check if we can trust getAttribute("value")
+	input = document.createElement("input");
+	input.setAttribute( "value", "" );
+	support.input = input.getAttribute( "value" ) === "";
+
+	// Check if an input maintains its value after becoming a radio
+	input.value = "t";
+	input.setAttribute( "type", "radio" );
+	support.radioValue = input.value === "t";
+
+	// #11217 - WebKit loses check when the name is after the checked attribute
+	input.setAttribute( "checked", "t" );
+	input.setAttribute( "name", "t" );
+
+	fragment = document.createDocumentFragment();
+	fragment.appendChild( input );
+
+	// Check if a disconnected checkbox will retain its checked
+	// value of true after appended to the DOM (IE6/7)
+	support.appendChecked = input.checked;
+
+	// WebKit doesn't clone checked state correctly in fragments
+	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+	// Support: IE<9
+	// Opera does not clone events (and typeof div.attachEvent === undefined).
+	// IE9-10 clones events bound via attachEvent, but they don't trigger with .click()
+	if ( div.attachEvent ) {
+		div.attachEvent( "onclick", function() {
+			support.noCloneEvent = false;
+		});
+
+		div.cloneNode( true ).click();
+	}
+
+	// Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event)
+	// Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)
+	for ( i in { submit: true, change: true, focusin: true }) {
+		div.setAttribute( eventName = "on" + i, "t" );
+
+		support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false;
+	}
+
+	div.style.backgroundClip = "content-box";
+	div.cloneNode( true ).style.backgroundClip = "";
+	support.clearCloneStyle = div.style.backgroundClip === "content-box";
+
+	// Support: IE<9
+	// Iteration over object's inherited properties before its own.
+	for ( i in jQuery( support ) ) {
+		break;
+	}
+	support.ownLast = i !== "0";
+
+	// Run tests that need a body at doc ready
+	jQuery(function() {
+		var container, marginDiv, tds,
+			divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",
+			body = document.getElementsByTagName("body")[0];
+
+		if ( !body ) {
+			// Return for frameset docs that don't have a body
+			return;
+		}
+
+		container = document.createElement("div");
+		container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px";
+
+		body.appendChild( container ).appendChild( div );
+
+		// Support: IE8
+		// Check if table cells still have offsetWidth/Height when they are set
+		// to display:none and there are still other visible table cells in a
+		// table row; if so, offsetWidth/Height are not reliable for use when
+		// determining if an element has been hidden directly using
+		// display:none (it is still safe to use offsets if a parent element is
+		// hidden; don safety goggles and see bug #4512 for more information).
+		div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+		tds = div.getElementsByTagName("td");
+		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+		isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+		tds[ 0 ].style.display = "";
+		tds[ 1 ].style.display = "none";
+
+		// Support: IE8
+		// Check if empty table cells still have offsetWidth/Height
+		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+		// Check box-sizing and margin behavior.
+		div.innerHTML = "";
+		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+
+		// Workaround failing boxSizing test due to offsetWidth returning wrong value
+		// with some non-1 values of body zoom, ticket #13543
+		jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() {
+			support.boxSizing = div.offsetWidth === 4;
+		});
+
+		// Use window.getComputedStyle because jsdom on node.js will break without it.
+		if ( window.getComputedStyle ) {
+			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+			// Check if div with explicit width and no margin-right incorrectly
+			// gets computed margin-right based on width of container. (#3333)
+			// Fails in WebKit before Feb 2011 nightlies
+			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+			marginDiv = div.appendChild( document.createElement("div") );
+			marginDiv.style.cssText = div.style.cssText = divReset;
+			marginDiv.style.marginRight = marginDiv.style.width = "0";
+			div.style.width = "1px";
+
+			support.reliableMarginRight =
+				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+		}
+
+		if ( typeof div.style.zoom !== core_strundefined ) {
+			// Support: IE<8
+			// Check if natively block-level elements act like inline-block
+			// elements when setting their display to 'inline' and giving
+			// them layout
+			div.innerHTML = "";
+			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+			// Support: IE6
+			// Check if elements with layout shrink-wrap their children
+			div.style.display = "block";
+			div.innerHTML = "<div></div>";
+			div.firstChild.style.width = "5px";
+			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+			if ( support.inlineBlockNeedsLayout ) {
+				// Prevent IE 6 from affecting layout for positioned elements #11048
+				// Prevent IE from shrinking the body in IE 7 mode #12869
+				// Support: IE<8
+				body.style.zoom = 1;
+			}
+		}
+
+		body.removeChild( container );
+
+		// Null elements to avoid leaks in IE
+		container = div = tds = marginDiv = null;
+	});
+
+	// Null elements to avoid leaks in IE
+	all = select = fragment = opt = a = input = null;
+
+	return support;
+})({});
+
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+	rmultiDash = /([A-Z])/g;
+
+function internalData( elem, name, data, pvt /* Internal Use Only */ ){
+	if ( !jQuery.acceptData( elem ) ) {
+		return;
+	}
+
+	var ret, thisCache,
+		internalKey = jQuery.expando,
+
+		// We have to handle DOM nodes and JS objects differently because IE6-7
+		// can't GC object references properly across the DOM-JS boundary
+		isNode = elem.nodeType,
+
+		// Only DOM nodes need the global jQuery cache; JS object data is
+		// attached directly to the object so GC can occur automatically
+		cache = isNode ? jQuery.cache : elem,
+
+		// Only defining an ID for JS objects if its cache already exists allows
+		// the code to shortcut on the same path as a DOM node with no cache
+		id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+	// Avoid doing any more work than we need to when trying to get data on an
+	// object that has no data at all
+	if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) {
+		return;
+	}
+
+	if ( !id ) {
+		// Only DOM nodes need a new unique ID for each element since their data
+		// ends up in the global cache
+		if ( isNode ) {
+			id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++;
+		} else {
+			id = internalKey;
+		}
+	}
+
+	if ( !cache[ id ] ) {
+		// Avoid exposing jQuery metadata on plain JS objects when the object
+		// is serialized using JSON.stringify
+		cache[ id ] = isNode ? {} : { toJSON: jQuery.noop };
+	}
+
+	// An object can be passed to jQuery.data instead of a key/value pair; this gets
+	// shallow copied over onto the existing cache
+	if ( typeof name === "object" || typeof name === "function" ) {
+		if ( pvt ) {
+			cache[ id ] = jQuery.extend( cache[ id ], name );
+		} else {
+			cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+		}
+	}
+
+	thisCache = cache[ id ];
+
+	// jQuery data() is stored in a separate object inside the object's internal data
+	// cache in order to avoid key collisions between internal data and user-defined
+	// data.
+	if ( !pvt ) {
+		if ( !thisCache.data ) {
+			thisCache.data = {};
+		}
+
+		thisCache = thisCache.data;
+	}
+
+	if ( data !== undefined ) {
+		thisCache[ jQuery.camelCase( name ) ] = data;
+	}
+
+	// Check for both converted-to-camel and non-converted data property names
+	// If a data property was specified
+	if ( typeof name === "string" ) {
+
+		// First Try to find as-is property data
+		ret = thisCache[ name ];
+
+		// Test for null|undefined property data
+		if ( ret == null ) {
+
+			// Try to find the camelCased property
+			ret = thisCache[ jQuery.camelCase( name ) ];
+		}
+	} else {
+		ret = thisCache;
+	}
+
+	return ret;
+}
+
+function internalRemoveData( elem, name, pvt ) {
+	if ( !jQuery.acceptData( elem ) ) {
+		return;
+	}
+
+	var thisCache, i,
+		isNode = elem.nodeType,
+
+		// See jQuery.data for more information
+		cache = isNode ? jQuery.cache : elem,
+		id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+	// If there is already no cache entry for this object, there is no
+	// purpose in continuing
+	if ( !cache[ id ] ) {
+		return;
+	}
+
+	if ( name ) {
+
+		thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+		if ( thisCache ) {
+
+			// Support array or space separated string names for data keys
+			if ( !jQuery.isArray( name ) ) {
+
+				// try the string as a key before any manipulation
+				if ( name in thisCache ) {
+					name = [ name ];
+				} else {
+
+					// split the camel cased version by spaces unless a key with the spaces exists
+					name = jQuery.camelCase( name );
+					if ( name in thisCache ) {
+						name = [ name ];
+					} else {
+						name = name.split(" ");
+					}
+				}
+			} else {
+				// If "name" is an array of keys...
+				// When data is initially created, via ("key", "val") signature,
+				// keys will be converted to camelCase.
+				// Since there is no way to tell _how_ a key was added, remove
+				// both plain key and camelCase key. #12786
+				// This will only penalize the array argument path.
+				name = name.concat( jQuery.map( name, jQuery.camelCase ) );
+			}
+
+			i = name.length;
+			while ( i-- ) {
+				delete thisCache[ name[i] ];
+			}
+
+			// If there is no data left in the cache, we want to continue
+			// and let the cache object itself get destroyed
+			if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) {
+				return;
+			}
+		}
+	}
+
+	// See jQuery.data for more information
+	if ( !pvt ) {
+		delete cache[ id ].data;
+
+		// Don't destroy the parent cache unless the internal data object
+		// had been the only thing left in it
+		if ( !isEmptyDataObject( cache[ id ] ) ) {
+			return;
+		}
+	}
+
+	// Destroy the cache
+	if ( isNode ) {
+		jQuery.cleanData( [ elem ], true );
+
+	// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+	/* jshint eqeqeq: false */
+	} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+		/* jshint eqeqeq: true */
+		delete cache[ id ];
+
+	// When all else fails, null
+	} else {
+		cache[ id ] = null;
+	}
+}
+
+jQuery.extend({
+	cache: {},
+
+	// The following elements throw uncatchable exceptions if you
+	// attempt to add expando properties to them.
+	noData: {
+		"applet": true,
+		"embed": true,
+		// Ban all objects except for Flash (which handle expandos)
+		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
+	},
+
+	hasData: function( elem ) {
+		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+		return !!elem && !isEmptyDataObject( elem );
+	},
+
+	data: function( elem, name, data ) {
+		return internalData( elem, name, data );
+	},
+
+	removeData: function( elem, name ) {
+		return internalRemoveData( elem, name );
+	},
+
+	// For internal use only.
+	_data: function( elem, name, data ) {
+		return internalData( elem, name, data, true );
+	},
+
+	_removeData: function( elem, name ) {
+		return internalRemoveData( elem, name, true );
+	},
+
+	// A method for determining if a DOM node can handle the data expando
+	acceptData: function( elem ) {
+		// Do not set data on non-element because it will not be cleared (#8335).
+		if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {
+			return false;
+		}
+
+		var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+		// nodes accept data unless otherwise specified; rejection can be conditional
+		return !noData || noData !== true && elem.getAttribute("classid") === noData;
+	}
+});
+
+jQuery.fn.extend({
+	data: function( key, value ) {
+		var attrs, name,
+			data = null,
+			i = 0,
+			elem = this[0];
+
+		// Special expections of .data basically thwart jQuery.access,
+		// so implement the relevant behavior ourselves
+
+		// Gets all values
+		if ( key === undefined ) {
+			if ( this.length ) {
+				data = jQuery.data( elem );
+
+				if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+					attrs = elem.attributes;
+					for ( ; i < attrs.length; i++ ) {
+						name = attrs[i].name;
+
+						if ( name.indexOf("data-") === 0 ) {
+							name = jQuery.camelCase( name.slice(5) );
+
+							dataAttr( elem, name, data[ name ] );
+						}
+					}
+					jQuery._data( elem, "parsedAttrs", true );
+				}
+			}
+
+			return data;
+		}
+
+		// Sets multiple values
+		if ( typeof key === "object" ) {
+			return this.each(function() {
+				jQuery.data( this, key );
+			});
+		}
+
+		return arguments.length > 1 ?
+
+			// Sets one value
+			this.each(function() {
+				jQuery.data( this, key, value );
+			}) :
+
+			// Gets one value
+			// Try to fetch any internally stored data first
+			elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
+	},
+
+	removeData: function( key ) {
+		return this.each(function() {
+			jQuery.removeData( this, key );
+		});
+	}
+});
+
+function dataAttr( elem, key, data ) {
+	// If nothing was found internally, try to fetch any
+	// data from the HTML5 data-* attribute
+	if ( data === undefined && elem.nodeType === 1 ) {
+
+		var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+		data = elem.getAttribute( name );
+
+		if ( typeof data === "string" ) {
+			try {
+				data = data === "true" ? true :
+					data === "false" ? false :
+					data === "null" ? null :
+					// Only convert to a number if it doesn't change the string
+					+data + "" === data ? +data :
+					rbrace.test( data ) ? jQuery.parseJSON( data ) :
+						data;
+			} catch( e ) {}
+
+			// Make sure we set the data so it isn't changed later
+			jQuery.data( elem, key, data );
+
+		} else {
+			data = undefined;
+		}
+	}
+
+	return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+	var name;
+	for ( name in obj ) {
+
+		// if the public data object is empty, the private is still empty
+		if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+			continue;
+		}
+		if ( name !== "toJSON" ) {
+			return false;
+		}
+	}
+
+	return true;
+}
+jQuery.extend({
+	queue: function( elem, type, data ) {
+		var queue;
+
+		if ( elem ) {
+			type = ( type || "fx" ) + "queue";
+			queue = jQuery._data( elem, type );
+
+			// Speed up dequeue by getting out quickly if this is just a lookup
+			if ( data ) {
+				if ( !queue || jQuery.isArray(data) ) {
+					queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+				} else {
+					queue.push( data );
+				}
+			}
+			return queue || [];
+		}
+	},
+
+	dequeue: function( elem, type ) {
+		type = type || "fx";
+
+		var queue = jQuery.queue( elem, type ),
+			startLength = queue.length,
+			fn = queue.shift(),
+			hooks = jQuery._queueHooks( elem, type ),
+			next = function() {
+				jQuery.dequeue( elem, type );
+			};
+
+		// If the fx queue is dequeued, always remove the progress sentinel
+		if ( fn === "inprogress" ) {
+			fn = queue.shift();
+			startLength--;
+		}
+
+		if ( fn ) {
+
+			// Add a progress sentinel to prevent the fx queue from being
+			// automatically dequeued
+			if ( type === "fx" ) {
+				queue.unshift( "inprogress" );
+			}
+
+			// clear up the last queue stop function
+			delete hooks.stop;
+			fn.call( elem, next, hooks );
+		}
+
+		if ( !startLength && hooks ) {
+			hooks.empty.fire();
+		}
+	},
+
+	// not intended for public consumption - generates a queueHooks object, or returns the current one
+	_queueHooks: function( elem, type ) {
+		var key = type + "queueHooks";
+		return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+			empty: jQuery.Callbacks("once memory").add(function() {
+				jQuery._removeData( elem, type + "queue" );
+				jQuery._removeData( elem, key );
+			})
+		});
+	}
+});
+
+jQuery.fn.extend({
+	queue: function( type, data ) {
+		var setter = 2;
+
+		if ( typeof type !== "string" ) {
+			data = type;
+			type = "fx";
+			setter--;
+		}
+
+		if ( arguments.length < setter ) {
+			return jQuery.queue( this[0], type );
+		}
+
+		return data === undefined ?
+			this :
+			this.each(function() {
+				var queue = jQuery.queue( this, type, data );
+
+				// ensure a hooks for this queue
+				jQuery._queueHooks( this, type );
+
+				if ( type === "fx" && queue[0] !== "inprogress" ) {
+					jQuery.dequeue( this, type );
+				}
+			});
+	},
+	dequeue: function( type ) {
+		return this.each(function() {
+			jQuery.dequeue( this, type );
+		});
+	},
+	// Based off of the plugin by Clint Helfers, with permission.
+	// http://blindsignals.com/index.php/2009/07/jquery-delay/
+	delay: function( time, type ) {
+		time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+		type = type || "fx";
+
+		return this.queue( type, function( next, hooks ) {
+			var timeout = setTimeout( next, time );
+			hooks.stop = function() {
+				clearTimeout( timeout );
+			};
+		});
+	},
+	clearQueue: function( type ) {
+		return this.queue( type || "fx", [] );
+	},
+	// Get a promise resolved when queues of a certain type
+	// are emptied (fx is the type by default)
+	promise: function( type, obj ) {
+		var tmp,
+			count = 1,
+			defer = jQuery.Deferred(),
+			elements = this,
+			i = this.length,
+			resolve = function() {
+				if ( !( --count ) ) {
+					defer.resolveWith( elements, [ elements ] );
+				}
+			};
+
+		if ( typeof type !== "string" ) {
+			obj = type;
+			type = undefined;
+		}
+		type = type || "fx";
+
+		while( i-- ) {
+			tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+			if ( tmp && tmp.empty ) {
+				count++;
+				tmp.empty.add( resolve );
+			}
+		}
+		resolve();
+		return defer.promise( obj );
+	}
+});
+var nodeHook, boolHook,
+	rclass = /[\t\r\n\f]/g,
+	rreturn = /\r/g,
+	rfocusable = /^(?:input|select|textarea|button|object)$/i,
+	rclickable = /^(?:a|area)$/i,
+	ruseDefault = /^(?:checked|selected)$/i,
+	getSetAttribute = jQuery.support.getSetAttribute,
+	getSetInput = jQuery.support.input;
+
+jQuery.fn.extend({
+	attr: function( name, value ) {
+		return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+	},
+
+	removeAttr: function( name ) {
+		return this.each(function() {
+			jQuery.removeAttr( this, name );
+		});
+	},
+
+	prop: function( name, value ) {
+		return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+	},
+
+	removeProp: function( name ) {
+		name = jQuery.propFix[ name ] || name;
+		return this.each(function() {
+			// try/catch handles cases where IE balks (such as removing a property on window)
+			try {
+				this[ name ] = undefined;
+				delete this[ name ];
+			} catch( e ) {}
+		});
+	},
+
+	addClass: function( value ) {
+		var classes, elem, cur, clazz, j,
+			i = 0,
+			len = this.length,
+			proceed = typeof value === "string" && value;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).addClass( value.call( this, j, this.className ) );
+			});
+		}
+
+		if ( proceed ) {
+			// The disjunction here is for better compressibility (see removeClass)
+			classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+			for ( ; i < len; i++ ) {
+				elem = this[ i ];
+				cur = elem.nodeType === 1 && ( elem.className ?
+					( " " + elem.className + " " ).replace( rclass, " " ) :
+					" "
+				);
+
+				if ( cur ) {
+					j = 0;
+					while ( (clazz = classes[j++]) ) {
+						if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
+							cur += clazz + " ";
+						}
+					}
+					elem.className = jQuery.trim( cur );
+
+				}
+			}
+		}
+
+		return this;
+	},
+
+	removeClass: function( value ) {
+		var classes, elem, cur, clazz, j,
+			i = 0,
+			len = this.length,
+			proceed = arguments.length === 0 || typeof value === "string" && value;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).removeClass( value.call( this, j, this.className ) );
+			});
+		}
+		if ( proceed ) {
+			classes = ( value || "" ).match( core_rnotwhite ) || [];
+
+			for ( ; i < len; i++ ) {
+				elem = this[ i ];
+				// This expression is here for better compressibility (see addClass)
+				cur = elem.nodeType === 1 && ( elem.className ?
+					( " " + elem.className + " " ).replace( rclass, " " ) :
+					""
+				);
+
+				if ( cur ) {
+					j = 0;
+					while ( (clazz = classes[j++]) ) {
+						// Remove *all* instances
+						while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
+							cur = cur.replace( " " + clazz + " ", " " );
+						}
+					}
+					elem.className = value ? jQuery.trim( cur ) : "";
+				}
+			}
+		}
+
+		return this;
+	},
+
+	toggleClass: function( value, stateVal ) {
+		var type = typeof value;
+
+		if ( typeof stateVal === "boolean" && type === "string" ) {
+			return stateVal ? this.addClass( value ) : this.removeClass( value );
+		}
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( i ) {
+				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+			});
+		}
+
+		return this.each(function() {
+			if ( type === "string" ) {
+				// toggle individual class names
+				var className,
+					i = 0,
+					self = jQuery( this ),
+					classNames = value.match( core_rnotwhite ) || [];
+
+				while ( (className = classNames[ i++ ]) ) {
+					// check each className given, space separated list
+					if ( self.hasClass( className ) ) {
+						self.removeClass( className );
+					} else {
+						self.addClass( className );
+					}
+				}
+
+			// Toggle whole class name
+			} else if ( type === core_strundefined || type === "boolean" ) {
+				if ( this.className ) {
+					// store className if set
+					jQuery._data( this, "__className__", this.className );
+				}
+
+				// If the element has a class name or if we're passed "false",
+				// then remove the whole classname (if there was one, the above saved it).
+				// Otherwise bring back whatever was previously saved (if anything),
+				// falling back to the empty string if nothing was stored.
+				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+			}
+		});
+	},
+
+	hasClass: function( selector ) {
+		var className = " " + selector + " ",
+			i = 0,
+			l = this.length;
+		for ( ; i < l; i++ ) {
+			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+				return true;
+			}
+		}
+
+		return false;
+	},
+
+	val: function( value ) {
+		var ret, hooks, isFunction,
+			elem = this[0];
+
+		if ( !arguments.length ) {
+			if ( elem ) {
+				hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+					return ret;
+				}
+
+				ret = elem.value;
+
+				return typeof ret === "string" ?
+					// handle most common string cases
+					ret.replace(rreturn, "") :
+					// handle cases where value is null/undef or number
+					ret == null ? "" : ret;
+			}
+
+			return;
+		}
+
+		isFunction = jQuery.isFunction( value );
+
+		return this.each(function( i ) {
+			var val;
+
+			if ( this.nodeType !== 1 ) {
+				return;
+			}
+
+			if ( isFunction ) {
+				val = value.call( this, i, jQuery( this ).val() );
+			} else {
+				val = value;
+			}
+
+			// Treat null/undefined as ""; convert numbers to string
+			if ( val == null ) {
+				val = "";
+			} else if ( typeof val === "number" ) {
+				val += "";
+			} else if ( jQuery.isArray( val ) ) {
+				val = jQuery.map(val, function ( value ) {
+					return value == null ? "" : value + "";
+				});
+			}
+
+			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+			// If set returns undefined, fall back to normal setting
+			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+				this.value = val;
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	valHooks: {
+		option: {
+			get: function( elem ) {
+				// Use proper attribute retrieval(#6932, #12072)
+				var val = jQuery.find.attr( elem, "value" );
+				return val != null ?
+					val :
+					elem.text;
+			}
+		},
+		select: {
+			get: function( elem ) {
+				var value, option,
+					options = elem.options,
+					index = elem.selectedIndex,
+					one = elem.type === "select-one" || index < 0,
+					values = one ? null : [],
+					max = one ? index + 1 : options.length,
+					i = index < 0 ?
+						max :
+						one ? index : 0;
+
+				// Loop through all the selected options
+				for ( ; i < max; i++ ) {
+					option = options[ i ];
+
+					// oldIE doesn't update selected after form reset (#2551)
+					if ( ( option.selected || i === index ) &&
+							// Don't return options that are disabled or in a disabled optgroup
+							( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
+							( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
+
+						// Get the specific value for the option
+						value = jQuery( option ).val();
+
+						// We don't need an array for one selects
+						if ( one ) {
+							return value;
+						}
+
+						// Multi-Selects return an array
+						values.push( value );
+					}
+				}
+
+				return values;
+			},
+
+			set: function( elem, value ) {
+				var optionSet, option,
+					options = elem.options,
+					values = jQuery.makeArray( value ),
+					i = options.length;
+
+				while ( i-- ) {
+					option = options[ i ];
+					if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) {
+						optionSet = true;
+					}
+				}
+
+				// force browsers to behave consistently when non-matching value is set
+				if ( !optionSet ) {
+					elem.selectedIndex = -1;
+				}
+				return values;
+			}
+		}
+	},
+
+	attr: function( elem, name, value ) {
+		var hooks, ret,
+			nType = elem.nodeType;
+
+		// don't get/set attributes on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		// Fallback to prop when attributes are not supported
+		if ( typeof elem.getAttribute === core_strundefined ) {
+			return jQuery.prop( elem, name, value );
+		}
+
+		// All attributes are lowercase
+		// Grab necessary hook if one is defined
+		if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
+			name = name.toLowerCase();
+			hooks = jQuery.attrHooks[ name ] ||
+				( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
+		}
+
+		if ( value !== undefined ) {
+
+			if ( value === null ) {
+				jQuery.removeAttr( elem, name );
+
+			} else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				elem.setAttribute( name, value + "" );
+				return value;
+			}
+
+		} else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+			return ret;
+
+		} else {
+			ret = jQuery.find.attr( elem, name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return ret == null ?
+				undefined :
+				ret;
+		}
+	},
+
+	removeAttr: function( elem, value ) {
+		var name, propName,
+			i = 0,
+			attrNames = value && value.match( core_rnotwhite );
+
+		if ( attrNames && elem.nodeType === 1 ) {
+			while ( (name = attrNames[i++]) ) {
+				propName = jQuery.propFix[ name ] || name;
+
+				// Boolean attributes get special treatment (#10870)
+				if ( jQuery.expr.match.bool.test( name ) ) {
+					// Set corresponding property to false
+					if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
+						elem[ propName ] = false;
+					// Support: IE<9
+					// Also clear defaultChecked/defaultSelected (if appropriate)
+					} else {
+						elem[ jQuery.camelCase( "default-" + name ) ] =
+							elem[ propName ] = false;
+					}
+
+				// See #9699 for explanation of this approach (setting first, then removal)
+				} else {
+					jQuery.attr( elem, name, "" );
+				}
+
+				elem.removeAttribute( getSetAttribute ? name : propName );
+			}
+		}
+	},
+
+	attrHooks: {
+		type: {
+			set: function( elem, value ) {
+				if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+					// Setting the type on a radio button after the value resets the value in IE6-9
+					// Reset value to default in case type is set after value during creation
+					var val = elem.value;
+					elem.setAttribute( "type", value );
+					if ( val ) {
+						elem.value = val;
+					}
+					return value;
+				}
+			}
+		}
+	},
+
+	propFix: {
+		"for": "htmlFor",
+		"class": "className"
+	},
+
+	prop: function( elem, name, value ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set properties on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		if ( notxml ) {
+			// Fix name and attach hooks
+			name = jQuery.propFix[ name ] || name;
+			hooks = jQuery.propHooks[ name ];
+		}
+
+		if ( value !== undefined ) {
+			return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?
+				ret :
+				( elem[ name ] = value );
+
+		} else {
+			return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ?
+				ret :
+				elem[ name ];
+		}
+	},
+
+	propHooks: {
+		tabIndex: {
+			get: function( elem ) {
+				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+				// Use proper attribute retrieval(#12072)
+				var tabindex = jQuery.find.attr( elem, "tabindex" );
+
+				return tabindex ?
+					parseInt( tabindex, 10 ) :
+					rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+						0 :
+						-1;
+			}
+		}
+	}
+});
+
+// Hooks for boolean attributes
+boolHook = {
+	set: function( elem, value, name ) {
+		if ( value === false ) {
+			// Remove boolean attributes when set to false
+			jQuery.removeAttr( elem, name );
+		} else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
+			// IE<8 needs the *property* name
+			elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
+
+		// Use defaultChecked and defaultSelected for oldIE
+		} else {
+			elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
+		}
+
+		return name;
+	}
+};
+jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
+	var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr;
+
+	jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ?
+		function( elem, name, isXML ) {
+			var fn = jQuery.expr.attrHandle[ name ],
+				ret = isXML ?
+					undefined :
+					/* jshint eqeqeq: false */
+					(jQuery.expr.attrHandle[ name ] = undefined) !=
+						getter( elem, name, isXML ) ?
+
+						name.toLowerCase() :
+						null;
+			jQuery.expr.attrHandle[ name ] = fn;
+			return ret;
+		} :
+		function( elem, name, isXML ) {
+			return isXML ?
+				undefined :
+				elem[ jQuery.camelCase( "default-" + name ) ] ?
+					name.toLowerCase() :
+					null;
+		};
+});
+
+// fix oldIE attroperties
+if ( !getSetInput || !getSetAttribute ) {
+	jQuery.attrHooks.value = {
+		set: function( elem, value, name ) {
+			if ( jQuery.nodeName( elem, "input" ) ) {
+				// Does not return so that setAttribute is also used
+				elem.defaultValue = value;
+			} else {
+				// Use nodeHook if defined (#1954); otherwise setAttribute is fine
+				return nodeHook && nodeHook.set( elem, value, name );
+			}
+		}
+	};
+}
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+	// Use this for any attribute in IE6/7
+	// This fixes almost every IE6/7 issue
+	nodeHook = {
+		set: function( elem, value, name ) {
+			// Set the existing or create a new attribute node
+			var ret = elem.getAttributeNode( name );
+			if ( !ret ) {
+				elem.setAttributeNode(
+					(ret = elem.ownerDocument.createAttribute( name ))
+				);
+			}
+
+			ret.value = value += "";
+
+			// Break association with cloned elements by also using setAttribute (#9646)
+			return name === "value" || value === elem.getAttribute( name ) ?
+				value :
+				undefined;
+		}
+	};
+	jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords =
+		// Some attributes are constructed with empty-string values when not defined
+		function( elem, name, isXML ) {
+			var ret;
+			return isXML ?
+				undefined :
+				(ret = elem.getAttributeNode( name )) && ret.value !== "" ?
+					ret.value :
+					null;
+		};
+	jQuery.valHooks.button = {
+		get: function( elem, name ) {
+			var ret = elem.getAttributeNode( name );
+			return ret && ret.specified ?
+				ret.value :
+				undefined;
+		},
+		set: nodeHook.set
+	};
+
+	// Set contenteditable to false on removals(#10429)
+	// Setting to empty string throws an error as an invalid value
+	jQuery.attrHooks.contenteditable = {
+		set: function( elem, value, name ) {
+			nodeHook.set( elem, value === "" ? false : value, name );
+		}
+	};
+
+	// Set width and height to auto instead of 0 on empty string( Bug #8150 )
+	// This is for removals
+	jQuery.each([ "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = {
+			set: function( elem, value ) {
+				if ( value === "" ) {
+					elem.setAttribute( name, "auto" );
+					return value;
+				}
+			}
+		};
+	});
+}
+
+
+// Some attributes require a special call on IE
+// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !jQuery.support.hrefNormalized ) {
+	// href/src property should get the full normalized URL (#10299/#12915)
+	jQuery.each([ "href", "src" ], function( i, name ) {
+		jQuery.propHooks[ name ] = {
+			get: function( elem ) {
+				return elem.getAttribute( name, 4 );
+			}
+		};
+	});
+}
+
+if ( !jQuery.support.style ) {
+	jQuery.attrHooks.style = {
+		get: function( elem ) {
+			// Return undefined in the case of empty string
+			// Note: IE uppercases css property names, but if we were to .toLowerCase()
+			// .cssText, that would destroy case senstitivity in URL's, like in "background"
+			return elem.style.cssText || undefined;
+		},
+		set: function( elem, value ) {
+			return ( elem.style.cssText = value + "" );
+		}
+	};
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+	jQuery.propHooks.selected = {
+		get: function( elem ) {
+			var parent = elem.parentNode;
+
+			if ( parent ) {
+				parent.selectedIndex;
+
+				// Make sure that it also works with optgroups, see #5701
+				if ( parent.parentNode ) {
+					parent.parentNode.selectedIndex;
+				}
+			}
+			return null;
+		}
+	};
+}
+
+jQuery.each([
+	"tabIndex",
+	"readOnly",
+	"maxLength",
+	"cellSpacing",
+	"cellPadding",
+	"rowSpan",
+	"colSpan",
+	"useMap",
+	"frameBorder",
+	"contentEditable"
+], function() {
+	jQuery.propFix[ this.toLowerCase() ] = this;
+});
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+	jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+jQuery.each([ "radio", "checkbox" ], function() {
+	jQuery.valHooks[ this ] = {
+		set: function( elem, value ) {
+			if ( jQuery.isArray( value ) ) {
+				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+			}
+		}
+	};
+	if ( !jQuery.support.checkOn ) {
+		jQuery.valHooks[ this ].get = function( elem ) {
+			// Support: Webkit
+			// "" is returned instead of "on" if a value isn't specified
+			return elem.getAttribute("value") === null ? "on" : elem.value;
+		};
+	}
+});
+var rformElems = /^(?:input|select|textarea)$/i,
+	rkeyEvent = /^key/,
+	rmouseEvent = /^(?:mouse|contextmenu)|click/,
+	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+	rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
+
+function returnTrue() {
+	return true;
+}
+
+function returnFalse() {
+	return false;
+}
+
+function safeActiveElement() {
+	try {
+		return document.activeElement;
+	} catch ( err ) { }
+}
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+	global: {},
+
+	add: function( elem, types, handler, data, selector ) {
+		var tmp, events, t, handleObjIn,
+			special, eventHandle, handleObj,
+			handlers, type, namespaces, origType,
+			elemData = jQuery._data( elem );
+
+		// Don't attach events to noData or text/comment nodes (but allow plain objects)
+		if ( !elemData ) {
+			return;
+		}
+
+		// Caller can pass in an object of custom data in lieu of the handler
+		if ( handler.handler ) {
+			handleObjIn = handler;
+			handler = handleObjIn.handler;
+			selector = handleObjIn.selector;
+		}
+
+		// Make sure that the handler has a unique ID, used to find/remove it later
+		if ( !handler.guid ) {
+			handler.guid = jQuery.guid++;
+		}
+
+		// Init the element's event structure and main handler, if this is the first
+		if ( !(events = elemData.events) ) {
+			events = elemData.events = {};
+		}
+		if ( !(eventHandle = elemData.handle) ) {
+			eventHandle = elemData.handle = function( e ) {
+				// Discard the second event of a jQuery.event.trigger() and
+				// when an event is called after a page has unloaded
+				return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
+					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+					undefined;
+			};
+			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+			eventHandle.elem = elem;
+		}
+
+		// Handle multiple events separated by a space
+		types = ( types || "" ).match( core_rnotwhite ) || [""];
+		t = types.length;
+		while ( t-- ) {
+			tmp = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tmp[1];
+			namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+			// There *must* be a type, no attaching namespace-only handlers
+			if ( !type ) {
+				continue;
+			}
+
+			// If event changes its type, use the special event handlers for the changed type
+			special = jQuery.event.special[ type ] || {};
+
+			// If selector defined, determine special event api type, otherwise given type
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+
+			// Update special based on newly reset type
+			special = jQuery.event.special[ type ] || {};
+
+			// handleObj is passed to all event handlers
+			handleObj = jQuery.extend({
+				type: type,
+				origType: origType,
+				data: data,
+				handler: handler,
+				guid: handler.guid,
+				selector: selector,
+				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+				namespace: namespaces.join(".")
+			}, handleObjIn );
+
+			// Init the event handler queue if we're the first
+			if ( !(handlers = events[ type ]) ) {
+				handlers = events[ type ] = [];
+				handlers.delegateCount = 0;
+
+				// Only use addEventListener/attachEvent if the special events handler returns false
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					if ( elem.addEventListener ) {
+						elem.addEventListener( type, eventHandle, false );
+
+					} else if ( elem.attachEvent ) {
+						elem.attachEvent( "on" + type, eventHandle );
+					}
+				}
+			}
+
+			if ( special.add ) {
+				special.add.call( elem, handleObj );
+
+				if ( !handleObj.handler.guid ) {
+					handleObj.handler.guid = handler.guid;
+				}
+			}
+
+			// Add to the element's handler list, delegates in front
+			if ( selector ) {
+				handlers.splice( handlers.delegateCount++, 0, handleObj );
+			} else {
+				handlers.push( handleObj );
+			}
+
+			// Keep track of which events have ever been used, for event optimization
+			jQuery.event.global[ type ] = true;
+		}
+
+		// Nullify elem to prevent memory leaks in IE
+		elem = null;
+	},
+
+	// Detach an event or set of events from an element
+	remove: function( elem, types, handler, selector, mappedTypes ) {
+		var j, handleObj, tmp,
+			origCount, t, events,
+			special, handlers, type,
+			namespaces, origType,
+			elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+		if ( !elemData || !(events = elemData.events) ) {
+			return;
+		}
+
+		// Once for each type.namespace in types; type may be omitted
+		types = ( types || "" ).match( core_rnotwhite ) || [""];
+		t = types.length;
+		while ( t-- ) {
+			tmp = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tmp[1];
+			namespaces = ( tmp[2] || "" ).split( "." ).sort();
+
+			// Unbind all events (on this namespace, if provided) for the element
+			if ( !type ) {
+				for ( type in events ) {
+					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+				}
+				continue;
+			}
+
+			special = jQuery.event.special[ type ] || {};
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+			handlers = events[ type ] || [];
+			tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
+
+			// Remove matching events
+			origCount = j = handlers.length;
+			while ( j-- ) {
+				handleObj = handlers[ j ];
+
+				if ( ( mappedTypes || origType === handleObj.origType ) &&
+					( !handler || handler.guid === handleObj.guid ) &&
+					( !tmp || tmp.test( handleObj.namespace ) ) &&
+					( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+					handlers.splice( j, 1 );
+
+					if ( handleObj.selector ) {
+						handlers.delegateCount--;
+					}
+					if ( special.remove ) {
+						special.remove.call( elem, handleObj );
+					}
+				}
+			}
+
+			// Remove generic event handler if we removed something and no more handlers exist
+			// (avoids potential for endless recursion during removal of special event handlers)
+			if ( origCount && !handlers.length ) {
+				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+					jQuery.removeEvent( elem, type, elemData.handle );
+				}
+
+				delete events[ type ];
+			}
+		}
+
+		// Remove the expando if it's no longer used
+		if ( jQuery.isEmptyObject( events ) ) {
+			delete elemData.handle;
+
+			// removeData also checks for emptiness and clears the expando if empty
+			// so use it instead of delete
+			jQuery._removeData( elem, "events" );
+		}
+	},
+
+	trigger: function( event, data, elem, onlyHandlers ) {
+		var handle, ontype, cur,
+			bubbleType, special, tmp, i,
+			eventPath = [ elem || document ],
+			type = core_hasOwn.call( event, "type" ) ? event.type : event,
+			namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
+
+		cur = tmp = elem = elem || document;
+
+		// Don't do events on text and comment nodes
+		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+			return;
+		}
+
+		// focus/blur morphs to focusin/out; ensure we're not firing them right now
+		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+			return;
+		}
+
+		if ( type.indexOf(".") >= 0 ) {
+			// Namespaced trigger; create a regexp to match event type in handle()
+			namespaces = type.split(".");
+			type = namespaces.shift();
+			namespaces.sort();
+		}
+		ontype = type.indexOf(":") < 0 && "on" + type;
+
+		// Caller can pass in a jQuery.Event object, Object, or just an event type string
+		event = event[ jQuery.expando ] ?
+			event :
+			new jQuery.Event( type, typeof event === "object" && event );
+
+		// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
+		event.isTrigger = onlyHandlers ? 2 : 3;
+		event.namespace = namespaces.join(".");
+		event.namespace_re = event.namespace ?
+			new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
+			null;
+
+		// Clean up the event in case it is being reused
+		event.result = undefined;
+		if ( !event.target ) {
+			event.target = elem;
+		}
+
+		// Clone any incoming data and prepend the event, creating the handler arg list
+		data = data == null ?
+			[ event ] :
+			jQuery.makeArray( data, [ event ] );
+
+		// Allow special events to draw outside the lines
+		special = jQuery.event.special[ type ] || {};
+		if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
+			return;
+		}
+
+		// Determine event propagation path in advance, per W3C events spec (#9951)
+		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+			bubbleType = special.delegateType || type;
+			if ( !rfocusMorph.test( bubbleType + type ) ) {
+				cur = cur.parentNode;
+			}
+			for ( ; cur; cur = cur.parentNode ) {
+				eventPath.push( cur );
+				tmp = cur;
+			}
+
+			// Only add window if we got to document (e.g., not plain obj or detached DOM)
+			if ( tmp === (elem.ownerDocument || document) ) {
+				eventPath.push( tmp.defaultView || tmp.parentWindow || window );
+			}
+		}
+
+		// Fire handlers on the event path
+		i = 0;
+		while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
+
+			event.type = i > 1 ?
+				bubbleType :
+				special.bindType || type;
+
+			// jQuery handler
+			handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+			if ( handle ) {
+				handle.apply( cur, data );
+			}
+
+			// Native handler
+			handle = ontype && cur[ ontype ];
+			if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+				event.preventDefault();
+			}
+		}
+		event.type = type;
+
+		// If nobody prevented the default action, do it now
+		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+			if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&
+				jQuery.acceptData( elem ) ) {
+
+				// Call a native DOM method on the target with the same name name as the event.
+				// Can't use an .isFunction() check here because IE6/7 fails that test.
+				// Don't do default actions on window, that's where global variables be (#6170)
+				if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) {
+
+					// Don't re-trigger an onFOO event when we call its FOO() method
+					tmp = elem[ ontype ];
+
+					if ( tmp ) {
+						elem[ ontype ] = null;
+					}
+
+					// Prevent re-triggering of the same event, since we already bubbled it above
+					jQuery.event.triggered = type;
+					try {
+						elem[ type ]();
+					} catch ( e ) {
+						// IE<9 dies on focus/blur to hidden element (#1486,#12518)
+						// only reproducible on winXP IE8 native, not IE9 in IE8 mode
+					}
+					jQuery.event.triggered = undefined;
+
+					if ( tmp ) {
+						elem[ ontype ] = tmp;
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	dispatch: function( event ) {
+
+		// Make a writable jQuery.Event from the native event object
+		event = jQuery.event.fix( event );
+
+		var i, ret, handleObj, matched, j,
+			handlerQueue = [],
+			args = core_slice.call( arguments ),
+			handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
+			special = jQuery.event.special[ event.type ] || {};
+
+		// Use the fix-ed jQuery.Event rather than the (read-only) native event
+		args[0] = event;
+		event.delegateTarget = this;
+
+		// Call the preDispatch hook for the mapped type, and let it bail if desired
+		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+			return;
+		}
+
+		// Determine handlers
+		handlerQueue = jQuery.event.handlers.call( this, event, handlers );
+
+		// Run delegates first; they may want to stop propagation beneath us
+		i = 0;
+		while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
+			event.currentTarget = matched.elem;
+
+			j = 0;
+			while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
+
+				// Triggered event must either 1) have no namespace, or
+				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+				if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
+
+					event.handleObj = handleObj;
+					event.data = handleObj.data;
+
+					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+							.apply( matched.elem, args );
+
+					if ( ret !== undefined ) {
+						if ( (event.result = ret) === false ) {
+							event.preventDefault();
+							event.stopPropagation();
+						}
+					}
+				}
+			}
+		}
+
+		// Call the postDispatch hook for the mapped type
+		if ( special.postDispatch ) {
+			special.postDispatch.call( this, event );
+		}
+
+		return event.result;
+	},
+
+	handlers: function( event, handlers ) {
+		var sel, handleObj, matches, i,
+			handlerQueue = [],
+			delegateCount = handlers.delegateCount,
+			cur = event.target;
+
+		// Find delegate handlers
+		// Black-hole SVG <use> instance trees (#13180)
+		// Avoid non-left-click bubbling in Firefox (#3861)
+		if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
+
+			/* jshint eqeqeq: false */
+			for ( ; cur != this; cur = cur.parentNode || this ) {
+				/* jshint eqeqeq: true */
+
+				// Don't check non-elements (#13208)
+				// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
+				if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
+					matches = [];
+					for ( i = 0; i < delegateCount; i++ ) {
+						handleObj = handlers[ i ];
+
+						// Don't conflict with Object.prototype properties (#13203)
+						sel = handleObj.selector + " ";
+
+						if ( matches[ sel ] === undefined ) {
+							matches[ sel ] = handleObj.needsContext ?
+								jQuery( sel, this ).index( cur ) >= 0 :
+								jQuery.find( sel, this, null, [ cur ] ).length;
+						}
+						if ( matches[ sel ] ) {
+							matches.push( handleObj );
+						}
+					}
+					if ( matches.length ) {
+						handlerQueue.push({ elem: cur, handlers: matches });
+					}
+				}
+			}
+		}
+
+		// Add the remaining (directly-bound) handlers
+		if ( delegateCount < handlers.length ) {
+			handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
+		}
+
+		return handlerQueue;
+	},
+
+	fix: function( event ) {
+		if ( event[ jQuery.expando ] ) {
+			return event;
+		}
+
+		// Create a writable copy of the event object and normalize some properties
+		var i, prop, copy,
+			type = event.type,
+			originalEvent = event,
+			fixHook = this.fixHooks[ type ];
+
+		if ( !fixHook ) {
+			this.fixHooks[ type ] = fixHook =
+				rmouseEvent.test( type ) ? this.mouseHooks :
+				rkeyEvent.test( type ) ? this.keyHooks :
+				{};
+		}
+		copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+		event = new jQuery.Event( originalEvent );
+
+		i = copy.length;
+		while ( i-- ) {
+			prop = copy[ i ];
+			event[ prop ] = originalEvent[ prop ];
+		}
+
+		// Support: IE<9
+		// Fix target property (#1925)
+		if ( !event.target ) {
+			event.target = originalEvent.srcElement || document;
+		}
+
+		// Support: Chrome 23+, Safari?
+		// Target should not be a text node (#504, #13143)
+		if ( event.target.nodeType === 3 ) {
+			event.target = event.target.parentNode;
+		}
+
+		// Support: IE<9
+		// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
+		event.metaKey = !!event.metaKey;
+
+		return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
+	},
+
+	// Includes some event props shared by KeyEvent and MouseEvent
+	props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+	fixHooks: {},
+
+	keyHooks: {
+		props: "char charCode key keyCode".split(" "),
+		filter: function( event, original ) {
+
+			// Add which for key events
+			if ( event.which == null ) {
+				event.which = original.charCode != null ? original.charCode : original.keyCode;
+			}
+
+			return event;
+		}
+	},
+
+	mouseHooks: {
+		props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+		filter: function( event, original ) {
+			var body, eventDoc, doc,
+				button = original.button,
+				fromElement = original.fromElement;
+
+			// Calculate pageX/Y if missing and clientX/Y available
+			if ( event.pageX == null && original.clientX != null ) {
+				eventDoc = event.target.ownerDocument || document;
+				doc = eventDoc.documentElement;
+				body = eventDoc.body;
+
+				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+			}
+
+			// Add relatedTarget, if necessary
+			if ( !event.relatedTarget && fromElement ) {
+				event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+			}
+
+			// Add which for click: 1 === left; 2 === middle; 3 === right
+			// Note: button is not normalized, so don't use it
+			if ( !event.which && button !== undefined ) {
+				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+			}
+
+			return event;
+		}
+	},
+
+	special: {
+		load: {
+			// Prevent triggered image.load events from bubbling to window.load
+			noBubble: true
+		},
+		focus: {
+			// Fire native event if possible so blur/focus sequence is correct
+			trigger: function() {
+				if ( this !== safeActiveElement() && this.focus ) {
+					try {
+						this.focus();
+						return false;
+					} catch ( e ) {
+						// Support: IE<9
+						// If we error on focus to hidden element (#1486, #12518),
+						// let .trigger() run the handlers
+					}
+				}
+			},
+			delegateType: "focusin"
+		},
+		blur: {
+			trigger: function() {
+				if ( this === safeActiveElement() && this.blur ) {
+					this.blur();
+					return false;
+				}
+			},
+			delegateType: "focusout"
+		},
+		click: {
+			// For checkbox, fire native event so checked state will be right
+			trigger: function() {
+				if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
+					this.click();
+					return false;
+				}
+			},
+
+			// For cross-browser consistency, don't fire native .click() on links
+			_default: function( event ) {
+				return jQuery.nodeName( event.target, "a" );
+			}
+		},
+
+		beforeunload: {
+			postDispatch: function( event ) {
+
+				// Even when returnValue equals to undefined Firefox will still show alert
+				if ( event.result !== undefined ) {
+					event.originalEvent.returnValue = event.result;
+				}
+			}
+		}
+	},
+
+	simulate: function( type, elem, event, bubble ) {
+		// Piggyback on a donor event to simulate a different one.
+		// Fake originalEvent to avoid donor's stopPropagation, but if the
+		// simulated event prevents default then we do the same on the donor.
+		var e = jQuery.extend(
+			new jQuery.Event(),
+			event,
+			{
+				type: type,
+				isSimulated: true,
+				originalEvent: {}
+			}
+		);
+		if ( bubble ) {
+			jQuery.event.trigger( e, null, elem );
+		} else {
+			jQuery.event.dispatch.call( elem, e );
+		}
+		if ( e.isDefaultPrevented() ) {
+			event.preventDefault();
+		}
+	}
+};
+
+jQuery.removeEvent = document.removeEventListener ?
+	function( elem, type, handle ) {
+		if ( elem.removeEventListener ) {
+			elem.removeEventListener( type, handle, false );
+		}
+	} :
+	function( elem, type, handle ) {
+		var name = "on" + type;
+
+		if ( elem.detachEvent ) {
+
+			// #8545, #7054, preventing memory leaks for custom events in IE6-8
+			// detachEvent needed property on element, by name of that event, to properly expose it to GC
+			if ( typeof elem[ name ] === core_strundefined ) {
+				elem[ name ] = null;
+			}
+
+			elem.detachEvent( name, handle );
+		}
+	};
+
+jQuery.Event = function( src, props ) {
+	// Allow instantiation without the 'new' keyword
+	if ( !(this instanceof jQuery.Event) ) {
+		return new jQuery.Event( src, props );
+	}
+
+	// Event object
+	if ( src && src.type ) {
+		this.originalEvent = src;
+		this.type = src.type;
+
+		// Events bubbling up the document may have been marked as prevented
+		// by a handler lower down the tree; reflect the correct value.
+		this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+			src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+	// Event type
+	} else {
+		this.type = src;
+	}
+
+	// Put explicitly provided properties onto the event object
+	if ( props ) {
+		jQuery.extend( this, props );
+	}
+
+	// Create a timestamp if incoming event doesn't have one
+	this.timeStamp = src && src.timeStamp || jQuery.now();
+
+	// Mark it as fixed
+	this[ jQuery.expando ] = true;
+};
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+	isDefaultPrevented: returnFalse,
+	isPropagationStopped: returnFalse,
+	isImmediatePropagationStopped: returnFalse,
+
+	preventDefault: function() {
+		var e = this.originalEvent;
+
+		this.isDefaultPrevented = returnTrue;
+		if ( !e ) {
+			return;
+		}
+
+		// If preventDefault exists, run it on the original event
+		if ( e.preventDefault ) {
+			e.preventDefault();
+
+		// Support: IE
+		// Otherwise set the returnValue property of the original event to false
+		} else {
+			e.returnValue = false;
+		}
+	},
+	stopPropagation: function() {
+		var e = this.originalEvent;
+
+		this.isPropagationStopped = returnTrue;
+		if ( !e ) {
+			return;
+		}
+		// If stopPropagation exists, run it on the original event
+		if ( e.stopPropagation ) {
+			e.stopPropagation();
+		}
+
+		// Support: IE
+		// Set the cancelBubble property of the original event to true
+		e.cancelBubble = true;
+	},
+	stopImmediatePropagation: function() {
+		this.isImmediatePropagationStopped = returnTrue;
+		this.stopPropagation();
+	}
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+	mouseenter: "mouseover",
+	mouseleave: "mouseout"
+}, function( orig, fix ) {
+	jQuery.event.special[ orig ] = {
+		delegateType: fix,
+		bindType: fix,
+
+		handle: function( event ) {
+			var ret,
+				target = this,
+				related = event.relatedTarget,
+				handleObj = event.handleObj;
+
+			// For mousenter/leave call the handler if related is outside the target.
+			// NB: No relatedTarget if the mouse left/entered the browser window
+			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+				event.type = handleObj.origType;
+				ret = handleObj.handler.apply( this, arguments );
+				event.type = fix;
+			}
+			return ret;
+		}
+	};
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+	jQuery.event.special.submit = {
+		setup: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Lazy-add a submit handler when a descendant form may potentially be submitted
+			jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+				// Node name check avoids a VML-related crash in IE (#9807)
+				var elem = e.target,
+					form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+				if ( form && !jQuery._data( form, "submitBubbles" ) ) {
+					jQuery.event.add( form, "submit._submit", function( event ) {
+						event._submit_bubble = true;
+					});
+					jQuery._data( form, "submitBubbles", true );
+				}
+			});
+			// return undefined since we don't need an event listener
+		},
+
+		postDispatch: function( event ) {
+			// If form was submitted by the user, bubble the event up the tree
+			if ( event._submit_bubble ) {
+				delete event._submit_bubble;
+				if ( this.parentNode && !event.isTrigger ) {
+					jQuery.event.simulate( "submit", this.parentNode, event, true );
+				}
+			}
+		},
+
+		teardown: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+			jQuery.event.remove( this, "._submit" );
+		}
+	};
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+	jQuery.event.special.change = {
+
+		setup: function() {
+
+			if ( rformElems.test( this.nodeName ) ) {
+				// IE doesn't fire change on a check/radio until blur; trigger it on click
+				// after a propertychange. Eat the blur-change in special.change.handle.
+				// This still fires onchange a second time for check/radio after blur.
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					jQuery.event.add( this, "propertychange._change", function( event ) {
+						if ( event.originalEvent.propertyName === "checked" ) {
+							this._just_changed = true;
+						}
+					});
+					jQuery.event.add( this, "click._change", function( event ) {
+						if ( this._just_changed && !event.isTrigger ) {
+							this._just_changed = false;
+						}
+						// Allow triggered, simulated change events (#11500)
+						jQuery.event.simulate( "change", this, event, true );
+					});
+				}
+				return false;
+			}
+			// Delegated event; lazy-add a change handler on descendant inputs
+			jQuery.event.add( this, "beforeactivate._change", function( e ) {
+				var elem = e.target;
+
+				if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) {
+					jQuery.event.add( elem, "change._change", function( event ) {
+						if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+							jQuery.event.simulate( "change", this.parentNode, event, true );
+						}
+					});
+					jQuery._data( elem, "changeBubbles", true );
+				}
+			});
+		},
+
+		handle: function( event ) {
+			var elem = event.target;
+
+			// Swallow native change events from checkbox/radio, we already triggered them above
+			if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+				return event.handleObj.handler.apply( this, arguments );
+			}
+		},
+
+		teardown: function() {
+			jQuery.event.remove( this, "._change" );
+
+			return !rformElems.test( this.nodeName );
+		}
+	};
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+		// Attach a single capturing handler while someone wants focusin/focusout
+		var attaches = 0,
+			handler = function( event ) {
+				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+			};
+
+		jQuery.event.special[ fix ] = {
+			setup: function() {
+				if ( attaches++ === 0 ) {
+					document.addEventListener( orig, handler, true );
+				}
+			},
+			teardown: function() {
+				if ( --attaches === 0 ) {
+					document.removeEventListener( orig, handler, true );
+				}
+			}
+		};
+	});
+}
+
+jQuery.fn.extend({
+
+	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+		var type, origFn;
+
+		// Types can be a map of types/handlers
+		if ( typeof types === "object" ) {
+			// ( types-Object, selector, data )
+			if ( typeof selector !== "string" ) {
+				// ( types-Object, data )
+				data = data || selector;
+				selector = undefined;
+			}
+			for ( type in types ) {
+				this.on( type, selector, data, types[ type ], one );
+			}
+			return this;
+		}
+
+		if ( data == null && fn == null ) {
+			// ( types, fn )
+			fn = selector;
+			data = selector = undefined;
+		} else if ( fn == null ) {
+			if ( typeof selector === "string" ) {
+				// ( types, selector, fn )
+				fn = data;
+				data = undefined;
+			} else {
+				// ( types, data, fn )
+				fn = data;
+				data = selector;
+				selector = undefined;
+			}
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		} else if ( !fn ) {
+			return this;
+		}
+
+		if ( one === 1 ) {
+			origFn = fn;
+			fn = function( event ) {
+				// Can use an empty set, since event contains the info
+				jQuery().off( event );
+				return origFn.apply( this, arguments );
+			};
+			// Use same guid so caller can remove using origFn
+			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+		}
+		return this.each( function() {
+			jQuery.event.add( this, types, fn, data, selector );
+		});
+	},
+	one: function( types, selector, data, fn ) {
+		return this.on( types, selector, data, fn, 1 );
+	},
+	off: function( types, selector, fn ) {
+		var handleObj, type;
+		if ( types && types.preventDefault && types.handleObj ) {
+			// ( event )  dispatched jQuery.Event
+			handleObj = types.handleObj;
+			jQuery( types.delegateTarget ).off(
+				handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+				handleObj.selector,
+				handleObj.handler
+			);
+			return this;
+		}
+		if ( typeof types === "object" ) {
+			// ( types-object [, selector] )
+			for ( type in types ) {
+				this.off( type, selector, types[ type ] );
+			}
+			return this;
+		}
+		if ( selector === false || typeof selector === "function" ) {
+			// ( types [, fn] )
+			fn = selector;
+			selector = undefined;
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		}
+		return this.each(function() {
+			jQuery.event.remove( this, types, fn, selector );
+		});
+	},
+
+	trigger: function( type, data ) {
+		return this.each(function() {
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	triggerHandler: function( type, data ) {
+		var elem = this[0];
+		if ( elem ) {
+			return jQuery.event.trigger( type, data, elem, true );
+		}
+	}
+});
+var isSimple = /^.[^:#\[\.,]*$/,
+	rparentsprev = /^(?:parents|prev(?:Until|All))/,
+	rneedsContext = jQuery.expr.match.needsContext,
+	// methods guaranteed to produce a unique set when starting from a unique set
+	guaranteedUnique = {
+		children: true,
+		contents: true,
+		next: true,
+		prev: true
+	};
+
+jQuery.fn.extend({
+	find: function( selector ) {
+		var i,
+			ret = [],
+			self = this,
+			len = self.length;
+
+		if ( typeof selector !== "string" ) {
+			return this.pushStack( jQuery( selector ).filter(function() {
+				for ( i = 0; i < len; i++ ) {
+					if ( jQuery.contains( self[ i ], this ) ) {
+						return true;
+					}
+				}
+			}) );
+		}
+
+		for ( i = 0; i < len; i++ ) {
+			jQuery.find( selector, self[ i ], ret );
+		}
+
+		// Needed because $( selector, context ) becomes $( context ).find( selector )
+		ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
+		ret.selector = this.selector ? this.selector + " " + selector : selector;
+		return ret;
+	},
+
+	has: function( target ) {
+		var i,
+			targets = jQuery( target, this ),
+			len = targets.length;
+
+		return this.filter(function() {
+			for ( i = 0; i < len; i++ ) {
+				if ( jQuery.contains( this, targets[i] ) ) {
+					return true;
+				}
+			}
+		});
+	},
+
+	not: function( selector ) {
+		return this.pushStack( winnow(this, selector || [], true) );
+	},
+
+	filter: function( selector ) {
+		return this.pushStack( winnow(this, selector || [], false) );
+	},
+
+	is: function( selector ) {
+		return !!winnow(
+			this,
+
+			// If this is a positional/relative selector, check membership in the returned set
+			// so $("p:first").is("p:last") won't return true for a doc with two "p".
+			typeof selector === "string" && rneedsContext.test( selector ) ?
+				jQuery( selector ) :
+				selector || [],
+			false
+		).length;
+	},
+
+	closest: function( selectors, context ) {
+		var cur,
+			i = 0,
+			l = this.length,
+			ret = [],
+			pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+				jQuery( selectors, context || this.context ) :
+				0;
+
+		for ( ; i < l; i++ ) {
+			for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {
+				// Always skip document fragments
+				if ( cur.nodeType < 11 && (pos ?
+					pos.index(cur) > -1 :
+
+					// Don't pass non-elements to Sizzle
+					cur.nodeType === 1 &&
+						jQuery.find.matchesSelector(cur, selectors)) ) {
+
+					cur = ret.push( cur );
+					break;
+				}
+			}
+		}
+
+		return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+
+		// No argument, return index in parent
+		if ( !elem ) {
+			return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1;
+		}
+
+		// index in selector
+		if ( typeof elem === "string" ) {
+			return jQuery.inArray( this[0], jQuery( elem ) );
+		}
+
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem.jquery ? elem[0] : elem, this );
+	},
+
+	add: function( selector, context ) {
+		var set = typeof selector === "string" ?
+				jQuery( selector, context ) :
+				jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+			all = jQuery.merge( this.get(), set );
+
+		return this.pushStack( jQuery.unique(all) );
+	},
+
+	addBack: function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter(selector)
+		);
+	}
+});
+
+function sibling( cur, dir ) {
+	do {
+		cur = cur[ dir ];
+	} while ( cur && cur.nodeType !== 1 );
+
+	return cur;
+}
+
+jQuery.each({
+	parent: function( elem ) {
+		var parent = elem.parentNode;
+		return parent && parent.nodeType !== 11 ? parent : null;
+	},
+	parents: function( elem ) {
+		return jQuery.dir( elem, "parentNode" );
+	},
+	parentsUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "parentNode", until );
+	},
+	next: function( elem ) {
+		return sibling( elem, "nextSibling" );
+	},
+	prev: function( elem ) {
+		return sibling( elem, "previousSibling" );
+	},
+	nextAll: function( elem ) {
+		return jQuery.dir( elem, "nextSibling" );
+	},
+	prevAll: function( elem ) {
+		return jQuery.dir( elem, "previousSibling" );
+	},
+	nextUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "nextSibling", until );
+	},
+	prevUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "previousSibling", until );
+	},
+	siblings: function( elem ) {
+		return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+	},
+	children: function( elem ) {
+		return jQuery.sibling( elem.firstChild );
+	},
+	contents: function( elem ) {
+		return jQuery.nodeName( elem, "iframe" ) ?
+			elem.contentDocument || elem.contentWindow.document :
+			jQuery.merge( [], elem.childNodes );
+	}
+}, function( name, fn ) {
+	jQuery.fn[ name ] = function( until, selector ) {
+		var ret = jQuery.map( this, fn, until );
+
+		if ( name.slice( -5 ) !== "Until" ) {
+			selector = until;
+		}
+
+		if ( selector && typeof selector === "string" ) {
+			ret = jQuery.filter( selector, ret );
+		}
+
+		if ( this.length > 1 ) {
+			// Remove duplicates
+			if ( !guaranteedUnique[ name ] ) {
+				ret = jQuery.unique( ret );
+			}
+
+			// Reverse order for parents* and prev-derivatives
+			if ( rparentsprev.test( name ) ) {
+				ret = ret.reverse();
+			}
+		}
+
+		return this.pushStack( ret );
+	};
+});
+
+jQuery.extend({
+	filter: function( expr, elems, not ) {
+		var elem = elems[ 0 ];
+
+		if ( not ) {
+			expr = ":not(" + expr + ")";
+		}
+
+		return elems.length === 1 && elem.nodeType === 1 ?
+			jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
+			jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
+				return elem.nodeType === 1;
+			}));
+	},
+
+	dir: function( elem, dir, until ) {
+		var matched = [],
+			cur = elem[ dir ];
+
+		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+			if ( cur.nodeType === 1 ) {
+				matched.push( cur );
+			}
+			cur = cur[dir];
+		}
+		return matched;
+	},
+
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType === 1 && n !== elem ) {
+				r.push( n );
+			}
+		}
+
+		return r;
+	}
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, not ) {
+	if ( jQuery.isFunction( qualifier ) ) {
+		return jQuery.grep( elements, function( elem, i ) {
+			/* jshint -W018 */
+			return !!qualifier.call( elem, i, elem ) !== not;
+		});
+
+	}
+
+	if ( qualifier.nodeType ) {
+		return jQuery.grep( elements, function( elem ) {
+			return ( elem === qualifier ) !== not;
+		});
+
+	}
+
+	if ( typeof qualifier === "string" ) {
+		if ( isSimple.test( qualifier ) ) {
+			return jQuery.filter( qualifier, elements, not );
+		}
+
+		qualifier = jQuery.filter( qualifier, elements );
+	}
+
+	return jQuery.grep( elements, function( elem ) {
+		return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not;
+	});
+}
+function createSafeFragment( document ) {
+	var list = nodeNames.split( "|" ),
+		safeFrag = document.createDocumentFragment();
+
+	if ( safeFrag.createElement ) {
+		while ( list.length ) {
+			safeFrag.createElement(
+				list.pop()
+			);
+		}
+	}
+	return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+	rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+	rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+	rleadingWhitespace = /^\s+/,
+	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+	rtagName = /<([\w:]+)/,
+	rtbody = /<tbody/i,
+	rhtml = /<|&#?\w+;/,
+	rnoInnerhtml = /<(?:script|style|link)/i,
+	manipulation_rcheckableType = /^(?:checkbox|radio)$/i,
+	// checked="checked" or checked
+	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+	rscriptType = /^$|\/(?:java|ecma)script/i,
+	rscriptTypeMasked = /^true\/(.*)/,
+	rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
+
+	// We have to close these tags to support XHTML (#13200)
+	wrapMap = {
+		option: [ 1, "<select multiple='multiple'>", "</select>" ],
+		legend: [ 1, "<fieldset>", "</fieldset>" ],
+		area: [ 1, "<map>", "</map>" ],
+		param: [ 1, "<object>", "</object>" ],
+		thead: [ 1, "<table>", "</table>" ],
+		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+
+		// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+		// unless wrapped in a div with non-breaking characters in front of it.
+		_default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>"  ]
+	},
+	safeFragment = createSafeFragment( document ),
+	fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+jQuery.fn.extend({
+	text: function( value ) {
+		return jQuery.access( this, function( value ) {
+			return value === undefined ?
+				jQuery.text( this ) :
+				this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+		}, null, value, arguments.length );
+	},
+
+	append: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+				var target = manipulationTarget( this, elem );
+				target.appendChild( elem );
+			}
+		});
+	},
+
+	prepend: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
+				var target = manipulationTarget( this, elem );
+				target.insertBefore( elem, target.firstChild );
+			}
+		});
+	},
+
+	before: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.parentNode ) {
+				this.parentNode.insertBefore( elem, this );
+			}
+		});
+	},
+
+	after: function() {
+		return this.domManip( arguments, function( elem ) {
+			if ( this.parentNode ) {
+				this.parentNode.insertBefore( elem, this.nextSibling );
+			}
+		});
+	},
+
+	// keepData is for internal use only--do not document
+	remove: function( selector, keepData ) {
+		var elem,
+			elems = selector ? jQuery.filter( selector, this ) : this,
+			i = 0;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( !keepData && elem.nodeType === 1 ) {
+				jQuery.cleanData( getAll( elem ) );
+			}
+
+			if ( elem.parentNode ) {
+				if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
+					setGlobalEval( getAll( elem, "script" ) );
+				}
+				elem.parentNode.removeChild( elem );
+			}
+		}
+
+		return this;
+	},
+
+	empty: function() {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			// Remove element nodes and prevent memory leaks
+			if ( elem.nodeType === 1 ) {
+				jQuery.cleanData( getAll( elem, false ) );
+			}
+
+			// Remove any remaining nodes
+			while ( elem.firstChild ) {
+				elem.removeChild( elem.firstChild );
+			}
+
+			// If this is a select, ensure that it displays empty (#12336)
+			// Support: IE<9
+			if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
+				elem.options.length = 0;
+			}
+		}
+
+		return this;
+	},
+
+	clone: function( dataAndEvents, deepDataAndEvents ) {
+		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+		return this.map( function () {
+			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+		});
+	},
+
+	html: function( value ) {
+		return jQuery.access( this, function( value ) {
+			var elem = this[0] || {},
+				i = 0,
+				l = this.length;
+
+			if ( value === undefined ) {
+				return elem.nodeType === 1 ?
+					elem.innerHTML.replace( rinlinejQuery, "" ) :
+					undefined;
+			}
+
+			// See if we can take a shortcut and just use innerHTML
+			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+				( jQuery.support.htmlSerialize || !rnoshimcache.test( value )  ) &&
+				( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+				!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+				value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+				try {
+					for (; i < l; i++ ) {
+						// Remove element nodes and prevent memory leaks
+						elem = this[i] || {};
+						if ( elem.nodeType === 1 ) {
+							jQuery.cleanData( getAll( elem, false ) );
+							elem.innerHTML = value;
+						}
+					}
+
+					elem = 0;
+
+				// If using innerHTML throws an exception, use the fallback method
+				} catch(e) {}
+			}
+
+			if ( elem ) {
+				this.empty().append( value );
+			}
+		}, null, value, arguments.length );
+	},
+
+	replaceWith: function() {
+		var
+			// Snapshot the DOM in case .domManip sweeps something relevant into its fragment
+			args = jQuery.map( this, function( elem ) {
+				return [ elem.nextSibling, elem.parentNode ];
+			}),
+			i = 0;
+
+		// Make the changes, replacing each context element with the new content
+		this.domManip( arguments, function( elem ) {
+			var next = args[ i++ ],
+				parent = args[ i++ ];
+
+			if ( parent ) {
+				// Don't use the snapshot next if it has moved (#13810)
+				if ( next && next.parentNode !== parent ) {
+					next = this.nextSibling;
+				}
+				jQuery( this ).remove();
+				parent.insertBefore( elem, next );
+			}
+		// Allow new content to include elements from the context set
+		}, true );
+
+		// Force removal if there was no new content (e.g., from empty arguments)
+		return i ? this : this.remove();
+	},
+
+	detach: function( selector ) {
+		return this.remove( selector, true );
+	},
+
+	domManip: function( args, callback, allowIntersection ) {
+
+		// Flatten any nested arrays
+		args = core_concat.apply( [], args );
+
+		var first, node, hasScripts,
+			scripts, doc, fragment,
+			i = 0,
+			l = this.length,
+			set = this,
+			iNoClone = l - 1,
+			value = args[0],
+			isFunction = jQuery.isFunction( value );
+
+		// We can't cloneNode fragments that contain checked, in WebKit
+		if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {
+			return this.each(function( index ) {
+				var self = set.eq( index );
+				if ( isFunction ) {
+					args[0] = value.call( this, index, self.html() );
+				}
+				self.domManip( args, callback, allowIntersection );
+			});
+		}
+
+		if ( l ) {
+			fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this );
+			first = fragment.firstChild;
+
+			if ( fragment.childNodes.length === 1 ) {
+				fragment = first;
+			}
+
+			if ( first ) {
+				scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
+				hasScripts = scripts.length;
+
+				// Use the original fragment for the last item instead of the first because it can end up
+				// being emptied incorrectly in certain situations (#8070).
+				for ( ; i < l; i++ ) {
+					node = fragment;
+
+					if ( i !== iNoClone ) {
+						node = jQuery.clone( node, true, true );
+
+						// Keep references to cloned scripts for later restoration
+						if ( hasScripts ) {
+							jQuery.merge( scripts, getAll( node, "script" ) );
+						}
+					}
+
+					callback.call( this[i], node, i );
+				}
+
+				if ( hasScripts ) {
+					doc = scripts[ scripts.length - 1 ].ownerDocument;
+
+					// Reenable scripts
+					jQuery.map( scripts, restoreScript );
+
+					// Evaluate executable scripts on first document insertion
+					for ( i = 0; i < hasScripts; i++ ) {
+						node = scripts[ i ];
+						if ( rscriptType.test( node.type || "" ) &&
+							!jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
+
+							if ( node.src ) {
+								// Hope ajax is available...
+								jQuery._evalUrl( node.src );
+							} else {
+								jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );
+							}
+						}
+					}
+				}
+
+				// Fix #11809: Avoid leaking memory
+				fragment = first = null;
+			}
+		}
+
+		return this;
+	}
+});
+
+// Support: IE<8
+// Manipulating tables requires a tbody
+function manipulationTarget( elem, content ) {
+	return jQuery.nodeName( elem, "table" ) &&
+		jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ?
+
+		elem.getElementsByTagName("tbody")[0] ||
+			elem.appendChild( elem.ownerDocument.createElement("tbody") ) :
+		elem;
+}
+
+// Replace/restore the type attribute of script elements for safe DOM manipulation
+function disableScript( elem ) {
+	elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type;
+	return elem;
+}
+function restoreScript( elem ) {
+	var match = rscriptTypeMasked.exec( elem.type );
+	if ( match ) {
+		elem.type = match[1];
+	} else {
+		elem.removeAttribute("type");
+	}
+	return elem;
+}
+
+// Mark scripts as having already been evaluated
+function setGlobalEval( elems, refElements ) {
+	var elem,
+		i = 0;
+	for ( ; (elem = elems[i]) != null; i++ ) {
+		jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) );
+	}
+}
+
+function cloneCopyEvent( src, dest ) {
+
+	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+		return;
+	}
+
+	var type, i, l,
+		oldData = jQuery._data( src ),
+		curData = jQuery._data( dest, oldData ),
+		events = oldData.events;
+
+	if ( events ) {
+		delete curData.handle;
+		curData.events = {};
+
+		for ( type in events ) {
+			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+				jQuery.event.add( dest, type, events[ type ][ i ] );
+			}
+		}
+	}
+
+	// make the cloned public data object a copy from the original
+	if ( curData.data ) {
+		curData.data = jQuery.extend( {}, curData.data );
+	}
+}
+
+function fixCloneNodeIssues( src, dest ) {
+	var nodeName, e, data;
+
+	// We do not need to do anything for non-Elements
+	if ( dest.nodeType !== 1 ) {
+		return;
+	}
+
+	nodeName = dest.nodeName.toLowerCase();
+
+	// IE6-8 copies events bound via attachEvent when using cloneNode.
+	if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) {
+		data = jQuery._data( dest );
+
+		for ( e in data.events ) {
+			jQuery.removeEvent( dest, e, data.handle );
+		}
+
+		// Event data gets referenced instead of copied if the expando gets copied too
+		dest.removeAttribute( jQuery.expando );
+	}
+
+	// IE blanks contents when cloning scripts, and tries to evaluate newly-set text
+	if ( nodeName === "script" && dest.text !== src.text ) {
+		disableScript( dest ).text = src.text;
+		restoreScript( dest );
+
+	// IE6-10 improperly clones children of object elements using classid.
+	// IE10 throws NoModificationAllowedError if parent is null, #12132.
+	} else if ( nodeName === "object" ) {
+		if ( dest.parentNode ) {
+			dest.outerHTML = src.outerHTML;
+		}
+
+		// This path appears unavoidable for IE9. When cloning an object
+		// element in IE9, the outerHTML strategy above is not sufficient.
+		// If the src has innerHTML and the destination does not,
+		// copy the src.innerHTML into the dest.innerHTML. #10324
+		if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) {
+			dest.innerHTML = src.innerHTML;
+		}
+
+	} else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) {
+		// IE6-8 fails to persist the checked state of a cloned checkbox
+		// or radio button. Worse, IE6-7 fail to give the cloned element
+		// a checked appearance if the defaultChecked value isn't also set
+
+		dest.defaultChecked = dest.checked = src.checked;
+
+		// IE6-7 get confused and end up setting the value of a cloned
+		// checkbox/radio button to an empty string instead of "on"
+		if ( dest.value !== src.value ) {
+			dest.value = src.value;
+		}
+
+	// IE6-8 fails to return the selected option to the default selected
+	// state when cloning options
+	} else if ( nodeName === "option" ) {
+		dest.defaultSelected = dest.selected = src.defaultSelected;
+
+	// IE6-8 fails to set the defaultValue to the correct value when
+	// cloning other types of input fields
+	} else if ( nodeName === "input" || nodeName === "textarea" ) {
+		dest.defaultValue = src.defaultValue;
+	}
+}
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function( name, original ) {
+	jQuery.fn[ name ] = function( selector ) {
+		var elems,
+			i = 0,
+			ret = [],
+			insert = jQuery( selector ),
+			last = insert.length - 1;
+
+		for ( ; i <= last; i++ ) {
+			elems = i === last ? this : this.clone(true);
+			jQuery( insert[i] )[ original ]( elems );
+
+			// Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get()
+			core_push.apply( ret, elems.get() );
+		}
+
+		return this.pushStack( ret );
+	};
+});
+
+function getAll( context, tag ) {
+	var elems, elem,
+		i = 0,
+		found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) :
+			typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) :
+			undefined;
+
+	if ( !found ) {
+		for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) {
+			if ( !tag || jQuery.nodeName( elem, tag ) ) {
+				found.push( elem );
+			} else {
+				jQuery.merge( found, getAll( elem, tag ) );
+			}
+		}
+	}
+
+	return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
+		jQuery.merge( [ context ], found ) :
+		found;
+}
+
+// Used in buildFragment, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+	if ( manipulation_rcheckableType.test( elem.type ) ) {
+		elem.defaultChecked = elem.checked;
+	}
+}
+
+jQuery.extend({
+	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+		var destElements, node, clone, i, srcElements,
+			inPage = jQuery.contains( elem.ownerDocument, elem );
+
+		if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+			clone = elem.cloneNode( true );
+
+		// IE<=8 does not properly clone detached, unknown element nodes
+		} else {
+			fragmentDiv.innerHTML = elem.outerHTML;
+			fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+		}
+
+		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+
+			// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
+			destElements = getAll( clone );
+			srcElements = getAll( elem );
+
+			// Fix all IE cloning issues
+			for ( i = 0; (node = srcElements[i]) != null; ++i ) {
+				// Ensure that the destination node is not null; Fixes #9587
+				if ( destElements[i] ) {
+					fixCloneNodeIssues( node, destElements[i] );
+				}
+			}
+		}
+
+		// Copy the events from the original to the clone
+		if ( dataAndEvents ) {
+			if ( deepDataAndEvents ) {
+				srcElements = srcElements || getAll( elem );
+				destElements = destElements || getAll( clone );
+
+				for ( i = 0; (node = srcElements[i]) != null; i++ ) {
+					cloneCopyEvent( node, destElements[i] );
+				}
+			} else {
+				cloneCopyEvent( elem, clone );
+			}
+		}
+
+		// Preserve script evaluation history
+		destElements = getAll( clone, "script" );
+		if ( destElements.length > 0 ) {
+			setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
+		}
+
+		destElements = srcElements = node = null;
+
+		// Return the cloned set
+		return clone;
+	},
+
+	buildFragment: function( elems, context, scripts, selection ) {
+		var j, elem, contains,
+			tmp, tag, tbody, wrap,
+			l = elems.length,
+
+			// Ensure a safe fragment
+			safe = createSafeFragment( context ),
+
+			nodes = [],
+			i = 0;
+
+		for ( ; i < l; i++ ) {
+			elem = elems[ i ];
+
+			if ( elem || elem === 0 ) {
+
+				// Add nodes directly
+				if ( jQuery.type( elem ) === "object" ) {
+					jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
+
+				// Convert non-html into a text node
+				} else if ( !rhtml.test( elem ) ) {
+					nodes.push( context.createTextNode( elem ) );
+
+				// Convert html into DOM nodes
+				} else {
+					tmp = tmp || safe.appendChild( context.createElement("div") );
+
+					// Deserialize a standard representation
+					tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+					wrap = wrapMap[ tag ] || wrapMap._default;
+
+					tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2];
+
+					// Descend through wrappers to the right content
+					j = wrap[0];
+					while ( j-- ) {
+						tmp = tmp.lastChild;
+					}
+
+					// Manually add leading whitespace removed by IE
+					if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+						nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) );
+					}
+
+					// Remove IE's autoinserted <tbody> from table fragments
+					if ( !jQuery.support.tbody ) {
+
+						// String was a <table>, *may* have spurious <tbody>
+						elem = tag === "table" && !rtbody.test( elem ) ?
+							tmp.firstChild :
+
+							// String was a bare <thead> or <tfoot>
+							wrap[1] === "<table>" && !rtbody.test( elem ) ?
+								tmp :
+								0;
+
+						j = elem && elem.childNodes.length;
+						while ( j-- ) {
+							if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) {
+								elem.removeChild( tbody );
+							}
+						}
+					}
+
+					jQuery.merge( nodes, tmp.childNodes );
+
+					// Fix #12392 for WebKit and IE > 9
+					tmp.textContent = "";
+
+					// Fix #12392 for oldIE
+					while ( tmp.firstChild ) {
+						tmp.removeChild( tmp.firstChild );
+					}
+
+					// Remember the top-level container for proper cleanup
+					tmp = safe.lastChild;
+				}
+			}
+		}
+
+		// Fix #11356: Clear elements from fragment
+		if ( tmp ) {
+			safe.removeChild( tmp );
+		}
+
+		// Reset defaultChecked for any radios and checkboxes
+		// about to be appended to the DOM in IE 6/7 (#8060)
+		if ( !jQuery.support.appendChecked ) {
+			jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked );
+		}
+
+		i = 0;
+		while ( (elem = nodes[ i++ ]) ) {
+
+			// #4087 - If origin and destination elements are the same, and this is
+			// that element, do not do anything
+			if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
+				continue;
+			}
+
+			contains = jQuery.contains( elem.ownerDocument, elem );
+
+			// Append to fragment
+			tmp = getAll( safe.appendChild( elem ), "script" );
+
+			// Preserve script evaluation history
+			if ( contains ) {
+				setGlobalEval( tmp );
+			}
+
+			// Capture executables
+			if ( scripts ) {
+				j = 0;
+				while ( (elem = tmp[ j++ ]) ) {
+					if ( rscriptType.test( elem.type || "" ) ) {
+						scripts.push( elem );
+					}
+				}
+			}
+		}
+
+		tmp = null;
+
+		return safe;
+	},
+
+	cleanData: function( elems, /* internal */ acceptData ) {
+		var elem, type, id, data,
+			i = 0,
+			internalKey = jQuery.expando,
+			cache = jQuery.cache,
+			deleteExpando = jQuery.support.deleteExpando,
+			special = jQuery.event.special;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( acceptData || jQuery.acceptData( elem ) ) {
+
+				id = elem[ internalKey ];
+				data = id && cache[ id ];
+
+				if ( data ) {
+					if ( data.events ) {
+						for ( type in data.events ) {
+							if ( special[ type ] ) {
+								jQuery.event.remove( elem, type );
+
+							// This is a shortcut to avoid jQuery.event.remove's overhead
+							} else {
+								jQuery.removeEvent( elem, type, data.handle );
+							}
+						}
+					}
+
+					// Remove cache only if it was not already removed by jQuery.event.remove
+					if ( cache[ id ] ) {
+
+						delete cache[ id ];
+
+						// IE does not allow us to delete expando properties from nodes,
+						// nor does it have a removeAttribute function on Document nodes;
+						// we must handle all of these cases
+						if ( deleteExpando ) {
+							delete elem[ internalKey ];
+
+						} else if ( typeof elem.removeAttribute !== core_strundefined ) {
+							elem.removeAttribute( internalKey );
+
+						} else {
+							elem[ internalKey ] = null;
+						}
+
+						core_deletedIds.push( id );
+					}
+				}
+			}
+		}
+	},
+
+	_evalUrl: function( url ) {
+		return jQuery.ajax({
+			url: url,
+			type: "GET",
+			dataType: "script",
+			async: false,
+			global: false,
+			"throws": true
+		});
+	}
+});
+jQuery.fn.extend({
+	wrapAll: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapAll( html.call(this, i) );
+			});
+		}
+
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+			if ( this[0].parentNode ) {
+				wrap.insertBefore( this[0] );
+			}
+
+			wrap.map(function() {
+				var elem = this;
+
+				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+					elem = elem.firstChild;
+				}
+
+				return elem;
+			}).append( this );
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapInner( html.call(this, i) );
+			});
+		}
+
+		return this.each(function() {
+			var self = jQuery( this ),
+				contents = self.contents();
+
+			if ( contents.length ) {
+				contents.wrapAll( html );
+
+			} else {
+				self.append( html );
+			}
+		});
+	},
+
+	wrap: function( html ) {
+		var isFunction = jQuery.isFunction( html );
+
+		return this.each(function(i) {
+			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+		});
+	},
+
+	unwrap: function() {
+		return this.parent().each(function() {
+			if ( !jQuery.nodeName( this, "body" ) ) {
+				jQuery( this ).replaceWith( this.childNodes );
+			}
+		}).end();
+	}
+});
+var iframe, getStyles, curCSS,
+	ralpha = /alpha\([^)]*\)/i,
+	ropacity = /opacity\s*=\s*([^)]*)/,
+	rposition = /^(top|right|bottom|left)$/,
+	// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+	// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+	rmargin = /^margin/,
+	rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+	rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+	rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ),
+	elemdisplay = { BODY: "block" },
+
+	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+	cssNormalTransform = {
+		letterSpacing: 0,
+		fontWeight: 400
+	},
+
+	cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+	cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+	// shortcut for names that are not vendor prefixed
+	if ( name in style ) {
+		return name;
+	}
+
+	// check for vendor prefixed names
+	var capName = name.charAt(0).toUpperCase() + name.slice(1),
+		origName = name,
+		i = cssPrefixes.length;
+
+	while ( i-- ) {
+		name = cssPrefixes[ i ] + capName;
+		if ( name in style ) {
+			return name;
+		}
+	}
+
+	return origName;
+}
+
+function isHidden( elem, el ) {
+	// isHidden might be called from jQuery#filter function;
+	// in that case, element will be second argument
+	elem = el || elem;
+	return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+	var display, elem, hidden,
+		values = [],
+		index = 0,
+		length = elements.length;
+
+	for ( ; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+
+		values[ index ] = jQuery._data( elem, "olddisplay" );
+		display = elem.style.display;
+		if ( show ) {
+			// Reset the inline display of this element to learn if it is
+			// being hidden by cascaded rules or not
+			if ( !values[ index ] && display === "none" ) {
+				elem.style.display = "";
+			}
+
+			// Set elements which have been overridden with display: none
+			// in a stylesheet to whatever the default browser style is
+			// for such an element
+			if ( elem.style.display === "" && isHidden( elem ) ) {
+				values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+			}
+		} else {
+
+			if ( !values[ index ] ) {
+				hidden = isHidden( elem );
+
+				if ( display && display !== "none" || !hidden ) {
+					jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
+				}
+			}
+		}
+	}
+
+	// Set the display of most of the elements in a second loop
+	// to avoid the constant reflow
+	for ( index = 0; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+			elem.style.display = show ? values[ index ] || "" : "none";
+		}
+	}
+
+	return elements;
+}
+
+jQuery.fn.extend({
+	css: function( name, value ) {
+		return jQuery.access( this, function( elem, name, value ) {
+			var len, styles,
+				map = {},
+				i = 0;
+
+			if ( jQuery.isArray( name ) ) {
+				styles = getStyles( elem );
+				len = name.length;
+
+				for ( ; i < len; i++ ) {
+					map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
+				}
+
+				return map;
+			}
+
+			return value !== undefined ?
+				jQuery.style( elem, name, value ) :
+				jQuery.css( elem, name );
+		}, name, value, arguments.length > 1 );
+	},
+	show: function() {
+		return showHide( this, true );
+	},
+	hide: function() {
+		return showHide( this );
+	},
+	toggle: function( state ) {
+		if ( typeof state === "boolean" ) {
+			return state ? this.show() : this.hide();
+		}
+
+		return this.each(function() {
+			if ( isHidden( this ) ) {
+				jQuery( this ).show();
+			} else {
+				jQuery( this ).hide();
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	// Add in style property hooks for overriding the default
+	// behavior of getting and setting a style property
+	cssHooks: {
+		opacity: {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// We should always get a number back from opacity
+					var ret = curCSS( elem, "opacity" );
+					return ret === "" ? "1" : ret;
+				}
+			}
+		}
+	},
+
+	// Don't automatically add "px" to these possibly-unitless properties
+	cssNumber: {
+		"columnCount": true,
+		"fillOpacity": true,
+		"fontWeight": true,
+		"lineHeight": true,
+		"opacity": true,
+		"order": true,
+		"orphans": true,
+		"widows": true,
+		"zIndex": true,
+		"zoom": true
+	},
+
+	// Add in properties whose names you wish to fix before
+	// setting or getting the value
+	cssProps: {
+		// normalize float css property
+		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+	},
+
+	// Get and set the style property on a DOM Node
+	style: function( elem, name, value, extra ) {
+		// Don't set styles on text and comment nodes
+		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+			return;
+		}
+
+		// Make sure that we're working with the right name
+		var ret, type, hooks,
+			origName = jQuery.camelCase( name ),
+			style = elem.style;
+
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// Check if we're setting a value
+		if ( value !== undefined ) {
+			type = typeof value;
+
+			// convert relative number strings (+= or -=) to relative numbers. #7345
+			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+				// Fixes bug #9237
+				type = "number";
+			}
+
+			// Make sure that NaN and null values aren't set. See: #7116
+			if ( value == null || type === "number" && isNaN( value ) ) {
+				return;
+			}
+
+			// If a number was passed in, add 'px' to the (except for certain CSS properties)
+			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+				value += "px";
+			}
+
+			// Fixes #8908, it can be done more correctly by specifing setters in cssHooks,
+			// but it would mean to define eight (for every problematic property) identical functions
+			if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) {
+				style[ name ] = "inherit";
+			}
+
+			// If a hook was provided, use that value, otherwise just set the specified value
+			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+
+				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+				// Fixes bug #5509
+				try {
+					style[ name ] = value;
+				} catch(e) {}
+			}
+
+		} else {
+			// If a hook was provided get the non-computed value from there
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+				return ret;
+			}
+
+			// Otherwise just get the value from the style object
+			return style[ name ];
+		}
+	},
+
+	css: function( elem, name, extra, styles ) {
+		var num, val, hooks,
+			origName = jQuery.camelCase( name );
+
+		// Make sure that we're working with the right name
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// If a hook was provided get the computed value from there
+		if ( hooks && "get" in hooks ) {
+			val = hooks.get( elem, true, extra );
+		}
+
+		// Otherwise, if a way to get the computed value exists, use that
+		if ( val === undefined ) {
+			val = curCSS( elem, name, styles );
+		}
+
+		//convert "normal" to computed value
+		if ( val === "normal" && name in cssNormalTransform ) {
+			val = cssNormalTransform[ name ];
+		}
+
+		// Return, converting to number if forced or a qualifier was provided and val looks numeric
+		if ( extra === "" || extra ) {
+			num = parseFloat( val );
+			return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
+		}
+		return val;
+	}
+});
+
+// NOTE: we've included the "window" in window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+	getStyles = function( elem ) {
+		return window.getComputedStyle( elem, null );
+	};
+
+	curCSS = function( elem, name, _computed ) {
+		var width, minWidth, maxWidth,
+			computed = _computed || getStyles( elem ),
+
+			// getPropertyValue is only needed for .css('filter') in IE9, see #12537
+			ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined,
+			style = elem.style;
+
+		if ( computed ) {
+
+			if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+				ret = jQuery.style( elem, name );
+			}
+
+			// A tribute to the "awesome hack by Dean Edwards"
+			// Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+			// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+			// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+			if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+
+				// Remember the original values
+				width = style.width;
+				minWidth = style.minWidth;
+				maxWidth = style.maxWidth;
+
+				// Put in the new values to get a computed value out
+				style.minWidth = style.maxWidth = style.width = ret;
+				ret = computed.width;
+
+				// Revert the changed values
+				style.width = width;
+				style.minWidth = minWidth;
+				style.maxWidth = maxWidth;
+			}
+		}
+
+		return ret;
+	};
+} else if ( document.documentElement.currentStyle ) {
+	getStyles = function( elem ) {
+		return elem.currentStyle;
+	};
+
+	curCSS = function( elem, name, _computed ) {
+		var left, rs, rsLeft,
+			computed = _computed || getStyles( elem ),
+			ret = computed ? computed[ name ] : undefined,
+			style = elem.style;
+
+		// Avoid setting ret to empty string here
+		// so we don't default to auto
+		if ( ret == null && style && style[ name ] ) {
+			ret = style[ name ];
+		}
+
+		// From the awesome hack by Dean Edwards
+		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+		// If we're not dealing with a regular pixel number
+		// but a number that has a weird ending, we need to convert it to pixels
+		// but not position css attributes, as those are proportional to the parent element instead
+		// and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+		if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+			// Remember the original values
+			left = style.left;
+			rs = elem.runtimeStyle;
+			rsLeft = rs && rs.left;
+
+			// Put in the new values to get a computed value out
+			if ( rsLeft ) {
+				rs.left = elem.currentStyle.left;
+			}
+			style.left = name === "fontSize" ? "1em" : ret;
+			ret = style.pixelLeft + "px";
+
+			// Revert the changed values
+			style.left = left;
+			if ( rsLeft ) {
+				rs.left = rsLeft;
+			}
+		}
+
+		return ret === "" ? "auto" : ret;
+	};
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+	var matches = rnumsplit.exec( value );
+	return matches ?
+		// Guard against undefined "subtract", e.g., when used as in cssHooks
+		Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+		value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
+	var i = extra === ( isBorderBox ? "border" : "content" ) ?
+		// If we already have the right measurement, avoid augmentation
+		4 :
+		// Otherwise initialize for horizontal or vertical properties
+		name === "width" ? 1 : 0,
+
+		val = 0;
+
+	for ( ; i < 4; i += 2 ) {
+		// both box models exclude margin, so add it if we want it
+		if ( extra === "margin" ) {
+			val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
+		}
+
+		if ( isBorderBox ) {
+			// border-box includes padding, so remove it if we want content
+			if ( extra === "content" ) {
+				val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+			}
+
+			// at this point, extra isn't border nor margin, so remove border
+			if ( extra !== "margin" ) {
+				val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+			}
+		} else {
+			// at this point, extra isn't content, so add padding
+			val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
+
+			// at this point, extra isn't content nor padding, so add border
+			if ( extra !== "padding" ) {
+				val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
+			}
+		}
+	}
+
+	return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+	// Start with offset property, which is equivalent to the border-box value
+	var valueIsBorderBox = true,
+		val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+		styles = getStyles( elem ),
+		isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
+
+	// some non-html elements return undefined for offsetWidth, so check for null/undefined
+	// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+	// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+	if ( val <= 0 || val == null ) {
+		// Fall back to computed then uncomputed css if necessary
+		val = curCSS( elem, name, styles );
+		if ( val < 0 || val == null ) {
+			val = elem.style[ name ];
+		}
+
+		// Computed unit is not pixels. Stop here and return.
+		if ( rnumnonpx.test(val) ) {
+			return val;
+		}
+
+		// we need the check for style in case a browser which returns unreliable values
+		// for getComputedStyle silently falls back to the reliable elem.style
+		valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+		// Normalize "", auto, and prepare for extra
+		val = parseFloat( val ) || 0;
+	}
+
+	// use the active box-sizing model to add/subtract irrelevant styles
+	return ( val +
+		augmentWidthOrHeight(
+			elem,
+			name,
+			extra || ( isBorderBox ? "border" : "content" ),
+			valueIsBorderBox,
+			styles
+		)
+	) + "px";
+}
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+	var doc = document,
+		display = elemdisplay[ nodeName ];
+
+	if ( !display ) {
+		display = actualDisplay( nodeName, doc );
+
+		// If the simple way fails, read from inside an iframe
+		if ( display === "none" || !display ) {
+			// Use the already-created iframe if possible
+			iframe = ( iframe ||
+				jQuery("<iframe frameborder='0' width='0' height='0'/>")
+				.css( "cssText", "display:block !important" )
+			).appendTo( doc.documentElement );
+
+			// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
+			doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document;
+			doc.write("<!doctype html><html><body>");
+			doc.close();
+
+			display = actualDisplay( nodeName, doc );
+			iframe.detach();
+		}
+
+		// Store the correct default display
+		elemdisplay[ nodeName ] = display;
+	}
+
+	return display;
+}
+
+// Called ONLY from within css_defaultDisplay
+function actualDisplay( name, doc ) {
+	var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
+		display = jQuery.css( elem[0], "display" );
+	elem.remove();
+	return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+	jQuery.cssHooks[ name ] = {
+		get: function( elem, computed, extra ) {
+			if ( computed ) {
+				// certain elements can have dimension info if we invisibly show them
+				// however, it must have a current display style that would benefit from this
+				return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ?
+					jQuery.swap( elem, cssShow, function() {
+						return getWidthOrHeight( elem, name, extra );
+					}) :
+					getWidthOrHeight( elem, name, extra );
+			}
+		},
+
+		set: function( elem, value, extra ) {
+			var styles = extra && getStyles( elem );
+			return setPositiveNumber( elem, value, extra ?
+				augmentWidthOrHeight(
+					elem,
+					name,
+					extra,
+					jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
+					styles
+				) : 0
+			);
+		}
+	};
+});
+
+if ( !jQuery.support.opacity ) {
+	jQuery.cssHooks.opacity = {
+		get: function( elem, computed ) {
+			// IE uses filters for opacity
+			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+				( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+				computed ? "1" : "";
+		},
+
+		set: function( elem, value ) {
+			var style = elem.style,
+				currentStyle = elem.currentStyle,
+				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+				filter = currentStyle && currentStyle.filter || style.filter || "";
+
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			style.zoom = 1;
+
+			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+			// if value === "", then remove inline opacity #12685
+			if ( ( value >= 1 || value === "" ) &&
+					jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+					style.removeAttribute ) {
+
+				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+				// if "filter:" is present at all, clearType is disabled, we want to avoid this
+				// style.removeAttribute is IE Only, but so apparently is this code path...
+				style.removeAttribute( "filter" );
+
+				// if there is no filter style applied in a css rule or unset inline opacity, we are done
+				if ( value === "" || currentStyle && !currentStyle.filter ) {
+					return;
+				}
+			}
+
+			// otherwise, set new filter values
+			style.filter = ralpha.test( filter ) ?
+				filter.replace( ralpha, opacity ) :
+				filter + " " + opacity;
+		}
+	};
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+	if ( !jQuery.support.reliableMarginRight ) {
+		jQuery.cssHooks.marginRight = {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+					// Work around by temporarily setting element display to inline-block
+					return jQuery.swap( elem, { "display": "inline-block" },
+						curCSS, [ elem, "marginRight" ] );
+				}
+			}
+		};
+	}
+
+	// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+	// getComputedStyle returns percent when specified for top/left/bottom/right
+	// rather than make the css module depend on the offset module, we just check for it here
+	if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+		jQuery.each( [ "top", "left" ], function( i, prop ) {
+			jQuery.cssHooks[ prop ] = {
+				get: function( elem, computed ) {
+					if ( computed ) {
+						computed = curCSS( elem, prop );
+						// if curCSS returns percentage, fallback to offset
+						return rnumnonpx.test( computed ) ?
+							jQuery( elem ).position()[ prop ] + "px" :
+							computed;
+					}
+				}
+			};
+		});
+	}
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.hidden = function( elem ) {
+		// Support: Opera <= 12.12
+		// Opera reports offsetWidths and offsetHeights less than zero on some elements
+		return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 ||
+			(!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none");
+	};
+
+	jQuery.expr.filters.visible = function( elem ) {
+		return !jQuery.expr.filters.hidden( elem );
+	};
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+	margin: "",
+	padding: "",
+	border: "Width"
+}, function( prefix, suffix ) {
+	jQuery.cssHooks[ prefix + suffix ] = {
+		expand: function( value ) {
+			var i = 0,
+				expanded = {},
+
+				// assumes a single number if not a string
+				parts = typeof value === "string" ? value.split(" ") : [ value ];
+
+			for ( ; i < 4; i++ ) {
+				expanded[ prefix + cssExpand[ i ] + suffix ] =
+					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+			}
+
+			return expanded;
+		}
+	};
+
+	if ( !rmargin.test( prefix ) ) {
+		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+	}
+});
+var r20 = /%20/g,
+	rbracket = /\[\]$/,
+	rCRLF = /\r?\n/g,
+	rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
+	rsubmittable = /^(?:input|select|textarea|keygen)/i;
+
+jQuery.fn.extend({
+	serialize: function() {
+		return jQuery.param( this.serializeArray() );
+	},
+	serializeArray: function() {
+		return this.map(function(){
+			// Can add propHook for "elements" to filter or add form elements
+			var elements = jQuery.prop( this, "elements" );
+			return elements ? jQuery.makeArray( elements ) : this;
+		})
+		.filter(function(){
+			var type = this.type;
+			// Use .is(":disabled") so that fieldset[disabled] works
+			return this.name && !jQuery( this ).is( ":disabled" ) &&
+				rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
+				( this.checked || !manipulation_rcheckableType.test( type ) );
+		})
+		.map(function( i, elem ){
+			var val = jQuery( this ).val();
+
+			return val == null ?
+				null :
+				jQuery.isArray( val ) ?
+					jQuery.map( val, function( val ){
+						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+					}) :
+					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+		}).get();
+	}
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+	var prefix,
+		s = [],
+		add = function( key, value ) {
+			// If value is a function, invoke it and return its value
+			value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+			s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+		};
+
+	// Set traditional to true for jQuery <= 1.3.2 behavior.
+	if ( traditional === undefined ) {
+		traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+	}
+
+	// If an array was passed in, assume that it is an array of form elements.
+	if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+		// Serialize the form elements
+		jQuery.each( a, function() {
+			add( this.name, this.value );
+		});
+
+	} else {
+		// If traditional, encode the "old" way (the way 1.3.2 or older
+		// did it), otherwise encode params recursively.
+		for ( prefix in a ) {
+			buildParams( prefix, a[ prefix ], traditional, add );
+		}
+	}
+
+	// Return the resulting serialization
+	return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+	var name;
+
+	if ( jQuery.isArray( obj ) ) {
+		// Serialize array item.
+		jQuery.each( obj, function( i, v ) {
+			if ( traditional || rbracket.test( prefix ) ) {
+				// Treat each array item as a scalar.
+				add( prefix, v );
+
+			} else {
+				// Item is non-scalar (array or object), encode its numeric index.
+				buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+			}
+		});
+
+	} else if ( !traditional && jQuery.type( obj ) === "object" ) {
+		// Serialize object item.
+		for ( name in obj ) {
+			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+		}
+
+	} else {
+		// Serialize scalar item.
+		add( prefix, obj );
+	}
+}
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+	// Handle event binding
+	jQuery.fn[ name ] = function( data, fn ) {
+		return arguments.length > 0 ?
+			this.on( name, null, data, fn ) :
+			this.trigger( name );
+	};
+});
+
+jQuery.fn.extend({
+	hover: function( fnOver, fnOut ) {
+		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+	},
+
+	bind: function( types, data, fn ) {
+		return this.on( types, null, data, fn );
+	},
+	unbind: function( types, fn ) {
+		return this.off( types, null, fn );
+	},
+
+	delegate: function( selector, types, data, fn ) {
+		return this.on( types, selector, data, fn );
+	},
+	undelegate: function( selector, types, fn ) {
+		// ( namespace ) or ( selector, types [, fn] )
+		return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+	}
+});
+var
+	// Document location
+	ajaxLocParts,
+	ajaxLocation,
+	ajax_nonce = jQuery.now(),
+
+	ajax_rquery = /\?/,
+	rhash = /#.*$/,
+	rts = /([?&])_=[^&]*/,
+	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+	// #7653, #8125, #8152: local protocol detection
+	rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+	rnoContent = /^(?:GET|HEAD)$/,
+	rprotocol = /^\/\//,
+	rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+	// Keep a copy of the old load method
+	_load = jQuery.fn.load,
+
+	/* Prefilters
+	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+	 * 2) These are called:
+	 *    - BEFORE asking for a transport
+	 *    - AFTER param serialization (s.data is a string if s.processData is true)
+	 * 3) key is the dataType
+	 * 4) the catchall symbol "*" can be used
+	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+	 */
+	prefilters = {},
+
+	/* Transports bindings
+	 * 1) key is the dataType
+	 * 2) the catchall symbol "*" can be used
+	 * 3) selection will start with transport dataType and THEN go to "*" if needed
+	 */
+	transports = {},
+
+	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+	allTypes = "*/".concat("*");
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+	ajaxLocation = location.href;
+} catch( e ) {
+	// Use the href attribute of an A element
+	// since IE will modify it given document.location
+	ajaxLocation = document.createElement( "a" );
+	ajaxLocation.href = "";
+	ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+	// dataTypeExpression is optional and defaults to "*"
+	return function( dataTypeExpression, func ) {
+
+		if ( typeof dataTypeExpression !== "string" ) {
+			func = dataTypeExpression;
+			dataTypeExpression = "*";
+		}
+
+		var dataType,
+			i = 0,
+			dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || [];
+
+		if ( jQuery.isFunction( func ) ) {
+			// For each dataType in the dataTypeExpression
+			while ( (dataType = dataTypes[i++]) ) {
+				// Prepend if requested
+				if ( dataType[0] === "+" ) {
+					dataType = dataType.slice( 1 ) || "*";
+					(structure[ dataType ] = structure[ dataType ] || []).unshift( func );
+
+				// Otherwise append
+				} else {
+					(structure[ dataType ] = structure[ dataType ] || []).push( func );
+				}
+			}
+		}
+	};
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
+
+	var inspected = {},
+		seekingTransport = ( structure === transports );
+
+	function inspect( dataType ) {
+		var selected;
+		inspected[ dataType ] = true;
+		jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
+			var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
+			if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
+				options.dataTypes.unshift( dataTypeOrTransport );
+				inspect( dataTypeOrTransport );
+				return false;
+			} else if ( seekingTransport ) {
+				return !( selected = dataTypeOrTransport );
+			}
+		});
+		return selected;
+	}
+
+	return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+	var deep, key,
+		flatOptions = jQuery.ajaxSettings.flatOptions || {};
+
+	for ( key in src ) {
+		if ( src[ key ] !== undefined ) {
+			( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
+		}
+	}
+	if ( deep ) {
+		jQuery.extend( true, target, deep );
+	}
+
+	return target;
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+	if ( typeof url !== "string" && _load ) {
+		return _load.apply( this, arguments );
+	}
+
+	var selector, response, type,
+		self = this,
+		off = url.indexOf(" ");
+
+	if ( off >= 0 ) {
+		selector = url.slice( off, url.length );
+		url = url.slice( 0, off );
+	}
+
+	// If it's a function
+	if ( jQuery.isFunction( params ) ) {
+
+		// We assume that it's the callback
+		callback = params;
+		params = undefined;
+
+	// Otherwise, build a param string
+	} else if ( params && typeof params === "object" ) {
+		type = "POST";
+	}
+
+	// If we have elements to modify, make the request
+	if ( self.length > 0 ) {
+		jQuery.ajax({
+			url: url,
+
+			// if "type" variable is undefined, then "GET" method will be used
+			type: type,
+			dataType: "html",
+			data: params
+		}).done(function( responseText ) {
+
+			// Save response for use in complete callback
+			response = arguments;
+
+			self.html( selector ?
+
+				// If a selector was specified, locate the right elements in a dummy div
+				// Exclude scripts to avoid IE 'Permission Denied' errors
+				jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+				// Otherwise use the full result
+				responseText );
+
+		}).complete( callback && function( jqXHR, status ) {
+			self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+		});
+	}
+
+	return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){
+	jQuery.fn[ type ] = function( fn ){
+		return this.on( type, fn );
+	};
+});
+
+jQuery.extend({
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Last-Modified header cache for next request
+	lastModified: {},
+	etag: {},
+
+	ajaxSettings: {
+		url: ajaxLocation,
+		type: "GET",
+		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+		global: true,
+		processData: true,
+		async: true,
+		contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+		/*
+		timeout: 0,
+		data: null,
+		dataType: null,
+		username: null,
+		password: null,
+		cache: null,
+		throws: false,
+		traditional: false,
+		headers: {},
+		*/
+
+		accepts: {
+			"*": allTypes,
+			text: "text/plain",
+			html: "text/html",
+			xml: "application/xml, text/xml",
+			json: "application/json, text/javascript"
+		},
+
+		contents: {
+			xml: /xml/,
+			html: /html/,
+			json: /json/
+		},
+
+		responseFields: {
+			xml: "responseXML",
+			text: "responseText",
+			json: "responseJSON"
+		},
+
+		// Data converters
+		// Keys separate source (or catchall "*") and destination types with a single space
+		converters: {
+
+			// Convert anything to text
+			"* text": String,
+
+			// Text to html (true = no transformation)
+			"text html": true,
+
+			// Evaluate text as a json expression
+			"text json": jQuery.parseJSON,
+
+			// Parse text as xml
+			"text xml": jQuery.parseXML
+		},
+
+		// For options that shouldn't be deep extended:
+		// you can add your own custom options here if
+		// and when you create one that shouldn't be
+		// deep extended (see ajaxExtend)
+		flatOptions: {
+			url: true,
+			context: true
+		}
+	},
+
+	// Creates a full fledged settings object into target
+	// with both ajaxSettings and settings fields.
+	// If target is omitted, writes into ajaxSettings.
+	ajaxSetup: function( target, settings ) {
+		return settings ?
+
+			// Building a settings object
+			ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+			// Extending ajaxSettings
+			ajaxExtend( jQuery.ajaxSettings, target );
+	},
+
+	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+	ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+	// Main method
+	ajax: function( url, options ) {
+
+		// If url is an object, simulate pre-1.5 signature
+		if ( typeof url === "object" ) {
+			options = url;
+			url = undefined;
+		}
+
+		// Force options to be an object
+		options = options || {};
+
+		var // Cross-domain detection vars
+			parts,
+			// Loop variable
+			i,
+			// URL without anti-cache param
+			cacheURL,
+			// Response headers as string
+			responseHeadersString,
+			// timeout handle
+			timeoutTimer,
+
+			// To know if global events are to be dispatched
+			fireGlobals,
+
+			transport,
+			// Response headers
+			responseHeaders,
+			// Create the final options object
+			s = jQuery.ajaxSetup( {}, options ),
+			// Callbacks context
+			callbackContext = s.context || s,
+			// Context for global events is callbackContext if it is a DOM node or jQuery collection
+			globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
+				jQuery( callbackContext ) :
+				jQuery.event,
+			// Deferreds
+			deferred = jQuery.Deferred(),
+			completeDeferred = jQuery.Callbacks("once memory"),
+			// Status-dependent callbacks
+			statusCode = s.statusCode || {},
+			// Headers (they are sent all at once)
+			requestHeaders = {},
+			requestHeadersNames = {},
+			// The jqXHR state
+			state = 0,
+			// Default abort message
+			strAbort = "canceled",
+			// Fake xhr
+			jqXHR = {
+				readyState: 0,
+
+				// Builds headers hashtable if needed
+				getResponseHeader: function( key ) {
+					var match;
+					if ( state === 2 ) {
+						if ( !responseHeaders ) {
+							responseHeaders = {};
+							while ( (match = rheaders.exec( responseHeadersString )) ) {
+								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+							}
+						}
+						match = responseHeaders[ key.toLowerCase() ];
+					}
+					return match == null ? null : match;
+				},
+
+				// Raw string
+				getAllResponseHeaders: function() {
+					return state === 2 ? responseHeadersString : null;
+				},
+
+				// Caches the header
+				setRequestHeader: function( name, value ) {
+					var lname = name.toLowerCase();
+					if ( !state ) {
+						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+						requestHeaders[ name ] = value;
+					}
+					return this;
+				},
+
+				// Overrides response content-type header
+				overrideMimeType: function( type ) {
+					if ( !state ) {
+						s.mimeType = type;
+					}
+					return this;
+				},
+
+				// Status-dependent callbacks
+				statusCode: function( map ) {
+					var code;
+					if ( map ) {
+						if ( state < 2 ) {
+							for ( code in map ) {
+								// Lazy-add the new callback in a way that preserves old ones
+								statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+							}
+						} else {
+							// Execute the appropriate callbacks
+							jqXHR.always( map[ jqXHR.status ] );
+						}
+					}
+					return this;
+				},
+
+				// Cancel the request
+				abort: function( statusText ) {
+					var finalText = statusText || strAbort;
+					if ( transport ) {
+						transport.abort( finalText );
+					}
+					done( 0, finalText );
+					return this;
+				}
+			};
+
+		// Attach deferreds
+		deferred.promise( jqXHR ).complete = completeDeferred.add;
+		jqXHR.success = jqXHR.done;
+		jqXHR.error = jqXHR.fail;
+
+		// Remove hash character (#7531: and string promotion)
+		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+		// Handle falsy url in the settings object (#10093: consistency with old signature)
+		// We also use the url parameter if available
+		s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+		// Alias method option to type as per ticket #12004
+		s.type = options.method || options.type || s.method || s.type;
+
+		// Extract dataTypes list
+		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""];
+
+		// A cross-domain request is in order when we have a protocol:host:port mismatch
+		if ( s.crossDomain == null ) {
+			parts = rurl.exec( s.url.toLowerCase() );
+			s.crossDomain = !!( parts &&
+				( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+					( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
+						( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
+			);
+		}
+
+		// Convert data if not already a string
+		if ( s.data && s.processData && typeof s.data !== "string" ) {
+			s.data = jQuery.param( s.data, s.traditional );
+		}
+
+		// Apply prefilters
+		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+		// If request was aborted inside a prefilter, stop there
+		if ( state === 2 ) {
+			return jqXHR;
+		}
+
+		// We can fire global events as of now if asked to
+		fireGlobals = s.global;
+
+		// Watch for a new set of requests
+		if ( fireGlobals && jQuery.active++ === 0 ) {
+			jQuery.event.trigger("ajaxStart");
+		}
+
+		// Uppercase the type
+		s.type = s.type.toUpperCase();
+
+		// Determine if request has content
+		s.hasContent = !rnoContent.test( s.type );
+
+		// Save the URL in case we're toying with the If-Modified-Since
+		// and/or If-None-Match header later on
+		cacheURL = s.url;
+
+		// More options handling for requests with no content
+		if ( !s.hasContent ) {
+
+			// If data is available, append data to url
+			if ( s.data ) {
+				cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+				// #9682: remove data so that it's not used in an eventual retry
+				delete s.data;
+			}
+
+			// Add anti-cache in url if needed
+			if ( s.cache === false ) {
+				s.url = rts.test( cacheURL ) ?
+
+					// If there is already a '_' parameter, set its value
+					cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
+
+					// Otherwise add one to the end
+					cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
+			}
+		}
+
+		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+		if ( s.ifModified ) {
+			if ( jQuery.lastModified[ cacheURL ] ) {
+				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+			}
+			if ( jQuery.etag[ cacheURL ] ) {
+				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+			}
+		}
+
+		// Set the correct header, if data is being sent
+		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+			jqXHR.setRequestHeader( "Content-Type", s.contentType );
+		}
+
+		// Set the Accepts header for the server, depending on the dataType
+		jqXHR.setRequestHeader(
+			"Accept",
+			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+				s.accepts[ "*" ]
+		);
+
+		// Check for headers option
+		for ( i in s.headers ) {
+			jqXHR.setRequestHeader( i, s.headers[ i ] );
+		}
+
+		// Allow custom headers/mimetypes and early abort
+		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+			// Abort if not done already and return
+			return jqXHR.abort();
+		}
+
+		// aborting is no longer a cancellation
+		strAbort = "abort";
+
+		// Install callbacks on deferreds
+		for ( i in { success: 1, error: 1, complete: 1 } ) {
+			jqXHR[ i ]( s[ i ] );
+		}
+
+		// Get transport
+		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+		// If no transport, we auto-abort
+		if ( !transport ) {
+			done( -1, "No Transport" );
+		} else {
+			jqXHR.readyState = 1;
+
+			// Send global event
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+			}
+			// Timeout
+			if ( s.async && s.timeout > 0 ) {
+				timeoutTimer = setTimeout(function() {
+					jqXHR.abort("timeout");
+				}, s.timeout );
+			}
+
+			try {
+				state = 1;
+				transport.send( requestHeaders, done );
+			} catch ( e ) {
+				// Propagate exception as error if not done
+				if ( state < 2 ) {
+					done( -1, e );
+				// Simply rethrow otherwise
+				} else {
+					throw e;
+				}
+			}
+		}
+
+		// Callback for when everything is done
+		function done( status, nativeStatusText, responses, headers ) {
+			var isSuccess, success, error, response, modified,
+				statusText = nativeStatusText;
+
+			// Called once
+			if ( state === 2 ) {
+				return;
+			}
+
+			// State is "done" now
+			state = 2;
+
+			// Clear timeout if it exists
+			if ( timeoutTimer ) {
+				clearTimeout( timeoutTimer );
+			}
+
+			// Dereference transport for early garbage collection
+			// (no matter how long the jqXHR object will be used)
+			transport = undefined;
+
+			// Cache response headers
+			responseHeadersString = headers || "";
+
+			// Set readyState
+			jqXHR.readyState = status > 0 ? 4 : 0;
+
+			// Determine if successful
+			isSuccess = status >= 200 && status < 300 || status === 304;
+
+			// Get response data
+			if ( responses ) {
+				response = ajaxHandleResponses( s, jqXHR, responses );
+			}
+
+			// Convert no matter what (that way responseXXX fields are always set)
+			response = ajaxConvert( s, response, jqXHR, isSuccess );
+
+			// If successful, handle type chaining
+			if ( isSuccess ) {
+
+				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+				if ( s.ifModified ) {
+					modified = jqXHR.getResponseHeader("Last-Modified");
+					if ( modified ) {
+						jQuery.lastModified[ cacheURL ] = modified;
+					}
+					modified = jqXHR.getResponseHeader("etag");
+					if ( modified ) {
+						jQuery.etag[ cacheURL ] = modified;
+					}
+				}
+
+				// if no content
+				if ( status === 204 || s.type === "HEAD" ) {
+					statusText = "nocontent";
+
+				// if not modified
+				} else if ( status === 304 ) {
+					statusText = "notmodified";
+
+				// If we have data, let's convert it
+				} else {
+					statusText = response.state;
+					success = response.data;
+					error = response.error;
+					isSuccess = !error;
+				}
+			} else {
+				// We extract error from statusText
+				// then normalize statusText and status for non-aborts
+				error = statusText;
+				if ( status || !statusText ) {
+					statusText = "error";
+					if ( status < 0 ) {
+						status = 0;
+					}
+				}
+			}
+
+			// Set data for the fake xhr object
+			jqXHR.status = status;
+			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+			// Success/Error
+			if ( isSuccess ) {
+				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+			} else {
+				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+			}
+
+			// Status-dependent callbacks
+			jqXHR.statusCode( statusCode );
+			statusCode = undefined;
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+					[ jqXHR, s, isSuccess ? success : error ] );
+			}
+
+			// Complete
+			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+				// Handle the global AJAX counter
+				if ( !( --jQuery.active ) ) {
+					jQuery.event.trigger("ajaxStop");
+				}
+			}
+		}
+
+		return jqXHR;
+	},
+
+	getJSON: function( url, data, callback ) {
+		return jQuery.get( url, data, callback, "json" );
+	},
+
+	getScript: function( url, callback ) {
+		return jQuery.get( url, undefined, callback, "script" );
+	}
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+	jQuery[ method ] = function( url, data, callback, type ) {
+		// shift arguments if data argument was omitted
+		if ( jQuery.isFunction( data ) ) {
+			type = type || callback;
+			callback = data;
+			data = undefined;
+		}
+
+		return jQuery.ajax({
+			url: url,
+			type: method,
+			dataType: type,
+			data: data,
+			success: callback
+		});
+	};
+});
+
+/* Handles responses to an ajax request:
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+	var firstDataType, ct, finalDataType, type,
+		contents = s.contents,
+		dataTypes = s.dataTypes;
+
+	// Remove auto dataType and get content-type in the process
+	while( dataTypes[ 0 ] === "*" ) {
+		dataTypes.shift();
+		if ( ct === undefined ) {
+			ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
+		}
+	}
+
+	// Check if we're dealing with a known content-type
+	if ( ct ) {
+		for ( type in contents ) {
+			if ( contents[ type ] && contents[ type ].test( ct ) ) {
+				dataTypes.unshift( type );
+				break;
+			}
+		}
+	}
+
+	// Check to see if we have a response for the expected dataType
+	if ( dataTypes[ 0 ] in responses ) {
+		finalDataType = dataTypes[ 0 ];
+	} else {
+		// Try convertible dataTypes
+		for ( type in responses ) {
+			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+				finalDataType = type;
+				break;
+			}
+			if ( !firstDataType ) {
+				firstDataType = type;
+			}
+		}
+		// Or just use first one
+		finalDataType = finalDataType || firstDataType;
+	}
+
+	// If we found a dataType
+	// We add the dataType to the list if needed
+	// and return the corresponding response
+	if ( finalDataType ) {
+		if ( finalDataType !== dataTypes[ 0 ] ) {
+			dataTypes.unshift( finalDataType );
+		}
+		return responses[ finalDataType ];
+	}
+}
+
+/* Chain conversions given the request and the original response
+ * Also sets the responseXXX fields on the jqXHR instance
+ */
+function ajaxConvert( s, response, jqXHR, isSuccess ) {
+	var conv2, current, conv, tmp, prev,
+		converters = {},
+		// Work with a copy of dataTypes in case we need to modify it for conversion
+		dataTypes = s.dataTypes.slice();
+
+	// Create converters map with lowercased keys
+	if ( dataTypes[ 1 ] ) {
+		for ( conv in s.converters ) {
+			converters[ conv.toLowerCase() ] = s.converters[ conv ];
+		}
+	}
+
+	current = dataTypes.shift();
+
+	// Convert to each sequential dataType
+	while ( current ) {
+
+		if ( s.responseFields[ current ] ) {
+			jqXHR[ s.responseFields[ current ] ] = response;
+		}
+
+		// Apply the dataFilter if provided
+		if ( !prev && isSuccess && s.dataFilter ) {
+			response = s.dataFilter( response, s.dataType );
+		}
+
+		prev = current;
+		current = dataTypes.shift();
+
+		if ( current ) {
+
+			// There's only work to do if current dataType is non-auto
+			if ( current === "*" ) {
+
+				current = prev;
+
+			// Convert response if prev dataType is non-auto and differs from current
+			} else if ( prev !== "*" && prev !== current ) {
+
+				// Seek a direct converter
+				conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+				// If none found, seek a pair
+				if ( !conv ) {
+					for ( conv2 in converters ) {
+
+						// If conv2 outputs current
+						tmp = conv2.split( " " );
+						if ( tmp[ 1 ] === current ) {
+
+							// If prev can be converted to accepted input
+							conv = converters[ prev + " " + tmp[ 0 ] ] ||
+								converters[ "* " + tmp[ 0 ] ];
+							if ( conv ) {
+								// Condense equivalence converters
+								if ( conv === true ) {
+									conv = converters[ conv2 ];
+
+								// Otherwise, insert the intermediate dataType
+								} else if ( converters[ conv2 ] !== true ) {
+									current = tmp[ 0 ];
+									dataTypes.unshift( tmp[ 1 ] );
+								}
+								break;
+							}
+						}
+					}
+				}
+
+				// Apply converter (if not an equivalence)
+				if ( conv !== true ) {
+
+					// Unless errors are allowed to bubble, catch and return them
+					if ( conv && s[ "throws" ] ) {
+						response = conv( response );
+					} else {
+						try {
+							response = conv( response );
+						} catch ( e ) {
+							return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return { state: "success", data: response };
+}
+// Install script dataType
+jQuery.ajaxSetup({
+	accepts: {
+		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+	},
+	contents: {
+		script: /(?:java|ecma)script/
+	},
+	converters: {
+		"text script": function( text ) {
+			jQuery.globalEval( text );
+			return text;
+		}
+	}
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+	if ( s.cache === undefined ) {
+		s.cache = false;
+	}
+	if ( s.crossDomain ) {
+		s.type = "GET";
+		s.global = false;
+	}
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+	// This transport only deals with cross domain requests
+	if ( s.crossDomain ) {
+
+		var script,
+			head = document.head || jQuery("head")[0] || document.documentElement;
+
+		return {
+
+			send: function( _, callback ) {
+
+				script = document.createElement("script");
+
+				script.async = true;
+
+				if ( s.scriptCharset ) {
+					script.charset = s.scriptCharset;
+				}
+
+				script.src = s.url;
+
+				// Attach handlers for all browsers
+				script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+					if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+						// Handle memory leak in IE
+						script.onload = script.onreadystatechange = null;
+
+						// Remove the script
+						if ( script.parentNode ) {
+							script.parentNode.removeChild( script );
+						}
+
+						// Dereference the script
+						script = null;
+
+						// Callback if not abort
+						if ( !isAbort ) {
+							callback( 200, "success" );
+						}
+					}
+				};
+
+				// Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
+				// Use native DOM manipulation to avoid our domManip AJAX trickery
+				head.insertBefore( script, head.firstChild );
+			},
+
+			abort: function() {
+				if ( script ) {
+					script.onload( undefined, true );
+				}
+			}
+		};
+	}
+});
+var oldCallbacks = [],
+	rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+	jsonp: "callback",
+	jsonpCallback: function() {
+		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );
+		this[ callback ] = true;
+		return callback;
+	}
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+	var callbackName, overwritten, responseContainer,
+		jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+			"url" :
+			typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
+		);
+
+	// Handle iff the expected data type is "jsonp" or we have a parameter to set
+	if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+		// Get callback name, remembering preexisting value associated with it
+		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+			s.jsonpCallback() :
+			s.jsonpCallback;
+
+		// Insert callback into url or form data
+		if ( jsonProp ) {
+			s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+		} else if ( s.jsonp !== false ) {
+			s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+		}
+
+		// Use data converter to retrieve json after script execution
+		s.converters["script json"] = function() {
+			if ( !responseContainer ) {
+				jQuery.error( callbackName + " was not called" );
+			}
+			return responseContainer[ 0 ];
+		};
+
+		// force json dataType
+		s.dataTypes[ 0 ] = "json";
+
+		// Install callback
+		overwritten = window[ callbackName ];
+		window[ callbackName ] = function() {
+			responseContainer = arguments;
+		};
+
+		// Clean-up function (fires after converters)
+		jqXHR.always(function() {
+			// Restore preexisting value
+			window[ callbackName ] = overwritten;
+
+			// Save back as free
+			if ( s[ callbackName ] ) {
+				// make sure that re-using the options doesn't screw things around
+				s.jsonpCallback = originalSettings.jsonpCallback;
+
+				// save the callback name for future use
+				oldCallbacks.push( callbackName );
+			}
+
+			// Call if it was a function and we have a response
+			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+				overwritten( responseContainer[ 0 ] );
+			}
+
+			responseContainer = overwritten = undefined;
+		});
+
+		// Delegate to script
+		return "script";
+	}
+});
+var xhrCallbacks, xhrSupported,
+	xhrId = 0,
+	// #5280: Internet Explorer will keep connections alive if we don't abort on unload
+	xhrOnUnloadAbort = window.ActiveXObject && function() {
+		// Abort all pending requests
+		var key;
+		for ( key in xhrCallbacks ) {
+			xhrCallbacks[ key ]( undefined, true );
+		}
+	};
+
+// Functions to create xhrs
+function createStandardXHR() {
+	try {
+		return new window.XMLHttpRequest();
+	} catch( e ) {}
+}
+
+function createActiveXHR() {
+	try {
+		return new window.ActiveXObject("Microsoft.XMLHTTP");
+	} catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+	/* Microsoft failed to properly
+	 * implement the XMLHttpRequest in IE7 (can't request local files),
+	 * so we use the ActiveXObject when it is available
+	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+	 * we need a fallback.
+	 */
+	function() {
+		return !this.isLocal && createStandardXHR() || createActiveXHR();
+	} :
+	// For all other browsers, use the standard XMLHttpRequest object
+	createStandardXHR;
+
+// Determine support properties
+xhrSupported = jQuery.ajaxSettings.xhr();
+jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+xhrSupported = jQuery.support.ajax = !!xhrSupported;
+
+// Create transport if the browser can provide an xhr
+if ( xhrSupported ) {
+
+	jQuery.ajaxTransport(function( s ) {
+		// Cross domain only allowed if supported through XMLHttpRequest
+		if ( !s.crossDomain || jQuery.support.cors ) {
+
+			var callback;
+
+			return {
+				send: function( headers, complete ) {
+
+					// Get a new xhr
+					var handle, i,
+						xhr = s.xhr();
+
+					// Open the socket
+					// Passing null username, generates a login popup on Opera (#2865)
+					if ( s.username ) {
+						xhr.open( s.type, s.url, s.async, s.username, s.password );
+					} else {
+						xhr.open( s.type, s.url, s.async );
+					}
+
+					// Apply custom fields if provided
+					if ( s.xhrFields ) {
+						for ( i in s.xhrFields ) {
+							xhr[ i ] = s.xhrFields[ i ];
+						}
+					}
+
+					// Override mime type if needed
+					if ( s.mimeType && xhr.overrideMimeType ) {
+						xhr.overrideMimeType( s.mimeType );
+					}
+
+					// X-Requested-With header
+					// For cross-domain requests, seeing as conditions for a preflight are
+					// akin to a jigsaw puzzle, we simply never set it to be sure.
+					// (it can always be set on a per-request basis or even using ajaxSetup)
+					// For same-domain requests, won't change header if already provided.
+					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+						headers["X-Requested-With"] = "XMLHttpRequest";
+					}
+
+					// Need an extra try/catch for cross domain requests in Firefox 3
+					try {
+						for ( i in headers ) {
+							xhr.setRequestHeader( i, headers[ i ] );
+						}
+					} catch( err ) {}
+
+					// Do send the request
+					// This may raise an exception which is actually
+					// handled in jQuery.ajax (so no try/catch here)
+					xhr.send( ( s.hasContent && s.data ) || null );
+
+					// Listener
+					callback = function( _, isAbort ) {
+						var status, responseHeaders, statusText, responses;
+
+						// Firefox throws exceptions when accessing properties
+						// of an xhr when a network error occurred
+						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+						try {
+
+							// Was never called and is aborted or complete
+							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+								// Only called once
+								callback = undefined;
+
+								// Do not keep as active anymore
+								if ( handle ) {
+									xhr.onreadystatechange = jQuery.noop;
+									if ( xhrOnUnloadAbort ) {
+										delete xhrCallbacks[ handle ];
+									}
+								}
+
+								// If it's an abort
+								if ( isAbort ) {
+									// Abort it manually if needed
+									if ( xhr.readyState !== 4 ) {
+										xhr.abort();
+									}
+								} else {
+									responses = {};
+									status = xhr.status;
+									responseHeaders = xhr.getAllResponseHeaders();
+
+									// When requesting binary data, IE6-9 will throw an exception
+									// on any attempt to access responseText (#11426)
+									if ( typeof xhr.responseText === "string" ) {
+										responses.text = xhr.responseText;
+									}
+
+									// Firefox throws an exception when accessing
+									// statusText for faulty cross-domain requests
+									try {
+										statusText = xhr.statusText;
+									} catch( e ) {
+										// We normalize with Webkit giving an empty statusText
+										statusText = "";
+									}
+
+									// Filter status for non standard behaviors
+
+									// If the request is local and we have data: assume a success
+									// (success with no data won't get notified, that's the best we
+									// can do given current implementations)
+									if ( !status && s.isLocal && !s.crossDomain ) {
+										status = responses.text ? 200 : 404;
+									// IE - #1450: sometimes returns 1223 when it should be 204
+									} else if ( status === 1223 ) {
+										status = 204;
+									}
+								}
+							}
+						} catch( firefoxAccessException ) {
+							if ( !isAbort ) {
+								complete( -1, firefoxAccessException );
+							}
+						}
+
+						// Call complete if needed
+						if ( responses ) {
+							complete( status, statusText, responses, responseHeaders );
+						}
+					};
+
+					if ( !s.async ) {
+						// if we're in sync mode we fire the callback
+						callback();
+					} else if ( xhr.readyState === 4 ) {
+						// (IE6 & IE7) if it's in cache and has been
+						// retrieved directly we need to fire the callback
+						setTimeout( callback );
+					} else {
+						handle = ++xhrId;
+						if ( xhrOnUnloadAbort ) {
+							// Create the active xhrs callbacks list if needed
+							// and attach the unload handler
+							if ( !xhrCallbacks ) {
+								xhrCallbacks = {};
+								jQuery( window ).unload( xhrOnUnloadAbort );
+							}
+							// Add to list of active xhrs callbacks
+							xhrCallbacks[ handle ] = callback;
+						}
+						xhr.onreadystatechange = callback;
+					}
+				},
+
+				abort: function() {
+					if ( callback ) {
+						callback( undefined, true );
+					}
+				}
+			};
+		}
+	});
+}
+var fxNow, timerId,
+	rfxtypes = /^(?:toggle|show|hide)$/,
+	rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+	rrun = /queueHooks$/,
+	animationPrefilters = [ defaultPrefilter ],
+	tweeners = {
+		"*": [function( prop, value ) {
+			var tween = this.createTween( prop, value ),
+				target = tween.cur(),
+				parts = rfxnum.exec( value ),
+				unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
+
+				// Starting value computation is required for potential unit mismatches
+				start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
+					rfxnum.exec( jQuery.css( tween.elem, prop ) ),
+				scale = 1,
+				maxIterations = 20;
+
+			if ( start && start[ 3 ] !== unit ) {
+				// Trust units reported by jQuery.css
+				unit = unit || start[ 3 ];
+
+				// Make sure we update the tween properties later on
+				parts = parts || [];
+
+				// Iteratively approximate from a nonzero starting point
+				start = +target || 1;
+
+				do {
+					// If previous iteration zeroed out, double until we get *something*
+					// Use a string for doubling factor so we don't accidentally see scale as unchanged below
+					scale = scale || ".5";
+
+					// Adjust and apply
+					start = start / scale;
+					jQuery.style( tween.elem, prop, start + unit );
+
+				// Update scale, tolerating zero or NaN from tween.cur()
+				// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+				} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+			}
+
+			// Update tween properties
+			if ( parts ) {
+				start = tween.start = +start || +target || 0;
+				tween.unit = unit;
+				// If a +=/-= token was provided, we're doing a relative animation
+				tween.end = parts[ 1 ] ?
+					start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
+					+parts[ 2 ];
+			}
+
+			return tween;
+		}]
+	};
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+	setTimeout(function() {
+		fxNow = undefined;
+	});
+	return ( fxNow = jQuery.now() );
+}
+
+function createTween( value, prop, animation ) {
+	var tween,
+		collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+		index = 0,
+		length = collection.length;
+	for ( ; index < length; index++ ) {
+		if ( (tween = collection[ index ].call( animation, prop, value )) ) {
+
+			// we're done with this property
+			return tween;
+		}
+	}
+}
+
+function Animation( elem, properties, options ) {
+	var result,
+		stopped,
+		index = 0,
+		length = animationPrefilters.length,
+		deferred = jQuery.Deferred().always( function() {
+			// don't match elem in the :animated selector
+			delete tick.elem;
+		}),
+		tick = function() {
+			if ( stopped ) {
+				return false;
+			}
+			var currentTime = fxNow || createFxNow(),
+				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+				// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+				temp = remaining / animation.duration || 0,
+				percent = 1 - temp,
+				index = 0,
+				length = animation.tweens.length;
+
+			for ( ; index < length ; index++ ) {
+				animation.tweens[ index ].run( percent );
+			}
+
+			deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+			if ( percent < 1 && length ) {
+				return remaining;
+			} else {
+				deferred.resolveWith( elem, [ animation ] );
+				return false;
+			}
+		},
+		animation = deferred.promise({
+			elem: elem,
+			props: jQuery.extend( {}, properties ),
+			opts: jQuery.extend( true, { specialEasing: {} }, options ),
+			originalProperties: properties,
+			originalOptions: options,
+			startTime: fxNow || createFxNow(),
+			duration: options.duration,
+			tweens: [],
+			createTween: function( prop, end ) {
+				var tween = jQuery.Tween( elem, animation.opts, prop, end,
+						animation.opts.specialEasing[ prop ] || animation.opts.easing );
+				animation.tweens.push( tween );
+				return tween;
+			},
+			stop: function( gotoEnd ) {
+				var index = 0,
+					// if we are going to the end, we want to run all the tweens
+					// otherwise we skip this part
+					length = gotoEnd ? animation.tweens.length : 0;
+				if ( stopped ) {
+					return this;
+				}
+				stopped = true;
+				for ( ; index < length ; index++ ) {
+					animation.tweens[ index ].run( 1 );
+				}
+
+				// resolve when we played the last frame
+				// otherwise, reject
+				if ( gotoEnd ) {
+					deferred.resolveWith( elem, [ animation, gotoEnd ] );
+				} else {
+					deferred.rejectWith( elem, [ animation, gotoEnd ] );
+				}
+				return this;
+			}
+		}),
+		props = animation.props;
+
+	propFilter( props, animation.opts.specialEasing );
+
+	for ( ; index < length ; index++ ) {
+		result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+		if ( result ) {
+			return result;
+		}
+	}
+
+	jQuery.map( props, createTween, animation );
+
+	if ( jQuery.isFunction( animation.opts.start ) ) {
+		animation.opts.start.call( elem, animation );
+	}
+
+	jQuery.fx.timer(
+		jQuery.extend( tick, {
+			elem: elem,
+			anim: animation,
+			queue: animation.opts.queue
+		})
+	);
+
+	// attach callbacks from options
+	return animation.progress( animation.opts.progress )
+		.done( animation.opts.done, animation.opts.complete )
+		.fail( animation.opts.fail )
+		.always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+	var index, name, easing, value, hooks;
+
+	// camelCase, specialEasing and expand cssHook pass
+	for ( index in props ) {
+		name = jQuery.camelCase( index );
+		easing = specialEasing[ name ];
+		value = props[ index ];
+		if ( jQuery.isArray( value ) ) {
+			easing = value[ 1 ];
+			value = props[ index ] = value[ 0 ];
+		}
+
+		if ( index !== name ) {
+			props[ name ] = value;
+			delete props[ index ];
+		}
+
+		hooks = jQuery.cssHooks[ name ];
+		if ( hooks && "expand" in hooks ) {
+			value = hooks.expand( value );
+			delete props[ name ];
+
+			// not quite $.extend, this wont overwrite keys already present.
+			// also - reusing 'index' from above because we have the correct "name"
+			for ( index in value ) {
+				if ( !( index in props ) ) {
+					props[ index ] = value[ index ];
+					specialEasing[ index ] = easing;
+				}
+			}
+		} else {
+			specialEasing[ name ] = easing;
+		}
+	}
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+	tweener: function( props, callback ) {
+		if ( jQuery.isFunction( props ) ) {
+			callback = props;
+			props = [ "*" ];
+		} else {
+			props = props.split(" ");
+		}
+
+		var prop,
+			index = 0,
+			length = props.length;
+
+		for ( ; index < length ; index++ ) {
+			prop = props[ index ];
+			tweeners[ prop ] = tweeners[ prop ] || [];
+			tweeners[ prop ].unshift( callback );
+		}
+	},
+
+	prefilter: function( callback, prepend ) {
+		if ( prepend ) {
+			animationPrefilters.unshift( callback );
+		} else {
+			animationPrefilters.push( callback );
+		}
+	}
+});
+
+function defaultPrefilter( elem, props, opts ) {
+	/* jshint validthis: true */
+	var prop, value, toggle, tween, hooks, oldfire,
+		anim = this,
+		orig = {},
+		style = elem.style,
+		hidden = elem.nodeType && isHidden( elem ),
+		dataShow = jQuery._data( elem, "fxshow" );
+
+	// handle queue: false promises
+	if ( !opts.queue ) {
+		hooks = jQuery._queueHooks( elem, "fx" );
+		if ( hooks.unqueued == null ) {
+			hooks.unqueued = 0;
+			oldfire = hooks.empty.fire;
+			hooks.empty.fire = function() {
+				if ( !hooks.unqueued ) {
+					oldfire();
+				}
+			};
+		}
+		hooks.unqueued++;
+
+		anim.always(function() {
+			// doing this makes sure that the complete handler will be called
+			// before this completes
+			anim.always(function() {
+				hooks.unqueued--;
+				if ( !jQuery.queue( elem, "fx" ).length ) {
+					hooks.empty.fire();
+				}
+			});
+		});
+	}
+
+	// height/width overflow pass
+	if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+		// Make sure that nothing sneaks out
+		// Record all 3 overflow attributes because IE does not
+		// change the overflow attribute when overflowX and
+		// overflowY are set to the same value
+		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+		// Set display property to inline-block for height/width
+		// animations on inline elements that are having width/height animated
+		if ( jQuery.css( elem, "display" ) === "inline" &&
+				jQuery.css( elem, "float" ) === "none" ) {
+
+			// inline-level elements accept inline-block;
+			// block-level elements need to be inline with layout
+			if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+				style.display = "inline-block";
+
+			} else {
+				style.zoom = 1;
+			}
+		}
+	}
+
+	if ( opts.overflow ) {
+		style.overflow = "hidden";
+		if ( !jQuery.support.shrinkWrapBlocks ) {
+			anim.always(function() {
+				style.overflow = opts.overflow[ 0 ];
+				style.overflowX = opts.overflow[ 1 ];
+				style.overflowY = opts.overflow[ 2 ];
+			});
+		}
+	}
+
+
+	// show/hide pass
+	for ( prop in props ) {
+		value = props[ prop ];
+		if ( rfxtypes.exec( value ) ) {
+			delete props[ prop ];
+			toggle = toggle || value === "toggle";
+			if ( value === ( hidden ? "hide" : "show" ) ) {
+				continue;
+			}
+			orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
+		}
+	}
+
+	if ( !jQuery.isEmptyObject( orig ) ) {
+		if ( dataShow ) {
+			if ( "hidden" in dataShow ) {
+				hidden = dataShow.hidden;
+			}
+		} else {
+			dataShow = jQuery._data( elem, "fxshow", {} );
+		}
+
+		// store state if its toggle - enables .stop().toggle() to "reverse"
+		if ( toggle ) {
+			dataShow.hidden = !hidden;
+		}
+		if ( hidden ) {
+			jQuery( elem ).show();
+		} else {
+			anim.done(function() {
+				jQuery( elem ).hide();
+			});
+		}
+		anim.done(function() {
+			var prop;
+			jQuery._removeData( elem, "fxshow" );
+			for ( prop in orig ) {
+				jQuery.style( elem, prop, orig[ prop ] );
+			}
+		});
+		for ( prop in orig ) {
+			tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
+
+			if ( !( prop in dataShow ) ) {
+				dataShow[ prop ] = tween.start;
+				if ( hidden ) {
+					tween.end = tween.start;
+					tween.start = prop === "width" || prop === "height" ? 1 : 0;
+				}
+			}
+		}
+	}
+}
+
+function Tween( elem, options, prop, end, easing ) {
+	return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+	constructor: Tween,
+	init: function( elem, options, prop, end, easing, unit ) {
+		this.elem = elem;
+		this.prop = prop;
+		this.easing = easing || "swing";
+		this.options = options;
+		this.start = this.now = this.cur();
+		this.end = end;
+		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+	},
+	cur: function() {
+		var hooks = Tween.propHooks[ this.prop ];
+
+		return hooks && hooks.get ?
+			hooks.get( this ) :
+			Tween.propHooks._default.get( this );
+	},
+	run: function( percent ) {
+		var eased,
+			hooks = Tween.propHooks[ this.prop ];
+
+		if ( this.options.duration ) {
+			this.pos = eased = jQuery.easing[ this.easing ](
+				percent, this.options.duration * percent, 0, 1, this.options.duration
+			);
+		} else {
+			this.pos = eased = percent;
+		}
+		this.now = ( this.end - this.start ) * eased + this.start;
+
+		if ( this.options.step ) {
+			this.options.step.call( this.elem, this.now, this );
+		}
+
+		if ( hooks && hooks.set ) {
+			hooks.set( this );
+		} else {
+			Tween.propHooks._default.set( this );
+		}
+		return this;
+	}
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+	_default: {
+		get: function( tween ) {
+			var result;
+
+			if ( tween.elem[ tween.prop ] != null &&
+				(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+				return tween.elem[ tween.prop ];
+			}
+
+			// passing an empty string as a 3rd parameter to .css will automatically
+			// attempt a parseFloat and fallback to a string if the parse fails
+			// so, simple values such as "10px" are parsed to Float.
+			// complex values such as "rotate(1rad)" are returned as is.
+			result = jQuery.css( tween.elem, tween.prop, "" );
+			// Empty strings, null, undefined and "auto" are converted to 0.
+			return !result || result === "auto" ? 0 : result;
+		},
+		set: function( tween ) {
+			// use step hook for back compat - use cssHook if its there - use .style if its
+			// available and use plain properties where available
+			if ( jQuery.fx.step[ tween.prop ] ) {
+				jQuery.fx.step[ tween.prop ]( tween );
+			} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+			} else {
+				tween.elem[ tween.prop ] = tween.now;
+			}
+		}
+	}
+};
+
+// Support: IE <=9
+// Panic based approach to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+	set: function( tween ) {
+		if ( tween.elem.nodeType && tween.elem.parentNode ) {
+			tween.elem[ tween.prop ] = tween.now;
+		}
+	}
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+	var cssFn = jQuery.fn[ name ];
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return speed == null || typeof speed === "boolean" ?
+			cssFn.apply( this, arguments ) :
+			this.animate( genFx( name, true ), speed, easing, callback );
+	};
+});
+
+jQuery.fn.extend({
+	fadeTo: function( speed, to, easing, callback ) {
+
+		// show any hidden elements after setting opacity to 0
+		return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+			// animate to the value specified
+			.end().animate({ opacity: to }, speed, easing, callback );
+	},
+	animate: function( prop, speed, easing, callback ) {
+		var empty = jQuery.isEmptyObject( prop ),
+			optall = jQuery.speed( speed, easing, callback ),
+			doAnimation = function() {
+				// Operate on a copy of prop so per-property easing won't be lost
+				var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+				// Empty animations, or finishing resolves immediately
+				if ( empty || jQuery._data( this, "finish" ) ) {
+					anim.stop( true );
+				}
+			};
+			doAnimation.finish = doAnimation;
+
+		return empty || optall.queue === false ?
+			this.each( doAnimation ) :
+			this.queue( optall.queue, doAnimation );
+	},
+	stop: function( type, clearQueue, gotoEnd ) {
+		var stopQueue = function( hooks ) {
+			var stop = hooks.stop;
+			delete hooks.stop;
+			stop( gotoEnd );
+		};
+
+		if ( typeof type !== "string" ) {
+			gotoEnd = clearQueue;
+			clearQueue = type;
+			type = undefined;
+		}
+		if ( clearQueue && type !== false ) {
+			this.queue( type || "fx", [] );
+		}
+
+		return this.each(function() {
+			var dequeue = true,
+				index = type != null && type + "queueHooks",
+				timers = jQuery.timers,
+				data = jQuery._data( this );
+
+			if ( index ) {
+				if ( data[ index ] && data[ index ].stop ) {
+					stopQueue( data[ index ] );
+				}
+			} else {
+				for ( index in data ) {
+					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+						stopQueue( data[ index ] );
+					}
+				}
+			}
+
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+					timers[ index ].anim.stop( gotoEnd );
+					dequeue = false;
+					timers.splice( index, 1 );
+				}
+			}
+
+			// start the next in the queue if the last step wasn't forced
+			// timers currently will call their complete callbacks, which will dequeue
+			// but only if they were gotoEnd
+			if ( dequeue || !gotoEnd ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	},
+	finish: function( type ) {
+		if ( type !== false ) {
+			type = type || "fx";
+		}
+		return this.each(function() {
+			var index,
+				data = jQuery._data( this ),
+				queue = data[ type + "queue" ],
+				hooks = data[ type + "queueHooks" ],
+				timers = jQuery.timers,
+				length = queue ? queue.length : 0;
+
+			// enable finishing flag on private data
+			data.finish = true;
+
+			// empty the queue first
+			jQuery.queue( this, type, [] );
+
+			if ( hooks && hooks.stop ) {
+				hooks.stop.call( this, true );
+			}
+
+			// look for any active animations, and finish them
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+					timers[ index ].anim.stop( true );
+					timers.splice( index, 1 );
+				}
+			}
+
+			// look for any animations in the old queue and finish them
+			for ( index = 0; index < length; index++ ) {
+				if ( queue[ index ] && queue[ index ].finish ) {
+					queue[ index ].finish.call( this );
+				}
+			}
+
+			// turn off finishing flag
+			delete data.finish;
+		});
+	}
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+	var which,
+		attrs = { height: type },
+		i = 0;
+
+	// if we include width, step value is 1 to do all cssExpand values,
+	// if we don't include width, step value is 2 to skip over Left and Right
+	includeWidth = includeWidth? 1 : 0;
+	for( ; i < 4 ; i += 2 - includeWidth ) {
+		which = cssExpand[ i ];
+		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+	}
+
+	if ( includeWidth ) {
+		attrs.opacity = attrs.width = type;
+	}
+
+	return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+	slideDown: genFx("show"),
+	slideUp: genFx("hide"),
+	slideToggle: genFx("toggle"),
+	fadeIn: { opacity: "show" },
+	fadeOut: { opacity: "hide" },
+	fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return this.animate( props, speed, easing, callback );
+	};
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+		complete: fn || !fn && easing ||
+			jQuery.isFunction( speed ) && speed,
+		duration: speed,
+		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+	};
+
+	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+	// normalize opt.queue - true/undefined/null -> "fx"
+	if ( opt.queue == null || opt.queue === true ) {
+		opt.queue = "fx";
+	}
+
+	// Queueing
+	opt.old = opt.complete;
+
+	opt.complete = function() {
+		if ( jQuery.isFunction( opt.old ) ) {
+			opt.old.call( this );
+		}
+
+		if ( opt.queue ) {
+			jQuery.dequeue( this, opt.queue );
+		}
+	};
+
+	return opt;
+};
+
+jQuery.easing = {
+	linear: function( p ) {
+		return p;
+	},
+	swing: function( p ) {
+		return 0.5 - Math.cos( p*Math.PI ) / 2;
+	}
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+	var timer,
+		timers = jQuery.timers,
+		i = 0;
+
+	fxNow = jQuery.now();
+
+	for ( ; i < timers.length; i++ ) {
+		timer = timers[ i ];
+		// Checks the timer has not already been removed
+		if ( !timer() && timers[ i ] === timer ) {
+			timers.splice( i--, 1 );
+		}
+	}
+
+	if ( !timers.length ) {
+		jQuery.fx.stop();
+	}
+	fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+	if ( timer() && jQuery.timers.push( timer ) ) {
+		jQuery.fx.start();
+	}
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.start = function() {
+	if ( !timerId ) {
+		timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+	}
+};
+
+jQuery.fx.stop = function() {
+	clearInterval( timerId );
+	timerId = null;
+};
+
+jQuery.fx.speeds = {
+	slow: 600,
+	fast: 200,
+	// Default speed
+	_default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.animated = function( elem ) {
+		return jQuery.grep(jQuery.timers, function( fn ) {
+			return elem === fn.elem;
+		}).length;
+	};
+}
+jQuery.fn.offset = function( options ) {
+	if ( arguments.length ) {
+		return options === undefined ?
+			this :
+			this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+	}
+
+	var docElem, win,
+		box = { top: 0, left: 0 },
+		elem = this[ 0 ],
+		doc = elem && elem.ownerDocument;
+
+	if ( !doc ) {
+		return;
+	}
+
+	docElem = doc.documentElement;
+
+	// Make sure it's not a disconnected DOM node
+	if ( !jQuery.contains( docElem, elem ) ) {
+		return box;
+	}
+
+	// If we don't have gBCR, just use 0,0 rather than error
+	// BlackBerry 5, iOS 3 (original iPhone)
+	if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
+		box = elem.getBoundingClientRect();
+	}
+	win = getWindow( doc );
+	return {
+		top: box.top  + ( win.pageYOffset || docElem.scrollTop )  - ( docElem.clientTop  || 0 ),
+		left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
+	};
+};
+
+jQuery.offset = {
+
+	setOffset: function( elem, options, i ) {
+		var position = jQuery.css( elem, "position" );
+
+		// set position first, in-case top/left are set even on static elem
+		if ( position === "static" ) {
+			elem.style.position = "relative";
+		}
+
+		var curElem = jQuery( elem ),
+			curOffset = curElem.offset(),
+			curCSSTop = jQuery.css( elem, "top" ),
+			curCSSLeft = jQuery.css( elem, "left" ),
+			calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+			props = {}, curPosition = {}, curTop, curLeft;
+
+		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+		if ( calculatePosition ) {
+			curPosition = curElem.position();
+			curTop = curPosition.top;
+			curLeft = curPosition.left;
+		} else {
+			curTop = parseFloat( curCSSTop ) || 0;
+			curLeft = parseFloat( curCSSLeft ) || 0;
+		}
+
+		if ( jQuery.isFunction( options ) ) {
+			options = options.call( elem, i, curOffset );
+		}
+
+		if ( options.top != null ) {
+			props.top = ( options.top - curOffset.top ) + curTop;
+		}
+		if ( options.left != null ) {
+			props.left = ( options.left - curOffset.left ) + curLeft;
+		}
+
+		if ( "using" in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	}
+};
+
+
+jQuery.fn.extend({
+
+	position: function() {
+		if ( !this[ 0 ] ) {
+			return;
+		}
+
+		var offsetParent, offset,
+			parentOffset = { top: 0, left: 0 },
+			elem = this[ 0 ];
+
+		// fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
+		if ( jQuery.css( elem, "position" ) === "fixed" ) {
+			// we assume that getBoundingClientRect is available when computed position is fixed
+			offset = elem.getBoundingClientRect();
+		} else {
+			// Get *real* offsetParent
+			offsetParent = this.offsetParent();
+
+			// Get correct offsets
+			offset = this.offset();
+			if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+				parentOffset = offsetParent.offset();
+			}
+
+			// Add offsetParent borders
+			parentOffset.top  += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
+			parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
+		}
+
+		// Subtract parent offsets and element margins
+		// note: when an element has margin: auto the offsetLeft and marginLeft
+		// are the same in Safari causing offset.left to incorrectly be 0
+		return {
+			top:  offset.top  - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+			left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
+		};
+	},
+
+	offsetParent: function() {
+		return this.map(function() {
+			var offsetParent = this.offsetParent || docElem;
+			while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
+				offsetParent = offsetParent.offsetParent;
+			}
+			return offsetParent || docElem;
+		});
+	}
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+	var top = /Y/.test( prop );
+
+	jQuery.fn[ method ] = function( val ) {
+		return jQuery.access( this, function( elem, method, val ) {
+			var win = getWindow( elem );
+
+			if ( val === undefined ) {
+				return win ? (prop in win) ? win[ prop ] :
+					win.document.documentElement[ method ] :
+					elem[ method ];
+			}
+
+			if ( win ) {
+				win.scrollTo(
+					!top ? val : jQuery( win ).scrollLeft(),
+					top ? val : jQuery( win ).scrollTop()
+				);
+
+			} else {
+				elem[ method ] = val;
+			}
+		}, method, val, arguments.length, null );
+	};
+});
+
+function getWindow( elem ) {
+	return jQuery.isWindow( elem ) ?
+		elem :
+		elem.nodeType === 9 ?
+			elem.defaultView || elem.parentWindow :
+			false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+		// margin is only for outerHeight, outerWidth
+		jQuery.fn[ funcName ] = function( margin, value ) {
+			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+			return jQuery.access( this, function( elem, type, value ) {
+				var doc;
+
+				if ( jQuery.isWindow( elem ) ) {
+					// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+					// isn't a whole lot we can do. See pull request at this URL for discussion:
+					// https://github.com/jquery/jquery/pull/764
+					return elem.document.documentElement[ "client" + name ];
+				}
+
+				// Get document width or height
+				if ( elem.nodeType === 9 ) {
+					doc = elem.documentElement;
+
+					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+					// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+					return Math.max(
+						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+						elem.body[ "offset" + name ], doc[ "offset" + name ],
+						doc[ "client" + name ]
+					);
+				}
+
+				return value === undefined ?
+					// Get width or height on the element, requesting but not forcing parseFloat
+					jQuery.css( elem, type, extra ) :
+
+					// Set width or height on the element
+					jQuery.style( elem, type, value, extra );
+			}, type, chainable ? margin : undefined, chainable, null );
+		};
+	});
+});
+// Limit scope pollution from any deprecated API
+// (function() {
+
+// The number of elements contained in the matched element set
+jQuery.fn.size = function() {
+	return this.length;
+};
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+// })();
+if ( typeof module === "object" && module && typeof module.exports === "object" ) {
+	// Expose jQuery as module.exports in loaders that implement the Node
+	// module pattern (including browserify). Do not create the global, since
+	// the user will be storing it themselves locally, and globals are frowned
+	// upon in the Node module world.
+	module.exports = jQuery;
+} else {
+	// Otherwise expose jQuery to the global object as usual
+	window.jQuery = window.$ = jQuery;
+
+	// Register as a named AMD module, since jQuery can be concatenated with other
+	// files that may use define, but not via a proper concatenation script that
+	// understands anonymous AMD modules. A named AMD is safest and most robust
+	// way to register. Lowercase jquery is used because AMD module names are
+	// derived from file names, and jQuery is normally delivered in a lowercase
+	// file name. Do this after creating the global so that if an AMD module wants
+	// to call noConflict to hide this version of jQuery, it will work.
+	if ( typeof define === "function" && define.amd ) {
+		define( "jquery", [], function () { return jQuery; } );
+	}
+}
+
+})( window );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_18_b81900_40x40.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_18_b81900_40x40.png
new file mode 100644
index 0000000..39f35bb
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_18_b81900_40x40.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_20_666666_40x40.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_20_666666_40x40.png
new file mode 100644
index 0000000..b01772c
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_diagonals-thick_20_666666_40x40.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_flat_10_000000_40x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_flat_10_000000_40x100.png
new file mode 100644
index 0000000..47dd5a7
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_flat_10_000000_40x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_f6f6f6_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_f6f6f6_1x400.png
new file mode 100644
index 0000000..862ba61
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_f6f6f6_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_fdf5ce_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_fdf5ce_1x400.png
new file mode 100644
index 0000000..f8db5b0
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_100_fdf5ce_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_65_ffffff_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..ec9f0be
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_gloss-wave_35_f6a828_500x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_gloss-wave_35_f6a828_500x100.png
new file mode 100644
index 0000000..28e5131
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_gloss-wave_35_f6a828_500x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_100_eeeeee_1x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
new file mode 100644
index 0000000..7a4a98b
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_100_eeeeee_1x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_75_ffe45c_1x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
new file mode 100644
index 0000000..c0849a2
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-bg_highlight-soft_75_ffe45c_1x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_222222_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000..c1cb117
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_222222_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_228ef1_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_228ef1_256x240.png
new file mode 100644
index 0000000..3a0140c
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_228ef1_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ef8c08_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ef8c08_256x240.png
new file mode 100644
index 0000000..036ee07
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ef8c08_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffd27a_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffd27a_256x240.png
new file mode 100644
index 0000000..8b6c058
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffd27a_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffffff_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffffff_256x240.png
new file mode 100644
index 0000000..4f624bb
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/images/ui-icons_ffffff_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/index.html b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/index.html
new file mode 100644
index 0000000..b878272
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/index.html
@@ -0,0 +1,513 @@
+<!doctype html>
+<html lang="us">
+<head>
+	<meta charset="utf-8">
+	<title>jQuery UI Example Page</title>
+	<link href="jquery-ui.css" rel="stylesheet">
+	<style>
+	body{
+		font: 62.5% "Trebuchet MS", sans-serif;
+		margin: 50px;
+	}
+	.demoHeaders {
+		margin-top: 2em;
+	}
+	#dialog-link {
+		padding: .4em 1em .4em 20px;
+		text-decoration: none;
+		position: relative;
+	}
+	#dialog-link span.ui-icon {
+		margin: 0 5px 0 0;
+		position: absolute;
+		left: .2em;
+		top: 50%;
+		margin-top: -8px;
+	}
+	#icons {
+		margin: 0;
+		padding: 0;
+	}
+	#icons li {
+		margin: 2px;
+		position: relative;
+		padding: 4px 0;
+		cursor: pointer;
+		float: left;
+		list-style: none;
+	}
+	#icons span.ui-icon {
+		float: left;
+		margin: 0 4px;
+	}
+	.fakewindowcontain .ui-widget-overlay {
+		position: absolute;
+	}
+	select {
+		width: 200px;
+	}
+	</style>
+</head>
+<body>
+
+<h1>Welcome to jQuery UI!</h1>
+
+<div class="ui-widget">
+	<p>This page demonstrates the widgets and theme you selected in Download Builder. Please make sure you are using them with a compatible jQuery version.</p>
+</div>
+
+<h1>YOUR COMPONENTS:</h1>
+
+
+<!-- Accordion -->
+<h2 class="demoHeaders">Accordion</h2>
+<div id="accordion">
+	<h3>First</h3>
+	<div>Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.</div>
+	<h3>Second</h3>
+	<div>Phasellus mattis tincidunt nibh.</div>
+	<h3>Third</h3>
+	<div>Nam dui erat, auctor a, dignissim quis.</div>
+</div>
+
+
+
+<!-- Autocomplete -->
+<h2 class="demoHeaders">Autocomplete</h2>
+<div>
+	<input id="autocomplete" title="type "a"">
+</div>
+
+
+
+<!-- Button -->
+<h2 class="demoHeaders">Button</h2>
+<button id="button">A button element</button>
+<form style="margin-top: 1em;">
+	<div id="radioset">
+		<input type="radio" id="radio1" name="radio"><label for="radio1">Choice 1</label>
+		<input type="radio" id="radio2" name="radio" checked="checked"><label for="radio2">Choice 2</label>
+		<input type="radio" id="radio3" name="radio"><label for="radio3">Choice 3</label>
+	</div>
+</form>
+
+
+
+<!-- Tabs -->
+<h2 class="demoHeaders">Tabs</h2>
+<div id="tabs">
+	<ul>
+		<li><a href="#tabs-1">First</a></li>
+		<li><a href="#tabs-2">Second</a></li>
+		<li><a href="#tabs-3">Third</a></li>
+	</ul>
+	<div id="tabs-1">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
+	<div id="tabs-2">Phasellus mattis tincidunt nibh. Cras orci urna, blandit id, pretium vel, aliquet ornare, felis. Maecenas scelerisque sem non nisl. Fusce sed lorem in enim dictum bibendum.</div>
+	<div id="tabs-3">Nam dui erat, auctor a, dignissim quis, sollicitudin eu, felis. Pellentesque nisi urna, interdum eget, sagittis et, consequat vestibulum, lacus. Mauris porttitor ullamcorper augue.</div>
+</div>
+
+
+
+<!-- Dialog NOTE: Dialog is not generated by UI in this demo so it can be visually styled in themeroller-->
+<h2 class="demoHeaders">Dialog</h2>
+<p><a href="#" id="dialog-link" class="ui-state-default ui-corner-all"><span class="ui-icon ui-icon-newwin"></span>Open Dialog</a></p>
+
+<h2 class="demoHeaders">Overlay and Shadow Classes <em>(not currently used in UI widgets)</em></h2>
+<div style="position: relative; width: 96%; height: 200px; padding:1% 2%; overflow:hidden;" class="fakewindowcontain">
+	<p>Lorem ipsum dolor sit amet,  Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. </p><p>Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit a [...]
+
+	<!-- ui-dialog -->
+	<div class="ui-overlay"><div class="ui-widget-overlay"></div><div class="ui-widget-shadow ui-corner-all" style="width: 302px; height: 152px; position: absolute; left: 50px; top: 30px;"></div></div>
+	<div style="position: absolute; width: 280px; height: 130px;left: 50px; top: 30px; padding: 10px;" class="ui-widget ui-widget-content ui-corner-all">
+		<div class="ui-dialog-content ui-widget-content" style="background: none; border: 0;">
+			<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
+		</div>
+	</div>
+
+</div>
+
+<!-- ui-dialog -->
+<div id="dialog" title="Dialog Title">
+	<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
+</div>
+
+
+
+<h2 class="demoHeaders">Framework Icons (content color preview)</h2>
+<ul id="icons" class="ui-widget ui-helper-clearfix">
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-n"><span class="ui-icon ui-icon-carat-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-ne"><span class="ui-icon ui-icon-carat-1-ne"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-e"><span class="ui-icon ui-icon-carat-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-se"><span class="ui-icon ui-icon-carat-1-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-s"><span class="ui-icon ui-icon-carat-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-sw"><span class="ui-icon ui-icon-carat-1-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-w"><span class="ui-icon ui-icon-carat-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-1-nw"><span class="ui-icon ui-icon-carat-1-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-2-n-s"><span class="ui-icon ui-icon-carat-2-n-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-carat-2-e-w"><span class="ui-icon ui-icon-carat-2-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-n"><span class="ui-icon ui-icon-triangle-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-ne"><span class="ui-icon ui-icon-triangle-1-ne"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-e"><span class="ui-icon ui-icon-triangle-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-se"><span class="ui-icon ui-icon-triangle-1-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-s"><span class="ui-icon ui-icon-triangle-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-sw"><span class="ui-icon ui-icon-triangle-1-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-w"><span class="ui-icon ui-icon-triangle-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-nw"><span class="ui-icon ui-icon-triangle-1-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-n-s"><span class="ui-icon ui-icon-triangle-2-n-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-e-w"><span class="ui-icon ui-icon-triangle-2-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-n"><span class="ui-icon ui-icon-arrow-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-ne"><span class="ui-icon ui-icon-arrow-1-ne"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-e"><span class="ui-icon ui-icon-arrow-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-se"><span class="ui-icon ui-icon-arrow-1-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-s"><span class="ui-icon ui-icon-arrow-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-sw"><span class="ui-icon ui-icon-arrow-1-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-w"><span class="ui-icon ui-icon-arrow-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-nw"><span class="ui-icon ui-icon-arrow-1-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-n-s"><span class="ui-icon ui-icon-arrow-2-n-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-ne-sw"><span class="ui-icon ui-icon-arrow-2-ne-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-e-w"><span class="ui-icon ui-icon-arrow-2-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-se-nw"><span class="ui-icon ui-icon-arrow-2-se-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-n"><span class="ui-icon ui-icon-arrowstop-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-e"><span class="ui-icon ui-icon-arrowstop-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-s"><span class="ui-icon ui-icon-arrowstop-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-w"><span class="ui-icon ui-icon-arrowstop-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-n"><span class="ui-icon ui-icon-arrowthick-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-ne"><span class="ui-icon ui-icon-arrowthick-1-ne"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-e"><span class="ui-icon ui-icon-arrowthick-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-se"><span class="ui-icon ui-icon-arrowthick-1-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-s"><span class="ui-icon ui-icon-arrowthick-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-sw"><span class="ui-icon ui-icon-arrowthick-1-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-w"><span class="ui-icon ui-icon-arrowthick-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-nw"><span class="ui-icon ui-icon-arrowthick-1-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-n-s"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-ne-sw"><span class="ui-icon ui-icon-arrowthick-2-ne-sw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-e-w"><span class="ui-icon ui-icon-arrowthick-2-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-se-nw"><span class="ui-icon ui-icon-arrowthick-2-se-nw"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-n"><span class="ui-icon ui-icon-arrowthickstop-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-e"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-s"><span class="ui-icon ui-icon-arrowthickstop-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-w"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-w"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-n"><span class="ui-icon ui-icon-arrowreturnthick-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-e"><span class="ui-icon ui-icon-arrowreturnthick-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-s"><span class="ui-icon ui-icon-arrowreturnthick-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-w"><span class="ui-icon ui-icon-arrowreturn-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-n"><span class="ui-icon ui-icon-arrowreturn-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-e"><span class="ui-icon ui-icon-arrowreturn-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-s"><span class="ui-icon ui-icon-arrowreturn-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-w"><span class="ui-icon ui-icon-arrowrefresh-1-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-n"><span class="ui-icon ui-icon-arrowrefresh-1-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-e"><span class="ui-icon ui-icon-arrowrefresh-1-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-s"><span class="ui-icon ui-icon-arrowrefresh-1-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4"><span class="ui-icon ui-icon-arrow-4"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4-diag"><span class="ui-icon ui-icon-arrow-4-diag"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-extlink"><span class="ui-icon ui-icon-extlink"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-newwin"><span class="ui-icon ui-icon-newwin"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-refresh"><span class="ui-icon ui-icon-refresh"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-shuffle"><span class="ui-icon ui-icon-shuffle"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-transfer-e-w"><span class="ui-icon ui-icon-transfer-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-transferthick-e-w"><span class="ui-icon ui-icon-transferthick-e-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-collapsed"><span class="ui-icon ui-icon-folder-collapsed"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-open"><span class="ui-icon ui-icon-folder-open"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-document"><span class="ui-icon ui-icon-document"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-document-b"><span class="ui-icon ui-icon-document-b"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-note"><span class="ui-icon ui-icon-note"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-closed"><span class="ui-icon ui-icon-mail-closed"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-open"><span class="ui-icon ui-icon-mail-open"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-suitcase"><span class="ui-icon ui-icon-suitcase"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-comment"><span class="ui-icon ui-icon-comment"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-person"><span class="ui-icon ui-icon-person"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-print"><span class="ui-icon ui-icon-print"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-trash"><span class="ui-icon ui-icon-trash"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-locked"><span class="ui-icon ui-icon-locked"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-unlocked"><span class="ui-icon ui-icon-unlocked"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-bookmark"><span class="ui-icon ui-icon-bookmark"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-tag"><span class="ui-icon ui-icon-tag"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-home"><span class="ui-icon ui-icon-home"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-flag"><span class="ui-icon ui-icon-flag"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-calculator"><span class="ui-icon ui-icon-calculator"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-cart"><span class="ui-icon ui-icon-cart"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-pencil"><span class="ui-icon ui-icon-pencil"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-clock"><span class="ui-icon ui-icon-clock"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-disk"><span class="ui-icon ui-icon-disk"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-calendar"><span class="ui-icon ui-icon-calendar"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomin"><span class="ui-icon ui-icon-zoomin"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomout"><span class="ui-icon ui-icon-zoomout"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-search"><span class="ui-icon ui-icon-search"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-wrench"><span class="ui-icon ui-icon-wrench"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-gear"><span class="ui-icon ui-icon-gear"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-heart"><span class="ui-icon ui-icon-heart"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-star"><span class="ui-icon ui-icon-star"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-link"><span class="ui-icon ui-icon-link"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-cancel"><span class="ui-icon ui-icon-cancel"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-plus"><span class="ui-icon ui-icon-plus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-plusthick"><span class="ui-icon ui-icon-plusthick"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-minus"><span class="ui-icon ui-icon-minus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-minusthick"><span class="ui-icon ui-icon-minusthick"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-close"><span class="ui-icon ui-icon-close"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-closethick"><span class="ui-icon ui-icon-closethick"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-key"><span class="ui-icon ui-icon-key"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-lightbulb"><span class="ui-icon ui-icon-lightbulb"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-scissors"><span class="ui-icon ui-icon-scissors"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-clipboard"><span class="ui-icon ui-icon-clipboard"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-copy"><span class="ui-icon ui-icon-copy"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-contact"><span class="ui-icon ui-icon-contact"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-image"><span class="ui-icon ui-icon-image"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-video"><span class="ui-icon ui-icon-video"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-script"><span class="ui-icon ui-icon-script"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-alert"><span class="ui-icon ui-icon-alert"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-info"><span class="ui-icon ui-icon-info"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-notice"><span class="ui-icon ui-icon-notice"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-help"><span class="ui-icon ui-icon-help"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-check"><span class="ui-icon ui-icon-check"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-bullet"><span class="ui-icon ui-icon-bullet"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-off"><span class="ui-icon ui-icon-radio-off"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-on"><span class="ui-icon ui-icon-radio-on"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-w"><span class="ui-icon ui-icon-pin-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-s"><span class="ui-icon ui-icon-pin-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-play"><span class="ui-icon ui-icon-play"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-pause"><span class="ui-icon ui-icon-pause"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-next"><span class="ui-icon ui-icon-seek-next"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-prev"><span class="ui-icon ui-icon-seek-prev"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-end"><span class="ui-icon ui-icon-seek-end"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-first"><span class="ui-icon ui-icon-seek-first"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-stop"><span class="ui-icon ui-icon-stop"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-eject"><span class="ui-icon ui-icon-eject"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-off"><span class="ui-icon ui-icon-volume-off"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-on"><span class="ui-icon ui-icon-volume-on"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-power"><span class="ui-icon ui-icon-power"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-signal-diag"><span class="ui-icon ui-icon-signal-diag"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-signal"><span class="ui-icon ui-icon-signal"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-0"><span class="ui-icon ui-icon-battery-0"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-1"><span class="ui-icon ui-icon-battery-1"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-2"><span class="ui-icon ui-icon-battery-2"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-3"><span class="ui-icon ui-icon-battery-3"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-plus"><span class="ui-icon ui-icon-circle-plus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-minus"><span class="ui-icon ui-icon-circle-minus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-close"><span class="ui-icon ui-icon-circle-close"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-e"><span class="ui-icon ui-icon-circle-triangle-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-s"><span class="ui-icon ui-icon-circle-triangle-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-w"><span class="ui-icon ui-icon-circle-triangle-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-n"><span class="ui-icon ui-icon-circle-triangle-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-e"><span class="ui-icon ui-icon-circle-arrow-e"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-s"><span class="ui-icon ui-icon-circle-arrow-s"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-w"><span class="ui-icon ui-icon-circle-arrow-w"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-n"><span class="ui-icon ui-icon-circle-arrow-n"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomin"><span class="ui-icon ui-icon-circle-zoomin"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomout"><span class="ui-icon ui-icon-circle-zoomout"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-check"><span class="ui-icon ui-icon-circle-check"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-plus"><span class="ui-icon ui-icon-circlesmall-plus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-minus"><span class="ui-icon ui-icon-circlesmall-minus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-close"><span class="ui-icon ui-icon-circlesmall-close"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-plus"><span class="ui-icon ui-icon-squaresmall-plus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-minus"><span class="ui-icon ui-icon-squaresmall-minus"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-close"><span class="ui-icon ui-icon-squaresmall-close"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-vertical"><span class="ui-icon ui-icon-grip-dotted-vertical"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-horizontal"><span class="ui-icon ui-icon-grip-dotted-horizontal"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-vertical"><span class="ui-icon ui-icon-grip-solid-vertical"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-horizontal"><span class="ui-icon ui-icon-grip-solid-horizontal"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-gripsmall-diagonal-se"><span class="ui-icon ui-icon-gripsmall-diagonal-se"></span></li>
+	<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-diagonal-se"><span class="ui-icon ui-icon-grip-diagonal-se"></span></li>
+</ul>
+
+
+<!-- Slider -->
+<h2 class="demoHeaders">Slider</h2>
+<div id="slider"></div>
+
+
+
+<!-- Datepicker -->
+<h2 class="demoHeaders">Datepicker</h2>
+<div id="datepicker"></div>
+
+
+
+<!-- Progressbar -->
+<h2 class="demoHeaders">Progressbar</h2>
+<div id="progressbar"></div>
+
+
+
+<!-- Progressbar -->
+<h2 class="demoHeaders">Selectmenu</h2>
+<select id="selectmenu">
+	<option>Slower</option>
+	<option>Slow</option>
+	<option selected="selected">Medium</option>
+	<option>Fast</option>
+	<option>Faster</option>
+</select>
+
+
+
+<!-- Spinner -->
+<h2 class="demoHeaders">Spinner</h2>
+<input id="spinner">
+
+
+
+<!-- Menu -->
+<h2 class="demoHeaders">Menu</h2>
+<ul style="width:100px;" id="menu">
+	<li>Item 1</li>
+	<li>Item 2</li>
+	<li>Item 3
+		<ul>
+			<li>Item 3-1</li>
+			<li>Item 3-2</li>
+			<li>Item 3-3</li>
+			<li>Item 3-4</li>
+			<li>Item 3-5</li>
+		</ul>
+	</li>
+	<li>Item 4</li>
+	<li>Item 5</li>
+</ul>
+
+
+
+<!-- Tooltip -->
+<h2 class="demoHeaders">Tooltip</h2>
+<p id="tooltip">
+	<a href="#" title="That's what this widget is">Tooltips</a> can be attached to any element. When you hover
+the element with your mouse, the title attribute is displayed in a little box next to the element, just like a native tooltip.
+</p>
+
+
+<!-- Highlight / Error -->
+<h2 class="demoHeaders">Highlight / Error</h2>
+<div class="ui-widget">
+	<div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;">
+		<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
+		<strong>Hey!</strong> Sample ui-state-highlight style.</p>
+	</div>
+</div>
+<br>
+<div class="ui-widget">
+	<div class="ui-state-error ui-corner-all" style="padding: 0 .7em;">
+		<p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
+		<strong>Alert:</strong> Sample ui-state-error style.</p>
+	</div>
+</div>
+
+<script src="external/jquery/jquery.js"></script>
+<script src="jquery-ui.js"></script>
+<script>
+
+$( "#accordion" ).accordion();
+
+
+
+var availableTags = [
+	"ActionScript",
+	"AppleScript",
+	"Asp",
+	"BASIC",
+	"C",
+	"C++",
+	"Clojure",
+	"COBOL",
+	"ColdFusion",
+	"Erlang",
+	"Fortran",
+	"Groovy",
+	"Haskell",
+	"Java",
+	"JavaScript",
+	"Lisp",
+	"Perl",
+	"PHP",
+	"Python",
+	"Ruby",
+	"Scala",
+	"Scheme"
+];
+$( "#autocomplete" ).autocomplete({
+	source: availableTags
+});
+
+
+
+$( "#button" ).button();
+$( "#radioset" ).buttonset();
+
+
+
+$( "#tabs" ).tabs();
+
+
+
+$( "#dialog" ).dialog({
+	autoOpen: false,
+	width: 400,
+	buttons: [
+		{
+			text: "Ok",
+			click: function() {
+				$( this ).dialog( "close" );
+			}
+		},
+		{
+			text: "Cancel",
+			click: function() {
+				$( this ).dialog( "close" );
+			}
+		}
+	]
+});
+
+// Link to open the dialog
+$( "#dialog-link" ).click(function( event ) {
+	$( "#dialog" ).dialog( "open" );
+	event.preventDefault();
+});
+
+
+
+$( "#datepicker" ).datepicker({
+	inline: true
+});
+
+
+
+$( "#slider" ).slider({
+	range: true,
+	values: [ 17, 67 ]
+});
+
+
+
+$( "#progressbar" ).progressbar({
+	value: 20
+});
+
+
+
+$( "#spinner" ).spinner();
+
+
+
+$( "#menu" ).menu();
+
+
+
+$( "#tooltip" ).tooltip();
+
+
+
+$( "#selectmenu" ).selectmenu();
+
+
+// Hover states on the static widgets
+$( "#dialog-link, #icons li" ).hover(
+	function() {
+		$( this ).addClass( "ui-state-hover" );
+	},
+	function() {
+		$( this ).removeClass( "ui-state-hover" );
+	}
+);
+</script>
+</body>
+</html>
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.css
new file mode 100644
index 0000000..a9ee10a
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.css
@@ -0,0 +1,1225 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, menu.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgText [...]
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+	display: none;
+}
+.ui-helper-hidden-accessible {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+}
+.ui-helper-reset {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	outline: 0;
+	line-height: 1.3;
+	text-decoration: none;
+	font-size: 100%;
+	list-style: none;
+}
+.ui-helper-clearfix:before,
+.ui-helper-clearfix:after {
+	content: "";
+	display: table;
+	border-collapse: collapse;
+}
+.ui-helper-clearfix:after {
+	clear: both;
+}
+.ui-helper-clearfix {
+	min-height: 0; /* support: IE7 */
+}
+.ui-helper-zfix {
+	width: 100%;
+	height: 100%;
+	top: 0;
+	left: 0;
+	position: absolute;
+	opacity: 0;
+	filter:Alpha(Opacity=0); /* support: IE8 */
+}
+
+.ui-front {
+	z-index: 100;
+}
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled {
+	cursor: default !important;
+}
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	display: block;
+	text-indent: -99999px;
+	overflow: hidden;
+	background-repeat: no-repeat;
+}
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+.ui-draggable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable {
+	position: relative;
+}
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
+}
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
+}
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
+}
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
+}
+.ui-selectable {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
+.ui-sortable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-accordion .ui-accordion-header {
+	display: block;
+	cursor: pointer;
+	position: relative;
+	margin: 2px 0 0 0;
+	padding: .5em .5em .5em .7em;
+	min-height: 0; /* support: IE7 */
+	font-size: 100%;
+}
+.ui-accordion .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-icons .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
+	position: absolute;
+	left: .5em;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-accordion .ui-accordion-content {
+	padding: 1em 2.2em;
+	border-top: 0;
+	overflow: auto;
+}
+.ui-autocomplete {
+	position: absolute;
+	top: 0;
+	left: 0;
+	cursor: default;
+}
+.ui-button {
+	display: inline-block;
+	position: relative;
+	padding: 0;
+	line-height: normal;
+	margin-right: .1em;
+	cursor: pointer;
+	vertical-align: middle;
+	text-align: center;
+	overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+	text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+	width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+	width: 2.4em;
+}
+.ui-button-icons-only {
+	width: 3.4em;
+}
+button.ui-button-icons-only {
+	width: 3.7em;
+}
+
+/* button text element */
+.ui-button .ui-button-text {
+	display: block;
+	line-height: normal;
+}
+.ui-button-text-only .ui-button-text {
+	padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+	padding: .4em;
+	text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+	padding-left: 2.1em;
+	padding-right: 2.1em;
+}
+/* no icon support for input elements, provide padding by default */
+input.ui-button {
+	padding: .4em 1em;
+}
+
+/* button icon element(s) */
+.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;
+}
+.ui-button-icon-only .ui-icon {
+	left: 50%;
+	margin-left: -8px;
+}
+.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;
+}
+.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;
+}
+
+/* button sets */
+.ui-buttonset {
+	margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+	margin-left: 0;
+	margin-right: -.3em;
+}
+
+/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+	position: absolute;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
+}
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.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;
+}
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 45%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.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;
+}
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
+}
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
+}
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
+}
+
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
+}
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+.ui-dialog {
+	overflow: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+	padding: .2em;
+	outline: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+	padding: .4em 1em;
+	position: relative;
+}
+.ui-dialog .ui-dialog-title {
+	float: left;
+	margin: .1em 0;
+	white-space: nowrap;
+	width: 90%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+	position: absolute;
+	right: .3em;
+	top: 50%;
+	width: 20px;
+	margin: -10px 0 0 0;
+	padding: 1px;
+	height: 20px;
+}
+.ui-dialog .ui-dialog-content {
+	position: relative;
+	border: 0;
+	padding: .5em 1em;
+	background: none;
+	overflow: auto;
+}
+.ui-dialog .ui-dialog-buttonpane {
+	text-align: left;
+	border-width: 1px 0 0 0;
+	background-image: none;
+	margin-top: .5em;
+	padding: .3em 1em .5em .4em;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+	float: right;
+}
+.ui-dialog .ui-dialog-buttonpane button {
+	margin: .5em .4em .5em 0;
+	cursor: pointer;
+}
+.ui-dialog .ui-resizable-se {
+	width: 12px;
+	height: 12px;
+	right: -5px;
+	bottom: -5px;
+	background-position: 16px 16px;
+}
+.ui-draggable .ui-dialog-titlebar {
+	cursor: move;
+}
+.ui-menu {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	display: block;
+	outline: none;
+}
+.ui-menu .ui-menu {
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	position: relative;
+	margin: 0;
+	padding: 3px 1em 3px .4em;
+	cursor: pointer;
+	min-height: 0; /* support: IE7 */
+	/* support: IE10, see #8844 */
+	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px 0;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-state-focus,
+.ui-menu .ui-state-active {
+	margin: -1px;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item {
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	left: .2em;
+	margin: auto 0;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	left: auto;
+	right: 0;
+}
+.ui-progressbar {
+	height: 2em;
+	text-align: left;
+	overflow: hidden;
+}
+.ui-progressbar .ui-progressbar-value {
+	margin: -1px;
+	height: 100%;
+}
+.ui-progressbar .ui-progressbar-overlay {
+	background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmo [...]
+	height: 100%;
+	filter: alpha(opacity=25); /* support: IE8 */
+	opacity: 0.25;
+}
+.ui-progressbar-indeterminate .ui-progressbar-value {
+	background-image: none;
+}
+.ui-selectmenu-menu {
+	padding: 0;
+	margin: 0;
+	position: absolute;
+	top: 0;
+	left: 0;
+	display: none;
+}
+.ui-selectmenu-menu .ui-menu {
+	overflow: auto;
+	/* Support: IE7 */
+	overflow-x: hidden;
+	padding-bottom: 1px;
+}
+.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
+	font-size: 1em;
+	font-weight: bold;
+	line-height: 1.5;
+	padding: 2px 0.4em;
+	margin: 0.5em 0 0 0;
+	height: auto;
+	border: 0;
+}
+.ui-selectmenu-open {
+	display: block;
+}
+.ui-selectmenu-button {
+	display: inline-block;
+	overflow: hidden;
+	position: relative;
+	text-decoration: none;
+	cursor: pointer;
+}
+.ui-selectmenu-button span.ui-icon {
+	right: 0.5em;
+	left: auto;
+	margin-top: -8px;
+	position: absolute;
+	top: 50%;
+}
+.ui-selectmenu-button span.ui-selectmenu-text {
+	text-align: left;
+	padding: 0.4em 2.1em 0.4em 1em;
+	display: block;
+	line-height: 1.4;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+.ui-slider {
+	position: relative;
+	text-align: left;
+}
+.ui-slider .ui-slider-handle {
+	position: absolute;
+	z-index: 2;
+	width: 1.2em;
+	height: 1.2em;
+	cursor: default;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-slider .ui-slider-range {
+	position: absolute;
+	z-index: 1;
+	font-size: .7em;
+	display: block;
+	border: 0;
+	background-position: 0 0;
+}
+
+/* support: IE8 - See #6727 */
+.ui-slider.ui-state-disabled .ui-slider-handle,
+.ui-slider.ui-state-disabled .ui-slider-range {
+	filter: inherit;
+}
+
+.ui-slider-horizontal {
+	height: .8em;
+}
+.ui-slider-horizontal .ui-slider-handle {
+	top: -.3em;
+	margin-left: -.6em;
+}
+.ui-slider-horizontal .ui-slider-range {
+	top: 0;
+	height: 100%;
+}
+.ui-slider-horizontal .ui-slider-range-min {
+	left: 0;
+}
+.ui-slider-horizontal .ui-slider-range-max {
+	right: 0;
+}
+
+.ui-slider-vertical {
+	width: .8em;
+	height: 100px;
+}
+.ui-slider-vertical .ui-slider-handle {
+	left: -.3em;
+	margin-left: 0;
+	margin-bottom: -.6em;
+}
+.ui-slider-vertical .ui-slider-range {
+	left: 0;
+	width: 100%;
+}
+.ui-slider-vertical .ui-slider-range-min {
+	bottom: 0;
+}
+.ui-slider-vertical .ui-slider-range-max {
+	top: 0;
+}
+.ui-spinner {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	padding: 0;
+	vertical-align: middle;
+}
+.ui-spinner-input {
+	border: none;
+	background: none;
+	color: inherit;
+	padding: 0;
+	margin: .2em 0;
+	vertical-align: middle;
+	margin-left: .4em;
+	margin-right: 22px;
+}
+.ui-spinner-button {
+	width: 16px;
+	height: 50%;
+	font-size: .5em;
+	padding: 0;
+	margin: 0;
+	text-align: center;
+	position: absolute;
+	cursor: default;
+	display: block;
+	overflow: hidden;
+	right: 0;
+}
+/* more specificity required here to override default borders */
+.ui-spinner a.ui-spinner-button {
+	border-top: none;
+	border-bottom: none;
+	border-right: none;
+}
+/* vertically center icon */
+.ui-spinner .ui-icon {
+	position: absolute;
+	margin-top: -8px;
+	top: 50%;
+	left: 0;
+}
+.ui-spinner-up {
+	top: 0;
+}
+.ui-spinner-down {
+	bottom: 0;
+}
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position: -65px -16px;
+}
+.ui-tabs {
+	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+	padding: .2em;
+}
+.ui-tabs .ui-tabs-nav {
+	margin: 0;
+	padding: .2em .2em 0;
+}
+.ui-tabs .ui-tabs-nav li {
+	list-style: none;
+	float: left;
+	position: relative;
+	top: 0;
+	margin: 1px .2em 0 0;
+	border-bottom-width: 0;
+	padding: 0;
+	white-space: nowrap;
+}
+.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+	float: left;
+	padding: .5em 1em;
+	text-decoration: none;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active {
+	margin-bottom: -1px;
+	padding-bottom: 1px;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
+	cursor: text;
+}
+.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
+	cursor: pointer;
+}
+.ui-tabs .ui-tabs-panel {
+	display: block;
+	border-width: 0;
+	padding: 1em 1.4em;
+	background: none;
+}
+.ui-tooltip {
+	padding: 8px;
+	position: absolute;
+	z-index: 9999;
+	max-width: 300px;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+body .ui-tooltip {
+	border-width: 2px;
+}
+
+/* Component containers
+----------------------------------*/
+.ui-widget {
+	font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+	font-size: 1.1em;
+}
+.ui-widget .ui-widget {
+	font-size: 1em;
+}
+.ui-widget input,
+.ui-widget select,
+.ui-widget textarea,
+.ui-widget button {
+	font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+	font-size: 1em;
+}
+.ui-widget-content {
+	border: 1px solid #dddddd;
+	background: #eeeeee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;
+	color: #333333;
+}
+.ui-widget-content a {
+	color: #333333;
+}
+.ui-widget-header {
+	border: 1px solid #e78f08;
+	background: #f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;
+	color: #ffffff;
+	font-weight: bold;
+}
+.ui-widget-header a {
+	color: #ffffff;
+}
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default,
+.ui-widget-content .ui-state-default,
+.ui-widget-header .ui-state-default {
+	border: 1px solid #cccccc;
+	background: #f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #1c94c4;
+}
+.ui-state-default a,
+.ui-state-default a:link,
+.ui-state-default a:visited {
+	color: #1c94c4;
+	text-decoration: none;
+}
+.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 #fbcb09;
+	background: #fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #c77405;
+}
+.ui-state-hover a,
+.ui-state-hover a:hover,
+.ui-state-hover a:link,
+.ui-state-hover a:visited,
+.ui-state-focus a,
+.ui-state-focus a:hover,
+.ui-state-focus a:link,
+.ui-state-focus a:visited {
+	color: #c77405;
+	text-decoration: none;
+}
+.ui-state-active,
+.ui-widget-content .ui-state-active,
+.ui-widget-header .ui-state-active {
+	border: 1px solid #fbd850;
+	background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #eb8f00;
+}
+.ui-state-active a,
+.ui-state-active a:link,
+.ui-state-active a:visited {
+	color: #eb8f00;
+	text-decoration: none;
+}
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight,
+.ui-widget-content .ui-state-highlight,
+.ui-widget-header .ui-state-highlight {
+	border: 1px solid #fed22f;
+	background: #ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;
+	color: #363636;
+}
+.ui-state-highlight a,
+.ui-widget-content .ui-state-highlight a,
+.ui-widget-header .ui-state-highlight a {
+	color: #363636;
+}
+.ui-state-error,
+.ui-widget-content .ui-state-error,
+.ui-widget-header .ui-state-error {
+	border: 1px solid #cd0a0a;
+	background: #b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;
+	color: #ffffff;
+}
+.ui-state-error a,
+.ui-widget-content .ui-state-error a,
+.ui-widget-header .ui-state-error a {
+	color: #ffffff;
+}
+.ui-state-error-text,
+.ui-widget-content .ui-state-error-text,
+.ui-widget-header .ui-state-error-text {
+	color: #ffffff;
+}
+.ui-priority-primary,
+.ui-widget-content .ui-priority-primary,
+.ui-widget-header .ui-priority-primary {
+	font-weight: bold;
+}
+.ui-priority-secondary,
+.ui-widget-content .ui-priority-secondary,
+.ui-widget-header .ui-priority-secondary {
+	opacity: .7;
+	filter:Alpha(Opacity=70); /* support: IE8 */
+	font-weight: normal;
+}
+.ui-state-disabled,
+.ui-widget-content .ui-state-disabled,
+.ui-widget-header .ui-state-disabled {
+	opacity: .35;
+	filter:Alpha(Opacity=35); /* support: IE8 */
+	background-image: none;
+}
+.ui-state-disabled .ui-icon {
+	filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
+}
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	width: 16px;
+	height: 16px;
+}
+.ui-icon,
+.ui-widget-content .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png");
+}
+.ui-widget-header .ui-icon {
+	background-image: url("images/ui-icons_ffffff_256x240.png");
+}
+.ui-state-default .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-hover .ui-icon,
+.ui-state-focus .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-active .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-highlight .ui-icon {
+	background-image: url("images/ui-icons_228ef1_256x240.png");
+}
+.ui-state-error .ui-icon,
+.ui-state-error-text .ui-icon {
+	background-image: url("images/ui-icons_ffd27a_256x240.png");
+}
+
+/* positioning */
+.ui-icon-blank { background-position: 16px 16px; }
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-left,
+.ui-corner-tl {
+	border-top-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-right,
+.ui-corner-tr {
+	border-top-right-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-left,
+.ui-corner-bl {
+	border-bottom-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-right,
+.ui-corner-br {
+	border-bottom-right-radius: 4px;
+}
+
+/* Overlays */
+.ui-widget-overlay {
+	background: #666666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;
+	opacity: .5;
+	filter: Alpha(Opacity=50); /* support: IE8 */
+}
+.ui-widget-shadow {
+	margin: -5px 0 0 -5px;
+	padding: 5px;
+	background: #000000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;
+	opacity: .2;
+	filter: Alpha(Opacity=20); /* support: IE8 */
+	border-radius: 5px;
+}
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.js b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.js
new file mode 100644
index 0000000..8f7dea0
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.js
@@ -0,0 +1,16582 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Includes: core.js, widget.js, mouse.js, position.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, menu.js, progressbar.js, selectmenu.js, slider.js, spinner.js, tabs.js, tooltip.js, effect.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake. [...]
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define([ "jquery" ], factory );
+	} else {
+
+		// Browser globals
+		factory( jQuery );
+	}
+}(function( $ ) {
+/*!
+ * jQuery UI Core 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/ui-core/
+ */
+
+
+// $.ui might exist from components with no dependencies, e.g., $.ui.position
+$.ui = $.ui || {};
+
+$.extend( $.ui, {
+	version: "1.11.2",
+
+	keyCode: {
+		BACKSPACE: 8,
+		COMMA: 188,
+		DELETE: 46,
+		DOWN: 40,
+		END: 35,
+		ENTER: 13,
+		ESCAPE: 27,
+		HOME: 36,
+		LEFT: 37,
+		PAGE_DOWN: 34,
+		PAGE_UP: 33,
+		PERIOD: 190,
+		RIGHT: 39,
+		SPACE: 32,
+		TAB: 9,
+		UP: 38
+	}
+});
+
+// plugins
+$.fn.extend({
+	scrollParent: function( includeHidden ) {
+		var position = this.css( "position" ),
+			excludeStaticParent = position === "absolute",
+			overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/,
+			scrollParent = this.parents().filter( function() {
+				var parent = $( this );
+				if ( excludeStaticParent && parent.css( "position" ) === "static" ) {
+					return false;
+				}
+				return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) );
+			}).eq( 0 );
+
+		return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent;
+	},
+
+	uniqueId: (function() {
+		var uuid = 0;
+
+		return function() {
+			return this.each(function() {
+				if ( !this.id ) {
+					this.id = "ui-id-" + ( ++uuid );
+				}
+			});
+		};
+	})(),
+
+	removeUniqueId: function() {
+		return this.each(function() {
+			if ( /^ui-id-\d+$/.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		});
+	}
+});
+
+// selectors
+function focusable( element, isTabIndexNotNaN ) {
+	var map, mapName, img,
+		nodeName = element.nodeName.toLowerCase();
+	if ( "area" === nodeName ) {
+		map = element.parentNode;
+		mapName = map.name;
+		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
+			return false;
+		}
+		img = $( "img[usemap='#" + mapName + "']" )[ 0 ];
+		return !!img && visible( img );
+	}
+	return ( /input|select|textarea|button|object/.test( nodeName ) ?
+		!element.disabled :
+		"a" === nodeName ?
+			element.href || isTabIndexNotNaN :
+			isTabIndexNotNaN) &&
+		// the element and all of its ancestors must be visible
+		visible( element );
+}
+
+function visible( element ) {
+	return $.expr.filters.visible( element ) &&
+		!$( element ).parents().addBack().filter(function() {
+			return $.css( this, "visibility" ) === "hidden";
+		}).length;
+}
+
+$.extend( $.expr[ ":" ], {
+	data: $.expr.createPseudo ?
+		$.expr.createPseudo(function( dataName ) {
+			return function( elem ) {
+				return !!$.data( elem, dataName );
+			};
+		}) :
+		// support: jQuery <1.8
+		function( elem, i, match ) {
+			return !!$.data( elem, match[ 3 ] );
+		},
+
+	focusable: function( element ) {
+		return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
+	},
+
+	tabbable: function( element ) {
+		var tabIndex = $.attr( element, "tabindex" ),
+			isTabIndexNaN = isNaN( tabIndex );
+		return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
+	}
+});
+
+// support: jQuery <1.8
+if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
+	$.each( [ "Width", "Height" ], function( i, name ) {
+		var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
+			type = name.toLowerCase(),
+			orig = {
+				innerWidth: $.fn.innerWidth,
+				innerHeight: $.fn.innerHeight,
+				outerWidth: $.fn.outerWidth,
+				outerHeight: $.fn.outerHeight
+			};
+
+		function reduce( elem, size, border, margin ) {
+			$.each( side, function() {
+				size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
+				if ( border ) {
+					size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
+				}
+				if ( margin ) {
+					size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
+				}
+			});
+			return size;
+		}
+
+		$.fn[ "inner" + name ] = function( size ) {
+			if ( size === undefined ) {
+				return orig[ "inner" + name ].call( this );
+			}
+
+			return this.each(function() {
+				$( this ).css( type, reduce( this, size ) + "px" );
+			});
+		};
+
+		$.fn[ "outer" + name] = function( size, margin ) {
+			if ( typeof size !== "number" ) {
+				return orig[ "outer" + name ].call( this, size );
+			}
+
+			return this.each(function() {
+				$( this).css( type, reduce( this, size, true, margin ) + "px" );
+			});
+		};
+	});
+}
+
+// support: jQuery <1.8
+if ( !$.fn.addBack ) {
+	$.fn.addBack = function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter( selector )
+		);
+	};
+}
+
+// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
+if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) {
+	$.fn.removeData = (function( removeData ) {
+		return function( key ) {
+			if ( arguments.length ) {
+				return removeData.call( this, $.camelCase( key ) );
+			} else {
+				return removeData.call( this );
+			}
+		};
+	})( $.fn.removeData );
+}
+
+// deprecated
+$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() );
+
+$.fn.extend({
+	focus: (function( orig ) {
+		return function( delay, fn ) {
+			return typeof delay === "number" ?
+				this.each(function() {
+					var elem = this;
+					setTimeout(function() {
+						$( elem ).focus();
+						if ( fn ) {
+							fn.call( elem );
+						}
+					}, delay );
+				}) :
+				orig.apply( this, arguments );
+		};
+	})( $.fn.focus ),
+
+	disableSelection: (function() {
+		var eventType = "onselectstart" in document.createElement( "div" ) ?
+			"selectstart" :
+			"mousedown";
+
+		return function() {
+			return this.bind( eventType + ".ui-disableSelection", function( event ) {
+				event.preventDefault();
+			});
+		};
+	})(),
+
+	enableSelection: function() {
+		return this.unbind( ".ui-disableSelection" );
+	},
+
+	zIndex: function( zIndex ) {
+		if ( zIndex !== undefined ) {
+			return this.css( "zIndex", zIndex );
+		}
+
+		if ( this.length ) {
+			var elem = $( this[ 0 ] ), position, value;
+			while ( elem.length && elem[ 0 ] !== document ) {
+				// Ignore z-index if position is set to a value where z-index is ignored by the browser
+				// This makes behavior of this function consistent across browsers
+				// WebKit always returns auto if the element is positioned
+				position = elem.css( "position" );
+				if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+					// IE returns 0 when zIndex is not specified
+					// other browsers return a string
+					// we ignore the case of nested elements with an explicit value of 0
+					// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+					value = parseInt( elem.css( "zIndex" ), 10 );
+					if ( !isNaN( value ) && value !== 0 ) {
+						return value;
+					}
+				}
+				elem = elem.parent();
+			}
+		}
+
+		return 0;
+	}
+});
+
+// $.ui.plugin is deprecated. Use $.widget() extensions instead.
+$.ui.plugin = {
+	add: function( module, option, set ) {
+		var i,
+			proto = $.ui[ module ].prototype;
+		for ( i in set ) {
+			proto.plugins[ i ] = proto.plugins[ i ] || [];
+			proto.plugins[ i ].push( [ option, set[ i ] ] );
+		}
+	},
+	call: function( instance, name, args, allowDisconnected ) {
+		var i,
+			set = instance.plugins[ name ];
+
+		if ( !set ) {
+			return;
+		}
+
+		if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) ) {
+			return;
+		}
+
+		for ( i = 0; i < set.length; i++ ) {
+			if ( instance.options[ set[ i ][ 0 ] ] ) {
+				set[ i ][ 1 ].apply( instance.element, args );
+			}
+		}
+	}
+};
+
+
+/*!
+ * jQuery UI Widget 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/jQuery.widget/
+ */
+
+
+var widget_uuid = 0,
+	widget_slice = Array.prototype.slice;
+
+$.cleanData = (function( orig ) {
+	return function( elems ) {
+		var events, elem, i;
+		for ( i = 0; (elem = elems[i]) != null; i++ ) {
+			try {
+
+				// Only trigger remove when necessary to save time
+				events = $._data( elem, "events" );
+				if ( events && events.remove ) {
+					$( elem ).triggerHandler( "remove" );
+				}
+
+			// http://bugs.jquery.com/ticket/8235
+			} catch ( e ) {}
+		}
+		orig( elems );
+	};
+})( $.cleanData );
+
+$.widget = function( name, base, prototype ) {
+	var fullName, existingConstructor, constructor, basePrototype,
+		// proxiedPrototype allows the provided prototype to remain unmodified
+		// so that it can be used as a mixin for multiple widgets (#8876)
+		proxiedPrototype = {},
+		namespace = name.split( "." )[ 0 ];
+
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+	// extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+		// copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+		// track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	});
+
+	basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( !$.isFunction( value ) ) {
+			proxiedPrototype[ prop ] = value;
+			return;
+		}
+		proxiedPrototype[ prop ] = (function() {
+			var _super = function() {
+					return base.prototype[ prop ].apply( this, arguments );
+				},
+				_superApply = function( args ) {
+					return base.prototype[ prop ].apply( this, args );
+				};
+			return function() {
+				var __super = this._super,
+					__superApply = this._superApply,
+					returnValue;
+
+				this._super = _super;
+				this._superApply = _superApply;
+
+				returnValue = value.apply( this, arguments );
+
+				this._super = __super;
+				this._superApply = __superApply;
+
+				return returnValue;
+			};
+		})();
+	});
+	constructor.prototype = $.widget.extend( basePrototype, {
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name
+	}, proxiedPrototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		widgetFullName: fullName
+	});
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
+		});
+		// remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+
+	return constructor;
+};
+
+$.widget.extend = function( target ) {
+	var input = widget_slice.call( arguments, 1 ),
+		inputIndex = 0,
+		inputLength = input.length,
+		key,
+		value;
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+				// Clone objects
+				if ( $.isPlainObject( value ) ) {
+					target[ key ] = $.isPlainObject( target[ key ] ) ?
+						$.widget.extend( {}, target[ key ], value ) :
+						// Don't extend strings, arrays, etc. with objects
+						$.widget.extend( {}, value );
+				// Copy everything else by reference
+				} else {
+					target[ key ] = value;
+				}
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName || name;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = widget_slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.widget.extend.apply( null, [ options ].concat(args) ) :
+			options;
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var methodValue,
+					instance = $.data( this, fullName );
+				if ( options === "instance" ) {
+					returnValue = instance;
+					return false;
+				}
+				if ( !instance ) {
+					return $.error( "cannot call methods on " + name + " prior to initialization; " +
+						"attempted to call method '" + options + "'" );
+				}
+				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
+					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
+				}
+				methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue && methodValue.jquery ?
+						returnValue.pushStack( methodValue.get() ) :
+						methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} );
+					if ( instance._init ) {
+						instance._init();
+					}
+				} else {
+					$.data( this, fullName, new object( options, this ) );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( /* options, element */ ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+	options: {
+		disabled: false,
+
+		// callbacks
+		create: null
+	},
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = widget_uuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+
+		if ( element !== this ) {
+			$.data( element, this.widgetFullName, this );
+			this._on( true, this.element, {
+				remove: function( event ) {
+					if ( event.target === element ) {
+						this.destroy();
+					}
+				}
+			});
+			this.document = $( element.style ?
+				// element within the document
+				element.ownerDocument :
+				// element is window or document
+				element.document || element );
+			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
+		}
+
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this._create();
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+	_getCreateOptions: $.noop,
+	_getCreateEventData: $.noop,
+	_create: $.noop,
+	_init: $.noop,
+
+	destroy: function() {
+		this._destroy();
+		// we can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.unbind( this.eventNamespace )
+			.removeData( this.widgetFullName )
+			// support: jquery <1.6.3
+			// http://bugs.jquery.com/ticket/9413
+			.removeData( $.camelCase( this.widgetFullName ) );
+		this.widget()
+			.unbind( this.eventNamespace )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetFullName + "-disabled " +
+				"ui-state-disabled" );
+
+		// clean up events and states
+		this.bindings.unbind( this.eventNamespace );
+		this.hoverable.removeClass( "ui-state-hover" );
+		this.focusable.removeClass( "ui-state-focus" );
+	},
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key,
+			parts,
+			curOption,
+			i;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( arguments.length === 1 ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( arguments.length === 1 ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				.toggleClass( this.widgetFullName + "-disabled", !!value );
+
+			// If the widget is becoming disabled, then nothing is interactive
+			if ( value ) {
+				this.hoverable.removeClass( "ui-state-hover" );
+				this.focusable.removeClass( "ui-state-focus" );
+			}
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOptions({ disabled: false });
+	},
+	disable: function() {
+		return this._setOptions({ disabled: true });
+	},
+
+	_on: function( suppressDisabledCheck, element, handlers ) {
+		var delegateElement,
+			instance = this;
+
+		// no suppressDisabledCheck flag, shuffle arguments
+		if ( typeof suppressDisabledCheck !== "boolean" ) {
+			handlers = element;
+			element = suppressDisabledCheck;
+			suppressDisabledCheck = false;
+		}
+
+		// no element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+			delegateElement = this.widget();
+		} else {
+			element = delegateElement = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+				// allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( !suppressDisabledCheck &&
+						( instance.options.disabled === true ||
+							$( this ).hasClass( "ui-state-disabled" ) ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
+				eventName = match[1] + instance.eventNamespace,
+				selector = match[2];
+			if ( selector ) {
+				delegateElement.delegate( selector, eventName, handlerProxy );
+			} else {
+				element.bind( eventName, handlerProxy );
+			}
+		});
+	},
+
+	_off: function( element, eventName ) {
+		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) +
+			this.eventNamespace;
+		element.unbind( eventName ).undelegate( eventName );
+
+		// Clear the stack to avoid memory leaks (#10056)
+		this.bindings = $( this.bindings.not( element ).get() );
+		this.focusable = $( this.focusable.not( element ).get() );
+		this.hoverable = $( this.hoverable.not( element ).get() );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-hover" );
+			}
+		});
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-focus" );
+			}
+		});
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+		var hasOptions,
+			effectName = !options ?
+				method :
+				options === true || typeof options === "number" ?
+					defaultEffect :
+					options.effect || defaultEffect;
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue(function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			});
+		}
+	};
+});
+
+var widget = $.widget;
+
+
+/*!
+ * jQuery UI Mouse 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/mouse/
+ */
+
+
+var mouseHandled = false;
+$( document ).mouseup( function() {
+	mouseHandled = false;
+});
+
+var mouse = $.widget("ui.mouse", {
+	version: "1.11.2",
+	options: {
+		cancel: "input,textarea,button,select,option",
+		distance: 1,
+		delay: 0
+	},
+	_mouseInit: function() {
+		var that = this;
+
+		this.element
+			.bind("mousedown." + this.widgetName, function(event) {
+				return that._mouseDown(event);
+			})
+			.bind("click." + this.widgetName, function(event) {
+				if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) {
+					$.removeData(event.target, that.widgetName + ".preventClickEvent");
+					event.stopImmediatePropagation();
+					return false;
+				}
+			});
+
+		this.started = false;
+	},
+
+	// TODO: make sure destroying one instance of mouse doesn't mess with
+	// other instances of mouse
+	_mouseDestroy: function() {
+		this.element.unbind("." + this.widgetName);
+		if ( this._mouseMoveDelegate ) {
+			this.document
+				.unbind("mousemove." + this.widgetName, this._mouseMoveDelegate)
+				.unbind("mouseup." + this.widgetName, this._mouseUpDelegate);
+		}
+	},
+
+	_mouseDown: function(event) {
+		// don't let more than one widget handle mouseStart
+		if ( mouseHandled ) {
+			return;
+		}
+
+		this._mouseMoved = false;
+
+		// we may have missed mouseup (out of window)
+		(this._mouseStarted && this._mouseUp(event));
+
+		this._mouseDownEvent = event;
+
+		var that = this,
+			btnIsLeft = (event.which === 1),
+			// event.target.nodeName works around a bug in IE 8 with
+			// disabled inputs (#7620)
+			elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false);
+		if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) {
+			return true;
+		}
+
+		this.mouseDelayMet = !this.options.delay;
+		if (!this.mouseDelayMet) {
+			this._mouseDelayTimer = setTimeout(function() {
+				that.mouseDelayMet = true;
+			}, this.options.delay);
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted = (this._mouseStart(event) !== false);
+			if (!this._mouseStarted) {
+				event.preventDefault();
+				return true;
+			}
+		}
+
+		// Click event may never have fired (Gecko & Opera)
+		if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) {
+			$.removeData(event.target, this.widgetName + ".preventClickEvent");
+		}
+
+		// these delegates are required to keep context
+		this._mouseMoveDelegate = function(event) {
+			return that._mouseMove(event);
+		};
+		this._mouseUpDelegate = function(event) {
+			return that._mouseUp(event);
+		};
+
+		this.document
+			.bind( "mousemove." + this.widgetName, this._mouseMoveDelegate )
+			.bind( "mouseup." + this.widgetName, this._mouseUpDelegate );
+
+		event.preventDefault();
+
+		mouseHandled = true;
+		return true;
+	},
+
+	_mouseMove: function(event) {
+		// Only check for mouseups outside the document if you've moved inside the document
+		// at least once. This prevents the firing of mouseup in the case of IE<9, which will
+		// fire a mousemove event if content is placed under the cursor. See #7778
+		// Support: IE <9
+		if ( this._mouseMoved ) {
+			// IE mouseup check - mouseup happened when mouse was out of window
+			if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) {
+				return this._mouseUp(event);
+
+			// Iframe mouseup check - mouseup occurred in another document
+			} else if ( !event.which ) {
+				return this._mouseUp( event );
+			}
+		}
+
+		if ( event.which || event.button ) {
+			this._mouseMoved = true;
+		}
+
+		if (this._mouseStarted) {
+			this._mouseDrag(event);
+			return event.preventDefault();
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted =
+				(this._mouseStart(this._mouseDownEvent, event) !== false);
+			(this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event));
+		}
+
+		return !this._mouseStarted;
+	},
+
+	_mouseUp: function(event) {
+		this.document
+			.unbind( "mousemove." + this.widgetName, this._mouseMoveDelegate )
+			.unbind( "mouseup." + this.widgetName, this._mouseUpDelegate );
+
+		if (this._mouseStarted) {
+			this._mouseStarted = false;
+
+			if (event.target === this._mouseDownEvent.target) {
+				$.data(event.target, this.widgetName + ".preventClickEvent", true);
+			}
+
+			this._mouseStop(event);
+		}
+
+		mouseHandled = false;
+		return false;
+	},
+
+	_mouseDistanceMet: function(event) {
+		return (Math.max(
+				Math.abs(this._mouseDownEvent.pageX - event.pageX),
+				Math.abs(this._mouseDownEvent.pageY - event.pageY)
+			) >= this.options.distance
+		);
+	},
+
+	_mouseDelayMet: function(/* event */) {
+		return this.mouseDelayMet;
+	},
+
+	// These are placeholder methods, to be overriden by extending plugin
+	_mouseStart: function(/* event */) {},
+	_mouseDrag: function(/* event */) {},
+	_mouseStop: function(/* event */) {},
+	_mouseCapture: function(/* event */) { return true; }
+});
+
+
+/*!
+ * jQuery UI Position 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/position/
+ */
+
+(function() {
+
+$.ui = $.ui || {};
+
+var cachedScrollbarWidth, supportsOffsetFractions,
+	max = Math.max,
+	abs = Math.abs,
+	round = Math.round,
+	rhorizontal = /left|center|right/,
+	rvertical = /top|center|bottom/,
+	roffset = /[\+\-]\d+(\.[\d]+)?%?/,
+	rposition = /^\w+/,
+	rpercent = /%$/,
+	_position = $.fn.position;
+
+function getOffsets( offsets, width, height ) {
+	return [
+		parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
+		parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
+	];
+}
+
+function parseCss( element, property ) {
+	return parseInt( $.css( element, property ), 10 ) || 0;
+}
+
+function getDimensions( elem ) {
+	var raw = elem[0];
+	if ( raw.nodeType === 9 ) {
+		return {
+			width: elem.width(),
+			height: elem.height(),
+			offset: { top: 0, left: 0 }
+		};
+	}
+	if ( $.isWindow( raw ) ) {
+		return {
+			width: elem.width(),
+			height: elem.height(),
+			offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
+		};
+	}
+	if ( raw.preventDefault ) {
+		return {
+			width: 0,
+			height: 0,
+			offset: { top: raw.pageY, left: raw.pageX }
+		};
+	}
+	return {
+		width: elem.outerWidth(),
+		height: elem.outerHeight(),
+		offset: elem.offset()
+	};
+}
+
+$.position = {
+	scrollbarWidth: function() {
+		if ( cachedScrollbarWidth !== undefined ) {
+			return cachedScrollbarWidth;
+		}
+		var w1, w2,
+			div = $( "<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
+			innerDiv = div.children()[0];
+
+		$( "body" ).append( div );
+		w1 = innerDiv.offsetWidth;
+		div.css( "overflow", "scroll" );
+
+		w2 = innerDiv.offsetWidth;
+
+		if ( w1 === w2 ) {
+			w2 = div[0].clientWidth;
+		}
+
+		div.remove();
+
+		return (cachedScrollbarWidth = w1 - w2);
+	},
+	getScrollInfo: function( within ) {
+		var overflowX = within.isWindow || within.isDocument ? "" :
+				within.element.css( "overflow-x" ),
+			overflowY = within.isWindow || within.isDocument ? "" :
+				within.element.css( "overflow-y" ),
+			hasOverflowX = overflowX === "scroll" ||
+				( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
+			hasOverflowY = overflowY === "scroll" ||
+				( overflowY === "auto" && within.height < within.element[0].scrollHeight );
+		return {
+			width: hasOverflowY ? $.position.scrollbarWidth() : 0,
+			height: hasOverflowX ? $.position.scrollbarWidth() : 0
+		};
+	},
+	getWithinInfo: function( element ) {
+		var withinElement = $( element || window ),
+			isWindow = $.isWindow( withinElement[0] ),
+			isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9;
+		return {
+			element: withinElement,
+			isWindow: isWindow,
+			isDocument: isDocument,
+			offset: withinElement.offset() || { left: 0, top: 0 },
+			scrollLeft: withinElement.scrollLeft(),
+			scrollTop: withinElement.scrollTop(),
+
+			// support: jQuery 1.6.x
+			// jQuery 1.6 doesn't support .outerWidth/Height() on documents or windows
+			width: isWindow || isDocument ? withinElement.width() : withinElement.outerWidth(),
+			height: isWindow || isDocument ? withinElement.height() : withinElement.outerHeight()
+		};
+	}
+};
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
+		target = $( options.of ),
+		within = $.position.getWithinInfo( options.within ),
+		scrollInfo = $.position.getScrollInfo( within ),
+		collision = ( options.collision || "flip" ).split( " " ),
+		offsets = {};
+
+	dimensions = getDimensions( target );
+	if ( target[0].preventDefault ) {
+		// force left top to allow flipping
+		options.at = "left top";
+	}
+	targetWidth = dimensions.width;
+	targetHeight = dimensions.height;
+	targetOffset = dimensions.offset;
+	// clone to reuse original targetOffset later
+	basePosition = $.extend( {}, targetOffset );
+
+	// force my and at to have valid horizontal and vertical positions
+	// if a value is missing or invalid, it will be converted to center
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[ this ] || "" ).split( " " ),
+			horizontalOffset,
+			verticalOffset;
+
+		if ( pos.length === 1) {
+			pos = rhorizontal.test( pos[ 0 ] ) ?
+				pos.concat( [ "center" ] ) :
+				rvertical.test( pos[ 0 ] ) ?
+					[ "center" ].concat( pos ) :
+					[ "center", "center" ];
+		}
+		pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
+		pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
+
+		// calculate offsets
+		horizontalOffset = roffset.exec( pos[ 0 ] );
+		verticalOffset = roffset.exec( pos[ 1 ] );
+		offsets[ this ] = [
+			horizontalOffset ? horizontalOffset[ 0 ] : 0,
+			verticalOffset ? verticalOffset[ 0 ] : 0
+		];
+
+		// reduce to just the positions without the offsets
+		options[ this ] = [
+			rposition.exec( pos[ 0 ] )[ 0 ],
+			rposition.exec( pos[ 1 ] )[ 0 ]
+		];
+	});
+
+	// normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	if ( options.at[ 0 ] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[ 0 ] === "center" ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[ 1 ] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[ 1 ] === "center" ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
+	basePosition.left += atOffset[ 0 ];
+	basePosition.top += atOffset[ 1 ];
+
+	return this.each(function() {
+		var collisionPosition, using,
+			elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseCss( this, "marginLeft" ),
+			marginTop = parseCss( this, "marginTop" ),
+			collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
+			collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
+			position = $.extend( {}, basePosition ),
+			myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
+
+		if ( options.my[ 0 ] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[ 0 ] === "center" ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[ 1 ] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[ 1 ] === "center" ) {
+			position.top -= elemHeight / 2;
+		}
+
+		position.left += myOffset[ 0 ];
+		position.top += myOffset[ 1 ];
+
+		// if the browser doesn't support fractions, then round for consistent results
+		if ( !supportsOffsetFractions ) {
+			position.left = round( position.left );
+			position.top = round( position.top );
+		}
+
+		collisionPosition = {
+			marginLeft: marginLeft,
+			marginTop: marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[ i ] ] ) {
+				$.ui.position[ collision[ i ] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
+					my: options.my,
+					at: options.at,
+					within: within,
+					elem: elem
+				});
+			}
+		});
+
+		if ( options.using ) {
+			// adds feedback as second argument to using callback, if present
+			using = function( props ) {
+				var left = targetOffset.left - position.left,
+					right = left + targetWidth - elemWidth,
+					top = targetOffset.top - position.top,
+					bottom = top + targetHeight - elemHeight,
+					feedback = {
+						target: {
+							element: target,
+							left: targetOffset.left,
+							top: targetOffset.top,
+							width: targetWidth,
+							height: targetHeight
+						},
+						element: {
+							element: elem,
+							left: position.left,
+							top: position.top,
+							width: elemWidth,
+							height: elemHeight
+						},
+						horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
+						vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
+					};
+				if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
+					feedback.horizontal = "center";
+				}
+				if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
+					feedback.vertical = "middle";
+				}
+				if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
+					feedback.important = "horizontal";
+				} else {
+					feedback.important = "vertical";
+				}
+				options.using.call( this, props, feedback );
+			};
+		}
+
+		elem.offset( $.extend( position, { using: using } ) );
+	});
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
+				outerWidth = within.width,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = withinOffset - collisionPosLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
+				newOverRight;
+
+			// element is wider than within
+			if ( data.collisionWidth > outerWidth ) {
+				// element is initially over the left side of within
+				if ( overLeft > 0 && overRight <= 0 ) {
+					newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
+					position.left += overLeft - newOverRight;
+				// element is initially over right side of within
+				} else if ( overRight > 0 && overLeft <= 0 ) {
+					position.left = withinOffset;
+				// element is initially over both left and right sides of within
+				} else {
+					if ( overLeft > overRight ) {
+						position.left = withinOffset + outerWidth - data.collisionWidth;
+					} else {
+						position.left = withinOffset;
+					}
+				}
+			// too far left -> align with left edge
+			} else if ( overLeft > 0 ) {
+				position.left += overLeft;
+			// too far right -> align with right edge
+			} else if ( overRight > 0 ) {
+				position.left -= overRight;
+			// adjust based on position and margin
+			} else {
+				position.left = max( position.left - collisionPosLeft, position.left );
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
+				outerHeight = data.within.height,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = withinOffset - collisionPosTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
+				newOverBottom;
+
+			// element is taller than within
+			if ( data.collisionHeight > outerHeight ) {
+				// element is initially over the top of within
+				if ( overTop > 0 && overBottom <= 0 ) {
+					newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
+					position.top += overTop - newOverBottom;
+				// element is initially over bottom of within
+				} else if ( overBottom > 0 && overTop <= 0 ) {
+					position.top = withinOffset;
+				// element is initially over both top and bottom of within
+				} else {
+					if ( overTop > overBottom ) {
+						position.top = withinOffset + outerHeight - data.collisionHeight;
+					} else {
+						position.top = withinOffset;
+					}
+				}
+			// too far up -> align with top
+			} else if ( overTop > 0 ) {
+				position.top += overTop;
+			// too far down -> align with bottom edge
+			} else if ( overBottom > 0 ) {
+				position.top -= overBottom;
+			// adjust based on position and margin
+			} else {
+				position.top = max( position.top - collisionPosTop, position.top );
+			}
+		}
+	},
+	flip: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.left + within.scrollLeft,
+				outerWidth = within.width,
+				offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = collisionPosLeft - offsetLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					data.at[ 0 ] === "right" ?
+						-data.targetWidth :
+						0,
+				offset = -2 * data.offset[ 0 ],
+				newOverRight,
+				newOverLeft;
+
+			if ( overLeft < 0 ) {
+				newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
+				if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			} else if ( overRight > 0 ) {
+				newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
+				if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.top + within.scrollTop,
+				outerHeight = within.height,
+				offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = collisionPosTop - offsetTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
+				top = data.my[ 1 ] === "top",
+				myOffset = top ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					data.at[ 1 ] === "bottom" ?
+						-data.targetHeight :
+						0,
+				offset = -2 * data.offset[ 1 ],
+				newOverTop,
+				newOverBottom;
+			if ( overTop < 0 ) {
+				newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
+				if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			} else if ( overBottom > 0 ) {
+				newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
+				if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+		}
+	},
+	flipfit: {
+		left: function() {
+			$.ui.position.flip.left.apply( this, arguments );
+			$.ui.position.fit.left.apply( this, arguments );
+		},
+		top: function() {
+			$.ui.position.flip.top.apply( this, arguments );
+			$.ui.position.fit.top.apply( this, arguments );
+		}
+	}
+};
+
+// fraction support test
+(function() {
+	var testElement, testElementParent, testElementStyle, offsetLeft, i,
+		body = document.getElementsByTagName( "body" )[ 0 ],
+		div = document.createElement( "div" );
+
+	//Create a "fake body" for testing based on method used in jQuery.support
+	testElement = document.createElement( body ? "div" : "body" );
+	testElementStyle = {
+		visibility: "hidden",
+		width: 0,
+		height: 0,
+		border: 0,
+		margin: 0,
+		background: "none"
+	};
+	if ( body ) {
+		$.extend( testElementStyle, {
+			position: "absolute",
+			left: "-1000px",
+			top: "-1000px"
+		});
+	}
+	for ( i in testElementStyle ) {
+		testElement.style[ i ] = testElementStyle[ i ];
+	}
+	testElement.appendChild( div );
+	testElementParent = body || document.documentElement;
+	testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+	div.style.cssText = "position: absolute; left: 10.7432222px;";
+
+	offsetLeft = $( div ).offset().left;
+	supportsOffsetFractions = offsetLeft > 10 && offsetLeft < 11;
+
+	testElement.innerHTML = "";
+	testElementParent.removeChild( testElement );
+})();
+
+})();
+
+var position = $.ui.position;
+
+
+/*!
+ * jQuery UI Draggable 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/draggable/
+ */
+
+
+$.widget("ui.draggable", $.ui.mouse, {
+	version: "1.11.2",
+	widgetEventPrefix: "drag",
+	options: {
+		addClasses: true,
+		appendTo: "parent",
+		axis: false,
+		connectToSortable: false,
+		containment: false,
+		cursor: "auto",
+		cursorAt: false,
+		grid: false,
+		handle: false,
+		helper: "original",
+		iframeFix: false,
+		opacity: false,
+		refreshPositions: false,
+		revert: false,
+		revertDuration: 500,
+		scope: "default",
+		scroll: true,
+		scrollSensitivity: 20,
+		scrollSpeed: 20,
+		snap: false,
+		snapMode: "both",
+		snapTolerance: 20,
+		stack: false,
+		zIndex: false,
+
+		// callbacks
+		drag: null,
+		start: null,
+		stop: null
+	},
+	_create: function() {
+
+		if ( this.options.helper === "original" ) {
+			this._setPositionRelative();
+		}
+		if (this.options.addClasses){
+			this.element.addClass("ui-draggable");
+		}
+		if (this.options.disabled){
+			this.element.addClass("ui-draggable-disabled");
+		}
+		this._setHandleClassName();
+
+		this._mouseInit();
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+		if ( key === "handle" ) {
+			this._removeHandleClassName();
+			this._setHandleClassName();
+		}
+	},
+
+	_destroy: function() {
+		if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) {
+			this.destroyOnClear = true;
+			return;
+		}
+		this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" );
+		this._removeHandleClassName();
+		this._mouseDestroy();
+	},
+
+	_mouseCapture: function(event) {
+		var o = this.options;
+
+		this._blurActiveElement( event );
+
+		// among others, prevent a drag on a resizable-handle
+		if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) {
+			return false;
+		}
+
+		//Quit if we're not on a valid handle
+		this.handle = this._getHandle(event);
+		if (!this.handle) {
+			return false;
+		}
+
+		this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix );
+
+		return true;
+
+	},
+
+	_blockFrames: function( selector ) {
+		this.iframeBlocks = this.document.find( selector ).map(function() {
+			var iframe = $( this );
+
+			return $( "<div>" )
+				.css( "position", "absolute" )
+				.appendTo( iframe.parent() )
+				.outerWidth( iframe.outerWidth() )
+				.outerHeight( iframe.outerHeight() )
+				.offset( iframe.offset() )[ 0 ];
+		});
+	},
+
+	_unblockFrames: function() {
+		if ( this.iframeBlocks ) {
+			this.iframeBlocks.remove();
+			delete this.iframeBlocks;
+		}
+	},
+
+	_blurActiveElement: function( event ) {
+		var document = this.document[ 0 ];
+
+		// Only need to blur if the event occurred on the draggable itself, see #10527
+		if ( !this.handleElement.is( event.target ) ) {
+			return;
+		}
+
+		// support: IE9
+		// IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe>
+		try {
+
+			// Support: IE9, IE10
+			// If the <body> is blurred, IE will switch windows, see #9520
+			if ( document.activeElement && document.activeElement.nodeName.toLowerCase() !== "body" ) {
+
+				// Blur any element that currently has focus, see #4261
+				$( document.activeElement ).blur();
+			}
+		} catch ( error ) {}
+	},
+
+	_mouseStart: function(event) {
+
+		var o = this.options;
+
+		//Create and append the visible helper
+		this.helper = this._createHelper(event);
+
+		this.helper.addClass("ui-draggable-dragging");
+
+		//Cache the helper size
+		this._cacheHelperProportions();
+
+		//If ddmanager is used for droppables, set the global draggable
+		if ($.ui.ddmanager) {
+			$.ui.ddmanager.current = this;
+		}
+
+		/*
+		 * - Position generation -
+		 * This block generates everything position related - it's the core of draggables.
+		 */
+
+		//Cache the margins of the original element
+		this._cacheMargins();
+
+		//Store the helper's css position
+		this.cssPosition = this.helper.css( "position" );
+		this.scrollParent = this.helper.scrollParent( true );
+		this.offsetParent = this.helper.offsetParent();
+		this.hasFixedAncestor = this.helper.parents().filter(function() {
+				return $( this ).css( "position" ) === "fixed";
+			}).length > 0;
+
+		//The element's absolute position on the page minus margins
+		this.positionAbs = this.element.offset();
+		this._refreshOffsets( event );
+
+		//Generate the original position
+		this.originalPosition = this.position = this._generatePosition( event, false );
+		this.originalPageX = event.pageX;
+		this.originalPageY = event.pageY;
+
+		//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
+		(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+		//Set a containment if given in the options
+		this._setContainment();
+
+		//Trigger event + callbacks
+		if (this._trigger("start", event) === false) {
+			this._clear();
+			return false;
+		}
+
+		//Recache the helper size
+		this._cacheHelperProportions();
+
+		//Prepare the droppable offsets
+		if ($.ui.ddmanager && !o.dropBehaviour) {
+			$.ui.ddmanager.prepareOffsets(this, event);
+		}
+
+		// Reset helper's right/bottom css if they're set and set explicit width/height instead
+		// as this prevents resizing of elements with right/bottom set (see #7772)
+		this._normalizeRightBottom();
+
+		this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+
+		//If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003)
+		if ( $.ui.ddmanager ) {
+			$.ui.ddmanager.dragStart(this, event);
+		}
+
+		return true;
+	},
+
+	_refreshOffsets: function( event ) {
+		this.offset = {
+			top: this.positionAbs.top - this.margins.top,
+			left: this.positionAbs.left - this.margins.left,
+			scroll: false,
+			parent: this._getParentOffset(),
+			relative: this._getRelativeOffset()
+		};
+
+		this.offset.click = {
+			left: event.pageX - this.offset.left,
+			top: event.pageY - this.offset.top
+		};
+	},
+
+	_mouseDrag: function(event, noPropagation) {
+		// reset any necessary cached properties (see #5009)
+		if ( this.hasFixedAncestor ) {
+			this.offset.parent = this._getParentOffset();
+		}
+
+		//Compute the helpers position
+		this.position = this._generatePosition( event, true );
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		//Call plugins and callbacks and use the resulting position if something is returned
+		if (!noPropagation) {
+			var ui = this._uiHash();
+			if (this._trigger("drag", event, ui) === false) {
+				this._mouseUp({});
+				return false;
+			}
+			this.position = ui.position;
+		}
+
+		this.helper[ 0 ].style.left = this.position.left + "px";
+		this.helper[ 0 ].style.top = this.position.top + "px";
+
+		if ($.ui.ddmanager) {
+			$.ui.ddmanager.drag(this, event);
+		}
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+
+		//If we are using droppables, inform the manager about the drop
+		var that = this,
+			dropped = false;
+		if ($.ui.ddmanager && !this.options.dropBehaviour) {
+			dropped = $.ui.ddmanager.drop(this, event);
+		}
+
+		//if a drop comes from outside (a sortable)
+		if (this.dropped) {
+			dropped = this.dropped;
+			this.dropped = false;
+		}
+
+		if ((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) {
+			$(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() {
+				if (that._trigger("stop", event) !== false) {
+					that._clear();
+				}
+			});
+		} else {
+			if (this._trigger("stop", event) !== false) {
+				this._clear();
+			}
+		}
+
+		return false;
+	},
+
+	_mouseUp: function( event ) {
+		this._unblockFrames();
+
+		//If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003)
+		if ( $.ui.ddmanager ) {
+			$.ui.ddmanager.dragStop(this, event);
+		}
+
+		// Only need to focus if the event occurred on the draggable itself, see #10527
+		if ( this.handleElement.is( event.target ) ) {
+			// The interaction is over; whether or not the click resulted in a drag, focus the element
+			this.element.focus();
+		}
+
+		return $.ui.mouse.prototype._mouseUp.call(this, event);
+	},
+
+	cancel: function() {
+
+		if (this.helper.is(".ui-draggable-dragging")) {
+			this._mouseUp({});
+		} else {
+			this._clear();
+		}
+
+		return this;
+
+	},
+
+	_getHandle: function(event) {
+		return this.options.handle ?
+			!!$( event.target ).closest( this.element.find( this.options.handle ) ).length :
+			true;
+	},
+
+	_setHandleClassName: function() {
+		this.handleElement = this.options.handle ?
+			this.element.find( this.options.handle ) : this.element;
+		this.handleElement.addClass( "ui-draggable-handle" );
+	},
+
+	_removeHandleClassName: function() {
+		this.handleElement.removeClass( "ui-draggable-handle" );
+	},
+
+	_createHelper: function(event) {
+
+		var o = this.options,
+			helperIsFunction = $.isFunction( o.helper ),
+			helper = helperIsFunction ?
+				$( o.helper.apply( this.element[ 0 ], [ event ] ) ) :
+				( o.helper === "clone" ?
+					this.element.clone().removeAttr( "id" ) :
+					this.element );
+
+		if (!helper.parents("body").length) {
+			helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo));
+		}
+
+		// http://bugs.jqueryui.com/ticket/9446
+		// a helper function can return the original element
+		// which wouldn't have been set to relative in _create
+		if ( helperIsFunction && helper[ 0 ] === this.element[ 0 ] ) {
+			this._setPositionRelative();
+		}
+
+		if (helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) {
+			helper.css("position", "absolute");
+		}
+
+		return helper;
+
+	},
+
+	_setPositionRelative: function() {
+		if ( !( /^(?:r|a|f)/ ).test( this.element.css( "position" ) ) ) {
+			this.element[ 0 ].style.position = "relative";
+		}
+	},
+
+	_adjustOffsetFromHelper: function(obj) {
+		if (typeof obj === "string") {
+			obj = obj.split(" ");
+		}
+		if ($.isArray(obj)) {
+			obj = { left: +obj[0], top: +obj[1] || 0 };
+		}
+		if ("left" in obj) {
+			this.offset.click.left = obj.left + this.margins.left;
+		}
+		if ("right" in obj) {
+			this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+		}
+		if ("top" in obj) {
+			this.offset.click.top = obj.top + this.margins.top;
+		}
+		if ("bottom" in obj) {
+			this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+		}
+	},
+
+	_isRootNode: function( element ) {
+		return ( /(html|body)/i ).test( element.tagName ) || element === this.document[ 0 ];
+	},
+
+	_getParentOffset: function() {
+
+		//Get the offsetParent and cache its position
+		var po = this.offsetParent.offset(),
+			document = this.document[ 0 ];
+
+		// This is a special case where we need to modify a offset calculated on start, since the following happened:
+		// 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+		// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+		//    the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+		if (this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) {
+			po.left += this.scrollParent.scrollLeft();
+			po.top += this.scrollParent.scrollTop();
+		}
+
+		if ( this._isRootNode( this.offsetParent[ 0 ] ) ) {
+			po = { top: 0, left: 0 };
+		}
+
+		return {
+			top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"), 10) || 0),
+			left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"), 10) || 0)
+		};
+
+	},
+
+	_getRelativeOffset: function() {
+		if ( this.cssPosition !== "relative" ) {
+			return { top: 0, left: 0 };
+		}
+
+		var p = this.element.position(),
+			scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] );
+
+		return {
+			top: p.top - ( parseInt(this.helper.css( "top" ), 10) || 0 ) + ( !scrollIsRootNode ? this.scrollParent.scrollTop() : 0 ),
+			left: p.left - ( parseInt(this.helper.css( "left" ), 10) || 0 ) + ( !scrollIsRootNode ? this.scrollParent.scrollLeft() : 0 )
+		};
+
+	},
+
+	_cacheMargins: function() {
+		this.margins = {
+			left: (parseInt(this.element.css("marginLeft"), 10) || 0),
+			top: (parseInt(this.element.css("marginTop"), 10) || 0),
+			right: (parseInt(this.element.css("marginRight"), 10) || 0),
+			bottom: (parseInt(this.element.css("marginBottom"), 10) || 0)
+		};
+	},
+
+	_cacheHelperProportions: function() {
+		this.helperProportions = {
+			width: this.helper.outerWidth(),
+			height: this.helper.outerHeight()
+		};
+	},
+
+	_setContainment: function() {
+
+		var isUserScrollable, c, ce,
+			o = this.options,
+			document = this.document[ 0 ];
+
+		this.relativeContainer = null;
+
+		if ( !o.containment ) {
+			this.containment = null;
+			return;
+		}
+
+		if ( o.containment === "window" ) {
+			this.containment = [
+				$( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left,
+				$( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top,
+				$( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left,
+				$( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top
+			];
+			return;
+		}
+
+		if ( o.containment === "document") {
+			this.containment = [
+				0,
+				0,
+				$( document ).width() - this.helperProportions.width - this.margins.left,
+				( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top
+			];
+			return;
+		}
+
+		if ( o.containment.constructor === Array ) {
+			this.containment = o.containment;
+			return;
+		}
+
+		if ( o.containment === "parent" ) {
+			o.containment = this.helper[ 0 ].parentNode;
+		}
+
+		c = $( o.containment );
+		ce = c[ 0 ];
+
+		if ( !ce ) {
+			return;
+		}
+
+		isUserScrollable = /(scroll|auto)/.test( c.css( "overflow" ) );
+
+		this.containment = [
+			( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ),
+			( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ),
+			( isUserScrollable ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) -
+				( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) -
+				( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) -
+				this.helperProportions.width -
+				this.margins.left -
+				this.margins.right,
+			( isUserScrollable ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) -
+				( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) -
+				( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) -
+				this.helperProportions.height -
+				this.margins.top -
+				this.margins.bottom
+		];
+		this.relativeContainer = c;
+	},
+
+	_convertPositionTo: function(d, pos) {
+
+		if (!pos) {
+			pos = this.position;
+		}
+
+		var mod = d === "absolute" ? 1 : -1,
+			scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] );
+
+		return {
+			top: (
+				pos.top	+																// The absolute mouse position
+				this.offset.relative.top * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top * mod -										// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.offset.scroll.top : ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) * mod)
+			),
+			left: (
+				pos.left +																// The absolute mouse position
+				this.offset.relative.left * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left * mod	-										// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.offset.scroll.left : ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) * mod)
+			)
+		};
+
+	},
+
+	_generatePosition: function( event, constrainPosition ) {
+
+		var containment, co, top, left,
+			o = this.options,
+			scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ),
+			pageX = event.pageX,
+			pageY = event.pageY;
+
+		// Cache the scroll
+		if ( !scrollIsRootNode || !this.offset.scroll ) {
+			this.offset.scroll = {
+				top: this.scrollParent.scrollTop(),
+				left: this.scrollParent.scrollLeft()
+			};
+		}
+
+		/*
+		 * - Position constraining -
+		 * Constrain the position to a mix of grid, containment.
+		 */
+
+		// If we are not dragging yet, we won't check for options
+		if ( constrainPosition ) {
+			if ( this.containment ) {
+				if ( this.relativeContainer ){
+					co = this.relativeContainer.offset();
+					containment = [
+						this.containment[ 0 ] + co.left,
+						this.containment[ 1 ] + co.top,
+						this.containment[ 2 ] + co.left,
+						this.containment[ 3 ] + co.top
+					];
+				} else {
+					containment = this.containment;
+				}
+
+				if (event.pageX - this.offset.click.left < containment[0]) {
+					pageX = containment[0] + this.offset.click.left;
+				}
+				if (event.pageY - this.offset.click.top < containment[1]) {
+					pageY = containment[1] + this.offset.click.top;
+				}
+				if (event.pageX - this.offset.click.left > containment[2]) {
+					pageX = containment[2] + this.offset.click.left;
+				}
+				if (event.pageY - this.offset.click.top > containment[3]) {
+					pageY = containment[3] + this.offset.click.top;
+				}
+			}
+
+			if (o.grid) {
+				//Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950)
+				top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY;
+				pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+				left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX;
+				pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+			}
+
+			if ( o.axis === "y" ) {
+				pageX = this.originalPageX;
+			}
+
+			if ( o.axis === "x" ) {
+				pageY = this.originalPageY;
+			}
+		}
+
+		return {
+			top: (
+				pageY -																	// The absolute mouse position
+				this.offset.click.top	-												// Click offset (relative to the element)
+				this.offset.relative.top -												// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top +												// The offsetParent's offset without borders (offset + border)
+				( this.cssPosition === "fixed" ? -this.offset.scroll.top : ( scrollIsRootNode ? 0 : this.offset.scroll.top ) )
+			),
+			left: (
+				pageX -																	// The absolute mouse position
+				this.offset.click.left -												// Click offset (relative to the element)
+				this.offset.relative.left -												// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left +												// The offsetParent's offset without borders (offset + border)
+				( this.cssPosition === "fixed" ? -this.offset.scroll.left : ( scrollIsRootNode ? 0 : this.offset.scroll.left ) )
+			)
+		};
+
+	},
+
+	_clear: function() {
+		this.helper.removeClass("ui-draggable-dragging");
+		if (this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) {
+			this.helper.remove();
+		}
+		this.helper = null;
+		this.cancelHelperRemoval = false;
+		if ( this.destroyOnClear ) {
+			this.destroy();
+		}
+	},
+
+	_normalizeRightBottom: function() {
+		if ( this.options.axis !== "y" && this.helper.css( "right" ) !== "auto" ) {
+			this.helper.width( this.helper.width() );
+			this.helper.css( "right", "auto" );
+		}
+		if ( this.options.axis !== "x" && this.helper.css( "bottom" ) !== "auto" ) {
+			this.helper.height( this.helper.height() );
+			this.helper.css( "bottom", "auto" );
+		}
+	},
+
+	// From now on bulk stuff - mainly helpers
+
+	_trigger: function( type, event, ui ) {
+		ui = ui || this._uiHash();
+		$.ui.plugin.call( this, type, [ event, ui, this ], true );
+
+		// Absolute position and offset (see #6884 ) have to be recalculated after plugins
+		if ( /^(drag|start|stop)/.test( type ) ) {
+			this.positionAbs = this._convertPositionTo( "absolute" );
+			ui.offset = this.positionAbs;
+		}
+		return $.Widget.prototype._trigger.call( this, type, event, ui );
+	},
+
+	plugins: {},
+
+	_uiHash: function() {
+		return {
+			helper: this.helper,
+			position: this.position,
+			originalPosition: this.originalPosition,
+			offset: this.positionAbs
+		};
+	}
+
+});
+
+$.ui.plugin.add( "draggable", "connectToSortable", {
+	start: function( event, ui, draggable ) {
+		var uiSortable = $.extend( {}, ui, {
+			item: draggable.element
+		});
+
+		draggable.sortables = [];
+		$( draggable.options.connectToSortable ).each(function() {
+			var sortable = $( this ).sortable( "instance" );
+
+			if ( sortable && !sortable.options.disabled ) {
+				draggable.sortables.push( sortable );
+
+				// refreshPositions is called at drag start to refresh the containerCache
+				// which is used in drag. This ensures it's initialized and synchronized
+				// with any changes that might have happened on the page since initialization.
+				sortable.refreshPositions();
+				sortable._trigger("activate", event, uiSortable);
+			}
+		});
+	},
+	stop: function( event, ui, draggable ) {
+		var uiSortable = $.extend( {}, ui, {
+			item: draggable.element
+		});
+
+		draggable.cancelHelperRemoval = false;
+
+		$.each( draggable.sortables, function() {
+			var sortable = this;
+
+			if ( sortable.isOver ) {
+				sortable.isOver = 0;
+
+				// Allow this sortable to handle removing the helper
+				draggable.cancelHelperRemoval = true;
+				sortable.cancelHelperRemoval = false;
+
+				// Use _storedCSS To restore properties in the sortable,
+				// as this also handles revert (#9675) since the draggable
+				// may have modified them in unexpected ways (#8809)
+				sortable._storedCSS = {
+					position: sortable.placeholder.css( "position" ),
+					top: sortable.placeholder.css( "top" ),
+					left: sortable.placeholder.css( "left" )
+				};
+
+				sortable._mouseStop(event);
+
+				// Once drag has ended, the sortable should return to using
+				// its original helper, not the shared helper from draggable
+				sortable.options.helper = sortable.options._helper;
+			} else {
+				// Prevent this Sortable from removing the helper.
+				// However, don't set the draggable to remove the helper
+				// either as another connected Sortable may yet handle the removal.
+				sortable.cancelHelperRemoval = true;
+
+				sortable._trigger( "deactivate", event, uiSortable );
+			}
+		});
+	},
+	drag: function( event, ui, draggable ) {
+		$.each( draggable.sortables, function() {
+			var innermostIntersecting = false,
+				sortable = this;
+
+			// Copy over variables that sortable's _intersectsWith uses
+			sortable.positionAbs = draggable.positionAbs;
+			sortable.helperProportions = draggable.helperProportions;
+			sortable.offset.click = draggable.offset.click;
+
+			if ( sortable._intersectsWith( sortable.containerCache ) ) {
+				innermostIntersecting = true;
+
+				$.each( draggable.sortables, function() {
+					// Copy over variables that sortable's _intersectsWith uses
+					this.positionAbs = draggable.positionAbs;
+					this.helperProportions = draggable.helperProportions;
+					this.offset.click = draggable.offset.click;
+
+					if ( this !== sortable &&
+							this._intersectsWith( this.containerCache ) &&
+							$.contains( sortable.element[ 0 ], this.element[ 0 ] ) ) {
+						innermostIntersecting = false;
+					}
+
+					return innermostIntersecting;
+				});
+			}
+
+			if ( innermostIntersecting ) {
+				// If it intersects, we use a little isOver variable and set it once,
+				// so that the move-in stuff gets fired only once.
+				if ( !sortable.isOver ) {
+					sortable.isOver = 1;
+
+					sortable.currentItem = ui.helper
+						.appendTo( sortable.element )
+						.data( "ui-sortable-item", true );
+
+					// Store helper option to later restore it
+					sortable.options._helper = sortable.options.helper;
+
+					sortable.options.helper = function() {
+						return ui.helper[ 0 ];
+					};
+
+					// Fire the start events of the sortable with our passed browser event,
+					// and our own helper (so it doesn't create a new one)
+					event.target = sortable.currentItem[ 0 ];
+					sortable._mouseCapture( event, true );
+					sortable._mouseStart( event, true, true );
+
+					// Because the browser event is way off the new appended portlet,
+					// modify necessary variables to reflect the changes
+					sortable.offset.click.top = draggable.offset.click.top;
+					sortable.offset.click.left = draggable.offset.click.left;
+					sortable.offset.parent.left -= draggable.offset.parent.left -
+						sortable.offset.parent.left;
+					sortable.offset.parent.top -= draggable.offset.parent.top -
+						sortable.offset.parent.top;
+
+					draggable._trigger( "toSortable", event );
+
+					// Inform draggable that the helper is in a valid drop zone,
+					// used solely in the revert option to handle "valid/invalid".
+					draggable.dropped = sortable.element;
+
+					// Need to refreshPositions of all sortables in the case that
+					// adding to one sortable changes the location of the other sortables (#9675)
+					$.each( draggable.sortables, function() {
+						this.refreshPositions();
+					});
+
+					// hack so receive/update callbacks work (mostly)
+					draggable.currentItem = draggable.element;
+					sortable.fromOutside = draggable;
+				}
+
+				if ( sortable.currentItem ) {
+					sortable._mouseDrag( event );
+					// Copy the sortable's position because the draggable's can potentially reflect
+					// a relative position, while sortable is always absolute, which the dragged
+					// element has now become. (#8809)
+					ui.position = sortable.position;
+				}
+			} else {
+				// If it doesn't intersect with the sortable, and it intersected before,
+				// we fake the drag stop of the sortable, but make sure it doesn't remove
+				// the helper by using cancelHelperRemoval.
+				if ( sortable.isOver ) {
+
+					sortable.isOver = 0;
+					sortable.cancelHelperRemoval = true;
+
+					// Calling sortable's mouseStop would trigger a revert,
+					// so revert must be temporarily false until after mouseStop is called.
+					sortable.options._revert = sortable.options.revert;
+					sortable.options.revert = false;
+
+					sortable._trigger( "out", event, sortable._uiHash( sortable ) );
+					sortable._mouseStop( event, true );
+
+					// restore sortable behaviors that were modfied
+					// when the draggable entered the sortable area (#9481)
+					sortable.options.revert = sortable.options._revert;
+					sortable.options.helper = sortable.options._helper;
+
+					if ( sortable.placeholder ) {
+						sortable.placeholder.remove();
+					}
+
+					// Recalculate the draggable's offset considering the sortable
+					// may have modified them in unexpected ways (#8809)
+					draggable._refreshOffsets( event );
+					ui.position = draggable._generatePosition( event, true );
+
+					draggable._trigger( "fromSortable", event );
+
+					// Inform draggable that the helper is no longer in a valid drop zone
+					draggable.dropped = false;
+
+					// Need to refreshPositions of all sortables just in case removing
+					// from one sortable changes the location of other sortables (#9675)
+					$.each( draggable.sortables, function() {
+						this.refreshPositions();
+					});
+				}
+			}
+		});
+	}
+});
+
+$.ui.plugin.add("draggable", "cursor", {
+	start: function( event, ui, instance ) {
+		var t = $( "body" ),
+			o = instance.options;
+
+		if (t.css("cursor")) {
+			o._cursor = t.css("cursor");
+		}
+		t.css("cursor", o.cursor);
+	},
+	stop: function( event, ui, instance ) {
+		var o = instance.options;
+		if (o._cursor) {
+			$("body").css("cursor", o._cursor);
+		}
+	}
+});
+
+$.ui.plugin.add("draggable", "opacity", {
+	start: function( event, ui, instance ) {
+		var t = $( ui.helper ),
+			o = instance.options;
+		if (t.css("opacity")) {
+			o._opacity = t.css("opacity");
+		}
+		t.css("opacity", o.opacity);
+	},
+	stop: function( event, ui, instance ) {
+		var o = instance.options;
+		if (o._opacity) {
+			$(ui.helper).css("opacity", o._opacity);
+		}
+	}
+});
+
+$.ui.plugin.add("draggable", "scroll", {
+	start: function( event, ui, i ) {
+		if ( !i.scrollParentNotHidden ) {
+			i.scrollParentNotHidden = i.helper.scrollParent( false );
+		}
+
+		if ( i.scrollParentNotHidden[ 0 ] !== i.document[ 0 ] && i.scrollParentNotHidden[ 0 ].tagName !== "HTML" ) {
+			i.overflowOffset = i.scrollParentNotHidden.offset();
+		}
+	},
+	drag: function( event, ui, i  ) {
+
+		var o = i.options,
+			scrolled = false,
+			scrollParent = i.scrollParentNotHidden[ 0 ],
+			document = i.document[ 0 ];
+
+		if ( scrollParent !== document && scrollParent.tagName !== "HTML" ) {
+			if ( !o.axis || o.axis !== "x" ) {
+				if ( ( i.overflowOffset.top + scrollParent.offsetHeight ) - event.pageY < o.scrollSensitivity ) {
+					scrollParent.scrollTop = scrolled = scrollParent.scrollTop + o.scrollSpeed;
+				} else if ( event.pageY - i.overflowOffset.top < o.scrollSensitivity ) {
+					scrollParent.scrollTop = scrolled = scrollParent.scrollTop - o.scrollSpeed;
+				}
+			}
+
+			if ( !o.axis || o.axis !== "y" ) {
+				if ( ( i.overflowOffset.left + scrollParent.offsetWidth ) - event.pageX < o.scrollSensitivity ) {
+					scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft + o.scrollSpeed;
+				} else if ( event.pageX - i.overflowOffset.left < o.scrollSensitivity ) {
+					scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft - o.scrollSpeed;
+				}
+			}
+
+		} else {
+
+			if (!o.axis || o.axis !== "x") {
+				if (event.pageY - $(document).scrollTop() < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+				} else if ($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+				}
+			}
+
+			if (!o.axis || o.axis !== "y") {
+				if (event.pageX - $(document).scrollLeft() < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+				} else if ($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+				}
+			}
+
+		}
+
+		if (scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) {
+			$.ui.ddmanager.prepareOffsets(i, event);
+		}
+
+	}
+});
+
+$.ui.plugin.add("draggable", "snap", {
+	start: function( event, ui, i ) {
+
+		var o = i.options;
+
+		i.snapElements = [];
+
+		$(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() {
+			var $t = $(this),
+				$o = $t.offset();
+			if (this !== i.element[0]) {
+				i.snapElements.push({
+					item: this,
+					width: $t.outerWidth(), height: $t.outerHeight(),
+					top: $o.top, left: $o.left
+				});
+			}
+		});
+
+	},
+	drag: function( event, ui, inst ) {
+
+		var ts, bs, ls, rs, l, r, t, b, i, first,
+			o = inst.options,
+			d = o.snapTolerance,
+			x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width,
+			y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height;
+
+		for (i = inst.snapElements.length - 1; i >= 0; i--){
+
+			l = inst.snapElements[i].left - inst.margins.left;
+			r = l + inst.snapElements[i].width;
+			t = inst.snapElements[i].top - inst.margins.top;
+			b = t + inst.snapElements[i].height;
+
+			if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) {
+				if (inst.snapElements[i].snapping) {
+					(inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item })));
+				}
+				inst.snapElements[i].snapping = false;
+				continue;
+			}
+
+			if (o.snapMode !== "inner") {
+				ts = Math.abs(t - y2) <= d;
+				bs = Math.abs(b - y1) <= d;
+				ls = Math.abs(l - x2) <= d;
+				rs = Math.abs(r - x1) <= d;
+				if (ts) {
+					ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top;
+				}
+				if (bs) {
+					ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top;
+				}
+				if (ls) {
+					ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left;
+				}
+				if (rs) {
+					ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left;
+				}
+			}
+
+			first = (ts || bs || ls || rs);
+
+			if (o.snapMode !== "outer") {
+				ts = Math.abs(t - y1) <= d;
+				bs = Math.abs(b - y2) <= d;
+				ls = Math.abs(l - x1) <= d;
+				rs = Math.abs(r - x2) <= d;
+				if (ts) {
+					ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top;
+				}
+				if (bs) {
+					ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top;
+				}
+				if (ls) {
+					ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left;
+				}
+				if (rs) {
+					ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left;
+				}
+			}
+
+			if (!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) {
+				(inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item })));
+			}
+			inst.snapElements[i].snapping = (ts || bs || ls || rs || first);
+
+		}
+
+	}
+});
+
+$.ui.plugin.add("draggable", "stack", {
+	start: function( event, ui, instance ) {
+		var min,
+			o = instance.options,
+			group = $.makeArray($(o.stack)).sort(function(a, b) {
+				return (parseInt($(a).css("zIndex"), 10) || 0) - (parseInt($(b).css("zIndex"), 10) || 0);
+			});
+
+		if (!group.length) { return; }
+
+		min = parseInt($(group[0]).css("zIndex"), 10) || 0;
+		$(group).each(function(i) {
+			$(this).css("zIndex", min + i);
+		});
+		this.css("zIndex", (min + group.length));
+	}
+});
+
+$.ui.plugin.add("draggable", "zIndex", {
+	start: function( event, ui, instance ) {
+		var t = $( ui.helper ),
+			o = instance.options;
+
+		if (t.css("zIndex")) {
+			o._zIndex = t.css("zIndex");
+		}
+		t.css("zIndex", o.zIndex);
+	},
+	stop: function( event, ui, instance ) {
+		var o = instance.options;
+
+		if (o._zIndex) {
+			$(ui.helper).css("zIndex", o._zIndex);
+		}
+	}
+});
+
+var draggable = $.ui.draggable;
+
+
+/*!
+ * jQuery UI Droppable 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/droppable/
+ */
+
+
+$.widget( "ui.droppable", {
+	version: "1.11.2",
+	widgetEventPrefix: "drop",
+	options: {
+		accept: "*",
+		activeClass: false,
+		addClasses: true,
+		greedy: false,
+		hoverClass: false,
+		scope: "default",
+		tolerance: "intersect",
+
+		// callbacks
+		activate: null,
+		deactivate: null,
+		drop: null,
+		out: null,
+		over: null
+	},
+	_create: function() {
+
+		var proportions,
+			o = this.options,
+			accept = o.accept;
+
+		this.isover = false;
+		this.isout = true;
+
+		this.accept = $.isFunction( accept ) ? accept : function( d ) {
+			return d.is( accept );
+		};
+
+		this.proportions = function( /* valueToWrite */ ) {
+			if ( arguments.length ) {
+				// Store the droppable's proportions
+				proportions = arguments[ 0 ];
+			} else {
+				// Retrieve or derive the droppable's proportions
+				return proportions ?
+					proportions :
+					proportions = {
+						width: this.element[ 0 ].offsetWidth,
+						height: this.element[ 0 ].offsetHeight
+					};
+			}
+		};
+
+		this._addToManager( o.scope );
+
+		o.addClasses && this.element.addClass( "ui-droppable" );
+
+	},
+
+	_addToManager: function( scope ) {
+		// Add the reference and positions to the manager
+		$.ui.ddmanager.droppables[ scope ] = $.ui.ddmanager.droppables[ scope ] || [];
+		$.ui.ddmanager.droppables[ scope ].push( this );
+	},
+
+	_splice: function( drop ) {
+		var i = 0;
+		for ( ; i < drop.length; i++ ) {
+			if ( drop[ i ] === this ) {
+				drop.splice( i, 1 );
+			}
+		}
+	},
+
+	_destroy: function() {
+		var drop = $.ui.ddmanager.droppables[ this.options.scope ];
+
+		this._splice( drop );
+
+		this.element.removeClass( "ui-droppable ui-droppable-disabled" );
+	},
+
+	_setOption: function( key, value ) {
+
+		if ( key === "accept" ) {
+			this.accept = $.isFunction( value ) ? value : function( d ) {
+				return d.is( value );
+			};
+		} else if ( key === "scope" ) {
+			var drop = $.ui.ddmanager.droppables[ this.options.scope ];
+
+			this._splice( drop );
+			this._addToManager( value );
+		}
+
+		this._super( key, value );
+	},
+
+	_activate: function( event ) {
+		var draggable = $.ui.ddmanager.current;
+		if ( this.options.activeClass ) {
+			this.element.addClass( this.options.activeClass );
+		}
+		if ( draggable ){
+			this._trigger( "activate", event, this.ui( draggable ) );
+		}
+	},
+
+	_deactivate: function( event ) {
+		var draggable = $.ui.ddmanager.current;
+		if ( this.options.activeClass ) {
+			this.element.removeClass( this.options.activeClass );
+		}
+		if ( draggable ){
+			this._trigger( "deactivate", event, this.ui( draggable ) );
+		}
+	},
+
+	_over: function( event ) {
+
+		var draggable = $.ui.ddmanager.current;
+
+		// Bail if draggable and droppable are same element
+		if ( !draggable || ( draggable.currentItem || draggable.element )[ 0 ] === this.element[ 0 ] ) {
+			return;
+		}
+
+		if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || draggable.element ) ) ) {
+			if ( this.options.hoverClass ) {
+				this.element.addClass( this.options.hoverClass );
+			}
+			this._trigger( "over", event, this.ui( draggable ) );
+		}
+
+	},
+
+	_out: function( event ) {
+
+		var draggable = $.ui.ddmanager.current;
+
+		// Bail if draggable and droppable are same element
+		if ( !draggable || ( draggable.currentItem || draggable.element )[ 0 ] === this.element[ 0 ] ) {
+			return;
+		}
+
+		if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || draggable.element ) ) ) {
+			if ( this.options.hoverClass ) {
+				this.element.removeClass( this.options.hoverClass );
+			}
+			this._trigger( "out", event, this.ui( draggable ) );
+		}
+
+	},
+
+	_drop: function( event, custom ) {
+
+		var draggable = custom || $.ui.ddmanager.current,
+			childrenIntersection = false;
+
+		// Bail if draggable and droppable are same element
+		if ( !draggable || ( draggable.currentItem || draggable.element )[ 0 ] === this.element[ 0 ] ) {
+			return false;
+		}
+
+		this.element.find( ":data(ui-droppable)" ).not( ".ui-draggable-dragging" ).each(function() {
+			var inst = $( this ).droppable( "instance" );
+			if (
+				inst.options.greedy &&
+				!inst.options.disabled &&
+				inst.options.scope === draggable.options.scope &&
+				inst.accept.call( inst.element[ 0 ], ( draggable.currentItem || draggable.element ) ) &&
+				$.ui.intersect( draggable, $.extend( inst, { offset: inst.element.offset() } ), inst.options.tolerance, event )
+			) { childrenIntersection = true; return false; }
+		});
+		if ( childrenIntersection ) {
+			return false;
+		}
+
+		if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || draggable.element ) ) ) {
+			if ( this.options.activeClass ) {
+				this.element.removeClass( this.options.activeClass );
+			}
+			if ( this.options.hoverClass ) {
+				this.element.removeClass( this.options.hoverClass );
+			}
+			this._trigger( "drop", event, this.ui( draggable ) );
+			return this.element;
+		}
+
+		return false;
+
+	},
+
+	ui: function( c ) {
+		return {
+			draggable: ( c.currentItem || c.element ),
+			helper: c.helper,
+			position: c.position,
+			offset: c.positionAbs
+		};
+	}
+
+});
+
+$.ui.intersect = (function() {
+	function isOverAxis( x, reference, size ) {
+		return ( x >= reference ) && ( x < ( reference + size ) );
+	}
+
+	return function( draggable, droppable, toleranceMode, event ) {
+
+		if ( !droppable.offset ) {
+			return false;
+		}
+
+		var x1 = ( draggable.positionAbs || draggable.position.absolute ).left + draggable.margins.left,
+			y1 = ( draggable.positionAbs || draggable.position.absolute ).top + draggable.margins.top,
+			x2 = x1 + draggable.helperProportions.width,
+			y2 = y1 + draggable.helperProportions.height,
+			l = droppable.offset.left,
+			t = droppable.offset.top,
+			r = l + droppable.proportions().width,
+			b = t + droppable.proportions().height;
+
+		switch ( toleranceMode ) {
+		case "fit":
+			return ( l <= x1 && x2 <= r && t <= y1 && y2 <= b );
+		case "intersect":
+			return ( l < x1 + ( draggable.helperProportions.width / 2 ) && // Right Half
+				x2 - ( draggable.helperProportions.width / 2 ) < r && // Left Half
+				t < y1 + ( draggable.helperProportions.height / 2 ) && // Bottom Half
+				y2 - ( draggable.helperProportions.height / 2 ) < b ); // Top Half
+		case "pointer":
+			return isOverAxis( event.pageY, t, droppable.proportions().height ) && isOverAxis( event.pageX, l, droppable.proportions().width );
+		case "touch":
+			return (
+				( y1 >= t && y1 <= b ) || // Top edge touching
+				( y2 >= t && y2 <= b ) || // Bottom edge touching
+				( y1 < t && y2 > b ) // Surrounded vertically
+			) && (
+				( x1 >= l && x1 <= r ) || // Left edge touching
+				( x2 >= l && x2 <= r ) || // Right edge touching
+				( x1 < l && x2 > r ) // Surrounded horizontally
+			);
+		default:
+			return false;
+		}
+	};
+})();
+
+/*
+	This manager tracks offsets of draggables and droppables
+*/
+$.ui.ddmanager = {
+	current: null,
+	droppables: { "default": [] },
+	prepareOffsets: function( t, event ) {
+
+		var i, j,
+			m = $.ui.ddmanager.droppables[ t.options.scope ] || [],
+			type = event ? event.type : null, // workaround for #2317
+			list = ( t.currentItem || t.element ).find( ":data(ui-droppable)" ).addBack();
+
+		droppablesLoop: for ( i = 0; i < m.length; i++ ) {
+
+			// No disabled and non-accepted
+			if ( m[ i ].options.disabled || ( t && !m[ i ].accept.call( m[ i ].element[ 0 ], ( t.currentItem || t.element ) ) ) ) {
+				continue;
+			}
+
+			// Filter out elements in the current dragged item
+			for ( j = 0; j < list.length; j++ ) {
+				if ( list[ j ] === m[ i ].element[ 0 ] ) {
+					m[ i ].proportions().height = 0;
+					continue droppablesLoop;
+				}
+			}
+
+			m[ i ].visible = m[ i ].element.css( "display" ) !== "none";
+			if ( !m[ i ].visible ) {
+				continue;
+			}
+
+			// Activate the droppable if used directly from draggables
+			if ( type === "mousedown" ) {
+				m[ i ]._activate.call( m[ i ], event );
+			}
+
+			m[ i ].offset = m[ i ].element.offset();
+			m[ i ].proportions({ width: m[ i ].element[ 0 ].offsetWidth, height: m[ i ].element[ 0 ].offsetHeight });
+
+		}
+
+	},
+	drop: function( draggable, event ) {
+
+		var dropped = false;
+		// Create a copy of the droppables in case the list changes during the drop (#9116)
+		$.each( ( $.ui.ddmanager.droppables[ draggable.options.scope ] || [] ).slice(), function() {
+
+			if ( !this.options ) {
+				return;
+			}
+			if ( !this.options.disabled && this.visible && $.ui.intersect( draggable, this, this.options.tolerance, event ) ) {
+				dropped = this._drop.call( this, event ) || dropped;
+			}
+
+			if ( !this.options.disabled && this.visible && this.accept.call( this.element[ 0 ], ( draggable.currentItem || draggable.element ) ) ) {
+				this.isout = true;
+				this.isover = false;
+				this._deactivate.call( this, event );
+			}
+
+		});
+		return dropped;
+
+	},
+	dragStart: function( draggable, event ) {
+		// Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003)
+		draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() {
+			if ( !draggable.options.refreshPositions ) {
+				$.ui.ddmanager.prepareOffsets( draggable, event );
+			}
+		});
+	},
+	drag: function( draggable, event ) {
+
+		// If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
+		if ( draggable.options.refreshPositions ) {
+			$.ui.ddmanager.prepareOffsets( draggable, event );
+		}
+
+		// Run through all droppables and check their positions based on specific tolerance options
+		$.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() {
+
+			if ( this.options.disabled || this.greedyChild || !this.visible ) {
+				return;
+			}
+
+			var parentInstance, scope, parent,
+				intersects = $.ui.intersect( draggable, this, this.options.tolerance, event ),
+				c = !intersects && this.isover ? "isout" : ( intersects && !this.isover ? "isover" : null );
+			if ( !c ) {
+				return;
+			}
+
+			if ( this.options.greedy ) {
+				// find droppable parents with same scope
+				scope = this.options.scope;
+				parent = this.element.parents( ":data(ui-droppable)" ).filter(function() {
+					return $( this ).droppable( "instance" ).options.scope === scope;
+				});
+
+				if ( parent.length ) {
+					parentInstance = $( parent[ 0 ] ).droppable( "instance" );
+					parentInstance.greedyChild = ( c === "isover" );
+				}
+			}
+
+			// we just moved into a greedy child
+			if ( parentInstance && c === "isover" ) {
+				parentInstance.isover = false;
+				parentInstance.isout = true;
+				parentInstance._out.call( parentInstance, event );
+			}
+
+			this[ c ] = true;
+			this[c === "isout" ? "isover" : "isout"] = false;
+			this[c === "isover" ? "_over" : "_out"].call( this, event );
+
+			// we just moved out of a greedy child
+			if ( parentInstance && c === "isout" ) {
+				parentInstance.isout = false;
+				parentInstance.isover = true;
+				parentInstance._over.call( parentInstance, event );
+			}
+		});
+
+	},
+	dragStop: function( draggable, event ) {
+		draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" );
+		// Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003)
+		if ( !draggable.options.refreshPositions ) {
+			$.ui.ddmanager.prepareOffsets( draggable, event );
+		}
+	}
+};
+
+var droppable = $.ui.droppable;
+
+
+/*!
+ * jQuery UI Resizable 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/resizable/
+ */
+
+
+$.widget("ui.resizable", $.ui.mouse, {
+	version: "1.11.2",
+	widgetEventPrefix: "resize",
+	options: {
+		alsoResize: false,
+		animate: false,
+		animateDuration: "slow",
+		animateEasing: "swing",
+		aspectRatio: false,
+		autoHide: false,
+		containment: false,
+		ghost: false,
+		grid: false,
+		handles: "e,s,se",
+		helper: false,
+		maxHeight: null,
+		maxWidth: null,
+		minHeight: 10,
+		minWidth: 10,
+		// See #7960
+		zIndex: 90,
+
+		// callbacks
+		resize: null,
+		start: null,
+		stop: null
+	},
+
+	_num: function( value ) {
+		return parseInt( value, 10 ) || 0;
+	},
+
+	_isNumber: function( value ) {
+		return !isNaN( parseInt( value, 10 ) );
+	},
+
+	_hasScroll: function( el, a ) {
+
+		if ( $( el ).css( "overflow" ) === "hidden") {
+			return false;
+		}
+
+		var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
+			has = false;
+
+		if ( el[ scroll ] > 0 ) {
+			return true;
+		}
+
+		// TODO: determine which cases actually cause this to happen
+		// if the element doesn't have the scroll set, see if it's possible to
+		// set the scroll
+		el[ scroll ] = 1;
+		has = ( el[ scroll ] > 0 );
+		el[ scroll ] = 0;
+		return has;
+	},
+
+	_create: function() {
+
+		var n, i, handle, axis, hname,
+			that = this,
+			o = this.options;
+		this.element.addClass("ui-resizable");
+
+		$.extend(this, {
+			_aspectRatio: !!(o.aspectRatio),
+			aspectRatio: o.aspectRatio,
+			originalElement: this.element,
+			_proportionallyResizeElements: [],
+			_helper: o.helper || o.ghost || o.animate ? o.helper || "ui-resizable-helper" : null
+		});
+
+		// Wrap the element if it cannot hold child nodes
+		if (this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)) {
+
+			this.element.wrap(
+				$("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({
+					position: this.element.css("position"),
+					width: this.element.outerWidth(),
+					height: this.element.outerHeight(),
+					top: this.element.css("top"),
+					left: this.element.css("left")
+				})
+			);
+
+			this.element = this.element.parent().data(
+				"ui-resizable", this.element.resizable( "instance" )
+			);
+
+			this.elementIsWrapper = true;
+
+			this.element.css({
+				marginLeft: this.originalElement.css("marginLeft"),
+				marginTop: this.originalElement.css("marginTop"),
+				marginRight: this.originalElement.css("marginRight"),
+				marginBottom: this.originalElement.css("marginBottom")
+			});
+			this.originalElement.css({
+				marginLeft: 0,
+				marginTop: 0,
+				marginRight: 0,
+				marginBottom: 0
+			});
+			// support: Safari
+			// Prevent Safari textarea resize
+			this.originalResizeStyle = this.originalElement.css("resize");
+			this.originalElement.css("resize", "none");
+
+			this._proportionallyResizeElements.push( this.originalElement.css({
+				position: "static",
+				zoom: 1,
+				display: "block"
+			}) );
+
+			// support: IE9
+			// avoid IE jump (hard set the margin)
+			this.originalElement.css({ margin: this.originalElement.css("margin") });
+
+			this._proportionallyResize();
+		}
+
+		this.handles = o.handles ||
+			( !$(".ui-resizable-handle", this.element).length ?
+				"e,s,se" : {
+					n: ".ui-resizable-n",
+					e: ".ui-resizable-e",
+					s: ".ui-resizable-s",
+					w: ".ui-resizable-w",
+					se: ".ui-resizable-se",
+					sw: ".ui-resizable-sw",
+					ne: ".ui-resizable-ne",
+					nw: ".ui-resizable-nw"
+				} );
+
+		if (this.handles.constructor === String) {
+
+			if ( this.handles === "all") {
+				this.handles = "n,e,s,w,se,sw,ne,nw";
+			}
+
+			n = this.handles.split(",");
+			this.handles = {};
+
+			for (i = 0; i < n.length; i++) {
+
+				handle = $.trim(n[i]);
+				hname = "ui-resizable-" + handle;
+				axis = $("<div class='ui-resizable-handle " + hname + "'></div>");
+
+				axis.css({ zIndex: o.zIndex });
+
+				// TODO : What's going on here?
+				if ("se" === handle) {
+					axis.addClass("ui-icon ui-icon-gripsmall-diagonal-se");
+				}
+
+				this.handles[handle] = ".ui-resizable-" + handle;
+				this.element.append(axis);
+			}
+
+		}
+
+		this._renderAxis = function(target) {
+
+			var i, axis, padPos, padWrapper;
+
+			target = target || this.element;
+
+			for (i in this.handles) {
+
+				if (this.handles[i].constructor === String) {
+					this.handles[i] = this.element.children( this.handles[ i ] ).first().show();
+				}
+
+				if (this.elementIsWrapper && this.originalElement[0].nodeName.match(/textarea|input|select|button/i)) {
+
+					axis = $(this.handles[i], this.element);
+
+					padWrapper = /sw|ne|nw|se|n|s/.test(i) ? axis.outerHeight() : axis.outerWidth();
+
+					padPos = [ "padding",
+						/ne|nw|n/.test(i) ? "Top" :
+						/se|sw|s/.test(i) ? "Bottom" :
+						/^e$/.test(i) ? "Right" : "Left" ].join("");
+
+					target.css(padPos, padWrapper);
+
+					this._proportionallyResize();
+
+				}
+
+				// TODO: What's that good for? There's not anything to be executed left
+				if (!$(this.handles[i]).length) {
+					continue;
+				}
+			}
+		};
+
+		// TODO: make renderAxis a prototype function
+		this._renderAxis(this.element);
+
+		this._handles = $(".ui-resizable-handle", this.element)
+			.disableSelection();
+
+		this._handles.mouseover(function() {
+			if (!that.resizing) {
+				if (this.className) {
+					axis = this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);
+				}
+				that.axis = axis && axis[1] ? axis[1] : "se";
+			}
+		});
+
+		if (o.autoHide) {
+			this._handles.hide();
+			$(this.element)
+				.addClass("ui-resizable-autohide")
+				.mouseenter(function() {
+					if (o.disabled) {
+						return;
+					}
+					$(this).removeClass("ui-resizable-autohide");
+					that._handles.show();
+				})
+				.mouseleave(function() {
+					if (o.disabled) {
+						return;
+					}
+					if (!that.resizing) {
+						$(this).addClass("ui-resizable-autohide");
+						that._handles.hide();
+					}
+				});
+		}
+
+		this._mouseInit();
+
+	},
+
+	_destroy: function() {
+
+		this._mouseDestroy();
+
+		var wrapper,
+			_destroy = function(exp) {
+				$(exp)
+					.removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing")
+					.removeData("resizable")
+					.removeData("ui-resizable")
+					.unbind(".resizable")
+					.find(".ui-resizable-handle")
+						.remove();
+			};
+
+		// TODO: Unwrap at same DOM position
+		if (this.elementIsWrapper) {
+			_destroy(this.element);
+			wrapper = this.element;
+			this.originalElement.css({
+				position: wrapper.css("position"),
+				width: wrapper.outerWidth(),
+				height: wrapper.outerHeight(),
+				top: wrapper.css("top"),
+				left: wrapper.css("left")
+			}).insertAfter( wrapper );
+			wrapper.remove();
+		}
+
+		this.originalElement.css("resize", this.originalResizeStyle);
+		_destroy(this.originalElement);
+
+		return this;
+	},
+
+	_mouseCapture: function(event) {
+		var i, handle,
+			capture = false;
+
+		for (i in this.handles) {
+			handle = $(this.handles[i])[0];
+			if (handle === event.target || $.contains(handle, event.target)) {
+				capture = true;
+			}
+		}
+
+		return !this.options.disabled && capture;
+	},
+
+	_mouseStart: function(event) {
+
+		var curleft, curtop, cursor,
+			o = this.options,
+			el = this.element;
+
+		this.resizing = true;
+
+		this._renderProxy();
+
+		curleft = this._num(this.helper.css("left"));
+		curtop = this._num(this.helper.css("top"));
+
+		if (o.containment) {
+			curleft += $(o.containment).scrollLeft() || 0;
+			curtop += $(o.containment).scrollTop() || 0;
+		}
+
+		this.offset = this.helper.offset();
+		this.position = { left: curleft, top: curtop };
+
+		this.size = this._helper ? {
+				width: this.helper.width(),
+				height: this.helper.height()
+			} : {
+				width: el.width(),
+				height: el.height()
+			};
+
+		this.originalSize = this._helper ? {
+				width: el.outerWidth(),
+				height: el.outerHeight()
+			} : {
+				width: el.width(),
+				height: el.height()
+			};
+
+		this.sizeDiff = {
+			width: el.outerWidth() - el.width(),
+			height: el.outerHeight() - el.height()
+		};
+
+		this.originalPosition = { left: curleft, top: curtop };
+		this.originalMousePosition = { left: event.pageX, top: event.pageY };
+
+		this.aspectRatio = (typeof o.aspectRatio === "number") ?
+			o.aspectRatio :
+			((this.originalSize.width / this.originalSize.height) || 1);
+
+		cursor = $(".ui-resizable-" + this.axis).css("cursor");
+		$("body").css("cursor", cursor === "auto" ? this.axis + "-resize" : cursor);
+
+		el.addClass("ui-resizable-resizing");
+		this._propagate("start", event);
+		return true;
+	},
+
+	_mouseDrag: function(event) {
+
+		var data, props,
+			smp = this.originalMousePosition,
+			a = this.axis,
+			dx = (event.pageX - smp.left) || 0,
+			dy = (event.pageY - smp.top) || 0,
+			trigger = this._change[a];
+
+		this._updatePrevProperties();
+
+		if (!trigger) {
+			return false;
+		}
+
+		data = trigger.apply(this, [ event, dx, dy ]);
+
+		this._updateVirtualBoundaries(event.shiftKey);
+		if (this._aspectRatio || event.shiftKey) {
+			data = this._updateRatio(data, event);
+		}
+
+		data = this._respectSize(data, event);
+
+		this._updateCache(data);
+
+		this._propagate("resize", event);
+
+		props = this._applyChanges();
+
+		if ( !this._helper && this._proportionallyResizeElements.length ) {
+			this._proportionallyResize();
+		}
+
+		if ( !$.isEmptyObject( props ) ) {
+			this._updatePrevProperties();
+			this._trigger( "resize", event, this.ui() );
+			this._applyChanges();
+		}
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+
+		this.resizing = false;
+		var pr, ista, soffseth, soffsetw, s, left, top,
+			o = this.options, that = this;
+
+		if (this._helper) {
+
+			pr = this._proportionallyResizeElements;
+			ista = pr.length && (/textarea/i).test(pr[0].nodeName);
+			soffseth = ista && this._hasScroll(pr[0], "left") ? 0 : that.sizeDiff.height;
+			soffsetw = ista ? 0 : that.sizeDiff.width;
+
+			s = {
+				width: (that.helper.width()  - soffsetw),
+				height: (that.helper.height() - soffseth)
+			};
+			left = (parseInt(that.element.css("left"), 10) +
+				(that.position.left - that.originalPosition.left)) || null;
+			top = (parseInt(that.element.css("top"), 10) +
+				(that.position.top - that.originalPosition.top)) || null;
+
+			if (!o.animate) {
+				this.element.css($.extend(s, { top: top, left: left }));
+			}
+
+			that.helper.height(that.size.height);
+			that.helper.width(that.size.width);
+
+			if (this._helper && !o.animate) {
+				this._proportionallyResize();
+			}
+		}
+
+		$("body").css("cursor", "auto");
+
+		this.element.removeClass("ui-resizable-resizing");
+
+		this._propagate("stop", event);
+
+		if (this._helper) {
+			this.helper.remove();
+		}
+
+		return false;
+
+	},
+
+	_updatePrevProperties: function() {
+		this.prevPosition = {
+			top: this.position.top,
+			left: this.position.left
+		};
+		this.prevSize = {
+			width: this.size.width,
+			height: this.size.height
+		};
+	},
+
+	_applyChanges: function() {
+		var props = {};
+
+		if ( this.position.top !== this.prevPosition.top ) {
+			props.top = this.position.top + "px";
+		}
+		if ( this.position.left !== this.prevPosition.left ) {
+			props.left = this.position.left + "px";
+		}
+		if ( this.size.width !== this.prevSize.width ) {
+			props.width = this.size.width + "px";
+		}
+		if ( this.size.height !== this.prevSize.height ) {
+			props.height = this.size.height + "px";
+		}
+
+		this.helper.css( props );
+
+		return props;
+	},
+
+	_updateVirtualBoundaries: function(forceAspectRatio) {
+		var pMinWidth, pMaxWidth, pMinHeight, pMaxHeight, b,
+			o = this.options;
+
+		b = {
+			minWidth: this._isNumber(o.minWidth) ? o.minWidth : 0,
+			maxWidth: this._isNumber(o.maxWidth) ? o.maxWidth : Infinity,
+			minHeight: this._isNumber(o.minHeight) ? o.minHeight : 0,
+			maxHeight: this._isNumber(o.maxHeight) ? o.maxHeight : Infinity
+		};
+
+		if (this._aspectRatio || forceAspectRatio) {
+			pMinWidth = b.minHeight * this.aspectRatio;
+			pMinHeight = b.minWidth / this.aspectRatio;
+			pMaxWidth = b.maxHeight * this.aspectRatio;
+			pMaxHeight = b.maxWidth / this.aspectRatio;
+
+			if (pMinWidth > b.minWidth) {
+				b.minWidth = pMinWidth;
+			}
+			if (pMinHeight > b.minHeight) {
+				b.minHeight = pMinHeight;
+			}
+			if (pMaxWidth < b.maxWidth) {
+				b.maxWidth = pMaxWidth;
+			}
+			if (pMaxHeight < b.maxHeight) {
+				b.maxHeight = pMaxHeight;
+			}
+		}
+		this._vBoundaries = b;
+	},
+
+	_updateCache: function(data) {
+		this.offset = this.helper.offset();
+		if (this._isNumber(data.left)) {
+			this.position.left = data.left;
+		}
+		if (this._isNumber(data.top)) {
+			this.position.top = data.top;
+		}
+		if (this._isNumber(data.height)) {
+			this.size.height = data.height;
+		}
+		if (this._isNumber(data.width)) {
+			this.size.width = data.width;
+		}
+	},
+
+	_updateRatio: function( data ) {
+
+		var cpos = this.position,
+			csize = this.size,
+			a = this.axis;
+
+		if (this._isNumber(data.height)) {
+			data.width = (data.height * this.aspectRatio);
+		} else if (this._isNumber(data.width)) {
+			data.height = (data.width / this.aspectRatio);
+		}
+
+		if (a === "sw") {
+			data.left = cpos.left + (csize.width - data.width);
+			data.top = null;
+		}
+		if (a === "nw") {
+			data.top = cpos.top + (csize.height - data.height);
+			data.left = cpos.left + (csize.width - data.width);
+		}
+
+		return data;
+	},
+
+	_respectSize: function( data ) {
+
+		var o = this._vBoundaries,
+			a = this.axis,
+			ismaxw = this._isNumber(data.width) && o.maxWidth && (o.maxWidth < data.width),
+			ismaxh = this._isNumber(data.height) && o.maxHeight && (o.maxHeight < data.height),
+			isminw = this._isNumber(data.width) && o.minWidth && (o.minWidth > data.width),
+			isminh = this._isNumber(data.height) && o.minHeight && (o.minHeight > data.height),
+			dw = this.originalPosition.left + this.originalSize.width,
+			dh = this.position.top + this.size.height,
+			cw = /sw|nw|w/.test(a), ch = /nw|ne|n/.test(a);
+		if (isminw) {
+			data.width = o.minWidth;
+		}
+		if (isminh) {
+			data.height = o.minHeight;
+		}
+		if (ismaxw) {
+			data.width = o.maxWidth;
+		}
+		if (ismaxh) {
+			data.height = o.maxHeight;
+		}
+
+		if (isminw && cw) {
+			data.left = dw - o.minWidth;
+		}
+		if (ismaxw && cw) {
+			data.left = dw - o.maxWidth;
+		}
+		if (isminh && ch) {
+			data.top = dh - o.minHeight;
+		}
+		if (ismaxh && ch) {
+			data.top = dh - o.maxHeight;
+		}
+
+		// Fixing jump error on top/left - bug #2330
+		if (!data.width && !data.height && !data.left && data.top) {
+			data.top = null;
+		} else if (!data.width && !data.height && !data.top && data.left) {
+			data.left = null;
+		}
+
+		return data;
+	},
+
+	_getPaddingPlusBorderDimensions: function( element ) {
+		var i = 0,
+			widths = [],
+			borders = [
+				element.css( "borderTopWidth" ),
+				element.css( "borderRightWidth" ),
+				element.css( "borderBottomWidth" ),
+				element.css( "borderLeftWidth" )
+			],
+			paddings = [
+				element.css( "paddingTop" ),
+				element.css( "paddingRight" ),
+				element.css( "paddingBottom" ),
+				element.css( "paddingLeft" )
+			];
+
+		for ( ; i < 4; i++ ) {
+			widths[ i ] = ( parseInt( borders[ i ], 10 ) || 0 );
+			widths[ i ] += ( parseInt( paddings[ i ], 10 ) || 0 );
+		}
+
+		return {
+			height: widths[ 0 ] + widths[ 2 ],
+			width: widths[ 1 ] + widths[ 3 ]
+		};
+	},
+
+	_proportionallyResize: function() {
+
+		if (!this._proportionallyResizeElements.length) {
+			return;
+		}
+
+		var prel,
+			i = 0,
+			element = this.helper || this.element;
+
+		for ( ; i < this._proportionallyResizeElements.length; i++) {
+
+			prel = this._proportionallyResizeElements[i];
+
+			// TODO: Seems like a bug to cache this.outerDimensions
+			// considering that we are in a loop.
+			if (!this.outerDimensions) {
+				this.outerDimensions = this._getPaddingPlusBorderDimensions( prel );
+			}
+
+			prel.css({
+				height: (element.height() - this.outerDimensions.height) || 0,
+				width: (element.width() - this.outerDimensions.width) || 0
+			});
+
+		}
+
+	},
+
+	_renderProxy: function() {
+
+		var el = this.element, o = this.options;
+		this.elementOffset = el.offset();
+
+		if (this._helper) {
+
+			this.helper = this.helper || $("<div style='overflow:hidden;'></div>");
+
+			this.helper.addClass(this._helper).css({
+				width: this.element.outerWidth() - 1,
+				height: this.element.outerHeight() - 1,
+				position: "absolute",
+				left: this.elementOffset.left + "px",
+				top: this.elementOffset.top + "px",
+				zIndex: ++o.zIndex //TODO: Don't modify option
+			});
+
+			this.helper
+				.appendTo("body")
+				.disableSelection();
+
+		} else {
+			this.helper = this.element;
+		}
+
+	},
+
+	_change: {
+		e: function(event, dx) {
+			return { width: this.originalSize.width + dx };
+		},
+		w: function(event, dx) {
+			var cs = this.originalSize, sp = this.originalPosition;
+			return { left: sp.left + dx, width: cs.width - dx };
+		},
+		n: function(event, dx, dy) {
+			var cs = this.originalSize, sp = this.originalPosition;
+			return { top: sp.top + dy, height: cs.height - dy };
+		},
+		s: function(event, dx, dy) {
+			return { height: this.originalSize.height + dy };
+		},
+		se: function(event, dx, dy) {
+			return $.extend(this._change.s.apply(this, arguments),
+				this._change.e.apply(this, [ event, dx, dy ]));
+		},
+		sw: function(event, dx, dy) {
+			return $.extend(this._change.s.apply(this, arguments),
+				this._change.w.apply(this, [ event, dx, dy ]));
+		},
+		ne: function(event, dx, dy) {
+			return $.extend(this._change.n.apply(this, arguments),
+				this._change.e.apply(this, [ event, dx, dy ]));
+		},
+		nw: function(event, dx, dy) {
+			return $.extend(this._change.n.apply(this, arguments),
+				this._change.w.apply(this, [ event, dx, dy ]));
+		}
+	},
+
+	_propagate: function(n, event) {
+		$.ui.plugin.call(this, n, [ event, this.ui() ]);
+		(n !== "resize" && this._trigger(n, event, this.ui()));
+	},
+
+	plugins: {},
+
+	ui: function() {
+		return {
+			originalElement: this.originalElement,
+			element: this.element,
+			helper: this.helper,
+			position: this.position,
+			size: this.size,
+			originalSize: this.originalSize,
+			originalPosition: this.originalPosition
+		};
+	}
+
+});
+
+/*
+ * Resizable Extensions
+ */
+
+$.ui.plugin.add("resizable", "animate", {
+
+	stop: function( event ) {
+		var that = $(this).resizable( "instance" ),
+			o = that.options,
+			pr = that._proportionallyResizeElements,
+			ista = pr.length && (/textarea/i).test(pr[0].nodeName),
+			soffseth = ista && that._hasScroll(pr[0], "left") ? 0 : that.sizeDiff.height,
+			soffsetw = ista ? 0 : that.sizeDiff.width,
+			style = { width: (that.size.width - soffsetw), height: (that.size.height - soffseth) },
+			left = (parseInt(that.element.css("left"), 10) +
+				(that.position.left - that.originalPosition.left)) || null,
+			top = (parseInt(that.element.css("top"), 10) +
+				(that.position.top - that.originalPosition.top)) || null;
+
+		that.element.animate(
+			$.extend(style, top && left ? { top: top, left: left } : {}), {
+				duration: o.animateDuration,
+				easing: o.animateEasing,
+				step: function() {
+
+					var data = {
+						width: parseInt(that.element.css("width"), 10),
+						height: parseInt(that.element.css("height"), 10),
+						top: parseInt(that.element.css("top"), 10),
+						left: parseInt(that.element.css("left"), 10)
+					};
+
+					if (pr && pr.length) {
+						$(pr[0]).css({ width: data.width, height: data.height });
+					}
+
+					// propagating resize, and updating values for each animation step
+					that._updateCache(data);
+					that._propagate("resize", event);
+
+				}
+			}
+		);
+	}
+
+});
+
+$.ui.plugin.add( "resizable", "containment", {
+
+	start: function() {
+		var element, p, co, ch, cw, width, height,
+			that = $( this ).resizable( "instance" ),
+			o = that.options,
+			el = that.element,
+			oc = o.containment,
+			ce = ( oc instanceof $ ) ? oc.get( 0 ) : ( /parent/.test( oc ) ) ? el.parent().get( 0 ) : oc;
+
+		if ( !ce ) {
+			return;
+		}
+
+		that.containerElement = $( ce );
+
+		if ( /document/.test( oc ) || oc === document ) {
+			that.containerOffset = {
+				left: 0,
+				top: 0
+			};
+			that.containerPosition = {
+				left: 0,
+				top: 0
+			};
+
+			that.parentData = {
+				element: $( document ),
+				left: 0,
+				top: 0,
+				width: $( document ).width(),
+				height: $( document ).height() || document.body.parentNode.scrollHeight
+			};
+		} else {
+			element = $( ce );
+			p = [];
+			$([ "Top", "Right", "Left", "Bottom" ]).each(function( i, name ) {
+				p[ i ] = that._num( element.css( "padding" + name ) );
+			});
+
+			that.containerOffset = element.offset();
+			that.containerPosition = element.position();
+			that.containerSize = {
+				height: ( element.innerHeight() - p[ 3 ] ),
+				width: ( element.innerWidth() - p[ 1 ] )
+			};
+
+			co = that.containerOffset;
+			ch = that.containerSize.height;
+			cw = that.containerSize.width;
+			width = ( that._hasScroll ( ce, "left" ) ? ce.scrollWidth : cw );
+			height = ( that._hasScroll ( ce ) ? ce.scrollHeight : ch ) ;
+
+			that.parentData = {
+				element: ce,
+				left: co.left,
+				top: co.top,
+				width: width,
+				height: height
+			};
+		}
+	},
+
+	resize: function( event ) {
+		var woset, hoset, isParent, isOffsetRelative,
+			that = $( this ).resizable( "instance" ),
+			o = that.options,
+			co = that.containerOffset,
+			cp = that.position,
+			pRatio = that._aspectRatio || event.shiftKey,
+			cop = {
+				top: 0,
+				left: 0
+			},
+			ce = that.containerElement,
+			continueResize = true;
+
+		if ( ce[ 0 ] !== document && ( /static/ ).test( ce.css( "position" ) ) ) {
+			cop = co;
+		}
+
+		if ( cp.left < ( that._helper ? co.left : 0 ) ) {
+			that.size.width = that.size.width +
+				( that._helper ?
+					( that.position.left - co.left ) :
+					( that.position.left - cop.left ) );
+
+			if ( pRatio ) {
+				that.size.height = that.size.width / that.aspectRatio;
+				continueResize = false;
+			}
+			that.position.left = o.helper ? co.left : 0;
+		}
+
+		if ( cp.top < ( that._helper ? co.top : 0 ) ) {
+			that.size.height = that.size.height +
+				( that._helper ?
+					( that.position.top - co.top ) :
+					that.position.top );
+
+			if ( pRatio ) {
+				that.size.width = that.size.height * that.aspectRatio;
+				continueResize = false;
+			}
+			that.position.top = that._helper ? co.top : 0;
+		}
+
+		isParent = that.containerElement.get( 0 ) === that.element.parent().get( 0 );
+		isOffsetRelative = /relative|absolute/.test( that.containerElement.css( "position" ) );
+
+		if ( isParent && isOffsetRelative ) {
+			that.offset.left = that.parentData.left + that.position.left;
+			that.offset.top = that.parentData.top + that.position.top;
+		} else {
+			that.offset.left = that.element.offset().left;
+			that.offset.top = that.element.offset().top;
+		}
+
+		woset = Math.abs( that.sizeDiff.width +
+			(that._helper ?
+				that.offset.left - cop.left :
+				(that.offset.left - co.left)) );
+
+		hoset = Math.abs( that.sizeDiff.height +
+			(that._helper ?
+				that.offset.top - cop.top :
+				(that.offset.top - co.top)) );
+
+		if ( woset + that.size.width >= that.parentData.width ) {
+			that.size.width = that.parentData.width - woset;
+			if ( pRatio ) {
+				that.size.height = that.size.width / that.aspectRatio;
+				continueResize = false;
+			}
+		}
+
+		if ( hoset + that.size.height >= that.parentData.height ) {
+			that.size.height = that.parentData.height - hoset;
+			if ( pRatio ) {
+				that.size.width = that.size.height * that.aspectRatio;
+				continueResize = false;
+			}
+		}
+
+		if ( !continueResize ){
+			that.position.left = that.prevPosition.left;
+			that.position.top = that.prevPosition.top;
+			that.size.width = that.prevSize.width;
+			that.size.height = that.prevSize.height;
+		}
+	},
+
+	stop: function() {
+		var that = $( this ).resizable( "instance" ),
+			o = that.options,
+			co = that.containerOffset,
+			cop = that.containerPosition,
+			ce = that.containerElement,
+			helper = $( that.helper ),
+			ho = helper.offset(),
+			w = helper.outerWidth() - that.sizeDiff.width,
+			h = helper.outerHeight() - that.sizeDiff.height;
+
+		if ( that._helper && !o.animate && ( /relative/ ).test( ce.css( "position" ) ) ) {
+			$( this ).css({
+				left: ho.left - cop.left - co.left,
+				width: w,
+				height: h
+			});
+		}
+
+		if ( that._helper && !o.animate && ( /static/ ).test( ce.css( "position" ) ) ) {
+			$( this ).css({
+				left: ho.left - cop.left - co.left,
+				width: w,
+				height: h
+			});
+		}
+	}
+});
+
+$.ui.plugin.add("resizable", "alsoResize", {
+
+	start: function() {
+		var that = $(this).resizable( "instance" ),
+			o = that.options,
+			_store = function(exp) {
+				$(exp).each(function() {
+					var el = $(this);
+					el.data("ui-resizable-alsoresize", {
+						width: parseInt(el.width(), 10), height: parseInt(el.height(), 10),
+						left: parseInt(el.css("left"), 10), top: parseInt(el.css("top"), 10)
+					});
+				});
+			};
+
+		if (typeof(o.alsoResize) === "object" && !o.alsoResize.parentNode) {
+			if (o.alsoResize.length) {
+				o.alsoResize = o.alsoResize[0];
+				_store(o.alsoResize);
+			} else {
+				$.each(o.alsoResize, function(exp) {
+					_store(exp);
+				});
+			}
+		} else {
+			_store(o.alsoResize);
+		}
+	},
+
+	resize: function(event, ui) {
+		var that = $(this).resizable( "instance" ),
+			o = that.options,
+			os = that.originalSize,
+			op = that.originalPosition,
+			delta = {
+				height: (that.size.height - os.height) || 0,
+				width: (that.size.width - os.width) || 0,
+				top: (that.position.top - op.top) || 0,
+				left: (that.position.left - op.left) || 0
+			},
+
+			_alsoResize = function(exp, c) {
+				$(exp).each(function() {
+					var el = $(this), start = $(this).data("ui-resizable-alsoresize"), style = {},
+						css = c && c.length ?
+							c :
+							el.parents(ui.originalElement[0]).length ?
+								[ "width", "height" ] :
+								[ "width", "height", "top", "left" ];
+
+					$.each(css, function(i, prop) {
+						var sum = (start[prop] || 0) + (delta[prop] || 0);
+						if (sum && sum >= 0) {
+							style[prop] = sum || null;
+						}
+					});
+
+					el.css(style);
+				});
+			};
+
+		if (typeof(o.alsoResize) === "object" && !o.alsoResize.nodeType) {
+			$.each(o.alsoResize, function(exp, c) {
+				_alsoResize(exp, c);
+			});
+		} else {
+			_alsoResize(o.alsoResize);
+		}
+	},
+
+	stop: function() {
+		$(this).removeData("resizable-alsoresize");
+	}
+});
+
+$.ui.plugin.add("resizable", "ghost", {
+
+	start: function() {
+
+		var that = $(this).resizable( "instance" ), o = that.options, cs = that.size;
+
+		that.ghost = that.originalElement.clone();
+		that.ghost
+			.css({
+				opacity: 0.25,
+				display: "block",
+				position: "relative",
+				height: cs.height,
+				width: cs.width,
+				margin: 0,
+				left: 0,
+				top: 0
+			})
+			.addClass("ui-resizable-ghost")
+			.addClass(typeof o.ghost === "string" ? o.ghost : "");
+
+		that.ghost.appendTo(that.helper);
+
+	},
+
+	resize: function() {
+		var that = $(this).resizable( "instance" );
+		if (that.ghost) {
+			that.ghost.css({
+				position: "relative",
+				height: that.size.height,
+				width: that.size.width
+			});
+		}
+	},
+
+	stop: function() {
+		var that = $(this).resizable( "instance" );
+		if (that.ghost && that.helper) {
+			that.helper.get(0).removeChild(that.ghost.get(0));
+		}
+	}
+
+});
+
+$.ui.plugin.add("resizable", "grid", {
+
+	resize: function() {
+		var outerDimensions,
+			that = $(this).resizable( "instance" ),
+			o = that.options,
+			cs = that.size,
+			os = that.originalSize,
+			op = that.originalPosition,
+			a = that.axis,
+			grid = typeof o.grid === "number" ? [ o.grid, o.grid ] : o.grid,
+			gridX = (grid[0] || 1),
+			gridY = (grid[1] || 1),
+			ox = Math.round((cs.width - os.width) / gridX) * gridX,
+			oy = Math.round((cs.height - os.height) / gridY) * gridY,
+			newWidth = os.width + ox,
+			newHeight = os.height + oy,
+			isMaxWidth = o.maxWidth && (o.maxWidth < newWidth),
+			isMaxHeight = o.maxHeight && (o.maxHeight < newHeight),
+			isMinWidth = o.minWidth && (o.minWidth > newWidth),
+			isMinHeight = o.minHeight && (o.minHeight > newHeight);
+
+		o.grid = grid;
+
+		if (isMinWidth) {
+			newWidth += gridX;
+		}
+		if (isMinHeight) {
+			newHeight += gridY;
+		}
+		if (isMaxWidth) {
+			newWidth -= gridX;
+		}
+		if (isMaxHeight) {
+			newHeight -= gridY;
+		}
+
+		if (/^(se|s|e)$/.test(a)) {
+			that.size.width = newWidth;
+			that.size.height = newHeight;
+		} else if (/^(ne)$/.test(a)) {
+			that.size.width = newWidth;
+			that.size.height = newHeight;
+			that.position.top = op.top - oy;
+		} else if (/^(sw)$/.test(a)) {
+			that.size.width = newWidth;
+			that.size.height = newHeight;
+			that.position.left = op.left - ox;
+		} else {
+			if ( newHeight - gridY <= 0 || newWidth - gridX <= 0) {
+				outerDimensions = that._getPaddingPlusBorderDimensions( this );
+			}
+
+			if ( newHeight - gridY > 0 ) {
+				that.size.height = newHeight;
+				that.position.top = op.top - oy;
+			} else {
+				newHeight = gridY - outerDimensions.height;
+				that.size.height = newHeight;
+				that.position.top = op.top + os.height - newHeight;
+			}
+			if ( newWidth - gridX > 0 ) {
+				that.size.width = newWidth;
+				that.position.left = op.left - ox;
+			} else {
+				newWidth = gridY - outerDimensions.height;
+				that.size.width = newWidth;
+				that.position.left = op.left + os.width - newWidth;
+			}
+		}
+	}
+
+});
+
+var resizable = $.ui.resizable;
+
+
+/*!
+ * jQuery UI Selectable 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/selectable/
+ */
+
+
+var selectable = $.widget("ui.selectable", $.ui.mouse, {
+	version: "1.11.2",
+	options: {
+		appendTo: "body",
+		autoRefresh: true,
+		distance: 0,
+		filter: "*",
+		tolerance: "touch",
+
+		// callbacks
+		selected: null,
+		selecting: null,
+		start: null,
+		stop: null,
+		unselected: null,
+		unselecting: null
+	},
+	_create: function() {
+		var selectees,
+			that = this;
+
+		this.element.addClass("ui-selectable");
+
+		this.dragged = false;
+
+		// cache selectee children based on filter
+		this.refresh = function() {
+			selectees = $(that.options.filter, that.element[0]);
+			selectees.addClass("ui-selectee");
+			selectees.each(function() {
+				var $this = $(this),
+					pos = $this.offset();
+				$.data(this, "selectable-item", {
+					element: this,
+					$element: $this,
+					left: pos.left,
+					top: pos.top,
+					right: pos.left + $this.outerWidth(),
+					bottom: pos.top + $this.outerHeight(),
+					startselected: false,
+					selected: $this.hasClass("ui-selected"),
+					selecting: $this.hasClass("ui-selecting"),
+					unselecting: $this.hasClass("ui-unselecting")
+				});
+			});
+		};
+		this.refresh();
+
+		this.selectees = selectees.addClass("ui-selectee");
+
+		this._mouseInit();
+
+		this.helper = $("<div class='ui-selectable-helper'></div>");
+	},
+
+	_destroy: function() {
+		this.selectees
+			.removeClass("ui-selectee")
+			.removeData("selectable-item");
+		this.element
+			.removeClass("ui-selectable ui-selectable-disabled");
+		this._mouseDestroy();
+	},
+
+	_mouseStart: function(event) {
+		var that = this,
+			options = this.options;
+
+		this.opos = [ event.pageX, event.pageY ];
+
+		if (this.options.disabled) {
+			return;
+		}
+
+		this.selectees = $(options.filter, this.element[0]);
+
+		this._trigger("start", event);
+
+		$(options.appendTo).append(this.helper);
+		// position helper (lasso)
+		this.helper.css({
+			"left": event.pageX,
+			"top": event.pageY,
+			"width": 0,
+			"height": 0
+		});
+
+		if (options.autoRefresh) {
+			this.refresh();
+		}
+
+		this.selectees.filter(".ui-selected").each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.startselected = true;
+			if (!event.metaKey && !event.ctrlKey) {
+				selectee.$element.removeClass("ui-selected");
+				selectee.selected = false;
+				selectee.$element.addClass("ui-unselecting");
+				selectee.unselecting = true;
+				// selectable UNSELECTING callback
+				that._trigger("unselecting", event, {
+					unselecting: selectee.element
+				});
+			}
+		});
+
+		$(event.target).parents().addBack().each(function() {
+			var doSelect,
+				selectee = $.data(this, "selectable-item");
+			if (selectee) {
+				doSelect = (!event.metaKey && !event.ctrlKey) || !selectee.$element.hasClass("ui-selected");
+				selectee.$element
+					.removeClass(doSelect ? "ui-unselecting" : "ui-selected")
+					.addClass(doSelect ? "ui-selecting" : "ui-unselecting");
+				selectee.unselecting = !doSelect;
+				selectee.selecting = doSelect;
+				selectee.selected = doSelect;
+				// selectable (UN)SELECTING callback
+				if (doSelect) {
+					that._trigger("selecting", event, {
+						selecting: selectee.element
+					});
+				} else {
+					that._trigger("unselecting", event, {
+						unselecting: selectee.element
+					});
+				}
+				return false;
+			}
+		});
+
+	},
+
+	_mouseDrag: function(event) {
+
+		this.dragged = true;
+
+		if (this.options.disabled) {
+			return;
+		}
+
+		var tmp,
+			that = this,
+			options = this.options,
+			x1 = this.opos[0],
+			y1 = this.opos[1],
+			x2 = event.pageX,
+			y2 = event.pageY;
+
+		if (x1 > x2) { tmp = x2; x2 = x1; x1 = tmp; }
+		if (y1 > y2) { tmp = y2; y2 = y1; y1 = tmp; }
+		this.helper.css({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 });
+
+		this.selectees.each(function() {
+			var selectee = $.data(this, "selectable-item"),
+				hit = false;
+
+			//prevent helper from being selected if appendTo: selectable
+			if (!selectee || selectee.element === that.element[0]) {
+				return;
+			}
+
+			if (options.tolerance === "touch") {
+				hit = ( !(selectee.left > x2 || selectee.right < x1 || selectee.top > y2 || selectee.bottom < y1) );
+			} else if (options.tolerance === "fit") {
+				hit = (selectee.left > x1 && selectee.right < x2 && selectee.top > y1 && selectee.bottom < y2);
+			}
+
+			if (hit) {
+				// SELECT
+				if (selectee.selected) {
+					selectee.$element.removeClass("ui-selected");
+					selectee.selected = false;
+				}
+				if (selectee.unselecting) {
+					selectee.$element.removeClass("ui-unselecting");
+					selectee.unselecting = false;
+				}
+				if (!selectee.selecting) {
+					selectee.$element.addClass("ui-selecting");
+					selectee.selecting = true;
+					// selectable SELECTING callback
+					that._trigger("selecting", event, {
+						selecting: selectee.element
+					});
+				}
+			} else {
+				// UNSELECT
+				if (selectee.selecting) {
+					if ((event.metaKey || event.ctrlKey) && selectee.startselected) {
+						selectee.$element.removeClass("ui-selecting");
+						selectee.selecting = false;
+						selectee.$element.addClass("ui-selected");
+						selectee.selected = true;
+					} else {
+						selectee.$element.removeClass("ui-selecting");
+						selectee.selecting = false;
+						if (selectee.startselected) {
+							selectee.$element.addClass("ui-unselecting");
+							selectee.unselecting = true;
+						}
+						// selectable UNSELECTING callback
+						that._trigger("unselecting", event, {
+							unselecting: selectee.element
+						});
+					}
+				}
+				if (selectee.selected) {
+					if (!event.metaKey && !event.ctrlKey && !selectee.startselected) {
+						selectee.$element.removeClass("ui-selected");
+						selectee.selected = false;
+
+						selectee.$element.addClass("ui-unselecting");
+						selectee.unselecting = true;
+						// selectable UNSELECTING callback
+						that._trigger("unselecting", event, {
+							unselecting: selectee.element
+						});
+					}
+				}
+			}
+		});
+
+		return false;
+	},
+
+	_mouseStop: function(event) {
+		var that = this;
+
+		this.dragged = false;
+
+		$(".ui-unselecting", this.element[0]).each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.$element.removeClass("ui-unselecting");
+			selectee.unselecting = false;
+			selectee.startselected = false;
+			that._trigger("unselected", event, {
+				unselected: selectee.element
+			});
+		});
+		$(".ui-selecting", this.element[0]).each(function() {
+			var selectee = $.data(this, "selectable-item");
+			selectee.$element.removeClass("ui-selecting").addClass("ui-selected");
+			selectee.selecting = false;
+			selectee.selected = true;
+			selectee.startselected = true;
+			that._trigger("selected", event, {
+				selected: selectee.element
+			});
+		});
+		this._trigger("stop", event);
+
+		this.helper.remove();
+
+		return false;
+	}
+
+});
+
+
+/*!
+ * jQuery UI Sortable 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/sortable/
+ */
+
+
+var sortable = $.widget("ui.sortable", $.ui.mouse, {
+	version: "1.11.2",
+	widgetEventPrefix: "sort",
+	ready: false,
+	options: {
+		appendTo: "parent",
+		axis: false,
+		connectWith: false,
+		containment: false,
+		cursor: "auto",
+		cursorAt: false,
+		dropOnEmpty: true,
+		forcePlaceholderSize: false,
+		forceHelperSize: false,
+		grid: false,
+		handle: false,
+		helper: "original",
+		items: "> *",
+		opacity: false,
+		placeholder: false,
+		revert: false,
+		scroll: true,
+		scrollSensitivity: 20,
+		scrollSpeed: 20,
+		scope: "default",
+		tolerance: "intersect",
+		zIndex: 1000,
+
+		// callbacks
+		activate: null,
+		beforeStop: null,
+		change: null,
+		deactivate: null,
+		out: null,
+		over: null,
+		receive: null,
+		remove: null,
+		sort: null,
+		start: null,
+		stop: null,
+		update: null
+	},
+
+	_isOverAxis: function( x, reference, size ) {
+		return ( x >= reference ) && ( x < ( reference + size ) );
+	},
+
+	_isFloating: function( item ) {
+		return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display"));
+	},
+
+	_create: function() {
+
+		var o = this.options;
+		this.containerCache = {};
+		this.element.addClass("ui-sortable");
+
+		//Get the items
+		this.refresh();
+
+		//Let's determine if the items are being displayed horizontally
+		this.floating = this.items.length ? o.axis === "x" || this._isFloating(this.items[0].item) : false;
+
+		//Let's determine the parent's offset
+		this.offset = this.element.offset();
+
+		//Initialize mouse events for interaction
+		this._mouseInit();
+
+		this._setHandleClassName();
+
+		//We're ready to go
+		this.ready = true;
+
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+
+		if ( key === "handle" ) {
+			this._setHandleClassName();
+		}
+	},
+
+	_setHandleClassName: function() {
+		this.element.find( ".ui-sortable-handle" ).removeClass( "ui-sortable-handle" );
+		$.each( this.items, function() {
+			( this.instance.options.handle ?
+				this.item.find( this.instance.options.handle ) : this.item )
+				.addClass( "ui-sortable-handle" );
+		});
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-sortable ui-sortable-disabled" )
+			.find( ".ui-sortable-handle" )
+				.removeClass( "ui-sortable-handle" );
+		this._mouseDestroy();
+
+		for ( var i = this.items.length - 1; i >= 0; i-- ) {
+			this.items[i].item.removeData(this.widgetName + "-item");
+		}
+
+		return this;
+	},
+
+	_mouseCapture: function(event, overrideHandle) {
+		var currentItem = null,
+			validHandle = false,
+			that = this;
+
+		if (this.reverting) {
+			return false;
+		}
+
+		if(this.options.disabled || this.options.type === "static") {
+			return false;
+		}
+
+		//We have to refresh the items data once first
+		this._refreshItems(event);
+
+		//Find out if the clicked node (or one of its parents) is a actual item in this.items
+		$(event.target).parents().each(function() {
+			if($.data(this, that.widgetName + "-item") === that) {
+				currentItem = $(this);
+				return false;
+			}
+		});
+		if($.data(event.target, that.widgetName + "-item") === that) {
+			currentItem = $(event.target);
+		}
+
+		if(!currentItem) {
+			return false;
+		}
+		if(this.options.handle && !overrideHandle) {
+			$(this.options.handle, currentItem).find("*").addBack().each(function() {
+				if(this === event.target) {
+					validHandle = true;
+				}
+			});
+			if(!validHandle) {
+				return false;
+			}
+		}
+
+		this.currentItem = currentItem;
+		this._removeCurrentsFromItems();
+		return true;
+
+	},
+
+	_mouseStart: function(event, overrideHandle, noActivation) {
+
+		var i, body,
+			o = this.options;
+
+		this.currentContainer = this;
+
+		//We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
+		this.refreshPositions();
+
+		//Create and append the visible helper
+		this.helper = this._createHelper(event);
+
+		//Cache the helper size
+		this._cacheHelperProportions();
+
+		/*
+		 * - Position generation -
+		 * This block generates everything position related - it's the core of draggables.
+		 */
+
+		//Cache the margins of the original element
+		this._cacheMargins();
+
+		//Get the next scrolling parent
+		this.scrollParent = this.helper.scrollParent();
+
+		//The element's absolute position on the page minus margins
+		this.offset = this.currentItem.offset();
+		this.offset = {
+			top: this.offset.top - this.margins.top,
+			left: this.offset.left - this.margins.left
+		};
+
+		$.extend(this.offset, {
+			click: { //Where the click happened, relative to the element
+				left: event.pageX - this.offset.left,
+				top: event.pageY - this.offset.top
+			},
+			parent: this._getParentOffset(),
+			relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
+		});
+
+		// Only after we got the offset, we can change the helper's position to absolute
+		// TODO: Still need to figure out a way to make relative sorting possible
+		this.helper.css("position", "absolute");
+		this.cssPosition = this.helper.css("position");
+
+		//Generate the original position
+		this.originalPosition = this._generatePosition(event);
+		this.originalPageX = event.pageX;
+		this.originalPageY = event.pageY;
+
+		//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
+		(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+		//Cache the former DOM position
+		this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
+
+		//If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
+		if(this.helper[0] !== this.currentItem[0]) {
+			this.currentItem.hide();
+		}
+
+		//Create the placeholder
+		this._createPlaceholder();
+
+		//Set a containment if given in the options
+		if(o.containment) {
+			this._setContainment();
+		}
+
+		if( o.cursor && o.cursor !== "auto" ) { // cursor option
+			body = this.document.find( "body" );
+
+			// support: IE
+			this.storedCursor = body.css( "cursor" );
+			body.css( "cursor", o.cursor );
+
+			this.storedStylesheet = $( "<style>*{ cursor: "+o.cursor+" !important; }</style>" ).appendTo( body );
+		}
+
+		if(o.opacity) { // opacity option
+			if (this.helper.css("opacity")) {
+				this._storedOpacity = this.helper.css("opacity");
+			}
+			this.helper.css("opacity", o.opacity);
+		}
+
+		if(o.zIndex) { // zIndex option
+			if (this.helper.css("zIndex")) {
+				this._storedZIndex = this.helper.css("zIndex");
+			}
+			this.helper.css("zIndex", o.zIndex);
+		}
+
+		//Prepare scrolling
+		if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+			this.overflowOffset = this.scrollParent.offset();
+		}
+
+		//Call callbacks
+		this._trigger("start", event, this._uiHash());
+
+		//Recache the helper size
+		if(!this._preserveHelperProportions) {
+			this._cacheHelperProportions();
+		}
+
+
+		//Post "activate" events to possible containers
+		if( !noActivation ) {
+			for ( i = this.containers.length - 1; i >= 0; i-- ) {
+				this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
+			}
+		}
+
+		//Prepare possible droppables
+		if($.ui.ddmanager) {
+			$.ui.ddmanager.current = this;
+		}
+
+		if ($.ui.ddmanager && !o.dropBehaviour) {
+			$.ui.ddmanager.prepareOffsets(this, event);
+		}
+
+		this.dragging = true;
+
+		this.helper.addClass("ui-sortable-helper");
+		this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+		return true;
+
+	},
+
+	_mouseDrag: function(event) {
+		var i, item, itemElement, intersection,
+			o = this.options,
+			scrolled = false;
+
+		//Compute the helpers position
+		this.position = this._generatePosition(event);
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		if (!this.lastPositionAbs) {
+			this.lastPositionAbs = this.positionAbs;
+		}
+
+		//Do scrolling
+		if(this.options.scroll) {
+			if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+
+				if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) {
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
+				} else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) {
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
+				}
+
+				if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) {
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
+				} else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) {
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
+				}
+
+			} else {
+
+				if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+				} else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+				}
+
+				if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+				} else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+				}
+
+			}
+
+			if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) {
+				$.ui.ddmanager.prepareOffsets(this, event);
+			}
+		}
+
+		//Regenerate the absolute position used for position checks
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		//Set the helper position
+		if(!this.options.axis || this.options.axis !== "y") {
+			this.helper[0].style.left = this.position.left+"px";
+		}
+		if(!this.options.axis || this.options.axis !== "x") {
+			this.helper[0].style.top = this.position.top+"px";
+		}
+
+		//Rearrange
+		for (i = this.items.length - 1; i >= 0; i--) {
+
+			//Cache variables and intersection, continue if no intersection
+			item = this.items[i];
+			itemElement = item.item[0];
+			intersection = this._intersectsWithPointer(item);
+			if (!intersection) {
+				continue;
+			}
+
+			// Only put the placeholder inside the current Container, skip all
+			// items from other containers. This works because when moving
+			// an item from one container to another the
+			// currentContainer is switched before the placeholder is moved.
+			//
+			// Without this, moving items in "sub-sortables" can cause
+			// the placeholder to jitter between the outer and inner container.
+			if (item.instance !== this.currentContainer) {
+				continue;
+			}
+
+			// cannot intersect with itself
+			// no useless actions that have been done before
+			// no action if the item moved is the parent of the item checked
+			if (itemElement !== this.currentItem[0] &&
+				this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement &&
+				!$.contains(this.placeholder[0], itemElement) &&
+				(this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true)
+			) {
+
+				this.direction = intersection === 1 ? "down" : "up";
+
+				if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) {
+					this._rearrange(event, item);
+				} else {
+					break;
+				}
+
+				this._trigger("change", event, this._uiHash());
+				break;
+			}
+		}
+
+		//Post events to containers
+		this._contactContainers(event);
+
+		//Interconnect with droppables
+		if($.ui.ddmanager) {
+			$.ui.ddmanager.drag(this, event);
+		}
+
+		//Call callbacks
+		this._trigger("sort", event, this._uiHash());
+
+		this.lastPositionAbs = this.positionAbs;
+		return false;
+
+	},
+
+	_mouseStop: function(event, noPropagation) {
+
+		if(!event) {
+			return;
+		}
+
+		//If we are using droppables, inform the manager about the drop
+		if ($.ui.ddmanager && !this.options.dropBehaviour) {
+			$.ui.ddmanager.drop(this, event);
+		}
+
+		if(this.options.revert) {
+			var that = this,
+				cur = this.placeholder.offset(),
+				axis = this.options.axis,
+				animation = {};
+
+			if ( !axis || axis === "x" ) {
+				animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft);
+			}
+			if ( !axis || axis === "y" ) {
+				animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop);
+			}
+			this.reverting = true;
+			$(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() {
+				that._clear(event);
+			});
+		} else {
+			this._clear(event, noPropagation);
+		}
+
+		return false;
+
+	},
+
+	cancel: function() {
+
+		if(this.dragging) {
+
+			this._mouseUp({ target: null });
+
+			if(this.options.helper === "original") {
+				this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+			} else {
+				this.currentItem.show();
+			}
+
+			//Post deactivating events to containers
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				this.containers[i]._trigger("deactivate", null, this._uiHash(this));
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", null, this._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+
+		if (this.placeholder) {
+			//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+			if(this.placeholder[0].parentNode) {
+				this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+			}
+			if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) {
+				this.helper.remove();
+			}
+
+			$.extend(this, {
+				helper: null,
+				dragging: false,
+				reverting: false,
+				_noFinalSort: null
+			});
+
+			if(this.domPosition.prev) {
+				$(this.domPosition.prev).after(this.currentItem);
+			} else {
+				$(this.domPosition.parent).prepend(this.currentItem);
+			}
+		}
+
+		return this;
+
+	},
+
+	serialize: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected),
+			str = [];
+		o = o || {};
+
+		$(items).each(function() {
+			var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/));
+			if (res) {
+				str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2]));
+			}
+		});
+
+		if(!str.length && o.key) {
+			str.push(o.key + "=");
+		}
+
+		return str.join("&");
+
+	},
+
+	toArray: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected),
+			ret = [];
+
+		o = o || {};
+
+		items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); });
+		return ret;
+
+	},
+
+	/* Be careful with the following core functions */
+	_intersectsWith: function(item) {
+
+		var x1 = this.positionAbs.left,
+			x2 = x1 + this.helperProportions.width,
+			y1 = this.positionAbs.top,
+			y2 = y1 + this.helperProportions.height,
+			l = item.left,
+			r = l + item.width,
+			t = item.top,
+			b = t + item.height,
+			dyClick = this.offset.click.top,
+			dxClick = this.offset.click.left,
+			isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ),
+			isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ),
+			isOverElement = isOverElementHeight && isOverElementWidth;
+
+		if ( this.options.tolerance === "pointer" ||
+			this.options.forcePointerForContainers ||
+			(this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"])
+		) {
+			return isOverElement;
+		} else {
+
+			return (l < x1 + (this.helperProportions.width / 2) && // Right Half
+				x2 - (this.helperProportions.width / 2) < r && // Left Half
+				t < y1 + (this.helperProportions.height / 2) && // Bottom Half
+				y2 - (this.helperProportions.height / 2) < b ); // Top Half
+
+		}
+	},
+
+	_intersectsWithPointer: function(item) {
+
+		var isOverElementHeight = (this.options.axis === "x") || this._isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
+			isOverElementWidth = (this.options.axis === "y") || this._isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),
+			isOverElement = isOverElementHeight && isOverElementWidth,
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (!isOverElement) {
+			return false;
+		}
+
+		return this.floating ?
+			( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 )
+			: ( verticalDirection && (verticalDirection === "down" ? 2 : 1) );
+
+	},
+
+	_intersectsWithSides: function(item) {
+
+		var isOverBottomHalf = this._isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height),
+			isOverRightHalf = this._isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width),
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (this.floating && horizontalDirection) {
+			return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf));
+		} else {
+			return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf));
+		}
+
+	},
+
+	_getDragVerticalDirection: function() {
+		var delta = this.positionAbs.top - this.lastPositionAbs.top;
+		return delta !== 0 && (delta > 0 ? "down" : "up");
+	},
+
+	_getDragHorizontalDirection: function() {
+		var delta = this.positionAbs.left - this.lastPositionAbs.left;
+		return delta !== 0 && (delta > 0 ? "right" : "left");
+	},
+
+	refresh: function(event) {
+		this._refreshItems(event);
+		this._setHandleClassName();
+		this.refreshPositions();
+		return this;
+	},
+
+	_connectWith: function() {
+		var options = this.options;
+		return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith;
+	},
+
+	_getItemsAsjQuery: function(connected) {
+
+		var i, j, cur, inst,
+			items = [],
+			queries = [],
+			connectWith = this._connectWith();
+
+		if(connectWith && connected) {
+			for (i = connectWith.length - 1; i >= 0; i--){
+				cur = $(connectWith[i]);
+				for ( j = cur.length - 1; j >= 0; j--){
+					inst = $.data(cur[j], this.widgetFullName);
+					if(inst && inst !== this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]);
+					}
+				}
+			}
+		}
+
+		queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]);
+
+		function addItems() {
+			items.push( this );
+		}
+		for (i = queries.length - 1; i >= 0; i--){
+			queries[i][0].each( addItems );
+		}
+
+		return $(items);
+
+	},
+
+	_removeCurrentsFromItems: function() {
+
+		var list = this.currentItem.find(":data(" + this.widgetName + "-item)");
+
+		this.items = $.grep(this.items, function (item) {
+			for (var j=0; j < list.length; j++) {
+				if(list[j] === item.item[0]) {
+					return false;
+				}
+			}
+			return true;
+		});
+
+	},
+
+	_refreshItems: function(event) {
+
+		this.items = [];
+		this.containers = [this];
+
+		var i, j, cur, inst, targetData, _queries, item, queriesLength,
+			items = this.items,
+			queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]],
+			connectWith = this._connectWith();
+
+		if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down
+			for (i = connectWith.length - 1; i >= 0; i--){
+				cur = $(connectWith[i]);
+				for (j = cur.length - 1; j >= 0; j--){
+					inst = $.data(cur[j], this.widgetFullName);
+					if(inst && inst !== this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]);
+						this.containers.push(inst);
+					}
+				}
+			}
+		}
+
+		for (i = queries.length - 1; i >= 0; i--) {
+			targetData = queries[i][1];
+			_queries = queries[i][0];
+
+			for (j=0, queriesLength = _queries.length; j < queriesLength; j++) {
+				item = $(_queries[j]);
+
+				item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager)
+
+				items.push({
+					item: item,
+					instance: targetData,
+					width: 0, height: 0,
+					left: 0, top: 0
+				});
+			}
+		}
+
+	},
+
+	refreshPositions: function(fast) {
+
+		//This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
+		if(this.offsetParent && this.helper) {
+			this.offset.parent = this._getParentOffset();
+		}
+
+		var i, item, t, p;
+
+		for (i = this.items.length - 1; i >= 0; i--){
+			item = this.items[i];
+
+			//We ignore calculating positions of all connected containers when we're not over them
+			if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) {
+				continue;
+			}
+
+			t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item;
+
+			if (!fast) {
+				item.width = t.outerWidth();
+				item.height = t.outerHeight();
+			}
+
+			p = t.offset();
+			item.left = p.left;
+			item.top = p.top;
+		}
+
+		if(this.options.custom && this.options.custom.refreshContainers) {
+			this.options.custom.refreshContainers.call(this);
+		} else {
+			for (i = this.containers.length - 1; i >= 0; i--){
+				p = this.containers[i].element.offset();
+				this.containers[i].containerCache.left = p.left;
+				this.containers[i].containerCache.top = p.top;
+				this.containers[i].containerCache.width = this.containers[i].element.outerWidth();
+				this.containers[i].containerCache.height = this.containers[i].element.outerHeight();
+			}
+		}
+
+		return this;
+	},
+
+	_createPlaceholder: function(that) {
+		that = that || this;
+		var className,
+			o = that.options;
+
+		if(!o.placeholder || o.placeholder.constructor === String) {
+			className = o.placeholder;
+			o.placeholder = {
+				element: function() {
+
+					var nodeName = that.currentItem[0].nodeName.toLowerCase(),
+						element = $( "<" + nodeName + ">", that.document[0] )
+							.addClass(className || that.currentItem[0].className+" ui-sortable-placeholder")
+							.removeClass("ui-sortable-helper");
+
+					if ( nodeName === "tr" ) {
+						that.currentItem.children().each(function() {
+							$( "<td> </td>", that.document[0] )
+								.attr( "colspan", $( this ).attr( "colspan" ) || 1 )
+								.appendTo( element );
+						});
+					} else if ( nodeName === "img" ) {
+						element.attr( "src", that.currentItem.attr( "src" ) );
+					}
+
+					if ( !className ) {
+						element.css( "visibility", "hidden" );
+					}
+
+					return element;
+				},
+				update: function(container, p) {
+
+					// 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
+					// 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
+					if(className && !o.forcePlaceholderSize) {
+						return;
+					}
+
+					//If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item
+					if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); }
+					if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); }
+				}
+			};
+		}
+
+		//Create the placeholder
+		that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem));
+
+		//Append it after the actual current item
+		that.currentItem.after(that.placeholder);
+
+		//Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
+		o.placeholder.update(that, that.placeholder);
+
+	},
+
+	_contactContainers: function(event) {
+		var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom, floating, axis,
+			innermostContainer = null,
+			innermostIndex = null;
+
+		// get innermost container that intersects with item
+		for (i = this.containers.length - 1; i >= 0; i--) {
+
+			// never consider a container that's located within the item itself
+			if($.contains(this.currentItem[0], this.containers[i].element[0])) {
+				continue;
+			}
+
+			if(this._intersectsWith(this.containers[i].containerCache)) {
+
+				// if we've already found a container and it's more "inner" than this, then continue
+				if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) {
+					continue;
+				}
+
+				innermostContainer = this.containers[i];
+				innermostIndex = i;
+
+			} else {
+				// container doesn't intersect. trigger "out" event if necessary
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", event, this._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+
+		// if no intersecting containers found, return
+		if(!innermostContainer) {
+			return;
+		}
+
+		// move the item into the container if it's not there already
+		if(this.containers.length === 1) {
+			if (!this.containers[innermostIndex].containerCache.over) {
+				this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+				this.containers[innermostIndex].containerCache.over = 1;
+			}
+		} else {
+
+			//When entering a new container, we will find the item with the least distance and append our item near it
+			dist = 10000;
+			itemWithLeastDistance = null;
+			floating = innermostContainer.floating || this._isFloating(this.currentItem);
+			posProperty = floating ? "left" : "top";
+			sizeProperty = floating ? "width" : "height";
+			axis = floating ? "clientX" : "clientY";
+
+			for (j = this.items.length - 1; j >= 0; j--) {
+				if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) {
+					continue;
+				}
+				if(this.items[j].item[0] === this.currentItem[0]) {
+					continue;
+				}
+
+				cur = this.items[j].item.offset()[posProperty];
+				nearBottom = false;
+				if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) {
+					nearBottom = true;
+				}
+
+				if ( Math.abs( event[ axis ] - cur ) < dist ) {
+					dist = Math.abs( event[ axis ] - cur );
+					itemWithLeastDistance = this.items[ j ];
+					this.direction = nearBottom ? "up": "down";
+				}
+			}
+
+			//Check if dropOnEmpty is enabled
+			if(!itemWithLeastDistance && !this.options.dropOnEmpty) {
+				return;
+			}
+
+			if(this.currentContainer === this.containers[innermostIndex]) {
+				if ( !this.currentContainer.containerCache.over ) {
+					this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() );
+					this.currentContainer.containerCache.over = 1;
+				}
+				return;
+			}
+
+			itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true);
+			this._trigger("change", event, this._uiHash());
+			this.containers[innermostIndex]._trigger("change", event, this._uiHash(this));
+			this.currentContainer = this.containers[innermostIndex];
+
+			//Update the placeholder
+			this.options.placeholder.update(this.currentContainer, this.placeholder);
+
+			this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+			this.containers[innermostIndex].containerCache.over = 1;
+		}
+
+
+	},
+
+	_createHelper: function(event) {
+
+		var o = this.options,
+			helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem);
+
+		//Add the helper to the DOM if that didn't happen already
+		if(!helper.parents("body").length) {
+			$(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]);
+		}
+
+		if(helper[0] === this.currentItem[0]) {
+			this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") };
+		}
+
+		if(!helper[0].style.width || o.forceHelperSize) {
+			helper.width(this.currentItem.width());
+		}
+		if(!helper[0].style.height || o.forceHelperSize) {
+			helper.height(this.currentItem.height());
+		}
+
+		return helper;
+
+	},
+
+	_adjustOffsetFromHelper: function(obj) {
+		if (typeof obj === "string") {
+			obj = obj.split(" ");
+		}
+		if ($.isArray(obj)) {
+			obj = {left: +obj[0], top: +obj[1] || 0};
+		}
+		if ("left" in obj) {
+			this.offset.click.left = obj.left + this.margins.left;
+		}
+		if ("right" in obj) {
+			this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+		}
+		if ("top" in obj) {
+			this.offset.click.top = obj.top + this.margins.top;
+		}
+		if ("bottom" in obj) {
+			this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+		}
+	},
+
+	_getParentOffset: function() {
+
+
+		//Get the offsetParent and cache its position
+		this.offsetParent = this.helper.offsetParent();
+		var po = this.offsetParent.offset();
+
+		// This is a special case where we need to modify a offset calculated on start, since the following happened:
+		// 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+		// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+		//    the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+		if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) {
+			po.left += this.scrollParent.scrollLeft();
+			po.top += this.scrollParent.scrollTop();
+		}
+
+		// This needs to be actually done for all browsers, since pageX/pageY includes this information
+		// with an ugly IE fix
+		if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) {
+			po = { top: 0, left: 0 };
+		}
+
+		return {
+			top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0),
+			left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0)
+		};
+
+	},
+
+	_getRelativeOffset: function() {
+
+		if(this.cssPosition === "relative") {
+			var p = this.currentItem.position();
+			return {
+				top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(),
+				left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft()
+			};
+		} else {
+			return { top: 0, left: 0 };
+		}
+
+	},
+
+	_cacheMargins: function() {
+		this.margins = {
+			left: (parseInt(this.currentItem.css("marginLeft"),10) || 0),
+			top: (parseInt(this.currentItem.css("marginTop"),10) || 0)
+		};
+	},
+
+	_cacheHelperProportions: function() {
+		this.helperProportions = {
+			width: this.helper.outerWidth(),
+			height: this.helper.outerHeight()
+		};
+	},
+
+	_setContainment: function() {
+
+		var ce, co, over,
+			o = this.options;
+		if(o.containment === "parent") {
+			o.containment = this.helper[0].parentNode;
+		}
+		if(o.containment === "document" || o.containment === "window") {
+			this.containment = [
+				0 - this.offset.relative.left - this.offset.parent.left,
+				0 - this.offset.relative.top - this.offset.parent.top,
+				$(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left,
+				($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top
+			];
+		}
+
+		if(!(/^(document|window|parent)$/).test(o.containment)) {
+			ce = $(o.containment)[0];
+			co = $(o.containment).offset();
+			over = ($(ce).css("overflow") !== "hidden");
+
+			this.containment = [
+				co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left,
+				co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top,
+				co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left,
+				co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top
+			];
+		}
+
+	},
+
+	_convertPositionTo: function(d, pos) {
+
+		if(!pos) {
+			pos = this.position;
+		}
+		var mod = d === "absolute" ? 1 : -1,
+			scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent,
+			scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		return {
+			top: (
+				pos.top	+																// The absolute mouse position
+				this.offset.relative.top * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top * mod -											// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod)
+			),
+			left: (
+				pos.left +																// The absolute mouse position
+				this.offset.relative.left * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left * mod	-										// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod)
+			)
+		};
+
+	},
+
+	_generatePosition: function(event) {
+
+		var top, left,
+			o = this.options,
+			pageX = event.pageX,
+			pageY = event.pageY,
+			scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		// This is another very weird special case that only happens for relative elements:
+		// 1. If the css position is relative
+		// 2. and the scroll parent is the document or similar to the offset parent
+		// we have to refresh the relative offset during the scroll so there are no jumps
+		if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) {
+			this.offset.relative = this._getRelativeOffset();
+		}
+
+		/*
+		 * - Position constraining -
+		 * Constrain the position to a mix of grid, containment.
+		 */
+
+		if(this.originalPosition) { //If we are not dragging yet, we won't check for options
+
+			if(this.containment) {
+				if(event.pageX - this.offset.click.left < this.containment[0]) {
+					pageX = this.containment[0] + this.offset.click.left;
+				}
+				if(event.pageY - this.offset.click.top < this.containment[1]) {
+					pageY = this.containment[1] + this.offset.click.top;
+				}
+				if(event.pageX - this.offset.click.left > this.containment[2]) {
+					pageX = this.containment[2] + this.offset.click.left;
+				}
+				if(event.pageY - this.offset.click.top > this.containment[3]) {
+					pageY = this.containment[3] + this.offset.click.top;
+				}
+			}
+
+			if(o.grid) {
+				top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1];
+				pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+				left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0];
+				pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+			}
+
+		}
+
+		return {
+			top: (
+				pageY -																// The absolute mouse position
+				this.offset.click.top -													// Click offset (relative to the element)
+				this.offset.relative.top	-											// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top +												// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
+			),
+			left: (
+				pageX -																// The absolute mouse position
+				this.offset.click.left -												// Click offset (relative to the element)
+				this.offset.relative.left	-											// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left +												// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
+			)
+		};
+
+	},
+
+	_rearrange: function(event, i, a, hardRefresh) {
+
+		a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling));
+
+		//Various things done here to improve the performance:
+		// 1. we create a setTimeout, that calls refreshPositions
+		// 2. on the instance, we have a counter variable, that get's higher after every append
+		// 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
+		// 4. this lets only the last addition to the timeout stack through
+		this.counter = this.counter ? ++this.counter : 1;
+		var counter = this.counter;
+
+		this._delay(function() {
+			if(counter === this.counter) {
+				this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove
+			}
+		});
+
+	},
+
+	_clear: function(event, noPropagation) {
+
+		this.reverting = false;
+		// We delay all events that have to be triggered to after the point where the placeholder has been removed and
+		// everything else normalized again
+		var i,
+			delayedTriggers = [];
+
+		// We first have to update the dom position of the actual currentItem
+		// Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
+		if(!this._noFinalSort && this.currentItem.parent().length) {
+			this.placeholder.before(this.currentItem);
+		}
+		this._noFinalSort = null;
+
+		if(this.helper[0] === this.currentItem[0]) {
+			for(i in this._storedCSS) {
+				if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") {
+					this._storedCSS[i] = "";
+				}
+			}
+			this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+		} else {
+			this.currentItem.show();
+		}
+
+		if(this.fromOutside && !noPropagation) {
+			delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); });
+		}
+		if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) {
+			delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed
+		}
+
+		// Check if the items Container has Changed and trigger appropriate
+		// events.
+		if (this !== this.currentContainer) {
+			if(!noPropagation) {
+				delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); };  }).call(this, this.currentContainer));
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this));  }; }).call(this, this.currentContainer));
+			}
+		}
+
+
+		//Post events to containers
+		function delayEvent( type, instance, container ) {
+			return function( event ) {
+				container._trigger( type, event, instance._uiHash( instance ) );
+			};
+		}
+		for (i = this.containers.length - 1; i >= 0; i--){
+			if (!noPropagation) {
+				delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) );
+			}
+			if(this.containers[i].containerCache.over) {
+				delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) );
+				this.containers[i].containerCache.over = 0;
+			}
+		}
+
+		//Do what was originally in plugins
+		if ( this.storedCursor ) {
+			this.document.find( "body" ).css( "cursor", this.storedCursor );
+			this.storedStylesheet.remove();
+		}
+		if(this._storedOpacity) {
+			this.helper.css("opacity", this._storedOpacity);
+		}
+		if(this._storedZIndex) {
+			this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex);
+		}
+
+		this.dragging = false;
+
+		if(!noPropagation) {
+			this._trigger("beforeStop", event, this._uiHash());
+		}
+
+		//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+		this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+
+		if ( !this.cancelHelperRemoval ) {
+			if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) {
+				this.helper.remove();
+			}
+			this.helper = null;
+		}
+
+		if(!noPropagation) {
+			for (i=0; i < delayedTriggers.length; i++) {
+				delayedTriggers[i].call(this, event);
+			} //Trigger all delayed events
+			this._trigger("stop", event, this._uiHash());
+		}
+
+		this.fromOutside = false;
+		return !this.cancelHelperRemoval;
+
+	},
+
+	_trigger: function() {
+		if ($.Widget.prototype._trigger.apply(this, arguments) === false) {
+			this.cancel();
+		}
+	},
+
+	_uiHash: function(_inst) {
+		var inst = _inst || this;
+		return {
+			helper: inst.helper,
+			placeholder: inst.placeholder || $([]),
+			position: inst.position,
+			originalPosition: inst.originalPosition,
+			offset: inst.positionAbs,
+			item: inst.currentItem,
+			sender: _inst ? _inst.element : null
+		};
+	}
+
+});
+
+
+/*!
+ * jQuery UI Accordion 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/accordion/
+ */
+
+
+var accordion = $.widget( "ui.accordion", {
+	version: "1.11.2",
+	options: {
+		active: 0,
+		animate: {},
+		collapsible: false,
+		event: "click",
+		header: "> li > :first-child,> :not(li):even",
+		heightStyle: "auto",
+		icons: {
+			activeHeader: "ui-icon-triangle-1-s",
+			header: "ui-icon-triangle-1-e"
+		},
+
+		// callbacks
+		activate: null,
+		beforeActivate: null
+	},
+
+	hideProps: {
+		borderTopWidth: "hide",
+		borderBottomWidth: "hide",
+		paddingTop: "hide",
+		paddingBottom: "hide",
+		height: "hide"
+	},
+
+	showProps: {
+		borderTopWidth: "show",
+		borderBottomWidth: "show",
+		paddingTop: "show",
+		paddingBottom: "show",
+		height: "show"
+	},
+
+	_create: function() {
+		var options = this.options;
+		this.prevShow = this.prevHide = $();
+		this.element.addClass( "ui-accordion ui-widget ui-helper-reset" )
+			// ARIA
+			.attr( "role", "tablist" );
+
+		// don't allow collapsible: false and active: false / null
+		if ( !options.collapsible && (options.active === false || options.active == null) ) {
+			options.active = 0;
+		}
+
+		this._processPanels();
+		// handle negative values
+		if ( options.active < 0 ) {
+			options.active += this.headers.length;
+		}
+		this._refresh();
+	},
+
+	_getCreateEventData: function() {
+		return {
+			header: this.active,
+			panel: !this.active.length ? $() : this.active.next()
+		};
+	},
+
+	_createIcons: function() {
+		var icons = this.options.icons;
+		if ( icons ) {
+			$( "<span>" )
+				.addClass( "ui-accordion-header-icon ui-icon " + icons.header )
+				.prependTo( this.headers );
+			this.active.children( ".ui-accordion-header-icon" )
+				.removeClass( icons.header )
+				.addClass( icons.activeHeader );
+			this.headers.addClass( "ui-accordion-icons" );
+		}
+	},
+
+	_destroyIcons: function() {
+		this.headers
+			.removeClass( "ui-accordion-icons" )
+			.children( ".ui-accordion-header-icon" )
+				.remove();
+	},
+
+	_destroy: function() {
+		var contents;
+
+		// clean up main element
+		this.element
+			.removeClass( "ui-accordion ui-widget ui-helper-reset" )
+			.removeAttr( "role" );
+
+		// clean up headers
+		this.headers
+			.removeClass( "ui-accordion-header ui-accordion-header-active ui-state-default " +
+				"ui-corner-all ui-state-active ui-state-disabled ui-corner-top" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-expanded" )
+			.removeAttr( "aria-selected" )
+			.removeAttr( "aria-controls" )
+			.removeAttr( "tabIndex" )
+			.removeUniqueId();
+
+		this._destroyIcons();
+
+		// clean up content panels
+		contents = this.headers.next()
+			.removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom " +
+				"ui-accordion-content ui-accordion-content-active ui-state-disabled" )
+			.css( "display", "" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-hidden" )
+			.removeAttr( "aria-labelledby" )
+			.removeUniqueId();
+
+		if ( this.options.heightStyle !== "content" ) {
+			contents.css( "height", "" );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "active" ) {
+			// _activate() will handle invalid values and update this.options
+			this._activate( value );
+			return;
+		}
+
+		if ( key === "event" ) {
+			if ( this.options.event ) {
+				this._off( this.headers, this.options.event );
+			}
+			this._setupEvents( value );
+		}
+
+		this._super( key, value );
+
+		// setting collapsible: false while collapsed; open first panel
+		if ( key === "collapsible" && !value && this.options.active === false ) {
+			this._activate( 0 );
+		}
+
+		if ( key === "icons" ) {
+			this._destroyIcons();
+			if ( value ) {
+				this._createIcons();
+			}
+		}
+
+		// #5332 - opacity doesn't cascade to positioned elements in IE
+		// so we need to add the disabled class to the headers and panels
+		if ( key === "disabled" ) {
+			this.element
+				.toggleClass( "ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+			this.headers.add( this.headers.next() )
+				.toggleClass( "ui-state-disabled", !!value );
+		}
+	},
+
+	_keydown: function( event ) {
+		if ( event.altKey || event.ctrlKey ) {
+			return;
+		}
+
+		var keyCode = $.ui.keyCode,
+			length = this.headers.length,
+			currentIndex = this.headers.index( event.target ),
+			toFocus = false;
+
+		switch ( event.keyCode ) {
+			case keyCode.RIGHT:
+			case keyCode.DOWN:
+				toFocus = this.headers[ ( currentIndex + 1 ) % length ];
+				break;
+			case keyCode.LEFT:
+			case keyCode.UP:
+				toFocus = this.headers[ ( currentIndex - 1 + length ) % length ];
+				break;
+			case keyCode.SPACE:
+			case keyCode.ENTER:
+				this._eventHandler( event );
+				break;
+			case keyCode.HOME:
+				toFocus = this.headers[ 0 ];
+				break;
+			case keyCode.END:
+				toFocus = this.headers[ length - 1 ];
+				break;
+		}
+
+		if ( toFocus ) {
+			$( event.target ).attr( "tabIndex", -1 );
+			$( toFocus ).attr( "tabIndex", 0 );
+			toFocus.focus();
+			event.preventDefault();
+		}
+	},
+
+	_panelKeyDown: function( event ) {
+		if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) {
+			$( event.currentTarget ).prev().focus();
+		}
+	},
+
+	refresh: function() {
+		var options = this.options;
+		this._processPanels();
+
+		// was collapsed or no panel
+		if ( ( options.active === false && options.collapsible === true ) || !this.headers.length ) {
+			options.active = false;
+			this.active = $();
+		// active false only when collapsible is true
+		} else if ( options.active === false ) {
+			this._activate( 0 );
+		// was active, but active panel is gone
+		} else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
+			// all remaining panel are disabled
+			if ( this.headers.length === this.headers.find(".ui-state-disabled").length ) {
+				options.active = false;
+				this.active = $();
+			// activate previous panel
+			} else {
+				this._activate( Math.max( 0, options.active - 1 ) );
+			}
+		// was active, active panel still exists
+		} else {
+			// make sure active index is correct
+			options.active = this.headers.index( this.active );
+		}
+
+		this._destroyIcons();
+
+		this._refresh();
+	},
+
+	_processPanels: function() {
+		var prevHeaders = this.headers,
+			prevPanels = this.panels;
+
+		this.headers = this.element.find( this.options.header )
+			.addClass( "ui-accordion-header ui-state-default ui-corner-all" );
+
+		this.panels = this.headers.next()
+			.addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" )
+			.filter( ":not(.ui-accordion-content-active)" )
+			.hide();
+
+		// Avoid memory leaks (#10056)
+		if ( prevPanels ) {
+			this._off( prevHeaders.not( this.headers ) );
+			this._off( prevPanels.not( this.panels ) );
+		}
+	},
+
+	_refresh: function() {
+		var maxHeight,
+			options = this.options,
+			heightStyle = options.heightStyle,
+			parent = this.element.parent();
+
+		this.active = this._findActive( options.active )
+			.addClass( "ui-accordion-header-active ui-state-active ui-corner-top" )
+			.removeClass( "ui-corner-all" );
+		this.active.next()
+			.addClass( "ui-accordion-content-active" )
+			.show();
+
+		this.headers
+			.attr( "role", "tab" )
+			.each(function() {
+				var header = $( this ),
+					headerId = header.uniqueId().attr( "id" ),
+					panel = header.next(),
+					panelId = panel.uniqueId().attr( "id" );
+				header.attr( "aria-controls", panelId );
+				panel.attr( "aria-labelledby", headerId );
+			})
+			.next()
+				.attr( "role", "tabpanel" );
+
+		this.headers
+			.not( this.active )
+			.attr({
+				"aria-selected": "false",
+				"aria-expanded": "false",
+				tabIndex: -1
+			})
+			.next()
+				.attr({
+					"aria-hidden": "true"
+				})
+				.hide();
+
+		// make sure at least one header is in the tab order
+		if ( !this.active.length ) {
+			this.headers.eq( 0 ).attr( "tabIndex", 0 );
+		} else {
+			this.active.attr({
+				"aria-selected": "true",
+				"aria-expanded": "true",
+				tabIndex: 0
+			})
+			.next()
+				.attr({
+					"aria-hidden": "false"
+				});
+		}
+
+		this._createIcons();
+
+		this._setupEvents( options.event );
+
+		if ( heightStyle === "fill" ) {
+			maxHeight = parent.height();
+			this.element.siblings( ":visible" ).each(function() {
+				var elem = $( this ),
+					position = elem.css( "position" );
+
+				if ( position === "absolute" || position === "fixed" ) {
+					return;
+				}
+				maxHeight -= elem.outerHeight( true );
+			});
+
+			this.headers.each(function() {
+				maxHeight -= $( this ).outerHeight( true );
+			});
+
+			this.headers.next()
+				.each(function() {
+					$( this ).height( Math.max( 0, maxHeight -
+						$( this ).innerHeight() + $( this ).height() ) );
+				})
+				.css( "overflow", "auto" );
+		} else if ( heightStyle === "auto" ) {
+			maxHeight = 0;
+			this.headers.next()
+				.each(function() {
+					maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() );
+				})
+				.height( maxHeight );
+		}
+	},
+
+	_activate: function( index ) {
+		var active = this._findActive( index )[ 0 ];
+
+		// trying to activate the already active panel
+		if ( active === this.active[ 0 ] ) {
+			return;
+		}
+
+		// trying to collapse, simulate a click on the currently active header
+		active = active || this.active[ 0 ];
+
+		this._eventHandler({
+			target: active,
+			currentTarget: active,
+			preventDefault: $.noop
+		});
+	},
+
+	_findActive: function( selector ) {
+		return typeof selector === "number" ? this.headers.eq( selector ) : $();
+	},
+
+	_setupEvents: function( event ) {
+		var events = {
+			keydown: "_keydown"
+		};
+		if ( event ) {
+			$.each( event.split( " " ), function( index, eventName ) {
+				events[ eventName ] = "_eventHandler";
+			});
+		}
+
+		this._off( this.headers.add( this.headers.next() ) );
+		this._on( this.headers, events );
+		this._on( this.headers.next(), { keydown: "_panelKeyDown" });
+		this._hoverable( this.headers );
+		this._focusable( this.headers );
+	},
+
+	_eventHandler: function( event ) {
+		var options = this.options,
+			active = this.active,
+			clicked = $( event.currentTarget ),
+			clickedIsActive = clicked[ 0 ] === active[ 0 ],
+			collapsing = clickedIsActive && options.collapsible,
+			toShow = collapsing ? $() : clicked.next(),
+			toHide = active.next(),
+			eventData = {
+				oldHeader: active,
+				oldPanel: toHide,
+				newHeader: collapsing ? $() : clicked,
+				newPanel: toShow
+			};
+
+		event.preventDefault();
+
+		if (
+				// click on active header, but not collapsible
+				( clickedIsActive && !options.collapsible ) ||
+				// allow canceling activation
+				( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
+			return;
+		}
+
+		options.active = collapsing ? false : this.headers.index( clicked );
+
+		// when the call to ._toggle() comes after the class changes
+		// it causes a very odd bug in IE 8 (see #6720)
+		this.active = clickedIsActive ? $() : clicked;
+		this._toggle( eventData );
+
+		// switch classes
+		// corner classes on the previously active header stay after the animation
+		active.removeClass( "ui-accordion-header-active ui-state-active" );
+		if ( options.icons ) {
+			active.children( ".ui-accordion-header-icon" )
+				.removeClass( options.icons.activeHeader )
+				.addClass( options.icons.header );
+		}
+
+		if ( !clickedIsActive ) {
+			clicked
+				.removeClass( "ui-corner-all" )
+				.addClass( "ui-accordion-header-active ui-state-active ui-corner-top" );
+			if ( options.icons ) {
+				clicked.children( ".ui-accordion-header-icon" )
+					.removeClass( options.icons.header )
+					.addClass( options.icons.activeHeader );
+			}
+
+			clicked
+				.next()
+				.addClass( "ui-accordion-content-active" );
+		}
+	},
+
+	_toggle: function( data ) {
+		var toShow = data.newPanel,
+			toHide = this.prevShow.length ? this.prevShow : data.oldPanel;
+
+		// handle activating a panel during the animation for another activation
+		this.prevShow.add( this.prevHide ).stop( true, true );
+		this.prevShow = toShow;
+		this.prevHide = toHide;
+
+		if ( this.options.animate ) {
+			this._animate( toShow, toHide, data );
+		} else {
+			toHide.hide();
+			toShow.show();
+			this._toggleComplete( data );
+		}
+
+		toHide.attr({
+			"aria-hidden": "true"
+		});
+		toHide.prev().attr( "aria-selected", "false" );
+		// if we're switching panels, remove the old header from the tab order
+		// if we're opening from collapsed state, remove the previous header from the tab order
+		// if we're collapsing, then keep the collapsing header in the tab order
+		if ( toShow.length && toHide.length ) {
+			toHide.prev().attr({
+				"tabIndex": -1,
+				"aria-expanded": "false"
+			});
+		} else if ( toShow.length ) {
+			this.headers.filter(function() {
+				return $( this ).attr( "tabIndex" ) === 0;
+			})
+			.attr( "tabIndex", -1 );
+		}
+
+		toShow
+			.attr( "aria-hidden", "false" )
+			.prev()
+				.attr({
+					"aria-selected": "true",
+					tabIndex: 0,
+					"aria-expanded": "true"
+				});
+	},
+
+	_animate: function( toShow, toHide, data ) {
+		var total, easing, duration,
+			that = this,
+			adjust = 0,
+			down = toShow.length &&
+				( !toHide.length || ( toShow.index() < toHide.index() ) ),
+			animate = this.options.animate || {},
+			options = down && animate.down || animate,
+			complete = function() {
+				that._toggleComplete( data );
+			};
+
+		if ( typeof options === "number" ) {
+			duration = options;
+		}
+		if ( typeof options === "string" ) {
+			easing = options;
+		}
+		// fall back from options to animation in case of partial down settings
+		easing = easing || options.easing || animate.easing;
+		duration = duration || options.duration || animate.duration;
+
+		if ( !toHide.length ) {
+			return toShow.animate( this.showProps, duration, easing, complete );
+		}
+		if ( !toShow.length ) {
+			return toHide.animate( this.hideProps, duration, easing, complete );
+		}
+
+		total = toShow.show().outerHeight();
+		toHide.animate( this.hideProps, {
+			duration: duration,
+			easing: easing,
+			step: function( now, fx ) {
+				fx.now = Math.round( now );
+			}
+		});
+		toShow
+			.hide()
+			.animate( this.showProps, {
+				duration: duration,
+				easing: easing,
+				complete: complete,
+				step: function( now, fx ) {
+					fx.now = Math.round( now );
+					if ( fx.prop !== "height" ) {
+						adjust += fx.now;
+					} else if ( that.options.heightStyle !== "content" ) {
+						fx.now = Math.round( total - toHide.outerHeight() - adjust );
+						adjust = 0;
+					}
+				}
+			});
+	},
+
+	_toggleComplete: function( data ) {
+		var toHide = data.oldPanel;
+
+		toHide
+			.removeClass( "ui-accordion-content-active" )
+			.prev()
+				.removeClass( "ui-corner-top" )
+				.addClass( "ui-corner-all" );
+
+		// Work around for rendering bug in IE (#5421)
+		if ( toHide.length ) {
+			toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className;
+		}
+		this._trigger( "activate", null, data );
+	}
+});
+
+
+/*!
+ * jQuery UI Menu 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/menu/
+ */
+
+
+var menu = $.widget( "ui.menu", {
+	version: "1.11.2",
+	defaultElement: "<ul>",
+	delay: 300,
+	options: {
+		icons: {
+			submenu: "ui-icon-carat-1-e"
+		},
+		items: "> *",
+		menus: "ul",
+		position: {
+			my: "left-1 top",
+			at: "right top"
+		},
+		role: "menu",
+
+		// callbacks
+		blur: null,
+		focus: null,
+		select: null
+	},
+
+	_create: function() {
+		this.activeMenu = this.element;
+
+		// Flag used to prevent firing of the click handler
+		// as the event bubbles up through nested menus
+		this.mouseHandled = false;
+		this.element
+			.uniqueId()
+			.addClass( "ui-menu ui-widget ui-widget-content" )
+			.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
+			.attr({
+				role: this.options.role,
+				tabIndex: 0
+			});
+
+		if ( this.options.disabled ) {
+			this.element
+				.addClass( "ui-state-disabled" )
+				.attr( "aria-disabled", "true" );
+		}
+
+		this._on({
+			// Prevent focus from sticking to links inside menu after clicking
+			// them (focus should always stay on UL during navigation).
+			"mousedown .ui-menu-item": function( event ) {
+				event.preventDefault();
+			},
+			"click .ui-menu-item": function( event ) {
+				var target = $( event.target );
+				if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
+					this.select( event );
+
+					// Only set the mouseHandled flag if the event will bubble, see #9469.
+					if ( !event.isPropagationStopped() ) {
+						this.mouseHandled = true;
+					}
+
+					// Open submenu on click
+					if ( target.has( ".ui-menu" ).length ) {
+						this.expand( event );
+					} else if ( !this.element.is( ":focus" ) && $( this.document[ 0 ].activeElement ).closest( ".ui-menu" ).length ) {
+
+						// Redirect focus to the menu
+						this.element.trigger( "focus", [ true ] );
+
+						// If the active item is on the top level, let it stay active.
+						// Otherwise, blur the active item since it is no longer visible.
+						if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
+							clearTimeout( this.timer );
+						}
+					}
+				}
+			},
+			"mouseenter .ui-menu-item": function( event ) {
+				// Ignore mouse events while typeahead is active, see #10458.
+				// Prevents focusing the wrong item when typeahead causes a scroll while the mouse
+				// is over an item in the menu
+				if ( this.previousFilter ) {
+					return;
+				}
+				var target = $( event.currentTarget );
+				// Remove ui-state-active class from siblings of the newly focused menu item
+				// to avoid a jump caused by adjacent elements both having a class with a border
+				target.siblings( ".ui-state-active" ).removeClass( "ui-state-active" );
+				this.focus( event, target );
+			},
+			mouseleave: "collapseAll",
+			"mouseleave .ui-menu": "collapseAll",
+			focus: function( event, keepActiveItem ) {
+				// If there's already an active item, keep it active
+				// If not, activate the first item
+				var item = this.active || this.element.find( this.options.items ).eq( 0 );
+
+				if ( !keepActiveItem ) {
+					this.focus( event, item );
+				}
+			},
+			blur: function( event ) {
+				this._delay(function() {
+					if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
+						this.collapseAll( event );
+					}
+				});
+			},
+			keydown: "_keydown"
+		});
+
+		this.refresh();
+
+		// Clicks outside of a menu collapse any open menus
+		this._on( this.document, {
+			click: function( event ) {
+				if ( this._closeOnDocumentClick( event ) ) {
+					this.collapseAll( event );
+				}
+
+				// Reset the mouseHandled flag
+				this.mouseHandled = false;
+			}
+		});
+	},
+
+	_destroy: function() {
+		// Destroy (sub)menus
+		this.element
+			.removeAttr( "aria-activedescendant" )
+			.find( ".ui-menu" ).addBack()
+				.removeClass( "ui-menu ui-widget ui-widget-content ui-menu-icons ui-front" )
+				.removeAttr( "role" )
+				.removeAttr( "tabIndex" )
+				.removeAttr( "aria-labelledby" )
+				.removeAttr( "aria-expanded" )
+				.removeAttr( "aria-hidden" )
+				.removeAttr( "aria-disabled" )
+				.removeUniqueId()
+				.show();
+
+		// Destroy menu items
+		this.element.find( ".ui-menu-item" )
+			.removeClass( "ui-menu-item" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-disabled" )
+			.removeUniqueId()
+			.removeClass( "ui-state-hover" )
+			.removeAttr( "tabIndex" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-haspopup" )
+			.children().each( function() {
+				var elem = $( this );
+				if ( elem.data( "ui-menu-submenu-carat" ) ) {
+					elem.remove();
+				}
+			});
+
+		// Destroy menu dividers
+		this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
+	},
+
+	_keydown: function( event ) {
+		var match, prev, character, skip,
+			preventDefault = true;
+
+		switch ( event.keyCode ) {
+		case $.ui.keyCode.PAGE_UP:
+			this.previousPage( event );
+			break;
+		case $.ui.keyCode.PAGE_DOWN:
+			this.nextPage( event );
+			break;
+		case $.ui.keyCode.HOME:
+			this._move( "first", "first", event );
+			break;
+		case $.ui.keyCode.END:
+			this._move( "last", "last", event );
+			break;
+		case $.ui.keyCode.UP:
+			this.previous( event );
+			break;
+		case $.ui.keyCode.DOWN:
+			this.next( event );
+			break;
+		case $.ui.keyCode.LEFT:
+			this.collapse( event );
+			break;
+		case $.ui.keyCode.RIGHT:
+			if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
+				this.expand( event );
+			}
+			break;
+		case $.ui.keyCode.ENTER:
+		case $.ui.keyCode.SPACE:
+			this._activate( event );
+			break;
+		case $.ui.keyCode.ESCAPE:
+			this.collapse( event );
+			break;
+		default:
+			preventDefault = false;
+			prev = this.previousFilter || "";
+			character = String.fromCharCode( event.keyCode );
+			skip = false;
+
+			clearTimeout( this.filterTimer );
+
+			if ( character === prev ) {
+				skip = true;
+			} else {
+				character = prev + character;
+			}
+
+			match = this._filterMenuItems( character );
+			match = skip && match.index( this.active.next() ) !== -1 ?
+				this.active.nextAll( ".ui-menu-item" ) :
+				match;
+
+			// If no matches on the current filter, reset to the last character pressed
+			// to move down the menu to the first item that starts with that character
+			if ( !match.length ) {
+				character = String.fromCharCode( event.keyCode );
+				match = this._filterMenuItems( character );
+			}
+
+			if ( match.length ) {
+				this.focus( event, match );
+				this.previousFilter = character;
+				this.filterTimer = this._delay(function() {
+					delete this.previousFilter;
+				}, 1000 );
+			} else {
+				delete this.previousFilter;
+			}
+		}
+
+		if ( preventDefault ) {
+			event.preventDefault();
+		}
+	},
+
+	_activate: function( event ) {
+		if ( !this.active.is( ".ui-state-disabled" ) ) {
+			if ( this.active.is( "[aria-haspopup='true']" ) ) {
+				this.expand( event );
+			} else {
+				this.select( event );
+			}
+		}
+	},
+
+	refresh: function() {
+		var menus, items,
+			that = this,
+			icon = this.options.icons.submenu,
+			submenus = this.element.find( this.options.menus );
+
+		this.element.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length );
+
+		// Initialize nested menus
+		submenus.filter( ":not(.ui-menu)" )
+			.addClass( "ui-menu ui-widget ui-widget-content ui-front" )
+			.hide()
+			.attr({
+				role: this.options.role,
+				"aria-hidden": "true",
+				"aria-expanded": "false"
+			})
+			.each(function() {
+				var menu = $( this ),
+					item = menu.parent(),
+					submenuCarat = $( "<span>" )
+						.addClass( "ui-menu-icon ui-icon " + icon )
+						.data( "ui-menu-submenu-carat", true );
+
+				item
+					.attr( "aria-haspopup", "true" )
+					.prepend( submenuCarat );
+				menu.attr( "aria-labelledby", item.attr( "id" ) );
+			});
+
+		menus = submenus.add( this.element );
+		items = menus.find( this.options.items );
+
+		// Initialize menu-items containing spaces and/or dashes only as dividers
+		items.not( ".ui-menu-item" ).each(function() {
+			var item = $( this );
+			if ( that._isDivider( item ) ) {
+				item.addClass( "ui-widget-content ui-menu-divider" );
+			}
+		});
+
+		// Don't refresh list items that are already adapted
+		items.not( ".ui-menu-item, .ui-menu-divider" )
+			.addClass( "ui-menu-item" )
+			.uniqueId()
+			.attr({
+				tabIndex: -1,
+				role: this._itemRole()
+			});
+
+		// Add aria-disabled attribute to any disabled menu item
+		items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
+
+		// If the active item has been removed, blur the menu
+		if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
+			this.blur();
+		}
+	},
+
+	_itemRole: function() {
+		return {
+			menu: "menuitem",
+			listbox: "option"
+		}[ this.options.role ];
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "icons" ) {
+			this.element.find( ".ui-menu-icon" )
+				.removeClass( this.options.icons.submenu )
+				.addClass( value.submenu );
+		}
+		if ( key === "disabled" ) {
+			this.element
+				.toggleClass( "ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+		}
+		this._super( key, value );
+	},
+
+	focus: function( event, item ) {
+		var nested, focused;
+		this.blur( event, event && event.type === "focus" );
+
+		this._scrollIntoView( item );
+
+		this.active = item.first();
+		focused = this.active.addClass( "ui-state-focus" ).removeClass( "ui-state-active" );
+		// Only update aria-activedescendant if there's a role
+		// otherwise we assume focus is managed elsewhere
+		if ( this.options.role ) {
+			this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
+		}
+
+		// Highlight active parent menu item, if any
+		this.active
+			.parent()
+			.closest( ".ui-menu-item" )
+			.addClass( "ui-state-active" );
+
+		if ( event && event.type === "keydown" ) {
+			this._close();
+		} else {
+			this.timer = this._delay(function() {
+				this._close();
+			}, this.delay );
+		}
+
+		nested = item.children( ".ui-menu" );
+		if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
+			this._startOpening(nested);
+		}
+		this.activeMenu = item.parent();
+
+		this._trigger( "focus", event, { item: item } );
+	},
+
+	_scrollIntoView: function( item ) {
+		var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
+		if ( this._hasScroll() ) {
+			borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
+			paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
+			offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
+			scroll = this.activeMenu.scrollTop();
+			elementHeight = this.activeMenu.height();
+			itemHeight = item.outerHeight();
+
+			if ( offset < 0 ) {
+				this.activeMenu.scrollTop( scroll + offset );
+			} else if ( offset + itemHeight > elementHeight ) {
+				this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
+			}
+		}
+	},
+
+	blur: function( event, fromFocus ) {
+		if ( !fromFocus ) {
+			clearTimeout( this.timer );
+		}
+
+		if ( !this.active ) {
+			return;
+		}
+
+		this.active.removeClass( "ui-state-focus" );
+		this.active = null;
+
+		this._trigger( "blur", event, { item: this.active } );
+	},
+
+	_startOpening: function( submenu ) {
+		clearTimeout( this.timer );
+
+		// Don't open if already open fixes a Firefox bug that caused a .5 pixel
+		// shift in the submenu position when mousing over the carat icon
+		if ( submenu.attr( "aria-hidden" ) !== "true" ) {
+			return;
+		}
+
+		this.timer = this._delay(function() {
+			this._close();
+			this._open( submenu );
+		}, this.delay );
+	},
+
+	_open: function( submenu ) {
+		var position = $.extend({
+			of: this.active
+		}, this.options.position );
+
+		clearTimeout( this.timer );
+		this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
+			.hide()
+			.attr( "aria-hidden", "true" );
+
+		submenu
+			.show()
+			.removeAttr( "aria-hidden" )
+			.attr( "aria-expanded", "true" )
+			.position( position );
+	},
+
+	collapseAll: function( event, all ) {
+		clearTimeout( this.timer );
+		this.timer = this._delay(function() {
+			// If we were passed an event, look for the submenu that contains the event
+			var currentMenu = all ? this.element :
+				$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
+
+			// If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
+			if ( !currentMenu.length ) {
+				currentMenu = this.element;
+			}
+
+			this._close( currentMenu );
+
+			this.blur( event );
+			this.activeMenu = currentMenu;
+		}, this.delay );
+	},
+
+	// With no arguments, closes the currently active menu - if nothing is active
+	// it closes all menus.  If passed an argument, it will search for menus BELOW
+	_close: function( startMenu ) {
+		if ( !startMenu ) {
+			startMenu = this.active ? this.active.parent() : this.element;
+		}
+
+		startMenu
+			.find( ".ui-menu" )
+				.hide()
+				.attr( "aria-hidden", "true" )
+				.attr( "aria-expanded", "false" )
+			.end()
+			.find( ".ui-state-active" ).not( ".ui-state-focus" )
+				.removeClass( "ui-state-active" );
+	},
+
+	_closeOnDocumentClick: function( event ) {
+		return !$( event.target ).closest( ".ui-menu" ).length;
+	},
+
+	_isDivider: function( item ) {
+
+		// Match hyphen, em dash, en dash
+		return !/[^\-\u2014\u2013\s]/.test( item.text() );
+	},
+
+	collapse: function( event ) {
+		var newItem = this.active &&
+			this.active.parent().closest( ".ui-menu-item", this.element );
+		if ( newItem && newItem.length ) {
+			this._close();
+			this.focus( event, newItem );
+		}
+	},
+
+	expand: function( event ) {
+		var newItem = this.active &&
+			this.active
+				.children( ".ui-menu " )
+				.find( this.options.items )
+				.first();
+
+		if ( newItem && newItem.length ) {
+			this._open( newItem.parent() );
+
+			// Delay so Firefox will not hide activedescendant change in expanding submenu from AT
+			this._delay(function() {
+				this.focus( event, newItem );
+			});
+		}
+	},
+
+	next: function( event ) {
+		this._move( "next", "first", event );
+	},
+
+	previous: function( event ) {
+		this._move( "prev", "last", event );
+	},
+
+	isFirstItem: function() {
+		return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
+	},
+
+	isLastItem: function() {
+		return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
+	},
+
+	_move: function( direction, filter, event ) {
+		var next;
+		if ( this.active ) {
+			if ( direction === "first" || direction === "last" ) {
+				next = this.active
+					[ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
+					.eq( -1 );
+			} else {
+				next = this.active
+					[ direction + "All" ]( ".ui-menu-item" )
+					.eq( 0 );
+			}
+		}
+		if ( !next || !next.length || !this.active ) {
+			next = this.activeMenu.find( this.options.items )[ filter ]();
+		}
+
+		this.focus( event, next );
+	},
+
+	nextPage: function( event ) {
+		var item, base, height;
+
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isLastItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.nextAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base - height < 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.find( this.options.items )
+				[ !this.active ? "first" : "last" ]() );
+		}
+	},
+
+	previousPage: function( event ) {
+		var item, base, height;
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isFirstItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.prevAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base + height > 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.find( this.options.items ).first() );
+		}
+	},
+
+	_hasScroll: function() {
+		return this.element.outerHeight() < this.element.prop( "scrollHeight" );
+	},
+
+	select: function( event ) {
+		// TODO: It should never be possible to not have an active item at this
+		// point, but the tests don't trigger mouseenter before click.
+		this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
+		var ui = { item: this.active };
+		if ( !this.active.has( ".ui-menu" ).length ) {
+			this.collapseAll( event, true );
+		}
+		this._trigger( "select", event, ui );
+	},
+
+	_filterMenuItems: function(character) {
+		var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ),
+			regex = new RegExp( "^" + escapedCharacter, "i" );
+
+		return this.activeMenu
+			.find( this.options.items )
+
+			// Only match on items, not dividers or other content (#10571)
+			.filter( ".ui-menu-item" )
+			.filter(function() {
+				return regex.test( $.trim( $( this ).text() ) );
+			});
+	}
+});
+
+
+/*!
+ * jQuery UI Autocomplete 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/autocomplete/
+ */
+
+
+$.widget( "ui.autocomplete", {
+	version: "1.11.2",
+	defaultElement: "<input>",
+	options: {
+		appendTo: null,
+		autoFocus: false,
+		delay: 300,
+		minLength: 1,
+		position: {
+			my: "left top",
+			at: "left bottom",
+			collision: "none"
+		},
+		source: null,
+
+		// callbacks
+		change: null,
+		close: null,
+		focus: null,
+		open: null,
+		response: null,
+		search: null,
+		select: null
+	},
+
+	requestIndex: 0,
+	pending: 0,
+
+	_create: function() {
+		// Some browsers only repeat keydown events, not keypress events,
+		// so we use the suppressKeyPress flag to determine if we've already
+		// handled the keydown event. #7269
+		// Unfortunately the code for & in keypress is the same as the up arrow,
+		// so we use the suppressKeyPressRepeat flag to avoid handling keypress
+		// events when we know the keydown event was used to modify the
+		// search term. #7799
+		var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
+			nodeName = this.element[ 0 ].nodeName.toLowerCase(),
+			isTextarea = nodeName === "textarea",
+			isInput = nodeName === "input";
+
+		this.isMultiLine =
+			// Textareas are always multi-line
+			isTextarea ? true :
+			// Inputs are always single-line, even if inside a contentEditable element
+			// IE also treats inputs as contentEditable
+			isInput ? false :
+			// All other element types are determined by whether or not they're contentEditable
+			this.element.prop( "isContentEditable" );
+
+		this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
+		this.isNewMenu = true;
+
+		this.element
+			.addClass( "ui-autocomplete-input" )
+			.attr( "autocomplete", "off" );
+
+		this._on( this.element, {
+			keydown: function( event ) {
+				if ( this.element.prop( "readOnly" ) ) {
+					suppressKeyPress = true;
+					suppressInput = true;
+					suppressKeyPressRepeat = true;
+					return;
+				}
+
+				suppressKeyPress = false;
+				suppressInput = false;
+				suppressKeyPressRepeat = false;
+				var keyCode = $.ui.keyCode;
+				switch ( event.keyCode ) {
+				case keyCode.PAGE_UP:
+					suppressKeyPress = true;
+					this._move( "previousPage", event );
+					break;
+				case keyCode.PAGE_DOWN:
+					suppressKeyPress = true;
+					this._move( "nextPage", event );
+					break;
+				case keyCode.UP:
+					suppressKeyPress = true;
+					this._keyEvent( "previous", event );
+					break;
+				case keyCode.DOWN:
+					suppressKeyPress = true;
+					this._keyEvent( "next", event );
+					break;
+				case keyCode.ENTER:
+					// when menu is open and has focus
+					if ( this.menu.active ) {
+						// #6055 - Opera still allows the keypress to occur
+						// which causes forms to submit
+						suppressKeyPress = true;
+						event.preventDefault();
+						this.menu.select( event );
+					}
+					break;
+				case keyCode.TAB:
+					if ( this.menu.active ) {
+						this.menu.select( event );
+					}
+					break;
+				case keyCode.ESCAPE:
+					if ( this.menu.element.is( ":visible" ) ) {
+						if ( !this.isMultiLine ) {
+							this._value( this.term );
+						}
+						this.close( event );
+						// Different browsers have different default behavior for escape
+						// Single press can mean undo or clear
+						// Double press in IE means clear the whole form
+						event.preventDefault();
+					}
+					break;
+				default:
+					suppressKeyPressRepeat = true;
+					// search timeout should be triggered before the input value is changed
+					this._searchTimeout( event );
+					break;
+				}
+			},
+			keypress: function( event ) {
+				if ( suppressKeyPress ) {
+					suppressKeyPress = false;
+					if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
+						event.preventDefault();
+					}
+					return;
+				}
+				if ( suppressKeyPressRepeat ) {
+					return;
+				}
+
+				// replicate some key handlers to allow them to repeat in Firefox and Opera
+				var keyCode = $.ui.keyCode;
+				switch ( event.keyCode ) {
+				case keyCode.PAGE_UP:
+					this._move( "previousPage", event );
+					break;
+				case keyCode.PAGE_DOWN:
+					this._move( "nextPage", event );
+					break;
+				case keyCode.UP:
+					this._keyEvent( "previous", event );
+					break;
+				case keyCode.DOWN:
+					this._keyEvent( "next", event );
+					break;
+				}
+			},
+			input: function( event ) {
+				if ( suppressInput ) {
+					suppressInput = false;
+					event.preventDefault();
+					return;
+				}
+				this._searchTimeout( event );
+			},
+			focus: function() {
+				this.selectedItem = null;
+				this.previous = this._value();
+			},
+			blur: function( event ) {
+				if ( this.cancelBlur ) {
+					delete this.cancelBlur;
+					return;
+				}
+
+				clearTimeout( this.searching );
+				this.close( event );
+				this._change( event );
+			}
+		});
+
+		this._initSource();
+		this.menu = $( "<ul>" )
+			.addClass( "ui-autocomplete ui-front" )
+			.appendTo( this._appendTo() )
+			.menu({
+				// disable ARIA support, the live region takes care of that
+				role: null
+			})
+			.hide()
+			.menu( "instance" );
+
+		this._on( this.menu.element, {
+			mousedown: function( event ) {
+				// prevent moving focus out of the text field
+				event.preventDefault();
+
+				// IE doesn't prevent moving focus even with event.preventDefault()
+				// so we set a flag to know when we should ignore the blur event
+				this.cancelBlur = true;
+				this._delay(function() {
+					delete this.cancelBlur;
+				});
+
+				// clicking on the scrollbar causes focus to shift to the body
+				// but we can't detect a mouseup or a click immediately afterward
+				// so we have to track the next mousedown and close the menu if
+				// the user clicks somewhere outside of the autocomplete
+				var menuElement = this.menu.element[ 0 ];
+				if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
+					this._delay(function() {
+						var that = this;
+						this.document.one( "mousedown", function( event ) {
+							if ( event.target !== that.element[ 0 ] &&
+									event.target !== menuElement &&
+									!$.contains( menuElement, event.target ) ) {
+								that.close();
+							}
+						});
+					});
+				}
+			},
+			menufocus: function( event, ui ) {
+				var label, item;
+				// support: Firefox
+				// Prevent accidental activation of menu items in Firefox (#7024 #9118)
+				if ( this.isNewMenu ) {
+					this.isNewMenu = false;
+					if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
+						this.menu.blur();
+
+						this.document.one( "mousemove", function() {
+							$( event.target ).trigger( event.originalEvent );
+						});
+
+						return;
+					}
+				}
+
+				item = ui.item.data( "ui-autocomplete-item" );
+				if ( false !== this._trigger( "focus", event, { item: item } ) ) {
+					// use value to match what will end up in the input, if it was a key event
+					if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
+						this._value( item.value );
+					}
+				}
+
+				// Announce the value in the liveRegion
+				label = ui.item.attr( "aria-label" ) || item.value;
+				if ( label && $.trim( label ).length ) {
+					this.liveRegion.children().hide();
+					$( "<div>" ).text( label ).appendTo( this.liveRegion );
+				}
+			},
+			menuselect: function( event, ui ) {
+				var item = ui.item.data( "ui-autocomplete-item" ),
+					previous = this.previous;
+
+				// only trigger when focus was lost (click on menu)
+				if ( this.element[ 0 ] !== this.document[ 0 ].activeElement ) {
+					this.element.focus();
+					this.previous = previous;
+					// #6109 - IE triggers two focus events and the second
+					// is asynchronous, so we need to reset the previous
+					// term synchronously and asynchronously :-(
+					this._delay(function() {
+						this.previous = previous;
+						this.selectedItem = item;
+					});
+				}
+
+				if ( false !== this._trigger( "select", event, { item: item } ) ) {
+					this._value( item.value );
+				}
+				// reset the term after the select event
+				// this allows custom select handling to work properly
+				this.term = this._value();
+
+				this.close( event );
+				this.selectedItem = item;
+			}
+		});
+
+		this.liveRegion = $( "<span>", {
+				role: "status",
+				"aria-live": "assertive",
+				"aria-relevant": "additions"
+			})
+			.addClass( "ui-helper-hidden-accessible" )
+			.appendTo( this.document[ 0 ].body );
+
+		// turning off autocomplete prevents the browser from remembering the
+		// value when navigating through history, so we re-enable autocomplete
+		// if the page is unloaded before the widget is destroyed. #7790
+		this._on( this.window, {
+			beforeunload: function() {
+				this.element.removeAttr( "autocomplete" );
+			}
+		});
+	},
+
+	_destroy: function() {
+		clearTimeout( this.searching );
+		this.element
+			.removeClass( "ui-autocomplete-input" )
+			.removeAttr( "autocomplete" );
+		this.menu.element.remove();
+		this.liveRegion.remove();
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+		if ( key === "source" ) {
+			this._initSource();
+		}
+		if ( key === "appendTo" ) {
+			this.menu.element.appendTo( this._appendTo() );
+		}
+		if ( key === "disabled" && value && this.xhr ) {
+			this.xhr.abort();
+		}
+	},
+
+	_appendTo: function() {
+		var element = this.options.appendTo;
+
+		if ( element ) {
+			element = element.jquery || element.nodeType ?
+				$( element ) :
+				this.document.find( element ).eq( 0 );
+		}
+
+		if ( !element || !element[ 0 ] ) {
+			element = this.element.closest( ".ui-front" );
+		}
+
+		if ( !element.length ) {
+			element = this.document[ 0 ].body;
+		}
+
+		return element;
+	},
+
+	_initSource: function() {
+		var array, url,
+			that = this;
+		if ( $.isArray( this.options.source ) ) {
+			array = this.options.source;
+			this.source = function( request, response ) {
+				response( $.ui.autocomplete.filter( array, request.term ) );
+			};
+		} else if ( typeof this.options.source === "string" ) {
+			url = this.options.source;
+			this.source = function( request, response ) {
+				if ( that.xhr ) {
+					that.xhr.abort();
+				}
+				that.xhr = $.ajax({
+					url: url,
+					data: request,
+					dataType: "json",
+					success: function( data ) {
+						response( data );
+					},
+					error: function() {
+						response([]);
+					}
+				});
+			};
+		} else {
+			this.source = this.options.source;
+		}
+	},
+
+	_searchTimeout: function( event ) {
+		clearTimeout( this.searching );
+		this.searching = this._delay(function() {
+
+			// Search if the value has changed, or if the user retypes the same value (see #7434)
+			var equalValues = this.term === this._value(),
+				menuVisible = this.menu.element.is( ":visible" ),
+				modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
+
+			if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
+				this.selectedItem = null;
+				this.search( null, event );
+			}
+		}, this.options.delay );
+	},
+
+	search: function( value, event ) {
+		value = value != null ? value : this._value();
+
+		// always save the actual value, not the one passed as an argument
+		this.term = this._value();
+
+		if ( value.length < this.options.minLength ) {
+			return this.close( event );
+		}
+
+		if ( this._trigger( "search", event ) === false ) {
+			return;
+		}
+
+		return this._search( value );
+	},
+
+	_search: function( value ) {
+		this.pending++;
+		this.element.addClass( "ui-autocomplete-loading" );
+		this.cancelSearch = false;
+
+		this.source( { term: value }, this._response() );
+	},
+
+	_response: function() {
+		var index = ++this.requestIndex;
+
+		return $.proxy(function( content ) {
+			if ( index === this.requestIndex ) {
+				this.__response( content );
+			}
+
+			this.pending--;
+			if ( !this.pending ) {
+				this.element.removeClass( "ui-autocomplete-loading" );
+			}
+		}, this );
+	},
+
+	__response: function( content ) {
+		if ( content ) {
+			content = this._normalize( content );
+		}
+		this._trigger( "response", null, { content: content } );
+		if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
+			this._suggest( content );
+			this._trigger( "open" );
+		} else {
+			// use ._close() instead of .close() so we don't cancel future searches
+			this._close();
+		}
+	},
+
+	close: function( event ) {
+		this.cancelSearch = true;
+		this._close( event );
+	},
+
+	_close: function( event ) {
+		if ( this.menu.element.is( ":visible" ) ) {
+			this.menu.element.hide();
+			this.menu.blur();
+			this.isNewMenu = true;
+			this._trigger( "close", event );
+		}
+	},
+
+	_change: function( event ) {
+		if ( this.previous !== this._value() ) {
+			this._trigger( "change", event, { item: this.selectedItem } );
+		}
+	},
+
+	_normalize: function( items ) {
+		// assume all items have the right format when the first item is complete
+		if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
+			return items;
+		}
+		return $.map( items, function( item ) {
+			if ( typeof item === "string" ) {
+				return {
+					label: item,
+					value: item
+				};
+			}
+			return $.extend( {}, item, {
+				label: item.label || item.value,
+				value: item.value || item.label
+			});
+		});
+	},
+
+	_suggest: function( items ) {
+		var ul = this.menu.element.empty();
+		this._renderMenu( ul, items );
+		this.isNewMenu = true;
+		this.menu.refresh();
+
+		// size and position menu
+		ul.show();
+		this._resizeMenu();
+		ul.position( $.extend({
+			of: this.element
+		}, this.options.position ) );
+
+		if ( this.options.autoFocus ) {
+			this.menu.next();
+		}
+	},
+
+	_resizeMenu: function() {
+		var ul = this.menu.element;
+		ul.outerWidth( Math.max(
+			// Firefox wraps long text (possibly a rounding bug)
+			// so we add 1px to avoid the wrapping (#7513)
+			ul.width( "" ).outerWidth() + 1,
+			this.element.outerWidth()
+		) );
+	},
+
+	_renderMenu: function( ul, items ) {
+		var that = this;
+		$.each( items, function( index, item ) {
+			that._renderItemData( ul, item );
+		});
+	},
+
+	_renderItemData: function( ul, item ) {
+		return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
+	},
+
+	_renderItem: function( ul, item ) {
+		return $( "<li>" ).text( item.label ).appendTo( ul );
+	},
+
+	_move: function( direction, event ) {
+		if ( !this.menu.element.is( ":visible" ) ) {
+			this.search( null, event );
+			return;
+		}
+		if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
+				this.menu.isLastItem() && /^next/.test( direction ) ) {
+
+			if ( !this.isMultiLine ) {
+				this._value( this.term );
+			}
+
+			this.menu.blur();
+			return;
+		}
+		this.menu[ direction ]( event );
+	},
+
+	widget: function() {
+		return this.menu.element;
+	},
+
+	_value: function() {
+		return this.valueMethod.apply( this.element, arguments );
+	},
+
+	_keyEvent: function( keyEvent, event ) {
+		if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
+			this._move( keyEvent, event );
+
+			// prevents moving cursor to beginning/end of the text field in some browsers
+			event.preventDefault();
+		}
+	}
+});
+
+$.extend( $.ui.autocomplete, {
+	escapeRegex: function( value ) {
+		return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
+	},
+	filter: function( array, term ) {
+		var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
+		return $.grep( array, function( value ) {
+			return matcher.test( value.label || value.value || value );
+		});
+	}
+});
+
+// live region extension, adding a `messages` option
+// NOTE: This is an experimental API. We are still investigating
+// a full solution for string manipulation and internationalization.
+$.widget( "ui.autocomplete", $.ui.autocomplete, {
+	options: {
+		messages: {
+			noResults: "No search results.",
+			results: function( amount ) {
+				return amount + ( amount > 1 ? " results are" : " result is" ) +
+					" available, use up and down arrow keys to navigate.";
+			}
+		}
+	},
+
+	__response: function( content ) {
+		var message;
+		this._superApply( arguments );
+		if ( this.options.disabled || this.cancelSearch ) {
+			return;
+		}
+		if ( content && content.length ) {
+			message = this.options.messages.results( content.length );
+		} else {
+			message = this.options.messages.noResults;
+		}
+		this.liveRegion.children().hide();
+		$( "<div>" ).text( message ).appendTo( this.liveRegion );
+	}
+});
+
+var autocomplete = $.ui.autocomplete;
+
+
+/*!
+ * jQuery UI Button 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/button/
+ */
+
+
+var lastActive,
+	baseClasses = "ui-button ui-widget ui-state-default ui-corner-all",
+	typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",
+	formResetHandler = function() {
+		var form = $( this );
+		setTimeout(function() {
+			form.find( ":ui-button" ).button( "refresh" );
+		}, 1 );
+	},
+	radioGroup = function( radio ) {
+		var name = radio.name,
+			form = radio.form,
+			radios = $( [] );
+		if ( name ) {
+			name = name.replace( /'/g, "\\'" );
+			if ( form ) {
+				radios = $( form ).find( "[name='" + name + "'][type=radio]" );
+			} else {
+				radios = $( "[name='" + name + "'][type=radio]", radio.ownerDocument )
+					.filter(function() {
+						return !this.form;
+					});
+			}
+		}
+		return radios;
+	};
+
+$.widget( "ui.button", {
+	version: "1.11.2",
+	defaultElement: "<button>",
+	options: {
+		disabled: null,
+		text: true,
+		label: null,
+		icons: {
+			primary: null,
+			secondary: null
+		}
+	},
+	_create: function() {
+		this.element.closest( "form" )
+			.unbind( "reset" + this.eventNamespace )
+			.bind( "reset" + this.eventNamespace, formResetHandler );
+
+		if ( typeof this.options.disabled !== "boolean" ) {
+			this.options.disabled = !!this.element.prop( "disabled" );
+		} else {
+			this.element.prop( "disabled", this.options.disabled );
+		}
+
+		this._determineButtonType();
+		this.hasTitle = !!this.buttonElement.attr( "title" );
+
+		var that = this,
+			options = this.options,
+			toggleButton = this.type === "checkbox" || this.type === "radio",
+			activeClass = !toggleButton ? "ui-state-active" : "";
+
+		if ( options.label === null ) {
+			options.label = (this.type === "input" ? this.buttonElement.val() : this.buttonElement.html());
+		}
+
+		this._hoverable( this.buttonElement );
+
+		this.buttonElement
+			.addClass( baseClasses )
+			.attr( "role", "button" )
+			.bind( "mouseenter" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				if ( this === lastActive ) {
+					$( this ).addClass( "ui-state-active" );
+				}
+			})
+			.bind( "mouseleave" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( activeClass );
+			})
+			.bind( "click" + this.eventNamespace, function( event ) {
+				if ( options.disabled ) {
+					event.preventDefault();
+					event.stopImmediatePropagation();
+				}
+			});
+
+		// Can't use _focusable() because the element that receives focus
+		// and the element that gets the ui-state-focus class are different
+		this._on({
+			focus: function() {
+				this.buttonElement.addClass( "ui-state-focus" );
+			},
+			blur: function() {
+				this.buttonElement.removeClass( "ui-state-focus" );
+			}
+		});
+
+		if ( toggleButton ) {
+			this.element.bind( "change" + this.eventNamespace, function() {
+				that.refresh();
+			});
+		}
+
+		if ( this.type === "checkbox" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return false;
+				}
+			});
+		} else if ( this.type === "radio" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return false;
+				}
+				$( this ).addClass( "ui-state-active" );
+				that.buttonElement.attr( "aria-pressed", "true" );
+
+				var radio = that.element[ 0 ];
+				radioGroup( radio )
+					.not( radio )
+					.map(function() {
+						return $( this ).button( "widget" )[ 0 ];
+					})
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			});
+		} else {
+			this.buttonElement
+				.bind( "mousedown" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).addClass( "ui-state-active" );
+					lastActive = this;
+					that.document.one( "mouseup", function() {
+						lastActive = null;
+					});
+				})
+				.bind( "mouseup" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).removeClass( "ui-state-active" );
+				})
+				.bind( "keydown" + this.eventNamespace, function(event) {
+					if ( options.disabled ) {
+						return false;
+					}
+					if ( event.keyCode === $.ui.keyCode.SPACE || event.keyCode === $.ui.keyCode.ENTER ) {
+						$( this ).addClass( "ui-state-active" );
+					}
+				})
+				// see #8559, we bind to blur here in case the button element loses
+				// focus between keydown and keyup, it would be left in an "active" state
+				.bind( "keyup" + this.eventNamespace + " blur" + this.eventNamespace, function() {
+					$( this ).removeClass( "ui-state-active" );
+				});
+
+			if ( this.buttonElement.is("a") ) {
+				this.buttonElement.keyup(function(event) {
+					if ( event.keyCode === $.ui.keyCode.SPACE ) {
+						// TODO pass through original event correctly (just as 2nd argument doesn't work)
+						$( this ).click();
+					}
+				});
+			}
+		}
+
+		this._setOption( "disabled", options.disabled );
+		this._resetButton();
+	},
+
+	_determineButtonType: function() {
+		var ancestor, labelSelector, checked;
+
+		if ( this.element.is("[type=checkbox]") ) {
+			this.type = "checkbox";
+		} else if ( this.element.is("[type=radio]") ) {
+			this.type = "radio";
+		} else if ( this.element.is("input") ) {
+			this.type = "input";
+		} else {
+			this.type = "button";
+		}
+
+		if ( this.type === "checkbox" || this.type === "radio" ) {
+			// we don't search against the document in case the element
+			// is disconnected from the DOM
+			ancestor = this.element.parents().last();
+			labelSelector = "label[for='" + this.element.attr("id") + "']";
+			this.buttonElement = ancestor.find( labelSelector );
+			if ( !this.buttonElement.length ) {
+				ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings();
+				this.buttonElement = ancestor.filter( labelSelector );
+				if ( !this.buttonElement.length ) {
+					this.buttonElement = ancestor.find( labelSelector );
+				}
+			}
+			this.element.addClass( "ui-helper-hidden-accessible" );
+
+			checked = this.element.is( ":checked" );
+			if ( checked ) {
+				this.buttonElement.addClass( "ui-state-active" );
+			}
+			this.buttonElement.prop( "aria-pressed", checked );
+		} else {
+			this.buttonElement = this.element;
+		}
+	},
+
+	widget: function() {
+		return this.buttonElement;
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-helper-hidden-accessible" );
+		this.buttonElement
+			.removeClass( baseClasses + " ui-state-active " + typeClasses )
+			.removeAttr( "role" )
+			.removeAttr( "aria-pressed" )
+			.html( this.buttonElement.find(".ui-button-text").html() );
+
+		if ( !this.hasTitle ) {
+			this.buttonElement.removeAttr( "title" );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+		if ( key === "disabled" ) {
+			this.widget().toggleClass( "ui-state-disabled", !!value );
+			this.element.prop( "disabled", !!value );
+			if ( value ) {
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					this.buttonElement.removeClass( "ui-state-focus" );
+				} else {
+					this.buttonElement.removeClass( "ui-state-focus ui-state-active" );
+				}
+			}
+			return;
+		}
+		this._resetButton();
+	},
+
+	refresh: function() {
+		//See #8237 & #8828
+		var isDisabled = this.element.is( "input, button" ) ? this.element.is( ":disabled" ) : this.element.hasClass( "ui-button-disabled" );
+
+		if ( isDisabled !== this.options.disabled ) {
+			this._setOption( "disabled", isDisabled );
+		}
+		if ( this.type === "radio" ) {
+			radioGroup( this.element[0] ).each(function() {
+				if ( $( this ).is( ":checked" ) ) {
+					$( this ).button( "widget" )
+						.addClass( "ui-state-active" )
+						.attr( "aria-pressed", "true" );
+				} else {
+					$( this ).button( "widget" )
+						.removeClass( "ui-state-active" )
+						.attr( "aria-pressed", "false" );
+				}
+			});
+		} else if ( this.type === "checkbox" ) {
+			if ( this.element.is( ":checked" ) ) {
+				this.buttonElement
+					.addClass( "ui-state-active" )
+					.attr( "aria-pressed", "true" );
+			} else {
+				this.buttonElement
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			}
+		}
+	},
+
+	_resetButton: function() {
+		if ( this.type === "input" ) {
+			if ( this.options.label ) {
+				this.element.val( this.options.label );
+			}
+			return;
+		}
+		var buttonElement = this.buttonElement.removeClass( typeClasses ),
+			buttonText = $( "<span></span>", this.document[0] )
+				.addClass( "ui-button-text" )
+				.html( this.options.label )
+				.appendTo( buttonElement.empty() )
+				.text(),
+			icons = this.options.icons,
+			multipleIcons = icons.primary && icons.secondary,
+			buttonClasses = [];
+
+		if ( icons.primary || icons.secondary ) {
+			if ( this.options.text ) {
+				buttonClasses.push( "ui-button-text-icon" + ( multipleIcons ? "s" : ( icons.primary ? "-primary" : "-secondary" ) ) );
+			}
+
+			if ( icons.primary ) {
+				buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" );
+			}
+
+			if ( icons.secondary ) {
+				buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" );
+			}
+
+			if ( !this.options.text ) {
+				buttonClasses.push( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" );
+
+				if ( !this.hasTitle ) {
+					buttonElement.attr( "title", $.trim( buttonText ) );
+				}
+			}
+		} else {
+			buttonClasses.push( "ui-button-text-only" );
+		}
+		buttonElement.addClass( buttonClasses.join( " " ) );
+	}
+});
+
+$.widget( "ui.buttonset", {
+	version: "1.11.2",
+	options: {
+		items: "button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(ui-button)"
+	},
+
+	_create: function() {
+		this.element.addClass( "ui-buttonset" );
+	},
+
+	_init: function() {
+		this.refresh();
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "disabled" ) {
+			this.buttons.button( "option", key, value );
+		}
+
+		this._super( key, value );
+	},
+
+	refresh: function() {
+		var rtl = this.element.css( "direction" ) === "rtl",
+			allButtons = this.element.find( this.options.items ),
+			existingButtons = allButtons.filter( ":ui-button" );
+
+		// Initialize new buttons
+		allButtons.not( ":ui-button" ).button();
+
+		// Refresh existing buttons
+		existingButtons.button( "refresh" );
+
+		this.buttons = allButtons
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-all ui-corner-left ui-corner-right" )
+				.filter( ":first" )
+					.addClass( rtl ? "ui-corner-right" : "ui-corner-left" )
+				.end()
+				.filter( ":last" )
+					.addClass( rtl ? "ui-corner-left" : "ui-corner-right" )
+				.end()
+			.end();
+	},
+
+	_destroy: function() {
+		this.element.removeClass( "ui-buttonset" );
+		this.buttons
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-left ui-corner-right" )
+			.end()
+			.button( "destroy" );
+	}
+});
+
+var button = $.ui.button;
+
+
+/*!
+ * jQuery UI Datepicker 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/datepicker/
+ */
+
+
+$.extend($.ui, { datepicker: { version: "1.11.2" } });
+
+var datepicker_instActive;
+
+function datepicker_getZindex( elem ) {
+	var position, value;
+	while ( elem.length && elem[ 0 ] !== document ) {
+		// Ignore z-index if position is set to a value where z-index is ignored by the browser
+		// This makes behavior of this function consistent across browsers
+		// WebKit always returns auto if the element is positioned
+		position = elem.css( "position" );
+		if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+			// IE returns 0 when zIndex is not specified
+			// other browsers return a string
+			// we ignore the case of nested elements with an explicit value of 0
+			// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+			value = parseInt( elem.css( "zIndex" ), 10 );
+			if ( !isNaN( value ) && value !== 0 ) {
+				return value;
+			}
+		}
+		elem = elem.parent();
+	}
+
+	return 0;
+}
+/* Date picker manager.
+   Use the singleton instance of this class, $.datepicker, to interact with the date picker.
+   Settings for (groups of) date pickers are maintained in an instance object,
+   allowing multiple different settings on the same page. */
+
+function Datepicker() {
+	this._curInst = null; // The current instance in use
+	this._keyEvent = false; // If the last event was a key event
+	this._disabledInputs = []; // List of date picker inputs that have been disabled
+	this._datepickerShowing = false; // True if the popup picker is showing , false if not
+	this._inDialog = false; // True if showing within a "dialog", false if not
+	this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division
+	this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class
+	this._appendClass = "ui-datepicker-append"; // The name of the append marker class
+	this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class
+	this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class
+	this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class
+	this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class
+	this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class
+	this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class
+	this.regional = []; // Available regional settings, indexed by language code
+	this.regional[""] = { // Default regional settings
+		closeText: "Done", // Display text for close link
+		prevText: "Prev", // Display text for previous month link
+		nextText: "Next", // Display text for next month link
+		currentText: "Today", // Display text for current month link
+		monthNames: ["January","February","March","April","May","June",
+			"July","August","September","October","November","December"], // Names of months for drop-down and formatting
+		monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting
+		dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting
+		dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting
+		dayNamesMin: ["Su","Mo","Tu","We","Th","Fr","Sa"], // Column headings for days starting at Sunday
+		weekHeader: "Wk", // Column header for week of the year
+		dateFormat: "mm/dd/yy", // See format options on parseDate
+		firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ...
+		isRTL: false, // True if right-to-left language, false if left-to-right
+		showMonthAfterYear: false, // True if the year select precedes month, false for month then year
+		yearSuffix: "" // Additional text to append to the year in the month headers
+	};
+	this._defaults = { // Global defaults for all the date picker instances
+		showOn: "focus", // "focus" for popup on focus,
+			// "button" for trigger button, or "both" for either
+		showAnim: "fadeIn", // Name of jQuery animation for popup
+		showOptions: {}, // Options for enhanced animations
+		defaultDate: null, // Used when field is blank: actual date,
+			// +/-number for offset from today, null for today
+		appendText: "", // Display text following the input box, e.g. showing the format
+		buttonText: "...", // Text for trigger button
+		buttonImage: "", // URL for trigger button image
+		buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
+		hideIfNoPrevNext: false, // True to hide next/previous month links
+			// if not applicable, false to just disable them
+		navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links
+		gotoCurrent: false, // True if today link goes back to current selection instead
+		changeMonth: false, // True if month can be selected directly, false if only prev/next
+		changeYear: false, // True if year can be selected directly, false if only prev/next
+		yearRange: "c-10:c+10", // Range of years to display in drop-down,
+			// either relative to today's year (-nn:+nn), relative to currently displayed year
+			// (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)
+		showOtherMonths: false, // True to show dates in other months, false to leave blank
+		selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable
+		showWeek: false, // True to show week of the year, false to not show it
+		calculateWeek: this.iso8601Week, // How to calculate the week of the year,
+			// takes a Date and returns the number of the week for it
+		shortYearCutoff: "+10", // Short year values < this are in the current century,
+			// > this are in the previous century,
+			// string value starting with "+" for current year + value
+		minDate: null, // The earliest selectable date, or null for no limit
+		maxDate: null, // The latest selectable date, or null for no limit
+		duration: "fast", // Duration of display/closure
+		beforeShowDay: null, // Function that takes a date and returns an array with
+			// [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "",
+			// [2] = cell title (optional), e.g. $.datepicker.noWeekends
+		beforeShow: null, // Function that takes an input field and
+			// returns a set of custom settings for the date picker
+		onSelect: null, // Define a callback function when a date is selected
+		onChangeMonthYear: null, // Define a callback function when the month or year is changed
+		onClose: null, // Define a callback function when the datepicker is closed
+		numberOfMonths: 1, // Number of months to show at a time
+		showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)
+		stepMonths: 1, // Number of months to step back/forward
+		stepBigMonths: 12, // Number of months to step back/forward for the big links
+		altField: "", // Selector for an alternate field to store selected dates into
+		altFormat: "", // The date format to use for the alternate field
+		constrainInput: true, // The input is constrained by the current date format
+		showButtonPanel: false, // True to show button panel, false to not show it
+		autoSize: false, // True to size the input for the date format, false to leave as is
+		disabled: false // The initial disabled state
+	};
+	$.extend(this._defaults, this.regional[""]);
+	this.regional.en = $.extend( true, {}, this.regional[ "" ]);
+	this.regional[ "en-US" ] = $.extend( true, {}, this.regional.en );
+	this.dpDiv = datepicker_bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"));
+}
+
+$.extend(Datepicker.prototype, {
+	/* Class name added to elements to indicate already configured with a date picker. */
+	markerClassName: "hasDatepicker",
+
+	//Keep track of the maximum number of rows displayed (see #7043)
+	maxRows: 4,
+
+	// TODO rename to "widget" when switching to widget factory
+	_widgetDatepicker: function() {
+		return this.dpDiv;
+	},
+
+	/* Override the default settings for all instances of the date picker.
+	 * @param  settings  object - the new settings to use as defaults (anonymous object)
+	 * @return the manager object
+	 */
+	setDefaults: function(settings) {
+		datepicker_extendRemove(this._defaults, settings || {});
+		return this;
+	},
+
+	/* Attach the date picker to a jQuery selection.
+	 * @param  target	element - the target input field or division or span
+	 * @param  settings  object - the new settings to use for this date picker instance (anonymous)
+	 */
+	_attachDatepicker: function(target, settings) {
+		var nodeName, inline, inst;
+		nodeName = target.nodeName.toLowerCase();
+		inline = (nodeName === "div" || nodeName === "span");
+		if (!target.id) {
+			this.uuid += 1;
+			target.id = "dp" + this.uuid;
+		}
+		inst = this._newInst($(target), inline);
+		inst.settings = $.extend({}, settings || {});
+		if (nodeName === "input") {
+			this._connectDatepicker(target, inst);
+		} else if (inline) {
+			this._inlineDatepicker(target, inst);
+		}
+	},
+
+	/* Create a new instance object. */
+	_newInst: function(target, inline) {
+		var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars
+		return {id: id, input: target, // associated target
+			selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection
+			drawMonth: 0, drawYear: 0, // month being drawn
+			inline: inline, // is datepicker inline or not
+			dpDiv: (!inline ? this.dpDiv : // presentation div
+			datepicker_bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")))};
+	},
+
+	/* Attach the date picker to an input field. */
+	_connectDatepicker: function(target, inst) {
+		var input = $(target);
+		inst.append = $([]);
+		inst.trigger = $([]);
+		if (input.hasClass(this.markerClassName)) {
+			return;
+		}
+		this._attachments(input, inst);
+		input.addClass(this.markerClassName).keydown(this._doKeyDown).
+			keypress(this._doKeyPress).keyup(this._doKeyUp);
+		this._autoSize(inst);
+		$.data(target, "datepicker", inst);
+		//If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665)
+		if( inst.settings.disabled ) {
+			this._disableDatepicker( target );
+		}
+	},
+
+	/* Make attachments based on settings. */
+	_attachments: function(input, inst) {
+		var showOn, buttonText, buttonImage,
+			appendText = this._get(inst, "appendText"),
+			isRTL = this._get(inst, "isRTL");
+
+		if (inst.append) {
+			inst.append.remove();
+		}
+		if (appendText) {
+			inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>");
+			input[isRTL ? "before" : "after"](inst.append);
+		}
+
+		input.unbind("focus", this._showDatepicker);
+
+		if (inst.trigger) {
+			inst.trigger.remove();
+		}
+
+		showOn = this._get(inst, "showOn");
+		if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field
+			input.focus(this._showDatepicker);
+		}
+		if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked
+			buttonText = this._get(inst, "buttonText");
+			buttonImage = this._get(inst, "buttonImage");
+			inst.trigger = $(this._get(inst, "buttonImageOnly") ?
+				$("<img/>").addClass(this._triggerClass).
+					attr({ src: buttonImage, alt: buttonText, title: buttonText }) :
+				$("<button type='button'></button>").addClass(this._triggerClass).
+					html(!buttonImage ? buttonText : $("<img/>").attr(
+					{ src:buttonImage, alt:buttonText, title:buttonText })));
+			input[isRTL ? "before" : "after"](inst.trigger);
+			inst.trigger.click(function() {
+				if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) {
+					$.datepicker._hideDatepicker();
+				} else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) {
+					$.datepicker._hideDatepicker();
+					$.datepicker._showDatepicker(input[0]);
+				} else {
+					$.datepicker._showDatepicker(input[0]);
+				}
+				return false;
+			});
+		}
+	},
+
+	/* Apply the maximum length for the date format. */
+	_autoSize: function(inst) {
+		if (this._get(inst, "autoSize") && !inst.inline) {
+			var findMax, max, maxI, i,
+				date = new Date(2009, 12 - 1, 20), // Ensure double digits
+				dateFormat = this._get(inst, "dateFormat");
+
+			if (dateFormat.match(/[DM]/)) {
+				findMax = function(names) {
+					max = 0;
+					maxI = 0;
+					for (i = 0; i < names.length; i++) {
+						if (names[i].length > max) {
+							max = names[i].length;
+							maxI = i;
+						}
+					}
+					return maxI;
+				};
+				date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ?
+					"monthNames" : "monthNamesShort"))));
+				date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ?
+					"dayNames" : "dayNamesShort"))) + 20 - date.getDay());
+			}
+			inst.input.attr("size", this._formatDate(inst, date).length);
+		}
+	},
+
+	/* Attach an inline date picker to a div. */
+	_inlineDatepicker: function(target, inst) {
+		var divSpan = $(target);
+		if (divSpan.hasClass(this.markerClassName)) {
+			return;
+		}
+		divSpan.addClass(this.markerClassName).append(inst.dpDiv);
+		$.data(target, "datepicker", inst);
+		this._setDate(inst, this._getDefaultDate(inst), true);
+		this._updateDatepicker(inst);
+		this._updateAlternate(inst);
+		//If disabled option is true, disable the datepicker before showing it (see ticket #5665)
+		if( inst.settings.disabled ) {
+			this._disableDatepicker( target );
+		}
+		// Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements
+		// http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height
+		inst.dpDiv.css( "display", "block" );
+	},
+
+	/* Pop-up the date picker in a "dialog" box.
+	 * @param  input element - ignored
+	 * @param  date	string or Date - the initial date to display
+	 * @param  onSelect  function - the function to call when a date is selected
+	 * @param  settings  object - update the dialog date picker instance's settings (anonymous object)
+	 * @param  pos int[2] - coordinates for the dialog's position within the screen or
+	 *					event - with x/y coordinates or
+	 *					leave empty for default (screen centre)
+	 * @return the manager object
+	 */
+	_dialogDatepicker: function(input, date, onSelect, settings, pos) {
+		var id, browserWidth, browserHeight, scrollX, scrollY,
+			inst = this._dialogInst; // internal instance
+
+		if (!inst) {
+			this.uuid += 1;
+			id = "dp" + this.uuid;
+			this._dialogInput = $("<input type='text' id='" + id +
+				"' style='position: absolute; top: -100px; width: 0px;'/>");
+			this._dialogInput.keydown(this._doKeyDown);
+			$("body").append(this._dialogInput);
+			inst = this._dialogInst = this._newInst(this._dialogInput, false);
+			inst.settings = {};
+			$.data(this._dialogInput[0], "datepicker", inst);
+		}
+		datepicker_extendRemove(inst.settings, settings || {});
+		date = (date && date.constructor === Date ? this._formatDate(inst, date) : date);
+		this._dialogInput.val(date);
+
+		this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null);
+		if (!this._pos) {
+			browserWidth = document.documentElement.clientWidth;
+			browserHeight = document.documentElement.clientHeight;
+			scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
+			scrollY = document.documentElement.scrollTop || document.body.scrollTop;
+			this._pos = // should use actual width/height below
+				[(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY];
+		}
+
+		// move input on screen for focus, but hidden behind dialog
+		this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px");
+		inst.settings.onSelect = onSelect;
+		this._inDialog = true;
+		this.dpDiv.addClass(this._dialogClass);
+		this._showDatepicker(this._dialogInput[0]);
+		if ($.blockUI) {
+			$.blockUI(this.dpDiv);
+		}
+		$.data(this._dialogInput[0], "datepicker", inst);
+		return this;
+	},
+
+	/* Detach a datepicker from its control.
+	 * @param  target	element - the target input field or division or span
+	 */
+	_destroyDatepicker: function(target) {
+		var nodeName,
+			$target = $(target),
+			inst = $.data(target, "datepicker");
+
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+
+		nodeName = target.nodeName.toLowerCase();
+		$.removeData(target, "datepicker");
+		if (nodeName === "input") {
+			inst.append.remove();
+			inst.trigger.remove();
+			$target.removeClass(this.markerClassName).
+				unbind("focus", this._showDatepicker).
+				unbind("keydown", this._doKeyDown).
+				unbind("keypress", this._doKeyPress).
+				unbind("keyup", this._doKeyUp);
+		} else if (nodeName === "div" || nodeName === "span") {
+			$target.removeClass(this.markerClassName).empty();
+		}
+	},
+
+	/* Enable the date picker to a jQuery selection.
+	 * @param  target	element - the target input field or division or span
+	 */
+	_enableDatepicker: function(target) {
+		var nodeName, inline,
+			$target = $(target),
+			inst = $.data(target, "datepicker");
+
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+
+		nodeName = target.nodeName.toLowerCase();
+		if (nodeName === "input") {
+			target.disabled = false;
+			inst.trigger.filter("button").
+				each(function() { this.disabled = false; }).end().
+				filter("img").css({opacity: "1.0", cursor: ""});
+		} else if (nodeName === "div" || nodeName === "span") {
+			inline = $target.children("." + this._inlineClass);
+			inline.children().removeClass("ui-state-disabled");
+			inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
+				prop("disabled", false);
+		}
+		this._disabledInputs = $.map(this._disabledInputs,
+			function(value) { return (value === target ? null : value); }); // delete entry
+	},
+
+	/* Disable the date picker to a jQuery selection.
+	 * @param  target	element - the target input field or division or span
+	 */
+	_disableDatepicker: function(target) {
+		var nodeName, inline,
+			$target = $(target),
+			inst = $.data(target, "datepicker");
+
+		if (!$target.hasClass(this.markerClassName)) {
+			return;
+		}
+
+		nodeName = target.nodeName.toLowerCase();
+		if (nodeName === "input") {
+			target.disabled = true;
+			inst.trigger.filter("button").
+				each(function() { this.disabled = true; }).end().
+				filter("img").css({opacity: "0.5", cursor: "default"});
+		} else if (nodeName === "div" || nodeName === "span") {
+			inline = $target.children("." + this._inlineClass);
+			inline.children().addClass("ui-state-disabled");
+			inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
+				prop("disabled", true);
+		}
+		this._disabledInputs = $.map(this._disabledInputs,
+			function(value) { return (value === target ? null : value); }); // delete entry
+		this._disabledInputs[this._disabledInputs.length] = target;
+	},
+
+	/* Is the first field in a jQuery collection disabled as a datepicker?
+	 * @param  target	element - the target input field or division or span
+	 * @return boolean - true if disabled, false if enabled
+	 */
+	_isDisabledDatepicker: function(target) {
+		if (!target) {
+			return false;
+		}
+		for (var i = 0; i < this._disabledInputs.length; i++) {
+			if (this._disabledInputs[i] === target) {
+				return true;
+			}
+		}
+		return false;
+	},
+
+	/* Retrieve the instance data for the target control.
+	 * @param  target  element - the target input field or division or span
+	 * @return  object - the associated instance data
+	 * @throws  error if a jQuery problem getting data
+	 */
+	_getInst: function(target) {
+		try {
+			return $.data(target, "datepicker");
+		}
+		catch (err) {
+			throw "Missing instance data for this datepicker";
+		}
+	},
+
+	/* Update or retrieve the settings for a date picker attached to an input field or division.
+	 * @param  target  element - the target input field or division or span
+	 * @param  name	object - the new settings to update or
+	 *				string - the name of the setting to change or retrieve,
+	 *				when retrieving also "all" for all instance settings or
+	 *				"defaults" for all global defaults
+	 * @param  value   any - the new value for the setting
+	 *				(omit if above is an object or to retrieve a value)
+	 */
+	_optionDatepicker: function(target, name, value) {
+		var settings, date, minDate, maxDate,
+			inst = this._getInst(target);
+
+		if (arguments.length === 2 && typeof name === "string") {
+			return (name === "defaults" ? $.extend({}, $.datepicker._defaults) :
+				(inst ? (name === "all" ? $.extend({}, inst.settings) :
+				this._get(inst, name)) : null));
+		}
+
+		settings = name || {};
+		if (typeof name === "string") {
+			settings = {};
+			settings[name] = value;
+		}
+
+		if (inst) {
+			if (this._curInst === inst) {
+				this._hideDatepicker();
+			}
+
+			date = this._getDateDatepicker(target, true);
+			minDate = this._getMinMaxDate(inst, "min");
+			maxDate = this._getMinMaxDate(inst, "max");
+			datepicker_extendRemove(inst.settings, settings);
+			// reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided
+			if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) {
+				inst.settings.minDate = this._formatDate(inst, minDate);
+			}
+			if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) {
+				inst.settings.maxDate = this._formatDate(inst, maxDate);
+			}
+			if ( "disabled" in settings ) {
+				if ( settings.disabled ) {
+					this._disableDatepicker(target);
+				} else {
+					this._enableDatepicker(target);
+				}
+			}
+			this._attachments($(target), inst);
+			this._autoSize(inst);
+			this._setDate(inst, date);
+			this._updateAlternate(inst);
+			this._updateDatepicker(inst);
+		}
+	},
+
+	// change method deprecated
+	_changeDatepicker: function(target, name, value) {
+		this._optionDatepicker(target, name, value);
+	},
+
+	/* Redraw the date picker attached to an input field or division.
+	 * @param  target  element - the target input field or division or span
+	 */
+	_refreshDatepicker: function(target) {
+		var inst = this._getInst(target);
+		if (inst) {
+			this._updateDatepicker(inst);
+		}
+	},
+
+	/* Set the dates for a jQuery selection.
+	 * @param  target element - the target input field or division or span
+	 * @param  date	Date - the new date
+	 */
+	_setDateDatepicker: function(target, date) {
+		var inst = this._getInst(target);
+		if (inst) {
+			this._setDate(inst, date);
+			this._updateDatepicker(inst);
+			this._updateAlternate(inst);
+		}
+	},
+
+	/* Get the date(s) for the first entry in a jQuery selection.
+	 * @param  target element - the target input field or division or span
+	 * @param  noDefault boolean - true if no default date is to be used
+	 * @return Date - the current date
+	 */
+	_getDateDatepicker: function(target, noDefault) {
+		var inst = this._getInst(target);
+		if (inst && !inst.inline) {
+			this._setDateFromField(inst, noDefault);
+		}
+		return (inst ? this._getDate(inst) : null);
+	},
+
+	/* Handle keystrokes. */
+	_doKeyDown: function(event) {
+		var onSelect, dateStr, sel,
+			inst = $.datepicker._getInst(event.target),
+			handled = true,
+			isRTL = inst.dpDiv.is(".ui-datepicker-rtl");
+
+		inst._keyEvent = true;
+		if ($.datepicker._datepickerShowing) {
+			switch (event.keyCode) {
+				case 9: $.datepicker._hideDatepicker();
+						handled = false;
+						break; // hide on tab out
+				case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." +
+									$.datepicker._currentClass + ")", inst.dpDiv);
+						if (sel[0]) {
+							$.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]);
+						}
+
+						onSelect = $.datepicker._get(inst, "onSelect");
+						if (onSelect) {
+							dateStr = $.datepicker._formatDate(inst);
+
+							// trigger custom callback
+							onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);
+						} else {
+							$.datepicker._hideDatepicker();
+						}
+
+						return false; // don't submit the form
+				case 27: $.datepicker._hideDatepicker();
+						break; // hide on escape
+				case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+							-$.datepicker._get(inst, "stepBigMonths") :
+							-$.datepicker._get(inst, "stepMonths")), "M");
+						break; // previous month/year on page up/+ ctrl
+				case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
+							+$.datepicker._get(inst, "stepBigMonths") :
+							+$.datepicker._get(inst, "stepMonths")), "M");
+						break; // next month/year on page down/+ ctrl
+				case 35: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._clearDate(event.target);
+						}
+						handled = event.ctrlKey || event.metaKey;
+						break; // clear on ctrl or command +end
+				case 36: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._gotoToday(event.target);
+						}
+						handled = event.ctrlKey || event.metaKey;
+						break; // current on ctrl or command +home
+				case 37: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D");
+						}
+						handled = event.ctrlKey || event.metaKey;
+						// -1 day on ctrl or command +left
+						if (event.originalEvent.altKey) {
+							$.datepicker._adjustDate(event.target, (event.ctrlKey ?
+								-$.datepicker._get(inst, "stepBigMonths") :
+								-$.datepicker._get(inst, "stepMonths")), "M");
+						}
+						// next month/year on alt +left on Mac
+						break;
+				case 38: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._adjustDate(event.target, -7, "D");
+						}
+						handled = event.ctrlKey || event.metaKey;
+						break; // -1 week on ctrl or command +up
+				case 39: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D");
+						}
+						handled = event.ctrlKey || event.metaKey;
+						// +1 day on ctrl or command +right
+						if (event.originalEvent.altKey) {
+							$.datepicker._adjustDate(event.target, (event.ctrlKey ?
+								+$.datepicker._get(inst, "stepBigMonths") :
+								+$.datepicker._get(inst, "stepMonths")), "M");
+						}
+						// next month/year on alt +right
+						break;
+				case 40: if (event.ctrlKey || event.metaKey) {
+							$.datepicker._adjustDate(event.target, +7, "D");
+						}
+						handled = event.ctrlKey || event.metaKey;
+						break; // +1 week on ctrl or command +down
+				default: handled = false;
+			}
+		} else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home
+			$.datepicker._showDatepicker(this);
+		} else {
+			handled = false;
+		}
+
+		if (handled) {
+			event.preventDefault();
+			event.stopPropagation();
+		}
+	},
+
+	/* Filter entered characters - based on date format. */
+	_doKeyPress: function(event) {
+		var chars, chr,
+			inst = $.datepicker._getInst(event.target);
+
+		if ($.datepicker._get(inst, "constrainInput")) {
+			chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat"));
+			chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode);
+			return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1);
+		}
+	},
+
+	/* Synchronise manual entry and field/alternate field. */
+	_doKeyUp: function(event) {
+		var date,
+			inst = $.datepicker._getInst(event.target);
+
+		if (inst.input.val() !== inst.lastVal) {
+			try {
+				date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"),
+					(inst.input ? inst.input.val() : null),
+					$.datepicker._getFormatConfig(inst));
+
+				if (date) { // only if valid
+					$.datepicker._setDateFromField(inst);
+					$.datepicker._updateAlternate(inst);
+					$.datepicker._updateDatepicker(inst);
+				}
+			}
+			catch (err) {
+			}
+		}
+		return true;
+	},
+
+	/* Pop-up the date picker for a given input field.
+	 * If false returned from beforeShow event handler do not show.
+	 * @param  input  element - the input field attached to the date picker or
+	 *					event - if triggered by focus
+	 */
+	_showDatepicker: function(input) {
+		input = input.target || input;
+		if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger
+			input = $("input", input.parentNode)[0];
+		}
+
+		if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here
+			return;
+		}
+
+		var inst, beforeShow, beforeShowSettings, isFixed,
+			offset, showAnim, duration;
+
+		inst = $.datepicker._getInst(input);
+		if ($.datepicker._curInst && $.datepicker._curInst !== inst) {
+			$.datepicker._curInst.dpDiv.stop(true, true);
+			if ( inst && $.datepicker._datepickerShowing ) {
+				$.datepicker._hideDatepicker( $.datepicker._curInst.input[0] );
+			}
+		}
+
+		beforeShow = $.datepicker._get(inst, "beforeShow");
+		beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {};
+		if(beforeShowSettings === false){
+			return;
+		}
+		datepicker_extendRemove(inst.settings, beforeShowSettings);
+
+		inst.lastVal = null;
+		$.datepicker._lastInput = input;
+		$.datepicker._setDateFromField(inst);
+
+		if ($.datepicker._inDialog) { // hide cursor
+			input.value = "";
+		}
+		if (!$.datepicker._pos) { // position below input
+			$.datepicker._pos = $.datepicker._findPos(input);
+			$.datepicker._pos[1] += input.offsetHeight; // add the height
+		}
+
+		isFixed = false;
+		$(input).parents().each(function() {
+			isFixed |= $(this).css("position") === "fixed";
+			return !isFixed;
+		});
+
+		offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]};
+		$.datepicker._pos = null;
+		//to avoid flashes on Firefox
+		inst.dpDiv.empty();
+		// determine sizing offscreen
+		inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"});
+		$.datepicker._updateDatepicker(inst);
+		// fix width for dynamic number of date pickers
+		// and adjust position before showing
+		offset = $.datepicker._checkOffset(inst, offset, isFixed);
+		inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ?
+			"static" : (isFixed ? "fixed" : "absolute")), display: "none",
+			left: offset.left + "px", top: offset.top + "px"});
+
+		if (!inst.inline) {
+			showAnim = $.datepicker._get(inst, "showAnim");
+			duration = $.datepicker._get(inst, "duration");
+			inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 );
+			$.datepicker._datepickerShowing = true;
+
+			if ( $.effects && $.effects.effect[ showAnim ] ) {
+				inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration);
+			} else {
+				inst.dpDiv[showAnim || "show"](showAnim ? duration : null);
+			}
+
+			if ( $.datepicker._shouldFocusInput( inst ) ) {
+				inst.input.focus();
+			}
+
+			$.datepicker._curInst = inst;
+		}
+	},
+
+	/* Generate the date picker content. */
+	_updateDatepicker: function(inst) {
+		this.maxRows = 4; //Reset the max number of rows being displayed (see #7043)
+		datepicker_instActive = inst; // for delegate hover events
+		inst.dpDiv.empty().append(this._generateHTML(inst));
+		this._attachHandlers(inst);
+
+		var origyearshtml,
+			numMonths = this._getNumberOfMonths(inst),
+			cols = numMonths[1],
+			width = 17,
+			activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" );
+
+		if ( activeCell.length > 0 ) {
+			datepicker_handleMouseover.apply( activeCell.get( 0 ) );
+		}
+
+		inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");
+		if (cols > 1) {
+			inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em");
+		}
+		inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") +
+			"Class"]("ui-datepicker-multi");
+		inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") +
+			"Class"]("ui-datepicker-rtl");
+
+		if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) {
+			inst.input.focus();
+		}
+
+		// deffered render of the years select (to avoid flashes on Firefox)
+		if( inst.yearshtml ){
+			origyearshtml = inst.yearshtml;
+			setTimeout(function(){
+				//assure that inst.yearshtml didn't change.
+				if( origyearshtml === inst.yearshtml && inst.yearshtml ){
+					inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml);
+				}
+				origyearshtml = inst.yearshtml = null;
+			}, 0);
+		}
+	},
+
+	// #6694 - don't focus the input if it's already focused
+	// this breaks the change event in IE
+	// Support: IE and jQuery <1.9
+	_shouldFocusInput: function( inst ) {
+		return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" );
+	},
+
+	/* Check positioning to remain on screen. */
+	_checkOffset: function(inst, offset, isFixed) {
+		var dpWidth = inst.dpDiv.outerWidth(),
+			dpHeight = inst.dpDiv.outerHeight(),
+			inputWidth = inst.input ? inst.input.outerWidth() : 0,
+			inputHeight = inst.input ? inst.input.outerHeight() : 0,
+			viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()),
+			viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop());
+
+		offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0);
+		offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0;
+		offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0;
+
+		// now check if datepicker is showing outside window viewport - move to a better place if so.
+		offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
+			Math.abs(offset.left + dpWidth - viewWidth) : 0);
+		offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
+			Math.abs(dpHeight + inputHeight) : 0);
+
+		return offset;
+	},
+
+	/* Find an object's position on the screen. */
+	_findPos: function(obj) {
+		var position,
+			inst = this._getInst(obj),
+			isRTL = this._get(inst, "isRTL");
+
+		while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) {
+			obj = obj[isRTL ? "previousSibling" : "nextSibling"];
+		}
+
+		position = $(obj).offset();
+		return [position.left, position.top];
+	},
+
+	/* Hide the date picker from view.
+	 * @param  input  element - the input field attached to the date picker
+	 */
+	_hideDatepicker: function(input) {
+		var showAnim, duration, postProcess, onClose,
+			inst = this._curInst;
+
+		if (!inst || (input && inst !== $.data(input, "datepicker"))) {
+			return;
+		}
+
+		if (this._datepickerShowing) {
+			showAnim = this._get(inst, "showAnim");
+			duration = this._get(inst, "duration");
+			postProcess = function() {
+				$.datepicker._tidyDialog(inst);
+			};
+
+			// DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed
+			if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) {
+				inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess);
+			} else {
+				inst.dpDiv[(showAnim === "slideDown" ? "slideUp" :
+					(showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess);
+			}
+
+			if (!showAnim) {
+				postProcess();
+			}
+			this._datepickerShowing = false;
+
+			onClose = this._get(inst, "onClose");
+			if (onClose) {
+				onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]);
+			}
+
+			this._lastInput = null;
+			if (this._inDialog) {
+				this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" });
+				if ($.blockUI) {
+					$.unblockUI();
+					$("body").append(this.dpDiv);
+				}
+			}
+			this._inDialog = false;
+		}
+	},
+
+	/* Tidy up after a dialog display. */
+	_tidyDialog: function(inst) {
+		inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar");
+	},
+
+	/* Close date picker if clicked elsewhere. */
+	_checkExternalClick: function(event) {
+		if (!$.datepicker._curInst) {
+			return;
+		}
+
+		var $target = $(event.target),
+			inst = $.datepicker._getInst($target[0]);
+
+		if ( ( ( $target[0].id !== $.datepicker._mainDivId &&
+				$target.parents("#" + $.datepicker._mainDivId).length === 0 &&
+				!$target.hasClass($.datepicker.markerClassName) &&
+				!$target.closest("." + $.datepicker._triggerClass).length &&
+				$.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) ||
+			( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) {
+				$.datepicker._hideDatepicker();
+		}
+	},
+
+	/* Adjust one of the date sub-fields. */
+	_adjustDate: function(id, offset, period) {
+		var target = $(id),
+			inst = this._getInst(target[0]);
+
+		if (this._isDisabledDatepicker(target[0])) {
+			return;
+		}
+		this._adjustInstDate(inst, offset +
+			(period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning
+			period);
+		this._updateDatepicker(inst);
+	},
+
+	/* Action for current link. */
+	_gotoToday: function(id) {
+		var date,
+			target = $(id),
+			inst = this._getInst(target[0]);
+
+		if (this._get(inst, "gotoCurrent") && inst.currentDay) {
+			inst.selectedDay = inst.currentDay;
+			inst.drawMonth = inst.selectedMonth = inst.currentMonth;
+			inst.drawYear = inst.selectedYear = inst.currentYear;
+		} else {
+			date = new Date();
+			inst.selectedDay = date.getDate();
+			inst.drawMonth = inst.selectedMonth = date.getMonth();
+			inst.drawYear = inst.selectedYear = date.getFullYear();
+		}
+		this._notifyChange(inst);
+		this._adjustDate(target);
+	},
+
+	/* Action for selecting a new month/year. */
+	_selectMonthYear: function(id, select, period) {
+		var target = $(id),
+			inst = this._getInst(target[0]);
+
+		inst["selected" + (period === "M" ? "Month" : "Year")] =
+		inst["draw" + (period === "M" ? "Month" : "Year")] =
+			parseInt(select.options[select.selectedIndex].value,10);
+
+		this._notifyChange(inst);
+		this._adjustDate(target);
+	},
+
+	/* Action for selecting a day. */
+	_selectDay: function(id, month, year, td) {
+		var inst,
+			target = $(id);
+
+		if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) {
+			return;
+		}
+
+		inst = this._getInst(target[0]);
+		inst.selectedDay = inst.currentDay = $("a", td).html();
+		inst.selectedMonth = inst.currentMonth = month;
+		inst.selectedYear = inst.currentYear = year;
+		this._selectDate(id, this._formatDate(inst,
+			inst.currentDay, inst.currentMonth, inst.currentYear));
+	},
+
+	/* Erase the input field and hide the date picker. */
+	_clearDate: function(id) {
+		var target = $(id);
+		this._selectDate(target, "");
+	},
+
+	/* Update the input field with the selected date. */
+	_selectDate: function(id, dateStr) {
+		var onSelect,
+			target = $(id),
+			inst = this._getInst(target[0]);
+
+		dateStr = (dateStr != null ? dateStr : this._formatDate(inst));
+		if (inst.input) {
+			inst.input.val(dateStr);
+		}
+		this._updateAlternate(inst);
+
+		onSelect = this._get(inst, "onSelect");
+		if (onSelect) {
+			onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);  // trigger custom callback
+		} else if (inst.input) {
+			inst.input.trigger("change"); // fire the change event
+		}
+
+		if (inst.inline){
+			this._updateDatepicker(inst);
+		} else {
+			this._hideDatepicker();
+			this._lastInput = inst.input[0];
+			if (typeof(inst.input[0]) !== "object") {
+				inst.input.focus(); // restore focus
+			}
+			this._lastInput = null;
+		}
+	},
+
+	/* Update any alternate field to synchronise with the main field. */
+	_updateAlternate: function(inst) {
+		var altFormat, date, dateStr,
+			altField = this._get(inst, "altField");
+
+		if (altField) { // update alternate field too
+			altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat");
+			date = this._getDate(inst);
+			dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst));
+			$(altField).each(function() { $(this).val(dateStr); });
+		}
+	},
+
+	/* Set as beforeShowDay function to prevent selection of weekends.
+	 * @param  date  Date - the date to customise
+	 * @return [boolean, string] - is this date selectable?, what is its CSS class?
+	 */
+	noWeekends: function(date) {
+		var day = date.getDay();
+		return [(day > 0 && day < 6), ""];
+	},
+
+	/* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+	 * @param  date  Date - the date to get the week for
+	 * @return  number - the number of the week within the year that contains this date
+	 */
+	iso8601Week: function(date) {
+		var time,
+			checkDate = new Date(date.getTime());
+
+		// Find Thursday of this week starting on Monday
+		checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+
+		time = checkDate.getTime();
+		checkDate.setMonth(0); // Compare with Jan 1
+		checkDate.setDate(1);
+		return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+	},
+
+	/* Parse a string value into a date object.
+	 * See formatDate below for the possible formats.
+	 *
+	 * @param  format string - the expected format of the date
+	 * @param  value string - the date in the above format
+	 * @param  settings Object - attributes include:
+	 *					shortYearCutoff  number - the cutoff year for determining the century (optional)
+	 *					dayNamesShort	string[7] - abbreviated names of the days from Sunday (optional)
+	 *					dayNames		string[7] - names of the days from Sunday (optional)
+	 *					monthNamesShort string[12] - abbreviated names of the months (optional)
+	 *					monthNames		string[12] - names of the months (optional)
+	 * @return  Date - the extracted date value or null if value is blank
+	 */
+	parseDate: function (format, value, settings) {
+		if (format == null || value == null) {
+			throw "Invalid arguments";
+		}
+
+		value = (typeof value === "object" ? value.toString() : value + "");
+		if (value === "") {
+			return null;
+		}
+
+		var iFormat, dim, extra,
+			iValue = 0,
+			shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff,
+			shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp :
+				new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)),
+			dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,
+			dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,
+			monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,
+			monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,
+			year = -1,
+			month = -1,
+			day = -1,
+			doy = -1,
+			literal = false,
+			date,
+			// Check whether a format character is doubled
+			lookAhead = function(match) {
+				var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
+				if (matches) {
+					iFormat++;
+				}
+				return matches;
+			},
+			// Extract a number from the string value
+			getNumber = function(match) {
+				var isDoubled = lookAhead(match),
+					size = (match === "@" ? 14 : (match === "!" ? 20 :
+					(match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))),
+					minSize = (match === "y" ? size : 1),
+					digits = new RegExp("^\\d{" + minSize + "," + size + "}"),
+					num = value.substring(iValue).match(digits);
+				if (!num) {
+					throw "Missing number at position " + iValue;
+				}
+				iValue += num[0].length;
+				return parseInt(num[0], 10);
+			},
+			// Extract a name from the string value and convert to an index
+			getName = function(match, shortNames, longNames) {
+				var index = -1,
+					names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) {
+						return [ [k, v] ];
+					}).sort(function (a, b) {
+						return -(a[1].length - b[1].length);
+					});
+
+				$.each(names, function (i, pair) {
+					var name = pair[1];
+					if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) {
+						index = pair[0];
+						iValue += name.length;
+						return false;
+					}
+				});
+				if (index !== -1) {
+					return index + 1;
+				} else {
+					throw "Unknown name at position " + iValue;
+				}
+			},
+			// Confirm that a literal character matches the string value
+			checkLiteral = function() {
+				if (value.charAt(iValue) !== format.charAt(iFormat)) {
+					throw "Unexpected literal at position " + iValue;
+				}
+				iValue++;
+			};
+
+		for (iFormat = 0; iFormat < format.length; iFormat++) {
+			if (literal) {
+				if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
+					literal = false;
+				} else {
+					checkLiteral();
+				}
+			} else {
+				switch (format.charAt(iFormat)) {
+					case "d":
+						day = getNumber("d");
+						break;
+					case "D":
+						getName("D", dayNamesShort, dayNames);
+						break;
+					case "o":
+						doy = getNumber("o");
+						break;
+					case "m":
+						month = getNumber("m");
+						break;
+					case "M":
+						month = getName("M", monthNamesShort, monthNames);
+						break;
+					case "y":
+						year = getNumber("y");
+						break;
+					case "@":
+						date = new Date(getNumber("@"));
+						year = date.getFullYear();
+						month = date.getMonth() + 1;
+						day = date.getDate();
+						break;
+					case "!":
+						date = new Date((getNumber("!") - this._ticksTo1970) / 10000);
+						year = date.getFullYear();
+						month = date.getMonth() + 1;
+						day = date.getDate();
+						break;
+					case "'":
+						if (lookAhead("'")){
+							checkLiteral();
+						} else {
+							literal = true;
+						}
+						break;
+					default:
+						checkLiteral();
+				}
+			}
+		}
+
+		if (iValue < value.length){
+			extra = value.substr(iValue);
+			if (!/^\s+/.test(extra)) {
+				throw "Extra/unparsed characters found in date: " + extra;
+			}
+		}
+
+		if (year === -1) {
+			year = new Date().getFullYear();
+		} else if (year < 100) {
+			year += new Date().getFullYear() - new Date().getFullYear() % 100 +
+				(year <= shortYearCutoff ? 0 : -100);
+		}
+
+		if (doy > -1) {
+			month = 1;
+			day = doy;
+			do {
+				dim = this._getDaysInMonth(year, month - 1);
+				if (day <= dim) {
+					break;
+				}
+				month++;
+				day -= dim;
+			} while (true);
+		}
+
+		date = this._daylightSavingAdjust(new Date(year, month - 1, day));
+		if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
+			throw "Invalid date"; // E.g. 31/02/00
+		}
+		return date;
+	},
+
+	/* Standard date formats. */
+	ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601)
+	COOKIE: "D, dd M yy",
+	ISO_8601: "yy-mm-dd",
+	RFC_822: "D, d M y",
+	RFC_850: "DD, dd-M-y",
+	RFC_1036: "D, d M y",
+	RFC_1123: "D, d M yy",
+	RFC_2822: "D, d M yy",
+	RSS: "D, d M y", // RFC 822
+	TICKS: "!",
+	TIMESTAMP: "@",
+	W3C: "yy-mm-dd", // ISO 8601
+
+	_ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) +
+		Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000),
+
+	/* Format a date object into a string value.
+	 * The format can be combinations of the following:
+	 * d  - day of month (no leading zero)
+	 * dd - day of month (two digit)
+	 * o  - day of year (no leading zeros)
+	 * oo - day of year (three digit)
+	 * D  - day name short
+	 * DD - day name long
+	 * m  - month of year (no leading zero)
+	 * mm - month of year (two digit)
+	 * M  - month name short
+	 * MM - month name long
+	 * y  - year (two digit)
+	 * yy - year (four digit)
+	 * @ - Unix timestamp (ms since 01/01/1970)
+	 * ! - Windows ticks (100ns since 01/01/0001)
+	 * "..." - literal text
+	 * '' - single quote
+	 *
+	 * @param  format string - the desired format of the date
+	 * @param  date Date - the date value to format
+	 * @param  settings Object - attributes include:
+	 *					dayNamesShort	string[7] - abbreviated names of the days from Sunday (optional)
+	 *					dayNames		string[7] - names of the days from Sunday (optional)
+	 *					monthNamesShort string[12] - abbreviated names of the months (optional)
+	 *					monthNames		string[12] - names of the months (optional)
+	 * @return  string - the date in the above format
+	 */
+	formatDate: function (format, date, settings) {
+		if (!date) {
+			return "";
+		}
+
+		var iFormat,
+			dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,
+			dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,
+			monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,
+			monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,
+			// Check whether a format character is doubled
+			lookAhead = function(match) {
+				var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
+				if (matches) {
+					iFormat++;
+				}
+				return matches;
+			},
+			// Format a number, with leading zero if necessary
+			formatNumber = function(match, value, len) {
+				var num = "" + value;
+				if (lookAhead(match)) {
+					while (num.length < len) {
+						num = "0" + num;
+					}
+				}
+				return num;
+			},
+			// Format a name, short or long as requested
+			formatName = function(match, value, shortNames, longNames) {
+				return (lookAhead(match) ? longNames[value] : shortNames[value]);
+			},
+			output = "",
+			literal = false;
+
+		if (date) {
+			for (iFormat = 0; iFormat < format.length; iFormat++) {
+				if (literal) {
+					if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
+						literal = false;
+					} else {
+						output += format.charAt(iFormat);
+					}
+				} else {
+					switch (format.charAt(iFormat)) {
+						case "d":
+							output += formatNumber("d", date.getDate(), 2);
+							break;
+						case "D":
+							output += formatName("D", date.getDay(), dayNamesShort, dayNames);
+							break;
+						case "o":
+							output += formatNumber("o",
+								Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3);
+							break;
+						case "m":
+							output += formatNumber("m", date.getMonth() + 1, 2);
+							break;
+						case "M":
+							output += formatName("M", date.getMonth(), monthNamesShort, monthNames);
+							break;
+						case "y":
+							output += (lookAhead("y") ? date.getFullYear() :
+								(date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100);
+							break;
+						case "@":
+							output += date.getTime();
+							break;
+						case "!":
+							output += date.getTime() * 10000 + this._ticksTo1970;
+							break;
+						case "'":
+							if (lookAhead("'")) {
+								output += "'";
+							} else {
+								literal = true;
+							}
+							break;
+						default:
+							output += format.charAt(iFormat);
+					}
+				}
+			}
+		}
+		return output;
+	},
+
+	/* Extract all possible characters from the date format. */
+	_possibleChars: function (format) {
+		var iFormat,
+			chars = "",
+			literal = false,
+			// Check whether a format character is doubled
+			lookAhead = function(match) {
+				var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
+				if (matches) {
+					iFormat++;
+				}
+				return matches;
+			};
+
+		for (iFormat = 0; iFormat < format.length; iFormat++) {
+			if (literal) {
+				if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
+					literal = false;
+				} else {
+					chars += format.charAt(iFormat);
+				}
+			} else {
+				switch (format.charAt(iFormat)) {
+					case "d": case "m": case "y": case "@":
+						chars += "0123456789";
+						break;
+					case "D": case "M":
+						return null; // Accept anything
+					case "'":
+						if (lookAhead("'")) {
+							chars += "'";
+						} else {
+							literal = true;
+						}
+						break;
+					default:
+						chars += format.charAt(iFormat);
+				}
+			}
+		}
+		return chars;
+	},
+
+	/* Get a setting value, defaulting if necessary. */
+	_get: function(inst, name) {
+		return inst.settings[name] !== undefined ?
+			inst.settings[name] : this._defaults[name];
+	},
+
+	/* Parse existing date and initialise date picker. */
+	_setDateFromField: function(inst, noDefault) {
+		if (inst.input.val() === inst.lastVal) {
+			return;
+		}
+
+		var dateFormat = this._get(inst, "dateFormat"),
+			dates = inst.lastVal = inst.input ? inst.input.val() : null,
+			defaultDate = this._getDefaultDate(inst),
+			date = defaultDate,
+			settings = this._getFormatConfig(inst);
+
+		try {
+			date = this.parseDate(dateFormat, dates, settings) || defaultDate;
+		} catch (event) {
+			dates = (noDefault ? "" : dates);
+		}
+		inst.selectedDay = date.getDate();
+		inst.drawMonth = inst.selectedMonth = date.getMonth();
+		inst.drawYear = inst.selectedYear = date.getFullYear();
+		inst.currentDay = (dates ? date.getDate() : 0);
+		inst.currentMonth = (dates ? date.getMonth() : 0);
+		inst.currentYear = (dates ? date.getFullYear() : 0);
+		this._adjustInstDate(inst);
+	},
+
+	/* Retrieve the default date shown on opening. */
+	_getDefaultDate: function(inst) {
+		return this._restrictMinMax(inst,
+			this._determineDate(inst, this._get(inst, "defaultDate"), new Date()));
+	},
+
+	/* A date may be specified as an exact value or a relative one. */
+	_determineDate: function(inst, date, defaultDate) {
+		var offsetNumeric = function(offset) {
+				var date = new Date();
+				date.setDate(date.getDate() + offset);
+				return date;
+			},
+			offsetString = function(offset) {
+				try {
+					return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"),
+						offset, $.datepicker._getFormatConfig(inst));
+				}
+				catch (e) {
+					// Ignore
+				}
+
+				var date = (offset.toLowerCase().match(/^c/) ?
+					$.datepicker._getDate(inst) : null) || new Date(),
+					year = date.getFullYear(),
+					month = date.getMonth(),
+					day = date.getDate(),
+					pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,
+					matches = pattern.exec(offset);
+
+				while (matches) {
+					switch (matches[2] || "d") {
+						case "d" : case "D" :
+							day += parseInt(matches[1],10); break;
+						case "w" : case "W" :
+							day += parseInt(matches[1],10) * 7; break;
+						case "m" : case "M" :
+							month += parseInt(matches[1],10);
+							day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
+							break;
+						case "y": case "Y" :
+							year += parseInt(matches[1],10);
+							day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
+							break;
+					}
+					matches = pattern.exec(offset);
+				}
+				return new Date(year, month, day);
+			},
+			newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) :
+				(typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime()))));
+
+		newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate);
+		if (newDate) {
+			newDate.setHours(0);
+			newDate.setMinutes(0);
+			newDate.setSeconds(0);
+			newDate.setMilliseconds(0);
+		}
+		return this._daylightSavingAdjust(newDate);
+	},
+
+	/* Handle switch to/from daylight saving.
+	 * Hours may be non-zero on daylight saving cut-over:
+	 * > 12 when midnight changeover, but then cannot generate
+	 * midnight datetime, so jump to 1AM, otherwise reset.
+	 * @param  date  (Date) the date to check
+	 * @return  (Date) the corrected date
+	 */
+	_daylightSavingAdjust: function(date) {
+		if (!date) {
+			return null;
+		}
+		date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0);
+		return date;
+	},
+
+	/* Set the date(s) directly. */
+	_setDate: function(inst, date, noChange) {
+		var clear = !date,
+			origMonth = inst.selectedMonth,
+			origYear = inst.selectedYear,
+			newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date()));
+
+		inst.selectedDay = inst.currentDay = newDate.getDate();
+		inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth();
+		inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear();
+		if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) {
+			this._notifyChange(inst);
+		}
+		this._adjustInstDate(inst);
+		if (inst.input) {
+			inst.input.val(clear ? "" : this._formatDate(inst));
+		}
+	},
+
+	/* Retrieve the date(s) directly. */
+	_getDate: function(inst) {
+		var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null :
+			this._daylightSavingAdjust(new Date(
+			inst.currentYear, inst.currentMonth, inst.currentDay)));
+			return startDate;
+	},
+
+	/* Attach the onxxx handlers.  These are declared statically so
+	 * they work with static code transformers like Caja.
+	 */
+	_attachHandlers: function(inst) {
+		var stepMonths = this._get(inst, "stepMonths"),
+			id = "#" + inst.id.replace( /\\\\/g, "\\" );
+		inst.dpDiv.find("[data-handler]").map(function () {
+			var handler = {
+				prev: function () {
+					$.datepicker._adjustDate(id, -stepMonths, "M");
+				},
+				next: function () {
+					$.datepicker._adjustDate(id, +stepMonths, "M");
+				},
+				hide: function () {
+					$.datepicker._hideDatepicker();
+				},
+				today: function () {
+					$.datepicker._gotoToday(id);
+				},
+				selectDay: function () {
+					$.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this);
+					return false;
+				},
+				selectMonth: function () {
+					$.datepicker._selectMonthYear(id, this, "M");
+					return false;
+				},
+				selectYear: function () {
+					$.datepicker._selectMonthYear(id, this, "Y");
+					return false;
+				}
+			};
+			$(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]);
+		});
+	},
+
+	/* Generate the HTML for the current state of the date picker. */
+	_generateHTML: function(inst) {
+		var maxDraw, prevText, prev, nextText, next, currentText, gotoDate,
+			controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin,
+			monthNames, monthNamesShort, beforeShowDay, showOtherMonths,
+			selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate,
+			cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows,
+			printDate, dRow, tbody, daySettings, otherMonth, unselectable,
+			tempDate = new Date(),
+			today = this._daylightSavingAdjust(
+				new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time
+			isRTL = this._get(inst, "isRTL"),
+			showButtonPanel = this._get(inst, "showButtonPanel"),
+			hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"),
+			navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"),
+			numMonths = this._getNumberOfMonths(inst),
+			showCurrentAtPos = this._get(inst, "showCurrentAtPos"),
+			stepMonths = this._get(inst, "stepMonths"),
+			isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1),
+			currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) :
+				new Date(inst.currentYear, inst.currentMonth, inst.currentDay))),
+			minDate = this._getMinMaxDate(inst, "min"),
+			maxDate = this._getMinMaxDate(inst, "max"),
+			drawMonth = inst.drawMonth - showCurrentAtPos,
+			drawYear = inst.drawYear;
+
+		if (drawMonth < 0) {
+			drawMonth += 12;
+			drawYear--;
+		}
+		if (maxDate) {
+			maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(),
+				maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate()));
+			maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw);
+			while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) {
+				drawMonth--;
+				if (drawMonth < 0) {
+					drawMonth = 11;
+					drawYear--;
+				}
+			}
+		}
+		inst.drawMonth = drawMonth;
+		inst.drawYear = drawYear;
+
+		prevText = this._get(inst, "prevText");
+		prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText,
+			this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)),
+			this._getFormatConfig(inst)));
+
+		prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ?
+			"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" +
+			" title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" :
+			(hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+ prevText +"'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>"));
+
+		nextText = this._get(inst, "nextText");
+		nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText,
+			this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)),
+			this._getFormatConfig(inst)));
+
+		next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ?
+			"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" +
+			" title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" :
+			(hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+ nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>"));
+
+		currentText = this._get(inst, "currentText");
+		gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today);
+		currentText = (!navigationAsDateFormat ? currentText :
+			this.formatDate(currentText, gotoDate, this._getFormatConfig(inst)));
+
+		controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" +
+			this._get(inst, "closeText") + "</button>" : "");
+
+		buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") +
+			(this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" +
+			">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : "";
+
+		firstDay = parseInt(this._get(inst, "firstDay"),10);
+		firstDay = (isNaN(firstDay) ? 0 : firstDay);
+
+		showWeek = this._get(inst, "showWeek");
+		dayNames = this._get(inst, "dayNames");
+		dayNamesMin = this._get(inst, "dayNamesMin");
+		monthNames = this._get(inst, "monthNames");
+		monthNamesShort = this._get(inst, "monthNamesShort");
+		beforeShowDay = this._get(inst, "beforeShowDay");
+		showOtherMonths = this._get(inst, "showOtherMonths");
+		selectOtherMonths = this._get(inst, "selectOtherMonths");
+		defaultDate = this._getDefaultDate(inst);
+		html = "";
+		dow;
+		for (row = 0; row < numMonths[0]; row++) {
+			group = "";
+			this.maxRows = 4;
+			for (col = 0; col < numMonths[1]; col++) {
+				selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay));
+				cornerClass = " ui-corner-all";
+				calender = "";
+				if (isMultiMonth) {
+					calender += "<div class='ui-datepicker-group";
+					if (numMonths[1] > 1) {
+						switch (col) {
+							case 0: calender += " ui-datepicker-group-first";
+								cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); break;
+							case numMonths[1]-1: calender += " ui-datepicker-group-last";
+								cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); break;
+							default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break;
+						}
+					}
+					calender += "'>";
+				}
+				calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" +
+					(/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") +
+					(/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") +
+					this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate,
+					row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers
+					"</div><table class='ui-datepicker-calendar'><thead>" +
+					"<tr>";
+				thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : "");
+				for (dow = 0; dow < 7; dow++) { // days of the week
+					day = (dow + firstDay) % 7;
+					thead += "<th scope='col'" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" +
+						"<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>";
+				}
+				calender += thead + "</tr></thead><tbody>";
+				daysInMonth = this._getDaysInMonth(drawYear, drawMonth);
+				if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) {
+					inst.selectedDay = Math.min(inst.selectedDay, daysInMonth);
+				}
+				leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7;
+				curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate
+				numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043)
+				this.maxRows = numRows;
+				printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays));
+				for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows
+					calender += "<tr>";
+					tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" +
+						this._get(inst, "calculateWeek")(printDate) + "</td>");
+					for (dow = 0; dow < 7; dow++) { // create date picker days
+						daySettings = (beforeShowDay ?
+							beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]);
+						otherMonth = (printDate.getMonth() !== drawMonth);
+						unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] ||
+							(minDate && printDate < minDate) || (maxDate && printDate > maxDate);
+						tbody += "<td class='" +
+							((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends
+							(otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months
+							((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key
+							(defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ?
+							// or defaultDate is current printedDate and defaultDate is selectedDate
+							" " + this._dayOverClass : "") + // highlight selected day
+							(unselectable ? " " + this._unselectableClass + " ui-state-disabled": "") +  // highlight unselectable days
+							(otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates
+							(printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day
+							(printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different)
+							((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "'") + "'" : "") + // cell title
+							(unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions
+							(otherMonth && !showOtherMonths ? "&#xa0;" : // display for other months
+							(unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" +
+							(printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") +
+							(printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day
+							(otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months
+							"' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date
+						printDate.setDate(printDate.getDate() + 1);
+						printDate = this._daylightSavingAdjust(printDate);
+					}
+					calender += tbody + "</tr>";
+				}
+				drawMonth++;
+				if (drawMonth > 11) {
+					drawMonth = 0;
+					drawYear++;
+				}
+				calender += "</tbody></table>" + (isMultiMonth ? "</div>" +
+							((numMonths[0] > 0 && col === numMonths[1]-1) ? "<div class='ui-datepicker-row-break'></div>" : "") : "");
+				group += calender;
+			}
+			html += group;
+		}
+		html += buttonPanel;
+		inst._keyEvent = false;
+		return html;
+	},
+
+	/* Generate the month and year header. */
+	_generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate,
+			secondary, monthNames, monthNamesShort) {
+
+		var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear,
+			changeMonth = this._get(inst, "changeMonth"),
+			changeYear = this._get(inst, "changeYear"),
+			showMonthAfterYear = this._get(inst, "showMonthAfterYear"),
+			html = "<div class='ui-datepicker-title'>",
+			monthHtml = "";
+
+		// month selection
+		if (secondary || !changeMonth) {
+			monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>";
+		} else {
+			inMinYear = (minDate && minDate.getFullYear() === drawYear);
+			inMaxYear = (maxDate && maxDate.getFullYear() === drawYear);
+			monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>";
+			for ( month = 0; month < 12; month++) {
+				if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) {
+					monthHtml += "<option value='" + month + "'" +
+						(month === drawMonth ? " selected='selected'" : "") +
+						">" + monthNamesShort[month] + "</option>";
+				}
+			}
+			monthHtml += "</select>";
+		}
+
+		if (!showMonthAfterYear) {
+			html += monthHtml + (secondary || !(changeMonth && changeYear) ? "&#xa0;" : "");
+		}
+
+		// year selection
+		if ( !inst.yearshtml ) {
+			inst.yearshtml = "";
+			if (secondary || !changeYear) {
+				html += "<span class='ui-datepicker-year'>" + drawYear + "</span>";
+			} else {
+				// determine range of years to display
+				years = this._get(inst, "yearRange").split(":");
+				thisYear = new Date().getFullYear();
+				determineYear = function(value) {
+					var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) :
+						(value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) :
+						parseInt(value, 10)));
+					return (isNaN(year) ? thisYear : year);
+				};
+				year = determineYear(years[0]);
+				endYear = Math.max(year, determineYear(years[1] || ""));
+				year = (minDate ? Math.max(year, minDate.getFullYear()) : year);
+				endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear);
+				inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";
+				for (; year <= endYear; year++) {
+					inst.yearshtml += "<option value='" + year + "'" +
+						(year === drawYear ? " selected='selected'" : "") +
+						">" + year + "</option>";
+				}
+				inst.yearshtml += "</select>";
+
+				html += inst.yearshtml;
+				inst.yearshtml = null;
+			}
+		}
+
+		html += this._get(inst, "yearSuffix");
+		if (showMonthAfterYear) {
+			html += (secondary || !(changeMonth && changeYear) ? "&#xa0;" : "") + monthHtml;
+		}
+		html += "</div>"; // Close datepicker_header
+		return html;
+	},
+
+	/* Adjust one of the date sub-fields. */
+	_adjustInstDate: function(inst, offset, period) {
+		var year = inst.drawYear + (period === "Y" ? offset : 0),
+			month = inst.drawMonth + (period === "M" ? offset : 0),
+			day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0),
+			date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day)));
+
+		inst.selectedDay = date.getDate();
+		inst.drawMonth = inst.selectedMonth = date.getMonth();
+		inst.drawYear = inst.selectedYear = date.getFullYear();
+		if (period === "M" || period === "Y") {
+			this._notifyChange(inst);
+		}
+	},
+
+	/* Ensure a date is within any min/max bounds. */
+	_restrictMinMax: function(inst, date) {
+		var minDate = this._getMinMaxDate(inst, "min"),
+			maxDate = this._getMinMaxDate(inst, "max"),
+			newDate = (minDate && date < minDate ? minDate : date);
+		return (maxDate && newDate > maxDate ? maxDate : newDate);
+	},
+
+	/* Notify change of month/year. */
+	_notifyChange: function(inst) {
+		var onChange = this._get(inst, "onChangeMonthYear");
+		if (onChange) {
+			onChange.apply((inst.input ? inst.input[0] : null),
+				[inst.selectedYear, inst.selectedMonth + 1, inst]);
+		}
+	},
+
+	/* Determine the number of months to show. */
+	_getNumberOfMonths: function(inst) {
+		var numMonths = this._get(inst, "numberOfMonths");
+		return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths));
+	},
+
+	/* Determine the current maximum date - ensure no time components are set. */
+	_getMinMaxDate: function(inst, minMax) {
+		return this._determineDate(inst, this._get(inst, minMax + "Date"), null);
+	},
+
+	/* Find the number of days in a given month. */
+	_getDaysInMonth: function(year, month) {
+		return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate();
+	},
+
+	/* Find the day of the week of the first of a month. */
+	_getFirstDayOfMonth: function(year, month) {
+		return new Date(year, month, 1).getDay();
+	},
+
+	/* Determines if we should allow a "next/prev" month display change. */
+	_canAdjustMonth: function(inst, offset, curYear, curMonth) {
+		var numMonths = this._getNumberOfMonths(inst),
+			date = this._daylightSavingAdjust(new Date(curYear,
+			curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1));
+
+		if (offset < 0) {
+			date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth()));
+		}
+		return this._isInRange(inst, date);
+	},
+
+	/* Is the given date in the accepted range? */
+	_isInRange: function(inst, date) {
+		var yearSplit, currentYear,
+			minDate = this._getMinMaxDate(inst, "min"),
+			maxDate = this._getMinMaxDate(inst, "max"),
+			minYear = null,
+			maxYear = null,
+			years = this._get(inst, "yearRange");
+			if (years){
+				yearSplit = years.split(":");
+				currentYear = new Date().getFullYear();
+				minYear = parseInt(yearSplit[0], 10);
+				maxYear = parseInt(yearSplit[1], 10);
+				if ( yearSplit[0].match(/[+\-].*/) ) {
+					minYear += currentYear;
+				}
+				if ( yearSplit[1].match(/[+\-].*/) ) {
+					maxYear += currentYear;
+				}
+			}
+
+		return ((!minDate || date.getTime() >= minDate.getTime()) &&
+			(!maxDate || date.getTime() <= maxDate.getTime()) &&
+			(!minYear || date.getFullYear() >= minYear) &&
+			(!maxYear || date.getFullYear() <= maxYear));
+	},
+
+	/* Provide the configuration settings for formatting/parsing. */
+	_getFormatConfig: function(inst) {
+		var shortYearCutoff = this._get(inst, "shortYearCutoff");
+		shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff :
+			new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10));
+		return {shortYearCutoff: shortYearCutoff,
+			dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"),
+			monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")};
+	},
+
+	/* Format the given date for display. */
+	_formatDate: function(inst, day, month, year) {
+		if (!day) {
+			inst.currentDay = inst.selectedDay;
+			inst.currentMonth = inst.selectedMonth;
+			inst.currentYear = inst.selectedYear;
+		}
+		var date = (day ? (typeof day === "object" ? day :
+			this._daylightSavingAdjust(new Date(year, month, day))) :
+			this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay)));
+		return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst));
+	}
+});
+
+/*
+ * Bind hover events for datepicker elements.
+ * Done via delegate so the binding only occurs once in the lifetime of the parent div.
+ * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker.
+ */
+function datepicker_bindHover(dpDiv) {
+	var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";
+	return dpDiv.delegate(selector, "mouseout", function() {
+			$(this).removeClass("ui-state-hover");
+			if (this.className.indexOf("ui-datepicker-prev") !== -1) {
+				$(this).removeClass("ui-datepicker-prev-hover");
+			}
+			if (this.className.indexOf("ui-datepicker-next") !== -1) {
+				$(this).removeClass("ui-datepicker-next-hover");
+			}
+		})
+		.delegate( selector, "mouseover", datepicker_handleMouseover );
+}
+
+function datepicker_handleMouseover() {
+	if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline? datepicker_instActive.dpDiv.parent()[0] : datepicker_instActive.input[0])) {
+		$(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");
+		$(this).addClass("ui-state-hover");
+		if (this.className.indexOf("ui-datepicker-prev") !== -1) {
+			$(this).addClass("ui-datepicker-prev-hover");
+		}
+		if (this.className.indexOf("ui-datepicker-next") !== -1) {
+			$(this).addClass("ui-datepicker-next-hover");
+		}
+	}
+}
+
+/* jQuery extend now ignores nulls! */
+function datepicker_extendRemove(target, props) {
+	$.extend(target, props);
+	for (var name in props) {
+		if (props[name] == null) {
+			target[name] = props[name];
+		}
+	}
+	return target;
+}
+
+/* Invoke the datepicker functionality.
+   @param  options  string - a command, optionally followed by additional parameters or
+					Object - settings for attaching new datepicker functionality
+   @return  jQuery object */
+$.fn.datepicker = function(options){
+
+	/* Verify an empty collection wasn't passed - Fixes #6976 */
+	if ( !this.length ) {
+		return this;
+	}
+
+	/* Initialise the date picker. */
+	if (!$.datepicker.initialized) {
+		$(document).mousedown($.datepicker._checkExternalClick);
+		$.datepicker.initialized = true;
+	}
+
+	/* Append datepicker main container to body if not exist. */
+	if ($("#"+$.datepicker._mainDivId).length === 0) {
+		$("body").append($.datepicker.dpDiv);
+	}
+
+	var otherArgs = Array.prototype.slice.call(arguments, 1);
+	if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) {
+		return $.datepicker["_" + options + "Datepicker"].
+			apply($.datepicker, [this[0]].concat(otherArgs));
+	}
+	if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") {
+		return $.datepicker["_" + options + "Datepicker"].
+			apply($.datepicker, [this[0]].concat(otherArgs));
+	}
+	return this.each(function() {
+		typeof options === "string" ?
+			$.datepicker["_" + options + "Datepicker"].
+				apply($.datepicker, [this].concat(otherArgs)) :
+			$.datepicker._attachDatepicker(this, options);
+	});
+};
+
+$.datepicker = new Datepicker(); // singleton instance
+$.datepicker.initialized = false;
+$.datepicker.uuid = new Date().getTime();
+$.datepicker.version = "1.11.2";
+
+var datepicker = $.datepicker;
+
+
+/*!
+ * jQuery UI Dialog 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/dialog/
+ */
+
+
+var dialog = $.widget( "ui.dialog", {
+	version: "1.11.2",
+	options: {
+		appendTo: "body",
+		autoOpen: true,
+		buttons: [],
+		closeOnEscape: true,
+		closeText: "Close",
+		dialogClass: "",
+		draggable: true,
+		hide: null,
+		height: "auto",
+		maxHeight: null,
+		maxWidth: null,
+		minHeight: 150,
+		minWidth: 150,
+		modal: false,
+		position: {
+			my: "center",
+			at: "center",
+			of: window,
+			collision: "fit",
+			// Ensure the titlebar is always visible
+			using: function( pos ) {
+				var topOffset = $( this ).css( pos ).offset().top;
+				if ( topOffset < 0 ) {
+					$( this ).css( "top", pos.top - topOffset );
+				}
+			}
+		},
+		resizable: true,
+		show: null,
+		title: null,
+		width: 300,
+
+		// callbacks
+		beforeClose: null,
+		close: null,
+		drag: null,
+		dragStart: null,
+		dragStop: null,
+		focus: null,
+		open: null,
+		resize: null,
+		resizeStart: null,
+		resizeStop: null
+	},
+
+	sizeRelatedOptions: {
+		buttons: true,
+		height: true,
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true,
+		width: true
+	},
+
+	resizableRelatedOptions: {
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true
+	},
+
+	_create: function() {
+		this.originalCss = {
+			display: this.element[ 0 ].style.display,
+			width: this.element[ 0 ].style.width,
+			minHeight: this.element[ 0 ].style.minHeight,
+			maxHeight: this.element[ 0 ].style.maxHeight,
+			height: this.element[ 0 ].style.height
+		};
+		this.originalPosition = {
+			parent: this.element.parent(),
+			index: this.element.parent().children().index( this.element )
+		};
+		this.originalTitle = this.element.attr( "title" );
+		this.options.title = this.options.title || this.originalTitle;
+
+		this._createWrapper();
+
+		this.element
+			.show()
+			.removeAttr( "title" )
+			.addClass( "ui-dialog-content ui-widget-content" )
+			.appendTo( this.uiDialog );
+
+		this._createTitlebar();
+		this._createButtonPane();
+
+		if ( this.options.draggable && $.fn.draggable ) {
+			this._makeDraggable();
+		}
+		if ( this.options.resizable && $.fn.resizable ) {
+			this._makeResizable();
+		}
+
+		this._isOpen = false;
+
+		this._trackFocus();
+	},
+
+	_init: function() {
+		if ( this.options.autoOpen ) {
+			this.open();
+		}
+	},
+
+	_appendTo: function() {
+		var element = this.options.appendTo;
+		if ( element && (element.jquery || element.nodeType) ) {
+			return $( element );
+		}
+		return this.document.find( element || "body" ).eq( 0 );
+	},
+
+	_destroy: function() {
+		var next,
+			originalPosition = this.originalPosition;
+
+		this._destroyOverlay();
+
+		this.element
+			.removeUniqueId()
+			.removeClass( "ui-dialog-content ui-widget-content" )
+			.css( this.originalCss )
+			// Without detaching first, the following becomes really slow
+			.detach();
+
+		this.uiDialog.stop( true, true ).remove();
+
+		if ( this.originalTitle ) {
+			this.element.attr( "title", this.originalTitle );
+		}
+
+		next = originalPosition.parent.children().eq( originalPosition.index );
+		// Don't try to place the dialog next to itself (#8613)
+		if ( next.length && next[ 0 ] !== this.element[ 0 ] ) {
+			next.before( this.element );
+		} else {
+			originalPosition.parent.append( this.element );
+		}
+	},
+
+	widget: function() {
+		return this.uiDialog;
+	},
+
+	disable: $.noop,
+	enable: $.noop,
+
+	close: function( event ) {
+		var activeElement,
+			that = this;
+
+		if ( !this._isOpen || this._trigger( "beforeClose", event ) === false ) {
+			return;
+		}
+
+		this._isOpen = false;
+		this._focusedElement = null;
+		this._destroyOverlay();
+		this._untrackInstance();
+
+		if ( !this.opener.filter( ":focusable" ).focus().length ) {
+
+			// support: IE9
+			// IE9 throws an "Unspecified error" accessing document.activeElement from an <iframe>
+			try {
+				activeElement = this.document[ 0 ].activeElement;
+
+				// Support: IE9, IE10
+				// If the <body> is blurred, IE will switch windows, see #4520
+				if ( activeElement && activeElement.nodeName.toLowerCase() !== "body" ) {
+
+					// Hiding a focused element doesn't trigger blur in WebKit
+					// so in case we have nothing to focus on, explicitly blur the active element
+					// https://bugs.webkit.org/show_bug.cgi?id=47182
+					$( activeElement ).blur();
+				}
+			} catch ( error ) {}
+		}
+
+		this._hide( this.uiDialog, this.options.hide, function() {
+			that._trigger( "close", event );
+		});
+	},
+
+	isOpen: function() {
+		return this._isOpen;
+	},
+
+	moveToTop: function() {
+		this._moveToTop();
+	},
+
+	_moveToTop: function( event, silent ) {
+		var moved = false,
+			zIndicies = this.uiDialog.siblings( ".ui-front:visible" ).map(function() {
+				return +$( this ).css( "z-index" );
+			}).get(),
+			zIndexMax = Math.max.apply( null, zIndicies );
+
+		if ( zIndexMax >= +this.uiDialog.css( "z-index" ) ) {
+			this.uiDialog.css( "z-index", zIndexMax + 1 );
+			moved = true;
+		}
+
+		if ( moved && !silent ) {
+			this._trigger( "focus", event );
+		}
+		return moved;
+	},
+
+	open: function() {
+		var that = this;
+		if ( this._isOpen ) {
+			if ( this._moveToTop() ) {
+				this._focusTabbable();
+			}
+			return;
+		}
+
+		this._isOpen = true;
+		this.opener = $( this.document[ 0 ].activeElement );
+
+		this._size();
+		this._position();
+		this._createOverlay();
+		this._moveToTop( null, true );
+
+		// Ensure the overlay is moved to the top with the dialog, but only when
+		// opening. The overlay shouldn't move after the dialog is open so that
+		// modeless dialogs opened after the modal dialog stack properly.
+		if ( this.overlay ) {
+			this.overlay.css( "z-index", this.uiDialog.css( "z-index" ) - 1 );
+		}
+
+		this._show( this.uiDialog, this.options.show, function() {
+			that._focusTabbable();
+			that._trigger( "focus" );
+		});
+
+		// Track the dialog immediately upon openening in case a focus event
+		// somehow occurs outside of the dialog before an element inside the
+		// dialog is focused (#10152)
+		this._makeFocusTarget();
+
+		this._trigger( "open" );
+	},
+
+	_focusTabbable: function() {
+		// Set focus to the first match:
+		// 1. An element that was focused previously
+		// 2. First element inside the dialog matching [autofocus]
+		// 3. Tabbable element inside the content element
+		// 4. Tabbable element inside the buttonpane
+		// 5. The close button
+		// 6. The dialog itself
+		var hasFocus = this._focusedElement;
+		if ( !hasFocus ) {
+			hasFocus = this.element.find( "[autofocus]" );
+		}
+		if ( !hasFocus.length ) {
+			hasFocus = this.element.find( ":tabbable" );
+		}
+		if ( !hasFocus.length ) {
+			hasFocus = this.uiDialogButtonPane.find( ":tabbable" );
+		}
+		if ( !hasFocus.length ) {
+			hasFocus = this.uiDialogTitlebarClose.filter( ":tabbable" );
+		}
+		if ( !hasFocus.length ) {
+			hasFocus = this.uiDialog;
+		}
+		hasFocus.eq( 0 ).focus();
+	},
+
+	_keepFocus: function( event ) {
+		function checkFocus() {
+			var activeElement = this.document[0].activeElement,
+				isActive = this.uiDialog[0] === activeElement ||
+					$.contains( this.uiDialog[0], activeElement );
+			if ( !isActive ) {
+				this._focusTabbable();
+			}
+		}
+		event.preventDefault();
+		checkFocus.call( this );
+		// support: IE
+		// IE <= 8 doesn't prevent moving focus even with event.preventDefault()
+		// so we check again later
+		this._delay( checkFocus );
+	},
+
+	_createWrapper: function() {
+		this.uiDialog = $("<div>")
+			.addClass( "ui-dialog ui-widget ui-widget-content ui-corner-all ui-front " +
+				this.options.dialogClass )
+			.hide()
+			.attr({
+				// Setting tabIndex makes the div focusable
+				tabIndex: -1,
+				role: "dialog"
+			})
+			.appendTo( this._appendTo() );
+
+		this._on( this.uiDialog, {
+			keydown: function( event ) {
+				if ( this.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+						event.keyCode === $.ui.keyCode.ESCAPE ) {
+					event.preventDefault();
+					this.close( event );
+					return;
+				}
+
+				// prevent tabbing out of dialogs
+				if ( event.keyCode !== $.ui.keyCode.TAB || event.isDefaultPrevented() ) {
+					return;
+				}
+				var tabbables = this.uiDialog.find( ":tabbable" ),
+					first = tabbables.filter( ":first" ),
+					last = tabbables.filter( ":last" );
+
+				if ( ( event.target === last[0] || event.target === this.uiDialog[0] ) && !event.shiftKey ) {
+					this._delay(function() {
+						first.focus();
+					});
+					event.preventDefault();
+				} else if ( ( event.target === first[0] || event.target === this.uiDialog[0] ) && event.shiftKey ) {
+					this._delay(function() {
+						last.focus();
+					});
+					event.preventDefault();
+				}
+			},
+			mousedown: function( event ) {
+				if ( this._moveToTop( event ) ) {
+					this._focusTabbable();
+				}
+			}
+		});
+
+		// We assume that any existing aria-describedby attribute means
+		// that the dialog content is marked up properly
+		// otherwise we brute force the content as the description
+		if ( !this.element.find( "[aria-describedby]" ).length ) {
+			this.uiDialog.attr({
+				"aria-describedby": this.element.uniqueId().attr( "id" )
+			});
+		}
+	},
+
+	_createTitlebar: function() {
+		var uiDialogTitle;
+
+		this.uiDialogTitlebar = $( "<div>" )
+			.addClass( "ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix" )
+			.prependTo( this.uiDialog );
+		this._on( this.uiDialogTitlebar, {
+			mousedown: function( event ) {
+				// Don't prevent click on close button (#8838)
+				// Focusing a dialog that is partially scrolled out of view
+				// causes the browser to scroll it into view, preventing the click event
+				if ( !$( event.target ).closest( ".ui-dialog-titlebar-close" ) ) {
+					// Dialog isn't getting focus when dragging (#8063)
+					this.uiDialog.focus();
+				}
+			}
+		});
+
+		// support: IE
+		// Use type="button" to prevent enter keypresses in textboxes from closing the
+		// dialog in IE (#9312)
+		this.uiDialogTitlebarClose = $( "<button type='button'></button>" )
+			.button({
+				label: this.options.closeText,
+				icons: {
+					primary: "ui-icon-closethick"
+				},
+				text: false
+			})
+			.addClass( "ui-dialog-titlebar-close" )
+			.appendTo( this.uiDialogTitlebar );
+		this._on( this.uiDialogTitlebarClose, {
+			click: function( event ) {
+				event.preventDefault();
+				this.close( event );
+			}
+		});
+
+		uiDialogTitle = $( "<span>" )
+			.uniqueId()
+			.addClass( "ui-dialog-title" )
+			.prependTo( this.uiDialogTitlebar );
+		this._title( uiDialogTitle );
+
+		this.uiDialog.attr({
+			"aria-labelledby": uiDialogTitle.attr( "id" )
+		});
+	},
+
+	_title: function( title ) {
+		if ( !this.options.title ) {
+			title.html( " " );
+		}
+		title.text( this.options.title );
+	},
+
+	_createButtonPane: function() {
+		this.uiDialogButtonPane = $( "<div>" )
+			.addClass( "ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" );
+
+		this.uiButtonSet = $( "<div>" )
+			.addClass( "ui-dialog-buttonset" )
+			.appendTo( this.uiDialogButtonPane );
+
+		this._createButtons();
+	},
+
+	_createButtons: function() {
+		var that = this,
+			buttons = this.options.buttons;
+
+		// if we already have a button pane, remove it
+		this.uiDialogButtonPane.remove();
+		this.uiButtonSet.empty();
+
+		if ( $.isEmptyObject( buttons ) || ($.isArray( buttons ) && !buttons.length) ) {
+			this.uiDialog.removeClass( "ui-dialog-buttons" );
+			return;
+		}
+
+		$.each( buttons, function( name, props ) {
+			var click, buttonOptions;
+			props = $.isFunction( props ) ?
+				{ click: props, text: name } :
+				props;
+			// Default to a non-submitting button
+			props = $.extend( { type: "button" }, props );
+			// Change the context for the click callback to be the main element
+			click = props.click;
+			props.click = function() {
+				click.apply( that.element[ 0 ], arguments );
+			};
+			buttonOptions = {
+				icons: props.icons,
+				text: props.showText
+			};
+			delete props.icons;
+			delete props.showText;
+			$( "<button></button>", props )
+				.button( buttonOptions )
+				.appendTo( that.uiButtonSet );
+		});
+		this.uiDialog.addClass( "ui-dialog-buttons" );
+		this.uiDialogButtonPane.appendTo( this.uiDialog );
+	},
+
+	_makeDraggable: function() {
+		var that = this,
+			options = this.options;
+
+		function filteredUi( ui ) {
+			return {
+				position: ui.position,
+				offset: ui.offset
+			};
+		}
+
+		this.uiDialog.draggable({
+			cancel: ".ui-dialog-content, .ui-dialog-titlebar-close",
+			handle: ".ui-dialog-titlebar",
+			containment: "document",
+			start: function( event, ui ) {
+				$( this ).addClass( "ui-dialog-dragging" );
+				that._blockFrames();
+				that._trigger( "dragStart", event, filteredUi( ui ) );
+			},
+			drag: function( event, ui ) {
+				that._trigger( "drag", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				var left = ui.offset.left - that.document.scrollLeft(),
+					top = ui.offset.top - that.document.scrollTop();
+
+				options.position = {
+					my: "left top",
+					at: "left" + (left >= 0 ? "+" : "") + left + " " +
+						"top" + (top >= 0 ? "+" : "") + top,
+					of: that.window
+				};
+				$( this ).removeClass( "ui-dialog-dragging" );
+				that._unblockFrames();
+				that._trigger( "dragStop", event, filteredUi( ui ) );
+			}
+		});
+	},
+
+	_makeResizable: function() {
+		var that = this,
+			options = this.options,
+			handles = options.resizable,
+			// .ui-resizable has position: relative defined in the stylesheet
+			// but dialogs have to use absolute or fixed positioning
+			position = this.uiDialog.css("position"),
+			resizeHandles = typeof handles === "string" ?
+				handles	:
+				"n,e,s,w,se,sw,ne,nw";
+
+		function filteredUi( ui ) {
+			return {
+				originalPosition: ui.originalPosition,
+				originalSize: ui.originalSize,
+				position: ui.position,
+				size: ui.size
+			};
+		}
+
+		this.uiDialog.resizable({
+			cancel: ".ui-dialog-content",
+			containment: "document",
+			alsoResize: this.element,
+			maxWidth: options.maxWidth,
+			maxHeight: options.maxHeight,
+			minWidth: options.minWidth,
+			minHeight: this._minHeight(),
+			handles: resizeHandles,
+			start: function( event, ui ) {
+				$( this ).addClass( "ui-dialog-resizing" );
+				that._blockFrames();
+				that._trigger( "resizeStart", event, filteredUi( ui ) );
+			},
+			resize: function( event, ui ) {
+				that._trigger( "resize", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				var offset = that.uiDialog.offset(),
+					left = offset.left - that.document.scrollLeft(),
+					top = offset.top - that.document.scrollTop();
+
+				options.height = that.uiDialog.height();
+				options.width = that.uiDialog.width();
+				options.position = {
+					my: "left top",
+					at: "left" + (left >= 0 ? "+" : "") + left + " " +
+						"top" + (top >= 0 ? "+" : "") + top,
+					of: that.window
+				};
+				$( this ).removeClass( "ui-dialog-resizing" );
+				that._unblockFrames();
+				that._trigger( "resizeStop", event, filteredUi( ui ) );
+			}
+		})
+		.css( "position", position );
+	},
+
+	_trackFocus: function() {
+		this._on( this.widget(), {
+			focusin: function( event ) {
+				this._makeFocusTarget();
+				this._focusedElement = $( event.target );
+			}
+		});
+	},
+
+	_makeFocusTarget: function() {
+		this._untrackInstance();
+		this._trackingInstances().unshift( this );
+	},
+
+	_untrackInstance: function() {
+		var instances = this._trackingInstances(),
+			exists = $.inArray( this, instances );
+		if ( exists !== -1 ) {
+			instances.splice( exists, 1 );
+		}
+	},
+
+	_trackingInstances: function() {
+		var instances = this.document.data( "ui-dialog-instances" );
+		if ( !instances ) {
+			instances = [];
+			this.document.data( "ui-dialog-instances", instances );
+		}
+		return instances;
+	},
+
+	_minHeight: function() {
+		var options = this.options;
+
+		return options.height === "auto" ?
+			options.minHeight :
+			Math.min( options.minHeight, options.height );
+	},
+
+	_position: function() {
+		// Need to show the dialog to get the actual offset in the position plugin
+		var isVisible = this.uiDialog.is( ":visible" );
+		if ( !isVisible ) {
+			this.uiDialog.show();
+		}
+		this.uiDialog.position( this.options.position );
+		if ( !isVisible ) {
+			this.uiDialog.hide();
+		}
+	},
+
+	_setOptions: function( options ) {
+		var that = this,
+			resize = false,
+			resizableOptions = {};
+
+		$.each( options, function( key, value ) {
+			that._setOption( key, value );
+
+			if ( key in that.sizeRelatedOptions ) {
+				resize = true;
+			}
+			if ( key in that.resizableRelatedOptions ) {
+				resizableOptions[ key ] = value;
+			}
+		});
+
+		if ( resize ) {
+			this._size();
+			this._position();
+		}
+		if ( this.uiDialog.is( ":data(ui-resizable)" ) ) {
+			this.uiDialog.resizable( "option", resizableOptions );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		var isDraggable, isResizable,
+			uiDialog = this.uiDialog;
+
+		if ( key === "dialogClass" ) {
+			uiDialog
+				.removeClass( this.options.dialogClass )
+				.addClass( value );
+		}
+
+		if ( key === "disabled" ) {
+			return;
+		}
+
+		this._super( key, value );
+
+		if ( key === "appendTo" ) {
+			this.uiDialog.appendTo( this._appendTo() );
+		}
+
+		if ( key === "buttons" ) {
+			this._createButtons();
+		}
+
+		if ( key === "closeText" ) {
+			this.uiDialogTitlebarClose.button({
+				// Ensure that we always pass a string
+				label: "" + value
+			});
+		}
+
+		if ( key === "draggable" ) {
+			isDraggable = uiDialog.is( ":data(ui-draggable)" );
+			if ( isDraggable && !value ) {
+				uiDialog.draggable( "destroy" );
+			}
+
+			if ( !isDraggable && value ) {
+				this._makeDraggable();
+			}
+		}
+
+		if ( key === "position" ) {
+			this._position();
+		}
+
+		if ( key === "resizable" ) {
+			// currently resizable, becoming non-resizable
+			isResizable = uiDialog.is( ":data(ui-resizable)" );
+			if ( isResizable && !value ) {
+				uiDialog.resizable( "destroy" );
+			}
+
+			// currently resizable, changing handles
+			if ( isResizable && typeof value === "string" ) {
+				uiDialog.resizable( "option", "handles", value );
+			}
+
+			// currently non-resizable, becoming resizable
+			if ( !isResizable && value !== false ) {
+				this._makeResizable();
+			}
+		}
+
+		if ( key === "title" ) {
+			this._title( this.uiDialogTitlebar.find( ".ui-dialog-title" ) );
+		}
+	},
+
+	_size: function() {
+		// If the user has resized the dialog, the .ui-dialog and .ui-dialog-content
+		// divs will both have width and height set, so we need to reset them
+		var nonContentHeight, minContentHeight, maxContentHeight,
+			options = this.options;
+
+		// Reset content sizing
+		this.element.show().css({
+			width: "auto",
+			minHeight: 0,
+			maxHeight: "none",
+			height: 0
+		});
+
+		if ( options.minWidth > options.width ) {
+			options.width = options.minWidth;
+		}
+
+		// reset wrapper sizing
+		// determine the height of all the non-content elements
+		nonContentHeight = this.uiDialog.css({
+				height: "auto",
+				width: options.width
+			})
+			.outerHeight();
+		minContentHeight = Math.max( 0, options.minHeight - nonContentHeight );
+		maxContentHeight = typeof options.maxHeight === "number" ?
+			Math.max( 0, options.maxHeight - nonContentHeight ) :
+			"none";
+
+		if ( options.height === "auto" ) {
+			this.element.css({
+				minHeight: minContentHeight,
+				maxHeight: maxContentHeight,
+				height: "auto"
+			});
+		} else {
+			this.element.height( Math.max( 0, options.height - nonContentHeight ) );
+		}
+
+		if ( this.uiDialog.is( ":data(ui-resizable)" ) ) {
+			this.uiDialog.resizable( "option", "minHeight", this._minHeight() );
+		}
+	},
+
+	_blockFrames: function() {
+		this.iframeBlocks = this.document.find( "iframe" ).map(function() {
+			var iframe = $( this );
+
+			return $( "<div>" )
+				.css({
+					position: "absolute",
+					width: iframe.outerWidth(),
+					height: iframe.outerHeight()
+				})
+				.appendTo( iframe.parent() )
+				.offset( iframe.offset() )[0];
+		});
+	},
+
+	_unblockFrames: function() {
+		if ( this.iframeBlocks ) {
+			this.iframeBlocks.remove();
+			delete this.iframeBlocks;
+		}
+	},
+
+	_allowInteraction: function( event ) {
+		if ( $( event.target ).closest( ".ui-dialog" ).length ) {
+			return true;
+		}
+
+		// TODO: Remove hack when datepicker implements
+		// the .ui-front logic (#8989)
+		return !!$( event.target ).closest( ".ui-datepicker" ).length;
+	},
+
+	_createOverlay: function() {
+		if ( !this.options.modal ) {
+			return;
+		}
+
+		// We use a delay in case the overlay is created from an
+		// event that we're going to be cancelling (#2804)
+		var isOpening = true;
+		this._delay(function() {
+			isOpening = false;
+		});
+
+		if ( !this.document.data( "ui-dialog-overlays" ) ) {
+
+			// Prevent use of anchors and inputs
+			// Using _on() for an event handler shared across many instances is
+			// safe because the dialogs stack and must be closed in reverse order
+			this._on( this.document, {
+				focusin: function( event ) {
+					if ( isOpening ) {
+						return;
+					}
+
+					if ( !this._allowInteraction( event ) ) {
+						event.preventDefault();
+						this._trackingInstances()[ 0 ]._focusTabbable();
+					}
+				}
+			});
+		}
+
+		this.overlay = $( "<div>" )
+			.addClass( "ui-widget-overlay ui-front" )
+			.appendTo( this._appendTo() );
+		this._on( this.overlay, {
+			mousedown: "_keepFocus"
+		});
+		this.document.data( "ui-dialog-overlays",
+			(this.document.data( "ui-dialog-overlays" ) || 0) + 1 );
+	},
+
+	_destroyOverlay: function() {
+		if ( !this.options.modal ) {
+			return;
+		}
+
+		if ( this.overlay ) {
+			var overlays = this.document.data( "ui-dialog-overlays" ) - 1;
+
+			if ( !overlays ) {
+				this.document
+					.unbind( "focusin" )
+					.removeData( "ui-dialog-overlays" );
+			} else {
+				this.document.data( "ui-dialog-overlays", overlays );
+			}
+
+			this.overlay.remove();
+			this.overlay = null;
+		}
+	}
+});
+
+
+/*!
+ * jQuery UI Progressbar 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/progressbar/
+ */
+
+
+var progressbar = $.widget( "ui.progressbar", {
+	version: "1.11.2",
+	options: {
+		max: 100,
+		value: 0,
+
+		change: null,
+		complete: null
+	},
+
+	min: 0,
+
+	_create: function() {
+		// Constrain initial value
+		this.oldValue = this.options.value = this._constrainedValue();
+
+		this.element
+			.addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" )
+			.attr({
+				// Only set static values, aria-valuenow and aria-valuemax are
+				// set inside _refreshValue()
+				role: "progressbar",
+				"aria-valuemin": this.min
+			});
+
+		this.valueDiv = $( "<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>" )
+			.appendTo( this.element );
+
+		this._refreshValue();
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-valuemin" )
+			.removeAttr( "aria-valuemax" )
+			.removeAttr( "aria-valuenow" );
+
+		this.valueDiv.remove();
+	},
+
+	value: function( newValue ) {
+		if ( newValue === undefined ) {
+			return this.options.value;
+		}
+
+		this.options.value = this._constrainedValue( newValue );
+		this._refreshValue();
+	},
+
+	_constrainedValue: function( newValue ) {
+		if ( newValue === undefined ) {
+			newValue = this.options.value;
+		}
+
+		this.indeterminate = newValue === false;
+
+		// sanitize value
+		if ( typeof newValue !== "number" ) {
+			newValue = 0;
+		}
+
+		return this.indeterminate ? false :
+			Math.min( this.options.max, Math.max( this.min, newValue ) );
+	},
+
+	_setOptions: function( options ) {
+		// Ensure "value" option is set after other values (like max)
+		var value = options.value;
+		delete options.value;
+
+		this._super( options );
+
+		this.options.value = this._constrainedValue( value );
+		this._refreshValue();
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "max" ) {
+			// Don't allow a max less than min
+			value = Math.max( this.min, value );
+		}
+		if ( key === "disabled" ) {
+			this.element
+				.toggleClass( "ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+		}
+		this._super( key, value );
+	},
+
+	_percentage: function() {
+		return this.indeterminate ? 100 : 100 * ( this.options.value - this.min ) / ( this.options.max - this.min );
+	},
+
+	_refreshValue: function() {
+		var value = this.options.value,
+			percentage = this._percentage();
+
+		this.valueDiv
+			.toggle( this.indeterminate || value > this.min )
+			.toggleClass( "ui-corner-right", value === this.options.max )
+			.width( percentage.toFixed(0) + "%" );
+
+		this.element.toggleClass( "ui-progressbar-indeterminate", this.indeterminate );
+
+		if ( this.indeterminate ) {
+			this.element.removeAttr( "aria-valuenow" );
+			if ( !this.overlayDiv ) {
+				this.overlayDiv = $( "<div class='ui-progressbar-overlay'></div>" ).appendTo( this.valueDiv );
+			}
+		} else {
+			this.element.attr({
+				"aria-valuemax": this.options.max,
+				"aria-valuenow": value
+			});
+			if ( this.overlayDiv ) {
+				this.overlayDiv.remove();
+				this.overlayDiv = null;
+			}
+		}
+
+		if ( this.oldValue !== value ) {
+			this.oldValue = value;
+			this._trigger( "change" );
+		}
+		if ( value === this.options.max ) {
+			this._trigger( "complete" );
+		}
+	}
+});
+
+
+/*!
+ * jQuery UI Selectmenu 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/selectmenu
+ */
+
+
+var selectmenu = $.widget( "ui.selectmenu", {
+	version: "1.11.2",
+	defaultElement: "<select>",
+	options: {
+		appendTo: null,
+		disabled: null,
+		icons: {
+			button: "ui-icon-triangle-1-s"
+		},
+		position: {
+			my: "left top",
+			at: "left bottom",
+			collision: "none"
+		},
+		width: null,
+
+		// callbacks
+		change: null,
+		close: null,
+		focus: null,
+		open: null,
+		select: null
+	},
+
+	_create: function() {
+		var selectmenuId = this.element.uniqueId().attr( "id" );
+		this.ids = {
+			element: selectmenuId,
+			button: selectmenuId + "-button",
+			menu: selectmenuId + "-menu"
+		};
+
+		this._drawButton();
+		this._drawMenu();
+
+		if ( this.options.disabled ) {
+			this.disable();
+		}
+	},
+
+	_drawButton: function() {
+		var that = this,
+			tabindex = this.element.attr( "tabindex" );
+
+		// Associate existing label with the new button
+		this.label = $( "label[for='" + this.ids.element + "']" ).attr( "for", this.ids.button );
+		this._on( this.label, {
+			click: function( event ) {
+				this.button.focus();
+				event.preventDefault();
+			}
+		});
+
+		// Hide original select element
+		this.element.hide();
+
+		// Create button
+		this.button = $( "<span>", {
+			"class": "ui-selectmenu-button ui-widget ui-state-default ui-corner-all",
+			tabindex: tabindex || this.options.disabled ? -1 : 0,
+			id: this.ids.button,
+			role: "combobox",
+			"aria-expanded": "false",
+			"aria-autocomplete": "list",
+			"aria-owns": this.ids.menu,
+			"aria-haspopup": "true"
+		})
+			.insertAfter( this.element );
+
+		$( "<span>", {
+			"class": "ui-icon " + this.options.icons.button
+		})
+			.prependTo( this.button );
+
+		this.buttonText = $( "<span>", {
+			"class": "ui-selectmenu-text"
+		})
+			.appendTo( this.button );
+
+		this._setText( this.buttonText, this.element.find( "option:selected" ).text() );
+		this._resizeButton();
+
+		this._on( this.button, this._buttonEvents );
+		this.button.one( "focusin", function() {
+
+			// Delay rendering the menu items until the button receives focus.
+			// The menu may have already been rendered via a programmatic open.
+			if ( !that.menuItems ) {
+				that._refreshMenu();
+			}
+		});
+		this._hoverable( this.button );
+		this._focusable( this.button );
+	},
+
+	_drawMenu: function() {
+		var that = this;
+
+		// Create menu
+		this.menu = $( "<ul>", {
+			"aria-hidden": "true",
+			"aria-labelledby": this.ids.button,
+			id: this.ids.menu
+		});
+
+		// Wrap menu
+		this.menuWrap = $( "<div>", {
+			"class": "ui-selectmenu-menu ui-front"
+		})
+			.append( this.menu )
+			.appendTo( this._appendTo() );
+
+		// Initialize menu widget
+		this.menuInstance = this.menu
+			.menu({
+				role: "listbox",
+				select: function( event, ui ) {
+					event.preventDefault();
+
+					// support: IE8
+					// If the item was selected via a click, the text selection
+					// will be destroyed in IE
+					that._setSelection();
+
+					that._select( ui.item.data( "ui-selectmenu-item" ), event );
+				},
+				focus: function( event, ui ) {
+					var item = ui.item.data( "ui-selectmenu-item" );
+
+					// Prevent inital focus from firing and check if its a newly focused item
+					if ( that.focusIndex != null && item.index !== that.focusIndex ) {
+						that._trigger( "focus", event, { item: item } );
+						if ( !that.isOpen ) {
+							that._select( item, event );
+						}
+					}
+					that.focusIndex = item.index;
+
+					that.button.attr( "aria-activedescendant",
+						that.menuItems.eq( item.index ).attr( "id" ) );
+				}
+			})
+			.menu( "instance" );
+
+		// Adjust menu styles to dropdown
+		this.menu
+			.addClass( "ui-corner-bottom" )
+			.removeClass( "ui-corner-all" );
+
+		// Don't close the menu on mouseleave
+		this.menuInstance._off( this.menu, "mouseleave" );
+
+		// Cancel the menu's collapseAll on document click
+		this.menuInstance._closeOnDocumentClick = function() {
+			return false;
+		};
+
+		// Selects often contain empty items, but never contain dividers
+		this.menuInstance._isDivider = function() {
+			return false;
+		};
+	},
+
+	refresh: function() {
+		this._refreshMenu();
+		this._setText( this.buttonText, this._getSelectedItem().text() );
+		if ( !this.options.width ) {
+			this._resizeButton();
+		}
+	},
+
+	_refreshMenu: function() {
+		this.menu.empty();
+
+		var item,
+			options = this.element.find( "option" );
+
+		if ( !options.length ) {
+			return;
+		}
+
+		this._parseOptions( options );
+		this._renderMenu( this.menu, this.items );
+
+		this.menuInstance.refresh();
+		this.menuItems = this.menu.find( "li" ).not( ".ui-selectmenu-optgroup" );
+
+		item = this._getSelectedItem();
+
+		// Update the menu to have the correct item focused
+		this.menuInstance.focus( null, item );
+		this._setAria( item.data( "ui-selectmenu-item" ) );
+
+		// Set disabled state
+		this._setOption( "disabled", this.element.prop( "disabled" ) );
+	},
+
+	open: function( event ) {
+		if ( this.options.disabled ) {
+			return;
+		}
+
+		// If this is the first time the menu is being opened, render the items
+		if ( !this.menuItems ) {
+			this._refreshMenu();
+		} else {
+
+			// Menu clears focus on close, reset focus to selected item
+			this.menu.find( ".ui-state-focus" ).removeClass( "ui-state-focus" );
+			this.menuInstance.focus( null, this._getSelectedItem() );
+		}
+
+		this.isOpen = true;
+		this._toggleAttr();
+		this._resizeMenu();
+		this._position();
+
+		this._on( this.document, this._documentClick );
+
+		this._trigger( "open", event );
+	},
+
+	_position: function() {
+		this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) );
+	},
+
+	close: function( event ) {
+		if ( !this.isOpen ) {
+			return;
+		}
+
+		this.isOpen = false;
+		this._toggleAttr();
+
+		this.range = null;
+		this._off( this.document );
+
+		this._trigger( "close", event );
+	},
+
+	widget: function() {
+		return this.button;
+	},
+
+	menuWidget: function() {
+		return this.menu;
+	},
+
+	_renderMenu: function( ul, items ) {
+		var that = this,
+			currentOptgroup = "";
+
+		$.each( items, function( index, item ) {
+			if ( item.optgroup !== currentOptgroup ) {
+				$( "<li>", {
+					"class": "ui-selectmenu-optgroup ui-menu-divider" +
+						( item.element.parent( "optgroup" ).prop( "disabled" ) ?
+							" ui-state-disabled" :
+							"" ),
+					text: item.optgroup
+				})
+					.appendTo( ul );
+
+				currentOptgroup = item.optgroup;
+			}
+
+			that._renderItemData( ul, item );
+		});
+	},
+
+	_renderItemData: function( ul, item ) {
+		return this._renderItem( ul, item ).data( "ui-selectmenu-item", item );
+	},
+
+	_renderItem: function( ul, item ) {
+		var li = $( "<li>" );
+
+		if ( item.disabled ) {
+			li.addClass( "ui-state-disabled" );
+		}
+		this._setText( li, item.label );
+
+		return li.appendTo( ul );
+	},
+
+	_setText: function( element, value ) {
+		if ( value ) {
+			element.text( value );
+		} else {
+			element.html( " " );
+		}
+	},
+
+	_move: function( direction, event ) {
+		var item, next,
+			filter = ".ui-menu-item";
+
+		if ( this.isOpen ) {
+			item = this.menuItems.eq( this.focusIndex );
+		} else {
+			item = this.menuItems.eq( this.element[ 0 ].selectedIndex );
+			filter += ":not(.ui-state-disabled)";
+		}
+
+		if ( direction === "first" || direction === "last" ) {
+			next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 );
+		} else {
+			next = item[ direction + "All" ]( filter ).eq( 0 );
+		}
+
+		if ( next.length ) {
+			this.menuInstance.focus( event, next );
+		}
+	},
+
+	_getSelectedItem: function() {
+		return this.menuItems.eq( this.element[ 0 ].selectedIndex );
+	},
+
+	_toggle: function( event ) {
+		this[ this.isOpen ? "close" : "open" ]( event );
+	},
+
+	_setSelection: function() {
+		var selection;
+
+		if ( !this.range ) {
+			return;
+		}
+
+		if ( window.getSelection ) {
+			selection = window.getSelection();
+			selection.removeAllRanges();
+			selection.addRange( this.range );
+
+		// support: IE8
+		} else {
+			this.range.select();
+		}
+
+		// support: IE
+		// Setting the text selection kills the button focus in IE, but
+		// restoring the focus doesn't kill the selection.
+		this.button.focus();
+	},
+
+	_documentClick: {
+		mousedown: function( event ) {
+			if ( !this.isOpen ) {
+				return;
+			}
+
+			if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" + this.ids.button ).length ) {
+				this.close( event );
+			}
+		}
+	},
+
+	_buttonEvents: {
+
+		// Prevent text selection from being reset when interacting with the selectmenu (#10144)
+		mousedown: function() {
+			var selection;
+
+			if ( window.getSelection ) {
+				selection = window.getSelection();
+				if ( selection.rangeCount ) {
+					this.range = selection.getRangeAt( 0 );
+				}
+
+			// support: IE8
+			} else {
+				this.range = document.selection.createRange();
+			}
+		},
+
+		click: function( event ) {
+			this._setSelection();
+			this._toggle( event );
+		},
+
+		keydown: function( event ) {
+			var preventDefault = true;
+			switch ( event.keyCode ) {
+				case $.ui.keyCode.TAB:
+				case $.ui.keyCode.ESCAPE:
+					this.close( event );
+					preventDefault = false;
+					break;
+				case $.ui.keyCode.ENTER:
+					if ( this.isOpen ) {
+						this._selectFocusedItem( event );
+					}
+					break;
+				case $.ui.keyCode.UP:
+					if ( event.altKey ) {
+						this._toggle( event );
+					} else {
+						this._move( "prev", event );
+					}
+					break;
+				case $.ui.keyCode.DOWN:
+					if ( event.altKey ) {
+						this._toggle( event );
+					} else {
+						this._move( "next", event );
+					}
+					break;
+				case $.ui.keyCode.SPACE:
+					if ( this.isOpen ) {
+						this._selectFocusedItem( event );
+					} else {
+						this._toggle( event );
+					}
+					break;
+				case $.ui.keyCode.LEFT:
+					this._move( "prev", event );
+					break;
+				case $.ui.keyCode.RIGHT:
+					this._move( "next", event );
+					break;
+				case $.ui.keyCode.HOME:
+				case $.ui.keyCode.PAGE_UP:
+					this._move( "first", event );
+					break;
+				case $.ui.keyCode.END:
+				case $.ui.keyCode.PAGE_DOWN:
+					this._move( "last", event );
+					break;
+				default:
+					this.menu.trigger( event );
+					preventDefault = false;
+			}
+
+			if ( preventDefault ) {
+				event.preventDefault();
+			}
+		}
+	},
+
+	_selectFocusedItem: function( event ) {
+		var item = this.menuItems.eq( this.focusIndex );
+		if ( !item.hasClass( "ui-state-disabled" ) ) {
+			this._select( item.data( "ui-selectmenu-item" ), event );
+		}
+	},
+
+	_select: function( item, event ) {
+		var oldIndex = this.element[ 0 ].selectedIndex;
+
+		// Change native select element
+		this.element[ 0 ].selectedIndex = item.index;
+		this._setText( this.buttonText, item.label );
+		this._setAria( item );
+		this._trigger( "select", event, { item: item } );
+
+		if ( item.index !== oldIndex ) {
+			this._trigger( "change", event, { item: item } );
+		}
+
+		this.close( event );
+	},
+
+	_setAria: function( item ) {
+		var id = this.menuItems.eq( item.index ).attr( "id" );
+
+		this.button.attr({
+			"aria-labelledby": id,
+			"aria-activedescendant": id
+		});
+		this.menu.attr( "aria-activedescendant", id );
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "icons" ) {
+			this.button.find( "span.ui-icon" )
+				.removeClass( this.options.icons.button )
+				.addClass( value.button );
+		}
+
+		this._super( key, value );
+
+		if ( key === "appendTo" ) {
+			this.menuWrap.appendTo( this._appendTo() );
+		}
+
+		if ( key === "disabled" ) {
+			this.menuInstance.option( "disabled", value );
+			this.button
+				.toggleClass( "ui-state-disabled", value )
+				.attr( "aria-disabled", value );
+
+			this.element.prop( "disabled", value );
+			if ( value ) {
+				this.button.attr( "tabindex", -1 );
+				this.close();
+			} else {
+				this.button.attr( "tabindex", 0 );
+			}
+		}
+
+		if ( key === "width" ) {
+			this._resizeButton();
+		}
+	},
+
+	_appendTo: function() {
+		var element = this.options.appendTo;
+
+		if ( element ) {
+			element = element.jquery || element.nodeType ?
+				$( element ) :
+				this.document.find( element ).eq( 0 );
+		}
+
+		if ( !element || !element[ 0 ] ) {
+			element = this.element.closest( ".ui-front" );
+		}
+
+		if ( !element.length ) {
+			element = this.document[ 0 ].body;
+		}
+
+		return element;
+	},
+
+	_toggleAttr: function() {
+		this.button
+			.toggleClass( "ui-corner-top", this.isOpen )
+			.toggleClass( "ui-corner-all", !this.isOpen )
+			.attr( "aria-expanded", this.isOpen );
+		this.menuWrap.toggleClass( "ui-selectmenu-open", this.isOpen );
+		this.menu.attr( "aria-hidden", !this.isOpen );
+	},
+
+	_resizeButton: function() {
+		var width = this.options.width;
+
+		if ( !width ) {
+			width = this.element.show().outerWidth();
+			this.element.hide();
+		}
+
+		this.button.outerWidth( width );
+	},
+
+	_resizeMenu: function() {
+		this.menu.outerWidth( Math.max(
+			this.button.outerWidth(),
+
+			// support: IE10
+			// IE10 wraps long text (possibly a rounding bug)
+			// so we add 1px to avoid the wrapping
+			this.menu.width( "" ).outerWidth() + 1
+		) );
+	},
+
+	_getCreateOptions: function() {
+		return { disabled: this.element.prop( "disabled" ) };
+	},
+
+	_parseOptions: function( options ) {
+		var data = [];
+		options.each(function( index, item ) {
+			var option = $( item ),
+				optgroup = option.parent( "optgroup" );
+			data.push({
+				element: option,
+				index: index,
+				value: option.attr( "value" ),
+				label: option.text(),
+				optgroup: optgroup.attr( "label" ) || "",
+				disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" )
+			});
+		});
+		this.items = data;
+	},
+
+	_destroy: function() {
+		this.menuWrap.remove();
+		this.button.remove();
+		this.element.show();
+		this.element.removeUniqueId();
+		this.label.attr( "for", this.ids.element );
+	}
+});
+
+
+/*!
+ * jQuery UI Slider 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/slider/
+ */
+
+
+var slider = $.widget( "ui.slider", $.ui.mouse, {
+	version: "1.11.2",
+	widgetEventPrefix: "slide",
+
+	options: {
+		animate: false,
+		distance: 0,
+		max: 100,
+		min: 0,
+		orientation: "horizontal",
+		range: false,
+		step: 1,
+		value: 0,
+		values: null,
+
+		// callbacks
+		change: null,
+		slide: null,
+		start: null,
+		stop: null
+	},
+
+	// number of pages in a slider
+	// (how many times can you page up/down to go through the whole range)
+	numPages: 5,
+
+	_create: function() {
+		this._keySliding = false;
+		this._mouseSliding = false;
+		this._animateOff = true;
+		this._handleIndex = null;
+		this._detectOrientation();
+		this._mouseInit();
+		this._calculateNewMax();
+
+		this.element
+			.addClass( "ui-slider" +
+				" ui-slider-" + this.orientation +
+				" ui-widget" +
+				" ui-widget-content" +
+				" ui-corner-all");
+
+		this._refresh();
+		this._setOption( "disabled", this.options.disabled );
+
+		this._animateOff = false;
+	},
+
+	_refresh: function() {
+		this._createRange();
+		this._createHandles();
+		this._setupEvents();
+		this._refreshValue();
+	},
+
+	_createHandles: function() {
+		var i, handleCount,
+			options = this.options,
+			existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ),
+			handle = "<span class='ui-slider-handle ui-state-default ui-corner-all' tabindex='0'></span>",
+			handles = [];
+
+		handleCount = ( options.values && options.values.length ) || 1;
+
+		if ( existingHandles.length > handleCount ) {
+			existingHandles.slice( handleCount ).remove();
+			existingHandles = existingHandles.slice( 0, handleCount );
+		}
+
+		for ( i = existingHandles.length; i < handleCount; i++ ) {
+			handles.push( handle );
+		}
+
+		this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) );
+
+		this.handle = this.handles.eq( 0 );
+
+		this.handles.each(function( i ) {
+			$( this ).data( "ui-slider-handle-index", i );
+		});
+	},
+
+	_createRange: function() {
+		var options = this.options,
+			classes = "";
+
+		if ( options.range ) {
+			if ( options.range === true ) {
+				if ( !options.values ) {
+					options.values = [ this._valueMin(), this._valueMin() ];
+				} else if ( options.values.length && options.values.length !== 2 ) {
+					options.values = [ options.values[0], options.values[0] ];
+				} else if ( $.isArray( options.values ) ) {
+					options.values = options.values.slice(0);
+				}
+			}
+
+			if ( !this.range || !this.range.length ) {
+				this.range = $( "<div></div>" )
+					.appendTo( this.element );
+
+				classes = "ui-slider-range" +
+				// note: this isn't the most fittingly semantic framework class for this element,
+				// but worked best visually with a variety of themes
+				" ui-widget-header ui-corner-all";
+			} else {
+				this.range.removeClass( "ui-slider-range-min ui-slider-range-max" )
+					// Handle range switching from true to min/max
+					.css({
+						"left": "",
+						"bottom": ""
+					});
+			}
+
+			this.range.addClass( classes +
+				( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) );
+		} else {
+			if ( this.range ) {
+				this.range.remove();
+			}
+			this.range = null;
+		}
+	},
+
+	_setupEvents: function() {
+		this._off( this.handles );
+		this._on( this.handles, this._handleEvents );
+		this._hoverable( this.handles );
+		this._focusable( this.handles );
+	},
+
+	_destroy: function() {
+		this.handles.remove();
+		if ( this.range ) {
+			this.range.remove();
+		}
+
+		this.element
+			.removeClass( "ui-slider" +
+				" ui-slider-horizontal" +
+				" ui-slider-vertical" +
+				" ui-widget" +
+				" ui-widget-content" +
+				" ui-corner-all" );
+
+		this._mouseDestroy();
+	},
+
+	_mouseCapture: function( event ) {
+		var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle,
+			that = this,
+			o = this.options;
+
+		if ( o.disabled ) {
+			return false;
+		}
+
+		this.elementSize = {
+			width: this.element.outerWidth(),
+			height: this.element.outerHeight()
+		};
+		this.elementOffset = this.element.offset();
+
+		position = { x: event.pageX, y: event.pageY };
+		normValue = this._normValueFromMouse( position );
+		distance = this._valueMax() - this._valueMin() + 1;
+		this.handles.each(function( i ) {
+			var thisDistance = Math.abs( normValue - that.values(i) );
+			if (( distance > thisDistance ) ||
+				( distance === thisDistance &&
+					(i === that._lastChangedValue || that.values(i) === o.min ))) {
+				distance = thisDistance;
+				closestHandle = $( this );
+				index = i;
+			}
+		});
+
+		allowed = this._start( event, index );
+		if ( allowed === false ) {
+			return false;
+		}
+		this._mouseSliding = true;
+
+		this._handleIndex = index;
+
+		closestHandle
+			.addClass( "ui-state-active" )
+			.focus();
+
+		offset = closestHandle.offset();
+		mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" );
+		this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : {
+			left: event.pageX - offset.left - ( closestHandle.width() / 2 ),
+			top: event.pageY - offset.top -
+				( closestHandle.height() / 2 ) -
+				( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) -
+				( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) +
+				( parseInt( closestHandle.css("marginTop"), 10 ) || 0)
+		};
+
+		if ( !this.handles.hasClass( "ui-state-hover" ) ) {
+			this._slide( event, index, normValue );
+		}
+		this._animateOff = true;
+		return true;
+	},
+
+	_mouseStart: function() {
+		return true;
+	},
+
+	_mouseDrag: function( event ) {
+		var position = { x: event.pageX, y: event.pageY },
+			normValue = this._normValueFromMouse( position );
+
+		this._slide( event, this._handleIndex, normValue );
+
+		return false;
+	},
+
+	_mouseStop: function( event ) {
+		this.handles.removeClass( "ui-state-active" );
+		this._mouseSliding = false;
+
+		this._stop( event, this._handleIndex );
+		this._change( event, this._handleIndex );
+
+		this._handleIndex = null;
+		this._clickOffset = null;
+		this._animateOff = false;
+
+		return false;
+	},
+
+	_detectOrientation: function() {
+		this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal";
+	},
+
+	_normValueFromMouse: function( position ) {
+		var pixelTotal,
+			pixelMouse,
+			percentMouse,
+			valueTotal,
+			valueMouse;
+
+		if ( this.orientation === "horizontal" ) {
+			pixelTotal = this.elementSize.width;
+			pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 );
+		} else {
+			pixelTotal = this.elementSize.height;
+			pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 );
+		}
+
+		percentMouse = ( pixelMouse / pixelTotal );
+		if ( percentMouse > 1 ) {
+			percentMouse = 1;
+		}
+		if ( percentMouse < 0 ) {
+			percentMouse = 0;
+		}
+		if ( this.orientation === "vertical" ) {
+			percentMouse = 1 - percentMouse;
+		}
+
+		valueTotal = this._valueMax() - this._valueMin();
+		valueMouse = this._valueMin() + percentMouse * valueTotal;
+
+		return this._trimAlignValue( valueMouse );
+	},
+
+	_start: function( event, index ) {
+		var uiHash = {
+			handle: this.handles[ index ],
+			value: this.value()
+		};
+		if ( this.options.values && this.options.values.length ) {
+			uiHash.value = this.values( index );
+			uiHash.values = this.values();
+		}
+		return this._trigger( "start", event, uiHash );
+	},
+
+	_slide: function( event, index, newVal ) {
+		var otherVal,
+			newValues,
+			allowed;
+
+		if ( this.options.values && this.options.values.length ) {
+			otherVal = this.values( index ? 0 : 1 );
+
+			if ( ( this.options.values.length === 2 && this.options.range === true ) &&
+					( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) )
+				) {
+				newVal = otherVal;
+			}
+
+			if ( newVal !== this.values( index ) ) {
+				newValues = this.values();
+				newValues[ index ] = newVal;
+				// A slide can be canceled by returning false from the slide callback
+				allowed = this._trigger( "slide", event, {
+					handle: this.handles[ index ],
+					value: newVal,
+					values: newValues
+				} );
+				otherVal = this.values( index ? 0 : 1 );
+				if ( allowed !== false ) {
+					this.values( index, newVal );
+				}
+			}
+		} else {
+			if ( newVal !== this.value() ) {
+				// A slide can be canceled by returning false from the slide callback
+				allowed = this._trigger( "slide", event, {
+					handle: this.handles[ index ],
+					value: newVal
+				} );
+				if ( allowed !== false ) {
+					this.value( newVal );
+				}
+			}
+		}
+	},
+
+	_stop: function( event, index ) {
+		var uiHash = {
+			handle: this.handles[ index ],
+			value: this.value()
+		};
+		if ( this.options.values && this.options.values.length ) {
+			uiHash.value = this.values( index );
+			uiHash.values = this.values();
+		}
+
+		this._trigger( "stop", event, uiHash );
+	},
+
+	_change: function( event, index ) {
+		if ( !this._keySliding && !this._mouseSliding ) {
+			var uiHash = {
+				handle: this.handles[ index ],
+				value: this.value()
+			};
+			if ( this.options.values && this.options.values.length ) {
+				uiHash.value = this.values( index );
+				uiHash.values = this.values();
+			}
+
+			//store the last changed value index for reference when handles overlap
+			this._lastChangedValue = index;
+
+			this._trigger( "change", event, uiHash );
+		}
+	},
+
+	value: function( newValue ) {
+		if ( arguments.length ) {
+			this.options.value = this._trimAlignValue( newValue );
+			this._refreshValue();
+			this._change( null, 0 );
+			return;
+		}
+
+		return this._value();
+	},
+
+	values: function( index, newValue ) {
+		var vals,
+			newValues,
+			i;
+
+		if ( arguments.length > 1 ) {
+			this.options.values[ index ] = this._trimAlignValue( newValue );
+			this._refreshValue();
+			this._change( null, index );
+			return;
+		}
+
+		if ( arguments.length ) {
+			if ( $.isArray( arguments[ 0 ] ) ) {
+				vals = this.options.values;
+				newValues = arguments[ 0 ];
+				for ( i = 0; i < vals.length; i += 1 ) {
+					vals[ i ] = this._trimAlignValue( newValues[ i ] );
+					this._change( null, i );
+				}
+				this._refreshValue();
+			} else {
+				if ( this.options.values && this.options.values.length ) {
+					return this._values( index );
+				} else {
+					return this.value();
+				}
+			}
+		} else {
+			return this._values();
+		}
+	},
+
+	_setOption: function( key, value ) {
+		var i,
+			valsLength = 0;
+
+		if ( key === "range" && this.options.range === true ) {
+			if ( value === "min" ) {
+				this.options.value = this._values( 0 );
+				this.options.values = null;
+			} else if ( value === "max" ) {
+				this.options.value = this._values( this.options.values.length - 1 );
+				this.options.values = null;
+			}
+		}
+
+		if ( $.isArray( this.options.values ) ) {
+			valsLength = this.options.values.length;
+		}
+
+		if ( key === "disabled" ) {
+			this.element.toggleClass( "ui-state-disabled", !!value );
+		}
+
+		this._super( key, value );
+
+		switch ( key ) {
+			case "orientation":
+				this._detectOrientation();
+				this.element
+					.removeClass( "ui-slider-horizontal ui-slider-vertical" )
+					.addClass( "ui-slider-" + this.orientation );
+				this._refreshValue();
+
+				// Reset positioning from previous orientation
+				this.handles.css( value === "horizontal" ? "bottom" : "left", "" );
+				break;
+			case "value":
+				this._animateOff = true;
+				this._refreshValue();
+				this._change( null, 0 );
+				this._animateOff = false;
+				break;
+			case "values":
+				this._animateOff = true;
+				this._refreshValue();
+				for ( i = 0; i < valsLength; i += 1 ) {
+					this._change( null, i );
+				}
+				this._animateOff = false;
+				break;
+			case "step":
+			case "min":
+			case "max":
+				this._animateOff = true;
+				this._calculateNewMax();
+				this._refreshValue();
+				this._animateOff = false;
+				break;
+			case "range":
+				this._animateOff = true;
+				this._refresh();
+				this._animateOff = false;
+				break;
+		}
+	},
+
+	//internal value getter
+	// _value() returns value trimmed by min and max, aligned by step
+	_value: function() {
+		var val = this.options.value;
+		val = this._trimAlignValue( val );
+
+		return val;
+	},
+
+	//internal values getter
+	// _values() returns array of values trimmed by min and max, aligned by step
+	// _values( index ) returns single value trimmed by min and max, aligned by step
+	_values: function( index ) {
+		var val,
+			vals,
+			i;
+
+		if ( arguments.length ) {
+			val = this.options.values[ index ];
+			val = this._trimAlignValue( val );
+
+			return val;
+		} else if ( this.options.values && this.options.values.length ) {
+			// .slice() creates a copy of the array
+			// this copy gets trimmed by min and max and then returned
+			vals = this.options.values.slice();
+			for ( i = 0; i < vals.length; i += 1) {
+				vals[ i ] = this._trimAlignValue( vals[ i ] );
+			}
+
+			return vals;
+		} else {
+			return [];
+		}
+	},
+
+	// returns the step-aligned value that val is closest to, between (inclusive) min and max
+	_trimAlignValue: function( val ) {
+		if ( val <= this._valueMin() ) {
+			return this._valueMin();
+		}
+		if ( val >= this._valueMax() ) {
+			return this._valueMax();
+		}
+		var step = ( this.options.step > 0 ) ? this.options.step : 1,
+			valModStep = (val - this._valueMin()) % step,
+			alignValue = val - valModStep;
+
+		if ( Math.abs(valModStep) * 2 >= step ) {
+			alignValue += ( valModStep > 0 ) ? step : ( -step );
+		}
+
+		// Since JavaScript has problems with large floats, round
+		// the final value to 5 digits after the decimal point (see #4124)
+		return parseFloat( alignValue.toFixed(5) );
+	},
+
+	_calculateNewMax: function() {
+		var remainder = ( this.options.max - this._valueMin() ) % this.options.step;
+		this.max = this.options.max - remainder;
+	},
+
+	_valueMin: function() {
+		return this.options.min;
+	},
+
+	_valueMax: function() {
+		return this.max;
+	},
+
+	_refreshValue: function() {
+		var lastValPercent, valPercent, value, valueMin, valueMax,
+			oRange = this.options.range,
+			o = this.options,
+			that = this,
+			animate = ( !this._animateOff ) ? o.animate : false,
+			_set = {};
+
+		if ( this.options.values && this.options.values.length ) {
+			this.handles.each(function( i ) {
+				valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100;
+				_set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
+				$( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
+				if ( that.options.range === true ) {
+					if ( that.orientation === "horizontal" ) {
+						if ( i === 0 ) {
+							that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate );
+						}
+						if ( i === 1 ) {
+							that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } );
+						}
+					} else {
+						if ( i === 0 ) {
+							that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate );
+						}
+						if ( i === 1 ) {
+							that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } );
+						}
+					}
+				}
+				lastValPercent = valPercent;
+			});
+		} else {
+			value = this.value();
+			valueMin = this._valueMin();
+			valueMax = this._valueMax();
+			valPercent = ( valueMax !== valueMin ) ?
+					( value - valueMin ) / ( valueMax - valueMin ) * 100 :
+					0;
+			_set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
+			this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
+
+			if ( oRange === "min" && this.orientation === "horizontal" ) {
+				this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate );
+			}
+			if ( oRange === "max" && this.orientation === "horizontal" ) {
+				this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } );
+			}
+			if ( oRange === "min" && this.orientation === "vertical" ) {
+				this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate );
+			}
+			if ( oRange === "max" && this.orientation === "vertical" ) {
+				this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } );
+			}
+		}
+	},
+
+	_handleEvents: {
+		keydown: function( event ) {
+			var allowed, curVal, newVal, step,
+				index = $( event.target ).data( "ui-slider-handle-index" );
+
+			switch ( event.keyCode ) {
+				case $.ui.keyCode.HOME:
+				case $.ui.keyCode.END:
+				case $.ui.keyCode.PAGE_UP:
+				case $.ui.keyCode.PAGE_DOWN:
+				case $.ui.keyCode.UP:
+				case $.ui.keyCode.RIGHT:
+				case $.ui.keyCode.DOWN:
+				case $.ui.keyCode.LEFT:
+					event.preventDefault();
+					if ( !this._keySliding ) {
+						this._keySliding = true;
+						$( event.target ).addClass( "ui-state-active" );
+						allowed = this._start( event, index );
+						if ( allowed === false ) {
+							return;
+						}
+					}
+					break;
+			}
+
+			step = this.options.step;
+			if ( this.options.values && this.options.values.length ) {
+				curVal = newVal = this.values( index );
+			} else {
+				curVal = newVal = this.value();
+			}
+
+			switch ( event.keyCode ) {
+				case $.ui.keyCode.HOME:
+					newVal = this._valueMin();
+					break;
+				case $.ui.keyCode.END:
+					newVal = this._valueMax();
+					break;
+				case $.ui.keyCode.PAGE_UP:
+					newVal = this._trimAlignValue(
+						curVal + ( ( this._valueMax() - this._valueMin() ) / this.numPages )
+					);
+					break;
+				case $.ui.keyCode.PAGE_DOWN:
+					newVal = this._trimAlignValue(
+						curVal - ( (this._valueMax() - this._valueMin()) / this.numPages ) );
+					break;
+				case $.ui.keyCode.UP:
+				case $.ui.keyCode.RIGHT:
+					if ( curVal === this._valueMax() ) {
+						return;
+					}
+					newVal = this._trimAlignValue( curVal + step );
+					break;
+				case $.ui.keyCode.DOWN:
+				case $.ui.keyCode.LEFT:
+					if ( curVal === this._valueMin() ) {
+						return;
+					}
+					newVal = this._trimAlignValue( curVal - step );
+					break;
+			}
+
+			this._slide( event, index, newVal );
+		},
+		keyup: function( event ) {
+			var index = $( event.target ).data( "ui-slider-handle-index" );
+
+			if ( this._keySliding ) {
+				this._keySliding = false;
+				this._stop( event, index );
+				this._change( event, index );
+				$( event.target ).removeClass( "ui-state-active" );
+			}
+		}
+	}
+});
+
+
+/*!
+ * jQuery UI Spinner 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/spinner/
+ */
+
+
+function spinner_modifier( fn ) {
+	return function() {
+		var previous = this.element.val();
+		fn.apply( this, arguments );
+		this._refresh();
+		if ( previous !== this.element.val() ) {
+			this._trigger( "change" );
+		}
+	};
+}
+
+var spinner = $.widget( "ui.spinner", {
+	version: "1.11.2",
+	defaultElement: "<input>",
+	widgetEventPrefix: "spin",
+	options: {
+		culture: null,
+		icons: {
+			down: "ui-icon-triangle-1-s",
+			up: "ui-icon-triangle-1-n"
+		},
+		incremental: true,
+		max: null,
+		min: null,
+		numberFormat: null,
+		page: 10,
+		step: 1,
+
+		change: null,
+		spin: null,
+		start: null,
+		stop: null
+	},
+
+	_create: function() {
+		// handle string values that need to be parsed
+		this._setOption( "max", this.options.max );
+		this._setOption( "min", this.options.min );
+		this._setOption( "step", this.options.step );
+
+		// Only format if there is a value, prevents the field from being marked
+		// as invalid in Firefox, see #9573.
+		if ( this.value() !== "" ) {
+			// Format the value, but don't constrain.
+			this._value( this.element.val(), true );
+		}
+
+		this._draw();
+		this._on( this._events );
+		this._refresh();
+
+		// turning off autocomplete prevents the browser from remembering the
+		// value when navigating through history, so we re-enable autocomplete
+		// if the page is unloaded before the widget is destroyed. #7790
+		this._on( this.window, {
+			beforeunload: function() {
+				this.element.removeAttr( "autocomplete" );
+			}
+		});
+	},
+
+	_getCreateOptions: function() {
+		var options = {},
+			element = this.element;
+
+		$.each( [ "min", "max", "step" ], function( i, option ) {
+			var value = element.attr( option );
+			if ( value !== undefined && value.length ) {
+				options[ option ] = value;
+			}
+		});
+
+		return options;
+	},
+
+	_events: {
+		keydown: function( event ) {
+			if ( this._start( event ) && this._keydown( event ) ) {
+				event.preventDefault();
+			}
+		},
+		keyup: "_stop",
+		focus: function() {
+			this.previous = this.element.val();
+		},
+		blur: function( event ) {
+			if ( this.cancelBlur ) {
+				delete this.cancelBlur;
+				return;
+			}
+
+			this._stop();
+			this._refresh();
+			if ( this.previous !== this.element.val() ) {
+				this._trigger( "change", event );
+			}
+		},
+		mousewheel: function( event, delta ) {
+			if ( !delta ) {
+				return;
+			}
+			if ( !this.spinning && !this._start( event ) ) {
+				return false;
+			}
+
+			this._spin( (delta > 0 ? 1 : -1) * this.options.step, event );
+			clearTimeout( this.mousewheelTimer );
+			this.mousewheelTimer = this._delay(function() {
+				if ( this.spinning ) {
+					this._stop( event );
+				}
+			}, 100 );
+			event.preventDefault();
+		},
+		"mousedown .ui-spinner-button": function( event ) {
+			var previous;
+
+			// We never want the buttons to have focus; whenever the user is
+			// interacting with the spinner, the focus should be on the input.
+			// If the input is focused then this.previous is properly set from
+			// when the input first received focus. If the input is not focused
+			// then we need to set this.previous based on the value before spinning.
+			previous = this.element[0] === this.document[0].activeElement ?
+				this.previous : this.element.val();
+			function checkFocus() {
+				var isActive = this.element[0] === this.document[0].activeElement;
+				if ( !isActive ) {
+					this.element.focus();
+					this.previous = previous;
+					// support: IE
+					// IE sets focus asynchronously, so we need to check if focus
+					// moved off of the input because the user clicked on the button.
+					this._delay(function() {
+						this.previous = previous;
+					});
+				}
+			}
+
+			// ensure focus is on (or stays on) the text field
+			event.preventDefault();
+			checkFocus.call( this );
+
+			// support: IE
+			// IE doesn't prevent moving focus even with event.preventDefault()
+			// so we set a flag to know when we should ignore the blur event
+			// and check (again) if focus moved off of the input.
+			this.cancelBlur = true;
+			this._delay(function() {
+				delete this.cancelBlur;
+				checkFocus.call( this );
+			});
+
+			if ( this._start( event ) === false ) {
+				return;
+			}
+
+			this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
+		},
+		"mouseup .ui-spinner-button": "_stop",
+		"mouseenter .ui-spinner-button": function( event ) {
+			// button will add ui-state-active if mouse was down while mouseleave and kept down
+			if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) {
+				return;
+			}
+
+			if ( this._start( event ) === false ) {
+				return false;
+			}
+			this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
+		},
+		// TODO: do we really want to consider this a stop?
+		// shouldn't we just stop the repeater and wait until mouseup before
+		// we trigger the stop event?
+		"mouseleave .ui-spinner-button": "_stop"
+	},
+
+	_draw: function() {
+		var uiSpinner = this.uiSpinner = this.element
+			.addClass( "ui-spinner-input" )
+			.attr( "autocomplete", "off" )
+			.wrap( this._uiSpinnerHtml() )
+			.parent()
+				// add buttons
+				.append( this._buttonHtml() );
+
+		this.element.attr( "role", "spinbutton" );
+
+		// button bindings
+		this.buttons = uiSpinner.find( ".ui-spinner-button" )
+			.attr( "tabIndex", -1 )
+			.button()
+			.removeClass( "ui-corner-all" );
+
+		// IE 6 doesn't understand height: 50% for the buttons
+		// unless the wrapper has an explicit height
+		if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) &&
+				uiSpinner.height() > 0 ) {
+			uiSpinner.height( uiSpinner.height() );
+		}
+
+		// disable spinner if element was already disabled
+		if ( this.options.disabled ) {
+			this.disable();
+		}
+	},
+
+	_keydown: function( event ) {
+		var options = this.options,
+			keyCode = $.ui.keyCode;
+
+		switch ( event.keyCode ) {
+		case keyCode.UP:
+			this._repeat( null, 1, event );
+			return true;
+		case keyCode.DOWN:
+			this._repeat( null, -1, event );
+			return true;
+		case keyCode.PAGE_UP:
+			this._repeat( null, options.page, event );
+			return true;
+		case keyCode.PAGE_DOWN:
+			this._repeat( null, -options.page, event );
+			return true;
+		}
+
+		return false;
+	},
+
+	_uiSpinnerHtml: function() {
+		return "<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>";
+	},
+
+	_buttonHtml: function() {
+		return "" +
+			"<a class='ui-spinner-button ui-spinner-up ui-corner-tr'>" +
+				"<span class='ui-icon " + this.options.icons.up + "'>▲</span>" +
+			"</a>" +
+			"<a class='ui-spinner-button ui-spinner-down ui-corner-br'>" +
+				"<span class='ui-icon " + this.options.icons.down + "'>▼</span>" +
+			"</a>";
+	},
+
+	_start: function( event ) {
+		if ( !this.spinning && this._trigger( "start", event ) === false ) {
+			return false;
+		}
+
+		if ( !this.counter ) {
+			this.counter = 1;
+		}
+		this.spinning = true;
+		return true;
+	},
+
+	_repeat: function( i, steps, event ) {
+		i = i || 500;
+
+		clearTimeout( this.timer );
+		this.timer = this._delay(function() {
+			this._repeat( 40, steps, event );
+		}, i );
+
+		this._spin( steps * this.options.step, event );
+	},
+
+	_spin: function( step, event ) {
+		var value = this.value() || 0;
+
+		if ( !this.counter ) {
+			this.counter = 1;
+		}
+
+		value = this._adjustValue( value + step * this._increment( this.counter ) );
+
+		if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) {
+			this._value( value );
+			this.counter++;
+		}
+	},
+
+	_increment: function( i ) {
+		var incremental = this.options.incremental;
+
+		if ( incremental ) {
+			return $.isFunction( incremental ) ?
+				incremental( i ) :
+				Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 );
+		}
+
+		return 1;
+	},
+
+	_precision: function() {
+		var precision = this._precisionOf( this.options.step );
+		if ( this.options.min !== null ) {
+			precision = Math.max( precision, this._precisionOf( this.options.min ) );
+		}
+		return precision;
+	},
+
+	_precisionOf: function( num ) {
+		var str = num.toString(),
+			decimal = str.indexOf( "." );
+		return decimal === -1 ? 0 : str.length - decimal - 1;
+	},
+
+	_adjustValue: function( value ) {
+		var base, aboveMin,
+			options = this.options;
+
+		// make sure we're at a valid step
+		// - find out where we are relative to the base (min or 0)
+		base = options.min !== null ? options.min : 0;
+		aboveMin = value - base;
+		// - round to the nearest step
+		aboveMin = Math.round(aboveMin / options.step) * options.step;
+		// - rounding is based on 0, so adjust back to our base
+		value = base + aboveMin;
+
+		// fix precision from bad JS floating point math
+		value = parseFloat( value.toFixed( this._precision() ) );
+
+		// clamp the value
+		if ( options.max !== null && value > options.max) {
+			return options.max;
+		}
+		if ( options.min !== null && value < options.min ) {
+			return options.min;
+		}
+
+		return value;
+	},
+
+	_stop: function( event ) {
+		if ( !this.spinning ) {
+			return;
+		}
+
+		clearTimeout( this.timer );
+		clearTimeout( this.mousewheelTimer );
+		this.counter = 0;
+		this.spinning = false;
+		this._trigger( "stop", event );
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "culture" || key === "numberFormat" ) {
+			var prevValue = this._parse( this.element.val() );
+			this.options[ key ] = value;
+			this.element.val( this._format( prevValue ) );
+			return;
+		}
+
+		if ( key === "max" || key === "min" || key === "step" ) {
+			if ( typeof value === "string" ) {
+				value = this._parse( value );
+			}
+		}
+		if ( key === "icons" ) {
+			this.buttons.first().find( ".ui-icon" )
+				.removeClass( this.options.icons.up )
+				.addClass( value.up );
+			this.buttons.last().find( ".ui-icon" )
+				.removeClass( this.options.icons.down )
+				.addClass( value.down );
+		}
+
+		this._super( key, value );
+
+		if ( key === "disabled" ) {
+			this.widget().toggleClass( "ui-state-disabled", !!value );
+			this.element.prop( "disabled", !!value );
+			this.buttons.button( value ? "disable" : "enable" );
+		}
+	},
+
+	_setOptions: spinner_modifier(function( options ) {
+		this._super( options );
+	}),
+
+	_parse: function( val ) {
+		if ( typeof val === "string" && val !== "" ) {
+			val = window.Globalize && this.options.numberFormat ?
+				Globalize.parseFloat( val, 10, this.options.culture ) : +val;
+		}
+		return val === "" || isNaN( val ) ? null : val;
+	},
+
+	_format: function( value ) {
+		if ( value === "" ) {
+			return "";
+		}
+		return window.Globalize && this.options.numberFormat ?
+			Globalize.format( value, this.options.numberFormat, this.options.culture ) :
+			value;
+	},
+
+	_refresh: function() {
+		this.element.attr({
+			"aria-valuemin": this.options.min,
+			"aria-valuemax": this.options.max,
+			// TODO: what should we do with values that can't be parsed?
+			"aria-valuenow": this._parse( this.element.val() )
+		});
+	},
+
+	isValid: function() {
+		var value = this.value();
+
+		// null is invalid
+		if ( value === null ) {
+			return false;
+		}
+
+		// if value gets adjusted, it's invalid
+		return value === this._adjustValue( value );
+	},
+
+	// update the value without triggering change
+	_value: function( value, allowAny ) {
+		var parsed;
+		if ( value !== "" ) {
+			parsed = this._parse( value );
+			if ( parsed !== null ) {
+				if ( !allowAny ) {
+					parsed = this._adjustValue( parsed );
+				}
+				value = this._format( parsed );
+			}
+		}
+		this.element.val( value );
+		this._refresh();
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-spinner-input" )
+			.prop( "disabled", false )
+			.removeAttr( "autocomplete" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-valuemin" )
+			.removeAttr( "aria-valuemax" )
+			.removeAttr( "aria-valuenow" );
+		this.uiSpinner.replaceWith( this.element );
+	},
+
+	stepUp: spinner_modifier(function( steps ) {
+		this._stepUp( steps );
+	}),
+	_stepUp: function( steps ) {
+		if ( this._start() ) {
+			this._spin( (steps || 1) * this.options.step );
+			this._stop();
+		}
+	},
+
+	stepDown: spinner_modifier(function( steps ) {
+		this._stepDown( steps );
+	}),
+	_stepDown: function( steps ) {
+		if ( this._start() ) {
+			this._spin( (steps || 1) * -this.options.step );
+			this._stop();
+		}
+	},
+
+	pageUp: spinner_modifier(function( pages ) {
+		this._stepUp( (pages || 1) * this.options.page );
+	}),
+
+	pageDown: spinner_modifier(function( pages ) {
+		this._stepDown( (pages || 1) * this.options.page );
+	}),
+
+	value: function( newVal ) {
+		if ( !arguments.length ) {
+			return this._parse( this.element.val() );
+		}
+		spinner_modifier( this._value ).call( this, newVal );
+	},
+
+	widget: function() {
+		return this.uiSpinner;
+	}
+});
+
+
+/*!
+ * jQuery UI Tabs 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/tabs/
+ */
+
+
+var tabs = $.widget( "ui.tabs", {
+	version: "1.11.2",
+	delay: 300,
+	options: {
+		active: null,
+		collapsible: false,
+		event: "click",
+		heightStyle: "content",
+		hide: null,
+		show: null,
+
+		// callbacks
+		activate: null,
+		beforeActivate: null,
+		beforeLoad: null,
+		load: null
+	},
+
+	_isLocal: (function() {
+		var rhash = /#.*$/;
+
+		return function( anchor ) {
+			var anchorUrl, locationUrl;
+
+			// support: IE7
+			// IE7 doesn't normalize the href property when set via script (#9317)
+			anchor = anchor.cloneNode( false );
+
+			anchorUrl = anchor.href.replace( rhash, "" );
+			locationUrl = location.href.replace( rhash, "" );
+
+			// decoding may throw an error if the URL isn't UTF-8 (#9518)
+			try {
+				anchorUrl = decodeURIComponent( anchorUrl );
+			} catch ( error ) {}
+			try {
+				locationUrl = decodeURIComponent( locationUrl );
+			} catch ( error ) {}
+
+			return anchor.hash.length > 1 && anchorUrl === locationUrl;
+		};
+	})(),
+
+	_create: function() {
+		var that = this,
+			options = this.options;
+
+		this.running = false;
+
+		this.element
+			.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" )
+			.toggleClass( "ui-tabs-collapsible", options.collapsible );
+
+		this._processTabs();
+		options.active = this._initialActive();
+
+		// Take disabling tabs via class attribute from HTML
+		// into account and update option properly.
+		if ( $.isArray( options.disabled ) ) {
+			options.disabled = $.unique( options.disabled.concat(
+				$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
+					return that.tabs.index( li );
+				})
+			) ).sort();
+		}
+
+		// check for length avoids error when initializing empty list
+		if ( this.options.active !== false && this.anchors.length ) {
+			this.active = this._findActive( options.active );
+		} else {
+			this.active = $();
+		}
+
+		this._refresh();
+
+		if ( this.active.length ) {
+			this.load( options.active );
+		}
+	},
+
+	_initialActive: function() {
+		var active = this.options.active,
+			collapsible = this.options.collapsible,
+			locationHash = location.hash.substring( 1 );
+
+		if ( active === null ) {
+			// check the fragment identifier in the URL
+			if ( locationHash ) {
+				this.tabs.each(function( i, tab ) {
+					if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
+						active = i;
+						return false;
+					}
+				});
+			}
+
+			// check for a tab marked active via a class
+			if ( active === null ) {
+				active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
+			}
+
+			// no active tab, set to false
+			if ( active === null || active === -1 ) {
+				active = this.tabs.length ? 0 : false;
+			}
+		}
+
+		// handle numbers: negative, out of range
+		if ( active !== false ) {
+			active = this.tabs.index( this.tabs.eq( active ) );
+			if ( active === -1 ) {
+				active = collapsible ? false : 0;
+			}
+		}
+
+		// don't allow collapsible: false and active: false
+		if ( !collapsible && active === false && this.anchors.length ) {
+			active = 0;
+		}
+
+		return active;
+	},
+
+	_getCreateEventData: function() {
+		return {
+			tab: this.active,
+			panel: !this.active.length ? $() : this._getPanelForTab( this.active )
+		};
+	},
+
+	_tabKeydown: function( event ) {
+		var focusedTab = $( this.document[0].activeElement ).closest( "li" ),
+			selectedIndex = this.tabs.index( focusedTab ),
+			goingForward = true;
+
+		if ( this._handlePageNav( event ) ) {
+			return;
+		}
+
+		switch ( event.keyCode ) {
+			case $.ui.keyCode.RIGHT:
+			case $.ui.keyCode.DOWN:
+				selectedIndex++;
+				break;
+			case $.ui.keyCode.UP:
+			case $.ui.keyCode.LEFT:
+				goingForward = false;
+				selectedIndex--;
+				break;
+			case $.ui.keyCode.END:
+				selectedIndex = this.anchors.length - 1;
+				break;
+			case $.ui.keyCode.HOME:
+				selectedIndex = 0;
+				break;
+			case $.ui.keyCode.SPACE:
+				// Activate only, no collapsing
+				event.preventDefault();
+				clearTimeout( this.activating );
+				this._activate( selectedIndex );
+				return;
+			case $.ui.keyCode.ENTER:
+				// Toggle (cancel delayed activation, allow collapsing)
+				event.preventDefault();
+				clearTimeout( this.activating );
+				// Determine if we should collapse or activate
+				this._activate( selectedIndex === this.options.active ? false : selectedIndex );
+				return;
+			default:
+				return;
+		}
+
+		// Focus the appropriate tab, based on which key was pressed
+		event.preventDefault();
+		clearTimeout( this.activating );
+		selectedIndex = this._focusNextTab( selectedIndex, goingForward );
+
+		// Navigating with control key will prevent automatic activation
+		if ( !event.ctrlKey ) {
+			// Update aria-selected immediately so that AT think the tab is already selected.
+			// Otherwise AT may confuse the user by stating that they need to activate the tab,
+			// but the tab will already be activated by the time the announcement finishes.
+			focusedTab.attr( "aria-selected", "false" );
+			this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
+
+			this.activating = this._delay(function() {
+				this.option( "active", selectedIndex );
+			}, this.delay );
+		}
+	},
+
+	_panelKeydown: function( event ) {
+		if ( this._handlePageNav( event ) ) {
+			return;
+		}
+
+		// Ctrl+up moves focus to the current tab
+		if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
+			event.preventDefault();
+			this.active.focus();
+		}
+	},
+
+	// Alt+page up/down moves focus to the previous/next tab (and activates)
+	_handlePageNav: function( event ) {
+		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
+			this._activate( this._focusNextTab( this.options.active - 1, false ) );
+			return true;
+		}
+		if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
+			this._activate( this._focusNextTab( this.options.active + 1, true ) );
+			return true;
+		}
+	},
+
+	_findNextTab: function( index, goingForward ) {
+		var lastTabIndex = this.tabs.length - 1;
+
+		function constrain() {
+			if ( index > lastTabIndex ) {
+				index = 0;
+			}
+			if ( index < 0 ) {
+				index = lastTabIndex;
+			}
+			return index;
+		}
+
+		while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
+			index = goingForward ? index + 1 : index - 1;
+		}
+
+		return index;
+	},
+
+	_focusNextTab: function( index, goingForward ) {
+		index = this._findNextTab( index, goingForward );
+		this.tabs.eq( index ).focus();
+		return index;
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "active" ) {
+			// _activate() will handle invalid values and update this.options
+			this._activate( value );
+			return;
+		}
+
+		if ( key === "disabled" ) {
+			// don't use the widget factory's disabled handling
+			this._setupDisabled( value );
+			return;
+		}
+
+		this._super( key, value);
+
+		if ( key === "collapsible" ) {
+			this.element.toggleClass( "ui-tabs-collapsible", value );
+			// Setting collapsible: false while collapsed; open first panel
+			if ( !value && this.options.active === false ) {
+				this._activate( 0 );
+			}
+		}
+
+		if ( key === "event" ) {
+			this._setupEvents( value );
+		}
+
+		if ( key === "heightStyle" ) {
+			this._setupHeightStyle( value );
+		}
+	},
+
+	_sanitizeSelector: function( hash ) {
+		return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
+	},
+
+	refresh: function() {
+		var options = this.options,
+			lis = this.tablist.children( ":has(a[href])" );
+
+		// get disabled tabs from class attribute from HTML
+		// this will get converted to a boolean if needed in _refresh()
+		options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
+			return lis.index( tab );
+		});
+
+		this._processTabs();
+
+		// was collapsed or no tabs
+		if ( options.active === false || !this.anchors.length ) {
+			options.active = false;
+			this.active = $();
+		// was active, but active tab is gone
+		} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
+			// all remaining tabs are disabled
+			if ( this.tabs.length === options.disabled.length ) {
+				options.active = false;
+				this.active = $();
+			// activate previous tab
+			} else {
+				this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
+			}
+		// was active, active tab still exists
+		} else {
+			// make sure active index is correct
+			options.active = this.tabs.index( this.active );
+		}
+
+		this._refresh();
+	},
+
+	_refresh: function() {
+		this._setupDisabled( this.options.disabled );
+		this._setupEvents( this.options.event );
+		this._setupHeightStyle( this.options.heightStyle );
+
+		this.tabs.not( this.active ).attr({
+			"aria-selected": "false",
+			"aria-expanded": "false",
+			tabIndex: -1
+		});
+		this.panels.not( this._getPanelForTab( this.active ) )
+			.hide()
+			.attr({
+				"aria-hidden": "true"
+			});
+
+		// Make sure one tab is in the tab order
+		if ( !this.active.length ) {
+			this.tabs.eq( 0 ).attr( "tabIndex", 0 );
+		} else {
+			this.active
+				.addClass( "ui-tabs-active ui-state-active" )
+				.attr({
+					"aria-selected": "true",
+					"aria-expanded": "true",
+					tabIndex: 0
+				});
+			this._getPanelForTab( this.active )
+				.show()
+				.attr({
+					"aria-hidden": "false"
+				});
+		}
+	},
+
+	_processTabs: function() {
+		var that = this,
+			prevTabs = this.tabs,
+			prevAnchors = this.anchors,
+			prevPanels = this.panels;
+
+		this.tablist = this._getList()
+			.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
+			.attr( "role", "tablist" )
+
+			// Prevent users from focusing disabled tabs via click
+			.delegate( "> li", "mousedown" + this.eventNamespace, function( event ) {
+				if ( $( this ).is( ".ui-state-disabled" ) ) {
+					event.preventDefault();
+				}
+			})
+
+			// support: IE <9
+			// Preventing the default action in mousedown doesn't prevent IE
+			// from focusing the element, so if the anchor gets focused, blur.
+			// We don't have to worry about focusing the previously focused
+			// element since clicking on a non-focusable element should focus
+			// the body anyway.
+			.delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
+				if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
+					this.blur();
+				}
+			});
+
+		this.tabs = this.tablist.find( "> li:has(a[href])" )
+			.addClass( "ui-state-default ui-corner-top" )
+			.attr({
+				role: "tab",
+				tabIndex: -1
+			});
+
+		this.anchors = this.tabs.map(function() {
+				return $( "a", this )[ 0 ];
+			})
+			.addClass( "ui-tabs-anchor" )
+			.attr({
+				role: "presentation",
+				tabIndex: -1
+			});
+
+		this.panels = $();
+
+		this.anchors.each(function( i, anchor ) {
+			var selector, panel, panelId,
+				anchorId = $( anchor ).uniqueId().attr( "id" ),
+				tab = $( anchor ).closest( "li" ),
+				originalAriaControls = tab.attr( "aria-controls" );
+
+			// inline tab
+			if ( that._isLocal( anchor ) ) {
+				selector = anchor.hash;
+				panelId = selector.substring( 1 );
+				panel = that.element.find( that._sanitizeSelector( selector ) );
+			// remote tab
+			} else {
+				// If the tab doesn't already have aria-controls,
+				// generate an id by using a throw-away element
+				panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
+				selector = "#" + panelId;
+				panel = that.element.find( selector );
+				if ( !panel.length ) {
+					panel = that._createPanel( panelId );
+					panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
+				}
+				panel.attr( "aria-live", "polite" );
+			}
+
+			if ( panel.length) {
+				that.panels = that.panels.add( panel );
+			}
+			if ( originalAriaControls ) {
+				tab.data( "ui-tabs-aria-controls", originalAriaControls );
+			}
+			tab.attr({
+				"aria-controls": panelId,
+				"aria-labelledby": anchorId
+			});
+			panel.attr( "aria-labelledby", anchorId );
+		});
+
+		this.panels
+			.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
+			.attr( "role", "tabpanel" );
+
+		// Avoid memory leaks (#10056)
+		if ( prevTabs ) {
+			this._off( prevTabs.not( this.tabs ) );
+			this._off( prevAnchors.not( this.anchors ) );
+			this._off( prevPanels.not( this.panels ) );
+		}
+	},
+
+	// allow overriding how to find the list for rare usage scenarios (#7715)
+	_getList: function() {
+		return this.tablist || this.element.find( "ol,ul" ).eq( 0 );
+	},
+
+	_createPanel: function( id ) {
+		return $( "<div>" )
+			.attr( "id", id )
+			.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
+			.data( "ui-tabs-destroy", true );
+	},
+
+	_setupDisabled: function( disabled ) {
+		if ( $.isArray( disabled ) ) {
+			if ( !disabled.length ) {
+				disabled = false;
+			} else if ( disabled.length === this.anchors.length ) {
+				disabled = true;
+			}
+		}
+
+		// disable tabs
+		for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) {
+			if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
+				$( li )
+					.addClass( "ui-state-disabled" )
+					.attr( "aria-disabled", "true" );
+			} else {
+				$( li )
+					.removeClass( "ui-state-disabled" )
+					.removeAttr( "aria-disabled" );
+			}
+		}
+
+		this.options.disabled = disabled;
+	},
+
+	_setupEvents: function( event ) {
+		var events = {};
+		if ( event ) {
+			$.each( event.split(" "), function( index, eventName ) {
+				events[ eventName ] = "_eventHandler";
+			});
+		}
+
+		this._off( this.anchors.add( this.tabs ).add( this.panels ) );
+		// Always prevent the default action, even when disabled
+		this._on( true, this.anchors, {
+			click: function( event ) {
+				event.preventDefault();
+			}
+		});
+		this._on( this.anchors, events );
+		this._on( this.tabs, { keydown: "_tabKeydown" } );
+		this._on( this.panels, { keydown: "_panelKeydown" } );
+
+		this._focusable( this.tabs );
+		this._hoverable( this.tabs );
+	},
+
+	_setupHeightStyle: function( heightStyle ) {
+		var maxHeight,
+			parent = this.element.parent();
+
+		if ( heightStyle === "fill" ) {
+			maxHeight = parent.height();
+			maxHeight -= this.element.outerHeight() - this.element.height();
+
+			this.element.siblings( ":visible" ).each(function() {
+				var elem = $( this ),
+					position = elem.css( "position" );
+
+				if ( position === "absolute" || position === "fixed" ) {
+					return;
+				}
+				maxHeight -= elem.outerHeight( true );
+			});
+
+			this.element.children().not( this.panels ).each(function() {
+				maxHeight -= $( this ).outerHeight( true );
+			});
+
+			this.panels.each(function() {
+				$( this ).height( Math.max( 0, maxHeight -
+					$( this ).innerHeight() + $( this ).height() ) );
+			})
+			.css( "overflow", "auto" );
+		} else if ( heightStyle === "auto" ) {
+			maxHeight = 0;
+			this.panels.each(function() {
+				maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
+			}).height( maxHeight );
+		}
+	},
+
+	_eventHandler: function( event ) {
+		var options = this.options,
+			active = this.active,
+			anchor = $( event.currentTarget ),
+			tab = anchor.closest( "li" ),
+			clickedIsActive = tab[ 0 ] === active[ 0 ],
+			collapsing = clickedIsActive && options.collapsible,
+			toShow = collapsing ? $() : this._getPanelForTab( tab ),
+			toHide = !active.length ? $() : this._getPanelForTab( active ),
+			eventData = {
+				oldTab: active,
+				oldPanel: toHide,
+				newTab: collapsing ? $() : tab,
+				newPanel: toShow
+			};
+
+		event.preventDefault();
+
+		if ( tab.hasClass( "ui-state-disabled" ) ||
+				// tab is already loading
+				tab.hasClass( "ui-tabs-loading" ) ||
+				// can't switch durning an animation
+				this.running ||
+				// click on active header, but not collapsible
+				( clickedIsActive && !options.collapsible ) ||
+				// allow canceling activation
+				( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
+			return;
+		}
+
+		options.active = collapsing ? false : this.tabs.index( tab );
+
+		this.active = clickedIsActive ? $() : tab;
+		if ( this.xhr ) {
+			this.xhr.abort();
+		}
+
+		if ( !toHide.length && !toShow.length ) {
+			$.error( "jQuery UI Tabs: Mismatching fragment identifier." );
+		}
+
+		if ( toShow.length ) {
+			this.load( this.tabs.index( tab ), event );
+		}
+		this._toggle( event, eventData );
+	},
+
+	// handles show/hide for selecting tabs
+	_toggle: function( event, eventData ) {
+		var that = this,
+			toShow = eventData.newPanel,
+			toHide = eventData.oldPanel;
+
+		this.running = true;
+
+		function complete() {
+			that.running = false;
+			that._trigger( "activate", event, eventData );
+		}
+
+		function show() {
+			eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
+
+			if ( toShow.length && that.options.show ) {
+				that._show( toShow, that.options.show, complete );
+			} else {
+				toShow.show();
+				complete();
+			}
+		}
+
+		// start out by hiding, then showing, then completing
+		if ( toHide.length && this.options.hide ) {
+			this._hide( toHide, this.options.hide, function() {
+				eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
+				show();
+			});
+		} else {
+			eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
+			toHide.hide();
+			show();
+		}
+
+		toHide.attr( "aria-hidden", "true" );
+		eventData.oldTab.attr({
+			"aria-selected": "false",
+			"aria-expanded": "false"
+		});
+		// If we're switching tabs, remove the old tab from the tab order.
+		// If we're opening from collapsed state, remove the previous tab from the tab order.
+		// If we're collapsing, then keep the collapsing tab in the tab order.
+		if ( toShow.length && toHide.length ) {
+			eventData.oldTab.attr( "tabIndex", -1 );
+		} else if ( toShow.length ) {
+			this.tabs.filter(function() {
+				return $( this ).attr( "tabIndex" ) === 0;
+			})
+			.attr( "tabIndex", -1 );
+		}
+
+		toShow.attr( "aria-hidden", "false" );
+		eventData.newTab.attr({
+			"aria-selected": "true",
+			"aria-expanded": "true",
+			tabIndex: 0
+		});
+	},
+
+	_activate: function( index ) {
+		var anchor,
+			active = this._findActive( index );
+
+		// trying to activate the already active panel
+		if ( active[ 0 ] === this.active[ 0 ] ) {
+			return;
+		}
+
+		// trying to collapse, simulate a click on the current active header
+		if ( !active.length ) {
+			active = this.active;
+		}
+
+		anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
+		this._eventHandler({
+			target: anchor,
+			currentTarget: anchor,
+			preventDefault: $.noop
+		});
+	},
+
+	_findActive: function( index ) {
+		return index === false ? $() : this.tabs.eq( index );
+	},
+
+	_getIndex: function( index ) {
+		// meta-function to give users option to provide a href string instead of a numerical index.
+		if ( typeof index === "string" ) {
+			index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) );
+		}
+
+		return index;
+	},
+
+	_destroy: function() {
+		if ( this.xhr ) {
+			this.xhr.abort();
+		}
+
+		this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" );
+
+		this.tablist
+			.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
+			.removeAttr( "role" );
+
+		this.anchors
+			.removeClass( "ui-tabs-anchor" )
+			.removeAttr( "role" )
+			.removeAttr( "tabIndex" )
+			.removeUniqueId();
+
+		this.tablist.unbind( this.eventNamespace );
+
+		this.tabs.add( this.panels ).each(function() {
+			if ( $.data( this, "ui-tabs-destroy" ) ) {
+				$( this ).remove();
+			} else {
+				$( this )
+					.removeClass( "ui-state-default ui-state-active ui-state-disabled " +
+						"ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" )
+					.removeAttr( "tabIndex" )
+					.removeAttr( "aria-live" )
+					.removeAttr( "aria-busy" )
+					.removeAttr( "aria-selected" )
+					.removeAttr( "aria-labelledby" )
+					.removeAttr( "aria-hidden" )
+					.removeAttr( "aria-expanded" )
+					.removeAttr( "role" );
+			}
+		});
+
+		this.tabs.each(function() {
+			var li = $( this ),
+				prev = li.data( "ui-tabs-aria-controls" );
+			if ( prev ) {
+				li
+					.attr( "aria-controls", prev )
+					.removeData( "ui-tabs-aria-controls" );
+			} else {
+				li.removeAttr( "aria-controls" );
+			}
+		});
+
+		this.panels.show();
+
+		if ( this.options.heightStyle !== "content" ) {
+			this.panels.css( "height", "" );
+		}
+	},
+
+	enable: function( index ) {
+		var disabled = this.options.disabled;
+		if ( disabled === false ) {
+			return;
+		}
+
+		if ( index === undefined ) {
+			disabled = false;
+		} else {
+			index = this._getIndex( index );
+			if ( $.isArray( disabled ) ) {
+				disabled = $.map( disabled, function( num ) {
+					return num !== index ? num : null;
+				});
+			} else {
+				disabled = $.map( this.tabs, function( li, num ) {
+					return num !== index ? num : null;
+				});
+			}
+		}
+		this._setupDisabled( disabled );
+	},
+
+	disable: function( index ) {
+		var disabled = this.options.disabled;
+		if ( disabled === true ) {
+			return;
+		}
+
+		if ( index === undefined ) {
+			disabled = true;
+		} else {
+			index = this._getIndex( index );
+			if ( $.inArray( index, disabled ) !== -1 ) {
+				return;
+			}
+			if ( $.isArray( disabled ) ) {
+				disabled = $.merge( [ index ], disabled ).sort();
+			} else {
+				disabled = [ index ];
+			}
+		}
+		this._setupDisabled( disabled );
+	},
+
+	load: function( index, event ) {
+		index = this._getIndex( index );
+		var that = this,
+			tab = this.tabs.eq( index ),
+			anchor = tab.find( ".ui-tabs-anchor" ),
+			panel = this._getPanelForTab( tab ),
+			eventData = {
+				tab: tab,
+				panel: panel
+			};
+
+		// not remote
+		if ( this._isLocal( anchor[ 0 ] ) ) {
+			return;
+		}
+
+		this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
+
+		// support: jQuery <1.8
+		// jQuery <1.8 returns false if the request is canceled in beforeSend,
+		// but as of 1.8, $.ajax() always returns a jqXHR object.
+		if ( this.xhr && this.xhr.statusText !== "canceled" ) {
+			tab.addClass( "ui-tabs-loading" );
+			panel.attr( "aria-busy", "true" );
+
+			this.xhr
+				.success(function( response ) {
+					// support: jQuery <1.8
+					// http://bugs.jquery.com/ticket/11778
+					setTimeout(function() {
+						panel.html( response );
+						that._trigger( "load", event, eventData );
+					}, 1 );
+				})
+				.complete(function( jqXHR, status ) {
+					// support: jQuery <1.8
+					// http://bugs.jquery.com/ticket/11778
+					setTimeout(function() {
+						if ( status === "abort" ) {
+							that.panels.stop( false, true );
+						}
+
+						tab.removeClass( "ui-tabs-loading" );
+						panel.removeAttr( "aria-busy" );
+
+						if ( jqXHR === that.xhr ) {
+							delete that.xhr;
+						}
+					}, 1 );
+				});
+		}
+	},
+
+	_ajaxSettings: function( anchor, event, eventData ) {
+		var that = this;
+		return {
+			url: anchor.attr( "href" ),
+			beforeSend: function( jqXHR, settings ) {
+				return that._trigger( "beforeLoad", event,
+					$.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
+			}
+		};
+	},
+
+	_getPanelForTab: function( tab ) {
+		var id = $( tab ).attr( "aria-controls" );
+		return this.element.find( this._sanitizeSelector( "#" + id ) );
+	}
+});
+
+
+/*!
+ * jQuery UI Tooltip 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/tooltip/
+ */
+
+
+var tooltip = $.widget( "ui.tooltip", {
+	version: "1.11.2",
+	options: {
+		content: function() {
+			// support: IE<9, Opera in jQuery <1.7
+			// .text() can't accept undefined, so coerce to a string
+			var title = $( this ).attr( "title" ) || "";
+			// Escape title, since we're going from an attribute to raw HTML
+			return $( "<a>" ).text( title ).html();
+		},
+		hide: true,
+		// Disabled elements have inconsistent behavior across browsers (#8661)
+		items: "[title]:not([disabled])",
+		position: {
+			my: "left top+15",
+			at: "left bottom",
+			collision: "flipfit flip"
+		},
+		show: true,
+		tooltipClass: null,
+		track: false,
+
+		// callbacks
+		close: null,
+		open: null
+	},
+
+	_addDescribedBy: function( elem, id ) {
+		var describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ );
+		describedby.push( id );
+		elem
+			.data( "ui-tooltip-id", id )
+			.attr( "aria-describedby", $.trim( describedby.join( " " ) ) );
+	},
+
+	_removeDescribedBy: function( elem ) {
+		var id = elem.data( "ui-tooltip-id" ),
+			describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ ),
+			index = $.inArray( id, describedby );
+
+		if ( index !== -1 ) {
+			describedby.splice( index, 1 );
+		}
+
+		elem.removeData( "ui-tooltip-id" );
+		describedby = $.trim( describedby.join( " " ) );
+		if ( describedby ) {
+			elem.attr( "aria-describedby", describedby );
+		} else {
+			elem.removeAttr( "aria-describedby" );
+		}
+	},
+
+	_create: function() {
+		this._on({
+			mouseover: "open",
+			focusin: "open"
+		});
+
+		// IDs of generated tooltips, needed for destroy
+		this.tooltips = {};
+
+		// IDs of parent tooltips where we removed the title attribute
+		this.parents = {};
+
+		if ( this.options.disabled ) {
+			this._disable();
+		}
+
+		// Append the aria-live region so tooltips announce correctly
+		this.liveRegion = $( "<div>" )
+			.attr({
+				role: "log",
+				"aria-live": "assertive",
+				"aria-relevant": "additions"
+			})
+			.addClass( "ui-helper-hidden-accessible" )
+			.appendTo( this.document[ 0 ].body );
+	},
+
+	_setOption: function( key, value ) {
+		var that = this;
+
+		if ( key === "disabled" ) {
+			this[ value ? "_disable" : "_enable" ]();
+			this.options[ key ] = value;
+			// disable element style changes
+			return;
+		}
+
+		this._super( key, value );
+
+		if ( key === "content" ) {
+			$.each( this.tooltips, function( id, tooltipData ) {
+				that._updateContent( tooltipData.element );
+			});
+		}
+	},
+
+	_disable: function() {
+		var that = this;
+
+		// close open tooltips
+		$.each( this.tooltips, function( id, tooltipData ) {
+			var event = $.Event( "blur" );
+			event.target = event.currentTarget = tooltipData.element[ 0 ];
+			that.close( event, true );
+		});
+
+		// remove title attributes to prevent native tooltips
+		this.element.find( this.options.items ).addBack().each(function() {
+			var element = $( this );
+			if ( element.is( "[title]" ) ) {
+				element
+					.data( "ui-tooltip-title", element.attr( "title" ) )
+					.removeAttr( "title" );
+			}
+		});
+	},
+
+	_enable: function() {
+		// restore title attributes
+		this.element.find( this.options.items ).addBack().each(function() {
+			var element = $( this );
+			if ( element.data( "ui-tooltip-title" ) ) {
+				element.attr( "title", element.data( "ui-tooltip-title" ) );
+			}
+		});
+	},
+
+	open: function( event ) {
+		var that = this,
+			target = $( event ? event.target : this.element )
+				// we need closest here due to mouseover bubbling,
+				// but always pointing at the same event target
+				.closest( this.options.items );
+
+		// No element to show a tooltip for or the tooltip is already open
+		if ( !target.length || target.data( "ui-tooltip-id" ) ) {
+			return;
+		}
+
+		if ( target.attr( "title" ) ) {
+			target.data( "ui-tooltip-title", target.attr( "title" ) );
+		}
+
+		target.data( "ui-tooltip-open", true );
+
+		// kill parent tooltips, custom or native, for hover
+		if ( event && event.type === "mouseover" ) {
+			target.parents().each(function() {
+				var parent = $( this ),
+					blurEvent;
+				if ( parent.data( "ui-tooltip-open" ) ) {
+					blurEvent = $.Event( "blur" );
+					blurEvent.target = blurEvent.currentTarget = this;
+					that.close( blurEvent, true );
+				}
+				if ( parent.attr( "title" ) ) {
+					parent.uniqueId();
+					that.parents[ this.id ] = {
+						element: this,
+						title: parent.attr( "title" )
+					};
+					parent.attr( "title", "" );
+				}
+			});
+		}
+
+		this._updateContent( target, event );
+	},
+
+	_updateContent: function( target, event ) {
+		var content,
+			contentOption = this.options.content,
+			that = this,
+			eventType = event ? event.type : null;
+
+		if ( typeof contentOption === "string" ) {
+			return this._open( event, target, contentOption );
+		}
+
+		content = contentOption.call( target[0], function( response ) {
+			// ignore async response if tooltip was closed already
+			if ( !target.data( "ui-tooltip-open" ) ) {
+				return;
+			}
+			// IE may instantly serve a cached response for ajax requests
+			// delay this call to _open so the other call to _open runs first
+			that._delay(function() {
+				// jQuery creates a special event for focusin when it doesn't
+				// exist natively. To improve performance, the native event
+				// object is reused and the type is changed. Therefore, we can't
+				// rely on the type being correct after the event finished
+				// bubbling, so we set it back to the previous value. (#8740)
+				if ( event ) {
+					event.type = eventType;
+				}
+				this._open( event, target, response );
+			});
+		});
+		if ( content ) {
+			this._open( event, target, content );
+		}
+	},
+
+	_open: function( event, target, content ) {
+		var tooltipData, tooltip, events, delayedShow, a11yContent,
+			positionOption = $.extend( {}, this.options.position );
+
+		if ( !content ) {
+			return;
+		}
+
+		// Content can be updated multiple times. If the tooltip already
+		// exists, then just update the content and bail.
+		tooltipData = this._find( target );
+		if ( tooltipData ) {
+			tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content );
+			return;
+		}
+
+		// if we have a title, clear it to prevent the native tooltip
+		// we have to check first to avoid defining a title if none exists
+		// (we don't want to cause an element to start matching [title])
+		//
+		// We use removeAttr only for key events, to allow IE to export the correct
+		// accessible attributes. For mouse events, set to empty string to avoid
+		// native tooltip showing up (happens only when removing inside mouseover).
+		if ( target.is( "[title]" ) ) {
+			if ( event && event.type === "mouseover" ) {
+				target.attr( "title", "" );
+			} else {
+				target.removeAttr( "title" );
+			}
+		}
+
+		tooltipData = this._tooltip( target );
+		tooltip = tooltipData.tooltip;
+		this._addDescribedBy( target, tooltip.attr( "id" ) );
+		tooltip.find( ".ui-tooltip-content" ).html( content );
+
+		// Support: Voiceover on OS X, JAWS on IE <= 9
+		// JAWS announces deletions even when aria-relevant="additions"
+		// Voiceover will sometimes re-read the entire log region's contents from the beginning
+		this.liveRegion.children().hide();
+		if ( content.clone ) {
+			a11yContent = content.clone();
+			a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" );
+		} else {
+			a11yContent = content;
+		}
+		$( "<div>" ).html( a11yContent ).appendTo( this.liveRegion );
+
+		function position( event ) {
+			positionOption.of = event;
+			if ( tooltip.is( ":hidden" ) ) {
+				return;
+			}
+			tooltip.position( positionOption );
+		}
+		if ( this.options.track && event && /^mouse/.test( event.type ) ) {
+			this._on( this.document, {
+				mousemove: position
+			});
+			// trigger once to override element-relative positioning
+			position( event );
+		} else {
+			tooltip.position( $.extend({
+				of: target
+			}, this.options.position ) );
+		}
+
+		tooltip.hide();
+
+		this._show( tooltip, this.options.show );
+		// Handle tracking tooltips that are shown with a delay (#8644). As soon
+		// as the tooltip is visible, position the tooltip using the most recent
+		// event.
+		if ( this.options.show && this.options.show.delay ) {
+			delayedShow = this.delayedShow = setInterval(function() {
+				if ( tooltip.is( ":visible" ) ) {
+					position( positionOption.of );
+					clearInterval( delayedShow );
+				}
+			}, $.fx.interval );
+		}
+
+		this._trigger( "open", event, { tooltip: tooltip } );
+
+		events = {
+			keyup: function( event ) {
+				if ( event.keyCode === $.ui.keyCode.ESCAPE ) {
+					var fakeEvent = $.Event(event);
+					fakeEvent.currentTarget = target[0];
+					this.close( fakeEvent, true );
+				}
+			}
+		};
+
+		// Only bind remove handler for delegated targets. Non-delegated
+		// tooltips will handle this in destroy.
+		if ( target[ 0 ] !== this.element[ 0 ] ) {
+			events.remove = function() {
+				this._removeTooltip( tooltip );
+			};
+		}
+
+		if ( !event || event.type === "mouseover" ) {
+			events.mouseleave = "close";
+		}
+		if ( !event || event.type === "focusin" ) {
+			events.focusout = "close";
+		}
+		this._on( true, target, events );
+	},
+
+	close: function( event ) {
+		var tooltip,
+			that = this,
+			target = $( event ? event.currentTarget : this.element ),
+			tooltipData = this._find( target );
+
+		// The tooltip may already be closed
+		if ( !tooltipData ) {
+			return;
+		}
+
+		tooltip = tooltipData.tooltip;
+
+		// disabling closes the tooltip, so we need to track when we're closing
+		// to avoid an infinite loop in case the tooltip becomes disabled on close
+		if ( tooltipData.closing ) {
+			return;
+		}
+
+		// Clear the interval for delayed tracking tooltips
+		clearInterval( this.delayedShow );
+
+		// only set title if we had one before (see comment in _open())
+		// If the title attribute has changed since open(), don't restore
+		if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) {
+			target.attr( "title", target.data( "ui-tooltip-title" ) );
+		}
+
+		this._removeDescribedBy( target );
+
+		tooltipData.hiding = true;
+		tooltip.stop( true );
+		this._hide( tooltip, this.options.hide, function() {
+			that._removeTooltip( $( this ) );
+		});
+
+		target.removeData( "ui-tooltip-open" );
+		this._off( target, "mouseleave focusout keyup" );
+
+		// Remove 'remove' binding only on delegated targets
+		if ( target[ 0 ] !== this.element[ 0 ] ) {
+			this._off( target, "remove" );
+		}
+		this._off( this.document, "mousemove" );
+
+		if ( event && event.type === "mouseleave" ) {
+			$.each( this.parents, function( id, parent ) {
+				$( parent.element ).attr( "title", parent.title );
+				delete that.parents[ id ];
+			});
+		}
+
+		tooltipData.closing = true;
+		this._trigger( "close", event, { tooltip: tooltip } );
+		if ( !tooltipData.hiding ) {
+			tooltipData.closing = false;
+		}
+	},
+
+	_tooltip: function( element ) {
+		var tooltip = $( "<div>" )
+				.attr( "role", "tooltip" )
+				.addClass( "ui-tooltip ui-widget ui-corner-all ui-widget-content " +
+					( this.options.tooltipClass || "" ) ),
+			id = tooltip.uniqueId().attr( "id" );
+
+		$( "<div>" )
+			.addClass( "ui-tooltip-content" )
+			.appendTo( tooltip );
+
+		tooltip.appendTo( this.document[0].body );
+
+		return this.tooltips[ id ] = {
+			element: element,
+			tooltip: tooltip
+		};
+	},
+
+	_find: function( target ) {
+		var id = target.data( "ui-tooltip-id" );
+		return id ? this.tooltips[ id ] : null;
+	},
+
+	_removeTooltip: function( tooltip ) {
+		tooltip.remove();
+		delete this.tooltips[ tooltip.attr( "id" ) ];
+	},
+
+	_destroy: function() {
+		var that = this;
+
+		// close open tooltips
+		$.each( this.tooltips, function( id, tooltipData ) {
+			// Delegate to close method to handle common cleanup
+			var event = $.Event( "blur" ),
+				element = tooltipData.element;
+			event.target = event.currentTarget = element[ 0 ];
+			that.close( event, true );
+
+			// Remove immediately; destroying an open tooltip doesn't use the
+			// hide animation
+			$( "#" + id ).remove();
+
+			// Restore the title
+			if ( element.data( "ui-tooltip-title" ) ) {
+				// If the title attribute has changed since open(), don't restore
+				if ( !element.attr( "title" ) ) {
+					element.attr( "title", element.data( "ui-tooltip-title" ) );
+				}
+				element.removeData( "ui-tooltip-title" );
+			}
+		});
+		this.liveRegion.remove();
+	}
+});
+
+
+/*!
+ * jQuery UI Effects 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/effects-core/
+ */
+
+
+var dataSpace = "ui-effects-",
+
+	// Create a local jQuery because jQuery Color relies on it and the
+	// global may not exist with AMD and a custom build (#10199)
+	jQuery = $;
+
+$.effects = {
+	effect: {}
+};
+
+/*!
+ * jQuery Color Animations v2.1.2
+ * https://github.com/jquery/jquery-color
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * Date: Wed Jan 16 08:47:09 2013 -0600
+ */
+(function( jQuery, undefined ) {
+
+	var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",
+
+	// plusequals test for += 100 -= 100
+	rplusequals = /^([\-+])=\s*(\d+\.?\d*)/,
+	// a set of RE's that can match strings and generate color tuples.
+	stringParsers = [ {
+			re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ],
+					execResult[ 3 ],
+					execResult[ 4 ]
+				];
+			}
+		}, {
+			re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ] * 2.55,
+					execResult[ 2 ] * 2.55,
+					execResult[ 3 ] * 2.55,
+					execResult[ 4 ]
+				];
+			}
+		}, {
+			// this regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+			// this regex ignores A-F because it's compared against an already lowercased string
+			re: /#([a-f0-9])([a-f0-9])([a-f0-9])/,
+			parse: function( execResult ) {
+				return [
+					parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ),
+					parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ),
+					parseInt( execResult[ 3 ] + execResult[ 3 ], 16 )
+				];
+			}
+		}, {
+			re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,
+			space: "hsla",
+			parse: function( execResult ) {
+				return [
+					execResult[ 1 ],
+					execResult[ 2 ] / 100,
+					execResult[ 3 ] / 100,
+					execResult[ 4 ]
+				];
+			}
+		} ],
+
+	// jQuery.Color( )
+	color = jQuery.Color = function( color, green, blue, alpha ) {
+		return new jQuery.Color.fn.parse( color, green, blue, alpha );
+	},
+	spaces = {
+		rgba: {
+			props: {
+				red: {
+					idx: 0,
+					type: "byte"
+				},
+				green: {
+					idx: 1,
+					type: "byte"
+				},
+				blue: {
+					idx: 2,
+					type: "byte"
+				}
+			}
+		},
+
+		hsla: {
+			props: {
+				hue: {
+					idx: 0,
+					type: "degrees"
+				},
+				saturation: {
+					idx: 1,
+					type: "percent"
+				},
+				lightness: {
+					idx: 2,
+					type: "percent"
+				}
+			}
+		}
+	},
+	propTypes = {
+		"byte": {
+			floor: true,
+			max: 255
+		},
+		"percent": {
+			max: 1
+		},
+		"degrees": {
+			mod: 360,
+			floor: true
+		}
+	},
+	support = color.support = {},
+
+	// element for support tests
+	supportElem = jQuery( "<p>" )[ 0 ],
+
+	// colors = jQuery.Color.names
+	colors,
+
+	// local aliases of functions called often
+	each = jQuery.each;
+
+// determine rgba support immediately
+supportElem.style.cssText = "background-color:rgba(1,1,1,.5)";
+support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1;
+
+// define cache name and alpha properties
+// for rgba and hsla spaces
+each( spaces, function( spaceName, space ) {
+	space.cache = "_" + spaceName;
+	space.props.alpha = {
+		idx: 3,
+		type: "percent",
+		def: 1
+	};
+});
+
+function clamp( value, prop, allowEmpty ) {
+	var type = propTypes[ prop.type ] || {};
+
+	if ( value == null ) {
+		return (allowEmpty || !prop.def) ? null : prop.def;
+	}
+
+	// ~~ is an short way of doing floor for positive numbers
+	value = type.floor ? ~~value : parseFloat( value );
+
+	// IE will pass in empty strings as value for alpha,
+	// which will hit this case
+	if ( isNaN( value ) ) {
+		return prop.def;
+	}
+
+	if ( type.mod ) {
+		// we add mod before modding to make sure that negatives values
+		// get converted properly: -10 -> 350
+		return (value + type.mod) % type.mod;
+	}
+
+	// for now all property types without mod have min and max
+	return 0 > value ? 0 : type.max < value ? type.max : value;
+}
+
+function stringParse( string ) {
+	var inst = color(),
+		rgba = inst._rgba = [];
+
+	string = string.toLowerCase();
+
+	each( stringParsers, function( i, parser ) {
+		var parsed,
+			match = parser.re.exec( string ),
+			values = match && parser.parse( match ),
+			spaceName = parser.space || "rgba";
+
+		if ( values ) {
+			parsed = inst[ spaceName ]( values );
+
+			// if this was an rgba parse the assignment might happen twice
+			// oh well....
+			inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ];
+			rgba = inst._rgba = parsed._rgba;
+
+			// exit each( stringParsers ) here because we matched
+			return false;
+		}
+	});
+
+	// Found a stringParser that handled it
+	if ( rgba.length ) {
+
+		// if this came from a parsed string, force "transparent" when alpha is 0
+		// chrome, (and maybe others) return "transparent" as rgba(0,0,0,0)
+		if ( rgba.join() === "0,0,0,0" ) {
+			jQuery.extend( rgba, colors.transparent );
+		}
+		return inst;
+	}
+
+	// named colors
+	return colors[ string ];
+}
+
+color.fn = jQuery.extend( color.prototype, {
+	parse: function( red, green, blue, alpha ) {
+		if ( red === undefined ) {
+			this._rgba = [ null, null, null, null ];
+			return this;
+		}
+		if ( red.jquery || red.nodeType ) {
+			red = jQuery( red ).css( green );
+			green = undefined;
+		}
+
+		var inst = this,
+			type = jQuery.type( red ),
+			rgba = this._rgba = [];
+
+		// more than 1 argument specified - assume ( red, green, blue, alpha )
+		if ( green !== undefined ) {
+			red = [ red, green, blue, alpha ];
+			type = "array";
+		}
+
+		if ( type === "string" ) {
+			return this.parse( stringParse( red ) || colors._default );
+		}
+
+		if ( type === "array" ) {
+			each( spaces.rgba.props, function( key, prop ) {
+				rgba[ prop.idx ] = clamp( red[ prop.idx ], prop );
+			});
+			return this;
+		}
+
+		if ( type === "object" ) {
+			if ( red instanceof color ) {
+				each( spaces, function( spaceName, space ) {
+					if ( red[ space.cache ] ) {
+						inst[ space.cache ] = red[ space.cache ].slice();
+					}
+				});
+			} else {
+				each( spaces, function( spaceName, space ) {
+					var cache = space.cache;
+					each( space.props, function( key, prop ) {
+
+						// if the cache doesn't exist, and we know how to convert
+						if ( !inst[ cache ] && space.to ) {
+
+							// if the value was null, we don't need to copy it
+							// if the key was alpha, we don't need to copy it either
+							if ( key === "alpha" || red[ key ] == null ) {
+								return;
+							}
+							inst[ cache ] = space.to( inst._rgba );
+						}
+
+						// this is the only case where we allow nulls for ALL properties.
+						// call clamp with alwaysAllowEmpty
+						inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true );
+					});
+
+					// everything defined but alpha?
+					if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) {
+						// use the default of 1
+						inst[ cache ][ 3 ] = 1;
+						if ( space.from ) {
+							inst._rgba = space.from( inst[ cache ] );
+						}
+					}
+				});
+			}
+			return this;
+		}
+	},
+	is: function( compare ) {
+		var is = color( compare ),
+			same = true,
+			inst = this;
+
+		each( spaces, function( _, space ) {
+			var localCache,
+				isCache = is[ space.cache ];
+			if (isCache) {
+				localCache = inst[ space.cache ] || space.to && space.to( inst._rgba ) || [];
+				each( space.props, function( _, prop ) {
+					if ( isCache[ prop.idx ] != null ) {
+						same = ( isCache[ prop.idx ] === localCache[ prop.idx ] );
+						return same;
+					}
+				});
+			}
+			return same;
+		});
+		return same;
+	},
+	_space: function() {
+		var used = [],
+			inst = this;
+		each( spaces, function( spaceName, space ) {
+			if ( inst[ space.cache ] ) {
+				used.push( spaceName );
+			}
+		});
+		return used.pop();
+	},
+	transition: function( other, distance ) {
+		var end = color( other ),
+			spaceName = end._space(),
+			space = spaces[ spaceName ],
+			startColor = this.alpha() === 0 ? color( "transparent" ) : this,
+			start = startColor[ space.cache ] || space.to( startColor._rgba ),
+			result = start.slice();
+
+		end = end[ space.cache ];
+		each( space.props, function( key, prop ) {
+			var index = prop.idx,
+				startValue = start[ index ],
+				endValue = end[ index ],
+				type = propTypes[ prop.type ] || {};
+
+			// if null, don't override start value
+			if ( endValue === null ) {
+				return;
+			}
+			// if null - use end
+			if ( startValue === null ) {
+				result[ index ] = endValue;
+			} else {
+				if ( type.mod ) {
+					if ( endValue - startValue > type.mod / 2 ) {
+						startValue += type.mod;
+					} else if ( startValue - endValue > type.mod / 2 ) {
+						startValue -= type.mod;
+					}
+				}
+				result[ index ] = clamp( ( endValue - startValue ) * distance + startValue, prop );
+			}
+		});
+		return this[ spaceName ]( result );
+	},
+	blend: function( opaque ) {
+		// if we are already opaque - return ourself
+		if ( this._rgba[ 3 ] === 1 ) {
+			return this;
+		}
+
+		var rgb = this._rgba.slice(),
+			a = rgb.pop(),
+			blend = color( opaque )._rgba;
+
+		return color( jQuery.map( rgb, function( v, i ) {
+			return ( 1 - a ) * blend[ i ] + a * v;
+		}));
+	},
+	toRgbaString: function() {
+		var prefix = "rgba(",
+			rgba = jQuery.map( this._rgba, function( v, i ) {
+				return v == null ? ( i > 2 ? 1 : 0 ) : v;
+			});
+
+		if ( rgba[ 3 ] === 1 ) {
+			rgba.pop();
+			prefix = "rgb(";
+		}
+
+		return prefix + rgba.join() + ")";
+	},
+	toHslaString: function() {
+		var prefix = "hsla(",
+			hsla = jQuery.map( this.hsla(), function( v, i ) {
+				if ( v == null ) {
+					v = i > 2 ? 1 : 0;
+				}
+
+				// catch 1 and 2
+				if ( i && i < 3 ) {
+					v = Math.round( v * 100 ) + "%";
+				}
+				return v;
+			});
+
+		if ( hsla[ 3 ] === 1 ) {
+			hsla.pop();
+			prefix = "hsl(";
+		}
+		return prefix + hsla.join() + ")";
+	},
+	toHexString: function( includeAlpha ) {
+		var rgba = this._rgba.slice(),
+			alpha = rgba.pop();
+
+		if ( includeAlpha ) {
+			rgba.push( ~~( alpha * 255 ) );
+		}
+
+		return "#" + jQuery.map( rgba, function( v ) {
+
+			// default to 0 when nulls exist
+			v = ( v || 0 ).toString( 16 );
+			return v.length === 1 ? "0" + v : v;
+		}).join("");
+	},
+	toString: function() {
+		return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString();
+	}
+});
+color.fn.parse.prototype = color.fn;
+
+// hsla conversions adapted from:
+// https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021
+
+function hue2rgb( p, q, h ) {
+	h = ( h + 1 ) % 1;
+	if ( h * 6 < 1 ) {
+		return p + ( q - p ) * h * 6;
+	}
+	if ( h * 2 < 1) {
+		return q;
+	}
+	if ( h * 3 < 2 ) {
+		return p + ( q - p ) * ( ( 2 / 3 ) - h ) * 6;
+	}
+	return p;
+}
+
+spaces.hsla.to = function( rgba ) {
+	if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) {
+		return [ null, null, null, rgba[ 3 ] ];
+	}
+	var r = rgba[ 0 ] / 255,
+		g = rgba[ 1 ] / 255,
+		b = rgba[ 2 ] / 255,
+		a = rgba[ 3 ],
+		max = Math.max( r, g, b ),
+		min = Math.min( r, g, b ),
+		diff = max - min,
+		add = max + min,
+		l = add * 0.5,
+		h, s;
+
+	if ( min === max ) {
+		h = 0;
+	} else if ( r === max ) {
+		h = ( 60 * ( g - b ) / diff ) + 360;
+	} else if ( g === max ) {
+		h = ( 60 * ( b - r ) / diff ) + 120;
+	} else {
+		h = ( 60 * ( r - g ) / diff ) + 240;
+	}
+
+	// chroma (diff) == 0 means greyscale which, by definition, saturation = 0%
+	// otherwise, saturation is based on the ratio of chroma (diff) to lightness (add)
+	if ( diff === 0 ) {
+		s = 0;
+	} else if ( l <= 0.5 ) {
+		s = diff / add;
+	} else {
+		s = diff / ( 2 - add );
+	}
+	return [ Math.round(h) % 360, s, l, a == null ? 1 : a ];
+};
+
+spaces.hsla.from = function( hsla ) {
+	if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) {
+		return [ null, null, null, hsla[ 3 ] ];
+	}
+	var h = hsla[ 0 ] / 360,
+		s = hsla[ 1 ],
+		l = hsla[ 2 ],
+		a = hsla[ 3 ],
+		q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s,
+		p = 2 * l - q;
+
+	return [
+		Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ),
+		Math.round( hue2rgb( p, q, h ) * 255 ),
+		Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ),
+		a
+	];
+};
+
+each( spaces, function( spaceName, space ) {
+	var props = space.props,
+		cache = space.cache,
+		to = space.to,
+		from = space.from;
+
+	// makes rgba() and hsla()
+	color.fn[ spaceName ] = function( value ) {
+
+		// generate a cache for this space if it doesn't exist
+		if ( to && !this[ cache ] ) {
+			this[ cache ] = to( this._rgba );
+		}
+		if ( value === undefined ) {
+			return this[ cache ].slice();
+		}
+
+		var ret,
+			type = jQuery.type( value ),
+			arr = ( type === "array" || type === "object" ) ? value : arguments,
+			local = this[ cache ].slice();
+
+		each( props, function( key, prop ) {
+			var val = arr[ type === "object" ? key : prop.idx ];
+			if ( val == null ) {
+				val = local[ prop.idx ];
+			}
+			local[ prop.idx ] = clamp( val, prop );
+		});
+
+		if ( from ) {
+			ret = color( from( local ) );
+			ret[ cache ] = local;
+			return ret;
+		} else {
+			return color( local );
+		}
+	};
+
+	// makes red() green() blue() alpha() hue() saturation() lightness()
+	each( props, function( key, prop ) {
+		// alpha is included in more than one space
+		if ( color.fn[ key ] ) {
+			return;
+		}
+		color.fn[ key ] = function( value ) {
+			var vtype = jQuery.type( value ),
+				fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ),
+				local = this[ fn ](),
+				cur = local[ prop.idx ],
+				match;
+
+			if ( vtype === "undefined" ) {
+				return cur;
+			}
+
+			if ( vtype === "function" ) {
+				value = value.call( this, cur );
+				vtype = jQuery.type( value );
+			}
+			if ( value == null && prop.empty ) {
+				return this;
+			}
+			if ( vtype === "string" ) {
+				match = rplusequals.exec( value );
+				if ( match ) {
+					value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 );
+				}
+			}
+			local[ prop.idx ] = value;
+			return this[ fn ]( local );
+		};
+	});
+});
+
+// add cssHook and .fx.step function for each named hook.
+// accept a space separated string of properties
+color.hook = function( hook ) {
+	var hooks = hook.split( " " );
+	each( hooks, function( i, hook ) {
+		jQuery.cssHooks[ hook ] = {
+			set: function( elem, value ) {
+				var parsed, curElem,
+					backgroundColor = "";
+
+				if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) {
+					value = color( parsed || value );
+					if ( !support.rgba && value._rgba[ 3 ] !== 1 ) {
+						curElem = hook === "backgroundColor" ? elem.parentNode : elem;
+						while (
+							(backgroundColor === "" || backgroundColor === "transparent") &&
+							curElem && curElem.style
+						) {
+							try {
+								backgroundColor = jQuery.css( curElem, "backgroundColor" );
+								curElem = curElem.parentNode;
+							} catch ( e ) {
+							}
+						}
+
+						value = value.blend( backgroundColor && backgroundColor !== "transparent" ?
+							backgroundColor :
+							"_default" );
+					}
+
+					value = value.toRgbaString();
+				}
+				try {
+					elem.style[ hook ] = value;
+				} catch ( e ) {
+					// wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit'
+				}
+			}
+		};
+		jQuery.fx.step[ hook ] = function( fx ) {
+			if ( !fx.colorInit ) {
+				fx.start = color( fx.elem, hook );
+				fx.end = color( fx.end );
+				fx.colorInit = true;
+			}
+			jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) );
+		};
+	});
+
+};
+
+color.hook( stepHooks );
+
+jQuery.cssHooks.borderColor = {
+	expand: function( value ) {
+		var expanded = {};
+
+		each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) {
+			expanded[ "border" + part + "Color" ] = value;
+		});
+		return expanded;
+	}
+};
+
+// Basic color names only.
+// Usage of any of the other color names requires adding yourself or including
+// jquery.color.svg-names.js.
+colors = jQuery.Color.names = {
+	// 4.1. Basic color keywords
+	aqua: "#00ffff",
+	black: "#000000",
+	blue: "#0000ff",
+	fuchsia: "#ff00ff",
+	gray: "#808080",
+	green: "#008000",
+	lime: "#00ff00",
+	maroon: "#800000",
+	navy: "#000080",
+	olive: "#808000",
+	purple: "#800080",
+	red: "#ff0000",
+	silver: "#c0c0c0",
+	teal: "#008080",
+	white: "#ffffff",
+	yellow: "#ffff00",
+
+	// 4.2.3. "transparent" color keyword
+	transparent: [ null, null, null, 0 ],
+
+	_default: "#ffffff"
+};
+
+})( jQuery );
+
+/******************************************************************************/
+/****************************** CLASS ANIMATIONS ******************************/
+/******************************************************************************/
+(function() {
+
+var classAnimationActions = [ "add", "remove", "toggle" ],
+	shorthandStyles = {
+		border: 1,
+		borderBottom: 1,
+		borderColor: 1,
+		borderLeft: 1,
+		borderRight: 1,
+		borderTop: 1,
+		borderWidth: 1,
+		margin: 1,
+		padding: 1
+	};
+
+$.each([ "borderLeftStyle", "borderRightStyle", "borderBottomStyle", "borderTopStyle" ], function( _, prop ) {
+	$.fx.step[ prop ] = function( fx ) {
+		if ( fx.end !== "none" && !fx.setAttr || fx.pos === 1 && !fx.setAttr ) {
+			jQuery.style( fx.elem, prop, fx.end );
+			fx.setAttr = true;
+		}
+	};
+});
+
+function getElementStyles( elem ) {
+	var key, len,
+		style = elem.ownerDocument.defaultView ?
+			elem.ownerDocument.defaultView.getComputedStyle( elem, null ) :
+			elem.currentStyle,
+		styles = {};
+
+	if ( style && style.length && style[ 0 ] && style[ style[ 0 ] ] ) {
+		len = style.length;
+		while ( len-- ) {
+			key = style[ len ];
+			if ( typeof style[ key ] === "string" ) {
+				styles[ $.camelCase( key ) ] = style[ key ];
+			}
+		}
+	// support: Opera, IE <9
+	} else {
+		for ( key in style ) {
+			if ( typeof style[ key ] === "string" ) {
+				styles[ key ] = style[ key ];
+			}
+		}
+	}
+
+	return styles;
+}
+
+function styleDifference( oldStyle, newStyle ) {
+	var diff = {},
+		name, value;
+
+	for ( name in newStyle ) {
+		value = newStyle[ name ];
+		if ( oldStyle[ name ] !== value ) {
+			if ( !shorthandStyles[ name ] ) {
+				if ( $.fx.step[ name ] || !isNaN( parseFloat( value ) ) ) {
+					diff[ name ] = value;
+				}
+			}
+		}
+	}
+
+	return diff;
+}
+
+// support: jQuery <1.8
+if ( !$.fn.addBack ) {
+	$.fn.addBack = function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter( selector )
+		);
+	};
+}
+
+$.effects.animateClass = function( value, duration, easing, callback ) {
+	var o = $.speed( duration, easing, callback );
+
+	return this.queue( function() {
+		var animated = $( this ),
+			baseClass = animated.attr( "class" ) || "",
+			applyClassChange,
+			allAnimations = o.children ? animated.find( "*" ).addBack() : animated;
+
+		// map the animated objects to store the original styles.
+		allAnimations = allAnimations.map(function() {
+			var el = $( this );
+			return {
+				el: el,
+				start: getElementStyles( this )
+			};
+		});
+
+		// apply class change
+		applyClassChange = function() {
+			$.each( classAnimationActions, function(i, action) {
+				if ( value[ action ] ) {
+					animated[ action + "Class" ]( value[ action ] );
+				}
+			});
+		};
+		applyClassChange();
+
+		// map all animated objects again - calculate new styles and diff
+		allAnimations = allAnimations.map(function() {
+			this.end = getElementStyles( this.el[ 0 ] );
+			this.diff = styleDifference( this.start, this.end );
+			return this;
+		});
+
+		// apply original class
+		animated.attr( "class", baseClass );
+
+		// map all animated objects again - this time collecting a promise
+		allAnimations = allAnimations.map(function() {
+			var styleInfo = this,
+				dfd = $.Deferred(),
+				opts = $.extend({}, o, {
+					queue: false,
+					complete: function() {
+						dfd.resolve( styleInfo );
+					}
+				});
+
+			this.el.animate( this.diff, opts );
+			return dfd.promise();
+		});
+
+		// once all animations have completed:
+		$.when.apply( $, allAnimations.get() ).done(function() {
+
+			// set the final class
+			applyClassChange();
+
+			// for each animated element,
+			// clear all css properties that were animated
+			$.each( arguments, function() {
+				var el = this.el;
+				$.each( this.diff, function(key) {
+					el.css( key, "" );
+				});
+			});
+
+			// this is guarnteed to be there if you use jQuery.speed()
+			// it also handles dequeuing the next anim...
+			o.complete.call( animated[ 0 ] );
+		});
+	});
+};
+
+$.fn.extend({
+	addClass: (function( orig ) {
+		return function( classNames, speed, easing, callback ) {
+			return speed ?
+				$.effects.animateClass.call( this,
+					{ add: classNames }, speed, easing, callback ) :
+				orig.apply( this, arguments );
+		};
+	})( $.fn.addClass ),
+
+	removeClass: (function( orig ) {
+		return function( classNames, speed, easing, callback ) {
+			return arguments.length > 1 ?
+				$.effects.animateClass.call( this,
+					{ remove: classNames }, speed, easing, callback ) :
+				orig.apply( this, arguments );
+		};
+	})( $.fn.removeClass ),
+
+	toggleClass: (function( orig ) {
+		return function( classNames, force, speed, easing, callback ) {
+			if ( typeof force === "boolean" || force === undefined ) {
+				if ( !speed ) {
+					// without speed parameter
+					return orig.apply( this, arguments );
+				} else {
+					return $.effects.animateClass.call( this,
+						(force ? { add: classNames } : { remove: classNames }),
+						speed, easing, callback );
+				}
+			} else {
+				// without force parameter
+				return $.effects.animateClass.call( this,
+					{ toggle: classNames }, force, speed, easing );
+			}
+		};
+	})( $.fn.toggleClass ),
+
+	switchClass: function( remove, add, speed, easing, callback) {
+		return $.effects.animateClass.call( this, {
+			add: add,
+			remove: remove
+		}, speed, easing, callback );
+	}
+});
+
+})();
+
+/******************************************************************************/
+/*********************************** EFFECTS **********************************/
+/******************************************************************************/
+
+(function() {
+
+$.extend( $.effects, {
+	version: "1.11.2",
+
+	// Saves a set of properties in a data storage
+	save: function( element, set ) {
+		for ( var i = 0; i < set.length; i++ ) {
+			if ( set[ i ] !== null ) {
+				element.data( dataSpace + set[ i ], element[ 0 ].style[ set[ i ] ] );
+			}
+		}
+	},
+
+	// Restores a set of previously saved properties from a data storage
+	restore: function( element, set ) {
+		var val, i;
+		for ( i = 0; i < set.length; i++ ) {
+			if ( set[ i ] !== null ) {
+				val = element.data( dataSpace + set[ i ] );
+				// support: jQuery 1.6.2
+				// http://bugs.jquery.com/ticket/9917
+				// jQuery 1.6.2 incorrectly returns undefined for any falsy value.
+				// We can't differentiate between "" and 0 here, so we just assume
+				// empty string since it's likely to be a more common value...
+				if ( val === undefined ) {
+					val = "";
+				}
+				element.css( set[ i ], val );
+			}
+		}
+	},
+
+	setMode: function( el, mode ) {
+		if (mode === "toggle") {
+			mode = el.is( ":hidden" ) ? "show" : "hide";
+		}
+		return mode;
+	},
+
+	// Translates a [top,left] array into a baseline value
+	// this should be a little more flexible in the future to handle a string & hash
+	getBaseline: function( origin, original ) {
+		var y, x;
+		switch ( origin[ 0 ] ) {
+			case "top": y = 0; break;
+			case "middle": y = 0.5; break;
+			case "bottom": y = 1; break;
+			default: y = origin[ 0 ] / original.height;
+		}
+		switch ( origin[ 1 ] ) {
+			case "left": x = 0; break;
+			case "center": x = 0.5; break;
+			case "right": x = 1; break;
+			default: x = origin[ 1 ] / original.width;
+		}
+		return {
+			x: x,
+			y: y
+		};
+	},
+
+	// Wraps the element around a wrapper that copies position properties
+	createWrapper: function( element ) {
+
+		// if the element is already wrapped, return it
+		if ( element.parent().is( ".ui-effects-wrapper" )) {
+			return element.parent();
+		}
+
+		// wrap the element
+		var props = {
+				width: element.outerWidth(true),
+				height: element.outerHeight(true),
+				"float": element.css( "float" )
+			},
+			wrapper = $( "<div></div>" )
+				.addClass( "ui-effects-wrapper" )
+				.css({
+					fontSize: "100%",
+					background: "transparent",
+					border: "none",
+					margin: 0,
+					padding: 0
+				}),
+			// Store the size in case width/height are defined in % - Fixes #5245
+			size = {
+				width: element.width(),
+				height: element.height()
+			},
+			active = document.activeElement;
+
+		// support: Firefox
+		// Firefox incorrectly exposes anonymous content
+		// https://bugzilla.mozilla.org/show_bug.cgi?id=561664
+		try {
+			active.id;
+		} catch ( e ) {
+			active = document.body;
+		}
+
+		element.wrap( wrapper );
+
+		// Fixes #7595 - Elements lose focus when wrapped.
+		if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+			$( active ).focus();
+		}
+
+		wrapper = element.parent(); //Hotfix for jQuery 1.4 since some change in wrap() seems to actually lose the reference to the wrapped element
+
+		// transfer positioning properties to the wrapper
+		if ( element.css( "position" ) === "static" ) {
+			wrapper.css({ position: "relative" });
+			element.css({ position: "relative" });
+		} else {
+			$.extend( props, {
+				position: element.css( "position" ),
+				zIndex: element.css( "z-index" )
+			});
+			$.each([ "top", "left", "bottom", "right" ], function(i, pos) {
+				props[ pos ] = element.css( pos );
+				if ( isNaN( parseInt( props[ pos ], 10 ) ) ) {
+					props[ pos ] = "auto";
+				}
+			});
+			element.css({
+				position: "relative",
+				top: 0,
+				left: 0,
+				right: "auto",
+				bottom: "auto"
+			});
+		}
+		element.css(size);
+
+		return wrapper.css( props ).show();
+	},
+
+	removeWrapper: function( element ) {
+		var active = document.activeElement;
+
+		if ( element.parent().is( ".ui-effects-wrapper" ) ) {
+			element.parent().replaceWith( element );
+
+			// Fixes #7595 - Elements lose focus when wrapped.
+			if ( element[ 0 ] === active || $.contains( element[ 0 ], active ) ) {
+				$( active ).focus();
+			}
+		}
+
+		return element;
+	},
+
+	setTransition: function( element, list, factor, value ) {
+		value = value || {};
+		$.each( list, function( i, x ) {
+			var unit = element.cssUnit( x );
+			if ( unit[ 0 ] > 0 ) {
+				value[ x ] = unit[ 0 ] * factor + unit[ 1 ];
+			}
+		});
+		return value;
+	}
+});
+
+// return an effect options object for the given parameters:
+function _normalizeArguments( effect, options, speed, callback ) {
+
+	// allow passing all options as the first parameter
+	if ( $.isPlainObject( effect ) ) {
+		options = effect;
+		effect = effect.effect;
+	}
+
+	// convert to an object
+	effect = { effect: effect };
+
+	// catch (effect, null, ...)
+	if ( options == null ) {
+		options = {};
+	}
+
+	// catch (effect, callback)
+	if ( $.isFunction( options ) ) {
+		callback = options;
+		speed = null;
+		options = {};
+	}
+
+	// catch (effect, speed, ?)
+	if ( typeof options === "number" || $.fx.speeds[ options ] ) {
+		callback = speed;
+		speed = options;
+		options = {};
+	}
+
+	// catch (effect, options, callback)
+	if ( $.isFunction( speed ) ) {
+		callback = speed;
+		speed = null;
+	}
+
+	// add options to effect
+	if ( options ) {
+		$.extend( effect, options );
+	}
+
+	speed = speed || options.duration;
+	effect.duration = $.fx.off ? 0 :
+		typeof speed === "number" ? speed :
+		speed in $.fx.speeds ? $.fx.speeds[ speed ] :
+		$.fx.speeds._default;
+
+	effect.complete = callback || options.complete;
+
+	return effect;
+}
+
+function standardAnimationOption( option ) {
+	// Valid standard speeds (nothing, number, named speed)
+	if ( !option || typeof option === "number" || $.fx.speeds[ option ] ) {
+		return true;
+	}
+
+	// Invalid strings - treat as "normal" speed
+	if ( typeof option === "string" && !$.effects.effect[ option ] ) {
+		return true;
+	}
+
+	// Complete callback
+	if ( $.isFunction( option ) ) {
+		return true;
+	}
+
+	// Options hash (but not naming an effect)
+	if ( typeof option === "object" && !option.effect ) {
+		return true;
+	}
+
+	// Didn't match any standard API
+	return false;
+}
+
+$.fn.extend({
+	effect: function( /* effect, options, speed, callback */ ) {
+		var args = _normalizeArguments.apply( this, arguments ),
+			mode = args.mode,
+			queue = args.queue,
+			effectMethod = $.effects.effect[ args.effect ];
+
+		if ( $.fx.off || !effectMethod ) {
+			// delegate to the original method (e.g., .show()) if possible
+			if ( mode ) {
+				return this[ mode ]( args.duration, args.complete );
+			} else {
+				return this.each( function() {
+					if ( args.complete ) {
+						args.complete.call( this );
+					}
+				});
+			}
+		}
+
+		function run( next ) {
+			var elem = $( this ),
+				complete = args.complete,
+				mode = args.mode;
+
+			function done() {
+				if ( $.isFunction( complete ) ) {
+					complete.call( elem[0] );
+				}
+				if ( $.isFunction( next ) ) {
+					next();
+				}
+			}
+
+			// If the element already has the correct final state, delegate to
+			// the core methods so the internal tracking of "olddisplay" works.
+			if ( elem.is( ":hidden" ) ? mode === "hide" : mode === "show" ) {
+				elem[ mode ]();
+				done();
+			} else {
+				effectMethod.call( elem[0], args, done );
+			}
+		}
+
+		return queue === false ? this.each( run ) : this.queue( queue || "fx", run );
+	},
+
+	show: (function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "show";
+				return this.effect.call( this, args );
+			}
+		};
+	})( $.fn.show ),
+
+	hide: (function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "hide";
+				return this.effect.call( this, args );
+			}
+		};
+	})( $.fn.hide ),
+
+	toggle: (function( orig ) {
+		return function( option ) {
+			if ( standardAnimationOption( option ) || typeof option === "boolean" ) {
+				return orig.apply( this, arguments );
+			} else {
+				var args = _normalizeArguments.apply( this, arguments );
+				args.mode = "toggle";
+				return this.effect.call( this, args );
+			}
+		};
+	})( $.fn.toggle ),
+
+	// helper functions
+	cssUnit: function(key) {
+		var style = this.css( key ),
+			val = [];
+
+		$.each( [ "em", "px", "%", "pt" ], function( i, unit ) {
+			if ( style.indexOf( unit ) > 0 ) {
+				val = [ parseFloat( style ), unit ];
+			}
+		});
+		return val;
+	}
+});
+
+})();
+
+/******************************************************************************/
+/*********************************** EASING ***********************************/
+/******************************************************************************/
+
+(function() {
+
+// based on easing equations from Robert Penner (http://www.robertpenner.com/easing)
+
+var baseEasings = {};
+
+$.each( [ "Quad", "Cubic", "Quart", "Quint", "Expo" ], function( i, name ) {
+	baseEasings[ name ] = function( p ) {
+		return Math.pow( p, i + 2 );
+	};
+});
+
+$.extend( baseEasings, {
+	Sine: function( p ) {
+		return 1 - Math.cos( p * Math.PI / 2 );
+	},
+	Circ: function( p ) {
+		return 1 - Math.sqrt( 1 - p * p );
+	},
+	Elastic: function( p ) {
+		return p === 0 || p === 1 ? p :
+			-Math.pow( 2, 8 * (p - 1) ) * Math.sin( ( (p - 1) * 80 - 7.5 ) * Math.PI / 15 );
+	},
+	Back: function( p ) {
+		return p * p * ( 3 * p - 2 );
+	},
+	Bounce: function( p ) {
+		var pow2,
+			bounce = 4;
+
+		while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
+		return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
+	}
+});
+
+$.each( baseEasings, function( name, easeIn ) {
+	$.easing[ "easeIn" + name ] = easeIn;
+	$.easing[ "easeOut" + name ] = function( p ) {
+		return 1 - easeIn( 1 - p );
+	};
+	$.easing[ "easeInOut" + name ] = function( p ) {
+		return p < 0.5 ?
+			easeIn( p * 2 ) / 2 :
+			1 - easeIn( p * -2 + 2 ) / 2;
+	};
+});
+
+})();
+
+var effect = $.effects;
+
+
+/*!
+ * jQuery UI Effects Blind 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/blind-effect/
+ */
+
+
+var effectBlind = $.effects.effect.blind = function( o, done ) {
+	// Create element
+	var el = $( this ),
+		rvertical = /up|down|vertical/,
+		rpositivemotion = /up|left|vertical|horizontal/,
+		props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+		mode = $.effects.setMode( el, o.mode || "hide" ),
+		direction = o.direction || "up",
+		vertical = rvertical.test( direction ),
+		ref = vertical ? "height" : "width",
+		ref2 = vertical ? "top" : "left",
+		motion = rpositivemotion.test( direction ),
+		animation = {},
+		show = mode === "show",
+		wrapper, distance, margin;
+
+	// if already wrapped, the wrapper's properties are my property. #6245
+	if ( el.parent().is( ".ui-effects-wrapper" ) ) {
+		$.effects.save( el.parent(), props );
+	} else {
+		$.effects.save( el, props );
+	}
+	el.show();
+	wrapper = $.effects.createWrapper( el ).css({
+		overflow: "hidden"
+	});
+
+	distance = wrapper[ ref ]();
+	margin = parseFloat( wrapper.css( ref2 ) ) || 0;
+
+	animation[ ref ] = show ? distance : 0;
+	if ( !motion ) {
+		el
+			.css( vertical ? "bottom" : "right", 0 )
+			.css( vertical ? "top" : "left", "auto" )
+			.css({ position: "absolute" });
+
+		animation[ ref2 ] = show ? margin : distance + margin;
+	}
+
+	// start at 0 if we are showing
+	if ( show ) {
+		wrapper.css( ref, 0 );
+		if ( !motion ) {
+			wrapper.css( ref2, margin + distance );
+		}
+	}
+
+	// Animate
+	wrapper.animate( animation, {
+		duration: o.duration,
+		easing: o.easing,
+		queue: false,
+		complete: function() {
+			if ( mode === "hide" ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		}
+	});
+};
+
+
+/*!
+ * jQuery UI Effects Bounce 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/bounce-effect/
+ */
+
+
+var effectBounce = $.effects.effect.bounce = function( o, done ) {
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+
+		// defaults:
+		mode = $.effects.setMode( el, o.mode || "effect" ),
+		hide = mode === "hide",
+		show = mode === "show",
+		direction = o.direction || "up",
+		distance = o.distance,
+		times = o.times || 5,
+
+		// number of internal animations
+		anims = times * 2 + ( show || hide ? 1 : 0 ),
+		speed = o.duration / anims,
+		easing = o.easing,
+
+		// utility:
+		ref = ( direction === "up" || direction === "down" ) ? "top" : "left",
+		motion = ( direction === "up" || direction === "left" ),
+		i,
+		upAnim,
+		downAnim,
+
+		// we will need to re-assemble the queue to stack our animations in place
+		queue = el.queue(),
+		queuelen = queue.length;
+
+	// Avoid touching opacity to prevent clearType and PNG issues in IE
+	if ( show || hide ) {
+		props.push( "opacity" );
+	}
+
+	$.effects.save( el, props );
+	el.show();
+	$.effects.createWrapper( el ); // Create Wrapper
+
+	// default distance for the BIGGEST bounce is the outer Distance / 3
+	if ( !distance ) {
+		distance = el[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3;
+	}
+
+	if ( show ) {
+		downAnim = { opacity: 1 };
+		downAnim[ ref ] = 0;
+
+		// if we are showing, force opacity 0 and set the initial position
+		// then do the "first" animation
+		el.css( "opacity", 0 )
+			.css( ref, motion ? -distance * 2 : distance * 2 )
+			.animate( downAnim, speed, easing );
+	}
+
+	// start at the smallest distance if we are hiding
+	if ( hide ) {
+		distance = distance / Math.pow( 2, times - 1 );
+	}
+
+	downAnim = {};
+	downAnim[ ref ] = 0;
+	// Bounces up/down/left/right then back to 0 -- times * 2 animations happen here
+	for ( i = 0; i < times; i++ ) {
+		upAnim = {};
+		upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance;
+
+		el.animate( upAnim, speed, easing )
+			.animate( downAnim, speed, easing );
+
+		distance = hide ? distance * 2 : distance / 2;
+	}
+
+	// Last Bounce when Hiding
+	if ( hide ) {
+		upAnim = { opacity: 0 };
+		upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance;
+
+		el.animate( upAnim, speed, easing );
+	}
+
+	el.queue(function() {
+		if ( hide ) {
+			el.hide();
+		}
+		$.effects.restore( el, props );
+		$.effects.removeWrapper( el );
+		done();
+	});
+
+	// inject all the animations we just queued to be first in line (after "inprogress")
+	if ( queuelen > 1) {
+		queue.splice.apply( queue,
+			[ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
+	}
+	el.dequeue();
+
+};
+
+
+/*!
+ * jQuery UI Effects Clip 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/clip-effect/
+ */
+
+
+var effectClip = $.effects.effect.clip = function( o, done ) {
+	// Create element
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+		mode = $.effects.setMode( el, o.mode || "hide" ),
+		show = mode === "show",
+		direction = o.direction || "vertical",
+		vert = direction === "vertical",
+		size = vert ? "height" : "width",
+		position = vert ? "top" : "left",
+		animation = {},
+		wrapper, animate, distance;
+
+	// Save & Show
+	$.effects.save( el, props );
+	el.show();
+
+	// Create Wrapper
+	wrapper = $.effects.createWrapper( el ).css({
+		overflow: "hidden"
+	});
+	animate = ( el[0].tagName === "IMG" ) ? wrapper : el;
+	distance = animate[ size ]();
+
+	// Shift
+	if ( show ) {
+		animate.css( size, 0 );
+		animate.css( position, distance / 2 );
+	}
+
+	// Create Animation Object:
+	animation[ size ] = show ? distance : 0;
+	animation[ position ] = show ? 0 : distance / 2;
+
+	// Animate
+	animate.animate( animation, {
+		queue: false,
+		duration: o.duration,
+		easing: o.easing,
+		complete: function() {
+			if ( !show ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		}
+	});
+
+};
+
+
+/*!
+ * jQuery UI Effects Drop 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/drop-effect/
+ */
+
+
+var effectDrop = $.effects.effect.drop = function( o, done ) {
+
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "opacity", "height", "width" ],
+		mode = $.effects.setMode( el, o.mode || "hide" ),
+		show = mode === "show",
+		direction = o.direction || "left",
+		ref = ( direction === "up" || direction === "down" ) ? "top" : "left",
+		motion = ( direction === "up" || direction === "left" ) ? "pos" : "neg",
+		animation = {
+			opacity: show ? 1 : 0
+		},
+		distance;
+
+	// Adjust
+	$.effects.save( el, props );
+	el.show();
+	$.effects.createWrapper( el );
+
+	distance = o.distance || el[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ) / 2;
+
+	if ( show ) {
+		el
+			.css( "opacity", 0 )
+			.css( ref, motion === "pos" ? -distance : distance );
+	}
+
+	// Animation
+	animation[ ref ] = ( show ?
+		( motion === "pos" ? "+=" : "-=" ) :
+		( motion === "pos" ? "-=" : "+=" ) ) +
+		distance;
+
+	// Animate
+	el.animate( animation, {
+		queue: false,
+		duration: o.duration,
+		easing: o.easing,
+		complete: function() {
+			if ( mode === "hide" ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		}
+	});
+};
+
+
+/*!
+ * jQuery UI Effects Explode 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/explode-effect/
+ */
+
+
+var effectExplode = $.effects.effect.explode = function( o, done ) {
+
+	var rows = o.pieces ? Math.round( Math.sqrt( o.pieces ) ) : 3,
+		cells = rows,
+		el = $( this ),
+		mode = $.effects.setMode( el, o.mode || "hide" ),
+		show = mode === "show",
+
+		// show and then visibility:hidden the element before calculating offset
+		offset = el.show().css( "visibility", "hidden" ).offset(),
+
+		// width and height of a piece
+		width = Math.ceil( el.outerWidth() / cells ),
+		height = Math.ceil( el.outerHeight() / rows ),
+		pieces = [],
+
+		// loop
+		i, j, left, top, mx, my;
+
+	// children animate complete:
+	function childComplete() {
+		pieces.push( this );
+		if ( pieces.length === rows * cells ) {
+			animComplete();
+		}
+	}
+
+	// clone the element for each row and cell.
+	for ( i = 0; i < rows ; i++ ) { // ===>
+		top = offset.top + i * height;
+		my = i - ( rows - 1 ) / 2 ;
+
+		for ( j = 0; j < cells ; j++ ) { // |||
+			left = offset.left + j * width;
+			mx = j - ( cells - 1 ) / 2 ;
+
+			// Create a clone of the now hidden main element that will be absolute positioned
+			// within a wrapper div off the -left and -top equal to size of our pieces
+			el
+				.clone()
+				.appendTo( "body" )
+				.wrap( "<div></div>" )
+				.css({
+					position: "absolute",
+					visibility: "visible",
+					left: -j * width,
+					top: -i * height
+				})
+
+			// select the wrapper - make it overflow: hidden and absolute positioned based on
+			// where the original was located +left and +top equal to the size of pieces
+				.parent()
+				.addClass( "ui-effects-explode" )
+				.css({
+					position: "absolute",
+					overflow: "hidden",
+					width: width,
+					height: height,
+					left: left + ( show ? mx * width : 0 ),
+					top: top + ( show ? my * height : 0 ),
+					opacity: show ? 0 : 1
+				}).animate({
+					left: left + ( show ? 0 : mx * width ),
+					top: top + ( show ? 0 : my * height ),
+					opacity: show ? 1 : 0
+				}, o.duration || 500, o.easing, childComplete );
+		}
+	}
+
+	function animComplete() {
+		el.css({
+			visibility: "visible"
+		});
+		$( pieces ).remove();
+		if ( !show ) {
+			el.hide();
+		}
+		done();
+	}
+};
+
+
+/*!
+ * jQuery UI Effects Fade 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/fade-effect/
+ */
+
+
+var effectFade = $.effects.effect.fade = function( o, done ) {
+	var el = $( this ),
+		mode = $.effects.setMode( el, o.mode || "toggle" );
+
+	el.animate({
+		opacity: mode
+	}, {
+		queue: false,
+		duration: o.duration,
+		easing: o.easing,
+		complete: done
+	});
+};
+
+
+/*!
+ * jQuery UI Effects Fold 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/fold-effect/
+ */
+
+
+var effectFold = $.effects.effect.fold = function( o, done ) {
+
+	// Create element
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+		mode = $.effects.setMode( el, o.mode || "hide" ),
+		show = mode === "show",
+		hide = mode === "hide",
+		size = o.size || 15,
+		percent = /([0-9]+)%/.exec( size ),
+		horizFirst = !!o.horizFirst,
+		widthFirst = show !== horizFirst,
+		ref = widthFirst ? [ "width", "height" ] : [ "height", "width" ],
+		duration = o.duration / 2,
+		wrapper, distance,
+		animation1 = {},
+		animation2 = {};
+
+	$.effects.save( el, props );
+	el.show();
+
+	// Create Wrapper
+	wrapper = $.effects.createWrapper( el ).css({
+		overflow: "hidden"
+	});
+	distance = widthFirst ?
+		[ wrapper.width(), wrapper.height() ] :
+		[ wrapper.height(), wrapper.width() ];
+
+	if ( percent ) {
+		size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ];
+	}
+	if ( show ) {
+		wrapper.css( horizFirst ? {
+			height: 0,
+			width: size
+		} : {
+			height: size,
+			width: 0
+		});
+	}
+
+	// Animation
+	animation1[ ref[ 0 ] ] = show ? distance[ 0 ] : size;
+	animation2[ ref[ 1 ] ] = show ? distance[ 1 ] : 0;
+
+	// Animate
+	wrapper
+		.animate( animation1, duration, o.easing )
+		.animate( animation2, duration, o.easing, function() {
+			if ( hide ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		});
+
+};
+
+
+/*!
+ * jQuery UI Effects Highlight 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/highlight-effect/
+ */
+
+
+var effectHighlight = $.effects.effect.highlight = function( o, done ) {
+	var elem = $( this ),
+		props = [ "backgroundImage", "backgroundColor", "opacity" ],
+		mode = $.effects.setMode( elem, o.mode || "show" ),
+		animation = {
+			backgroundColor: elem.css( "backgroundColor" )
+		};
+
+	if (mode === "hide") {
+		animation.opacity = 0;
+	}
+
+	$.effects.save( elem, props );
+
+	elem
+		.show()
+		.css({
+			backgroundImage: "none",
+			backgroundColor: o.color || "#ffff99"
+		})
+		.animate( animation, {
+			queue: false,
+			duration: o.duration,
+			easing: o.easing,
+			complete: function() {
+				if ( mode === "hide" ) {
+					elem.hide();
+				}
+				$.effects.restore( elem, props );
+				done();
+			}
+		});
+};
+
+
+/*!
+ * jQuery UI Effects Size 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/size-effect/
+ */
+
+
+var effectSize = $.effects.effect.size = function( o, done ) {
+
+	// Create element
+	var original, baseline, factor,
+		el = $( this ),
+		props0 = [ "position", "top", "bottom", "left", "right", "width", "height", "overflow", "opacity" ],
+
+		// Always restore
+		props1 = [ "position", "top", "bottom", "left", "right", "overflow", "opacity" ],
+
+		// Copy for children
+		props2 = [ "width", "height", "overflow" ],
+		cProps = [ "fontSize" ],
+		vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ],
+		hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ],
+
+		// Set options
+		mode = $.effects.setMode( el, o.mode || "effect" ),
+		restore = o.restore || mode !== "effect",
+		scale = o.scale || "both",
+		origin = o.origin || [ "middle", "center" ],
+		position = el.css( "position" ),
+		props = restore ? props0 : props1,
+		zero = {
+			height: 0,
+			width: 0,
+			outerHeight: 0,
+			outerWidth: 0
+		};
+
+	if ( mode === "show" ) {
+		el.show();
+	}
+	original = {
+		height: el.height(),
+		width: el.width(),
+		outerHeight: el.outerHeight(),
+		outerWidth: el.outerWidth()
+	};
+
+	if ( o.mode === "toggle" && mode === "show" ) {
+		el.from = o.to || zero;
+		el.to = o.from || original;
+	} else {
+		el.from = o.from || ( mode === "show" ? zero : original );
+		el.to = o.to || ( mode === "hide" ? zero : original );
+	}
+
+	// Set scaling factor
+	factor = {
+		from: {
+			y: el.from.height / original.height,
+			x: el.from.width / original.width
+		},
+		to: {
+			y: el.to.height / original.height,
+			x: el.to.width / original.width
+		}
+	};
+
+	// Scale the css box
+	if ( scale === "box" || scale === "both" ) {
+
+		// Vertical props scaling
+		if ( factor.from.y !== factor.to.y ) {
+			props = props.concat( vProps );
+			el.from = $.effects.setTransition( el, vProps, factor.from.y, el.from );
+			el.to = $.effects.setTransition( el, vProps, factor.to.y, el.to );
+		}
+
+		// Horizontal props scaling
+		if ( factor.from.x !== factor.to.x ) {
+			props = props.concat( hProps );
+			el.from = $.effects.setTransition( el, hProps, factor.from.x, el.from );
+			el.to = $.effects.setTransition( el, hProps, factor.to.x, el.to );
+		}
+	}
+
+	// Scale the content
+	if ( scale === "content" || scale === "both" ) {
+
+		// Vertical props scaling
+		if ( factor.from.y !== factor.to.y ) {
+			props = props.concat( cProps ).concat( props2 );
+			el.from = $.effects.setTransition( el, cProps, factor.from.y, el.from );
+			el.to = $.effects.setTransition( el, cProps, factor.to.y, el.to );
+		}
+	}
+
+	$.effects.save( el, props );
+	el.show();
+	$.effects.createWrapper( el );
+	el.css( "overflow", "hidden" ).css( el.from );
+
+	// Adjust
+	if (origin) { // Calculate baseline shifts
+		baseline = $.effects.getBaseline( origin, original );
+		el.from.top = ( original.outerHeight - el.outerHeight() ) * baseline.y;
+		el.from.left = ( original.outerWidth - el.outerWidth() ) * baseline.x;
+		el.to.top = ( original.outerHeight - el.to.outerHeight ) * baseline.y;
+		el.to.left = ( original.outerWidth - el.to.outerWidth ) * baseline.x;
+	}
+	el.css( el.from ); // set top & left
+
+	// Animate
+	if ( scale === "content" || scale === "both" ) { // Scale the children
+
+		// Add margins/font-size
+		vProps = vProps.concat([ "marginTop", "marginBottom" ]).concat(cProps);
+		hProps = hProps.concat([ "marginLeft", "marginRight" ]);
+		props2 = props0.concat(vProps).concat(hProps);
+
+		el.find( "*[width]" ).each( function() {
+			var child = $( this ),
+				c_original = {
+					height: child.height(),
+					width: child.width(),
+					outerHeight: child.outerHeight(),
+					outerWidth: child.outerWidth()
+				};
+			if (restore) {
+				$.effects.save(child, props2);
+			}
+
+			child.from = {
+				height: c_original.height * factor.from.y,
+				width: c_original.width * factor.from.x,
+				outerHeight: c_original.outerHeight * factor.from.y,
+				outerWidth: c_original.outerWidth * factor.from.x
+			};
+			child.to = {
+				height: c_original.height * factor.to.y,
+				width: c_original.width * factor.to.x,
+				outerHeight: c_original.height * factor.to.y,
+				outerWidth: c_original.width * factor.to.x
+			};
+
+			// Vertical props scaling
+			if ( factor.from.y !== factor.to.y ) {
+				child.from = $.effects.setTransition( child, vProps, factor.from.y, child.from );
+				child.to = $.effects.setTransition( child, vProps, factor.to.y, child.to );
+			}
+
+			// Horizontal props scaling
+			if ( factor.from.x !== factor.to.x ) {
+				child.from = $.effects.setTransition( child, hProps, factor.from.x, child.from );
+				child.to = $.effects.setTransition( child, hProps, factor.to.x, child.to );
+			}
+
+			// Animate children
+			child.css( child.from );
+			child.animate( child.to, o.duration, o.easing, function() {
+
+				// Restore children
+				if ( restore ) {
+					$.effects.restore( child, props2 );
+				}
+			});
+		});
+	}
+
+	// Animate
+	el.animate( el.to, {
+		queue: false,
+		duration: o.duration,
+		easing: o.easing,
+		complete: function() {
+			if ( el.to.opacity === 0 ) {
+				el.css( "opacity", el.from.opacity );
+			}
+			if ( mode === "hide" ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			if ( !restore ) {
+
+				// we need to calculate our new positioning based on the scaling
+				if ( position === "static" ) {
+					el.css({
+						position: "relative",
+						top: el.to.top,
+						left: el.to.left
+					});
+				} else {
+					$.each([ "top", "left" ], function( idx, pos ) {
+						el.css( pos, function( _, str ) {
+							var val = parseInt( str, 10 ),
+								toRef = idx ? el.to.left : el.to.top;
+
+							// if original was "auto", recalculate the new value from wrapper
+							if ( str === "auto" ) {
+								return toRef + "px";
+							}
+
+							return val + toRef + "px";
+						});
+					});
+				}
+			}
+
+			$.effects.removeWrapper( el );
+			done();
+		}
+	});
+
+};
+
+
+/*!
+ * jQuery UI Effects Scale 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/scale-effect/
+ */
+
+
+var effectScale = $.effects.effect.scale = function( o, done ) {
+
+	// Create element
+	var el = $( this ),
+		options = $.extend( true, {}, o ),
+		mode = $.effects.setMode( el, o.mode || "effect" ),
+		percent = parseInt( o.percent, 10 ) ||
+			( parseInt( o.percent, 10 ) === 0 ? 0 : ( mode === "hide" ? 0 : 100 ) ),
+		direction = o.direction || "both",
+		origin = o.origin,
+		original = {
+			height: el.height(),
+			width: el.width(),
+			outerHeight: el.outerHeight(),
+			outerWidth: el.outerWidth()
+		},
+		factor = {
+			y: direction !== "horizontal" ? (percent / 100) : 1,
+			x: direction !== "vertical" ? (percent / 100) : 1
+		};
+
+	// We are going to pass this effect to the size effect:
+	options.effect = "size";
+	options.queue = false;
+	options.complete = done;
+
+	// Set default origin and restore for show/hide
+	if ( mode !== "effect" ) {
+		options.origin = origin || [ "middle", "center" ];
+		options.restore = true;
+	}
+
+	options.from = o.from || ( mode === "show" ? {
+		height: 0,
+		width: 0,
+		outerHeight: 0,
+		outerWidth: 0
+	} : original );
+	options.to = {
+		height: original.height * factor.y,
+		width: original.width * factor.x,
+		outerHeight: original.outerHeight * factor.y,
+		outerWidth: original.outerWidth * factor.x
+	};
+
+	// Fade option to support puff
+	if ( options.fade ) {
+		if ( mode === "show" ) {
+			options.from.opacity = 0;
+			options.to.opacity = 1;
+		}
+		if ( mode === "hide" ) {
+			options.from.opacity = 1;
+			options.to.opacity = 0;
+		}
+	}
+
+	// Animate
+	el.effect( options );
+
+};
+
+
+/*!
+ * jQuery UI Effects Puff 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/puff-effect/
+ */
+
+
+var effectPuff = $.effects.effect.puff = function( o, done ) {
+	var elem = $( this ),
+		mode = $.effects.setMode( elem, o.mode || "hide" ),
+		hide = mode === "hide",
+		percent = parseInt( o.percent, 10 ) || 150,
+		factor = percent / 100,
+		original = {
+			height: elem.height(),
+			width: elem.width(),
+			outerHeight: elem.outerHeight(),
+			outerWidth: elem.outerWidth()
+		};
+
+	$.extend( o, {
+		effect: "scale",
+		queue: false,
+		fade: true,
+		mode: mode,
+		complete: done,
+		percent: hide ? percent : 100,
+		from: hide ?
+			original :
+			{
+				height: original.height * factor,
+				width: original.width * factor,
+				outerHeight: original.outerHeight * factor,
+				outerWidth: original.outerWidth * factor
+			}
+	});
+
+	elem.effect( o );
+};
+
+
+/*!
+ * jQuery UI Effects Pulsate 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/pulsate-effect/
+ */
+
+
+var effectPulsate = $.effects.effect.pulsate = function( o, done ) {
+	var elem = $( this ),
+		mode = $.effects.setMode( elem, o.mode || "show" ),
+		show = mode === "show",
+		hide = mode === "hide",
+		showhide = ( show || mode === "hide" ),
+
+		// showing or hiding leaves of the "last" animation
+		anims = ( ( o.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ),
+		duration = o.duration / anims,
+		animateTo = 0,
+		queue = elem.queue(),
+		queuelen = queue.length,
+		i;
+
+	if ( show || !elem.is(":visible")) {
+		elem.css( "opacity", 0 ).show();
+		animateTo = 1;
+	}
+
+	// anims - 1 opacity "toggles"
+	for ( i = 1; i < anims; i++ ) {
+		elem.animate({
+			opacity: animateTo
+		}, duration, o.easing );
+		animateTo = 1 - animateTo;
+	}
+
+	elem.animate({
+		opacity: animateTo
+	}, duration, o.easing);
+
+	elem.queue(function() {
+		if ( hide ) {
+			elem.hide();
+		}
+		done();
+	});
+
+	// We just queued up "anims" animations, we need to put them next in the queue
+	if ( queuelen > 1 ) {
+		queue.splice.apply( queue,
+			[ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
+	}
+	elem.dequeue();
+};
+
+
+/*!
+ * jQuery UI Effects Shake 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/shake-effect/
+ */
+
+
+var effectShake = $.effects.effect.shake = function( o, done ) {
+
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
+		mode = $.effects.setMode( el, o.mode || "effect" ),
+		direction = o.direction || "left",
+		distance = o.distance || 20,
+		times = o.times || 3,
+		anims = times * 2 + 1,
+		speed = Math.round( o.duration / anims ),
+		ref = (direction === "up" || direction === "down") ? "top" : "left",
+		positiveMotion = (direction === "up" || direction === "left"),
+		animation = {},
+		animation1 = {},
+		animation2 = {},
+		i,
+
+		// we will need to re-assemble the queue to stack our animations in place
+		queue = el.queue(),
+		queuelen = queue.length;
+
+	$.effects.save( el, props );
+	el.show();
+	$.effects.createWrapper( el );
+
+	// Animation
+	animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance;
+	animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2;
+	animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2;
+
+	// Animate
+	el.animate( animation, speed, o.easing );
+
+	// Shakes
+	for ( i = 1; i < times; i++ ) {
+		el.animate( animation1, speed, o.easing ).animate( animation2, speed, o.easing );
+	}
+	el
+		.animate( animation1, speed, o.easing )
+		.animate( animation, speed / 2, o.easing )
+		.queue(function() {
+			if ( mode === "hide" ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		});
+
+	// inject all the animations we just queued to be first in line (after "inprogress")
+	if ( queuelen > 1) {
+		queue.splice.apply( queue,
+			[ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) );
+	}
+	el.dequeue();
+
+};
+
+
+/*!
+ * jQuery UI Effects Slide 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/slide-effect/
+ */
+
+
+var effectSlide = $.effects.effect.slide = function( o, done ) {
+
+	// Create element
+	var el = $( this ),
+		props = [ "position", "top", "bottom", "left", "right", "width", "height" ],
+		mode = $.effects.setMode( el, o.mode || "show" ),
+		show = mode === "show",
+		direction = o.direction || "left",
+		ref = (direction === "up" || direction === "down") ? "top" : "left",
+		positiveMotion = (direction === "up" || direction === "left"),
+		distance,
+		animation = {};
+
+	// Adjust
+	$.effects.save( el, props );
+	el.show();
+	distance = o.distance || el[ ref === "top" ? "outerHeight" : "outerWidth" ]( true );
+
+	$.effects.createWrapper( el ).css({
+		overflow: "hidden"
+	});
+
+	if ( show ) {
+		el.css( ref, positiveMotion ? (isNaN(distance) ? "-" + distance : -distance) : distance );
+	}
+
+	// Animation
+	animation[ ref ] = ( show ?
+		( positiveMotion ? "+=" : "-=") :
+		( positiveMotion ? "-=" : "+=")) +
+		distance;
+
+	// Animate
+	el.animate( animation, {
+		queue: false,
+		duration: o.duration,
+		easing: o.easing,
+		complete: function() {
+			if ( mode === "hide" ) {
+				el.hide();
+			}
+			$.effects.restore( el, props );
+			$.effects.removeWrapper( el );
+			done();
+		}
+	});
+};
+
+
+/*!
+ * jQuery UI Effects Transfer 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/transfer-effect/
+ */
+
+
+var effectTransfer = $.effects.effect.transfer = function( o, done ) {
+	var elem = $( this ),
+		target = $( o.to ),
+		targetFixed = target.css( "position" ) === "fixed",
+		body = $("body"),
+		fixTop = targetFixed ? body.scrollTop() : 0,
+		fixLeft = targetFixed ? body.scrollLeft() : 0,
+		endPosition = target.offset(),
+		animation = {
+			top: endPosition.top - fixTop,
+			left: endPosition.left - fixLeft,
+			height: target.innerHeight(),
+			width: target.innerWidth()
+		},
+		startPosition = elem.offset(),
+		transfer = $( "<div class='ui-effects-transfer'></div>" )
+			.appendTo( document.body )
+			.addClass( o.className )
+			.css({
+				top: startPosition.top - fixTop,
+				left: startPosition.left - fixLeft,
+				height: elem.innerHeight(),
+				width: elem.innerWidth(),
+				position: targetFixed ? "fixed" : "absolute"
+			})
+			.animate( animation, o.duration, o.easing, function() {
+				transfer.remove();
+				done();
+			});
+};
+
+
+
+}));
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.css
new file mode 100644
index 0000000..0890485
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.css
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, menu.css, progressbar.css, selectmenu.css, slider.css, spinner.css, tabs.css, tooltip.css, theme.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgText [...]
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;heig [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.js b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.js
new file mode 100644
index 0000000..4e8712c
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.min.js
@@ -0,0 +1,13 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Includes: core.js, widget.js, mouse.js, position.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, menu.js, progressbar.js, selectmenu.js, slider.js, spinner.js, tabs.js, tooltip.js, effect.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake. [...]
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+(function(e){"function"==typeof define&&define.amd?define(["jquery"],e):e(jQuery)})(function(e){function t(t,s){var n,a,o,r=t.nodeName.toLowerCase();return"area"===r?(n=t.parentNode,a=n.name,t.href&&a&&"map"===n.nodeName.toLowerCase()?(o=e("img[usemap='#"+a+"']")[0],!!o&&i(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||s:s)&&i(t)}function i(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"vis [...]
+},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_normalizeRightBottom:function(){"y"!==this.options.axis&&"auto"!==this.helper.css("right")&&(this.helper.width(this.helper.width()),this.helper.css("right","auto")),"x"!==this.options.axis&&"auto"!==this.helper.css("bottom")&&(this.helper.height(this.helper.h [...]
+this.element.addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){t=e(i.options.filter,i.element[0]),t.addClass("ui-selectee"),t.each(function(){var t=e(this),i=t.offset();e.data(this,"selectable-item",{element:this,$element:t,left:i.left,top:i.top,right:i.left+t.outerWidth(),bottom:i.top+t.outerHeight(),startselected:!1,selected:t.hasClass("ui-selected"),selecting:t.hasClass("ui-selecting"),unselecting:t.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=t.addCla [...]
+t.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(t=n.height(),this.element.siblings [...]
+},_enableDatepicker:function(t){var i,s,n=e(t),a=e.data(t,"datepicker");n.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,a.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.m [...]
+e||(e=this.element.find("[autofocus]")),e.length||(e=this.element.find(":tabbable")),e.length||(e=this.uiDialogButtonPane.find(":tabbable")),e.length||(e=this.uiDialogTitlebarClose.filter(":tabbable")),e.length||(e=this.uiDialog),e.eq(0).focus()},_keepFocus:function(t){function i(){var t=this.document[0].activeElement,i=this.uiDialog[0]===t||e.contains(this.uiDialog[0],t);i||this._focusTabbable()}t.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=e("< [...]
+},_start:function(e){return this.spinning||this._trigger("start",e)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(e,t,i){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,i)},e),this._spin(t*this.options.step,i)},_spin:function(e,t){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+e*this._increment(this.counter)),this.spinning&&this._trigger("spin",t,{value:i})===!1||(this._value(i),this.counte [...]
+var n=t.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(e.fn.hide),toggle:function(e){return function(s){if(i(s)||"boolean"==typeof s)return e.apply(this,arguments);var n=t.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(e.fn.toggle),cssUnit:function(t){var i=this.css(t),s=[];return e.each(["em","px","%","pt"],function(e,t){i.indexOf(t)>0&&(s=[parseFloat(i),t])}),s}})}(),function(){var t={};e.each(["Quad","Cubic","Quart","Quint","Expo"],funct [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.css
new file mode 100644
index 0000000..d3d88d4
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.css
@@ -0,0 +1,833 @@
+/*!
+ * jQuery UI CSS Framework 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/theming/
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+	display: none;
+}
+.ui-helper-hidden-accessible {
+	border: 0;
+	clip: rect(0 0 0 0);
+	height: 1px;
+	margin: -1px;
+	overflow: hidden;
+	padding: 0;
+	position: absolute;
+	width: 1px;
+}
+.ui-helper-reset {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	outline: 0;
+	line-height: 1.3;
+	text-decoration: none;
+	font-size: 100%;
+	list-style: none;
+}
+.ui-helper-clearfix:before,
+.ui-helper-clearfix:after {
+	content: "";
+	display: table;
+	border-collapse: collapse;
+}
+.ui-helper-clearfix:after {
+	clear: both;
+}
+.ui-helper-clearfix {
+	min-height: 0; /* support: IE7 */
+}
+.ui-helper-zfix {
+	width: 100%;
+	height: 100%;
+	top: 0;
+	left: 0;
+	position: absolute;
+	opacity: 0;
+	filter:Alpha(Opacity=0); /* support: IE8 */
+}
+
+.ui-front {
+	z-index: 100;
+}
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled {
+	cursor: default !important;
+}
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	display: block;
+	text-indent: -99999px;
+	overflow: hidden;
+	background-repeat: no-repeat;
+}
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+.ui-draggable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable {
+	position: relative;
+}
+.ui-resizable-handle {
+	position: absolute;
+	font-size: 0.1px;
+	display: block;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-resizable-disabled .ui-resizable-handle,
+.ui-resizable-autohide .ui-resizable-handle {
+	display: none;
+}
+.ui-resizable-n {
+	cursor: n-resize;
+	height: 7px;
+	width: 100%;
+	top: -5px;
+	left: 0;
+}
+.ui-resizable-s {
+	cursor: s-resize;
+	height: 7px;
+	width: 100%;
+	bottom: -5px;
+	left: 0;
+}
+.ui-resizable-e {
+	cursor: e-resize;
+	width: 7px;
+	right: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-w {
+	cursor: w-resize;
+	width: 7px;
+	left: -5px;
+	top: 0;
+	height: 100%;
+}
+.ui-resizable-se {
+	cursor: se-resize;
+	width: 12px;
+	height: 12px;
+	right: 1px;
+	bottom: 1px;
+}
+.ui-resizable-sw {
+	cursor: sw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	bottom: -5px;
+}
+.ui-resizable-nw {
+	cursor: nw-resize;
+	width: 9px;
+	height: 9px;
+	left: -5px;
+	top: -5px;
+}
+.ui-resizable-ne {
+	cursor: ne-resize;
+	width: 9px;
+	height: 9px;
+	right: -5px;
+	top: -5px;
+}
+.ui-selectable {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-selectable-helper {
+	position: absolute;
+	z-index: 100;
+	border: 1px dotted black;
+}
+.ui-sortable-handle {
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-accordion .ui-accordion-header {
+	display: block;
+	cursor: pointer;
+	position: relative;
+	margin: 2px 0 0 0;
+	padding: .5em .5em .5em .7em;
+	min-height: 0; /* support: IE7 */
+	font-size: 100%;
+}
+.ui-accordion .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-icons .ui-accordion-icons {
+	padding-left: 2.2em;
+}
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
+	position: absolute;
+	left: .5em;
+	top: 50%;
+	margin-top: -8px;
+}
+.ui-accordion .ui-accordion-content {
+	padding: 1em 2.2em;
+	border-top: 0;
+	overflow: auto;
+}
+.ui-autocomplete {
+	position: absolute;
+	top: 0;
+	left: 0;
+	cursor: default;
+}
+.ui-button {
+	display: inline-block;
+	position: relative;
+	padding: 0;
+	line-height: normal;
+	margin-right: .1em;
+	cursor: pointer;
+	vertical-align: middle;
+	text-align: center;
+	overflow: visible; /* removes extra width in IE */
+}
+.ui-button,
+.ui-button:link,
+.ui-button:visited,
+.ui-button:hover,
+.ui-button:active {
+	text-decoration: none;
+}
+/* to make room for the icon, a width needs to be set here */
+.ui-button-icon-only {
+	width: 2.2em;
+}
+/* button elements seem to need a little more width */
+button.ui-button-icon-only {
+	width: 2.4em;
+}
+.ui-button-icons-only {
+	width: 3.4em;
+}
+button.ui-button-icons-only {
+	width: 3.7em;
+}
+
+/* button text element */
+.ui-button .ui-button-text {
+	display: block;
+	line-height: normal;
+}
+.ui-button-text-only .ui-button-text {
+	padding: .4em 1em;
+}
+.ui-button-icon-only .ui-button-text,
+.ui-button-icons-only .ui-button-text {
+	padding: .4em;
+	text-indent: -9999999px;
+}
+.ui-button-text-icon-primary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 1em .4em 2.1em;
+}
+.ui-button-text-icon-secondary .ui-button-text,
+.ui-button-text-icons .ui-button-text {
+	padding: .4em 2.1em .4em 1em;
+}
+.ui-button-text-icons .ui-button-text {
+	padding-left: 2.1em;
+	padding-right: 2.1em;
+}
+/* no icon support for input elements, provide padding by default */
+input.ui-button {
+	padding: .4em 1em;
+}
+
+/* button icon element(s) */
+.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;
+}
+.ui-button-icon-only .ui-icon {
+	left: 50%;
+	margin-left: -8px;
+}
+.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;
+}
+.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;
+}
+
+/* button sets */
+.ui-buttonset {
+	margin-right: 7px;
+}
+.ui-buttonset .ui-button {
+	margin-left: 0;
+	margin-right: -.3em;
+}
+
+/* workarounds */
+/* reset extra padding in Firefox, see h5bp.com/l */
+input.ui-button::-moz-focus-inner,
+button.ui-button::-moz-focus-inner {
+	border: 0;
+	padding: 0;
+}
+.ui-datepicker {
+	width: 17em;
+	padding: .2em .2em 0;
+	display: none;
+}
+.ui-datepicker .ui-datepicker-header {
+	position: relative;
+	padding: .2em 0;
+}
+.ui-datepicker .ui-datepicker-prev,
+.ui-datepicker .ui-datepicker-next {
+	position: absolute;
+	top: 2px;
+	width: 1.8em;
+	height: 1.8em;
+}
+.ui-datepicker .ui-datepicker-prev-hover,
+.ui-datepicker .ui-datepicker-next-hover {
+	top: 1px;
+}
+.ui-datepicker .ui-datepicker-prev {
+	left: 2px;
+}
+.ui-datepicker .ui-datepicker-next {
+	right: 2px;
+}
+.ui-datepicker .ui-datepicker-prev-hover {
+	left: 1px;
+}
+.ui-datepicker .ui-datepicker-next-hover {
+	right: 1px;
+}
+.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;
+}
+.ui-datepicker .ui-datepicker-title {
+	margin: 0 2.3em;
+	line-height: 1.8em;
+	text-align: center;
+}
+.ui-datepicker .ui-datepicker-title select {
+	font-size: 1em;
+	margin: 1px 0;
+}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year {
+	width: 45%;
+}
+.ui-datepicker table {
+	width: 100%;
+	font-size: .9em;
+	border-collapse: collapse;
+	margin: 0 0 .4em;
+}
+.ui-datepicker th {
+	padding: .7em .3em;
+	text-align: center;
+	font-weight: bold;
+	border: 0;
+}
+.ui-datepicker td {
+	border: 0;
+	padding: 1px;
+}
+.ui-datepicker td span,
+.ui-datepicker td a {
+	display: block;
+	padding: .2em;
+	text-align: right;
+	text-decoration: none;
+}
+.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;
+}
+.ui-datepicker .ui-datepicker-buttonpane button {
+	float: right;
+	margin: .5em .2em .4em;
+	cursor: pointer;
+	padding: .2em .6em .3em .6em;
+	width: auto;
+	overflow: visible;
+}
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
+	float: left;
+}
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi {
+	width: auto;
+}
+.ui-datepicker-multi .ui-datepicker-group {
+	float: left;
+}
+.ui-datepicker-multi .ui-datepicker-group table {
+	width: 95%;
+	margin: 0 auto .4em;
+}
+.ui-datepicker-multi-2 .ui-datepicker-group {
+	width: 50%;
+}
+.ui-datepicker-multi-3 .ui-datepicker-group {
+	width: 33.3%;
+}
+.ui-datepicker-multi-4 .ui-datepicker-group {
+	width: 25%;
+}
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
+	border-left-width: 0;
+}
+.ui-datepicker-multi .ui-datepicker-buttonpane {
+	clear: left;
+}
+.ui-datepicker-row-break {
+	clear: both;
+	width: 100%;
+	font-size: 0;
+}
+
+/* RTL support */
+.ui-datepicker-rtl {
+	direction: rtl;
+}
+.ui-datepicker-rtl .ui-datepicker-prev {
+	right: 2px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next {
+	left: 2px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-prev:hover {
+	right: 1px;
+	left: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-next:hover {
+	left: 1px;
+	right: auto;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane {
+	clear: right;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button {
+	float: left;
+}
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
+.ui-datepicker-rtl .ui-datepicker-group {
+	float: right;
+}
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
+	border-right-width: 0;
+	border-left-width: 1px;
+}
+.ui-dialog {
+	overflow: hidden;
+	position: absolute;
+	top: 0;
+	left: 0;
+	padding: .2em;
+	outline: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+	padding: .4em 1em;
+	position: relative;
+}
+.ui-dialog .ui-dialog-title {
+	float: left;
+	margin: .1em 0;
+	white-space: nowrap;
+	width: 90%;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+	position: absolute;
+	right: .3em;
+	top: 50%;
+	width: 20px;
+	margin: -10px 0 0 0;
+	padding: 1px;
+	height: 20px;
+}
+.ui-dialog .ui-dialog-content {
+	position: relative;
+	border: 0;
+	padding: .5em 1em;
+	background: none;
+	overflow: auto;
+}
+.ui-dialog .ui-dialog-buttonpane {
+	text-align: left;
+	border-width: 1px 0 0 0;
+	background-image: none;
+	margin-top: .5em;
+	padding: .3em 1em .5em .4em;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+	float: right;
+}
+.ui-dialog .ui-dialog-buttonpane button {
+	margin: .5em .4em .5em 0;
+	cursor: pointer;
+}
+.ui-dialog .ui-resizable-se {
+	width: 12px;
+	height: 12px;
+	right: -5px;
+	bottom: -5px;
+	background-position: 16px 16px;
+}
+.ui-draggable .ui-dialog-titlebar {
+	cursor: move;
+}
+.ui-menu {
+	list-style: none;
+	padding: 0;
+	margin: 0;
+	display: block;
+	outline: none;
+}
+.ui-menu .ui-menu {
+	position: absolute;
+}
+.ui-menu .ui-menu-item {
+	position: relative;
+	margin: 0;
+	padding: 3px 1em 3px .4em;
+	cursor: pointer;
+	min-height: 0; /* support: IE7 */
+	/* support: IE10, see #8844 */
+	list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
+}
+.ui-menu .ui-menu-divider {
+	margin: 5px 0;
+	height: 0;
+	font-size: 0;
+	line-height: 0;
+	border-width: 1px 0 0 0;
+}
+.ui-menu .ui-state-focus,
+.ui-menu .ui-state-active {
+	margin: -1px;
+}
+
+/* icon support */
+.ui-menu-icons {
+	position: relative;
+}
+.ui-menu-icons .ui-menu-item {
+	padding-left: 2em;
+}
+
+/* left-aligned */
+.ui-menu .ui-icon {
+	position: absolute;
+	top: 0;
+	bottom: 0;
+	left: .2em;
+	margin: auto 0;
+}
+
+/* right-aligned */
+.ui-menu .ui-menu-icon {
+	left: auto;
+	right: 0;
+}
+.ui-progressbar {
+	height: 2em;
+	text-align: left;
+	overflow: hidden;
+}
+.ui-progressbar .ui-progressbar-value {
+	margin: -1px;
+	height: 100%;
+}
+.ui-progressbar .ui-progressbar-overlay {
+	background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmo [...]
+	height: 100%;
+	filter: alpha(opacity=25); /* support: IE8 */
+	opacity: 0.25;
+}
+.ui-progressbar-indeterminate .ui-progressbar-value {
+	background-image: none;
+}
+.ui-selectmenu-menu {
+	padding: 0;
+	margin: 0;
+	position: absolute;
+	top: 0;
+	left: 0;
+	display: none;
+}
+.ui-selectmenu-menu .ui-menu {
+	overflow: auto;
+	/* Support: IE7 */
+	overflow-x: hidden;
+	padding-bottom: 1px;
+}
+.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
+	font-size: 1em;
+	font-weight: bold;
+	line-height: 1.5;
+	padding: 2px 0.4em;
+	margin: 0.5em 0 0 0;
+	height: auto;
+	border: 0;
+}
+.ui-selectmenu-open {
+	display: block;
+}
+.ui-selectmenu-button {
+	display: inline-block;
+	overflow: hidden;
+	position: relative;
+	text-decoration: none;
+	cursor: pointer;
+}
+.ui-selectmenu-button span.ui-icon {
+	right: 0.5em;
+	left: auto;
+	margin-top: -8px;
+	position: absolute;
+	top: 50%;
+}
+.ui-selectmenu-button span.ui-selectmenu-text {
+	text-align: left;
+	padding: 0.4em 2.1em 0.4em 1em;
+	display: block;
+	line-height: 1.4;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+.ui-slider {
+	position: relative;
+	text-align: left;
+}
+.ui-slider .ui-slider-handle {
+	position: absolute;
+	z-index: 2;
+	width: 1.2em;
+	height: 1.2em;
+	cursor: default;
+	-ms-touch-action: none;
+	touch-action: none;
+}
+.ui-slider .ui-slider-range {
+	position: absolute;
+	z-index: 1;
+	font-size: .7em;
+	display: block;
+	border: 0;
+	background-position: 0 0;
+}
+
+/* support: IE8 - See #6727 */
+.ui-slider.ui-state-disabled .ui-slider-handle,
+.ui-slider.ui-state-disabled .ui-slider-range {
+	filter: inherit;
+}
+
+.ui-slider-horizontal {
+	height: .8em;
+}
+.ui-slider-horizontal .ui-slider-handle {
+	top: -.3em;
+	margin-left: -.6em;
+}
+.ui-slider-horizontal .ui-slider-range {
+	top: 0;
+	height: 100%;
+}
+.ui-slider-horizontal .ui-slider-range-min {
+	left: 0;
+}
+.ui-slider-horizontal .ui-slider-range-max {
+	right: 0;
+}
+
+.ui-slider-vertical {
+	width: .8em;
+	height: 100px;
+}
+.ui-slider-vertical .ui-slider-handle {
+	left: -.3em;
+	margin-left: 0;
+	margin-bottom: -.6em;
+}
+.ui-slider-vertical .ui-slider-range {
+	left: 0;
+	width: 100%;
+}
+.ui-slider-vertical .ui-slider-range-min {
+	bottom: 0;
+}
+.ui-slider-vertical .ui-slider-range-max {
+	top: 0;
+}
+.ui-spinner {
+	position: relative;
+	display: inline-block;
+	overflow: hidden;
+	padding: 0;
+	vertical-align: middle;
+}
+.ui-spinner-input {
+	border: none;
+	background: none;
+	color: inherit;
+	padding: 0;
+	margin: .2em 0;
+	vertical-align: middle;
+	margin-left: .4em;
+	margin-right: 22px;
+}
+.ui-spinner-button {
+	width: 16px;
+	height: 50%;
+	font-size: .5em;
+	padding: 0;
+	margin: 0;
+	text-align: center;
+	position: absolute;
+	cursor: default;
+	display: block;
+	overflow: hidden;
+	right: 0;
+}
+/* more specificity required here to override default borders */
+.ui-spinner a.ui-spinner-button {
+	border-top: none;
+	border-bottom: none;
+	border-right: none;
+}
+/* vertically center icon */
+.ui-spinner .ui-icon {
+	position: absolute;
+	margin-top: -8px;
+	top: 50%;
+	left: 0;
+}
+.ui-spinner-up {
+	top: 0;
+}
+.ui-spinner-down {
+	bottom: 0;
+}
+
+/* TR overrides */
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position: -65px -16px;
+}
+.ui-tabs {
+	position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+	padding: .2em;
+}
+.ui-tabs .ui-tabs-nav {
+	margin: 0;
+	padding: .2em .2em 0;
+}
+.ui-tabs .ui-tabs-nav li {
+	list-style: none;
+	float: left;
+	position: relative;
+	top: 0;
+	margin: 1px .2em 0 0;
+	border-bottom-width: 0;
+	padding: 0;
+	white-space: nowrap;
+}
+.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
+	float: left;
+	padding: .5em 1em;
+	text-decoration: none;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active {
+	margin-bottom: -1px;
+	padding-bottom: 1px;
+}
+.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
+.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
+	cursor: text;
+}
+.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
+	cursor: pointer;
+}
+.ui-tabs .ui-tabs-panel {
+	display: block;
+	border-width: 0;
+	padding: 1em 1.4em;
+	background: none;
+}
+.ui-tooltip {
+	padding: 8px;
+	position: absolute;
+	z-index: 9999;
+	max-width: 300px;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+body .ui-tooltip {
+	border-width: 2px;
+}
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.min.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.min.css
new file mode 100644
index 0000000..7eb8b66
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.structure.min.css
@@ -0,0 +1,5 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;heig [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.css
new file mode 100644
index 0000000..5b66435
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.css
@@ -0,0 +1,410 @@
+/*!
+ * jQuery UI CSS Framework 1.11.2
+ * http://jqueryui.com
+ *
+ * Copyright 2014 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/theming/
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTex [...]
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget {
+	font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+	font-size: 1.1em;
+}
+.ui-widget .ui-widget {
+	font-size: 1em;
+}
+.ui-widget input,
+.ui-widget select,
+.ui-widget textarea,
+.ui-widget button {
+	font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
+	font-size: 1em;
+}
+.ui-widget-content {
+	border: 1px solid #dddddd;
+	background: #eeeeee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;
+	color: #333333;
+}
+.ui-widget-content a {
+	color: #333333;
+}
+.ui-widget-header {
+	border: 1px solid #e78f08;
+	background: #f6a828 url("images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;
+	color: #ffffff;
+	font-weight: bold;
+}
+.ui-widget-header a {
+	color: #ffffff;
+}
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default,
+.ui-widget-content .ui-state-default,
+.ui-widget-header .ui-state-default {
+	border: 1px solid #cccccc;
+	background: #f6f6f6 url("images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #1c94c4;
+}
+.ui-state-default a,
+.ui-state-default a:link,
+.ui-state-default a:visited {
+	color: #1c94c4;
+	text-decoration: none;
+}
+.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 #fbcb09;
+	background: #fdf5ce url("images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #c77405;
+}
+.ui-state-hover a,
+.ui-state-hover a:hover,
+.ui-state-hover a:link,
+.ui-state-hover a:visited,
+.ui-state-focus a,
+.ui-state-focus a:hover,
+.ui-state-focus a:link,
+.ui-state-focus a:visited {
+	color: #c77405;
+	text-decoration: none;
+}
+.ui-state-active,
+.ui-widget-content .ui-state-active,
+.ui-widget-header .ui-state-active {
+	border: 1px solid #fbd850;
+	background: #ffffff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;
+	font-weight: bold;
+	color: #eb8f00;
+}
+.ui-state-active a,
+.ui-state-active a:link,
+.ui-state-active a:visited {
+	color: #eb8f00;
+	text-decoration: none;
+}
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight,
+.ui-widget-content .ui-state-highlight,
+.ui-widget-header .ui-state-highlight {
+	border: 1px solid #fed22f;
+	background: #ffe45c url("images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;
+	color: #363636;
+}
+.ui-state-highlight a,
+.ui-widget-content .ui-state-highlight a,
+.ui-widget-header .ui-state-highlight a {
+	color: #363636;
+}
+.ui-state-error,
+.ui-widget-content .ui-state-error,
+.ui-widget-header .ui-state-error {
+	border: 1px solid #cd0a0a;
+	background: #b81900 url("images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;
+	color: #ffffff;
+}
+.ui-state-error a,
+.ui-widget-content .ui-state-error a,
+.ui-widget-header .ui-state-error a {
+	color: #ffffff;
+}
+.ui-state-error-text,
+.ui-widget-content .ui-state-error-text,
+.ui-widget-header .ui-state-error-text {
+	color: #ffffff;
+}
+.ui-priority-primary,
+.ui-widget-content .ui-priority-primary,
+.ui-widget-header .ui-priority-primary {
+	font-weight: bold;
+}
+.ui-priority-secondary,
+.ui-widget-content .ui-priority-secondary,
+.ui-widget-header .ui-priority-secondary {
+	opacity: .7;
+	filter:Alpha(Opacity=70); /* support: IE8 */
+	font-weight: normal;
+}
+.ui-state-disabled,
+.ui-widget-content .ui-state-disabled,
+.ui-widget-header .ui-state-disabled {
+	opacity: .35;
+	filter:Alpha(Opacity=35); /* support: IE8 */
+	background-image: none;
+}
+.ui-state-disabled .ui-icon {
+	filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
+}
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon {
+	width: 16px;
+	height: 16px;
+}
+.ui-icon,
+.ui-widget-content .ui-icon {
+	background-image: url("images/ui-icons_222222_256x240.png");
+}
+.ui-widget-header .ui-icon {
+	background-image: url("images/ui-icons_ffffff_256x240.png");
+}
+.ui-state-default .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-hover .ui-icon,
+.ui-state-focus .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-active .ui-icon {
+	background-image: url("images/ui-icons_ef8c08_256x240.png");
+}
+.ui-state-highlight .ui-icon {
+	background-image: url("images/ui-icons_228ef1_256x240.png");
+}
+.ui-state-error .ui-icon,
+.ui-state-error-text .ui-icon {
+	background-image: url("images/ui-icons_ffd27a_256x240.png");
+}
+
+/* positioning */
+.ui-icon-blank { background-position: 16px 16px; }
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-left,
+.ui-corner-tl {
+	border-top-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-top,
+.ui-corner-right,
+.ui-corner-tr {
+	border-top-right-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-left,
+.ui-corner-bl {
+	border-bottom-left-radius: 4px;
+}
+.ui-corner-all,
+.ui-corner-bottom,
+.ui-corner-right,
+.ui-corner-br {
+	border-bottom-right-radius: 4px;
+}
+
+/* Overlays */
+.ui-widget-overlay {
+	background: #666666 url("images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;
+	opacity: .5;
+	filter: Alpha(Opacity=50); /* support: IE8 */
+}
+.ui-widget-shadow {
+	margin: -5px 0 0 -5px;
+	padding: 5px;
+	background: #000000 url("images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;
+	opacity: .2;
+	filter: Alpha(Opacity=20); /* support: IE8 */
+	border-radius: 5px;
+}
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.min.css b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.min.css
new file mode 100644
index 0000000..9d5beff
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-1.11.2.custom/jquery-ui.theme.min.css
@@ -0,0 +1,5 @@
+/*! jQuery UI - v1.11.2 - 2014-12-05
+* http://jqueryui.com
+* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:# [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000..5b5dab2
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 0000000..ad3d634
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..42ccba2
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 0000000..5a46b47
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 0000000..86c2baa
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000..4443fdc
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 0000000..7c9fa6c
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_222222_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000..ee039dc
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_222222_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000..45e8928
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_454545_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..7ec70d1
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_454545_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_888888_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_888888_256x240.png
new file mode 100644
index 0000000..5ba708c
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_888888_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000..7930a55
Binary files /dev/null and b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery-1.8.2.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery-1.8.2.js
new file mode 100644
index 0000000..d4f3bb3
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery-1.8.2.js
@@ -0,0 +1,9440 @@
+/*!
+ * jQuery JavaScript Library v1.8.2
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: Thu Sep 20 2012 21:13:05 GMT-0400 (Eastern Daylight Time)
+ */
+(function( window, undefined ) {
+var
+	// A central reference to the root jQuery(document)
+	rootjQuery,
+
+	// The deferred used on DOM ready
+	readyList,
+
+	// Use the correct document accordingly with window argument (sandbox)
+	document = window.document,
+	location = window.location,
+	navigator = window.navigator,
+
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	// Save a reference to some core methods
+	core_push = Array.prototype.push,
+	core_slice = Array.prototype.slice,
+	core_indexOf = Array.prototype.indexOf,
+	core_toString = Object.prototype.toString,
+	core_hasOwn = Object.prototype.hasOwnProperty,
+	core_trim = String.prototype.trim,
+
+	// Define a local copy of jQuery
+	jQuery = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context, rootjQuery );
+	},
+
+	// Used for matching numbers
+	core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,
+
+	// Used for detecting and trimming whitespace
+	core_rnotwhite = /\S/,
+	core_rspace = /\s+/,
+
+	// Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+	// A simple way to check for HTML strings
+	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+	rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
+
+	// Match a standalone tag
+	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+	// JSON RegExp
+	rvalidchars = /^[\],:{}\s]*$/,
+	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+	rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+	rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,
+
+	// Matches dashed string for camelizing
+	rmsPrefix = /^-ms-/,
+	rdashAlpha = /-([\da-z])/gi,
+
+	// Used by jQuery.camelCase as callback to replace()
+	fcamelCase = function( all, letter ) {
+		return ( letter + "" ).toUpperCase();
+	},
+
+	// The ready event handler and self cleanup method
+	DOMContentLoaded = function() {
+		if ( document.addEventListener ) {
+			document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+			jQuery.ready();
+		} else if ( document.readyState === "complete" ) {
+			// we're here because readyState === "complete" in oldIE
+			// which is good enough for us to call the dom ready!
+			document.detachEvent( "onreadystatechange", DOMContentLoaded );
+			jQuery.ready();
+		}
+	},
+
+	// [[Class]] -> type pairs
+	class2type = {};
+
+jQuery.fn = jQuery.prototype = {
+	constructor: jQuery,
+	init: function( selector, context, rootjQuery ) {
+		var match, elem, ret, doc;
+
+		// Handle $(""), $(null), $(undefined), $(false)
+		if ( !selector ) {
+			return this;
+		}
+
+		// Handle $(DOMElement)
+		if ( selector.nodeType ) {
+			this.context = this[0] = selector;
+			this.length = 1;
+			return this;
+		}
+
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+				// Assume that strings that start and end with <> are HTML and skip the regex check
+				match = [ null, selector, null ];
+
+			} else {
+				match = rquickExpr.exec( selector );
+			}
+
+			// Match html or make sure no context is specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] ) {
+					context = context instanceof jQuery ? context[0] : context;
+					doc = ( context && context.nodeType ? context.ownerDocument || context : document );
+
+					// scripts is true for back-compat
+					selector = jQuery.parseHTML( match[1], doc, true );
+					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+						this.attr.call( selector, context, true );
+					}
+
+					return jQuery.merge( this, selector );
+
+				// HANDLE: $(#id)
+				} else {
+					elem = document.getElementById( match[2] );
+
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE and Opera return items
+						// by name instead of ID
+						if ( elem.id !== match[2] ) {
+							return rootjQuery.find( selector );
+						}
+
+						// Otherwise, we inject the element directly into the jQuery object
+						this.length = 1;
+						this[0] = elem;
+					}
+
+					this.context = document;
+					this.selector = selector;
+					return this;
+				}
+
+			// HANDLE: $(expr, $(...))
+			} else if ( !context || context.jquery ) {
+				return ( context || rootjQuery ).find( selector );
+
+			// HANDLE: $(expr, context)
+			// (which is just equivalent to: $(context).find(expr)
+			} else {
+				return this.constructor( context ).find( selector );
+			}
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) ) {
+			return rootjQuery.ready( selector );
+		}
+
+		if ( selector.selector !== undefined ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return jQuery.makeArray( selector, this );
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The current version of jQuery being used
+	jquery: "1.8.2",
+
+	// The default length of a jQuery object is 0
+	length: 0,
+
+	// The number of elements contained in the matched element set
+	size: function() {
+		return this.length;
+	},
+
+	toArray: function() {
+		return core_slice.call( this );
+	},
+
+	// Get the Nth element in the matched element set OR
+	// Get the whole matched element set as a clean array
+	get: function( num ) {
+		return num == null ?
+
+			// Return a 'clean' array
+			this.toArray() :
+
+			// Return just the object
+			( num < 0 ? this[ this.length + num ] : this[ num ] );
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems, name, selector ) {
+
+		// Build a new jQuery matched element set
+		var ret = jQuery.merge( this.constructor(), elems );
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+
+		ret.context = this.context;
+
+		if ( name === "find" ) {
+			ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
+		} else if ( name ) {
+			ret.selector = this.selector + "." + name + "(" + selector + ")";
+		}
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Execute a callback for every element in the matched set.
+	// (You can seed the arguments with an array of args, but this is
+	// only used internally.)
+	each: function( callback, args ) {
+		return jQuery.each( this, callback, args );
+	},
+
+	ready: function( fn ) {
+		// Add the callback
+		jQuery.ready.promise().done( fn );
+
+		return this;
+	},
+
+	eq: function( i ) {
+		i = +i;
+		return i === -1 ?
+			this.slice( i ) :
+			this.slice( i, i + 1 );
+	},
+
+	first: function() {
+		return this.eq( 0 );
+	},
+
+	last: function() {
+		return this.eq( -1 );
+	},
+
+	slice: function() {
+		return this.pushStack( core_slice.apply( this, arguments ),
+			"slice", core_slice.call(arguments).join(",") );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function( elem, i ) {
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	end: function() {
+		return this.prevObject || this.constructor(null);
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: core_push,
+	sort: [].sort,
+	splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+	var options, name, src, copy, copyIsArray, clone,
+		target = arguments[0] || {},
+		i = 1,
+		length = arguments.length,
+		deep = false;
+
+	// Handle a deep copy situation
+	if ( typeof target === "boolean" ) {
+		deep = target;
+		target = arguments[1] || {};
+		// skip the boolean and the target
+		i = 2;
+	}
+
+	// Handle case when target is a string or something (possible in deep copy)
+	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+		target = {};
+	}
+
+	// extend jQuery itself if only one argument is passed
+	if ( length === i ) {
+		target = this;
+		--i;
+	}
+
+	for ( ; i < length; i++ ) {
+		// Only deal with non-null/undefined values
+		if ( (options = arguments[ i ]) != null ) {
+			// Extend the base object
+			for ( name in options ) {
+				src = target[ name ];
+				copy = options[ name ];
+
+				// Prevent never-ending loop
+				if ( target === copy ) {
+					continue;
+				}
+
+				// Recurse if we're merging plain objects or arrays
+				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+					if ( copyIsArray ) {
+						copyIsArray = false;
+						clone = src && jQuery.isArray(src) ? src : [];
+
+					} else {
+						clone = src && jQuery.isPlainObject(src) ? src : {};
+					}
+
+					// Never move original objects, clone them
+					target[ name ] = jQuery.extend( deep, clone, copy );
+
+				// Don't bring in undefined values
+				} else if ( copy !== undefined ) {
+					target[ name ] = copy;
+				}
+			}
+		}
+	}
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	noConflict: function( deep ) {
+		if ( window.$ === jQuery ) {
+			window.$ = _$;
+		}
+
+		if ( deep && window.jQuery === jQuery ) {
+			window.jQuery = _jQuery;
+		}
+
+		return jQuery;
+	},
+
+	// Is the DOM ready to be used? Set to true once it occurs.
+	isReady: false,
+
+	// A counter to track how many items to wait for before
+	// the ready event fires. See #6781
+	readyWait: 1,
+
+	// Hold (or release) the ready event
+	holdReady: function( hold ) {
+		if ( hold ) {
+			jQuery.readyWait++;
+		} else {
+			jQuery.ready( true );
+		}
+	},
+
+	// Handle when the DOM is ready
+	ready: function( wait ) {
+
+		// Abort if there are pending holds or we're already ready
+		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+			return;
+		}
+
+		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+		if ( !document.body ) {
+			return setTimeout( jQuery.ready, 1 );
+		}
+
+		// Remember that the DOM is ready
+		jQuery.isReady = true;
+
+		// If a normal DOM Ready event fired, decrement, and wait if need be
+		if ( wait !== true && --jQuery.readyWait > 0 ) {
+			return;
+		}
+
+		// If there are functions bound, to execute
+		readyList.resolveWith( document, [ jQuery ] );
+
+		// Trigger any bound ready events
+		if ( jQuery.fn.trigger ) {
+			jQuery( document ).trigger("ready").off("ready");
+		}
+	},
+
+	// See test/unit/core.js for details concerning isFunction.
+	// Since version 1.3, DOM methods and functions like alert
+	// aren't supported. They return false on IE (#2968).
+	isFunction: function( obj ) {
+		return jQuery.type(obj) === "function";
+	},
+
+	isArray: Array.isArray || function( obj ) {
+		return jQuery.type(obj) === "array";
+	},
+
+	isWindow: function( obj ) {
+		return obj != null && obj == obj.window;
+	},
+
+	isNumeric: function( obj ) {
+		return !isNaN( parseFloat(obj) ) && isFinite( obj );
+	},
+
+	type: function( obj ) {
+		return obj == null ?
+			String( obj ) :
+			class2type[ core_toString.call(obj) ] || "object";
+	},
+
+	isPlainObject: function( obj ) {
+		// Must be an Object.
+		// Because of IE, we also have to check the presence of the constructor property.
+		// Make sure that DOM nodes and window objects don't pass through, as well
+		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+			return false;
+		}
+
+		try {
+			// Not own constructor property must be Object
+			if ( obj.constructor &&
+				!core_hasOwn.call(obj, "constructor") &&
+				!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+				return false;
+			}
+		} catch ( e ) {
+			// IE8,9 Will throw exceptions on certain host objects #9897
+			return false;
+		}
+
+		// Own properties are enumerated firstly, so to speed up,
+		// if last one is own, then all properties are own.
+
+		var key;
+		for ( key in obj ) {}
+
+		return key === undefined || core_hasOwn.call( obj, key );
+	},
+
+	isEmptyObject: function( obj ) {
+		var name;
+		for ( name in obj ) {
+			return false;
+		}
+		return true;
+	},
+
+	error: function( msg ) {
+		throw new Error( msg );
+	},
+
+	// data: string of html
+	// context (optional): If specified, the fragment will be created in this context, defaults to document
+	// scripts (optional): If true, will include scripts passed in the html string
+	parseHTML: function( data, context, scripts ) {
+		var parsed;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		if ( typeof context === "boolean" ) {
+			scripts = context;
+			context = 0;
+		}
+		context = context || document;
+
+		// Single tag
+		if ( (parsed = rsingleTag.exec( data )) ) {
+			return [ context.createElement( parsed[1] ) ];
+		}
+
+		parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] );
+		return jQuery.merge( [],
+			(parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes );
+	},
+
+	parseJSON: function( data ) {
+		if ( !data || typeof data !== "string") {
+			return null;
+		}
+
+		// Make sure leading/trailing whitespace is removed (IE can't handle it)
+		data = jQuery.trim( data );
+
+		// Attempt to parse using the native JSON parser first
+		if ( window.JSON && window.JSON.parse ) {
+			return window.JSON.parse( data );
+		}
+
+		// Make sure the incoming data is actual JSON
+		// Logic borrowed from http://json.org/json2.js
+		if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+			.replace( rvalidtokens, "]" )
+			.replace( rvalidbraces, "")) ) {
+
+			return ( new Function( "return " + data ) )();
+
+		}
+		jQuery.error( "Invalid JSON: " + data );
+	},
+
+	// Cross-browser xml parsing
+	parseXML: function( data ) {
+		var xml, tmp;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		try {
+			if ( window.DOMParser ) { // Standard
+				tmp = new DOMParser();
+				xml = tmp.parseFromString( data , "text/xml" );
+			} else { // IE
+				xml = new ActiveXObject( "Microsoft.XMLDOM" );
+				xml.async = "false";
+				xml.loadXML( data );
+			}
+		} catch( e ) {
+			xml = undefined;
+		}
+		if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+			jQuery.error( "Invalid XML: " + data );
+		}
+		return xml;
+	},
+
+	noop: function() {},
+
+	// Evaluates a script in a global context
+	// Workarounds based on findings by Jim Driscoll
+	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+	globalEval: function( data ) {
+		if ( data && core_rnotwhite.test( data ) ) {
+			// We use execScript on Internet Explorer
+			// We use an anonymous function so that context is window
+			// rather than jQuery in Firefox
+			( window.execScript || function( data ) {
+				window[ "eval" ].call( window, data );
+			} )( data );
+		}
+	},
+
+	// Convert dashed to camelCase; used by the css and data modules
+	// Microsoft forgot to hump their vendor prefix (#9572)
+	camelCase: function( string ) {
+		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+	},
+
+	// args is for internal usage only
+	each: function( obj, callback, args ) {
+		var name,
+			i = 0,
+			length = obj.length,
+			isObj = length === undefined || jQuery.isFunction( obj );
+
+		if ( args ) {
+			if ( isObj ) {
+				for ( name in obj ) {
+					if ( callback.apply( obj[ name ], args ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.apply( obj[ i++ ], args ) === false ) {
+						break;
+					}
+				}
+			}
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( isObj ) {
+				for ( name in obj ) {
+					if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) {
+						break;
+					}
+				}
+			}
+		}
+
+		return obj;
+	},
+
+	// Use native String.trim function wherever possible
+	trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+		function( text ) {
+			return text == null ?
+				"" :
+				core_trim.call( text );
+		} :
+
+		// Otherwise use our own trimming functionality
+		function( text ) {
+			return text == null ?
+				"" :
+				( text + "" ).replace( rtrim, "" );
+		},
+
+	// results is for internal usage only
+	makeArray: function( arr, results ) {
+		var type,
+			ret = results || [];
+
+		if ( arr != null ) {
+			// The window, strings (and functions) also have 'length'
+			// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+			type = jQuery.type( arr );
+
+			if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) {
+				core_push.call( ret, arr );
+			} else {
+				jQuery.merge( ret, arr );
+			}
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, arr, i ) {
+		var len;
+
+		if ( arr ) {
+			if ( core_indexOf ) {
+				return core_indexOf.call( arr, elem, i );
+			}
+
+			len = arr.length;
+			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+			for ( ; i < len; i++ ) {
+				// Skip accessing in sparse arrays
+				if ( i in arr && arr[ i ] === elem ) {
+					return i;
+				}
+			}
+		}
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		var l = second.length,
+			i = first.length,
+			j = 0;
+
+		if ( typeof l === "number" ) {
+			for ( ; j < l; j++ ) {
+				first[ i++ ] = second[ j ];
+			}
+
+		} else {
+			while ( second[j] !== undefined ) {
+				first[ i++ ] = second[ j++ ];
+			}
+		}
+
+		first.length = i;
+
+		return first;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var retVal,
+			ret = [],
+			i = 0,
+			length = elems.length;
+		inv = !!inv;
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( ; i < length; i++ ) {
+			retVal = !!callback( elems[ i ], i );
+			if ( inv !== retVal ) {
+				ret.push( elems[ i ] );
+			}
+		}
+
+		return ret;
+	},
+
+	// arg is for internal usage only
+	map: function( elems, callback, arg ) {
+		var value, key,
+			ret = [],
+			i = 0,
+			length = elems.length,
+			// jquery objects are treated as arrays
+			isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
+
+		// Go through the array, translating each of the items to their
+		if ( isArray ) {
+			for ( ; i < length; i++ ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+
+		// Go through every key on the object,
+		} else {
+			for ( key in elems ) {
+				value = callback( elems[ key ], key, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+		}
+
+		// Flatten any nested arrays
+		return ret.concat.apply( [], ret );
+	},
+
+	// A global GUID counter for objects
+	guid: 1,
+
+	// Bind a function to a context, optionally partially applying any
+	// arguments.
+	proxy: function( fn, context ) {
+		var tmp, args, proxy;
+
+		if ( typeof context === "string" ) {
+			tmp = fn[ context ];
+			context = fn;
+			fn = tmp;
+		}
+
+		// Quick check to determine if target is callable, in the spec
+		// this throws a TypeError, but we will just return undefined.
+		if ( !jQuery.isFunction( fn ) ) {
+			return undefined;
+		}
+
+		// Simulated bind
+		args = core_slice.call( arguments, 2 );
+		proxy = function() {
+			return fn.apply( context, args.concat( core_slice.call( arguments ) ) );
+		};
+
+		// Set the guid of unique handler to the same of original handler, so it can be removed
+		proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+		return proxy;
+	},
+
+	// Multifunctional method to get and set values of a collection
+	// The value/s can optionally be executed if it's a function
+	access: function( elems, fn, key, value, chainable, emptyGet, pass ) {
+		var exec,
+			bulk = key == null,
+			i = 0,
+			length = elems.length;
+
+		// Sets many values
+		if ( key && typeof key === "object" ) {
+			for ( i in key ) {
+				jQuery.access( elems, fn, i, key[i], 1, emptyGet, value );
+			}
+			chainable = 1;
+
+		// Sets one value
+		} else if ( value !== undefined ) {
+			// Optionally, function values get executed if exec is true
+			exec = pass === undefined && jQuery.isFunction( value );
+
+			if ( bulk ) {
+				// Bulk operations only iterate when executing function values
+				if ( exec ) {
+					exec = fn;
+					fn = function( elem, key, value ) {
+						return exec.call( jQuery( elem ), value );
+					};
+
+				// Otherwise they run against the entire set
+				} else {
+					fn.call( elems, value );
+					fn = null;
+				}
+			}
+
+			if ( fn ) {
+				for (; i < length; i++ ) {
+					fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+				}
+			}
+
+			chainable = 1;
+		}
+
+		return chainable ?
+			elems :
+
+			// Gets
+			bulk ?
+				fn.call( elems ) :
+				length ? fn( elems[0], key ) : emptyGet;
+	},
+
+	now: function() {
+		return ( new Date() ).getTime();
+	}
+});
+
+jQuery.ready.promise = function( obj ) {
+	if ( !readyList ) {
+
+		readyList = jQuery.Deferred();
+
+		// Catch cases where $(document).ready() is called after the browser event has already occurred.
+		// we once tried to use readyState "interactive" here, but it caused issues like the one
+		// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+		if ( document.readyState === "complete" ) {
+			// Handle it asynchronously to allow scripts the opportunity to delay ready
+			setTimeout( jQuery.ready, 1 );
+
+		// Standards-based browsers support DOMContentLoaded
+		} else if ( document.addEventListener ) {
+			// Use the handy event callback
+			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+			// A fallback to window.onload, that will always work
+			window.addEventListener( "load", jQuery.ready, false );
+
+		// If IE event model is used
+		} else {
+			// Ensure firing before onload, maybe late but safe also for iframes
+			document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+			// A fallback to window.onload, that will always work
+			window.attachEvent( "onload", jQuery.ready );
+
+			// If IE and not a frame
+			// continually check to see if the document is ready
+			var top = false;
+
+			try {
+				top = window.frameElement == null && document.documentElement;
+			} catch(e) {}
+
+			if ( top && top.doScroll ) {
+				(function doScrollCheck() {
+					if ( !jQuery.isReady ) {
+
+						try {
+							// Use the trick by Diego Perini
+							// http://javascript.nwbox.com/IEContentLoaded/
+							top.doScroll("left");
+						} catch(e) {
+							return setTimeout( doScrollCheck, 50 );
+						}
+
+						// and execute any waiting functions
+						jQuery.ready();
+					}
+				})();
+			}
+		}
+	}
+	return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+	class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+	var object = optionsCache[ options ] = {};
+	jQuery.each( options.split( core_rspace ), function( _, flag ) {
+		object[ flag ] = true;
+	});
+	return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *	options: an optional list of space-separated options that will change how
+ *			the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *	once:			will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *	memory:			will keep track of previous values and will call any callback added
+ *					after the list has been fired right away with the latest "memorized"
+ *					values (like a Deferred)
+ *
+ *	unique:			will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *	stopOnFalse:	interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+	// Convert options from String-formatted to Object-formatted if needed
+	// (we check in cache first)
+	options = typeof options === "string" ?
+		( optionsCache[ options ] || createOptions( options ) ) :
+		jQuery.extend( {}, options );
+
+	var // Last fire value (for non-forgettable lists)
+		memory,
+		// Flag to know if list was already fired
+		fired,
+		// Flag to know if list is currently firing
+		firing,
+		// First callback to fire (used internally by add and fireWith)
+		firingStart,
+		// End of the loop when firing
+		firingLength,
+		// Index of currently firing callback (modified by remove if needed)
+		firingIndex,
+		// Actual callback list
+		list = [],
+		// Stack of fire calls for repeatable lists
+		stack = !options.once && [],
+		// Fire callbacks
+		fire = function( data ) {
+			memory = options.memory && data;
+			fired = true;
+			firingIndex = firingStart || 0;
+			firingStart = 0;
+			firingLength = list.length;
+			firing = true;
+			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+					memory = false; // To prevent further calls using add
+					break;
+				}
+			}
+			firing = false;
+			if ( list ) {
+				if ( stack ) {
+					if ( stack.length ) {
+						fire( stack.shift() );
+					}
+				} else if ( memory ) {
+					list = [];
+				} else {
+					self.disable();
+				}
+			}
+		},
+		// Actual Callbacks object
+		self = {
+			// Add a callback or a collection of callbacks to the list
+			add: function() {
+				if ( list ) {
+					// First, we save the current length
+					var start = list.length;
+					(function add( args ) {
+						jQuery.each( args, function( _, arg ) {
+							var type = jQuery.type( arg );
+							if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) {
+								list.push( arg );
+							} else if ( arg && arg.length && type !== "string" ) {
+								// Inspect recursively
+								add( arg );
+							}
+						});
+					})( arguments );
+					// Do we need to add the callbacks to the
+					// current firing batch?
+					if ( firing ) {
+						firingLength = list.length;
+					// With memory, if we're not firing then
+					// we should call right away
+					} else if ( memory ) {
+						firingStart = start;
+						fire( memory );
+					}
+				}
+				return this;
+			},
+			// Remove a callback from the list
+			remove: function() {
+				if ( list ) {
+					jQuery.each( arguments, function( _, arg ) {
+						var index;
+						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+							list.splice( index, 1 );
+							// Handle firing indexes
+							if ( firing ) {
+								if ( index <= firingLength ) {
+									firingLength--;
+								}
+								if ( index <= firingIndex ) {
+									firingIndex--;
+								}
+							}
+						}
+					});
+				}
+				return this;
+			},
+			// Control if a given callback is in the list
+			has: function( fn ) {
+				return jQuery.inArray( fn, list ) > -1;
+			},
+			// Remove all callbacks from the list
+			empty: function() {
+				list = [];
+				return this;
+			},
+			// Have the list do nothing anymore
+			disable: function() {
+				list = stack = memory = undefined;
+				return this;
+			},
+			// Is it disabled?
+			disabled: function() {
+				return !list;
+			},
+			// Lock the list in its current state
+			lock: function() {
+				stack = undefined;
+				if ( !memory ) {
+					self.disable();
+				}
+				return this;
+			},
+			// Is it locked?
+			locked: function() {
+				return !stack;
+			},
+			// Call all callbacks with the given context and arguments
+			fireWith: function( context, args ) {
+				args = args || [];
+				args = [ context, args.slice ? args.slice() : args ];
+				if ( list && ( !fired || stack ) ) {
+					if ( firing ) {
+						stack.push( args );
+					} else {
+						fire( args );
+					}
+				}
+				return this;
+			},
+			// Call all the callbacks with the given arguments
+			fire: function() {
+				self.fireWith( this, arguments );
+				return this;
+			},
+			// To know if the callbacks have already been called at least once
+			fired: function() {
+				return !!fired;
+			}
+		};
+
+	return self;
+};
+jQuery.extend({
+
+	Deferred: function( func ) {
+		var tuples = [
+				// action, add listener, listener list, final state
+				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+				[ "notify", "progress", jQuery.Callbacks("memory") ]
+			],
+			state = "pending",
+			promise = {
+				state: function() {
+					return state;
+				},
+				always: function() {
+					deferred.done( arguments ).fail( arguments );
+					return this;
+				},
+				then: function( /* fnDone, fnFail, fnProgress */ ) {
+					var fns = arguments;
+					return jQuery.Deferred(function( newDefer ) {
+						jQuery.each( tuples, function( i, tuple ) {
+							var action = tuple[ 0 ],
+								fn = fns[ i ];
+							// deferred[ done | fail | progress ] for forwarding actions to newDefer
+							deferred[ tuple[1] ]( jQuery.isFunction( fn ) ?
+								function() {
+									var returned = fn.apply( this, arguments );
+									if ( returned && jQuery.isFunction( returned.promise ) ) {
+										returned.promise()
+											.done( newDefer.resolve )
+											.fail( newDefer.reject )
+											.progress( newDefer.notify );
+									} else {
+										newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
+									}
+								} :
+								newDefer[ action ]
+							);
+						});
+						fns = null;
+					}).promise();
+				},
+				// Get a promise for this deferred
+				// If obj is provided, the promise aspect is added to the object
+				promise: function( obj ) {
+					return obj != null ? jQuery.extend( obj, promise ) : promise;
+				}
+			},
+			deferred = {};
+
+		// Keep pipe for back-compat
+		promise.pipe = promise.then;
+
+		// Add list-specific methods
+		jQuery.each( tuples, function( i, tuple ) {
+			var list = tuple[ 2 ],
+				stateString = tuple[ 3 ];
+
+			// promise[ done | fail | progress ] = list.add
+			promise[ tuple[1] ] = list.add;
+
+			// Handle state
+			if ( stateString ) {
+				list.add(function() {
+					// state = [ resolved | rejected ]
+					state = stateString;
+
+				// [ reject_list | resolve_list ].disable; progress_list.lock
+				}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+			}
+
+			// deferred[ resolve | reject | notify ] = list.fire
+			deferred[ tuple[0] ] = list.fire;
+			deferred[ tuple[0] + "With" ] = list.fireWith;
+		});
+
+		// Make the deferred a promise
+		promise.promise( deferred );
+
+		// Call given func if any
+		if ( func ) {
+			func.call( deferred, deferred );
+		}
+
+		// All done!
+		return deferred;
+	},
+
+	// Deferred helper
+	when: function( subordinate /* , ..., subordinateN */ ) {
+		var i = 0,
+			resolveValues = core_slice.call( arguments ),
+			length = resolveValues.length,
+
+			// the count of uncompleted subordinates
+			remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+			// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+			deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+			// Update function for both resolve and progress values
+			updateFunc = function( i, contexts, values ) {
+				return function( value ) {
+					contexts[ i ] = this;
+					values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+					if( values === progressValues ) {
+						deferred.notifyWith( contexts, values );
+					} else if ( !( --remaining ) ) {
+						deferred.resolveWith( contexts, values );
+					}
+				};
+			},
+
+			progressValues, progressContexts, resolveContexts;
+
+		// add listeners to Deferred subordinates; treat others as resolved
+		if ( length > 1 ) {
+			progressValues = new Array( length );
+			progressContexts = new Array( length );
+			resolveContexts = new Array( length );
+			for ( ; i < length; i++ ) {
+				if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+					resolveValues[ i ].promise()
+						.done( updateFunc( i, resolveContexts, resolveValues ) )
+						.fail( deferred.reject )
+						.progress( updateFunc( i, progressContexts, progressValues ) );
+				} else {
+					--remaining;
+				}
+			}
+		}
+
+		// if we're not waiting on anything, resolve the master
+		if ( !remaining ) {
+			deferred.resolveWith( resolveContexts, resolveValues );
+		}
+
+		return deferred.promise();
+	}
+});
+jQuery.support = (function() {
+
+	var support,
+		all,
+		a,
+		select,
+		opt,
+		input,
+		fragment,
+		eventName,
+		i,
+		isSupported,
+		clickFn,
+		div = document.createElement("div");
+
+	// Preliminary tests
+	div.setAttribute( "className", "t" );
+	div.innerHTML = "  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+	all = div.getElementsByTagName("*");
+	a = div.getElementsByTagName("a")[ 0 ];
+	a.style.cssText = "top:1px;float:left;opacity:.5";
+
+	// Can't get basic test support
+	if ( !all || !all.length ) {
+		return {};
+	}
+
+	// First batch of supports tests
+	select = document.createElement("select");
+	opt = select.appendChild( document.createElement("option") );
+	input = div.getElementsByTagName("input")[ 0 ];
+
+	support = {
+		// IE strips leading whitespace when .innerHTML is used
+		leadingWhitespace: ( div.firstChild.nodeType === 3 ),
+
+		// Make sure that tbody elements aren't automatically inserted
+		// IE will insert them into empty tables
+		tbody: !div.getElementsByTagName("tbody").length,
+
+		// Make sure that link elements get serialized correctly by innerHTML
+		// This requires a wrapper element in IE
+		htmlSerialize: !!div.getElementsByTagName("link").length,
+
+		// Get the style information from getAttribute
+		// (IE uses .cssText instead)
+		style: /top/.test( a.getAttribute("style") ),
+
+		// Make sure that URLs aren't manipulated
+		// (IE normalizes it by default)
+		hrefNormalized: ( a.getAttribute("href") === "/a" ),
+
+		// Make sure that element opacity exists
+		// (IE uses filter instead)
+		// Use a regex to work around a WebKit issue. See #5145
+		opacity: /^0.5/.test( a.style.opacity ),
+
+		// Verify style float existence
+		// (IE uses styleFloat instead of cssFloat)
+		cssFloat: !!a.style.cssFloat,
+
+		// Make sure that if no value is specified for a checkbox
+		// that it defaults to "on".
+		// (WebKit defaults to "" instead)
+		checkOn: ( input.value === "on" ),
+
+		// Make sure that a selected-by-default option has a working selected property.
+		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+		optSelected: opt.selected,
+
+		// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+		getSetAttribute: div.className !== "t",
+
+		// Tests for enctype support on a form(#6743)
+		enctype: !!document.createElement("form").enctype,
+
+		// Makes sure cloning an html5 element does not cause problems
+		// Where outerHTML is undefined, this still works
+		html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
+
+		// jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
+		boxModel: ( document.compatMode === "CSS1Compat" ),
+
+		// Will be defined later
+		submitBubbles: true,
+		changeBubbles: true,
+		focusinBubbles: false,
+		deleteExpando: true,
+		noCloneEvent: true,
+		inlineBlockNeedsLayout: false,
+		shrinkWrapBlocks: false,
+		reliableMarginRight: true,
+		boxSizingReliable: true,
+		pixelPosition: false
+	};
+
+	// Make sure checked status is properly cloned
+	input.checked = true;
+	support.noCloneChecked = input.cloneNode( true ).checked;
+
+	// Make sure that the options inside disabled selects aren't marked as disabled
+	// (WebKit marks them as disabled)
+	select.disabled = true;
+	support.optDisabled = !opt.disabled;
+
+	// Test to see if it's possible to delete an expando from an element
+	// Fails in Internet Explorer
+	try {
+		delete div.test;
+	} catch( e ) {
+		support.deleteExpando = false;
+	}
+
+	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
+		div.attachEvent( "onclick", clickFn = function() {
+			// Cloning a node shouldn't copy over any
+			// bound event handlers (IE does this)
+			support.noCloneEvent = false;
+		});
+		div.cloneNode( true ).fireEvent("onclick");
+		div.detachEvent( "onclick", clickFn );
+	}
+
+	// Check if a radio maintains its value
+	// after being appended to the DOM
+	input = document.createElement("input");
+	input.value = "t";
+	input.setAttribute( "type", "radio" );
+	support.radioValue = input.value === "t";
+
+	input.setAttribute( "checked", "checked" );
+
+	// #11217 - WebKit loses check when the name is after the checked attribute
+	input.setAttribute( "name", "t" );
+
+	div.appendChild( input );
+	fragment = document.createDocumentFragment();
+	fragment.appendChild( div.lastChild );
+
+	// WebKit doesn't clone checked state correctly in fragments
+	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+	// Check if a disconnected checkbox will retain its checked
+	// value of true after appended to the DOM (IE6/7)
+	support.appendChecked = input.checked;
+
+	fragment.removeChild( input );
+	fragment.appendChild( div );
+
+	// Technique from Juriy Zaytsev
+	// http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
+	// We only care about the case where non-standard event systems
+	// are used, namely in IE. Short-circuiting here helps us to
+	// avoid an eval call (in setAttribute) which can cause CSP
+	// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
+	if ( div.attachEvent ) {
+		for ( i in {
+			submit: true,
+			change: true,
+			focusin: true
+		}) {
+			eventName = "on" + i;
+			isSupported = ( eventName in div );
+			if ( !isSupported ) {
+				div.setAttribute( eventName, "return;" );
+				isSupported = ( typeof div[ eventName ] === "function" );
+			}
+			support[ i + "Bubbles" ] = isSupported;
+		}
+	}
+
+	// Run tests that need a body at doc ready
+	jQuery(function() {
+		var container, div, tds, marginDiv,
+			divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;",
+			body = document.getElementsByTagName("body")[0];
+
+		if ( !body ) {
+			// Return for frameset docs that don't have a body
+			return;
+		}
+
+		container = document.createElement("div");
+		container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px";
+		body.insertBefore( container, body.firstChild );
+
+		// Construct the test element
+		div = document.createElement("div");
+		container.appendChild( div );
+
+		// Check if table cells still have offsetWidth/Height when they are set
+		// to display:none and there are still other visible table cells in a
+		// table row; if so, offsetWidth/Height are not reliable for use when
+		// determining if an element has been hidden directly using
+		// display:none (it is still safe to use offsets if a parent element is
+		// hidden; don safety goggles and see bug #4512 for more information).
+		// (only IE 8 fails this test)
+		div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+		tds = div.getElementsByTagName("td");
+		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+		isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+		tds[ 0 ].style.display = "";
+		tds[ 1 ].style.display = "none";
+
+		// Check if empty table cells still have offsetWidth/Height
+		// (IE <= 8 fail this test)
+		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+		// Check box-sizing and margin behavior
+		div.innerHTML = "";
+		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+		support.boxSizing = ( div.offsetWidth === 4 );
+		support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );
+
+		// NOTE: To any future maintainer, we've window.getComputedStyle
+		// because jsdom on node.js will break without it.
+		if ( window.getComputedStyle ) {
+			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+			// Check if div with explicit width and no margin-right incorrectly
+			// gets computed margin-right based on width of container. For more
+			// info see bug #3333
+			// Fails in WebKit before Feb 2011 nightlies
+			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+			marginDiv = document.createElement("div");
+			marginDiv.style.cssText = div.style.cssText = divReset;
+			marginDiv.style.marginRight = marginDiv.style.width = "0";
+			div.style.width = "1px";
+			div.appendChild( marginDiv );
+			support.reliableMarginRight =
+				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+		}
+
+		if ( typeof div.style.zoom !== "undefined" ) {
+			// Check if natively block-level elements act like inline-block
+			// elements when setting their display to 'inline' and giving
+			// them layout
+			// (IE < 8 does this)
+			div.innerHTML = "";
+			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+			// Check if elements with layout shrink-wrap their children
+			// (IE 6 does this)
+			div.style.display = "block";
+			div.style.overflow = "visible";
+			div.innerHTML = "<div></div>";
+			div.firstChild.style.width = "5px";
+			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+			container.style.zoom = 1;
+		}
+
+		// Null elements to avoid leaks in IE
+		body.removeChild( container );
+		container = div = tds = marginDiv = null;
+	});
+
+	// Null elements to avoid leaks in IE
+	fragment.removeChild( div );
+	all = a = select = opt = input = fragment = div = null;
+
+	return support;
+})();
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+	rmultiDash = /([A-Z])/g;
+
+jQuery.extend({
+	cache: {},
+
+	deletedIds: [],
+
+	// Remove at next major release (1.9/2.0)
+	uuid: 0,
+
+	// Unique for each copy of jQuery on the page
+	// Non-digits removed to match rinlinejQuery
+	expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
+
+	// The following elements throw uncatchable exceptions if you
+	// attempt to add expando properties to them.
+	noData: {
+		"embed": true,
+		// Ban all objects except for Flash (which handle expandos)
+		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+		"applet": true
+	},
+
+	hasData: function( elem ) {
+		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+		return !!elem && !isEmptyDataObject( elem );
+	},
+
+	data: function( elem, name, data, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var thisCache, ret,
+			internalKey = jQuery.expando,
+			getByName = typeof name === "string",
+
+			// We have to handle DOM nodes and JS objects differently because IE6-7
+			// can't GC object references properly across the DOM-JS boundary
+			isNode = elem.nodeType,
+
+			// Only DOM nodes need the global jQuery cache; JS object data is
+			// attached directly to the object so GC can occur automatically
+			cache = isNode ? jQuery.cache : elem,
+
+			// Only defining an ID for JS objects if its cache already exists allows
+			// the code to shortcut on the same path as a DOM node with no cache
+			id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+		// Avoid doing any more work than we need to when trying to get data on an
+		// object that has no data at all
+		if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
+			return;
+		}
+
+		if ( !id ) {
+			// Only DOM nodes need a new unique ID for each element since their data
+			// ends up in the global cache
+			if ( isNode ) {
+				elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++;
+			} else {
+				id = internalKey;
+			}
+		}
+
+		if ( !cache[ id ] ) {
+			cache[ id ] = {};
+
+			// Avoids exposing jQuery metadata on plain JS objects when the object
+			// is serialized using JSON.stringify
+			if ( !isNode ) {
+				cache[ id ].toJSON = jQuery.noop;
+			}
+		}
+
+		// An object can be passed to jQuery.data instead of a key/value pair; this gets
+		// shallow copied over onto the existing cache
+		if ( typeof name === "object" || typeof name === "function" ) {
+			if ( pvt ) {
+				cache[ id ] = jQuery.extend( cache[ id ], name );
+			} else {
+				cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+			}
+		}
+
+		thisCache = cache[ id ];
+
+		// jQuery data() is stored in a separate object inside the object's internal data
+		// cache in order to avoid key collisions between internal data and user-defined
+		// data.
+		if ( !pvt ) {
+			if ( !thisCache.data ) {
+				thisCache.data = {};
+			}
+
+			thisCache = thisCache.data;
+		}
+
+		if ( data !== undefined ) {
+			thisCache[ jQuery.camelCase( name ) ] = data;
+		}
+
+		// Check for both converted-to-camel and non-converted data property names
+		// If a data property was specified
+		if ( getByName ) {
+
+			// First Try to find as-is property data
+			ret = thisCache[ name ];
+
+			// Test for null|undefined property data
+			if ( ret == null ) {
+
+				// Try to find the camelCased property
+				ret = thisCache[ jQuery.camelCase( name ) ];
+			}
+		} else {
+			ret = thisCache;
+		}
+
+		return ret;
+	},
+
+	removeData: function( elem, name, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var thisCache, i, l,
+
+			isNode = elem.nodeType,
+
+			// See jQuery.data for more information
+			cache = isNode ? jQuery.cache : elem,
+			id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+		// If there is already no cache entry for this object, there is no
+		// purpose in continuing
+		if ( !cache[ id ] ) {
+			return;
+		}
+
+		if ( name ) {
+
+			thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+			if ( thisCache ) {
+
+				// Support array or space separated string names for data keys
+				if ( !jQuery.isArray( name ) ) {
+
+					// try the string as a key before any manipulation
+					if ( name in thisCache ) {
+						name = [ name ];
+					} else {
+
+						// split the camel cased version by spaces unless a key with the spaces exists
+						name = jQuery.camelCase( name );
+						if ( name in thisCache ) {
+							name = [ name ];
+						} else {
+							name = name.split(" ");
+						}
+					}
+				}
+
+				for ( i = 0, l = name.length; i < l; i++ ) {
+					delete thisCache[ name[i] ];
+				}
+
+				// If there is no data left in the cache, we want to continue
+				// and let the cache object itself get destroyed
+				if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
+					return;
+				}
+			}
+		}
+
+		// See jQuery.data for more information
+		if ( !pvt ) {
+			delete cache[ id ].data;
+
+			// Don't destroy the parent cache unless the internal data object
+			// had been the only thing left in it
+			if ( !isEmptyDataObject( cache[ id ] ) ) {
+				return;
+			}
+		}
+
+		// Destroy the cache
+		if ( isNode ) {
+			jQuery.cleanData( [ elem ], true );
+
+		// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+		} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+			delete cache[ id ];
+
+		// When all else fails, null
+		} else {
+			cache[ id ] = null;
+		}
+	},
+
+	// For internal use only.
+	_data: function( elem, name, data ) {
+		return jQuery.data( elem, name, data, true );
+	},
+
+	// A method for determining if a DOM node can handle the data expando
+	acceptData: function( elem ) {
+		var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+		// nodes accept data unless otherwise specified; rejection can be conditional
+		return !noData || noData !== true && elem.getAttribute("classid") === noData;
+	}
+});
+
+jQuery.fn.extend({
+	data: function( key, value ) {
+		var parts, part, attr, name, l,
+			elem = this[0],
+			i = 0,
+			data = null;
+
+		// Gets all values
+		if ( key === undefined ) {
+			if ( this.length ) {
+				data = jQuery.data( elem );
+
+				if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+					attr = elem.attributes;
+					for ( l = attr.length; i < l; i++ ) {
+						name = attr[i].name;
+
+						if ( !name.indexOf( "data-" ) ) {
+							name = jQuery.camelCase( name.substring(5) );
+
+							dataAttr( elem, name, data[ name ] );
+						}
+					}
+					jQuery._data( elem, "parsedAttrs", true );
+				}
+			}
+
+			return data;
+		}
+
+		// Sets multiple values
+		if ( typeof key === "object" ) {
+			return this.each(function() {
+				jQuery.data( this, key );
+			});
+		}
+
+		parts = key.split( ".", 2 );
+		parts[1] = parts[1] ? "." + parts[1] : "";
+		part = parts[1] + "!";
+
+		return jQuery.access( this, function( value ) {
+
+			if ( value === undefined ) {
+				data = this.triggerHandler( "getData" + part, [ parts[0] ] );
+
+				// Try to fetch any internally stored data first
+				if ( data === undefined && elem ) {
+					data = jQuery.data( elem, key );
+					data = dataAttr( elem, key, data );
+				}
+
+				return data === undefined && parts[1] ?
+					this.data( parts[0] ) :
+					data;
+			}
+
+			parts[1] = value;
+			this.each(function() {
+				var self = jQuery( this );
+
+				self.triggerHandler( "setData" + part, parts );
+				jQuery.data( this, key, value );
+				self.triggerHandler( "changeData" + part, parts );
+			});
+		}, null, value, arguments.length > 1, null, false );
+	},
+
+	removeData: function( key ) {
+		return this.each(function() {
+			jQuery.removeData( this, key );
+		});
+	}
+});
+
+function dataAttr( elem, key, data ) {
+	// If nothing was found internally, try to fetch any
+	// data from the HTML5 data-* attribute
+	if ( data === undefined && elem.nodeType === 1 ) {
+
+		var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+		data = elem.getAttribute( name );
+
+		if ( typeof data === "string" ) {
+			try {
+				data = data === "true" ? true :
+				data === "false" ? false :
+				data === "null" ? null :
+				// Only convert to a number if it doesn't change the string
+				+data + "" === data ? +data :
+				rbrace.test( data ) ? jQuery.parseJSON( data ) :
+					data;
+			} catch( e ) {}
+
+			// Make sure we set the data so it isn't changed later
+			jQuery.data( elem, key, data );
+
+		} else {
+			data = undefined;
+		}
+	}
+
+	return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+	var name;
+	for ( name in obj ) {
+
+		// if the public data object is empty, the private is still empty
+		if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+			continue;
+		}
+		if ( name !== "toJSON" ) {
+			return false;
+		}
+	}
+
+	return true;
+}
+jQuery.extend({
+	queue: function( elem, type, data ) {
+		var queue;
+
+		if ( elem ) {
+			type = ( type || "fx" ) + "queue";
+			queue = jQuery._data( elem, type );
+
+			// Speed up dequeue by getting out quickly if this is just a lookup
+			if ( data ) {
+				if ( !queue || jQuery.isArray(data) ) {
+					queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+				} else {
+					queue.push( data );
+				}
+			}
+			return queue || [];
+		}
+	},
+
+	dequeue: function( elem, type ) {
+		type = type || "fx";
+
+		var queue = jQuery.queue( elem, type ),
+			startLength = queue.length,
+			fn = queue.shift(),
+			hooks = jQuery._queueHooks( elem, type ),
+			next = function() {
+				jQuery.dequeue( elem, type );
+			};
+
+		// If the fx queue is dequeued, always remove the progress sentinel
+		if ( fn === "inprogress" ) {
+			fn = queue.shift();
+			startLength--;
+		}
+
+		if ( fn ) {
+
+			// Add a progress sentinel to prevent the fx queue from being
+			// automatically dequeued
+			if ( type === "fx" ) {
+				queue.unshift( "inprogress" );
+			}
+
+			// clear up the last queue stop function
+			delete hooks.stop;
+			fn.call( elem, next, hooks );
+		}
+
+		if ( !startLength && hooks ) {
+			hooks.empty.fire();
+		}
+	},
+
+	// not intended for public consumption - generates a queueHooks object, or returns the current one
+	_queueHooks: function( elem, type ) {
+		var key = type + "queueHooks";
+		return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+			empty: jQuery.Callbacks("once memory").add(function() {
+				jQuery.removeData( elem, type + "queue", true );
+				jQuery.removeData( elem, key, true );
+			})
+		});
+	}
+});
+
+jQuery.fn.extend({
+	queue: function( type, data ) {
+		var setter = 2;
+
+		if ( typeof type !== "string" ) {
+			data = type;
+			type = "fx";
+			setter--;
+		}
+
+		if ( arguments.length < setter ) {
+			return jQuery.queue( this[0], type );
+		}
+
+		return data === undefined ?
+			this :
+			this.each(function() {
+				var queue = jQuery.queue( this, type, data );
+
+				// ensure a hooks for this queue
+				jQuery._queueHooks( this, type );
+
+				if ( type === "fx" && queue[0] !== "inprogress" ) {
+					jQuery.dequeue( this, type );
+				}
+			});
+	},
+	dequeue: function( type ) {
+		return this.each(function() {
+			jQuery.dequeue( this, type );
+		});
+	},
+	// Based off of the plugin by Clint Helfers, with permission.
+	// http://blindsignals.com/index.php/2009/07/jquery-delay/
+	delay: function( time, type ) {
+		time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+		type = type || "fx";
+
+		return this.queue( type, function( next, hooks ) {
+			var timeout = setTimeout( next, time );
+			hooks.stop = function() {
+				clearTimeout( timeout );
+			};
+		});
+	},
+	clearQueue: function( type ) {
+		return this.queue( type || "fx", [] );
+	},
+	// Get a promise resolved when queues of a certain type
+	// are emptied (fx is the type by default)
+	promise: function( type, obj ) {
+		var tmp,
+			count = 1,
+			defer = jQuery.Deferred(),
+			elements = this,
+			i = this.length,
+			resolve = function() {
+				if ( !( --count ) ) {
+					defer.resolveWith( elements, [ elements ] );
+				}
+			};
+
+		if ( typeof type !== "string" ) {
+			obj = type;
+			type = undefined;
+		}
+		type = type || "fx";
+
+		while( i-- ) {
+			tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+			if ( tmp && tmp.empty ) {
+				count++;
+				tmp.empty.add( resolve );
+			}
+		}
+		resolve();
+		return defer.promise( obj );
+	}
+});
+var nodeHook, boolHook, fixSpecified,
+	rclass = /[\t\r\n]/g,
+	rreturn = /\r/g,
+	rtype = /^(?:button|input)$/i,
+	rfocusable = /^(?:button|input|object|select|textarea)$/i,
+	rclickable = /^a(?:rea|)$/i,
+	rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
+	getSetAttribute = jQuery.support.getSetAttribute;
+
+jQuery.fn.extend({
+	attr: function( name, value ) {
+		return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+	},
+
+	removeAttr: function( name ) {
+		return this.each(function() {
+			jQuery.removeAttr( this, name );
+		});
+	},
+
+	prop: function( name, value ) {
+		return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+	},
+
+	removeProp: function( name ) {
+		name = jQuery.propFix[ name ] || name;
+		return this.each(function() {
+			// try/catch handles cases where IE balks (such as removing a property on window)
+			try {
+				this[ name ] = undefined;
+				delete this[ name ];
+			} catch( e ) {}
+		});
+	},
+
+	addClass: function( value ) {
+		var classNames, i, l, elem,
+			setClass, c, cl;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).addClass( value.call(this, j, this.className) );
+			});
+		}
+
+		if ( value && typeof value === "string" ) {
+			classNames = value.split( core_rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.nodeType === 1 ) {
+					if ( !elem.className && classNames.length === 1 ) {
+						elem.className = value;
+
+					} else {
+						setClass = " " + elem.className + " ";
+
+						for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+							if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) {
+								setClass += classNames[ c ] + " ";
+							}
+						}
+						elem.className = jQuery.trim( setClass );
+					}
+				}
+			}
+		}
+
+		return this;
+	},
+
+	removeClass: function( value ) {
+		var removes, className, elem, c, cl, i, l;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).removeClass( value.call(this, j, this.className) );
+			});
+		}
+		if ( (value && typeof value === "string") || value === undefined ) {
+			removes = ( value || "" ).split( core_rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+				if ( elem.nodeType === 1 && elem.className ) {
+
+					className = (" " + elem.className + " ").replace( rclass, " " );
+
+					// loop over each item in the removal list
+					for ( c = 0, cl = removes.length; c < cl; c++ ) {
+						// Remove until there is nothing to remove,
+						while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) {
+							className = className.replace( " " + removes[ c ] + " " , " " );
+						}
+					}
+					elem.className = value ? jQuery.trim( className ) : "";
+				}
+			}
+		}
+
+		return this;
+	},
+
+	toggleClass: function( value, stateVal ) {
+		var type = typeof value,
+			isBool = typeof stateVal === "boolean";
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( i ) {
+				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+			});
+		}
+
+		return this.each(function() {
+			if ( type === "string" ) {
+				// toggle individual class names
+				var className,
+					i = 0,
+					self = jQuery( this ),
+					state = stateVal,
+					classNames = value.split( core_rspace );
+
+				while ( (className = classNames[ i++ ]) ) {
+					// check each className given, space separated list
+					state = isBool ? state : !self.hasClass( className );
+					self[ state ? "addClass" : "removeClass" ]( className );
+				}
+
+			} else if ( type === "undefined" || type === "boolean" ) {
+				if ( this.className ) {
+					// store className if set
+					jQuery._data( this, "__className__", this.className );
+				}
+
+				// toggle whole className
+				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+			}
+		});
+	},
+
+	hasClass: function( selector ) {
+		var className = " " + selector + " ",
+			i = 0,
+			l = this.length;
+		for ( ; i < l; i++ ) {
+			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+				return true;
+			}
+		}
+
+		return false;
+	},
+
+	val: function( value ) {
+		var hooks, ret, isFunction,
+			elem = this[0];
+
+		if ( !arguments.length ) {
+			if ( elem ) {
+				hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+					return ret;
+				}
+
+				ret = elem.value;
+
+				return typeof ret === "string" ?
+					// handle most common string cases
+					ret.replace(rreturn, "") :
+					// handle cases where value is null/undef or number
+					ret == null ? "" : ret;
+			}
+
+			return;
+		}
+
+		isFunction = jQuery.isFunction( value );
+
+		return this.each(function( i ) {
+			var val,
+				self = jQuery(this);
+
+			if ( this.nodeType !== 1 ) {
+				return;
+			}
+
+			if ( isFunction ) {
+				val = value.call( this, i, self.val() );
+			} else {
+				val = value;
+			}
+
+			// Treat null/undefined as ""; convert numbers to string
+			if ( val == null ) {
+				val = "";
+			} else if ( typeof val === "number" ) {
+				val += "";
+			} else if ( jQuery.isArray( val ) ) {
+				val = jQuery.map(val, function ( value ) {
+					return value == null ? "" : value + "";
+				});
+			}
+
+			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+			// If set returns undefined, fall back to normal setting
+			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+				this.value = val;
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	valHooks: {
+		option: {
+			get: function( elem ) {
+				// attributes.value is undefined in Blackberry 4.7 but
+				// uses .value. See #6932
+				var val = elem.attributes.value;
+				return !val || val.specified ? elem.value : elem.text;
+			}
+		},
+		select: {
+			get: function( elem ) {
+				var value, i, max, option,
+					index = elem.selectedIndex,
+					values = [],
+					options = elem.options,
+					one = elem.type === "select-one";
+
+				// Nothing was selected
+				if ( index < 0 ) {
+					return null;
+				}
+
+				// Loop through all the selected options
+				i = one ? index : 0;
+				max = one ? index + 1 : options.length;
+				for ( ; i < max; i++ ) {
+					option = options[ i ];
+
+					// Don't return options that are disabled or in a disabled optgroup
+					if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
+							(!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+
+						// Get the specific value for the option
+						value = jQuery( option ).val();
+
+						// We don't need an array for one selects
+						if ( one ) {
+							return value;
+						}
+
+						// Multi-Selects return an array
+						values.push( value );
+					}
+				}
+
+				// Fixes Bug #2551 -- select.val() broken in IE after form.reset()
+				if ( one && !values.length && options.length ) {
+					return jQuery( options[ index ] ).val();
+				}
+
+				return values;
+			},
+
+			set: function( elem, value ) {
+				var values = jQuery.makeArray( value );
+
+				jQuery(elem).find("option").each(function() {
+					this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+				});
+
+				if ( !values.length ) {
+					elem.selectedIndex = -1;
+				}
+				return values;
+			}
+		}
+	},
+
+	// Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9
+	attrFn: {},
+
+	attr: function( elem, name, value, pass ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set attributes on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) {
+			return jQuery( elem )[ name ]( value );
+		}
+
+		// Fallback to prop when attributes are not supported
+		if ( typeof elem.getAttribute === "undefined" ) {
+			return jQuery.prop( elem, name, value );
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		// All attributes are lowercase
+		// Grab necessary hook if one is defined
+		if ( notxml ) {
+			name = name.toLowerCase();
+			hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
+		}
+
+		if ( value !== undefined ) {
+
+			if ( value === null ) {
+				jQuery.removeAttr( elem, name );
+				return;
+
+			} else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				elem.setAttribute( name, value + "" );
+				return value;
+			}
+
+		} else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
+			return ret;
+
+		} else {
+
+			ret = elem.getAttribute( name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return ret === null ?
+				undefined :
+				ret;
+		}
+	},
+
+	removeAttr: function( elem, value ) {
+		var propName, attrNames, name, isBool,
+			i = 0;
+
+		if ( value && elem.nodeType === 1 ) {
+
+			attrNames = value.split( core_rspace );
+
+			for ( ; i < attrNames.length; i++ ) {
+				name = attrNames[ i ];
+
+				if ( name ) {
+					propName = jQuery.propFix[ name ] || name;
+					isBool = rboolean.test( name );
+
+					// See #9699 for explanation of this approach (setting first, then removal)
+					// Do not do this for boolean attributes (see #10870)
+					if ( !isBool ) {
+						jQuery.attr( elem, name, "" );
+					}
+					elem.removeAttribute( getSetAttribute ? name : propName );
+
+					// Set corresponding property to false for boolean attributes
+					if ( isBool && propName in elem ) {
+						elem[ propName ] = false;
+					}
+				}
+			}
+		}
+	},
+
+	attrHooks: {
+		type: {
+			set: function( elem, value ) {
+				// We can't allow the type property to be changed (since it causes problems in IE)
+				if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
+					jQuery.error( "type property can't be changed" );
+				} else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+					// Setting the type on a radio button after the value resets the value in IE6-9
+					// Reset value to it's default in case type is set after value
+					// This is for element creation
+					var val = elem.value;
+					elem.setAttribute( "type", value );
+					if ( val ) {
+						elem.value = val;
+					}
+					return value;
+				}
+			}
+		},
+		// Use the value property for back compat
+		// Use the nodeHook for button elements in IE6/7 (#1954)
+		value: {
+			get: function( elem, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.get( elem, name );
+				}
+				return name in elem ?
+					elem.value :
+					null;
+			},
+			set: function( elem, value, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.set( elem, value, name );
+				}
+				// Does not return so that setAttribute is also used
+				elem.value = value;
+			}
+		}
+	},
+
+	propFix: {
+		tabindex: "tabIndex",
+		readonly: "readOnly",
+		"for": "htmlFor",
+		"class": "className",
+		maxlength: "maxLength",
+		cellspacing: "cellSpacing",
+		cellpadding: "cellPadding",
+		rowspan: "rowSpan",
+		colspan: "colSpan",
+		usemap: "useMap",
+		frameborder: "frameBorder",
+		contenteditable: "contentEditable"
+	},
+
+	prop: function( elem, name, value ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set properties on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		if ( notxml ) {
+			// Fix name and attach hooks
+			name = jQuery.propFix[ name ] || name;
+			hooks = jQuery.propHooks[ name ];
+		}
+
+		if ( value !== undefined ) {
+			if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				return ( elem[ name ] = value );
+			}
+
+		} else {
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+				return ret;
+
+			} else {
+				return elem[ name ];
+			}
+		}
+	},
+
+	propHooks: {
+		tabIndex: {
+			get: function( elem ) {
+				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+				var attributeNode = elem.getAttributeNode("tabindex");
+
+				return attributeNode && attributeNode.specified ?
+					parseInt( attributeNode.value, 10 ) :
+					rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+						0 :
+						undefined;
+			}
+		}
+	}
+});
+
+// Hook for boolean attributes
+boolHook = {
+	get: function( elem, name ) {
+		// Align boolean attributes with corresponding properties
+		// Fall back to attribute presence where some booleans are not supported
+		var attrNode,
+			property = jQuery.prop( elem, name );
+		return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
+			name.toLowerCase() :
+			undefined;
+	},
+	set: function( elem, value, name ) {
+		var propName;
+		if ( value === false ) {
+			// Remove boolean attributes when set to false
+			jQuery.removeAttr( elem, name );
+		} else {
+			// value is true since we know at this point it's type boolean and not false
+			// Set boolean attributes to the same name and set the DOM property
+			propName = jQuery.propFix[ name ] || name;
+			if ( propName in elem ) {
+				// Only set the IDL specifically if it already exists on the element
+				elem[ propName ] = true;
+			}
+
+			elem.setAttribute( name, name.toLowerCase() );
+		}
+		return name;
+	}
+};
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+	fixSpecified = {
+		name: true,
+		id: true,
+		coords: true
+	};
+
+	// Use this for any attribute in IE6/7
+	// This fixes almost every IE6/7 issue
+	nodeHook = jQuery.valHooks.button = {
+		get: function( elem, name ) {
+			var ret;
+			ret = elem.getAttributeNode( name );
+			return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ?
+				ret.value :
+				undefined;
+		},
+		set: function( elem, value, name ) {
+			// Set the existing or create a new attribute node
+			var ret = elem.getAttributeNode( name );
+			if ( !ret ) {
+				ret = document.createAttribute( name );
+				elem.setAttributeNode( ret );
+			}
+			return ( ret.value = value + "" );
+		}
+	};
+
+	// Set width and height to auto instead of 0 on empty string( Bug #8150 )
+	// This is for removals
+	jQuery.each([ "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			set: function( elem, value ) {
+				if ( value === "" ) {
+					elem.setAttribute( name, "auto" );
+					return value;
+				}
+			}
+		});
+	});
+
+	// Set contenteditable to false on removals(#10429)
+	// Setting to empty string throws an error as an invalid value
+	jQuery.attrHooks.contenteditable = {
+		get: nodeHook.get,
+		set: function( elem, value, name ) {
+			if ( value === "" ) {
+				value = "false";
+			}
+			nodeHook.set( elem, value, name );
+		}
+	};
+}
+
+
+// Some attributes require a special call on IE
+if ( !jQuery.support.hrefNormalized ) {
+	jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			get: function( elem ) {
+				var ret = elem.getAttribute( name, 2 );
+				return ret === null ? undefined : ret;
+			}
+		});
+	});
+}
+
+if ( !jQuery.support.style ) {
+	jQuery.attrHooks.style = {
+		get: function( elem ) {
+			// Return undefined in the case of empty string
+			// Normalize to lowercase since IE uppercases css property names
+			return elem.style.cssText.toLowerCase() || undefined;
+		},
+		set: function( elem, value ) {
+			return ( elem.style.cssText = value + "" );
+		}
+	};
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+	jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+		get: function( elem ) {
+			var parent = elem.parentNode;
+
+			if ( parent ) {
+				parent.selectedIndex;
+
+				// Make sure that it also works with optgroups, see #5701
+				if ( parent.parentNode ) {
+					parent.parentNode.selectedIndex;
+				}
+			}
+			return null;
+		}
+	});
+}
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+	jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+	jQuery.each([ "radio", "checkbox" ], function() {
+		jQuery.valHooks[ this ] = {
+			get: function( elem ) {
+				// Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+				return elem.getAttribute("value") === null ? "on" : elem.value;
+			}
+		};
+	});
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+	jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+		set: function( elem, value ) {
+			if ( jQuery.isArray( value ) ) {
+				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+			}
+		}
+	});
+});
+var rformElems = /^(?:textarea|input|select)$/i,
+	rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/,
+	rhoverHack = /(?:^|\s)hover(\.\S+|)\b/,
+	rkeyEvent = /^key/,
+	rmouseEvent = /^(?:mouse|contextmenu)|click/,
+	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+	hoverHack = function( events ) {
+		return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
+	};
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+	add: function( elem, types, handler, data, selector ) {
+
+		var elemData, eventHandle, events,
+			t, tns, type, namespaces, handleObj,
+			handleObjIn, handlers, special;
+
+		// Don't attach events to noData or text/comment nodes (allow plain objects tho)
+		if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
+			return;
+		}
+
+		// Caller can pass in an object of custom data in lieu of the handler
+		if ( handler.handler ) {
+			handleObjIn = handler;
+			handler = handleObjIn.handler;
+			selector = handleObjIn.selector;
+		}
+
+		// Make sure that the handler has a unique ID, used to find/remove it later
+		if ( !handler.guid ) {
+			handler.guid = jQuery.guid++;
+		}
+
+		// Init the element's event structure and main handler, if this is the first
+		events = elemData.events;
+		if ( !events ) {
+			elemData.events = events = {};
+		}
+		eventHandle = elemData.handle;
+		if ( !eventHandle ) {
+			elemData.handle = eventHandle = function( e ) {
+				// Discard the second event of a jQuery.event.trigger() and
+				// when an event is called after a page has unloaded
+				return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
+					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+					undefined;
+			};
+			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+			eventHandle.elem = elem;
+		}
+
+		// Handle multiple events separated by a space
+		// jQuery(...).bind("mouseover mouseout", fn);
+		types = jQuery.trim( hoverHack(types) ).split( " " );
+		for ( t = 0; t < types.length; t++ ) {
+
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = tns[1];
+			namespaces = ( tns[2] || "" ).split( "." ).sort();
+
+			// If event changes its type, use the special event handlers for the changed type
+			special = jQuery.event.special[ type ] || {};
+
+			// If selector defined, determine special event api type, otherwise given type
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+
+			// Update special based on newly reset type
+			special = jQuery.event.special[ type ] || {};
+
+			// handleObj is passed to all event handlers
+			handleObj = jQuery.extend({
+				type: type,
+				origType: tns[1],
+				data: data,
+				handler: handler,
+				guid: handler.guid,
+				selector: selector,
+				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+				namespace: namespaces.join(".")
+			}, handleObjIn );
+
+			// Init the event handler queue if we're the first
+			handlers = events[ type ];
+			if ( !handlers ) {
+				handlers = events[ type ] = [];
+				handlers.delegateCount = 0;
+
+				// Only use addEventListener/attachEvent if the special events handler returns false
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					if ( elem.addEventListener ) {
+						elem.addEventListener( type, eventHandle, false );
+
+					} else if ( elem.attachEvent ) {
+						elem.attachEvent( "on" + type, eventHandle );
+					}
+				}
+			}
+
+			if ( special.add ) {
+				special.add.call( elem, handleObj );
+
+				if ( !handleObj.handler.guid ) {
+					handleObj.handler.guid = handler.guid;
+				}
+			}
+
+			// Add to the element's handler list, delegates in front
+			if ( selector ) {
+				handlers.splice( handlers.delegateCount++, 0, handleObj );
+			} else {
+				handlers.push( handleObj );
+			}
+
+			// Keep track of which events have ever been used, for event optimization
+			jQuery.event.global[ type ] = true;
+		}
+
+		// Nullify elem to prevent memory leaks in IE
+		elem = null;
+	},
+
+	global: {},
+
+	// Detach an event or set of events from an element
+	remove: function( elem, types, handler, selector, mappedTypes ) {
+
+		var t, tns, type, origType, namespaces, origCount,
+			j, events, special, eventType, handleObj,
+			elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+		if ( !elemData || !(events = elemData.events) ) {
+			return;
+		}
+
+		// Once for each type.namespace in types; type may be omitted
+		types = jQuery.trim( hoverHack( types || "" ) ).split(" ");
+		for ( t = 0; t < types.length; t++ ) {
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tns[1];
+			namespaces = tns[2];
+
+			// Unbind all events (on this namespace, if provided) for the element
+			if ( !type ) {
+				for ( type in events ) {
+					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+				}
+				continue;
+			}
+
+			special = jQuery.event.special[ type ] || {};
+			type = ( selector? special.delegateType : special.bindType ) || type;
+			eventType = events[ type ] || [];
+			origCount = eventType.length;
+			namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+
+			// Remove matching events
+			for ( j = 0; j < eventType.length; j++ ) {
+				handleObj = eventType[ j ];
+
+				if ( ( mappedTypes || origType === handleObj.origType ) &&
+					 ( !handler || handler.guid === handleObj.guid ) &&
+					 ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
+					 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+					eventType.splice( j--, 1 );
+
+					if ( handleObj.selector ) {
+						eventType.delegateCount--;
+					}
+					if ( special.remove ) {
+						special.remove.call( elem, handleObj );
+					}
+				}
+			}
+
+			// Remove generic event handler if we removed something and no more handlers exist
+			// (avoids potential for endless recursion during removal of special event handlers)
+			if ( eventType.length === 0 && origCount !== eventType.length ) {
+				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+					jQuery.removeEvent( elem, type, elemData.handle );
+				}
+
+				delete events[ type ];
+			}
+		}
+
+		// Remove the expando if it's no longer used
+		if ( jQuery.isEmptyObject( events ) ) {
+			delete elemData.handle;
+
+			// removeData also checks for emptiness and clears the expando if empty
+			// so use it instead of delete
+			jQuery.removeData( elem, "events", true );
+		}
+	},
+
+	// Events that are safe to short-circuit if no handlers are attached.
+	// Native DOM events should not be added, they may have inline handlers.
+	customEvent: {
+		"getData": true,
+		"setData": true,
+		"changeData": true
+	},
+
+	trigger: function( event, data, elem, onlyHandlers ) {
+		// Don't do events on text and comment nodes
+		if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
+			return;
+		}
+
+		// Event object or event type
+		var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType,
+			type = event.type || event,
+			namespaces = [];
+
+		// focus/blur morphs to focusin/out; ensure we're not firing them right now
+		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+			return;
+		}
+
+		if ( type.indexOf( "!" ) >= 0 ) {
+			// Exclusive events trigger only for the exact event (no namespaces)
+			type = type.slice(0, -1);
+			exclusive = true;
+		}
+
+		if ( type.indexOf( "." ) >= 0 ) {
+			// Namespaced trigger; create a regexp to match event type in handle()
+			namespaces = type.split(".");
+			type = namespaces.shift();
+			namespaces.sort();
+		}
+
+		if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
+			// No jQuery handlers for this event type, and it can't have inline handlers
+			return;
+		}
+
+		// Caller can pass in an Event, Object, or just an event type string
+		event = typeof event === "object" ?
+			// jQuery.Event object
+			event[ jQuery.expando ] ? event :
+			// Object literal
+			new jQuery.Event( type, event ) :
+			// Just the event type (string)
+			new jQuery.Event( type );
+
+		event.type = type;
+		event.isTrigger = true;
+		event.exclusive = exclusive;
+		event.namespace = namespaces.join( "." );
+		event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+		ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
+
+		// Handle a global trigger
+		if ( !elem ) {
+
+			// TODO: Stop taunting the data cache; remove global events and always attach to document
+			cache = jQuery.cache;
+			for ( i in cache ) {
+				if ( cache[ i ].events && cache[ i ].events[ type ] ) {
+					jQuery.event.trigger( event, data, cache[ i ].handle.elem, true );
+				}
+			}
+			return;
+		}
+
+		// Clean up the event in case it is being reused
+		event.result = undefined;
+		if ( !event.target ) {
+			event.target = elem;
+		}
+
+		// Clone any incoming data and prepend the event, creating the handler arg list
+		data = data != null ? jQuery.makeArray( data ) : [];
+		data.unshift( event );
+
+		// Allow special events to draw outside the lines
+		special = jQuery.event.special[ type ] || {};
+		if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
+			return;
+		}
+
+		// Determine event propagation path in advance, per W3C events spec (#9951)
+		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+		eventPath = [[ elem, special.bindType || type ]];
+		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+			bubbleType = special.delegateType || type;
+			cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;
+			for ( old = elem; cur; cur = cur.parentNode ) {
+				eventPath.push([ cur, bubbleType ]);
+				old = cur;
+			}
+
+			// Only add window if we got to document (e.g., not plain obj or detached DOM)
+			if ( old === (elem.ownerDocument || document) ) {
+				eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);
+			}
+		}
+
+		// Fire handlers on the event path
+		for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {
+
+			cur = eventPath[i][0];
+			event.type = eventPath[i][1];
+
+			handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+			if ( handle ) {
+				handle.apply( cur, data );
+			}
+			// Note that this is a bare JS function and not a jQuery handler
+			handle = ontype && cur[ ontype ];
+			if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+				event.preventDefault();
+			}
+		}
+		event.type = type;
+
+		// If nobody prevented the default action, do it now
+		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+			if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
+				!(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+				// Call a native DOM method on the target with the same name name as the event.
+				// Can't use an .isFunction() check here because IE6/7 fails that test.
+				// Don't do default actions on window, that's where global variables be (#6170)
+				// IE<9 dies on focus/blur to hidden element (#1486)
+				if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {
+
+					// Don't re-trigger an onFOO event when we call its FOO() method
+					old = elem[ ontype ];
+
+					if ( old ) {
+						elem[ ontype ] = null;
+					}
+
+					// Prevent re-triggering of the same event, since we already bubbled it above
+					jQuery.event.triggered = type;
+					elem[ type ]();
+					jQuery.event.triggered = undefined;
+
+					if ( old ) {
+						elem[ ontype ] = old;
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	dispatch: function( event ) {
+
+		// Make a writable jQuery.Event from the native event object
+		event = jQuery.event.fix( event || window.event );
+
+		var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related,
+			handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
+			delegateCount = handlers.delegateCount,
+			args = core_slice.call( arguments ),
+			run_all = !event.exclusive && !event.namespace,
+			special = jQuery.event.special[ event.type ] || {},
+			handlerQueue = [];
+
+		// Use the fix-ed jQuery.Event rather than the (read-only) native event
+		args[0] = event;
+		event.delegateTarget = this;
+
+		// Call the preDispatch hook for the mapped type, and let it bail if desired
+		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+			return;
+		}
+
+		// Determine handlers that should run if there are delegated events
+		// Avoid non-left-click bubbling in Firefox (#3861)
+		if ( delegateCount && !(event.button && event.type === "click") ) {
+
+			for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
+
+				// Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
+				if ( cur.disabled !== true || event.type !== "click" ) {
+					selMatch = {};
+					matches = [];
+					for ( i = 0; i < delegateCount; i++ ) {
+						handleObj = handlers[ i ];
+						sel = handleObj.selector;
+
+						if ( selMatch[ sel ] === undefined ) {
+							selMatch[ sel ] = handleObj.needsContext ?
+								jQuery( sel, this ).index( cur ) >= 0 :
+								jQuery.find( sel, this, null, [ cur ] ).length;
+						}
+						if ( selMatch[ sel ] ) {
+							matches.push( handleObj );
+						}
+					}
+					if ( matches.length ) {
+						handlerQueue.push({ elem: cur, matches: matches });
+					}
+				}
+			}
+		}
+
+		// Add the remaining (directly-bound) handlers
+		if ( handlers.length > delegateCount ) {
+			handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
+		}
+
+		// Run delegates first; they may want to stop propagation beneath us
+		for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
+			matched = handlerQueue[ i ];
+			event.currentTarget = matched.elem;
+
+			for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
+				handleObj = matched.matches[ j ];
+
+				// Triggered event must either 1) be non-exclusive and have no namespace, or
+				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+				if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
+
+					event.data = handleObj.data;
+					event.handleObj = handleObj;
+
+					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+							.apply( matched.elem, args );
+
+					if ( ret !== undefined ) {
+						event.result = ret;
+						if ( ret === false ) {
+							event.preventDefault();
+							event.stopPropagation();
+						}
+					}
+				}
+			}
+		}
+
+		// Call the postDispatch hook for the mapped type
+		if ( special.postDispatch ) {
+			special.postDispatch.call( this, event );
+		}
+
+		return event.result;
+	},
+
+	// Includes some event props shared by KeyEvent and MouseEvent
+	// *** attrChange attrName relatedNode srcElement  are not normalized, non-W3C, deprecated, will be removed in 1.8 ***
+	props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+	fixHooks: {},
+
+	keyHooks: {
+		props: "char charCode key keyCode".split(" "),
+		filter: function( event, original ) {
+
+			// Add which for key events
+			if ( event.which == null ) {
+				event.which = original.charCode != null ? original.charCode : original.keyCode;
+			}
+
+			return event;
+		}
+	},
+
+	mouseHooks: {
+		props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+		filter: function( event, original ) {
+			var eventDoc, doc, body,
+				button = original.button,
+				fromElement = original.fromElement;
+
+			// Calculate pageX/Y if missing and clientX/Y available
+			if ( event.pageX == null && original.clientX != null ) {
+				eventDoc = event.target.ownerDocument || document;
+				doc = eventDoc.documentElement;
+				body = eventDoc.body;
+
+				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+			}
+
+			// Add relatedTarget, if necessary
+			if ( !event.relatedTarget && fromElement ) {
+				event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+			}
+
+			// Add which for click: 1 === left; 2 === middle; 3 === right
+			// Note: button is not normalized, so don't use it
+			if ( !event.which && button !== undefined ) {
+				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+			}
+
+			return event;
+		}
+	},
+
+	fix: function( event ) {
+		if ( event[ jQuery.expando ] ) {
+			return event;
+		}
+
+		// Create a writable copy of the event object and normalize some properties
+		var i, prop,
+			originalEvent = event,
+			fixHook = jQuery.event.fixHooks[ event.type ] || {},
+			copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+		event = jQuery.Event( originalEvent );
+
+		for ( i = copy.length; i; ) {
+			prop = copy[ --i ];
+			event[ prop ] = originalEvent[ prop ];
+		}
+
+		// Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
+		if ( !event.target ) {
+			event.target = originalEvent.srcElement || document;
+		}
+
+		// Target should not be a text node (#504, Safari)
+		if ( event.target.nodeType === 3 ) {
+			event.target = event.target.parentNode;
+		}
+
+		// For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8)
+		event.metaKey = !!event.metaKey;
+
+		return fixHook.filter? fixHook.filter( event, originalEvent ) : event;
+	},
+
+	special: {
+		load: {
+			// Prevent triggered image.load events from bubbling to window.load
+			noBubble: true
+		},
+
+		focus: {
+			delegateType: "focusin"
+		},
+		blur: {
+			delegateType: "focusout"
+		},
+
+		beforeunload: {
+			setup: function( data, namespaces, eventHandle ) {
+				// We only want to do this special case on windows
+				if ( jQuery.isWindow( this ) ) {
+					this.onbeforeunload = eventHandle;
+				}
+			},
+
+			teardown: function( namespaces, eventHandle ) {
+				if ( this.onbeforeunload === eventHandle ) {
+					this.onbeforeunload = null;
+				}
+			}
+		}
+	},
+
+	simulate: function( type, elem, event, bubble ) {
+		// Piggyback on a donor event to simulate a different one.
+		// Fake originalEvent to avoid donor's stopPropagation, but if the
+		// simulated event prevents default then we do the same on the donor.
+		var e = jQuery.extend(
+			new jQuery.Event(),
+			event,
+			{ type: type,
+				isSimulated: true,
+				originalEvent: {}
+			}
+		);
+		if ( bubble ) {
+			jQuery.event.trigger( e, null, elem );
+		} else {
+			jQuery.event.dispatch.call( elem, e );
+		}
+		if ( e.isDefaultPrevented() ) {
+			event.preventDefault();
+		}
+	}
+};
+
+// Some plugins are using, but it's undocumented/deprecated and will be removed.
+// The 1.7 special event interface should provide all the hooks needed now.
+jQuery.event.handle = jQuery.event.dispatch;
+
+jQuery.removeEvent = document.removeEventListener ?
+	function( elem, type, handle ) {
+		if ( elem.removeEventListener ) {
+			elem.removeEventListener( type, handle, false );
+		}
+	} :
+	function( elem, type, handle ) {
+		var name = "on" + type;
+
+		if ( elem.detachEvent ) {
+
+			// #8545, #7054, preventing memory leaks for custom events in IE6-8 –
+			// detachEvent needed property on element, by name of that event, to properly expose it to GC
+			if ( typeof elem[ name ] === "undefined" ) {
+				elem[ name ] = null;
+			}
+
+			elem.detachEvent( name, handle );
+		}
+	};
+
+jQuery.Event = function( src, props ) {
+	// Allow instantiation without the 'new' keyword
+	if ( !(this instanceof jQuery.Event) ) {
+		return new jQuery.Event( src, props );
+	}
+
+	// Event object
+	if ( src && src.type ) {
+		this.originalEvent = src;
+		this.type = src.type;
+
+		// Events bubbling up the document may have been marked as prevented
+		// by a handler lower down the tree; reflect the correct value.
+		this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+			src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+	// Event type
+	} else {
+		this.type = src;
+	}
+
+	// Put explicitly provided properties onto the event object
+	if ( props ) {
+		jQuery.extend( this, props );
+	}
+
+	// Create a timestamp if incoming event doesn't have one
+	this.timeStamp = src && src.timeStamp || jQuery.now();
+
+	// Mark it as fixed
+	this[ jQuery.expando ] = true;
+};
+
+function returnFalse() {
+	return false;
+}
+function returnTrue() {
+	return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+	preventDefault: function() {
+		this.isDefaultPrevented = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+
+		// if preventDefault exists run it on the original event
+		if ( e.preventDefault ) {
+			e.preventDefault();
+
+		// otherwise set the returnValue property of the original event to false (IE)
+		} else {
+			e.returnValue = false;
+		}
+	},
+	stopPropagation: function() {
+		this.isPropagationStopped = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+		// if stopPropagation exists run it on the original event
+		if ( e.stopPropagation ) {
+			e.stopPropagation();
+		}
+		// otherwise set the cancelBubble property of the original event to true (IE)
+		e.cancelBubble = true;
+	},
+	stopImmediatePropagation: function() {
+		this.isImmediatePropagationStopped = returnTrue;
+		this.stopPropagation();
+	},
+	isDefaultPrevented: returnFalse,
+	isPropagationStopped: returnFalse,
+	isImmediatePropagationStopped: returnFalse
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+	mouseenter: "mouseover",
+	mouseleave: "mouseout"
+}, function( orig, fix ) {
+	jQuery.event.special[ orig ] = {
+		delegateType: fix,
+		bindType: fix,
+
+		handle: function( event ) {
+			var ret,
+				target = this,
+				related = event.relatedTarget,
+				handleObj = event.handleObj,
+				selector = handleObj.selector;
+
+			// For mousenter/leave call the handler if related is outside the target.
+			// NB: No relatedTarget if the mouse left/entered the browser window
+			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+				event.type = handleObj.origType;
+				ret = handleObj.handler.apply( this, arguments );
+				event.type = fix;
+			}
+			return ret;
+		}
+	};
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+	jQuery.event.special.submit = {
+		setup: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Lazy-add a submit handler when a descendant form may potentially be submitted
+			jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+				// Node name check avoids a VML-related crash in IE (#9807)
+				var elem = e.target,
+					form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+				if ( form && !jQuery._data( form, "_submit_attached" ) ) {
+					jQuery.event.add( form, "submit._submit", function( event ) {
+						event._submit_bubble = true;
+					});
+					jQuery._data( form, "_submit_attached", true );
+				}
+			});
+			// return undefined since we don't need an event listener
+		},
+
+		postDispatch: function( event ) {
+			// If form was submitted by the user, bubble the event up the tree
+			if ( event._submit_bubble ) {
+				delete event._submit_bubble;
+				if ( this.parentNode && !event.isTrigger ) {
+					jQuery.event.simulate( "submit", this.parentNode, event, true );
+				}
+			}
+		},
+
+		teardown: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+			jQuery.event.remove( this, "._submit" );
+		}
+	};
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+	jQuery.event.special.change = {
+
+		setup: function() {
+
+			if ( rformElems.test( this.nodeName ) ) {
+				// IE doesn't fire change on a check/radio until blur; trigger it on click
+				// after a propertychange. Eat the blur-change in special.change.handle.
+				// This still fires onchange a second time for check/radio after blur.
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					jQuery.event.add( this, "propertychange._change", function( event ) {
+						if ( event.originalEvent.propertyName === "checked" ) {
+							this._just_changed = true;
+						}
+					});
+					jQuery.event.add( this, "click._change", function( event ) {
+						if ( this._just_changed && !event.isTrigger ) {
+							this._just_changed = false;
+						}
+						// Allow triggered, simulated change events (#11500)
+						jQuery.event.simulate( "change", this, event, true );
+					});
+				}
+				return false;
+			}
+			// Delegated event; lazy-add a change handler on descendant inputs
+			jQuery.event.add( this, "beforeactivate._change", function( e ) {
+				var elem = e.target;
+
+				if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) {
+					jQuery.event.add( elem, "change._change", function( event ) {
+						if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+							jQuery.event.simulate( "change", this.parentNode, event, true );
+						}
+					});
+					jQuery._data( elem, "_change_attached", true );
+				}
+			});
+		},
+
+		handle: function( event ) {
+			var elem = event.target;
+
+			// Swallow native change events from checkbox/radio, we already triggered them above
+			if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+				return event.handleObj.handler.apply( this, arguments );
+			}
+		},
+
+		teardown: function() {
+			jQuery.event.remove( this, "._change" );
+
+			return !rformElems.test( this.nodeName );
+		}
+	};
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+		// Attach a single capturing handler while someone wants focusin/focusout
+		var attaches = 0,
+			handler = function( event ) {
+				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+			};
+
+		jQuery.event.special[ fix ] = {
+			setup: function() {
+				if ( attaches++ === 0 ) {
+					document.addEventListener( orig, handler, true );
+				}
+			},
+			teardown: function() {
+				if ( --attaches === 0 ) {
+					document.removeEventListener( orig, handler, true );
+				}
+			}
+		};
+	});
+}
+
+jQuery.fn.extend({
+
+	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+		var origFn, type;
+
+		// Types can be a map of types/handlers
+		if ( typeof types === "object" ) {
+			// ( types-Object, selector, data )
+			if ( typeof selector !== "string" ) { // && selector != null
+				// ( types-Object, data )
+				data = data || selector;
+				selector = undefined;
+			}
+			for ( type in types ) {
+				this.on( type, selector, data, types[ type ], one );
+			}
+			return this;
+		}
+
+		if ( data == null && fn == null ) {
+			// ( types, fn )
+			fn = selector;
+			data = selector = undefined;
+		} else if ( fn == null ) {
+			if ( typeof selector === "string" ) {
+				// ( types, selector, fn )
+				fn = data;
+				data = undefined;
+			} else {
+				// ( types, data, fn )
+				fn = data;
+				data = selector;
+				selector = undefined;
+			}
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		} else if ( !fn ) {
+			return this;
+		}
+
+		if ( one === 1 ) {
+			origFn = fn;
+			fn = function( event ) {
+				// Can use an empty set, since event contains the info
+				jQuery().off( event );
+				return origFn.apply( this, arguments );
+			};
+			// Use same guid so caller can remove using origFn
+			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+		}
+		return this.each( function() {
+			jQuery.event.add( this, types, fn, data, selector );
+		});
+	},
+	one: function( types, selector, data, fn ) {
+		return this.on( types, selector, data, fn, 1 );
+	},
+	off: function( types, selector, fn ) {
+		var handleObj, type;
+		if ( types && types.preventDefault && types.handleObj ) {
+			// ( event )  dispatched jQuery.Event
+			handleObj = types.handleObj;
+			jQuery( types.delegateTarget ).off(
+				handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+				handleObj.selector,
+				handleObj.handler
+			);
+			return this;
+		}
+		if ( typeof types === "object" ) {
+			// ( types-object [, selector] )
+			for ( type in types ) {
+				this.off( type, selector, types[ type ] );
+			}
+			return this;
+		}
+		if ( selector === false || typeof selector === "function" ) {
+			// ( types [, fn] )
+			fn = selector;
+			selector = undefined;
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		}
+		return this.each(function() {
+			jQuery.event.remove( this, types, fn, selector );
+		});
+	},
+
+	bind: function( types, data, fn ) {
+		return this.on( types, null, data, fn );
+	},
+	unbind: function( types, fn ) {
+		return this.off( types, null, fn );
+	},
+
+	live: function( types, data, fn ) {
+		jQuery( this.context ).on( types, this.selector, data, fn );
+		return this;
+	},
+	die: function( types, fn ) {
+		jQuery( this.context ).off( types, this.selector || "**", fn );
+		return this;
+	},
+
+	delegate: function( selector, types, data, fn ) {
+		return this.on( types, selector, data, fn );
+	},
+	undelegate: function( selector, types, fn ) {
+		// ( namespace ) or ( selector, types [, fn] )
+		return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+	},
+
+	trigger: function( type, data ) {
+		return this.each(function() {
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	triggerHandler: function( type, data ) {
+		if ( this[0] ) {
+			return jQuery.event.trigger( type, data, this[0], true );
+		}
+	},
+
+	toggle: function( fn ) {
+		// Save reference to arguments for access in closure
+		var args = arguments,
+			guid = fn.guid || jQuery.guid++,
+			i = 0,
+			toggler = function( event ) {
+				// Figure out which function to execute
+				var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+				jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+				// Make sure that clicks stop
+				event.preventDefault();
+
+				// and execute the function
+				return args[ lastToggle ].apply( this, arguments ) || false;
+			};
+
+		// link all the functions, so any of them can unbind this click handler
+		toggler.guid = guid;
+		while ( i < args.length ) {
+			args[ i++ ].guid = guid;
+		}
+
+		return this.click( toggler );
+	},
+
+	hover: function( fnOver, fnOut ) {
+		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+	}
+});
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+	// Handle event binding
+	jQuery.fn[ name ] = function( data, fn ) {
+		if ( fn == null ) {
+			fn = data;
+			data = null;
+		}
+
+		return arguments.length > 0 ?
+			this.on( name, null, data, fn ) :
+			this.trigger( name );
+	};
+
+	if ( rkeyEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;
+	}
+
+	if ( rmouseEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;
+	}
+});
+/*!
+ * Sizzle CSS Selector Engine
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://sizzlejs.com/
+ */
+(function( window, undefined ) {
+
+var cachedruns,
+	assertGetIdNotName,
+	Expr,
+	getText,
+	isXML,
+	contains,
+	compile,
+	sortOrder,
+	hasDuplicate,
+	outermostContext,
+
+	baseHasDuplicate = true,
+	strundefined = "undefined",
+
+	expando = ( "sizcache" + Math.random() ).replace( ".", "" ),
+
+	Token = String,
+	document = window.document,
+	docElem = document.documentElement,
+	dirruns = 0,
+	done = 0,
+	pop = [].pop,
+	push = [].push,
+	slice = [].slice,
+	// Use a stripped-down indexOf if a native one is unavailable
+	indexOf = [].indexOf || function( elem ) {
+		var i = 0,
+			len = this.length;
+		for ( ; i < len; i++ ) {
+			if ( this[i] === elem ) {
+				return i;
+			}
+		}
+		return -1;
+	},
+
+	// Augment a function for special use by Sizzle
+	markFunction = function( fn, value ) {
+		fn[ expando ] = value == null || value;
+		return fn;
+	},
+
+	createCache = function() {
+		var cache = {},
+			keys = [];
+
+		return markFunction(function( key, value ) {
+			// Only keep the most recent entries
+			if ( keys.push( key ) > Expr.cacheLength ) {
+				delete cache[ keys.shift() ];
+			}
+
+			return (cache[ key ] = value);
+		}, cache );
+	},
+
+	classCache = createCache(),
+	tokenCache = createCache(),
+	compilerCache = createCache(),
+
+	// Regex
+
+	// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+	whitespace = "[\\x20\\t\\r\\n\\f]",
+	// http://www.w3.org/TR/css3-syntax/#characters
+	characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",
+
+	// Loosely modeled on CSS identifier characters
+	// An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors)
+	// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+	identifier = characterEncoding.replace( "w", "w#" ),
+
+	// Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+	operators = "([*^$|!~]?=)",
+	attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+		"*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+	// Prefer arguments not in parens/brackets,
+	//   then attribute selectors and non-pseudos (denoted by :),
+	//   then anything else
+	// These preferences are here to reduce the number of selectors
+	//   needing tokenize in the PSEUDO preFilter
+	pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)",
+
+	// For matchExpr.POS and matchExpr.needsContext
+	pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
+		"*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)",
+
+	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+	rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ),
+	rpseudo = new RegExp( pseudos ),
+
+	// Easily-parseable/retrievable ID or TAG or CLASS selectors
+	rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,
+
+	rnot = /^:not/,
+	rsibling = /[\x20\t\r\n\f]*[+~]/,
+	rendsWithNot = /:not\($/,
+
+	rheader = /h\d/i,
+	rinputs = /input|select|textarea|button/i,
+
+	rbackslash = /\\(?!\\)/g,
+
+	matchExpr = {
+		"ID": new RegExp( "^#(" + characterEncoding + ")" ),
+		"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+		"NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ),
+		"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+		"ATTR": new RegExp( "^" + attributes ),
+		"PSEUDO": new RegExp( "^" + pseudos ),
+		"POS": new RegExp( pos, "i" ),
+		"CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace +
+			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+		// For use in libraries implementing .is()
+		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" )
+	},
+
+	// Support
+
+	// Used for testing something on an element
+	assert = function( fn ) {
+		var div = document.createElement("div");
+
+		try {
+			return fn( div );
+		} catch (e) {
+			return false;
+		} finally {
+			// release memory in IE
+			div = null;
+		}
+	},
+
+	// Check if getElementsByTagName("*") returns only elements
+	assertTagNameNoComments = assert(function( div ) {
+		div.appendChild( document.createComment("") );
+		return !div.getElementsByTagName("*").length;
+	}),
+
+	// Check if getAttribute returns normalized href attributes
+	assertHrefNotNormalized = assert(function( div ) {
+		div.innerHTML = "<a href='#'></a>";
+		return div.firstChild && typeof div.firstChild.getAttribute !== strundefined &&
+			div.firstChild.getAttribute("href") === "#";
+	}),
+
+	// Check if attributes should be retrieved by attribute nodes
+	assertAttributes = assert(function( div ) {
+		div.innerHTML = "<select></select>";
+		var type = typeof div.lastChild.getAttribute("multiple");
+		// IE8 returns a string for some attributes even when not present
+		return type !== "boolean" && type !== "string";
+	}),
+
+	// Check if getElementsByClassName can be trusted
+	assertUsableClassName = assert(function( div ) {
+		// Opera can't find a second classname (in 9.6)
+		div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>";
+		if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) {
+			return false;
+		}
+
+		// Safari 3.2 caches class attributes and doesn't catch changes
+		div.lastChild.className = "e";
+		return div.getElementsByClassName("e").length === 2;
+	}),
+
+	// Check if getElementById returns elements by name
+	// Check if getElementsByName privileges form controls or returns elements by ID
+	assertUsableName = assert(function( div ) {
+		// Inject content
+		div.id = expando + 0;
+		div.innerHTML = "<a name='" + expando + "'></a><div name='" + expando + "'></div>";
+		docElem.insertBefore( div, docElem.firstChild );
+
+		// Test
+		var pass = document.getElementsByName &&
+			// buggy browsers will return fewer than the correct 2
+			document.getElementsByName( expando ).length === 2 +
+			// buggy browsers will return more than the correct 0
+			document.getElementsByName( expando + 0 ).length;
+		assertGetIdNotName = !document.getElementById( expando );
+
+		// Cleanup
+		docElem.removeChild( div );
+
+		return pass;
+	});
+
+// If slice is not available, provide a backup
+try {
+	slice.call( docElem.childNodes, 0 )[0].nodeType;
+} catch ( e ) {
+	slice = function( i ) {
+		var elem,
+			results = [];
+		for ( ; (elem = this[i]); i++ ) {
+			results.push( elem );
+		}
+		return results;
+	};
+}
+
+function Sizzle( selector, context, results, seed ) {
+	results = results || [];
+	context = context || document;
+	var match, elem, xml, m,
+		nodeType = context.nodeType;
+
+	if ( !selector || typeof selector !== "string" ) {
+		return results;
+	}
+
+	if ( nodeType !== 1 && nodeType !== 9 ) {
+		return [];
+	}
+
+	xml = isXML( context );
+
+	if ( !xml && !seed ) {
+		if ( (match = rquickExpr.exec( selector )) ) {
+			// Speed-up: Sizzle("#ID")
+			if ( (m = match[1]) ) {
+				if ( nodeType === 9 ) {
+					elem = context.getElementById( m );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE, Opera, and Webkit return items
+						// by name instead of ID
+						if ( elem.id === m ) {
+							results.push( elem );
+							return results;
+						}
+					} else {
+						return results;
+					}
+				} else {
+					// Context is not a document
+					if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+						contains( context, elem ) && elem.id === m ) {
+						results.push( elem );
+						return results;
+					}
+				}
+
+			// Speed-up: Sizzle("TAG")
+			} else if ( match[2] ) {
+				push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) );
+				return results;
+
+			// Speed-up: Sizzle(".CLASS")
+			} else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) {
+				push.apply( results, slice.call(context.getElementsByClassName( m ), 0) );
+				return results;
+			}
+		}
+	}
+
+	// All others
+	return select( selector.replace( rtrim, "$1" ), context, results, seed, xml );
+}
+
+Sizzle.matches = function( expr, elements ) {
+	return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+	return Sizzle( expr, null, null, [ elem ] ).length > 0;
+};
+
+// Returns a function to use in pseudos for input types
+function createInputPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return name === "input" && elem.type === type;
+	};
+}
+
+// Returns a function to use in pseudos for buttons
+function createButtonPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return (name === "input" || name === "button") && elem.type === type;
+	};
+}
+
+// Returns a function to use in pseudos for positionals
+function createPositionalPseudo( fn ) {
+	return markFunction(function( argument ) {
+		argument = +argument;
+		return markFunction(function( seed, matches ) {
+			var j,
+				matchIndexes = fn( [], seed.length, argument ),
+				i = matchIndexes.length;
+
+			// Match elements found at the specified indexes
+			while ( i-- ) {
+				if ( seed[ (j = matchIndexes[i]) ] ) {
+					seed[j] = !(matches[j] = seed[j]);
+				}
+			}
+		});
+	});
+}
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+	var node,
+		ret = "",
+		i = 0,
+		nodeType = elem.nodeType;
+
+	if ( nodeType ) {
+		if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+			// Use textContent for elements
+			// innerText usage removed for consistency of new lines (see #11153)
+			if ( typeof elem.textContent === "string" ) {
+				return elem.textContent;
+			} else {
+				// Traverse its children
+				for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+					ret += getText( elem );
+				}
+			}
+		} else if ( nodeType === 3 || nodeType === 4 ) {
+			return elem.nodeValue;
+		}
+		// Do not include comment or processing instruction nodes
+	} else {
+
+		// If no nodeType, this is expected to be an array
+		for ( ; (node = elem[i]); i++ ) {
+			// Do not traverse comment nodes
+			ret += getText( node );
+		}
+	}
+	return ret;
+};
+
+isXML = Sizzle.isXML = function( elem ) {
+	// documentElement is verified for cases where it doesn't yet exist
+	// (such as loading iframes in IE - #4833)
+	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+	return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+// Element contains another
+contains = Sizzle.contains = docElem.contains ?
+	function( a, b ) {
+		var adown = a.nodeType === 9 ? a.documentElement : a,
+			bup = b && b.parentNode;
+		return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) );
+	} :
+	docElem.compareDocumentPosition ?
+	function( a, b ) {
+		return b && !!( a.compareDocumentPosition( b ) & 16 );
+	} :
+	function( a, b ) {
+		while ( (b = b.parentNode) ) {
+			if ( b === a ) {
+				return true;
+			}
+		}
+		return false;
+	};
+
+Sizzle.attr = function( elem, name ) {
+	var val,
+		xml = isXML( elem );
+
+	if ( !xml ) {
+		name = name.toLowerCase();
+	}
+	if ( (val = Expr.attrHandle[ name ]) ) {
+		return val( elem );
+	}
+	if ( xml || assertAttributes ) {
+		return elem.getAttribute( name );
+	}
+	val = elem.getAttributeNode( name );
+	return val ?
+		typeof elem[ name ] === "boolean" ?
+			elem[ name ] ? name : null :
+			val.specified ? val.value : null :
+		null;
+};
+
+Expr = Sizzle.selectors = {
+
+	// Can be adjusted by the user
+	cacheLength: 50,
+
+	createPseudo: markFunction,
+
+	match: matchExpr,
+
+	// IE6/7 return a modified href
+	attrHandle: assertHrefNotNormalized ?
+		{} :
+		{
+			"href": function( elem ) {
+				return elem.getAttribute( "href", 2 );
+			},
+			"type": function( elem ) {
+				return elem.getAttribute("type");
+			}
+		},
+
+	find: {
+		"ID": assertGetIdNotName ?
+			function( id, context, xml ) {
+				if ( typeof context.getElementById !== strundefined && !xml ) {
+					var m = context.getElementById( id );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					return m && m.parentNode ? [m] : [];
+				}
+			} :
+			function( id, context, xml ) {
+				if ( typeof context.getElementById !== strundefined && !xml ) {
+					var m = context.getElementById( id );
+
+					return m ?
+						m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ?
+							[m] :
+							undefined :
+						[];
+				}
+			},
+
+		"TAG": assertTagNameNoComments ?
+			function( tag, context ) {
+				if ( typeof context.getElementsByTagName !== strundefined ) {
+					return context.getElementsByTagName( tag );
+				}
+			} :
+			function( tag, context ) {
+				var results = context.getElementsByTagName( tag );
+
+				// Filter out possible comments
+				if ( tag === "*" ) {
+					var elem,
+						tmp = [],
+						i = 0;
+
+					for ( ; (elem = results[i]); i++ ) {
+						if ( elem.nodeType === 1 ) {
+							tmp.push( elem );
+						}
+					}
+
+					return tmp;
+				}
+				return results;
+			},
+
+		"NAME": assertUsableName && function( tag, context ) {
+			if ( typeof context.getElementsByName !== strundefined ) {
+				return context.getElementsByName( name );
+			}
+		},
+
+		"CLASS": assertUsableClassName && function( className, context, xml ) {
+			if ( typeof context.getElementsByClassName !== strundefined && !xml ) {
+				return context.getElementsByClassName( className );
+			}
+		}
+	},
+
+	relative: {
+		">": { dir: "parentNode", first: true },
+		" ": { dir: "parentNode" },
+		"+": { dir: "previousSibling", first: true },
+		"~": { dir: "previousSibling" }
+	},
+
+	preFilter: {
+		"ATTR": function( match ) {
+			match[1] = match[1].replace( rbackslash, "" );
+
+			// Move the given value to match[3] whether quoted or unquoted
+			match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" );
+
+			if ( match[2] === "~=" ) {
+				match[3] = " " + match[3] + " ";
+			}
+
+			return match.slice( 0, 4 );
+		},
+
+		"CHILD": function( match ) {
+			/* matches from matchExpr["CHILD"]
+				1 type (only|nth|...)
+				2 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+				3 xn-component of xn+y argument ([+-]?\d*n|)
+				4 sign of xn-component
+				5 x of xn-component
+				6 sign of y-component
+				7 y of y-component
+			*/
+			match[1] = match[1].toLowerCase();
+
+			if ( match[1] === "nth" ) {
+				// nth-child requires argument
+				if ( !match[2] ) {
+					Sizzle.error( match[0] );
+				}
+
+				// numeric x and y parameters for Expr.filter.CHILD
+				// remember that false/true cast respectively to 0/1
+				match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) );
+				match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" );
+
+			// other types prohibit arguments
+			} else if ( match[2] ) {
+				Sizzle.error( match[0] );
+			}
+
+			return match;
+		},
+
+		"PSEUDO": function( match ) {
+			var unquoted, excess;
+			if ( matchExpr["CHILD"].test( match[0] ) ) {
+				return null;
+			}
+
+			if ( match[3] ) {
+				match[2] = match[3];
+			} else if ( (unquoted = match[4]) ) {
+				// Only check arguments that contain a pseudo
+				if ( rpseudo.test(unquoted) &&
+					// Get excess from tokenize (recursively)
+					(excess = tokenize( unquoted, true )) &&
+					// advance to the next closing parenthesis
+					(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+					// excess is a negative index
+					unquoted = unquoted.slice( 0, excess );
+					match[0] = match[0].slice( 0, excess );
+				}
+				match[2] = unquoted;
+			}
+
+			// Return only captures needed by the pseudo filter method (type and argument)
+			return match.slice( 0, 3 );
+		}
+	},
+
+	filter: {
+		"ID": assertGetIdNotName ?
+			function( id ) {
+				id = id.replace( rbackslash, "" );
+				return function( elem ) {
+					return elem.getAttribute("id") === id;
+				};
+			} :
+			function( id ) {
+				id = id.replace( rbackslash, "" );
+				return function( elem ) {
+					var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+					return node && node.value === id;
+				};
+			},
+
+		"TAG": function( nodeName ) {
+			if ( nodeName === "*" ) {
+				return function() { return true; };
+			}
+			nodeName = nodeName.replace( rbackslash, "" ).toLowerCase();
+
+			return function( elem ) {
+				return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+			};
+		},
+
+		"CLASS": function( className ) {
+			var pattern = classCache[ expando ][ className ];
+			if ( !pattern ) {
+				pattern = classCache( className, new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)") );
+			}
+			return function( elem ) {
+				return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
+			};
+		},
+
+		"ATTR": function( name, operator, check ) {
+			return function( elem, context ) {
+				var result = Sizzle.attr( elem, name );
+
+				if ( result == null ) {
+					return operator === "!=";
+				}
+				if ( !operator ) {
+					return true;
+				}
+
+				result += "";
+
+				return operator === "=" ? result === check :
+					operator === "!=" ? result !== check :
+					operator === "^=" ? check && result.indexOf( check ) === 0 :
+					operator === "*=" ? check && result.indexOf( check ) > -1 :
+					operator === "$=" ? check && result.substr( result.length - check.length ) === check :
+					operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+					operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" :
+					false;
+			};
+		},
+
+		"CHILD": function( type, argument, first, last ) {
+
+			if ( type === "nth" ) {
+				return function( elem ) {
+					var node, diff,
+						parent = elem.parentNode;
+
+					if ( first === 1 && last === 0 ) {
+						return true;
+					}
+
+					if ( parent ) {
+						diff = 0;
+						for ( node = parent.firstChild; node; node = node.nextSibling ) {
+							if ( node.nodeType === 1 ) {
+								diff++;
+								if ( elem === node ) {
+									break;
+								}
+							}
+						}
+					}
+
+					// Incorporate the offset (or cast to NaN), then check against cycle size
+					diff -= last;
+					return diff === first || ( diff % first === 0 && diff / first >= 0 );
+				};
+			}
+
+			return function( elem ) {
+				var node = elem;
+
+				switch ( type ) {
+					case "only":
+					case "first":
+						while ( (node = node.previousSibling) ) {
+							if ( node.nodeType === 1 ) {
+								return false;
+							}
+						}
+
+						if ( type === "first" ) {
+							return true;
+						}
+
+						node = elem;
+
+						/* falls through */
+					case "last":
+						while ( (node = node.nextSibling) ) {
+							if ( node.nodeType === 1 ) {
+								return false;
+							}
+						}
+
+						return true;
+				}
+			};
+		},
+
+		"PSEUDO": function( pseudo, argument ) {
+			// pseudo-class names are case-insensitive
+			// http://www.w3.org/TR/selectors/#pseudo-classes
+			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+			// Remember that setFilters inherits from pseudos
+			var args,
+				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+					Sizzle.error( "unsupported pseudo: " + pseudo );
+
+			// The user may use createPseudo to indicate that
+			// arguments are needed to create the filter function
+			// just as Sizzle does
+			if ( fn[ expando ] ) {
+				return fn( argument );
+			}
+
+			// But maintain support for old signatures
+			if ( fn.length > 1 ) {
+				args = [ pseudo, pseudo, "", argument ];
+				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+					markFunction(function( seed, matches ) {
+						var idx,
+							matched = fn( seed, argument ),
+							i = matched.length;
+						while ( i-- ) {
+							idx = indexOf.call( seed, matched[i] );
+							seed[ idx ] = !( matches[ idx ] = matched[i] );
+						}
+					}) :
+					function( elem ) {
+						return fn( elem, 0, args );
+					};
+			}
+
+			return fn;
+		}
+	},
+
+	pseudos: {
+		"not": markFunction(function( selector ) {
+			// Trim the selector passed to compile
+			// to avoid treating leading and trailing
+			// spaces as combinators
+			var input = [],
+				results = [],
+				matcher = compile( selector.replace( rtrim, "$1" ) );
+
+			return matcher[ expando ] ?
+				markFunction(function( seed, matches, context, xml ) {
+					var elem,
+						unmatched = matcher( seed, null, xml, [] ),
+						i = seed.length;
+
+					// Match elements unmatched by `matcher`
+					while ( i-- ) {
+						if ( (elem = unmatched[i]) ) {
+							seed[i] = !(matches[i] = elem);
+						}
+					}
+				}) :
+				function( elem, context, xml ) {
+					input[0] = elem;
+					matcher( input, null, xml, results );
+					return !results.pop();
+				};
+		}),
+
+		"has": markFunction(function( selector ) {
+			return function( elem ) {
+				return Sizzle( selector, elem ).length > 0;
+			};
+		}),
+
+		"contains": markFunction(function( text ) {
+			return function( elem ) {
+				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+			};
+		}),
+
+		"enabled": function( elem ) {
+			return elem.disabled === false;
+		},
+
+		"disabled": function( elem ) {
+			return elem.disabled === true;
+		},
+
+		"checked": function( elem ) {
+			// In CSS3, :checked should return both checked and selected elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			var nodeName = elem.nodeName.toLowerCase();
+			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+		},
+
+		"selected": function( elem ) {
+			// Accessing this property makes selected-by-default
+			// options in Safari work properly
+			if ( elem.parentNode ) {
+				elem.parentNode.selectedIndex;
+			}
+
+			return elem.selected === true;
+		},
+
+		"parent": function( elem ) {
+			return !Expr.pseudos["empty"]( elem );
+		},
+
+		"empty": function( elem ) {
+			// http://www.w3.org/TR/selectors/#empty-pseudo
+			// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+			//   not comment, processing instructions, or others
+			// Thanks to Diego Perini for the nodeName shortcut
+			//   Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+			var nodeType;
+			elem = elem.firstChild;
+			while ( elem ) {
+				if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) {
+					return false;
+				}
+				elem = elem.nextSibling;
+			}
+			return true;
+		},
+
+		"header": function( elem ) {
+			return rheader.test( elem.nodeName );
+		},
+
+		"text": function( elem ) {
+			var type, attr;
+			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+			// use getAttribute instead to test this case
+			return elem.nodeName.toLowerCase() === "input" &&
+				(type = elem.type) === "text" &&
+				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type );
+		},
+
+		// Input types
+		"radio": createInputPseudo("radio"),
+		"checkbox": createInputPseudo("checkbox"),
+		"file": createInputPseudo("file"),
+		"password": createInputPseudo("password"),
+		"image": createInputPseudo("image"),
+
+		"submit": createButtonPseudo("submit"),
+		"reset": createButtonPseudo("reset"),
+
+		"button": function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return name === "input" && elem.type === "button" || name === "button";
+		},
+
+		"input": function( elem ) {
+			return rinputs.test( elem.nodeName );
+		},
+
+		"focus": function( elem ) {
+			var doc = elem.ownerDocument;
+			return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href);
+		},
+
+		"active": function( elem ) {
+			return elem === elem.ownerDocument.activeElement;
+		},
+
+		// Positional types
+		"first": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ 0 ];
+		}),
+
+		"last": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ length - 1 ];
+		}),
+
+		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ argument < 0 ? argument + length : argument ];
+		}),
+
+		"even": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = 0; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"odd": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = 1; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		})
+	}
+};
+
+function siblingCheck( a, b, ret ) {
+	if ( a === b ) {
+		return ret;
+	}
+
+	var cur = a.nextSibling;
+
+	while ( cur ) {
+		if ( cur === b ) {
+			return -1;
+		}
+
+		cur = cur.nextSibling;
+	}
+
+	return 1;
+}
+
+sortOrder = docElem.compareDocumentPosition ?
+	function( a, b ) {
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+
+		return ( !a.compareDocumentPosition || !b.compareDocumentPosition ?
+			a.compareDocumentPosition :
+			a.compareDocumentPosition(b) & 4
+		) ? -1 : 1;
+	} :
+	function( a, b ) {
+		// The nodes are identical, we can exit early
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+
+		// Fallback to using sourceIndex (in IE) if it's available on both nodes
+		} else if ( a.sourceIndex && b.sourceIndex ) {
+			return a.sourceIndex - b.sourceIndex;
+		}
+
+		var al, bl,
+			ap = [],
+			bp = [],
+			aup = a.parentNode,
+			bup = b.parentNode,
+			cur = aup;
+
+		// If the nodes are siblings (or identical) we can do a quick check
+		if ( aup === bup ) {
+			return siblingCheck( a, b );
+
+		// If no parents were found then the nodes are disconnected
+		} else if ( !aup ) {
+			return -1;
+
+		} else if ( !bup ) {
+			return 1;
+		}
+
+		// Otherwise they're somewhere else in the tree so we need
+		// to build up a full list of the parentNodes for comparison
+		while ( cur ) {
+			ap.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		cur = bup;
+
+		while ( cur ) {
+			bp.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		al = ap.length;
+		bl = bp.length;
+
+		// Start walking down the tree looking for a discrepancy
+		for ( var i = 0; i < al && i < bl; i++ ) {
+			if ( ap[i] !== bp[i] ) {
+				return siblingCheck( ap[i], bp[i] );
+			}
+		}
+
+		// We ended someplace up the tree so do a sibling check
+		return i === al ?
+			siblingCheck( a, bp[i], -1 ) :
+			siblingCheck( ap[i], b, 1 );
+	};
+
+// Always assume the presence of duplicates if sort doesn't
+// pass them to our comparison function (as in Google Chrome).
+[0, 0].sort( sortOrder );
+baseHasDuplicate = !hasDuplicate;
+
+// Document sorting and removing duplicates
+Sizzle.uniqueSort = function( results ) {
+	var elem,
+		i = 1;
+
+	hasDuplicate = baseHasDuplicate;
+	results.sort( sortOrder );
+
+	if ( hasDuplicate ) {
+		for ( ; (elem = results[i]); i++ ) {
+			if ( elem === results[ i - 1 ] ) {
+				results.splice( i--, 1 );
+			}
+		}
+	}
+
+	return results;
+};
+
+Sizzle.error = function( msg ) {
+	throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+function tokenize( selector, parseOnly ) {
+	var matched, match, tokens, type, soFar, groups, preFilters,
+		cached = tokenCache[ expando ][ selector ];
+
+	if ( cached ) {
+		return parseOnly ? 0 : cached.slice( 0 );
+	}
+
+	soFar = selector;
+	groups = [];
+	preFilters = Expr.preFilter;
+
+	while ( soFar ) {
+
+		// Comma and first run
+		if ( !matched || (match = rcomma.exec( soFar )) ) {
+			if ( match ) {
+				soFar = soFar.slice( match[0].length );
+			}
+			groups.push( tokens = [] );
+		}
+
+		matched = false;
+
+		// Combinators
+		if ( (match = rcombinators.exec( soFar )) ) {
+			tokens.push( matched = new Token( match.shift() ) );
+			soFar = soFar.slice( matched.length );
+
+			// Cast descendant combinators to space
+			matched.type = match[0].replace( rtrim, " " );
+		}
+
+		// Filters
+		for ( type in Expr.filter ) {
+			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+				// The last two arguments here are (context, xml) for backCompat
+				(match = preFilters[ type ]( match, document, true ))) ) {
+
+				tokens.push( matched = new Token( match.shift() ) );
+				soFar = soFar.slice( matched.length );
+				matched.type = type;
+				matched.matches = match;
+			}
+		}
+
+		if ( !matched ) {
+			break;
+		}
+	}
+
+	// Return the length of the invalid excess
+	// if we're just parsing
+	// Otherwise, throw an error or return tokens
+	return parseOnly ?
+		soFar.length :
+		soFar ?
+			Sizzle.error( selector ) :
+			// Cache the tokens
+			tokenCache( selector, groups ).slice( 0 );
+}
+
+function addCombinator( matcher, combinator, base ) {
+	var dir = combinator.dir,
+		checkNonElements = base && combinator.dir === "parentNode",
+		doneName = done++;
+
+	return combinator.first ?
+		// Check against closest ancestor/preceding element
+		function( elem, context, xml ) {
+			while ( (elem = elem[ dir ]) ) {
+				if ( checkNonElements || elem.nodeType === 1  ) {
+					return matcher( elem, context, xml );
+				}
+			}
+		} :
+
+		// Check against all ancestor/preceding elements
+		function( elem, context, xml ) {
+			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+			if ( !xml ) {
+				var cache,
+					dirkey = dirruns + " " + doneName + " ",
+					cachedkey = dirkey + cachedruns;
+				while ( (elem = elem[ dir ]) ) {
+					if ( checkNonElements || elem.nodeType === 1 ) {
+						if ( (cache = elem[ expando ]) === cachedkey ) {
+							return elem.sizset;
+						} else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) {
+							if ( elem.sizset ) {
+								return elem;
+							}
+						} else {
+							elem[ expando ] = cachedkey;
+							if ( matcher( elem, context, xml ) ) {
+								elem.sizset = true;
+								return elem;
+							}
+							elem.sizset = false;
+						}
+					}
+				}
+			} else {
+				while ( (elem = elem[ dir ]) ) {
+					if ( checkNonElements || elem.nodeType === 1 ) {
+						if ( matcher( elem, context, xml ) ) {
+							return elem;
+						}
+					}
+				}
+			}
+		};
+}
+
+function elementMatcher( matchers ) {
+	return matchers.length > 1 ?
+		function( elem, context, xml ) {
+			var i = matchers.length;
+			while ( i-- ) {
+				if ( !matchers[i]( elem, context, xml ) ) {
+					return false;
+				}
+			}
+			return true;
+		} :
+		matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+	var elem,
+		newUnmatched = [],
+		i = 0,
+		len = unmatched.length,
+		mapped = map != null;
+
+	for ( ; i < len; i++ ) {
+		if ( (elem = unmatched[i]) ) {
+			if ( !filter || filter( elem, context, xml ) ) {
+				newUnmatched.push( elem );
+				if ( mapped ) {
+					map.push( i );
+				}
+			}
+		}
+	}
+
+	return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+	if ( postFilter && !postFilter[ expando ] ) {
+		postFilter = setMatcher( postFilter );
+	}
+	if ( postFinder && !postFinder[ expando ] ) {
+		postFinder = setMatcher( postFinder, postSelector );
+	}
+	return markFunction(function( seed, results, context, xml ) {
+		// Positional selectors apply to seed elements, so it is invalid to follow them with relative ones
+		if ( seed && postFinder ) {
+			return;
+		}
+
+		var i, elem, postFilterIn,
+			preMap = [],
+			postMap = [],
+			preexisting = results.length,
+
+			// Get initial elements from seed or context
+			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [], seed ),
+
+			// Prefilter to get matcher input, preserving a map for seed-results synchronization
+			matcherIn = preFilter && ( seed || !selector ) ?
+				condense( elems, preMap, preFilter, context, xml ) :
+				elems,
+
+			matcherOut = matcher ?
+				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+					// ...intermediate processing is necessary
+					[] :
+
+					// ...otherwise use results directly
+					results :
+				matcherIn;
+
+		// Find primary matches
+		if ( matcher ) {
+			matcher( matcherIn, matcherOut, context, xml );
+		}
+
+		// Apply postFilter
+		if ( postFilter ) {
+			postFilterIn = condense( matcherOut, postMap );
+			postFilter( postFilterIn, [], context, xml );
+
+			// Un-match failing elements by moving them back to matcherIn
+			i = postFilterIn.length;
+			while ( i-- ) {
+				if ( (elem = postFilterIn[i]) ) {
+					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+				}
+			}
+		}
+
+		// Keep seed and results synchronized
+		if ( seed ) {
+			// Ignore postFinder because it can't coexist with seed
+			i = preFilter && matcherOut.length;
+			while ( i-- ) {
+				if ( (elem = matcherOut[i]) ) {
+					seed[ preMap[i] ] = !(results[ preMap[i] ] = elem);
+				}
+			}
+		} else {
+			matcherOut = condense(
+				matcherOut === results ?
+					matcherOut.splice( preexisting, matcherOut.length ) :
+					matcherOut
+			);
+			if ( postFinder ) {
+				postFinder( null, results, matcherOut, xml );
+			} else {
+				push.apply( results, matcherOut );
+			}
+		}
+	});
+}
+
+function matcherFromTokens( tokens ) {
+	var checkContext, matcher, j,
+		len = tokens.length,
+		leadingRelative = Expr.relative[ tokens[0].type ],
+		implicitRelative = leadingRelative || Expr.relative[" "],
+		i = leadingRelative ? 1 : 0,
+
+		// The foundational matcher ensures that elements are reachable from top-level context(s)
+		matchContext = addCombinator( function( elem ) {
+			return elem === checkContext;
+		}, implicitRelative, true ),
+		matchAnyContext = addCombinator( function( elem ) {
+			return indexOf.call( checkContext, elem ) > -1;
+		}, implicitRelative, true ),
+		matchers = [ function( elem, context, xml ) {
+			return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+				(checkContext = context).nodeType ?
+					matchContext( elem, context, xml ) :
+					matchAnyContext( elem, context, xml ) );
+		} ];
+
+	for ( ; i < len; i++ ) {
+		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+			matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
+		} else {
+			// The concatenated values are (context, xml) for backCompat
+			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+			// Return special upon seeing a positional matcher
+			if ( matcher[ expando ] ) {
+				// Find the next relative operator (if any) for proper handling
+				j = ++i;
+				for ( ; j < len; j++ ) {
+					if ( Expr.relative[ tokens[j].type ] ) {
+						break;
+					}
+				}
+				return setMatcher(
+					i > 1 && elementMatcher( matchers ),
+					i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ),
+					matcher,
+					i < j && matcherFromTokens( tokens.slice( i, j ) ),
+					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+					j < len && tokens.join("")
+				);
+			}
+			matchers.push( matcher );
+		}
+	}
+
+	return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+	var bySet = setMatchers.length > 0,
+		byElement = elementMatchers.length > 0,
+		superMatcher = function( seed, context, xml, results, expandContext ) {
+			var elem, j, matcher,
+				setMatched = [],
+				matchedCount = 0,
+				i = "0",
+				unmatched = seed && [],
+				outermost = expandContext != null,
+				contextBackup = outermostContext,
+				// We must always have either seed elements or context
+				elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+				// Nested matchers should use non-integer dirruns
+				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E);
+
+			if ( outermost ) {
+				outermostContext = context !== document && context;
+				cachedruns = superMatcher.el;
+			}
+
+			// Add elements passing elementMatchers directly to results
+			for ( ; (elem = elems[i]) != null; i++ ) {
+				if ( byElement && elem ) {
+					for ( j = 0; (matcher = elementMatchers[j]); j++ ) {
+						if ( matcher( elem, context, xml ) ) {
+							results.push( elem );
+							break;
+						}
+					}
+					if ( outermost ) {
+						dirruns = dirrunsUnique;
+						cachedruns = ++superMatcher.el;
+					}
+				}
+
+				// Track unmatched elements for set filters
+				if ( bySet ) {
+					// They will have gone through all possible matchers
+					if ( (elem = !matcher && elem) ) {
+						matchedCount--;
+					}
+
+					// Lengthen the array for every element, matched or not
+					if ( seed ) {
+						unmatched.push( elem );
+					}
+				}
+			}
+
+			// Apply set filters to unmatched elements
+			matchedCount += i;
+			if ( bySet && i !== matchedCount ) {
+				for ( j = 0; (matcher = setMatchers[j]); j++ ) {
+					matcher( unmatched, setMatched, context, xml );
+				}
+
+				if ( seed ) {
+					// Reintegrate element matches to eliminate the need for sorting
+					if ( matchedCount > 0 ) {
+						while ( i-- ) {
+							if ( !(unmatched[i] || setMatched[i]) ) {
+								setMatched[i] = pop.call( results );
+							}
+						}
+					}
+
+					// Discard index placeholder values to get only actual matches
+					setMatched = condense( setMatched );
+				}
+
+				// Add matches to results
+				push.apply( results, setMatched );
+
+				// Seedless set matches succeeding multiple successful matchers stipulate sorting
+				if ( outermost && !seed && setMatched.length > 0 &&
+					( matchedCount + setMatchers.length ) > 1 ) {
+
+					Sizzle.uniqueSort( results );
+				}
+			}
+
+			// Override manipulation of globals by nested matchers
+			if ( outermost ) {
+				dirruns = dirrunsUnique;
+				outermostContext = contextBackup;
+			}
+
+			return unmatched;
+		};
+
+	superMatcher.el = 0;
+	return bySet ?
+		markFunction( superMatcher ) :
+		superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+	var i,
+		setMatchers = [],
+		elementMatchers = [],
+		cached = compilerCache[ expando ][ selector ];
+
+	if ( !cached ) {
+		// Generate a function of recursive functions that can be used to check each element
+		if ( !group ) {
+			group = tokenize( selector );
+		}
+		i = group.length;
+		while ( i-- ) {
+			cached = matcherFromTokens( group[i] );
+			if ( cached[ expando ] ) {
+				setMatchers.push( cached );
+			} else {
+				elementMatchers.push( cached );
+			}
+		}
+
+		// Cache the compiled function
+		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+	}
+	return cached;
+};
+
+function multipleContexts( selector, contexts, results, seed ) {
+	var i = 0,
+		len = contexts.length;
+	for ( ; i < len; i++ ) {
+		Sizzle( selector, contexts[i], results, seed );
+	}
+	return results;
+}
+
+function select( selector, context, results, seed, xml ) {
+	var i, tokens, token, type, find,
+		match = tokenize( selector ),
+		j = match.length;
+
+	if ( !seed ) {
+		// Try to minimize operations if there is only one group
+		if ( match.length === 1 ) {
+
+			// Take a shortcut and set the context if the root selector is an ID
+			tokens = match[0] = match[0].slice( 0 );
+			if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+					context.nodeType === 9 && !xml &&
+					Expr.relative[ tokens[1].type ] ) {
+
+				context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0];
+				if ( !context ) {
+					return results;
+				}
+
+				selector = selector.slice( tokens.shift().length );
+			}
+
+			// Fetch a seed set for right-to-left matching
+			for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) {
+				token = tokens[i];
+
+				// Abort if we hit a combinator
+				if ( Expr.relative[ (type = token.type) ] ) {
+					break;
+				}
+				if ( (find = Expr.find[ type ]) ) {
+					// Search, expanding context for leading sibling combinators
+					if ( (seed = find(
+						token.matches[0].replace( rbackslash, "" ),
+						rsibling.test( tokens[0].type ) && context.parentNode || context,
+						xml
+					)) ) {
+
+						// If seed is empty or no tokens remain, we can return early
+						tokens.splice( i, 1 );
+						selector = seed.length && tokens.join("");
+						if ( !selector ) {
+							push.apply( results, slice.call( seed, 0 ) );
+							return results;
+						}
+
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// Compile and execute a filtering function
+	// Provide `match` to avoid retokenization if we modified the selector above
+	compile( selector, match )(
+		seed,
+		context,
+		xml,
+		results,
+		rsibling.test( selector )
+	);
+	return results;
+}
+
+if ( document.querySelectorAll ) {
+	(function() {
+		var disconnectedMatch,
+			oldSelect = select,
+			rescape = /'|\\/g,
+			rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,
+
+			// qSa(:focus) reports false when true (Chrome 21),
+			// A support test would require too much code (would include document ready)
+			rbuggyQSA = [":focus"],
+
+			// matchesSelector(:focus) reports false when true (Chrome 21),
+			// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+			// A support test would require too much code (would include document ready)
+			// just skip matchesSelector for :active
+			rbuggyMatches = [ ":active", ":focus" ],
+			matches = docElem.matchesSelector ||
+				docElem.mozMatchesSelector ||
+				docElem.webkitMatchesSelector ||
+				docElem.oMatchesSelector ||
+				docElem.msMatchesSelector;
+
+		// Build QSA regex
+		// Regex strategy adopted from Diego Perini
+		assert(function( div ) {
+			// Select is set to empty string on purpose
+			// This is to test IE's treatment of not explictly
+			// setting a boolean content attribute,
+			// since its presence should be enough
+			// http://bugs.jquery.com/ticket/12359
+			div.innerHTML = "<select><option selected=''></option></select>";
+
+			// IE8 - Some boolean attributes are not treated correctly
+			if ( !div.querySelectorAll("[selected]").length ) {
+				rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" );
+			}
+
+			// Webkit/Opera - :checked should return selected option elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			// IE8 throws error here (do not put tests after this one)
+			if ( !div.querySelectorAll(":checked").length ) {
+				rbuggyQSA.push(":checked");
+			}
+		});
+
+		assert(function( div ) {
+
+			// Opera 10-12/IE9 - ^= $= *= and empty values
+			// Should not select anything
+			div.innerHTML = "<p test=''></p>";
+			if ( div.querySelectorAll("[test^='']").length ) {
+				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" );
+			}
+
+			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+			// IE8 throws error here (do not put tests after this one)
+			div.innerHTML = "<input type='hidden'/>";
+			if ( !div.querySelectorAll(":enabled").length ) {
+				rbuggyQSA.push(":enabled", ":disabled");
+			}
+		});
+
+		// rbuggyQSA always contains :focus, so no need for a length check
+		rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") );
+
+		select = function( selector, context, results, seed, xml ) {
+			// Only use querySelectorAll when not filtering,
+			// when this is not xml,
+			// and when no QSA bugs apply
+			if ( !seed && !xml && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+				var groups, i,
+					old = true,
+					nid = expando,
+					newContext = context,
+					newSelector = context.nodeType === 9 && selector;
+
+				// qSA works strangely on Element-rooted queries
+				// We can work around this by specifying an extra ID on the root
+				// and working up from there (Thanks to Andrew Dupont for the technique)
+				// IE 8 doesn't work on object elements
+				if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+					groups = tokenize( selector );
+
+					if ( (old = context.getAttribute("id")) ) {
+						nid = old.replace( rescape, "\\$&" );
+					} else {
+						context.setAttribute( "id", nid );
+					}
+					nid = "[id='" + nid + "'] ";
+
+					i = groups.length;
+					while ( i-- ) {
+						groups[i] = nid + groups[i].join("");
+					}
+					newContext = rsibling.test( selector ) && context.parentNode || context;
+					newSelector = groups.join(",");
+				}
+
+				if ( newSelector ) {
+					try {
+						push.apply( results, slice.call( newContext.querySelectorAll(
+							newSelector
+						), 0 ) );
+						return results;
+					} catch(qsaError) {
+					} finally {
+						if ( !old ) {
+							context.removeAttribute("id");
+						}
+					}
+				}
+			}
+
+			return oldSelect( selector, context, results, seed, xml );
+		};
+
+		if ( matches ) {
+			assert(function( div ) {
+				// Check to see if it's possible to do matchesSelector
+				// on a disconnected node (IE 9)
+				disconnectedMatch = matches.call( div, "div" );
+
+				// This should fail with an exception
+				// Gecko does not error, returns false instead
+				try {
+					matches.call( div, "[test!='']:sizzle" );
+					rbuggyMatches.push( "!=", pseudos );
+				} catch ( e ) {}
+			});
+
+			// rbuggyMatches always contains :active and :focus, so no need for a length check
+			rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") );
+
+			Sizzle.matchesSelector = function( elem, expr ) {
+				// Make sure that attribute selectors are quoted
+				expr = expr.replace( rattributeQuotes, "='$1']" );
+
+				// rbuggyMatches always contains :active, so no need for an existence check
+				if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && (!rbuggyQSA || !rbuggyQSA.test( expr )) ) {
+					try {
+						var ret = matches.call( elem, expr );
+
+						// IE 9's matchesSelector returns false on disconnected nodes
+						if ( ret || disconnectedMatch ||
+								// As well, disconnected nodes are said to be in a document
+								// fragment in IE 9
+								elem.document && elem.document.nodeType !== 11 ) {
+							return ret;
+						}
+					} catch(e) {}
+				}
+
+				return Sizzle( expr, null, null, [ elem ] ).length > 0;
+			};
+		}
+	})();
+}
+
+// Deprecated
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Back-compat
+function setFilters() {}
+Expr.filters = setFilters.prototype = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+// Override sizzle attribute retrieval
+Sizzle.attr = jQuery.attr;
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})( window );
+var runtil = /Until$/,
+	rparentsprev = /^(?:parents|prev(?:Until|All))/,
+	isSimple = /^.[^:#\[\.,]*$/,
+	rneedsContext = jQuery.expr.match.needsContext,
+	// methods guaranteed to produce a unique set when starting from a unique set
+	guaranteedUnique = {
+		children: true,
+		contents: true,
+		next: true,
+		prev: true
+	};
+
+jQuery.fn.extend({
+	find: function( selector ) {
+		var i, l, length, n, r, ret,
+			self = this;
+
+		if ( typeof selector !== "string" ) {
+			return jQuery( selector ).filter(function() {
+				for ( i = 0, l = self.length; i < l; i++ ) {
+					if ( jQuery.contains( self[ i ], this ) ) {
+						return true;
+					}
+				}
+			});
+		}
+
+		ret = this.pushStack( "", "find", selector );
+
+		for ( i = 0, l = this.length; i < l; i++ ) {
+			length = ret.length;
+			jQuery.find( selector, this[i], ret );
+
+			if ( i > 0 ) {
+				// Make sure that the results are unique
+				for ( n = length; n < ret.length; n++ ) {
+					for ( r = 0; r < length; r++ ) {
+						if ( ret[r] === ret[n] ) {
+							ret.splice(n--, 1);
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	has: function( target ) {
+		var i,
+			targets = jQuery( target, this ),
+			len = targets.length;
+
+		return this.filter(function() {
+			for ( i = 0; i < len; i++ ) {
+				if ( jQuery.contains( this, targets[i] ) ) {
+					return true;
+				}
+			}
+		});
+	},
+
+	not: function( selector ) {
+		return this.pushStack( winnow(this, selector, false), "not", selector);
+	},
+
+	filter: function( selector ) {
+		return this.pushStack( winnow(this, selector, true), "filter", selector );
+	},
+
+	is: function( selector ) {
+		return !!selector && (
+			typeof selector === "string" ?
+				// If this is a positional/relative selector, check membership in the returned set
+				// so $("p:first").is("p:last") won't return true for a doc with two "p".
+				rneedsContext.test( selector ) ?
+					jQuery( selector, this.context ).index( this[0] ) >= 0 :
+					jQuery.filter( selector, this ).length > 0 :
+				this.filter( selector ).length > 0 );
+	},
+
+	closest: function( selectors, context ) {
+		var cur,
+			i = 0,
+			l = this.length,
+			ret = [],
+			pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+				jQuery( selectors, context || this.context ) :
+				0;
+
+		for ( ; i < l; i++ ) {
+			cur = this[i];
+
+			while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) {
+				if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+					ret.push( cur );
+					break;
+				}
+				cur = cur.parentNode;
+			}
+		}
+
+		ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
+
+		return this.pushStack( ret, "closest", selectors );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+
+		// No argument, return index in parent
+		if ( !elem ) {
+			return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;
+		}
+
+		// index in selector
+		if ( typeof elem === "string" ) {
+			return jQuery.inArray( this[0], jQuery( elem ) );
+		}
+
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem.jquery ? elem[0] : elem, this );
+	},
+
+	add: function( selector, context ) {
+		var set = typeof selector === "string" ?
+				jQuery( selector, context ) :
+				jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+			all = jQuery.merge( this.get(), set );
+
+		return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+			all :
+			jQuery.unique( all ) );
+	},
+
+	addBack: function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter(selector)
+		);
+	}
+});
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+	return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+function sibling( cur, dir ) {
+	do {
+		cur = cur[ dir ];
+	} while ( cur && cur.nodeType !== 1 );
+
+	return cur;
+}
+
+jQuery.each({
+	parent: function( elem ) {
+		var parent = elem.parentNode;
+		return parent && parent.nodeType !== 11 ? parent : null;
+	},
+	parents: function( elem ) {
+		return jQuery.dir( elem, "parentNode" );
+	},
+	parentsUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "parentNode", until );
+	},
+	next: function( elem ) {
+		return sibling( elem, "nextSibling" );
+	},
+	prev: function( elem ) {
+		return sibling( elem, "previousSibling" );
+	},
+	nextAll: function( elem ) {
+		return jQuery.dir( elem, "nextSibling" );
+	},
+	prevAll: function( elem ) {
+		return jQuery.dir( elem, "previousSibling" );
+	},
+	nextUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "nextSibling", until );
+	},
+	prevUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "previousSibling", until );
+	},
+	siblings: function( elem ) {
+		return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+	},
+	children: function( elem ) {
+		return jQuery.sibling( elem.firstChild );
+	},
+	contents: function( elem ) {
+		return jQuery.nodeName( elem, "iframe" ) ?
+			elem.contentDocument || elem.contentWindow.document :
+			jQuery.merge( [], elem.childNodes );
+	}
+}, function( name, fn ) {
+	jQuery.fn[ name ] = function( until, selector ) {
+		var ret = jQuery.map( this, fn, until );
+
+		if ( !runtil.test( name ) ) {
+			selector = until;
+		}
+
+		if ( selector && typeof selector === "string" ) {
+			ret = jQuery.filter( selector, ret );
+		}
+
+		ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+		if ( this.length > 1 && rparentsprev.test( name ) ) {
+			ret = ret.reverse();
+		}
+
+		return this.pushStack( ret, name, core_slice.call( arguments ).join(",") );
+	};
+});
+
+jQuery.extend({
+	filter: function( expr, elems, not ) {
+		if ( not ) {
+			expr = ":not(" + expr + ")";
+		}
+
+		return elems.length === 1 ?
+			jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+			jQuery.find.matches(expr, elems);
+	},
+
+	dir: function( elem, dir, until ) {
+		var matched = [],
+			cur = elem[ dir ];
+
+		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+			if ( cur.nodeType === 1 ) {
+				matched.push( cur );
+			}
+			cur = cur[dir];
+		}
+		return matched;
+	},
+
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType === 1 && n !== elem ) {
+				r.push( n );
+			}
+		}
+
+		return r;
+	}
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+	// Can't pass null or undefined to indexOf in Firefox 4
+	// Set to 0 to skip string check
+	qualifier = qualifier || 0;
+
+	if ( jQuery.isFunction( qualifier ) ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			var retVal = !!qualifier.call( elem, i, elem );
+			return retVal === keep;
+		});
+
+	} else if ( qualifier.nodeType ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			return ( elem === qualifier ) === keep;
+		});
+
+	} else if ( typeof qualifier === "string" ) {
+		var filtered = jQuery.grep(elements, function( elem ) {
+			return elem.nodeType === 1;
+		});
+
+		if ( isSimple.test( qualifier ) ) {
+			return jQuery.filter(qualifier, filtered, !keep);
+		} else {
+			qualifier = jQuery.filter( qualifier, filtered );
+		}
+	}
+
+	return jQuery.grep(elements, function( elem, i ) {
+		return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
+	});
+}
+function createSafeFragment( document ) {
+	var list = nodeNames.split( "|" ),
+	safeFrag = document.createDocumentFragment();
+
+	if ( safeFrag.createElement ) {
+		while ( list.length ) {
+			safeFrag.createElement(
+				list.pop()
+			);
+		}
+	}
+	return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+	rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+	rleadingWhitespace = /^\s+/,
+	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+	rtagName = /<([\w:]+)/,
+	rtbody = /<tbody/i,
+	rhtml = /<|&#?\w+;/,
+	rnoInnerhtml = /<(?:script|style|link)/i,
+	rnocache = /<(?:script|object|embed|option|style)/i,
+	rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+	rcheckableType = /^(?:checkbox|radio)$/,
+	// checked="checked" or checked
+	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+	rscriptType = /\/(java|ecma)script/i,
+	rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,
+	wrapMap = {
+		option: [ 1, "<select multiple='multiple'>", "</select>" ],
+		legend: [ 1, "<fieldset>", "</fieldset>" ],
+		thead: [ 1, "<table>", "</table>" ],
+		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+		area: [ 1, "<map>", "</map>" ],
+		_default: [ 0, "", "" ]
+	},
+	safeFragment = createSafeFragment( document ),
+	fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+// unless wrapped in a div with non-breaking characters in front of it.
+if ( !jQuery.support.htmlSerialize ) {
+	wrapMap._default = [ 1, "X<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+	text: function( value ) {
+		return jQuery.access( this, function( value ) {
+			return value === undefined ?
+				jQuery.text( this ) :
+				this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+		}, null, value, arguments.length );
+	},
+
+	wrapAll: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapAll( html.call(this, i) );
+			});
+		}
+
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+			if ( this[0].parentNode ) {
+				wrap.insertBefore( this[0] );
+			}
+
+			wrap.map(function() {
+				var elem = this;
+
+				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+					elem = elem.firstChild;
+				}
+
+				return elem;
+			}).append( this );
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapInner( html.call(this, i) );
+			});
+		}
+
+		return this.each(function() {
+			var self = jQuery( this ),
+				contents = self.contents();
+
+			if ( contents.length ) {
+				contents.wrapAll( html );
+
+			} else {
+				self.append( html );
+			}
+		});
+	},
+
+	wrap: function( html ) {
+		var isFunction = jQuery.isFunction( html );
+
+		return this.each(function(i) {
+			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+		});
+	},
+
+	unwrap: function() {
+		return this.parent().each(function() {
+			if ( !jQuery.nodeName( this, "body" ) ) {
+				jQuery( this ).replaceWith( this.childNodes );
+			}
+		}).end();
+	},
+
+	append: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 ) {
+				this.appendChild( elem );
+			}
+		});
+	},
+
+	prepend: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 ) {
+				this.insertBefore( elem, this.firstChild );
+			}
+		});
+	},
+
+	before: function() {
+		if ( !isDisconnected( this[0] ) ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this );
+			});
+		}
+
+		if ( arguments.length ) {
+			var set = jQuery.clean( arguments );
+			return this.pushStack( jQuery.merge( set, this ), "before", this.selector );
+		}
+	},
+
+	after: function() {
+		if ( !isDisconnected( this[0] ) ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this.nextSibling );
+			});
+		}
+
+		if ( arguments.length ) {
+			var set = jQuery.clean( arguments );
+			return this.pushStack( jQuery.merge( this, set ), "after", this.selector );
+		}
+	},
+
+	// keepData is for internal use only--do not document
+	remove: function( selector, keepData ) {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+				if ( !keepData && elem.nodeType === 1 ) {
+					jQuery.cleanData( elem.getElementsByTagName("*") );
+					jQuery.cleanData( [ elem ] );
+				}
+
+				if ( elem.parentNode ) {
+					elem.parentNode.removeChild( elem );
+				}
+			}
+		}
+
+		return this;
+	},
+
+	empty: function() {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			// Remove element nodes and prevent memory leaks
+			if ( elem.nodeType === 1 ) {
+				jQuery.cleanData( elem.getElementsByTagName("*") );
+			}
+
+			// Remove any remaining nodes
+			while ( elem.firstChild ) {
+				elem.removeChild( elem.firstChild );
+			}
+		}
+
+		return this;
+	},
+
+	clone: function( dataAndEvents, deepDataAndEvents ) {
+		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+		return this.map( function () {
+			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+		});
+	},
+
+	html: function( value ) {
+		return jQuery.access( this, function( value ) {
+			var elem = this[0] || {},
+				i = 0,
+				l = this.length;
+
+			if ( value === undefined ) {
+				return elem.nodeType === 1 ?
+					elem.innerHTML.replace( rinlinejQuery, "" ) :
+					undefined;
+			}
+
+			// See if we can take a shortcut and just use innerHTML
+			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+				( jQuery.support.htmlSerialize || !rnoshimcache.test( value )  ) &&
+				( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+				!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+				value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+				try {
+					for (; i < l; i++ ) {
+						// Remove element nodes and prevent memory leaks
+						elem = this[i] || {};
+						if ( elem.nodeType === 1 ) {
+							jQuery.cleanData( elem.getElementsByTagName( "*" ) );
+							elem.innerHTML = value;
+						}
+					}
+
+					elem = 0;
+
+				// If using innerHTML throws an exception, use the fallback method
+				} catch(e) {}
+			}
+
+			if ( elem ) {
+				this.empty().append( value );
+			}
+		}, null, value, arguments.length );
+	},
+
+	replaceWith: function( value ) {
+		if ( !isDisconnected( this[0] ) ) {
+			// Make sure that the elements are removed from the DOM before they are inserted
+			// this can help fix replacing a parent with child elements
+			if ( jQuery.isFunction( value ) ) {
+				return this.each(function(i) {
+					var self = jQuery(this), old = self.html();
+					self.replaceWith( value.call( this, i, old ) );
+				});
+			}
+
+			if ( typeof value !== "string" ) {
+				value = jQuery( value ).detach();
+			}
+
+			return this.each(function() {
+				var next = this.nextSibling,
+					parent = this.parentNode;
+
+				jQuery( this ).remove();
+
+				if ( next ) {
+					jQuery(next).before( value );
+				} else {
+					jQuery(parent).append( value );
+				}
+			});
+		}
+
+		return this.length ?
+			this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+			this;
+	},
+
+	detach: function( selector ) {
+		return this.remove( selector, true );
+	},
+
+	domManip: function( args, table, callback ) {
+
+		// Flatten any nested arrays
+		args = [].concat.apply( [], args );
+
+		var results, first, fragment, iNoClone,
+			i = 0,
+			value = args[0],
+			scripts = [],
+			l = this.length;
+
+		// We can't cloneNode fragments that contain checked, in WebKit
+		if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) {
+			return this.each(function() {
+				jQuery(this).domManip( args, table, callback );
+			});
+		}
+
+		if ( jQuery.isFunction(value) ) {
+			return this.each(function(i) {
+				var self = jQuery(this);
+				args[0] = value.call( this, i, table ? self.html() : undefined );
+				self.domManip( args, table, callback );
+			});
+		}
+
+		if ( this[0] ) {
+			results = jQuery.buildFragment( args, this, scripts );
+			fragment = results.fragment;
+			first = fragment.firstChild;
+
+			if ( fragment.childNodes.length === 1 ) {
+				fragment = first;
+			}
+
+			if ( first ) {
+				table = table && jQuery.nodeName( first, "tr" );
+
+				// Use the original fragment for the last item instead of the first because it can end up
+				// being emptied incorrectly in certain situations (#8070).
+				// Fragments from the fragment cache must always be cloned and never used in place.
+				for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) {
+					callback.call(
+						table && jQuery.nodeName( this[i], "table" ) ?
+							findOrAppend( this[i], "tbody" ) :
+							this[i],
+						i === iNoClone ?
+							fragment :
+							jQuery.clone( fragment, true, true )
+					);
+				}
+			}
+
+			// Fix #11809: Avoid leaking memory
+			fragment = first = null;
+
+			if ( scripts.length ) {
+				jQuery.each( scripts, function( i, elem ) {
+					if ( elem.src ) {
+						if ( jQuery.ajax ) {
+							jQuery.ajax({
+								url: elem.src,
+								type: "GET",
+								dataType: "script",
+								async: false,
+								global: false,
+								"throws": true
+							});
+						} else {
+							jQuery.error("no ajax");
+						}
+					} else {
+						jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) );
+					}
+
+					if ( elem.parentNode ) {
+						elem.parentNode.removeChild( elem );
+					}
+				});
+			}
+		}
+
+		return this;
+	}
+});
+
+function findOrAppend( elem, tag ) {
+	return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) );
+}
+
+function cloneCopyEvent( src, dest ) {
+
+	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+		return;
+	}
+
+	var type, i, l,
+		oldData = jQuery._data( src ),
+		curData = jQuery._data( dest, oldData ),
+		events = oldData.events;
+
+	if ( events ) {
+		delete curData.handle;
+		curData.events = {};
+
+		for ( type in events ) {
+			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+				jQuery.event.add( dest, type, events[ type ][ i ] );
+			}
+		}
+	}
+
+	// make the cloned public data object a copy from the original
+	if ( curData.data ) {
+		curData.data = jQuery.extend( {}, curData.data );
+	}
+}
+
+function cloneFixAttributes( src, dest ) {
+	var nodeName;
+
+	// We do not need to do anything for non-Elements
+	if ( dest.nodeType !== 1 ) {
+		return;
+	}
+
+	// clearAttributes removes the attributes, which we don't want,
+	// but also removes the attachEvent events, which we *do* want
+	if ( dest.clearAttributes ) {
+		dest.clearAttributes();
+	}
+
+	// mergeAttributes, in contrast, only merges back on the
+	// original attributes, not the events
+	if ( dest.mergeAttributes ) {
+		dest.mergeAttributes( src );
+	}
+
+	nodeName = dest.nodeName.toLowerCase();
+
+	if ( nodeName === "object" ) {
+		// IE6-10 improperly clones children of object elements using classid.
+		// IE10 throws NoModificationAllowedError if parent is null, #12132.
+		if ( dest.parentNode ) {
+			dest.outerHTML = src.outerHTML;
+		}
+
+		// This path appears unavoidable for IE9. When cloning an object
+		// element in IE9, the outerHTML strategy above is not sufficient.
+		// If the src has innerHTML and the destination does not,
+		// copy the src.innerHTML into the dest.innerHTML. #10324
+		if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) {
+			dest.innerHTML = src.innerHTML;
+		}
+
+	} else if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+		// IE6-8 fails to persist the checked state of a cloned checkbox
+		// or radio button. Worse, IE6-7 fail to give the cloned element
+		// a checked appearance if the defaultChecked value isn't also set
+
+		dest.defaultChecked = dest.checked = src.checked;
+
+		// IE6-7 get confused and end up setting the value of a cloned
+		// checkbox/radio button to an empty string instead of "on"
+		if ( dest.value !== src.value ) {
+			dest.value = src.value;
+		}
+
+	// IE6-8 fails to return the selected option to the default selected
+	// state when cloning options
+	} else if ( nodeName === "option" ) {
+		dest.selected = src.defaultSelected;
+
+	// IE6-8 fails to set the defaultValue to the correct value when
+	// cloning other types of input fields
+	} else if ( nodeName === "input" || nodeName === "textarea" ) {
+		dest.defaultValue = src.defaultValue;
+
+	// IE blanks contents when cloning scripts
+	} else if ( nodeName === "script" && dest.text !== src.text ) {
+		dest.text = src.text;
+	}
+
+	// Event data gets referenced instead of copied if the expando
+	// gets copied too
+	dest.removeAttribute( jQuery.expando );
+}
+
+jQuery.buildFragment = function( args, context, scripts ) {
+	var fragment, cacheable, cachehit,
+		first = args[ 0 ];
+
+	// Set context from what may come in as undefined or a jQuery collection or a node
+	// Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 &
+	// also doubles as fix for #8950 where plain objects caused createDocumentFragment exception
+	context = context || document;
+	context = !context.nodeType && context[0] || context;
+	context = context.ownerDocument || context;
+
+	// Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+	// Cloning options loses the selected state, so don't cache them
+	// IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+	// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+	// Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501
+	if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document &&
+		first.charAt(0) === "<" && !rnocache.test( first ) &&
+		(jQuery.support.checkClone || !rchecked.test( first )) &&
+		(jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {
+
+		// Mark cacheable and look for a hit
+		cacheable = true;
+		fragment = jQuery.fragments[ first ];
+		cachehit = fragment !== undefined;
+	}
+
+	if ( !fragment ) {
+		fragment = context.createDocumentFragment();
+		jQuery.clean( args, context, fragment, scripts );
+
+		// Update the cache, but only store false
+		// unless this is a second parsing of the same content
+		if ( cacheable ) {
+			jQuery.fragments[ first ] = cachehit && fragment;
+		}
+	}
+
+	return { fragment: fragment, cacheable: cacheable };
+};
+
+jQuery.fragments = {};
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function( name, original ) {
+	jQuery.fn[ name ] = function( selector ) {
+		var elems,
+			i = 0,
+			ret = [],
+			insert = jQuery( selector ),
+			l = insert.length,
+			parent = this.length === 1 && this[0].parentNode;
+
+		if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) {
+			insert[ original ]( this[0] );
+			return this;
+		} else {
+			for ( ; i < l; i++ ) {
+				elems = ( i > 0 ? this.clone(true) : this ).get();
+				jQuery( insert[i] )[ original ]( elems );
+				ret = ret.concat( elems );
+			}
+
+			return this.pushStack( ret, name, insert.selector );
+		}
+	};
+});
+
+function getAll( elem ) {
+	if ( typeof elem.getElementsByTagName !== "undefined" ) {
+		return elem.getElementsByTagName( "*" );
+
+	} else if ( typeof elem.querySelectorAll !== "undefined" ) {
+		return elem.querySelectorAll( "*" );
+
+	} else {
+		return [];
+	}
+}
+
+// Used in clean, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+	if ( rcheckableType.test( elem.type ) ) {
+		elem.defaultChecked = elem.checked;
+	}
+}
+
+jQuery.extend({
+	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+		var srcElements,
+			destElements,
+			i,
+			clone;
+
+		if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+			clone = elem.cloneNode( true );
+
+		// IE<=8 does not properly clone detached, unknown element nodes
+		} else {
+			fragmentDiv.innerHTML = elem.outerHTML;
+			fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+		}
+
+		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+			// IE copies events bound via attachEvent when using cloneNode.
+			// Calling detachEvent on the clone will also remove the events
+			// from the original. In order to get around this, we use some
+			// proprietary methods to clear the events. Thanks to MooTools
+			// guys for this hotness.
+
+			cloneFixAttributes( elem, clone );
+
+			// Using Sizzle here is crazy slow, so we use getElementsByTagName instead
+			srcElements = getAll( elem );
+			destElements = getAll( clone );
+
+			// Weird iteration because IE will replace the length property
+			// with an element if you are cloning the body and one of the
+			// elements on the page has a name or id of "length"
+			for ( i = 0; srcElements[i]; ++i ) {
+				// Ensure that the destination node is not null; Fixes #9587
+				if ( destElements[i] ) {
+					cloneFixAttributes( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		// Copy the events from the original to the clone
+		if ( dataAndEvents ) {
+			cloneCopyEvent( elem, clone );
+
+			if ( deepDataAndEvents ) {
+				srcElements = getAll( elem );
+				destElements = getAll( clone );
+
+				for ( i = 0; srcElements[i]; ++i ) {
+					cloneCopyEvent( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		srcElements = destElements = null;
+
+		// Return the cloned set
+		return clone;
+	},
+
+	clean: function( elems, context, fragment, scripts ) {
+		var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags,
+			safe = context === document && safeFragment,
+			ret = [];
+
+		// Ensure that context is a document
+		if ( !context || typeof context.createDocumentFragment === "undefined" ) {
+			context = document;
+		}
+
+		// Use the already-created safe fragment if context permits
+		for ( i = 0; (elem = elems[i]) != null; i++ ) {
+			if ( typeof elem === "number" ) {
+				elem += "";
+			}
+
+			if ( !elem ) {
+				continue;
+			}
+
+			// Convert html string into DOM nodes
+			if ( typeof elem === "string" ) {
+				if ( !rhtml.test( elem ) ) {
+					elem = context.createTextNode( elem );
+				} else {
+					// Ensure a safe container in which to render the html
+					safe = safe || createSafeFragment( context );
+					div = context.createElement("div");
+					safe.appendChild( div );
+
+					// Fix "XHTML"-style tags in all browsers
+					elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+					// Go to html and back, then peel off extra wrappers
+					tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+					wrap = wrapMap[ tag ] || wrapMap._default;
+					depth = wrap[0];
+					div.innerHTML = wrap[1] + elem + wrap[2];
+
+					// Move to the right depth
+					while ( depth-- ) {
+						div = div.lastChild;
+					}
+
+					// Remove IE's autoinserted <tbody> from table fragments
+					if ( !jQuery.support.tbody ) {
+
+						// String was a <table>, *may* have spurious <tbody>
+						hasBody = rtbody.test(elem);
+							tbody = tag === "table" && !hasBody ?
+								div.firstChild && div.firstChild.childNodes :
+
+								// String was a bare <thead> or <tfoot>
+								wrap[1] === "<table>" && !hasBody ?
+									div.childNodes :
+									[];
+
+						for ( j = tbody.length - 1; j >= 0 ; --j ) {
+							if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+								tbody[ j ].parentNode.removeChild( tbody[ j ] );
+							}
+						}
+					}
+
+					// IE completely kills leading whitespace when innerHTML is used
+					if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+						div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+					}
+
+					elem = div.childNodes;
+
+					// Take out of fragment container (we need a fresh div each time)
+					div.parentNode.removeChild( div );
+				}
+			}
+
+			if ( elem.nodeType ) {
+				ret.push( elem );
+			} else {
+				jQuery.merge( ret, elem );
+			}
+		}
+
+		// Fix #11356: Clear elements from safeFragment
+		if ( div ) {
+			elem = div = safe = null;
+		}
+
+		// Reset defaultChecked for any radios and checkboxes
+		// about to be appended to the DOM in IE 6/7 (#8060)
+		if ( !jQuery.support.appendChecked ) {
+			for ( i = 0; (elem = ret[i]) != null; i++ ) {
+				if ( jQuery.nodeName( elem, "input" ) ) {
+					fixDefaultChecked( elem );
+				} else if ( typeof elem.getElementsByTagName !== "undefined" ) {
+					jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
+				}
+			}
+		}
+
+		// Append elements to a provided document fragment
+		if ( fragment ) {
+			// Special handling of each script element
+			handleScript = function( elem ) {
+				// Check if we consider it executable
+				if ( !elem.type || rscriptType.test( elem.type ) ) {
+					// Detach the script and store it in the scripts array (if provided) or the fragment
+					// Return truthy to indicate that it has been handled
+					return scripts ?
+						scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) :
+						fragment.appendChild( elem );
+				}
+			};
+
+			for ( i = 0; (elem = ret[i]) != null; i++ ) {
+				// Check if we're done after handling an executable script
+				if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) {
+					// Append to fragment and handle embedded scripts
+					fragment.appendChild( elem );
+					if ( typeof elem.getElementsByTagName !== "undefined" ) {
+						// handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration
+						jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript );
+
+						// Splice the scripts into ret after their former ancestor and advance our index beyond them
+						ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
+						i += jsTags.length;
+					}
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	cleanData: function( elems, /* internal */ acceptData ) {
+		var data, id, elem, type,
+			i = 0,
+			internalKey = jQuery.expando,
+			cache = jQuery.cache,
+			deleteExpando = jQuery.support.deleteExpando,
+			special = jQuery.event.special;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( acceptData || jQuery.acceptData( elem ) ) {
+
+				id = elem[ internalKey ];
+				data = id && cache[ id ];
+
+				if ( data ) {
+					if ( data.events ) {
+						for ( type in data.events ) {
+							if ( special[ type ] ) {
+								jQuery.event.remove( elem, type );
+
+							// This is a shortcut to avoid jQuery.event.remove's overhead
+							} else {
+								jQuery.removeEvent( elem, type, data.handle );
+							}
+						}
+					}
+
+					// Remove cache only if it was not already removed by jQuery.event.remove
+					if ( cache[ id ] ) {
+
+						delete cache[ id ];
+
+						// IE does not allow us to delete expando properties from nodes,
+						// nor does it have a removeAttribute function on Document nodes;
+						// we must handle all of these cases
+						if ( deleteExpando ) {
+							delete elem[ internalKey ];
+
+						} else if ( elem.removeAttribute ) {
+							elem.removeAttribute( internalKey );
+
+						} else {
+							elem[ internalKey ] = null;
+						}
+
+						jQuery.deletedIds.push( id );
+					}
+				}
+			}
+		}
+	}
+});
+// Limit scope pollution from any deprecated API
+(function() {
+
+var matched, browser;
+
+// Use of jQuery.browser is frowned upon.
+// More details: http://api.jquery.com/jQuery.browser
+// jQuery.uaMatch maintained for back-compat
+jQuery.uaMatch = function( ua ) {
+	ua = ua.toLowerCase();
+
+	var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+		/(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+		/(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+		/(msie) ([\w.]+)/.exec( ua ) ||
+		ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
+		[];
+
+	return {
+		browser: match[ 1 ] || "",
+		version: match[ 2 ] || "0"
+	};
+};
+
+matched = jQuery.uaMatch( navigator.userAgent );
+browser = {};
+
+if ( matched.browser ) {
+	browser[ matched.browser ] = true;
+	browser.version = matched.version;
+}
+
+// Chrome is Webkit, but Webkit is also Safari.
+if ( browser.chrome ) {
+	browser.webkit = true;
+} else if ( browser.webkit ) {
+	browser.safari = true;
+}
+
+jQuery.browser = browser;
+
+jQuery.sub = function() {
+	function jQuerySub( selector, context ) {
+		return new jQuerySub.fn.init( selector, context );
+	}
+	jQuery.extend( true, jQuerySub, this );
+	jQuerySub.superclass = this;
+	jQuerySub.fn = jQuerySub.prototype = this();
+	jQuerySub.fn.constructor = jQuerySub;
+	jQuerySub.sub = this.sub;
+	jQuerySub.fn.init = function init( selector, context ) {
+		if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
+			context = jQuerySub( context );
+		}
+
+		return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
+	};
+	jQuerySub.fn.init.prototype = jQuerySub.fn;
+	var rootjQuerySub = jQuerySub(document);
+	return jQuerySub;
+};
+
+})();
+var curCSS, iframe, iframeDoc,
+	ralpha = /alpha\([^)]*\)/i,
+	ropacity = /opacity=([^)]*)/,
+	rposition = /^(top|right|bottom|left)$/,
+	// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+	// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+	rmargin = /^margin/,
+	rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+	rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+	rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ),
+	elemdisplay = {},
+
+	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+	cssNormalTransform = {
+		letterSpacing: 0,
+		fontWeight: 400
+	},
+
+	cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+	cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
+
+	eventsToggle = jQuery.fn.toggle;
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+	// shortcut for names that are not vendor prefixed
+	if ( name in style ) {
+		return name;
+	}
+
+	// check for vendor prefixed names
+	var capName = name.charAt(0).toUpperCase() + name.slice(1),
+		origName = name,
+		i = cssPrefixes.length;
+
+	while ( i-- ) {
+		name = cssPrefixes[ i ] + capName;
+		if ( name in style ) {
+			return name;
+		}
+	}
+
+	return origName;
+}
+
+function isHidden( elem, el ) {
+	elem = el || elem;
+	return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+	var elem, display,
+		values = [],
+		index = 0,
+		length = elements.length;
+
+	for ( ; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		values[ index ] = jQuery._data( elem, "olddisplay" );
+		if ( show ) {
+			// Reset the inline display of this element to learn if it is
+			// being hidden by cascaded rules or not
+			if ( !values[ index ] && elem.style.display === "none" ) {
+				elem.style.display = "";
+			}
+
+			// Set elements which have been overridden with display: none
+			// in a stylesheet to whatever the default browser style is
+			// for such an element
+			if ( elem.style.display === "" && isHidden( elem ) ) {
+				values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+			}
+		} else {
+			display = curCSS( elem, "display" );
+
+			if ( !values[ index ] && display !== "none" ) {
+				jQuery._data( elem, "olddisplay", display );
+			}
+		}
+	}
+
+	// Set the display of most of the elements in a second loop
+	// to avoid the constant reflow
+	for ( index = 0; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+			elem.style.display = show ? values[ index ] || "" : "none";
+		}
+	}
+
+	return elements;
+}
+
+jQuery.fn.extend({
+	css: function( name, value ) {
+		return jQuery.access( this, function( elem, name, value ) {
+			return value !== undefined ?
+				jQuery.style( elem, name, value ) :
+				jQuery.css( elem, name );
+		}, name, value, arguments.length > 1 );
+	},
+	show: function() {
+		return showHide( this, true );
+	},
+	hide: function() {
+		return showHide( this );
+	},
+	toggle: function( state, fn2 ) {
+		var bool = typeof state === "boolean";
+
+		if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) {
+			return eventsToggle.apply( this, arguments );
+		}
+
+		return this.each(function() {
+			if ( bool ? state : isHidden( this ) ) {
+				jQuery( this ).show();
+			} else {
+				jQuery( this ).hide();
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	// Add in style property hooks for overriding the default
+	// behavior of getting and setting a style property
+	cssHooks: {
+		opacity: {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// We should always get a number back from opacity
+					var ret = curCSS( elem, "opacity" );
+					return ret === "" ? "1" : ret;
+
+				}
+			}
+		}
+	},
+
+	// Exclude the following css properties to add px
+	cssNumber: {
+		"fillOpacity": true,
+		"fontWeight": true,
+		"lineHeight": true,
+		"opacity": true,
+		"orphans": true,
+		"widows": true,
+		"zIndex": true,
+		"zoom": true
+	},
+
+	// Add in properties whose names you wish to fix before
+	// setting or getting the value
+	cssProps: {
+		// normalize float css property
+		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+	},
+
+	// Get and set the style property on a DOM Node
+	style: function( elem, name, value, extra ) {
+		// Don't set styles on text and comment nodes
+		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+			return;
+		}
+
+		// Make sure that we're working with the right name
+		var ret, type, hooks,
+			origName = jQuery.camelCase( name ),
+			style = elem.style;
+
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// Check if we're setting a value
+		if ( value !== undefined ) {
+			type = typeof value;
+
+			// convert relative number strings (+= or -=) to relative numbers. #7345
+			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+				// Fixes bug #9237
+				type = "number";
+			}
+
+			// Make sure that NaN and null values aren't set. See: #7116
+			if ( value == null || type === "number" && isNaN( value ) ) {
+				return;
+			}
+
+			// If a number was passed in, add 'px' to the (except for certain CSS properties)
+			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+				value += "px";
+			}
+
+			// If a hook was provided, use that value, otherwise just set the specified value
+			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+				// Fixes bug #5509
+				try {
+					style[ name ] = value;
+				} catch(e) {}
+			}
+
+		} else {
+			// If a hook was provided get the non-computed value from there
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+				return ret;
+			}
+
+			// Otherwise just get the value from the style object
+			return style[ name ];
+		}
+	},
+
+	css: function( elem, name, numeric, extra ) {
+		var val, num, hooks,
+			origName = jQuery.camelCase( name );
+
+		// Make sure that we're working with the right name
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// If a hook was provided get the computed value from there
+		if ( hooks && "get" in hooks ) {
+			val = hooks.get( elem, true, extra );
+		}
+
+		// Otherwise, if a way to get the computed value exists, use that
+		if ( val === undefined ) {
+			val = curCSS( elem, name );
+		}
+
+		//convert "normal" to computed value
+		if ( val === "normal" && name in cssNormalTransform ) {
+			val = cssNormalTransform[ name ];
+		}
+
+		// Return, converting to number if forced or a qualifier was provided and val looks numeric
+		if ( numeric || extra !== undefined ) {
+			num = parseFloat( val );
+			return numeric || jQuery.isNumeric( num ) ? num || 0 : val;
+		}
+		return val;
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations
+	swap: function( elem, options, callback ) {
+		var ret, name,
+			old = {};
+
+		// Remember the old values, and insert the new ones
+		for ( name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		ret = callback.call( elem );
+
+		// Revert the old values
+		for ( name in options ) {
+			elem.style[ name ] = old[ name ];
+		}
+
+		return ret;
+	}
+});
+
+// NOTE: To any future maintainer, we've window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+	curCSS = function( elem, name ) {
+		var ret, width, minWidth, maxWidth,
+			computed = window.getComputedStyle( elem, null ),
+			style = elem.style;
+
+		if ( computed ) {
+
+			ret = computed[ name ];
+			if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+				ret = jQuery.style( elem, name );
+			}
+
+			// A tribute to the "awesome hack by Dean Edwards"
+			// Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+			// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+			// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+			if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+				width = style.width;
+				minWidth = style.minWidth;
+				maxWidth = style.maxWidth;
+
+				style.minWidth = style.maxWidth = style.width = ret;
+				ret = computed.width;
+
+				style.width = width;
+				style.minWidth = minWidth;
+				style.maxWidth = maxWidth;
+			}
+		}
+
+		return ret;
+	};
+} else if ( document.documentElement.currentStyle ) {
+	curCSS = function( elem, name ) {
+		var left, rsLeft,
+			ret = elem.currentStyle && elem.currentStyle[ name ],
+			style = elem.style;
+
+		// Avoid setting ret to empty string here
+		// so we don't default to auto
+		if ( ret == null && style && style[ name ] ) {
+			ret = style[ name ];
+		}
+
+		// From the awesome hack by Dean Edwards
+		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+		// If we're not dealing with a regular pixel number
+		// but a number that has a weird ending, we need to convert it to pixels
+		// but not position css attributes, as those are proportional to the parent element instead
+		// and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+		if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+			// Remember the original values
+			left = style.left;
+			rsLeft = elem.runtimeStyle && elem.runtimeStyle.left;
+
+			// Put in the new values to get a computed value out
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = elem.currentStyle.left;
+			}
+			style.left = name === "fontSize" ? "1em" : ret;
+			ret = style.pixelLeft + "px";
+
+			// Revert the changed values
+			style.left = left;
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = rsLeft;
+			}
+		}
+
+		return ret === "" ? "auto" : ret;
+	};
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+	var matches = rnumsplit.exec( value );
+	return matches ?
+			Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+			value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox ) {
+	var i = extra === ( isBorderBox ? "border" : "content" ) ?
+		// If we already have the right measurement, avoid augmentation
+		4 :
+		// Otherwise initialize for horizontal or vertical properties
+		name === "width" ? 1 : 0,
+
+		val = 0;
+
+	for ( ; i < 4; i += 2 ) {
+		// both box models exclude margin, so add it if we want it
+		if ( extra === "margin" ) {
+			// we use jQuery.css instead of curCSS here
+			// because of the reliableMarginRight CSS hook!
+			val += jQuery.css( elem, extra + cssExpand[ i ], true );
+		}
+
+		// From this point on we use curCSS for maximum performance (relevant in animations)
+		if ( isBorderBox ) {
+			// border-box includes padding, so remove it if we want content
+			if ( extra === "content" ) {
+				val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+			}
+
+			// at this point, extra isn't border nor margin, so remove border
+			if ( extra !== "margin" ) {
+				val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+			}
+		} else {
+			// at this point, extra isn't content, so add padding
+			val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+
+			// at this point, extra isn't content nor padding, so add border
+			if ( extra !== "padding" ) {
+				val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+			}
+		}
+	}
+
+	return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+	// Start with offset property, which is equivalent to the border-box value
+	var val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+		valueIsBorderBox = true,
+		isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box";
+
+	// some non-html elements return undefined for offsetWidth, so check for null/undefined
+	// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+	// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+	if ( val <= 0 || val == null ) {
+		// Fall back to computed then uncomputed css if necessary
+		val = curCSS( elem, name );
+		if ( val < 0 || val == null ) {
+			val = elem.style[ name ];
+		}
+
+		// Computed unit is not pixels. Stop here and return.
+		if ( rnumnonpx.test(val) ) {
+			return val;
+		}
+
+		// we need the check for style in case a browser which returns unreliable values
+		// for getComputedStyle silently falls back to the reliable elem.style
+		valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+		// Normalize "", auto, and prepare for extra
+		val = parseFloat( val ) || 0;
+	}
+
+	// use the active box-sizing model to add/subtract irrelevant styles
+	return ( val +
+		augmentWidthOrHeight(
+			elem,
+			name,
+			extra || ( isBorderBox ? "border" : "content" ),
+			valueIsBorderBox
+		)
+	) + "px";
+}
+
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+	if ( elemdisplay[ nodeName ] ) {
+		return elemdisplay[ nodeName ];
+	}
+
+	var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ),
+		display = elem.css("display");
+	elem.remove();
+
+	// If the simple way fails,
+	// get element's real default display by attaching it to a temp iframe
+	if ( display === "none" || display === "" ) {
+		// Use the already-created iframe if possible
+		iframe = document.body.appendChild(
+			iframe || jQuery.extend( document.createElement("iframe"), {
+				frameBorder: 0,
+				width: 0,
+				height: 0
+			})
+		);
+
+		// Create a cacheable copy of the iframe document on first call.
+		// IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML
+		// document to it; WebKit & Firefox won't allow reusing the iframe document.
+		if ( !iframeDoc || !iframe.createElement ) {
+			iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;
+			iframeDoc.write("<!doctype html><html><body>");
+			iframeDoc.close();
+		}
+
+		elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) );
+
+		display = curCSS( elem, "display" );
+		document.body.removeChild( iframe );
+	}
+
+	// Store the correct default display
+	elemdisplay[ nodeName ] = display;
+
+	return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+	jQuery.cssHooks[ name ] = {
+		get: function( elem, computed, extra ) {
+			if ( computed ) {
+				// certain elements can have dimension info if we invisibly show them
+				// however, it must have a current display style that would benefit from this
+				if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) {
+					return jQuery.swap( elem, cssShow, function() {
+						return getWidthOrHeight( elem, name, extra );
+					});
+				} else {
+					return getWidthOrHeight( elem, name, extra );
+				}
+			}
+		},
+
+		set: function( elem, value, extra ) {
+			return setPositiveNumber( elem, value, extra ?
+				augmentWidthOrHeight(
+					elem,
+					name,
+					extra,
+					jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"
+				) : 0
+			);
+		}
+	};
+});
+
+if ( !jQuery.support.opacity ) {
+	jQuery.cssHooks.opacity = {
+		get: function( elem, computed ) {
+			// IE uses filters for opacity
+			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+				( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+				computed ? "1" : "";
+		},
+
+		set: function( elem, value ) {
+			var style = elem.style,
+				currentStyle = elem.currentStyle,
+				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+				filter = currentStyle && currentStyle.filter || style.filter || "";
+
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			style.zoom = 1;
+
+			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+			if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+				style.removeAttribute ) {
+
+				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+				// if "filter:" is present at all, clearType is disabled, we want to avoid this
+				// style.removeAttribute is IE Only, but so apparently is this code path...
+				style.removeAttribute( "filter" );
+
+				// if there there is no filter style applied in a css rule, we are done
+				if ( currentStyle && !currentStyle.filter ) {
+					return;
+				}
+			}
+
+			// otherwise, set new filter values
+			style.filter = ralpha.test( filter ) ?
+				filter.replace( ralpha, opacity ) :
+				filter + " " + opacity;
+		}
+	};
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+	if ( !jQuery.support.reliableMarginRight ) {
+		jQuery.cssHooks.marginRight = {
+			get: function( elem, computed ) {
+				// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+				// Work around by temporarily setting element display to inline-block
+				return jQuery.swap( elem, { "display": "inline-block" }, function() {
+					if ( computed ) {
+						return curCSS( elem, "marginRight" );
+					}
+				});
+			}
+		};
+	}
+
+	// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+	// getComputedStyle returns percent when specified for top/left/bottom/right
+	// rather than make the css module depend on the offset module, we just check for it here
+	if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+		jQuery.each( [ "top", "left" ], function( i, prop ) {
+			jQuery.cssHooks[ prop ] = {
+				get: function( elem, computed ) {
+					if ( computed ) {
+						var ret = curCSS( elem, prop );
+						// if curCSS returns percentage, fallback to offset
+						return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret;
+					}
+				}
+			};
+		});
+	}
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.hidden = function( elem ) {
+		return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none");
+	};
+
+	jQuery.expr.filters.visible = function( elem ) {
+		return !jQuery.expr.filters.hidden( elem );
+	};
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+	margin: "",
+	padding: "",
+	border: "Width"
+}, function( prefix, suffix ) {
+	jQuery.cssHooks[ prefix + suffix ] = {
+		expand: function( value ) {
+			var i,
+
+				// assumes a single number if not a string
+				parts = typeof value === "string" ? value.split(" ") : [ value ],
+				expanded = {};
+
+			for ( i = 0; i < 4; i++ ) {
+				expanded[ prefix + cssExpand[ i ] + suffix ] =
+					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+			}
+
+			return expanded;
+		}
+	};
+
+	if ( !rmargin.test( prefix ) ) {
+		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+	}
+});
+var r20 = /%20/g,
+	rbracket = /\[\]$/,
+	rCRLF = /\r?\n/g,
+	rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
+	rselectTextarea = /^(?:select|textarea)/i;
+
+jQuery.fn.extend({
+	serialize: function() {
+		return jQuery.param( this.serializeArray() );
+	},
+	serializeArray: function() {
+		return this.map(function(){
+			return this.elements ? jQuery.makeArray( this.elements ) : this;
+		})
+		.filter(function(){
+			return this.name && !this.disabled &&
+				( this.checked || rselectTextarea.test( this.nodeName ) ||
+					rinput.test( this.type ) );
+		})
+		.map(function( i, elem ){
+			var val = jQuery( this ).val();
+
+			return val == null ?
+				null :
+				jQuery.isArray( val ) ?
+					jQuery.map( val, function( val, i ){
+						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+					}) :
+					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+		}).get();
+	}
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+	var prefix,
+		s = [],
+		add = function( key, value ) {
+			// If value is a function, invoke it and return its value
+			value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+			s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+		};
+
+	// Set traditional to true for jQuery <= 1.3.2 behavior.
+	if ( traditional === undefined ) {
+		traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+	}
+
+	// If an array was passed in, assume that it is an array of form elements.
+	if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+		// Serialize the form elements
+		jQuery.each( a, function() {
+			add( this.name, this.value );
+		});
+
+	} else {
+		// If traditional, encode the "old" way (the way 1.3.2 or older
+		// did it), otherwise encode params recursively.
+		for ( prefix in a ) {
+			buildParams( prefix, a[ prefix ], traditional, add );
+		}
+	}
+
+	// Return the resulting serialization
+	return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+	var name;
+
+	if ( jQuery.isArray( obj ) ) {
+		// Serialize array item.
+		jQuery.each( obj, function( i, v ) {
+			if ( traditional || rbracket.test( prefix ) ) {
+				// Treat each array item as a scalar.
+				add( prefix, v );
+
+			} else {
+				// If array item is non-scalar (array or object), encode its
+				// numeric index to resolve deserialization ambiguity issues.
+				// Note that rack (as of 1.0.0) can't currently deserialize
+				// nested arrays properly, and attempting to do so may cause
+				// a server error. Possible fixes are to modify rack's
+				// deserialization algorithm or to provide an option or flag
+				// to force array serialization to be shallow.
+				buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+			}
+		});
+
+	} else if ( !traditional && jQuery.type( obj ) === "object" ) {
+		// Serialize object item.
+		for ( name in obj ) {
+			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+		}
+
+	} else {
+		// Serialize scalar item.
+		add( prefix, obj );
+	}
+}
+var
+	// Document location
+	ajaxLocParts,
+	ajaxLocation,
+
+	rhash = /#.*$/,
+	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+	// #7653, #8125, #8152: local protocol detection
+	rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,
+	rnoContent = /^(?:GET|HEAD)$/,
+	rprotocol = /^\/\//,
+	rquery = /\?/,
+	rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
+	rts = /([?&])_=[^&]*/,
+	rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+	// Keep a copy of the old load method
+	_load = jQuery.fn.load,
+
+	/* Prefilters
+	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+	 * 2) These are called:
+	 *    - BEFORE asking for a transport
+	 *    - AFTER param serialization (s.data is a string if s.processData is true)
+	 * 3) key is the dataType
+	 * 4) the catchall symbol "*" can be used
+	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+	 */
+	prefilters = {},
+
+	/* Transports bindings
+	 * 1) key is the dataType
+	 * 2) the catchall symbol "*" can be used
+	 * 3) selection will start with transport dataType and THEN go to "*" if needed
+	 */
+	transports = {},
+
+	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+	allTypes = ["*/"] + ["*"];
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+	ajaxLocation = location.href;
+} catch( e ) {
+	// Use the href attribute of an A element
+	// since IE will modify it given document.location
+	ajaxLocation = document.createElement( "a" );
+	ajaxLocation.href = "";
+	ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+	// dataTypeExpression is optional and defaults to "*"
+	return function( dataTypeExpression, func ) {
+
+		if ( typeof dataTypeExpression !== "string" ) {
+			func = dataTypeExpression;
+			dataTypeExpression = "*";
+		}
+
+		var dataType, list, placeBefore,
+			dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ),
+			i = 0,
+			length = dataTypes.length;
+
+		if ( jQuery.isFunction( func ) ) {
+			// For each dataType in the dataTypeExpression
+			for ( ; i < length; i++ ) {
+				dataType = dataTypes[ i ];
+				// We control if we're asked to add before
+				// any existing element
+				placeBefore = /^\+/.test( dataType );
+				if ( placeBefore ) {
+					dataType = dataType.substr( 1 ) || "*";
+				}
+				list = structure[ dataType ] = structure[ dataType ] || [];
+				// then we add to the structure accordingly
+				list[ placeBefore ? "unshift" : "push" ]( func );
+			}
+		}
+	};
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
+		dataType /* internal */, inspected /* internal */ ) {
+
+	dataType = dataType || options.dataTypes[ 0 ];
+	inspected = inspected || {};
+
+	inspected[ dataType ] = true;
+
+	var selection,
+		list = structure[ dataType ],
+		i = 0,
+		length = list ? list.length : 0,
+		executeOnly = ( structure === prefilters );
+
+	for ( ; i < length && ( executeOnly || !selection ); i++ ) {
+		selection = list[ i ]( options, originalOptions, jqXHR );
+		// If we got redirected to another dataType
+		// we try there if executing only and not done already
+		if ( typeof selection === "string" ) {
+			if ( !executeOnly || inspected[ selection ] ) {
+				selection = undefined;
+			} else {
+				options.dataTypes.unshift( selection );
+				selection = inspectPrefiltersOrTransports(
+						structure, options, originalOptions, jqXHR, selection, inspected );
+			}
+		}
+	}
+	// If we're only executing or nothing was selected
+	// we try the catchall dataType if not done already
+	if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
+		selection = inspectPrefiltersOrTransports(
+				structure, options, originalOptions, jqXHR, "*", inspected );
+	}
+	// unnecessary when only executing (prefilters)
+	// but it'll be ignored by the caller in that case
+	return selection;
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+	var key, deep,
+		flatOptions = jQuery.ajaxSettings.flatOptions || {};
+	for ( key in src ) {
+		if ( src[ key ] !== undefined ) {
+			( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+		}
+	}
+	if ( deep ) {
+		jQuery.extend( true, target, deep );
+	}
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+	if ( typeof url !== "string" && _load ) {
+		return _load.apply( this, arguments );
+	}
+
+	// Don't do a request if no elements are being requested
+	if ( !this.length ) {
+		return this;
+	}
+
+	var selector, type, response,
+		self = this,
+		off = url.indexOf(" ");
+
+	if ( off >= 0 ) {
+		selector = url.slice( off, url.length );
+		url = url.slice( 0, off );
+	}
+
+	// If it's a function
+	if ( jQuery.isFunction( params ) ) {
+
+		// We assume that it's the callback
+		callback = params;
+		params = undefined;
+
+	// Otherwise, build a param string
+	} else if ( params && typeof params === "object" ) {
+		type = "POST";
+	}
+
+	// Request the remote document
+	jQuery.ajax({
+		url: url,
+
+		// if "type" variable is undefined, then "GET" method will be used
+		type: type,
+		dataType: "html",
+		data: params,
+		complete: function( jqXHR, status ) {
+			if ( callback ) {
+				self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+			}
+		}
+	}).done(function( responseText ) {
+
+		// Save response for use in complete callback
+		response = arguments;
+
+		// See if a selector was specified
+		self.html( selector ?
+
+			// Create a dummy div to hold the results
+			jQuery("<div>")
+
+				// inject the contents of the document in, removing the scripts
+				// to avoid any 'Permission Denied' errors in IE
+				.append( responseText.replace( rscript, "" ) )
+
+				// Locate the specified elements
+				.find( selector ) :
+
+			// If not, just inject the full result
+			responseText );
+
+	});
+
+	return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
+	jQuery.fn[ o ] = function( f ){
+		return this.on( o, f );
+	};
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+	jQuery[ method ] = function( url, data, callback, type ) {
+		// shift arguments if data argument was omitted
+		if ( jQuery.isFunction( data ) ) {
+			type = type || callback;
+			callback = data;
+			data = undefined;
+		}
+
+		return jQuery.ajax({
+			type: method,
+			url: url,
+			data: data,
+			success: callback,
+			dataType: type
+		});
+	};
+});
+
+jQuery.extend({
+
+	getScript: function( url, callback ) {
+		return jQuery.get( url, undefined, callback, "script" );
+	},
+
+	getJSON: function( url, data, callback ) {
+		return jQuery.get( url, data, callback, "json" );
+	},
+
+	// Creates a full fledged settings object into target
+	// with both ajaxSettings and settings fields.
+	// If target is omitted, writes into ajaxSettings.
+	ajaxSetup: function( target, settings ) {
+		if ( settings ) {
+			// Building a settings object
+			ajaxExtend( target, jQuery.ajaxSettings );
+		} else {
+			// Extending ajaxSettings
+			settings = target;
+			target = jQuery.ajaxSettings;
+		}
+		ajaxExtend( target, settings );
+		return target;
+	},
+
+	ajaxSettings: {
+		url: ajaxLocation,
+		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+		global: true,
+		type: "GET",
+		contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+		processData: true,
+		async: true,
+		/*
+		timeout: 0,
+		data: null,
+		dataType: null,
+		username: null,
+		password: null,
+		cache: null,
+		throws: false,
+		traditional: false,
+		headers: {},
+		*/
+
+		accepts: {
+			xml: "application/xml, text/xml",
+			html: "text/html",
+			text: "text/plain",
+			json: "application/json, text/javascript",
+			"*": allTypes
+		},
+
+		contents: {
+			xml: /xml/,
+			html: /html/,
+			json: /json/
+		},
+
+		responseFields: {
+			xml: "responseXML",
+			text: "responseText"
+		},
+
+		// List of data converters
+		// 1) key format is "source_type destination_type" (a single space in-between)
+		// 2) the catchall symbol "*" can be used for source_type
+		converters: {
+
+			// Convert anything to text
+			"* text": window.String,
+
+			// Text to html (true = no transformation)
+			"text html": true,
+
+			// Evaluate text as a json expression
+			"text json": jQuery.parseJSON,
+
+			// Parse text as xml
+			"text xml": jQuery.parseXML
+		},
+
+		// For options that shouldn't be deep extended:
+		// you can add your own custom options here if
+		// and when you create one that shouldn't be
+		// deep extended (see ajaxExtend)
+		flatOptions: {
+			context: true,
+			url: true
+		}
+	},
+
+	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+	ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+	// Main method
+	ajax: function( url, options ) {
+
+		// If url is an object, simulate pre-1.5 signature
+		if ( typeof url === "object" ) {
+			options = url;
+			url = undefined;
+		}
+
+		// Force options to be an object
+		options = options || {};
+
+		var // ifModified key
+			ifModifiedKey,
+			// Response headers
+			responseHeadersString,
+			responseHeaders,
+			// transport
+			transport,
+			// timeout handle
+			timeoutTimer,
+			// Cross-domain detection vars
+			parts,
+			// To know if global events are to be dispatched
+			fireGlobals,
+			// Loop variable
+			i,
+			// Create the final options object
+			s = jQuery.ajaxSetup( {}, options ),
+			// Callbacks context
+			callbackContext = s.context || s,
+			// Context for global events
+			// It's the callbackContext if one was provided in the options
+			// and if it's a DOM node or a jQuery collection
+			globalEventContext = callbackContext !== s &&
+				( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+						jQuery( callbackContext ) : jQuery.event,
+			// Deferreds
+			deferred = jQuery.Deferred(),
+			completeDeferred = jQuery.Callbacks( "once memory" ),
+			// Status-dependent callbacks
+			statusCode = s.statusCode || {},
+			// Headers (they are sent all at once)
+			requestHeaders = {},
+			requestHeadersNames = {},
+			// The jqXHR state
+			state = 0,
+			// Default abort message
+			strAbort = "canceled",
+			// Fake xhr
+			jqXHR = {
+
+				readyState: 0,
+
+				// Caches the header
+				setRequestHeader: function( name, value ) {
+					if ( !state ) {
+						var lname = name.toLowerCase();
+						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+						requestHeaders[ name ] = value;
+					}
+					return this;
+				},
+
+				// Raw string
+				getAllResponseHeaders: function() {
+					return state === 2 ? responseHeadersString : null;
+				},
+
+				// Builds headers hashtable if needed
+				getResponseHeader: function( key ) {
+					var match;
+					if ( state === 2 ) {
+						if ( !responseHeaders ) {
+							responseHeaders = {};
+							while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+							}
+						}
+						match = responseHeaders[ key.toLowerCase() ];
+					}
+					return match === undefined ? null : match;
+				},
+
+				// Overrides response content-type header
+				overrideMimeType: function( type ) {
+					if ( !state ) {
+						s.mimeType = type;
+					}
+					return this;
+				},
+
+				// Cancel the request
+				abort: function( statusText ) {
+					statusText = statusText || strAbort;
+					if ( transport ) {
+						transport.abort( statusText );
+					}
+					done( 0, statusText );
+					return this;
+				}
+			};
+
+		// Callback for when everything is done
+		// It is defined here because jslint complains if it is declared
+		// at the end of the function (which would be more logical and readable)
+		function done( status, nativeStatusText, responses, headers ) {
+			var isSuccess, success, error, response, modified,
+				statusText = nativeStatusText;
+
+			// Called once
+			if ( state === 2 ) {
+				return;
+			}
+
+			// State is "done" now
+			state = 2;
+
+			// Clear timeout if it exists
+			if ( timeoutTimer ) {
+				clearTimeout( timeoutTimer );
+			}
+
+			// Dereference transport for early garbage collection
+			// (no matter how long the jqXHR object will be used)
+			transport = undefined;
+
+			// Cache response headers
+			responseHeadersString = headers || "";
+
+			// Set readyState
+			jqXHR.readyState = status > 0 ? 4 : 0;
+
+			// Get response data
+			if ( responses ) {
+				response = ajaxHandleResponses( s, jqXHR, responses );
+			}
+
+			// If successful, handle type chaining
+			if ( status >= 200 && status < 300 || status === 304 ) {
+
+				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+				if ( s.ifModified ) {
+
+					modified = jqXHR.getResponseHeader("Last-Modified");
+					if ( modified ) {
+						jQuery.lastModified[ ifModifiedKey ] = modified;
+					}
+					modified = jqXHR.getResponseHeader("Etag");
+					if ( modified ) {
+						jQuery.etag[ ifModifiedKey ] = modified;
+					}
+				}
+
+				// If not modified
+				if ( status === 304 ) {
+
+					statusText = "notmodified";
+					isSuccess = true;
+
+				// If we have data
+				} else {
+
+					isSuccess = ajaxConvert( s, response );
+					statusText = isSuccess.state;
+					success = isSuccess.data;
+					error = isSuccess.error;
+					isSuccess = !error;
+				}
+			} else {
+				// We extract error from statusText
+				// then normalize statusText and status for non-aborts
+				error = statusText;
+				if ( !statusText || status ) {
+					statusText = "error";
+					if ( status < 0 ) {
+						status = 0;
+					}
+				}
+			}
+
+			// Set data for the fake xhr object
+			jqXHR.status = status;
+			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+			// Success/Error
+			if ( isSuccess ) {
+				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+			} else {
+				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+			}
+
+			// Status-dependent callbacks
+			jqXHR.statusCode( statusCode );
+			statusCode = undefined;
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+						[ jqXHR, s, isSuccess ? success : error ] );
+			}
+
+			// Complete
+			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+				// Handle the global AJAX counter
+				if ( !( --jQuery.active ) ) {
+					jQuery.event.trigger( "ajaxStop" );
+				}
+			}
+		}
+
+		// Attach deferreds
+		deferred.promise( jqXHR );
+		jqXHR.success = jqXHR.done;
+		jqXHR.error = jqXHR.fail;
+		jqXHR.complete = completeDeferred.add;
+
+		// Status-dependent callbacks
+		jqXHR.statusCode = function( map ) {
+			if ( map ) {
+				var tmp;
+				if ( state < 2 ) {
+					for ( tmp in map ) {
+						statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+					}
+				} else {
+					tmp = map[ jqXHR.status ];
+					jqXHR.always( tmp );
+				}
+			}
+			return this;
+		};
+
+		// Remove hash character (#7531: and string promotion)
+		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+		// We also use the url parameter if available
+		s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+		// Extract dataTypes list
+		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace );
+
+		// A cross-domain request is in order when we have a protocol:host:port mismatch
+		if ( s.crossDomain == null ) {
+			parts = rurl.exec( s.url.toLowerCase() ) || false;
+			s.crossDomain = parts && ( parts.join(":") + ( parts[ 3 ] ? "" : parts[ 1 ] === "http:" ? 80 : 443 ) ) !==
+				( ajaxLocParts.join(":") + ( ajaxLocParts[ 3 ] ? "" : ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) );
+		}
+
+		// Convert data if not already a string
+		if ( s.data && s.processData && typeof s.data !== "string" ) {
+			s.data = jQuery.param( s.data, s.traditional );
+		}
+
+		// Apply prefilters
+		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+		// If request was aborted inside a prefilter, stop there
+		if ( state === 2 ) {
+			return jqXHR;
+		}
+
+		// We can fire global events as of now if asked to
+		fireGlobals = s.global;
+
+		// Uppercase the type
+		s.type = s.type.toUpperCase();
+
+		// Determine if request has content
+		s.hasContent = !rnoContent.test( s.type );
+
+		// Watch for a new set of requests
+		if ( fireGlobals && jQuery.active++ === 0 ) {
+			jQuery.event.trigger( "ajaxStart" );
+		}
+
+		// More options handling for requests with no content
+		if ( !s.hasContent ) {
+
+			// If data is available, append data to url
+			if ( s.data ) {
+				s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+				// #9682: remove data so that it's not used in an eventual retry
+				delete s.data;
+			}
+
+			// Get ifModifiedKey before adding the anti-cache parameter
+			ifModifiedKey = s.url;
+
+			// Add anti-cache in url if needed
+			if ( s.cache === false ) {
+
+				var ts = jQuery.now(),
+					// try replacing _= if it is there
+					ret = s.url.replace( rts, "$1_=" + ts );
+
+				// if nothing was replaced, add timestamp to the end
+				s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+			}
+		}
+
+		// Set the correct header, if data is being sent
+		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+			jqXHR.setRequestHeader( "Content-Type", s.contentType );
+		}
+
+		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+		if ( s.ifModified ) {
+			ifModifiedKey = ifModifiedKey || s.url;
+			if ( jQuery.lastModified[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] );
+			}
+			if ( jQuery.etag[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] );
+			}
+		}
+
+		// Set the Accepts header for the server, depending on the dataType
+		jqXHR.setRequestHeader(
+			"Accept",
+			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+				s.accepts[ "*" ]
+		);
+
+		// Check for headers option
+		for ( i in s.headers ) {
+			jqXHR.setRequestHeader( i, s.headers[ i ] );
+		}
+
+		// Allow custom headers/mimetypes and early abort
+		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+				// Abort if not done already and return
+				return jqXHR.abort();
+
+		}
+
+		// aborting is no longer a cancellation
+		strAbort = "abort";
+
+		// Install callbacks on deferreds
+		for ( i in { success: 1, error: 1, complete: 1 } ) {
+			jqXHR[ i ]( s[ i ] );
+		}
+
+		// Get transport
+		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+		// If no transport, we auto-abort
+		if ( !transport ) {
+			done( -1, "No Transport" );
+		} else {
+			jqXHR.readyState = 1;
+			// Send global event
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+			}
+			// Timeout
+			if ( s.async && s.timeout > 0 ) {
+				timeoutTimer = setTimeout( function(){
+					jqXHR.abort( "timeout" );
+				}, s.timeout );
+			}
+
+			try {
+				state = 1;
+				transport.send( requestHeaders, done );
+			} catch (e) {
+				// Propagate exception as error if not done
+				if ( state < 2 ) {
+					done( -1, e );
+				// Simply rethrow otherwise
+				} else {
+					throw e;
+				}
+			}
+		}
+
+		return jqXHR;
+	},
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Last-Modified header cache for next request
+	lastModified: {},
+	etag: {}
+
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+	var ct, type, finalDataType, firstDataType,
+		contents = s.contents,
+		dataTypes = s.dataTypes,
+		responseFields = s.responseFields;
+
+	// Fill responseXXX fields
+	for ( type in responseFields ) {
+		if ( type in responses ) {
+			jqXHR[ responseFields[type] ] = responses[ type ];
+		}
+	}
+
+	// Remove auto dataType and get content-type in the process
+	while( dataTypes[ 0 ] === "*" ) {
+		dataTypes.shift();
+		if ( ct === undefined ) {
+			ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
+		}
+	}
+
+	// Check if we're dealing with a known content-type
+	if ( ct ) {
+		for ( type in contents ) {
+			if ( contents[ type ] && contents[ type ].test( ct ) ) {
+				dataTypes.unshift( type );
+				break;
+			}
+		}
+	}
+
+	// Check to see if we have a response for the expected dataType
+	if ( dataTypes[ 0 ] in responses ) {
+		finalDataType = dataTypes[ 0 ];
+	} else {
+		// Try convertible dataTypes
+		for ( type in responses ) {
+			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+				finalDataType = type;
+				break;
+			}
+			if ( !firstDataType ) {
+				firstDataType = type;
+			}
+		}
+		// Or just use first one
+		finalDataType = finalDataType || firstDataType;
+	}
+
+	// If we found a dataType
+	// We add the dataType to the list if needed
+	// and return the corresponding response
+	if ( finalDataType ) {
+		if ( finalDataType !== dataTypes[ 0 ] ) {
+			dataTypes.unshift( finalDataType );
+		}
+		return responses[ finalDataType ];
+	}
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+
+	var conv, conv2, current, tmp,
+		// Work with a copy of dataTypes in case we need to modify it for conversion
+		dataTypes = s.dataTypes.slice(),
+		prev = dataTypes[ 0 ],
+		converters = {},
+		i = 0;
+
+	// Apply the dataFilter if provided
+	if ( s.dataFilter ) {
+		response = s.dataFilter( response, s.dataType );
+	}
+
+	// Create converters map with lowercased keys
+	if ( dataTypes[ 1 ] ) {
+		for ( conv in s.converters ) {
+			converters[ conv.toLowerCase() ] = s.converters[ conv ];
+		}
+	}
+
+	// Convert to each sequential dataType, tolerating list modification
+	for ( ; (current = dataTypes[++i]); ) {
+
+		// There's only work to do if current dataType is non-auto
+		if ( current !== "*" ) {
+
+			// Convert response if prev dataType is non-auto and differs from current
+			if ( prev !== "*" && prev !== current ) {
+
+				// Seek a direct converter
+				conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+				// If none found, seek a pair
+				if ( !conv ) {
+					for ( conv2 in converters ) {
+
+						// If conv2 outputs current
+						tmp = conv2.split(" ");
+						if ( tmp[ 1 ] === current ) {
+
+							// If prev can be converted to accepted input
+							conv = converters[ prev + " " + tmp[ 0 ] ] ||
+								converters[ "* " + tmp[ 0 ] ];
+							if ( conv ) {
+								// Condense equivalence converters
+								if ( conv === true ) {
+									conv = converters[ conv2 ];
+
+								// Otherwise, insert the intermediate dataType
+								} else if ( converters[ conv2 ] !== true ) {
+									current = tmp[ 0 ];
+									dataTypes.splice( i--, 0, current );
+								}
+
+								break;
+							}
+						}
+					}
+				}
+
+				// Apply converter (if not an equivalence)
+				if ( conv !== true ) {
+
+					// Unless errors are allowed to bubble, catch and return them
+					if ( conv && s["throws"] ) {
+						response = conv( response );
+					} else {
+						try {
+							response = conv( response );
+						} catch ( e ) {
+							return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+						}
+					}
+				}
+			}
+
+			// Update prev for next iteration
+			prev = current;
+		}
+	}
+
+	return { state: "success", data: response };
+}
+var oldCallbacks = [],
+	rquestion = /\?/,
+	rjsonp = /(=)\?(?=&|$)|\?\?/,
+	nonce = jQuery.now();
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+	jsonp: "callback",
+	jsonpCallback: function() {
+		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+		this[ callback ] = true;
+		return callback;
+	}
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+	var callbackName, overwritten, responseContainer,
+		data = s.data,
+		url = s.url,
+		hasCallback = s.jsonp !== false,
+		replaceInUrl = hasCallback && rjsonp.test( url ),
+		replaceInData = hasCallback && !replaceInUrl && typeof data === "string" &&
+			!( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") &&
+			rjsonp.test( data );
+
+	// Handle iff the expected data type is "jsonp" or we have a parameter to set
+	if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) {
+
+		// Get callback name, remembering preexisting value associated with it
+		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+			s.jsonpCallback() :
+			s.jsonpCallback;
+		overwritten = window[ callbackName ];
+
+		// Insert callback into url or form data
+		if ( replaceInUrl ) {
+			s.url = url.replace( rjsonp, "$1" + callbackName );
+		} else if ( replaceInData ) {
+			s.data = data.replace( rjsonp, "$1" + callbackName );
+		} else if ( hasCallback ) {
+			s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+		}
+
+		// Use data converter to retrieve json after script execution
+		s.converters["script json"] = function() {
+			if ( !responseContainer ) {
+				jQuery.error( callbackName + " was not called" );
+			}
+			return responseContainer[ 0 ];
+		};
+
+		// force json dataType
+		s.dataTypes[ 0 ] = "json";
+
+		// Install callback
+		window[ callbackName ] = function() {
+			responseContainer = arguments;
+		};
+
+		// Clean-up function (fires after converters)
+		jqXHR.always(function() {
+			// Restore preexisting value
+			window[ callbackName ] = overwritten;
+
+			// Save back as free
+			if ( s[ callbackName ] ) {
+				// make sure that re-using the options doesn't screw things around
+				s.jsonpCallback = originalSettings.jsonpCallback;
+
+				// save the callback name for future use
+				oldCallbacks.push( callbackName );
+			}
+
+			// Call if it was a function and we have a response
+			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+				overwritten( responseContainer[ 0 ] );
+			}
+
+			responseContainer = overwritten = undefined;
+		});
+
+		// Delegate to script
+		return "script";
+	}
+});
+// Install script dataType
+jQuery.ajaxSetup({
+	accepts: {
+		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+	},
+	contents: {
+		script: /javascript|ecmascript/
+	},
+	converters: {
+		"text script": function( text ) {
+			jQuery.globalEval( text );
+			return text;
+		}
+	}
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+	if ( s.cache === undefined ) {
+		s.cache = false;
+	}
+	if ( s.crossDomain ) {
+		s.type = "GET";
+		s.global = false;
+	}
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+	// This transport only deals with cross domain requests
+	if ( s.crossDomain ) {
+
+		var script,
+			head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
+
+		return {
+
+			send: function( _, callback ) {
+
+				script = document.createElement( "script" );
+
+				script.async = "async";
+
+				if ( s.scriptCharset ) {
+					script.charset = s.scriptCharset;
+				}
+
+				script.src = s.url;
+
+				// Attach handlers for all browsers
+				script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+					if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+						// Handle memory leak in IE
+						script.onload = script.onreadystatechange = null;
+
+						// Remove the script
+						if ( head && script.parentNode ) {
+							head.removeChild( script );
+						}
+
+						// Dereference the script
+						script = undefined;
+
+						// Callback if not abort
+						if ( !isAbort ) {
+							callback( 200, "success" );
+						}
+					}
+				};
+				// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+				// This arises when a base node is used (#2709 and #4378).
+				head.insertBefore( script, head.firstChild );
+			},
+
+			abort: function() {
+				if ( script ) {
+					script.onload( 0, 1 );
+				}
+			}
+		};
+	}
+});
+var xhrCallbacks,
+	// #5280: Internet Explorer will keep connections alive if we don't abort on unload
+	xhrOnUnloadAbort = window.ActiveXObject ? function() {
+		// Abort all pending requests
+		for ( var key in xhrCallbacks ) {
+			xhrCallbacks[ key ]( 0, 1 );
+		}
+	} : false,
+	xhrId = 0;
+
+// Functions to create xhrs
+function createStandardXHR() {
+	try {
+		return new window.XMLHttpRequest();
+	} catch( e ) {}
+}
+
+function createActiveXHR() {
+	try {
+		return new window.ActiveXObject( "Microsoft.XMLHTTP" );
+	} catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+	/* Microsoft failed to properly
+	 * implement the XMLHttpRequest in IE7 (can't request local files),
+	 * so we use the ActiveXObject when it is available
+	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+	 * we need a fallback.
+	 */
+	function() {
+		return !this.isLocal && createStandardXHR() || createActiveXHR();
+	} :
+	// For all other browsers, use the standard XMLHttpRequest object
+	createStandardXHR;
+
+// Determine support properties
+(function( xhr ) {
+	jQuery.extend( jQuery.support, {
+		ajax: !!xhr,
+		cors: !!xhr && ( "withCredentials" in xhr )
+	});
+})( jQuery.ajaxSettings.xhr() );
+
+// Create transport if the browser can provide an xhr
+if ( jQuery.support.ajax ) {
+
+	jQuery.ajaxTransport(function( s ) {
+		// Cross domain only allowed if supported through XMLHttpRequest
+		if ( !s.crossDomain || jQuery.support.cors ) {
+
+			var callback;
+
+			return {
+				send: function( headers, complete ) {
+
+					// Get a new xhr
+					var handle, i,
+						xhr = s.xhr();
+
+					// Open the socket
+					// Passing null username, generates a login popup on Opera (#2865)
+					if ( s.username ) {
+						xhr.open( s.type, s.url, s.async, s.username, s.password );
+					} else {
+						xhr.open( s.type, s.url, s.async );
+					}
+
+					// Apply custom fields if provided
+					if ( s.xhrFields ) {
+						for ( i in s.xhrFields ) {
+							xhr[ i ] = s.xhrFields[ i ];
+						}
+					}
+
+					// Override mime type if needed
+					if ( s.mimeType && xhr.overrideMimeType ) {
+						xhr.overrideMimeType( s.mimeType );
+					}
+
+					// X-Requested-With header
+					// For cross-domain requests, seeing as conditions for a preflight are
+					// akin to a jigsaw puzzle, we simply never set it to be sure.
+					// (it can always be set on a per-request basis or even using ajaxSetup)
+					// For same-domain requests, won't change header if already provided.
+					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+						headers[ "X-Requested-With" ] = "XMLHttpRequest";
+					}
+
+					// Need an extra try/catch for cross domain requests in Firefox 3
+					try {
+						for ( i in headers ) {
+							xhr.setRequestHeader( i, headers[ i ] );
+						}
+					} catch( _ ) {}
+
+					// Do send the request
+					// This may raise an exception which is actually
+					// handled in jQuery.ajax (so no try/catch here)
+					xhr.send( ( s.hasContent && s.data ) || null );
+
+					// Listener
+					callback = function( _, isAbort ) {
+
+						var status,
+							statusText,
+							responseHeaders,
+							responses,
+							xml;
+
+						// Firefox throws exceptions when accessing properties
+						// of an xhr when a network error occurred
+						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+						try {
+
+							// Was never called and is aborted or complete
+							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+								// Only called once
+								callback = undefined;
+
+								// Do not keep as active anymore
+								if ( handle ) {
+									xhr.onreadystatechange = jQuery.noop;
+									if ( xhrOnUnloadAbort ) {
+										delete xhrCallbacks[ handle ];
+									}
+								}
+
+								// If it's an abort
+								if ( isAbort ) {
+									// Abort it manually if needed
+									if ( xhr.readyState !== 4 ) {
+										xhr.abort();
+									}
+								} else {
+									status = xhr.status;
+									responseHeaders = xhr.getAllResponseHeaders();
+									responses = {};
+									xml = xhr.responseXML;
+
+									// Construct response list
+									if ( xml && xml.documentElement /* #4958 */ ) {
+										responses.xml = xml;
+									}
+
+									// When requesting binary data, IE6-9 will throw an exception
+									// on any attempt to access responseText (#11426)
+									try {
+										responses.text = xhr.responseText;
+									} catch( _ ) {
+									}
+
+									// Firefox throws an exception when accessing
+									// statusText for faulty cross-domain requests
+									try {
+										statusText = xhr.statusText;
+									} catch( e ) {
+										// We normalize with Webkit giving an empty statusText
+										statusText = "";
+									}
+
+									// Filter status for non standard behaviors
+
+									// If the request is local and we have data: assume a success
+									// (success with no data won't get notified, that's the best we
+									// can do given current implementations)
+									if ( !status && s.isLocal && !s.crossDomain ) {
+										status = responses.text ? 200 : 404;
+									// IE - #1450: sometimes returns 1223 when it should be 204
+									} else if ( status === 1223 ) {
+										status = 204;
+									}
+								}
+							}
+						} catch( firefoxAccessException ) {
+							if ( !isAbort ) {
+								complete( -1, firefoxAccessException );
+							}
+						}
+
+						// Call complete if needed
+						if ( responses ) {
+							complete( status, statusText, responses, responseHeaders );
+						}
+					};
+
+					if ( !s.async ) {
+						// if we're in sync mode we fire the callback
+						callback();
+					} else if ( xhr.readyState === 4 ) {
+						// (IE6 & IE7) if it's in cache and has been
+						// retrieved directly we need to fire the callback
+						setTimeout( callback, 0 );
+					} else {
+						handle = ++xhrId;
+						if ( xhrOnUnloadAbort ) {
+							// Create the active xhrs callbacks list if needed
+							// and attach the unload handler
+							if ( !xhrCallbacks ) {
+								xhrCallbacks = {};
+								jQuery( window ).unload( xhrOnUnloadAbort );
+							}
+							// Add to list of active xhrs callbacks
+							xhrCallbacks[ handle ] = callback;
+						}
+						xhr.onreadystatechange = callback;
+					}
+				},
+
+				abort: function() {
+					if ( callback ) {
+						callback(0,1);
+					}
+				}
+			};
+		}
+	});
+}
+var fxNow, timerId,
+	rfxtypes = /^(?:toggle|show|hide)$/,
+	rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+	rrun = /queueHooks$/,
+	animationPrefilters = [ defaultPrefilter ],
+	tweeners = {
+		"*": [function( prop, value ) {
+			var end, unit,
+				tween = this.createTween( prop, value ),
+				parts = rfxnum.exec( value ),
+				target = tween.cur(),
+				start = +target || 0,
+				scale = 1,
+				maxIterations = 20;
+
+			if ( parts ) {
+				end = +parts[2];
+				unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+
+				// We need to compute starting value
+				if ( unit !== "px" && start ) {
+					// Iteratively approximate from a nonzero starting point
+					// Prefer the current property, because this process will be trivial if it uses the same units
+					// Fallback to end or a simple constant
+					start = jQuery.css( tween.elem, prop, true ) || end || 1;
+
+					do {
+						// If previous iteration zeroed out, double until we get *something*
+						// Use a string for doubling factor so we don't accidentally see scale as unchanged below
+						scale = scale || ".5";
+
+						// Adjust and apply
+						start = start / scale;
+						jQuery.style( tween.elem, prop, start + unit );
+
+					// Update scale, tolerating zero or NaN from tween.cur()
+					// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+					} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+				}
+
+				tween.unit = unit;
+				tween.start = start;
+				// If a +=/-= token was provided, we're doing a relative animation
+				tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end;
+			}
+			return tween;
+		}]
+	};
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+	setTimeout(function() {
+		fxNow = undefined;
+	}, 0 );
+	return ( fxNow = jQuery.now() );
+}
+
+function createTweens( animation, props ) {
+	jQuery.each( props, function( prop, value ) {
+		var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+			index = 0,
+			length = collection.length;
+		for ( ; index < length; index++ ) {
+			if ( collection[ index ].call( animation, prop, value ) ) {
+
+				// we're done with this property
+				return;
+			}
+		}
+	});
+}
+
+function Animation( elem, properties, options ) {
+	var result,
+		index = 0,
+		tweenerIndex = 0,
+		length = animationPrefilters.length,
+		deferred = jQuery.Deferred().always( function() {
+			// don't match elem in the :animated selector
+			delete tick.elem;
+		}),
+		tick = function() {
+			var currentTime = fxNow || createFxNow(),
+				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+				percent = 1 - ( remaining / animation.duration || 0 ),
+				index = 0,
+				length = animation.tweens.length;
+
+			for ( ; index < length ; index++ ) {
+				animation.tweens[ index ].run( percent );
+			}
+
+			deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+			if ( percent < 1 && length ) {
+				return remaining;
+			} else {
+				deferred.resolveWith( elem, [ animation ] );
+				return false;
+			}
+		},
+		animation = deferred.promise({
+			elem: elem,
+			props: jQuery.extend( {}, properties ),
+			opts: jQuery.extend( true, { specialEasing: {} }, options ),
+			originalProperties: properties,
+			originalOptions: options,
+			startTime: fxNow || createFxNow(),
+			duration: options.duration,
+			tweens: [],
+			createTween: function( prop, end, easing ) {
+				var tween = jQuery.Tween( elem, animation.opts, prop, end,
+						animation.opts.specialEasing[ prop ] || animation.opts.easing );
+				animation.tweens.push( tween );
+				return tween;
+			},
+			stop: function( gotoEnd ) {
+				var index = 0,
+					// if we are going to the end, we want to run all the tweens
+					// otherwise we skip this part
+					length = gotoEnd ? animation.tweens.length : 0;
+
+				for ( ; index < length ; index++ ) {
+					animation.tweens[ index ].run( 1 );
+				}
+
+				// resolve when we played the last frame
+				// otherwise, reject
+				if ( gotoEnd ) {
+					deferred.resolveWith( elem, [ animation, gotoEnd ] );
+				} else {
+					deferred.rejectWith( elem, [ animation, gotoEnd ] );
+				}
+				return this;
+			}
+		}),
+		props = animation.props;
+
+	propFilter( props, animation.opts.specialEasing );
+
+	for ( ; index < length ; index++ ) {
+		result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+		if ( result ) {
+			return result;
+		}
+	}
+
+	createTweens( animation, props );
+
+	if ( jQuery.isFunction( animation.opts.start ) ) {
+		animation.opts.start.call( elem, animation );
+	}
+
+	jQuery.fx.timer(
+		jQuery.extend( tick, {
+			anim: animation,
+			queue: animation.opts.queue,
+			elem: elem
+		})
+	);
+
+	// attach callbacks from options
+	return animation.progress( animation.opts.progress )
+		.done( animation.opts.done, animation.opts.complete )
+		.fail( animation.opts.fail )
+		.always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+	var index, name, easing, value, hooks;
+
+	// camelCase, specialEasing and expand cssHook pass
+	for ( index in props ) {
+		name = jQuery.camelCase( index );
+		easing = specialEasing[ name ];
+		value = props[ index ];
+		if ( jQuery.isArray( value ) ) {
+			easing = value[ 1 ];
+			value = props[ index ] = value[ 0 ];
+		}
+
+		if ( index !== name ) {
+			props[ name ] = value;
+			delete props[ index ];
+		}
+
+		hooks = jQuery.cssHooks[ name ];
+		if ( hooks && "expand" in hooks ) {
+			value = hooks.expand( value );
+			delete props[ name ];
+
+			// not quite $.extend, this wont overwrite keys already present.
+			// also - reusing 'index' from above because we have the correct "name"
+			for ( index in value ) {
+				if ( !( index in props ) ) {
+					props[ index ] = value[ index ];
+					specialEasing[ index ] = easing;
+				}
+			}
+		} else {
+			specialEasing[ name ] = easing;
+		}
+	}
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+	tweener: function( props, callback ) {
+		if ( jQuery.isFunction( props ) ) {
+			callback = props;
+			props = [ "*" ];
+		} else {
+			props = props.split(" ");
+		}
+
+		var prop,
+			index = 0,
+			length = props.length;
+
+		for ( ; index < length ; index++ ) {
+			prop = props[ index ];
+			tweeners[ prop ] = tweeners[ prop ] || [];
+			tweeners[ prop ].unshift( callback );
+		}
+	},
+
+	prefilter: function( callback, prepend ) {
+		if ( prepend ) {
+			animationPrefilters.unshift( callback );
+		} else {
+			animationPrefilters.push( callback );
+		}
+	}
+});
+
+function defaultPrefilter( elem, props, opts ) {
+	var index, prop, value, length, dataShow, tween, hooks, oldfire,
+		anim = this,
+		style = elem.style,
+		orig = {},
+		handled = [],
+		hidden = elem.nodeType && isHidden( elem );
+
+	// handle queue: false promises
+	if ( !opts.queue ) {
+		hooks = jQuery._queueHooks( elem, "fx" );
+		if ( hooks.unqueued == null ) {
+			hooks.unqueued = 0;
+			oldfire = hooks.empty.fire;
+			hooks.empty.fire = function() {
+				if ( !hooks.unqueued ) {
+					oldfire();
+				}
+			};
+		}
+		hooks.unqueued++;
+
+		anim.always(function() {
+			// doing this makes sure that the complete handler will be called
+			// before this completes
+			anim.always(function() {
+				hooks.unqueued--;
+				if ( !jQuery.queue( elem, "fx" ).length ) {
+					hooks.empty.fire();
+				}
+			});
+		});
+	}
+
+	// height/width overflow pass
+	if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+		// Make sure that nothing sneaks out
+		// Record all 3 overflow attributes because IE does not
+		// change the overflow attribute when overflowX and
+		// overflowY are set to the same value
+		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+		// Set display property to inline-block for height/width
+		// animations on inline elements that are having width/height animated
+		if ( jQuery.css( elem, "display" ) === "inline" &&
+				jQuery.css( elem, "float" ) === "none" ) {
+
+			// inline-level elements accept inline-block;
+			// block-level elements need to be inline with layout
+			if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+				style.display = "inline-block";
+
+			} else {
+				style.zoom = 1;
+			}
+		}
+	}
+
+	if ( opts.overflow ) {
+		style.overflow = "hidden";
+		if ( !jQuery.support.shrinkWrapBlocks ) {
+			anim.done(function() {
+				style.overflow = opts.overflow[ 0 ];
+				style.overflowX = opts.overflow[ 1 ];
+				style.overflowY = opts.overflow[ 2 ];
+			});
+		}
+	}
+
+
+	// show/hide pass
+	for ( index in props ) {
+		value = props[ index ];
+		if ( rfxtypes.exec( value ) ) {
+			delete props[ index ];
+			if ( value === ( hidden ? "hide" : "show" ) ) {
+				continue;
+			}
+			handled.push( index );
+		}
+	}
+
+	length = handled.length;
+	if ( length ) {
+		dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+		if ( hidden ) {
+			jQuery( elem ).show();
+		} else {
+			anim.done(function() {
+				jQuery( elem ).hide();
+			});
+		}
+		anim.done(function() {
+			var prop;
+			jQuery.removeData( elem, "fxshow", true );
+			for ( prop in orig ) {
+				jQuery.style( elem, prop, orig[ prop ] );
+			}
+		});
+		for ( index = 0 ; index < length ; index++ ) {
+			prop = handled[ index ];
+			tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+			orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
+
+			if ( !( prop in dataShow ) ) {
+				dataShow[ prop ] = tween.start;
+				if ( hidden ) {
+					tween.end = tween.start;
+					tween.start = prop === "width" || prop === "height" ? 1 : 0;
+				}
+			}
+		}
+	}
+}
+
+function Tween( elem, options, prop, end, easing ) {
+	return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+	constructor: Tween,
+	init: function( elem, options, prop, end, easing, unit ) {
+		this.elem = elem;
+		this.prop = prop;
+		this.easing = easing || "swing";
+		this.options = options;
+		this.start = this.now = this.cur();
+		this.end = end;
+		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+	},
+	cur: function() {
+		var hooks = Tween.propHooks[ this.prop ];
+
+		return hooks && hooks.get ?
+			hooks.get( this ) :
+			Tween.propHooks._default.get( this );
+	},
+	run: function( percent ) {
+		var eased,
+			hooks = Tween.propHooks[ this.prop ];
+
+		if ( this.options.duration ) {
+			this.pos = eased = jQuery.easing[ this.easing ](
+				percent, this.options.duration * percent, 0, 1, this.options.duration
+			);
+		} else {
+			this.pos = eased = percent;
+		}
+		this.now = ( this.end - this.start ) * eased + this.start;
+
+		if ( this.options.step ) {
+			this.options.step.call( this.elem, this.now, this );
+		}
+
+		if ( hooks && hooks.set ) {
+			hooks.set( this );
+		} else {
+			Tween.propHooks._default.set( this );
+		}
+		return this;
+	}
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+	_default: {
+		get: function( tween ) {
+			var result;
+
+			if ( tween.elem[ tween.prop ] != null &&
+				(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+				return tween.elem[ tween.prop ];
+			}
+
+			// passing any value as a 4th parameter to .css will automatically
+			// attempt a parseFloat and fallback to a string if the parse fails
+			// so, simple values such as "10px" are parsed to Float.
+			// complex values such as "rotate(1rad)" are returned as is.
+			result = jQuery.css( tween.elem, tween.prop, false, "" );
+			// Empty strings, null, undefined and "auto" are converted to 0.
+			return !result || result === "auto" ? 0 : result;
+		},
+		set: function( tween ) {
+			// use step hook for back compat - use cssHook if its there - use .style if its
+			// available and use plain properties where available
+			if ( jQuery.fx.step[ tween.prop ] ) {
+				jQuery.fx.step[ tween.prop ]( tween );
+			} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+			} else {
+				tween.elem[ tween.prop ] = tween.now;
+			}
+		}
+	}
+};
+
+// Remove in 2.0 - this supports IE8's panic based approach
+// to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+	set: function( tween ) {
+		if ( tween.elem.nodeType && tween.elem.parentNode ) {
+			tween.elem[ tween.prop ] = tween.now;
+		}
+	}
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+	var cssFn = jQuery.fn[ name ];
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return speed == null || typeof speed === "boolean" ||
+			// special check for .toggle( handler, handler, ... )
+			( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ?
+			cssFn.apply( this, arguments ) :
+			this.animate( genFx( name, true ), speed, easing, callback );
+	};
+});
+
+jQuery.fn.extend({
+	fadeTo: function( speed, to, easing, callback ) {
+
+		// show any hidden elements after setting opacity to 0
+		return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+			// animate to the value specified
+			.end().animate({ opacity: to }, speed, easing, callback );
+	},
+	animate: function( prop, speed, easing, callback ) {
+		var empty = jQuery.isEmptyObject( prop ),
+			optall = jQuery.speed( speed, easing, callback ),
+			doAnimation = function() {
+				// Operate on a copy of prop so per-property easing won't be lost
+				var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+				// Empty animations resolve immediately
+				if ( empty ) {
+					anim.stop( true );
+				}
+			};
+
+		return empty || optall.queue === false ?
+			this.each( doAnimation ) :
+			this.queue( optall.queue, doAnimation );
+	},
+	stop: function( type, clearQueue, gotoEnd ) {
+		var stopQueue = function( hooks ) {
+			var stop = hooks.stop;
+			delete hooks.stop;
+			stop( gotoEnd );
+		};
+
+		if ( typeof type !== "string" ) {
+			gotoEnd = clearQueue;
+			clearQueue = type;
+			type = undefined;
+		}
+		if ( clearQueue && type !== false ) {
+			this.queue( type || "fx", [] );
+		}
+
+		return this.each(function() {
+			var dequeue = true,
+				index = type != null && type + "queueHooks",
+				timers = jQuery.timers,
+				data = jQuery._data( this );
+
+			if ( index ) {
+				if ( data[ index ] && data[ index ].stop ) {
+					stopQueue( data[ index ] );
+				}
+			} else {
+				for ( index in data ) {
+					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+						stopQueue( data[ index ] );
+					}
+				}
+			}
+
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+					timers[ index ].anim.stop( gotoEnd );
+					dequeue = false;
+					timers.splice( index, 1 );
+				}
+			}
+
+			// start the next in the queue if the last step wasn't forced
+			// timers currently will call their complete callbacks, which will dequeue
+			// but only if they were gotoEnd
+			if ( dequeue || !gotoEnd ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	}
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+	var which,
+		attrs = { height: type },
+		i = 0;
+
+	// if we include width, step value is 1 to do all cssExpand values,
+	// if we don't include width, step value is 2 to skip over Left and Right
+	includeWidth = includeWidth? 1 : 0;
+	for( ; i < 4 ; i += 2 - includeWidth ) {
+		which = cssExpand[ i ];
+		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+	}
+
+	if ( includeWidth ) {
+		attrs.opacity = attrs.width = type;
+	}
+
+	return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+	slideDown: genFx("show"),
+	slideUp: genFx("hide"),
+	slideToggle: genFx("toggle"),
+	fadeIn: { opacity: "show" },
+	fadeOut: { opacity: "hide" },
+	fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return this.animate( props, speed, easing, callback );
+	};
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+		complete: fn || !fn && easing ||
+			jQuery.isFunction( speed ) && speed,
+		duration: speed,
+		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+	};
+
+	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+	// normalize opt.queue - true/undefined/null -> "fx"
+	if ( opt.queue == null || opt.queue === true ) {
+		opt.queue = "fx";
+	}
+
+	// Queueing
+	opt.old = opt.complete;
+
+	opt.complete = function() {
+		if ( jQuery.isFunction( opt.old ) ) {
+			opt.old.call( this );
+		}
+
+		if ( opt.queue ) {
+			jQuery.dequeue( this, opt.queue );
+		}
+	};
+
+	return opt;
+};
+
+jQuery.easing = {
+	linear: function( p ) {
+		return p;
+	},
+	swing: function( p ) {
+		return 0.5 - Math.cos( p*Math.PI ) / 2;
+	}
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+	var timer,
+		timers = jQuery.timers,
+		i = 0;
+
+	for ( ; i < timers.length; i++ ) {
+		timer = timers[ i ];
+		// Checks the timer has not already been removed
+		if ( !timer() && timers[ i ] === timer ) {
+			timers.splice( i--, 1 );
+		}
+	}
+
+	if ( !timers.length ) {
+		jQuery.fx.stop();
+	}
+};
+
+jQuery.fx.timer = function( timer ) {
+	if ( timer() && jQuery.timers.push( timer ) && !timerId ) {
+		timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+	}
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.stop = function() {
+	clearInterval( timerId );
+	timerId = null;
+};
+
+jQuery.fx.speeds = {
+	slow: 600,
+	fast: 200,
+	// Default speed
+	_default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.animated = function( elem ) {
+		return jQuery.grep(jQuery.timers, function( fn ) {
+			return elem === fn.elem;
+		}).length;
+	};
+}
+var rroot = /^(?:body|html)$/i;
+
+jQuery.fn.offset = function( options ) {
+	if ( arguments.length ) {
+		return options === undefined ?
+			this :
+			this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+	}
+
+	var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft,
+		box = { top: 0, left: 0 },
+		elem = this[ 0 ],
+		doc = elem && elem.ownerDocument;
+
+	if ( !doc ) {
+		return;
+	}
+
+	if ( (body = doc.body) === elem ) {
+		return jQuery.offset.bodyOffset( elem );
+	}
+
+	docElem = doc.documentElement;
+
+	// Make sure it's not a disconnected DOM node
+	if ( !jQuery.contains( docElem, elem ) ) {
+		return box;
+	}
+
+	// If we don't have gBCR, just use 0,0 rather than error
+	// BlackBerry 5, iOS 3 (original iPhone)
+	if ( typeof elem.getBoundingClientRect !== "undefined" ) {
+		box = elem.getBoundingClientRect();
+	}
+	win = getWindow( doc );
+	clientTop  = docElem.clientTop  || body.clientTop  || 0;
+	clientLeft = docElem.clientLeft || body.clientLeft || 0;
+	scrollTop  = win.pageYOffset || docElem.scrollTop;
+	scrollLeft = win.pageXOffset || docElem.scrollLeft;
+	return {
+		top: box.top  + scrollTop  - clientTop,
+		left: box.left + scrollLeft - clientLeft
+	};
+};
+
+jQuery.offset = {
+
+	bodyOffset: function( body ) {
+		var top = body.offsetTop,
+			left = body.offsetLeft;
+
+		if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) {
+			top  += parseFloat( jQuery.css(body, "marginTop") ) || 0;
+			left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
+		}
+
+		return { top: top, left: left };
+	},
+
+	setOffset: function( elem, options, i ) {
+		var position = jQuery.css( elem, "position" );
+
+		// set position first, in-case top/left are set even on static elem
+		if ( position === "static" ) {
+			elem.style.position = "relative";
+		}
+
+		var curElem = jQuery( elem ),
+			curOffset = curElem.offset(),
+			curCSSTop = jQuery.css( elem, "top" ),
+			curCSSLeft = jQuery.css( elem, "left" ),
+			calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+			props = {}, curPosition = {}, curTop, curLeft;
+
+		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+		if ( calculatePosition ) {
+			curPosition = curElem.position();
+			curTop = curPosition.top;
+			curLeft = curPosition.left;
+		} else {
+			curTop = parseFloat( curCSSTop ) || 0;
+			curLeft = parseFloat( curCSSLeft ) || 0;
+		}
+
+		if ( jQuery.isFunction( options ) ) {
+			options = options.call( elem, i, curOffset );
+		}
+
+		if ( options.top != null ) {
+			props.top = ( options.top - curOffset.top ) + curTop;
+		}
+		if ( options.left != null ) {
+			props.left = ( options.left - curOffset.left ) + curLeft;
+		}
+
+		if ( "using" in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	}
+};
+
+
+jQuery.fn.extend({
+
+	position: function() {
+		if ( !this[0] ) {
+			return;
+		}
+
+		var elem = this[0],
+
+		// Get *real* offsetParent
+		offsetParent = this.offsetParent(),
+
+		// Get correct offsets
+		offset       = this.offset(),
+		parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+		// Subtract element margins
+		// note: when an element has margin: auto the offsetLeft and marginLeft
+		// are the same in Safari causing offset.left to incorrectly be 0
+		offset.top  -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+		offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+		// Add offsetParent borders
+		parentOffset.top  += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+		parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+		// Subtract the two offsets
+		return {
+			top:  offset.top  - parentOffset.top,
+			left: offset.left - parentOffset.left
+		};
+	},
+
+	offsetParent: function() {
+		return this.map(function() {
+			var offsetParent = this.offsetParent || document.body;
+			while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+				offsetParent = offsetParent.offsetParent;
+			}
+			return offsetParent || document.body;
+		});
+	}
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+	var top = /Y/.test( prop );
+
+	jQuery.fn[ method ] = function( val ) {
+		return jQuery.access( this, function( elem, method, val ) {
+			var win = getWindow( elem );
+
+			if ( val === undefined ) {
+				return win ? (prop in win) ? win[ prop ] :
+					win.document.documentElement[ method ] :
+					elem[ method ];
+			}
+
+			if ( win ) {
+				win.scrollTo(
+					!top ? val : jQuery( win ).scrollLeft(),
+					 top ? val : jQuery( win ).scrollTop()
+				);
+
+			} else {
+				elem[ method ] = val;
+			}
+		}, method, val, arguments.length, null );
+	};
+});
+
+function getWindow( elem ) {
+	return jQuery.isWindow( elem ) ?
+		elem :
+		elem.nodeType === 9 ?
+			elem.defaultView || elem.parentWindow :
+			false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+		// margin is only for outerHeight, outerWidth
+		jQuery.fn[ funcName ] = function( margin, value ) {
+			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+			return jQuery.access( this, function( elem, type, value ) {
+				var doc;
+
+				if ( jQuery.isWindow( elem ) ) {
+					// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+					// isn't a whole lot we can do. See pull request at this URL for discussion:
+					// https://github.com/jquery/jquery/pull/764
+					return elem.document.documentElement[ "client" + name ];
+				}
+
+				// Get document width or height
+				if ( elem.nodeType === 9 ) {
+					doc = elem.documentElement;
+
+					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+					// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+					return Math.max(
+						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+						elem.body[ "offset" + name ], doc[ "offset" + name ],
+						doc[ "client" + name ]
+					);
+				}
+
+				return value === undefined ?
+					// Get width or height on the element, requesting but not forcing parseFloat
+					jQuery.css( elem, type, value, extra ) :
+
+					// Set width or height on the element
+					jQuery.style( elem, type, value, extra );
+			}, type, chainable ? margin : undefined, chainable, null );
+		};
+	});
+});
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+	define( "jquery", [], function () { return jQuery; } );
+}
+
+})( window );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.accordion.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.accordion.css
new file mode 100644
index 0000000..60975ac
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.accordion.css
@@ -0,0 +1,16 @@
+/*!
+ * jQuery UI Accordion @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+.ui-accordion .ui-accordion-header { display: block; cursor: pointer; position: relative; margin-top: 2px; padding: .5em .5em .5em .7em; zoom: 1; }
+.ui-accordion .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-noicons { padding-left: .7em; }
+.ui-accordion .ui-accordion-icons .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; overflow: auto; zoom: 1; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.all.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.all.css
new file mode 100644
index 0000000..c92e2a5
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.all.css
@@ -0,0 +1,12 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import "jquery.ui.base.css";
+ at import "jquery.ui.theme.css";
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.autocomplete.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.autocomplete.css
new file mode 100644
index 0000000..05ae310
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.autocomplete.css
@@ -0,0 +1,18 @@
+/*!
+ * jQuery UI Autocomplete @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete {
+	position: absolute;
+	top: 0; /* #8656 */
+	cursor: default;
+}
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.base.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.base.css
new file mode 100644
index 0000000..916f0b5
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.base.css
@@ -0,0 +1,26 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import url("jquery.ui.core.css");
+
+ at import url("jquery.ui.accordion.css");
+ at import url("jquery.ui.autocomplete.css");
+ at import url("jquery.ui.button.css");
+ at import url("jquery.ui.datepicker.css");
+ at import url("jquery.ui.dialog.css");
+ at import url("jquery.ui.menu.css");
+ at import url("jquery.ui.menubar.css");
+ at import url("jquery.ui.progressbar.css");
+ at import url("jquery.ui.resizable.css");
+ at import url("jquery.ui.selectable.css");
+ at import url("jquery.ui.slider.css");
+ at import url("jquery.ui.spinner.css");
+ at import url("jquery.ui.tabs.css");
+ at import url("jquery.ui.tooltip.css");
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.css
new file mode 100644
index 0000000..1faeff6
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.css
@@ -0,0 +1,40 @@
+/*!
+ * jQuery UI Button @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; }
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; } 
+button.ui-button-icons-only { width: 3.7em; } 
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.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; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.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; }
+.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; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.js
new file mode 100644
index 0000000..5ae5264
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.button.js
@@ -0,0 +1,415 @@
+/*!
+ * jQuery UI Button @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/button/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var lastActive, startXPos, startYPos, clickDragged,
+	baseClasses = "ui-button ui-widget ui-state-default ui-corner-all",
+	stateClasses = "ui-state-hover ui-state-active ",
+	typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",
+	formResetHandler = function() {
+		var buttons = $( this ).find( ":ui-button" );
+		setTimeout(function() {
+			buttons.button( "refresh" );
+		}, 1 );
+	},
+	radioGroup = function( radio ) {
+		var name = radio.name,
+			form = radio.form,
+			radios = $( [] );
+		if ( name ) {
+			if ( form ) {
+				radios = $( form ).find( "[name='" + name + "']" );
+			} else {
+				radios = $( "[name='" + name + "']", radio.ownerDocument )
+					.filter(function() {
+						return !this.form;
+					});
+			}
+		}
+		return radios;
+	};
+
+$.widget( "ui.button", {
+	version: "@VERSION",
+	defaultElement: "<button>",
+	options: {
+		disabled: null,
+		text: true,
+		label: null,
+		icons: {
+			primary: null,
+			secondary: null
+		}
+	},
+	_create: function() {
+		this.element.closest( "form" )
+			.unbind( "reset" + this.eventNamespace )
+			.bind( "reset" + this.eventNamespace, formResetHandler );
+
+		if ( typeof this.options.disabled !== "boolean" ) {
+			this.options.disabled = !!this.element.prop( "disabled" );
+		} else {
+			this.element.prop( "disabled", this.options.disabled );
+		}
+
+		this._determineButtonType();
+		this.hasTitle = !!this.buttonElement.attr( "title" );
+
+		var that = this,
+			options = this.options,
+			toggleButton = this.type === "checkbox" || this.type === "radio",
+			hoverClass = "ui-state-hover" + ( !toggleButton ? " ui-state-active" : "" ),
+			focusClass = "ui-state-focus";
+
+		if ( options.label === null ) {
+			options.label = (this.type === "input" ? this.buttonElement.val() : this.buttonElement.html());
+		}
+
+		this.buttonElement
+			.addClass( baseClasses )
+			.attr( "role", "button" )
+			.bind( "mouseenter" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).addClass( "ui-state-hover" );
+				if ( this === lastActive ) {
+					$( this ).addClass( "ui-state-active" );
+				}
+			})
+			.bind( "mouseleave" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( hoverClass );
+			})
+			.bind( "click" + this.eventNamespace, function( event ) {
+				if ( options.disabled ) {
+					event.preventDefault();
+					event.stopImmediatePropagation();
+				}
+			});
+
+		this.element
+			.bind( "focus" + this.eventNamespace, function() {
+				// no need to check disabled, focus won't be triggered anyway
+				that.buttonElement.addClass( focusClass );
+			})
+			.bind( "blur" + this.eventNamespace, function() {
+				that.buttonElement.removeClass( focusClass );
+			});
+
+		if ( toggleButton ) {
+			this.element.bind( "change" + this.eventNamespace, function() {
+				if ( clickDragged ) {
+					return;
+				}
+				that.refresh();
+			});
+			// if mouse moves between mousedown and mouseup (drag) set clickDragged flag
+			// prevents issue where button state changes but checkbox/radio checked state
+			// does not in Firefox (see ticket #6970)
+			this.buttonElement
+				.bind( "mousedown" + this.eventNamespace, function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					clickDragged = false;
+					startXPos = event.pageX;
+					startYPos = event.pageY;
+				})
+				.bind( "mouseup" + this.eventNamespace, function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					if ( startXPos !== event.pageX || startYPos !== event.pageY ) {
+						clickDragged = true;
+					}
+			});
+		}
+
+		if ( this.type === "checkbox" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).toggleClass( "ui-state-active" );
+				that.buttonElement.attr( "aria-pressed", that.element[0].checked );
+			});
+		} else if ( this.type === "radio" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).addClass( "ui-state-active" );
+				that.buttonElement.attr( "aria-pressed", "true" );
+
+				var radio = that.element[ 0 ];
+				radioGroup( radio )
+					.not( radio )
+					.map(function() {
+						return $( this ).button( "widget" )[ 0 ];
+					})
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			});
+		} else {
+			this.buttonElement
+				.bind( "mousedown" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).addClass( "ui-state-active" );
+					lastActive = this;
+					that.document.one( "mouseup", function() {
+						lastActive = null;
+					});
+				})
+				.bind( "mouseup" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).removeClass( "ui-state-active" );
+				})
+				.bind( "keydown" + this.eventNamespace, function(event) {
+					if ( options.disabled ) {
+						return false;
+					}
+					if ( event.keyCode === $.ui.keyCode.SPACE || event.keyCode === $.ui.keyCode.ENTER ) {
+						$( this ).addClass( "ui-state-active" );
+					}
+				})
+				.bind( "keyup" + this.eventNamespace, function() {
+					$( this ).removeClass( "ui-state-active" );
+				});
+
+			if ( this.buttonElement.is("a") ) {
+				this.buttonElement.keyup(function(event) {
+					if ( event.keyCode === $.ui.keyCode.SPACE ) {
+						// TODO pass through original event correctly (just as 2nd argument doesn't work)
+						$( this ).click();
+					}
+				});
+			}
+		}
+
+		// TODO: pull out $.Widget's handling for the disabled option into
+		// $.Widget.prototype._setOptionDisabled so it's easy to proxy and can
+		// be overridden by individual plugins
+		this._setOption( "disabled", options.disabled );
+		this._resetButton();
+	},
+
+	_determineButtonType: function() {
+		var ancestor, labelSelector, checked;
+
+		if ( this.element.is("[type=checkbox]") ) {
+			this.type = "checkbox";
+		} else if ( this.element.is("[type=radio]") ) {
+			this.type = "radio";
+		} else if ( this.element.is("input") ) {
+			this.type = "input";
+		} else {
+			this.type = "button";
+		}
+
+		if ( this.type === "checkbox" || this.type === "radio" ) {
+			// we don't search against the document in case the element
+			// is disconnected from the DOM
+			ancestor = this.element.parents().last();
+			labelSelector = "label[for='" + this.element.attr("id") + "']";
+			this.buttonElement = ancestor.find( labelSelector );
+			if ( !this.buttonElement.length ) {
+				ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings();
+				this.buttonElement = ancestor.filter( labelSelector );
+				if ( !this.buttonElement.length ) {
+					this.buttonElement = ancestor.find( labelSelector );
+				}
+			}
+			this.element.addClass( "ui-helper-hidden-accessible" );
+
+			checked = this.element.is( ":checked" );
+			if ( checked ) {
+				this.buttonElement.addClass( "ui-state-active" );
+			}
+			this.buttonElement.prop( "aria-pressed", checked );
+		} else {
+			this.buttonElement = this.element;
+		}
+	},
+
+	widget: function() {
+		return this.buttonElement;
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-helper-hidden-accessible" );
+		this.buttonElement
+			.removeClass( baseClasses + " " + stateClasses + " " + typeClasses )
+			.removeAttr( "role" )
+			.removeAttr( "aria-pressed" )
+			.html( this.buttonElement.find(".ui-button-text").html() );
+
+		if ( !this.hasTitle ) {
+			this.buttonElement.removeAttr( "title" );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+		if ( key === "disabled" ) {
+			if ( value ) {
+				this.element.prop( "disabled", true );
+			} else {
+				this.element.prop( "disabled", false );
+			}
+			return;
+		}
+		this._resetButton();
+	},
+
+	refresh: function() {
+		var isDisabled = this.element.is( ":disabled" );
+		if ( isDisabled !== this.options.disabled ) {
+			this._setOption( "disabled", isDisabled );
+		}
+		if ( this.type === "radio" ) {
+			radioGroup( this.element[0] ).each(function() {
+				if ( $( this ).is( ":checked" ) ) {
+					$( this ).button( "widget" )
+						.addClass( "ui-state-active" )
+						.attr( "aria-pressed", "true" );
+				} else {
+					$( this ).button( "widget" )
+						.removeClass( "ui-state-active" )
+						.attr( "aria-pressed", "false" );
+				}
+			});
+		} else if ( this.type === "checkbox" ) {
+			if ( this.element.is( ":checked" ) ) {
+				this.buttonElement
+					.addClass( "ui-state-active" )
+					.attr( "aria-pressed", "true" );
+			} else {
+				this.buttonElement
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			}
+		}
+	},
+
+	_resetButton: function() {
+		if ( this.type === "input" ) {
+			if ( this.options.label ) {
+				this.element.val( this.options.label );
+			}
+			return;
+		}
+		var buttonElement = this.buttonElement.removeClass( typeClasses ),
+			buttonText = $( "<span></span>", this.document[0] )
+				.addClass( "ui-button-text" )
+				.html( this.options.label )
+				.appendTo( buttonElement.empty() )
+				.text(),
+			icons = this.options.icons,
+			multipleIcons = icons.primary && icons.secondary,
+			buttonClasses = [];
+
+		if ( icons.primary || icons.secondary ) {
+			if ( this.options.text ) {
+				buttonClasses.push( "ui-button-text-icon" + ( multipleIcons ? "s" : ( icons.primary ? "-primary" : "-secondary" ) ) );
+			}
+
+			if ( icons.primary ) {
+				buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" );
+			}
+
+			if ( icons.secondary ) {
+				buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" );
+			}
+
+			if ( !this.options.text ) {
+				buttonClasses.push( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" );
+
+				if ( !this.hasTitle ) {
+					buttonElement.attr( "title", $.trim( buttonText ) );
+				}
+			}
+		} else {
+			buttonClasses.push( "ui-button-text-only" );
+		}
+		buttonElement.addClass( buttonClasses.join( " " ) );
+	}
+});
+
+$.widget( "ui.buttonset", {
+	version: "@VERSION",
+	options: {
+		items: "button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(button)"
+	},
+
+	_create: function() {
+		this.element.addClass( "ui-buttonset" );
+	},
+
+	_init: function() {
+		this.refresh();
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "disabled" ) {
+			this.buttons.button( "option", key, value );
+		}
+
+		this._super( key, value );
+	},
+
+	refresh: function() {
+		var rtl = this.element.css( "direction" ) === "rtl";
+
+		this.buttons = this.element.find( this.options.items )
+			.filter( ":ui-button" )
+				.button( "refresh" )
+			.end()
+			.not( ":ui-button" )
+				.button()
+			.end()
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-all ui-corner-left ui-corner-right" )
+				.filter( ":first" )
+					.addClass( rtl ? "ui-corner-right" : "ui-corner-left" )
+				.end()
+				.filter( ":last" )
+					.addClass( rtl ? "ui-corner-left" : "ui-corner-right" )
+				.end()
+			.end();
+	},
+
+	_destroy: function() {
+		this.element.removeClass( "ui-buttonset" );
+		this.buttons
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-left ui-corner-right" )
+			.end()
+			.button( "destroy" );
+	}
+});
+
+}( jQuery ) );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.css
new file mode 100644
index 0000000..8cf4d02
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.css
@@ -0,0 +1,39 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.js
new file mode 100644
index 0000000..e569eea
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.core.js
@@ -0,0 +1,343 @@
+/*!
+ * jQuery UI Core @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/ui-core/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	runiqueId = /^ui-id-\d+$/;
+
+// prevent duplicate loading
+// this is only a problem because we proxy existing functions
+// and we don't want to double proxy them
+$.ui = $.ui || {};
+if ( $.ui.version ) {
+	return;
+}
+
+$.extend( $.ui, {
+	version: "@VERSION",
+
+	keyCode: {
+		BACKSPACE: 8,
+		COMMA: 188,
+		DELETE: 46,
+		DOWN: 40,
+		END: 35,
+		ENTER: 13,
+		ESCAPE: 27,
+		HOME: 36,
+		LEFT: 37,
+		NUMPAD_ADD: 107,
+		NUMPAD_DECIMAL: 110,
+		NUMPAD_DIVIDE: 111,
+		NUMPAD_ENTER: 108,
+		NUMPAD_MULTIPLY: 106,
+		NUMPAD_SUBTRACT: 109,
+		PAGE_DOWN: 34,
+		PAGE_UP: 33,
+		PERIOD: 190,
+		RIGHT: 39,
+		SPACE: 32,
+		TAB: 9,
+		UP: 38
+	}
+});
+
+// plugins
+$.fn.extend({
+	_focus: $.fn.focus,
+	focus: function( delay, fn ) {
+		return typeof delay === "number" ?
+			this.each(function() {
+				var elem = this;
+				setTimeout(function() {
+					$( elem ).focus();
+					if ( fn ) {
+						fn.call( elem );
+					}
+				}, delay );
+			}) :
+			this._focus.apply( this, arguments );
+	},
+
+	scrollParent: function() {
+		var scrollParent;
+		if (($.ui.ie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) {
+			scrollParent = this.parents().filter(function() {
+				return (/(relative|absolute|fixed)/).test($.css(this,'position')) && (/(auto|scroll)/).test($.css(this,'overflow')+$.css(this,'overflow-y')+$.css(this,'overflow-x'));
+			}).eq(0);
+		} else {
+			scrollParent = this.parents().filter(function() {
+				return (/(auto|scroll)/).test($.css(this,'overflow')+$.css(this,'overflow-y')+$.css(this,'overflow-x'));
+			}).eq(0);
+		}
+
+		return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent;
+	},
+
+	zIndex: function( zIndex ) {
+		if ( zIndex !== undefined ) {
+			return this.css( "zIndex", zIndex );
+		}
+
+		if ( this.length ) {
+			var elem = $( this[ 0 ] ), position, value;
+			while ( elem.length && elem[ 0 ] !== document ) {
+				// Ignore z-index if position is set to a value where z-index is ignored by the browser
+				// This makes behavior of this function consistent across browsers
+				// WebKit always returns auto if the element is positioned
+				position = elem.css( "position" );
+				if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+					// IE returns 0 when zIndex is not specified
+					// other browsers return a string
+					// we ignore the case of nested elements with an explicit value of 0
+					// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+					value = parseInt( elem.css( "zIndex" ), 10 );
+					if ( !isNaN( value ) && value !== 0 ) {
+						return value;
+					}
+				}
+				elem = elem.parent();
+			}
+		}
+
+		return 0;
+	},
+
+	uniqueId: function() {
+		return this.each(function() {
+			if ( !this.id ) {
+				this.id = "ui-id-" + (++uuid);
+			}
+		});
+	},
+
+	removeUniqueId: function() {
+		return this.each(function() {
+			if ( runiqueId.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		});
+	}
+});
+
+// support: jQuery <1.8
+if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
+	$.each( [ "Width", "Height" ], function( i, name ) {
+		var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
+			type = name.toLowerCase(),
+			orig = {
+				innerWidth: $.fn.innerWidth,
+				innerHeight: $.fn.innerHeight,
+				outerWidth: $.fn.outerWidth,
+				outerHeight: $.fn.outerHeight
+			};
+
+		function reduce( elem, size, border, margin ) {
+			$.each( side, function() {
+				size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
+				if ( border ) {
+					size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
+				}
+				if ( margin ) {
+					size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
+				}
+			});
+			return size;
+		}
+
+		$.fn[ "inner" + name ] = function( size ) {
+			if ( size === undefined ) {
+				return orig[ "inner" + name ].call( this );
+			}
+
+			return this.each(function() {
+				$( this ).css( type, reduce( this, size ) + "px" );
+			});
+		};
+
+		$.fn[ "outer" + name] = function( size, margin ) {
+			if ( typeof size !== "number" ) {
+				return orig[ "outer" + name ].call( this, size );
+			}
+
+			return this.each(function() {
+				$( this).css( type, reduce( this, size, true, margin ) + "px" );
+			});
+		};
+	});
+}
+
+// selectors
+function focusable( element, isTabIndexNotNaN ) {
+	var map, mapName, img,
+		nodeName = element.nodeName.toLowerCase();
+	if ( "area" === nodeName ) {
+		map = element.parentNode;
+		mapName = map.name;
+		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
+			return false;
+		}
+		img = $( "img[usemap=#" + mapName + "]" )[0];
+		return !!img && visible( img );
+	}
+	return ( /input|select|textarea|button|object/.test( nodeName ) ?
+		!element.disabled :
+		"a" === nodeName ?
+			element.href || isTabIndexNotNaN :
+			isTabIndexNotNaN) &&
+		// the element and all of its ancestors must be visible
+		visible( element );
+}
+
+function visible( element ) {
+	return !$( element ).parents().andSelf().filter(function() {
+		return $.css( this, "visibility" ) === "hidden" ||
+			$.expr.filters.hidden( this );
+	}).length;
+}
+
+$.extend( $.expr[ ":" ], {
+	data: $.expr.createPseudo ?
+		$.expr.createPseudo(function( dataName ) {
+			return function( elem ) {
+				return !!$.data( elem, dataName );
+			};
+		}) :
+		// support: jQuery <1.8
+		function( elem, i, match ) {
+			return !!$.data( elem, match[ 3 ] );
+		},
+
+	focusable: function( element ) {
+		return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
+	},
+
+	tabbable: function( element ) {
+		var tabIndex = $.attr( element, "tabindex" ),
+			isTabIndexNaN = isNaN( tabIndex );
+		return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
+	}
+});
+
+// support
+$(function() {
+	var body = document.body,
+		div = body.appendChild( div = document.createElement( "div" ) );
+
+	// access offsetHeight before setting the style to prevent a layout bug
+	// in IE 9 which causes the element to continue to take up space even
+	// after it is removed from the DOM (#8026)
+	div.offsetHeight;
+
+	$.extend( div.style, {
+		minHeight: "100px",
+		height: "auto",
+		padding: 0,
+		borderWidth: 0
+	});
+
+	$.support.minHeight = div.offsetHeight === 100;
+	$.support.selectstart = "onselectstart" in div;
+
+	// set display to none to avoid a layout bug in IE
+	// http://dev.jquery.com/ticket/4014
+	body.removeChild( div ).style.display = "none";
+});
+
+
+
+
+
+// deprecated
+
+(function() {
+	var uaMatch = /msie ([\w.]+)/.exec( navigator.userAgent.toLowerCase() ) || [];
+	$.ui.ie = uaMatch.length ? true : false;
+	$.ui.ie6 = parseFloat( uaMatch[ 1 ], 10 ) === 6;
+})();
+
+$.fn.extend({
+	disableSelection: function() {
+		return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
+			".ui-disableSelection", function( event ) {
+				event.preventDefault();
+			});
+	},
+
+	enableSelection: function() {
+		return this.unbind( ".ui-disableSelection" );
+	}
+});
+
+$.extend( $.ui, {
+	// $.ui.plugin is deprecated.  Use the proxy pattern instead.
+	plugin: {
+		add: function( module, option, set ) {
+			var i,
+				proto = $.ui[ module ].prototype;
+			for ( i in set ) {
+				proto.plugins[ i ] = proto.plugins[ i ] || [];
+				proto.plugins[ i ].push( [ option, set[ i ] ] );
+			}
+		},
+		call: function( instance, name, args ) {
+			var i,
+				set = instance.plugins[ name ];
+			if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {
+				return;
+			}
+
+			for ( i = 0; i < set.length; i++ ) {
+				if ( instance.options[ set[ i ][ 0 ] ] ) {
+					set[ i ][ 1 ].apply( instance.element, args );
+				}
+			}
+		}
+	},
+
+	contains: $.contains,
+
+	// only used by resizable
+	hasScroll: function( el, a ) {
+
+		//If overflow is hidden, the element might have extra content, but the user wants to hide it
+		if ( $( el ).css( "overflow" ) === "hidden") {
+			return false;
+		}
+
+		var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
+			has = false;
+
+		if ( el[ scroll ] > 0 ) {
+			return true;
+		}
+
+		// TODO: determine which cases actually cause this to happen
+		// if the element doesn't have the scroll set, see if it's possible to
+		// set the scroll
+		el[ scroll ] = 1;
+		has = ( el[ scroll ] > 0 );
+		el[ scroll ] = 0;
+		return has;
+	},
+
+	// these are odd functions, fix the API or move into individual plugins
+	isOverAxis: function( x, reference, size ) {
+		//Determines when x coordinate is over "b" element axis
+		return ( x > reference ) && ( x < ( reference + size ) );
+	},
+	isOver: function( y, x, top, left, height, width ) {
+		//Determines when x, y coordinates is over "b" element
+		return $.ui.isOverAxis( y, top, height ) && $.ui.isOverAxis( x, left, width );
+	}
+});
+
+})( jQuery );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.datepicker.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.datepicker.css
new file mode 100644
index 0000000..c1f2699
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.datepicker.css
@@ -0,0 +1,67 @@
+/*!
+ * jQuery UI Datepicker @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.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;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.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; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.css
new file mode 100644
index 0000000..2937af9
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.css
@@ -0,0 +1,22 @@
+/*!
+ * jQuery UI Dialog @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.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; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.js
new file mode 100644
index 0000000..8593fff
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.dialog.js
@@ -0,0 +1,847 @@
+/*!
+ * jQuery UI Dialog @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/dialog/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *  jquery.ui.button.js
+ *	jquery.ui.draggable.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.position.js
+ *	jquery.ui.resizable.js
+ */
+(function( $, undefined ) {
+
+var uiDialogClasses = "ui-dialog ui-widget ui-widget-content ui-corner-all ",
+	sizeRelatedOptions = {
+		buttons: true,
+		height: true,
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true,
+		width: true
+	},
+	resizableRelatedOptions = {
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true
+	};
+
+$.widget("ui.dialog", {
+	version: "@VERSION",
+	options: {
+		autoOpen: true,
+		buttons: {},
+		closeOnEscape: true,
+		closeText: "close",
+		dialogClass: "",
+		draggable: true,
+		hide: null,
+		height: "auto",
+		maxHeight: false,
+		maxWidth: false,
+		minHeight: 150,
+		minWidth: 150,
+		modal: false,
+		position: {
+			my: "center",
+			at: "center",
+			of: window,
+			collision: "fit",
+			// ensure that the titlebar is never outside the document
+			using: function( pos ) {
+				var topOffset = $( this ).css( pos ).offset().top;
+				if ( topOffset < 0 ) {
+					$( this ).css( "top", pos.top - topOffset );
+				}
+			}
+		},
+		resizable: true,
+		show: null,
+		stack: true,
+		title: "",
+		width: 300,
+		zIndex: 1000
+	},
+
+	_create: function() {
+		this.originalTitle = this.element.attr( "title" );
+		// #5742 - .attr() might return a DOMElement
+		if ( typeof this.originalTitle !== "string" ) {
+			this.originalTitle = "";
+		}
+		this.oldPosition = {
+			parent: this.element.parent(),
+			index: this.element.parent().children().index( this.element )
+		};
+		this.options.title = this.options.title || this.originalTitle;
+		var that = this,
+			options = this.options,
+
+			title = options.title || " ",
+
+			uiDialog = ( this.uiDialog = $( "<div>" ) )
+				.addClass( uiDialogClasses + options.dialogClass )
+				.css({
+					display: "none",
+					outline: 0, // TODO: move to stylesheet
+					zIndex: options.zIndex
+				})
+				// setting tabIndex makes the div focusable
+				.attr( "tabIndex", -1)
+				.keydown(function( event ) {
+					if ( options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+							event.keyCode === $.ui.keyCode.ESCAPE ) {
+						that.close( event );
+						event.preventDefault();
+					}
+				})
+				.mousedown(function( event ) {
+					that.moveToTop( false, event );
+				})
+				.appendTo( "body" ),
+
+			uiDialogContent = this.element
+				.show()
+				.removeAttr( "title" )
+				.addClass( "ui-dialog-content ui-widget-content" )
+				.appendTo( uiDialog ),
+
+			uiDialogTitlebar = ( this.uiDialogTitlebar = $( "<div>" ) )
+				.addClass( "ui-dialog-titlebar  ui-widget-header  " +
+					"ui-corner-all  ui-helper-clearfix" )
+				.prependTo( uiDialog ),
+
+			uiDialogTitlebarClose = $( "<a href='#'></a>" )
+				.addClass( "ui-dialog-titlebar-close  ui-corner-all" )
+				.attr( "role", "button" )
+				.click(function( event ) {
+					event.preventDefault();
+					that.close( event );
+				})
+				.appendTo( uiDialogTitlebar ),
+
+			uiDialogTitlebarCloseText = ( this.uiDialogTitlebarCloseText = $( "<span>" ) )
+				.addClass( "ui-icon ui-icon-closethick" )
+				.text( options.closeText )
+				.appendTo( uiDialogTitlebarClose ),
+
+			uiDialogTitle = $( "<span>" )
+				.uniqueId()
+				.addClass( "ui-dialog-title" )
+				.html( title )
+				.prependTo( uiDialogTitlebar ),
+
+			uiDialogButtonPane = ( this.uiDialogButtonPane = $( "<div>" ) )
+				.addClass( "ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" ),
+
+			uiButtonSet = ( this.uiButtonSet = $( "<div>" ) )
+				.addClass( "ui-dialog-buttonset" )
+				.appendTo( uiDialogButtonPane );
+
+		uiDialog.attr({
+			role: "dialog",
+			"aria-labelledby": uiDialogTitle.attr( "id" )
+		});
+
+		uiDialogTitlebar.find( "*" ).add( uiDialogTitlebar ).disableSelection();
+		this._hoverable( uiDialogTitlebarClose );
+		this._focusable( uiDialogTitlebarClose );
+
+		if ( options.draggable && $.fn.draggable ) {
+			this._makeDraggable();
+		}
+		if ( options.resizable && $.fn.resizable ) {
+			this._makeResizable();
+		}
+
+		this._createButtons( options.buttons );
+		this._isOpen = false;
+
+		if ( $.fn.bgiframe ) {
+			uiDialog.bgiframe();
+		}
+
+		// prevent tabbing out of modal dialogs
+		this._on( uiDialog, { keydown: function( event ) {
+			if ( !options.modal || event.keyCode !== $.ui.keyCode.TAB ) {
+				return;
+			}
+
+			var tabbables = $( ":tabbable", uiDialog ),
+				first = tabbables.filter( ":first" ),
+				last  = tabbables.filter( ":last" );
+
+			if ( event.target === last[0] && !event.shiftKey ) {
+				first.focus( 1 );
+				return false;
+			} else if ( event.target === first[0] && event.shiftKey ) {
+				last.focus( 1 );
+				return false;
+			}
+		}});
+	},
+
+	_init: function() {
+		if ( this.options.autoOpen ) {
+			this.open();
+		}
+	},
+
+	_destroy: function() {
+		var next,
+			oldPosition = this.oldPosition;
+
+		if ( this.overlay ) {
+			this.overlay.destroy();
+		}
+		this.uiDialog.hide();
+		this.element
+			.removeClass( "ui-dialog-content ui-widget-content" )
+			.hide()
+			.appendTo( "body" );
+		this.uiDialog.remove();
+
+		if ( this.originalTitle ) {
+			this.element.attr( "title", this.originalTitle );
+		}
+
+		next = oldPosition.parent.children().eq( oldPosition.index );
+		// Don't try to place the dialog next to itself (#8613)
+		if ( next.length && next[ 0 ] !== this.element[ 0 ] ) {
+			next.before( this.element );
+		} else {
+			oldPosition.parent.append( this.element );
+		}
+	},
+
+	widget: function() {
+		return this.uiDialog;
+	},
+
+	close: function( event ) {
+		var that = this,
+			maxZ, thisZ;
+
+		if ( !this._isOpen ) {
+			return;
+		}
+
+		if ( false === this._trigger( "beforeClose", event ) ) {
+			return;
+		}
+
+		this._isOpen = false;
+
+		if ( this.overlay ) {
+			this.overlay.destroy();
+		}
+
+		if ( this.options.hide ) {
+			this.uiDialog.hide( this.options.hide, function() {
+				that._trigger( "close", event );
+			});
+		} else {
+			this.uiDialog.hide();
+			this._trigger( "close", event );
+		}
+
+		$.ui.dialog.overlay.resize();
+
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		if ( this.options.modal ) {
+			maxZ = 0;
+			$( ".ui-dialog" ).each(function() {
+				if ( this !== that.uiDialog[0] ) {
+					thisZ = $( this ).css( "z-index" );
+					if ( !isNaN( thisZ ) ) {
+						maxZ = Math.max( maxZ, thisZ );
+					}
+				}
+			});
+			$.ui.dialog.maxZ = maxZ;
+		}
+
+		return this;
+	},
+
+	isOpen: function() {
+		return this._isOpen;
+	},
+
+	// the force parameter allows us to move modal dialogs to their correct
+	// position on open
+	moveToTop: function( force, event ) {
+		var options = this.options,
+			saveScroll;
+
+		if ( ( options.modal && !force ) ||
+				( !options.stack && !options.modal ) ) {
+			return this._trigger( "focus", event );
+		}
+
+		if ( options.zIndex > $.ui.dialog.maxZ ) {
+			$.ui.dialog.maxZ = options.zIndex;
+		}
+		if ( this.overlay ) {
+			$.ui.dialog.maxZ += 1;
+			$.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ;
+			this.overlay.$el.css( "z-index", $.ui.dialog.overlay.maxZ );
+		}
+
+		// Save and then restore scroll
+		// Opera 9.5+ resets when parent z-index is changed.
+		// http://bugs.jqueryui.com/ticket/3193
+		saveScroll = {
+			scrollTop: this.element.scrollTop(),
+			scrollLeft: this.element.scrollLeft()
+		};
+		$.ui.dialog.maxZ += 1;
+		this.uiDialog.css( "z-index", $.ui.dialog.maxZ );
+		this.element.attr( saveScroll );
+		this._trigger( "focus", event );
+
+		return this;
+	},
+
+	open: function() {
+		if ( this._isOpen ) {
+			return;
+		}
+
+		var hasFocus,
+			options = this.options,
+			uiDialog = this.uiDialog;
+
+		this._size();
+		this._position( options.position );
+		uiDialog.show( options.show );
+		this.overlay = options.modal ? new $.ui.dialog.overlay( this ) : null;
+		this.moveToTop( true );
+
+		// set focus to the first tabbable element in the content area or the first button
+		// if there are no tabbable elements, set focus on the dialog itself
+		hasFocus = this.element.find( ":tabbable" );
+		if ( !hasFocus.length ) {
+			hasFocus = this.uiDialogButtonPane.find( ":tabbable" );
+			if ( !hasFocus.length ) {
+				hasFocus = uiDialog;
+			}
+		}
+		hasFocus.eq( 0 ).focus();
+
+		this._isOpen = true;
+		this._trigger( "open" );
+
+		return this;
+	},
+
+	_createButtons: function( buttons ) {
+		var uiDialogButtonPane, uiButtonSet,
+			that = this,
+			hasButtons = false;
+
+		// if we already have a button pane, remove it
+		this.uiDialogButtonPane.remove();
+		this.uiButtonSet.empty();
+
+		if ( typeof buttons === "object" && buttons !== null ) {
+			$.each( buttons, function() {
+				return !(hasButtons = true);
+			});
+		}
+		if ( hasButtons ) {
+			$.each( buttons, function( name, props ) {
+				props = $.isFunction( props ) ?
+					{ click: props, text: name } :
+					props;
+				var button = $( "<button type='button'></button>" )
+					.attr( props, true )
+					.unbind( "click" )
+					.click(function() {
+						props.click.apply( that.element[0], arguments );
+					})
+					.appendTo( that.uiButtonSet );
+				if ( $.fn.button ) {
+					button.button();
+				}
+			});
+			this.uiDialog.addClass( "ui-dialog-buttons" );
+			this.uiDialogButtonPane.appendTo( this.uiDialog );
+		} else {
+			this.uiDialog.removeClass( "ui-dialog-buttons" );
+		}
+	},
+
+	_makeDraggable: function() {
+		var that = this,
+			options = this.options;
+
+		function filteredUi( ui ) {
+			return {
+				position: ui.position,
+				offset: ui.offset
+			};
+		}
+
+		this.uiDialog.draggable({
+			cancel: ".ui-dialog-content, .ui-dialog-titlebar-close",
+			handle: ".ui-dialog-titlebar",
+			containment: "document",
+			start: function( event, ui ) {
+				$( this )
+					.addClass( "ui-dialog-dragging" );
+				that._trigger( "dragStart", event, filteredUi( ui ) );
+			},
+			drag: function( event, ui ) {
+				that._trigger( "drag", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				options.position = [
+					ui.position.left - that.document.scrollLeft(),
+					ui.position.top - that.document.scrollTop()
+				];
+				$( this )
+					.removeClass( "ui-dialog-dragging" );
+				that._trigger( "dragStop", event, filteredUi( ui ) );
+				$.ui.dialog.overlay.resize();
+			}
+		});
+	},
+
+	_makeResizable: function( handles ) {
+		handles = (handles === undefined ? this.options.resizable : handles);
+		var that = this,
+			options = this.options,
+			// .ui-resizable has position: relative defined in the stylesheet
+			// but dialogs have to use absolute or fixed positioning
+			position = this.uiDialog.css( "position" ),
+			resizeHandles = typeof handles === 'string' ?
+				handles	:
+				"n,e,s,w,se,sw,ne,nw";
+
+		function filteredUi( ui ) {
+			return {
+				originalPosition: ui.originalPosition,
+				originalSize: ui.originalSize,
+				position: ui.position,
+				size: ui.size
+			};
+		}
+
+		this.uiDialog.resizable({
+			cancel: ".ui-dialog-content",
+			containment: "document",
+			alsoResize: this.element,
+			maxWidth: options.maxWidth,
+			maxHeight: options.maxHeight,
+			minWidth: options.minWidth,
+			minHeight: this._minHeight(),
+			handles: resizeHandles,
+			start: function( event, ui ) {
+				$( this ).addClass( "ui-dialog-resizing" );
+				that._trigger( "resizeStart", event, filteredUi( ui ) );
+			},
+			resize: function( event, ui ) {
+				that._trigger( "resize", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				$( this ).removeClass( "ui-dialog-resizing" );
+				options.height = $( this ).height();
+				options.width = $( this ).width();
+				that._trigger( "resizeStop", event, filteredUi( ui ) );
+				$.ui.dialog.overlay.resize();
+			}
+		})
+		.css( "position", position )
+		.find( ".ui-resizable-se" )
+			.addClass( "ui-icon ui-icon-grip-diagonal-se" );
+	},
+
+	_minHeight: function() {
+		var options = this.options;
+
+		if ( options.height === "auto" ) {
+			return options.minHeight;
+		} else {
+			return Math.min( options.minHeight, options.height );
+		}
+	},
+
+	_position: function( position ) {
+		var myAt = [],
+			offset = [ 0, 0 ],
+			isVisible;
+
+		if ( position ) {
+			// deep extending converts arrays to objects in jQuery <= 1.3.2 :-(
+	//		if (typeof position == 'string' || $.isArray(position)) {
+	//			myAt = $.isArray(position) ? position : position.split(' ');
+
+			if ( typeof position === "string" || (typeof position === "object" && "0" in position ) ) {
+				myAt = position.split ? position.split( " " ) : [ position[ 0 ], position[ 1 ] ];
+				if ( myAt.length === 1 ) {
+					myAt[ 1 ] = myAt[ 0 ];
+				}
+
+				$.each( [ "left", "top" ], function( i, offsetPosition ) {
+					if ( +myAt[ i ] === myAt[ i ] ) {
+						offset[ i ] = myAt[ i ];
+						myAt[ i ] = offsetPosition;
+					}
+				});
+
+				position = {
+					my: myAt.join( " " ),
+					at: myAt.join( " " ),
+					offset: offset.join( " " )
+				};
+			}
+
+			position = $.extend( {}, $.ui.dialog.prototype.options.position, position );
+		} else {
+			position = $.ui.dialog.prototype.options.position;
+		}
+
+		// need to show the dialog to get the actual offset in the position plugin
+		isVisible = this.uiDialog.is( ":visible" );
+		if ( !isVisible ) {
+			this.uiDialog.show();
+		}
+		this.uiDialog.position( position );
+		if ( !isVisible ) {
+			this.uiDialog.hide();
+		}
+	},
+
+	_setOptions: function( options ) {
+		var that = this,
+			resizableOptions = {},
+			resize = false;
+
+		$.each( options, function( key, value ) {
+			that._setOption( key, value );
+
+			if ( key in sizeRelatedOptions ) {
+				resize = true;
+			}
+			if ( key in resizableRelatedOptions ) {
+				resizableOptions[ key ] = value;
+			}
+		});
+
+		if ( resize ) {
+			this._size();
+		}
+		if ( this.uiDialog.is( ":data(resizable)" ) ) {
+			this.uiDialog.resizable( "option", resizableOptions );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		var isDraggable, isResizable,
+			uiDialog = this.uiDialog;
+
+		switch ( key ) {
+			case "buttons":
+				this._createButtons( value );
+				break;
+			case "closeText":
+				// ensure that we always pass a string
+				this.uiDialogTitlebarCloseText.text( "" + value );
+				break;
+			case "dialogClass":
+				uiDialog
+					.removeClass( this.options.dialogClass )
+					.addClass( uiDialogClasses + value );
+				break;
+			case "disabled":
+				if ( value ) {
+					uiDialog.addClass( "ui-dialog-disabled" );
+				} else {
+					uiDialog.removeClass( "ui-dialog-disabled" );
+				}
+				break;
+			case "draggable":
+				isDraggable = uiDialog.is( ":data(draggable)" );
+				if ( isDraggable && !value ) {
+					uiDialog.draggable( "destroy" );
+				}
+
+				if ( !isDraggable && value ) {
+					this._makeDraggable();
+				}
+				break;
+			case "position":
+				this._position( value );
+				break;
+			case "resizable":
+				// currently resizable, becoming non-resizable
+				isResizable = uiDialog.is( ":data(resizable)" );
+				if ( isResizable && !value ) {
+					uiDialog.resizable( "destroy" );
+				}
+
+				// currently resizable, changing handles
+				if ( isResizable && typeof value === "string" ) {
+					uiDialog.resizable( "option", "handles", value );
+				}
+
+				// currently non-resizable, becoming resizable
+				if ( !isResizable && value !== false ) {
+					this._makeResizable( value );
+				}
+				break;
+			case "title":
+				// convert whatever was passed in o a string, for html() to not throw up
+				$( ".ui-dialog-title", this.uiDialogTitlebar )
+					.html( "" + ( value || " " ) );
+				break;
+		}
+
+		this._super( key, value );
+	},
+
+	_size: function() {
+		/* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content
+		 * divs will both have width and height set, so we need to reset them
+		 */
+		var nonContentHeight, minContentHeight, autoHeight,
+			options = this.options,
+			isVisible = this.uiDialog.is( ":visible" );
+
+		// reset content sizing
+		this.element.show().css({
+			width: "auto",
+			minHeight: 0,
+			height: 0
+		});
+
+		if ( options.minWidth > options.width ) {
+			options.width = options.minWidth;
+		}
+
+		// reset wrapper sizing
+		// determine the height of all the non-content elements
+		nonContentHeight = this.uiDialog.css({
+				height: "auto",
+				width: options.width
+			})
+			.outerHeight();
+		minContentHeight = Math.max( 0, options.minHeight - nonContentHeight );
+
+		if ( options.height === "auto" ) {
+			// only needed for IE6 support
+			if ( $.support.minHeight ) {
+				this.element.css({
+					minHeight: minContentHeight,
+					height: "auto"
+				});
+			} else {
+				this.uiDialog.show();
+				autoHeight = this.element.css( "height", "auto" ).height();
+				if ( !isVisible ) {
+					this.uiDialog.hide();
+				}
+				this.element.height( Math.max( autoHeight, minContentHeight ) );
+			}
+		} else {
+			this.element.height( Math.max( options.height - nonContentHeight, 0 ) );
+		}
+
+		if (this.uiDialog.is( ":data(resizable)" ) ) {
+			this.uiDialog.resizable( "option", "minHeight", this._minHeight() );
+		}
+	}
+});
+
+$.extend($.ui.dialog, {
+	uuid: 0,
+	maxZ: 0,
+
+	getTitleId: function($el) {
+		var id = $el.attr( "id" );
+		if ( !id ) {
+			this.uuid += 1;
+			id = this.uuid;
+		}
+		return "ui-dialog-title-" + id;
+	},
+
+	overlay: function( dialog ) {
+		this.$el = $.ui.dialog.overlay.create( dialog );
+	}
+});
+
+$.extend( $.ui.dialog.overlay, {
+	instances: [],
+	// reuse old instances due to IE memory leak with alpha transparency (see #5185)
+	oldInstances: [],
+	maxZ: 0,
+	events: $.map(
+		"focus,mousedown,mouseup,keydown,keypress,click".split( "," ),
+		function( event ) {
+			return event + ".dialog-overlay";
+		}
+	).join( " " ),
+	create: function( dialog ) {
+		if ( this.instances.length === 0 ) {
+			// prevent use of anchors and inputs
+			// we use a setTimeout in case the overlay is created from an
+			// event that we're going to be cancelling (see #2804)
+			setTimeout(function() {
+				// handle $(el).dialog().dialog('close') (see #4065)
+				if ( $.ui.dialog.overlay.instances.length ) {
+					$( document ).bind( $.ui.dialog.overlay.events, function( event ) {
+						// stop events if the z-index of the target is < the z-index of the overlay
+						// we cannot return true when we don't want to cancel the event (#3523)
+						if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) {
+							return false;
+						}
+					});
+				}
+			}, 1 );
+
+			// handle window resize
+			$( window ).bind( "resize.dialog-overlay", $.ui.dialog.overlay.resize );
+		}
+
+		var $el = ( this.oldInstances.pop() || $( "<div>" ).addClass( "ui-widget-overlay" ) );
+
+		// allow closing by pressing the escape key
+		$( document ).bind( "keydown.dialog-overlay", function( event ) {
+			var instances = $.ui.dialog.overlay.instances;
+			// only react to the event if we're the top overlay
+			if ( instances.length !== 0 && instances[ instances.length - 1 ] === $el &&
+				dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+				event.keyCode === $.ui.keyCode.ESCAPE ) {
+
+				dialog.close( event );
+				event.preventDefault();
+			}
+		});
+
+		$el.appendTo( document.body ).css({
+			width: this.width(),
+			height: this.height()
+		});
+
+		if ( $.fn.bgiframe ) {
+			$el.bgiframe();
+		}
+
+		this.instances.push( $el );
+		return $el;
+	},
+
+	destroy: function( $el ) {
+		var indexOf = $.inArray( $el, this.instances ),
+			maxZ = 0;
+
+		if ( indexOf !== -1 ) {
+			this.oldInstances.push( this.instances.splice( indexOf, 1 )[ 0 ] );
+		}
+
+		if ( this.instances.length === 0 ) {
+			$( [ document, window ] ).unbind( ".dialog-overlay" );
+		}
+
+		$el.height( 0 ).width( 0 ).remove();
+
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		$.each( this.instances, function() {
+			maxZ = Math.max( maxZ, this.css( "z-index" ) );
+		});
+		this.maxZ = maxZ;
+	},
+
+	height: function() {
+		var scrollHeight,
+			offsetHeight;
+		// handle IE
+		if ( $.ui.ie ) {
+			scrollHeight = Math.max(
+				document.documentElement.scrollHeight,
+				document.body.scrollHeight
+			);
+			offsetHeight = Math.max(
+				document.documentElement.offsetHeight,
+				document.body.offsetHeight
+			);
+
+			if ( scrollHeight < offsetHeight ) {
+				return $( window ).height() + "px";
+			} else {
+				return scrollHeight + "px";
+			}
+		// handle "good" browsers
+		} else {
+			return $( document ).height() + "px";
+		}
+	},
+
+	width: function() {
+		var scrollWidth,
+			offsetWidth;
+		// handle IE
+		if ( $.ui.ie ) {
+			scrollWidth = Math.max(
+				document.documentElement.scrollWidth,
+				document.body.scrollWidth
+			);
+			offsetWidth = Math.max(
+				document.documentElement.offsetWidth,
+				document.body.offsetWidth
+			);
+
+			if ( scrollWidth < offsetWidth ) {
+				return $( window ).width() + "px";
+			} else {
+				return scrollWidth + "px";
+			}
+		// handle "good" browsers
+		} else {
+			return $( document ).width() + "px";
+		}
+	},
+
+	resize: function() {
+		/* If the dialog is draggable and the user drags it past the
+		 * right edge of the window, the document becomes wider so we
+		 * need to stretch the overlay. If the user then drags the
+		 * dialog back to the left, the document will become narrower,
+		 * so we need to shrink the overlay to the appropriate size.
+		 * This is handled by shrinking the overlay before setting it
+		 * to the full document size.
+		 */
+		var $overlays = $( [] );
+		$.each( $.ui.dialog.overlay.instances, function() {
+			$overlays = $overlays.add( this );
+		});
+
+		$overlays.css({
+			width: 0,
+			height: 0
+		}).css({
+			width: $.ui.dialog.overlay.width(),
+			height: $.ui.dialog.overlay.height()
+		});
+	}
+});
+
+$.extend( $.ui.dialog.overlay.prototype, {
+	destroy: function() {
+		$.ui.dialog.overlay.destroy( this.$el );
+	}
+});
+
+}( jQuery ) );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.css
new file mode 100644
index 0000000..ea4b24e
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.css
@@ -0,0 +1,30 @@
+/*!
+ * jQuery UI Menu @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; }
+.ui-menu .ui-menu { margin-top: -3px; position: absolute; }
+.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; }
+.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; }
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; }
+
+.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; }
+.ui-menu .ui-state-disabled a { cursor: default; }
+
+/* icon support */
+.ui-menu-icons { position: relative; }
+.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; }
+
+/* left-aligned */
+.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; }
+
+/* right-aligned */
+.ui-menu .ui-menu-icon { position: static; float: right; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.js
new file mode 100644
index 0000000..59cb6d9
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menu.js
@@ -0,0 +1,628 @@
+/*!
+ * jQuery UI Menu @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/menu/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ */
+(function( $, undefined ) {
+
+var mouseHandled = false;
+
+$.widget( "ui.menu", {
+	version: "@VERSION",
+	defaultElement: "<ul>",
+	delay: 300,
+	options: {
+		icons: {
+			submenu: "ui-icon-carat-1-e"
+		},
+		menus: "ul",
+		position: {
+			my: "left top",
+			at: "right top"
+		},
+		role: "menu",
+
+		// callbacks
+		blur: null,
+		focus: null,
+		select: null
+	},
+
+	_create: function() {
+		this.activeMenu = this.element;
+		this.element
+			.uniqueId()
+			.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
+			.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
+			.attr({
+				role: this.options.role,
+				tabIndex: 0
+			})
+			// need to catch all clicks on disabled menu
+			// not possible through _on
+			.bind( "click" + this.eventNamespace, $.proxy(function( event ) {
+				if ( this.options.disabled ) {
+					event.preventDefault();
+				}
+			}, this ));
+
+		if ( this.options.disabled ) {
+			this.element
+				.addClass( "ui-state-disabled" )
+				.attr( "aria-disabled", "true" );
+		}
+
+		this._on({
+			// Prevent focus from sticking to links inside menu after clicking
+			// them (focus should always stay on UL during navigation).
+			"mousedown .ui-menu-item > a": function( event ) {
+				event.preventDefault();
+			},
+			"click .ui-state-disabled > a": function( event ) {
+				event.preventDefault();
+			},
+			"click .ui-menu-item:has(a)": function( event ) {
+				var target = $( event.target ).closest( ".ui-menu-item" );
+				if ( !mouseHandled && target.not( ".ui-state-disabled" ).length ) {
+					mouseHandled = true;
+
+					this.select( event );
+					// Open submenu on click
+					if ( target.has( ".ui-menu" ).length ) {
+						this.expand( event );
+					} else if ( !this.element.is( ":focus" ) ) {
+						// Redirect focus to the menu
+						this.element.trigger( "focus", [ true ] );
+
+						// If the active item is on the top level, let it stay active.
+						// Otherwise, blur the active item since it is no longer visible.
+						if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
+							clearTimeout( this.timer );
+						}
+					}
+				}
+			},
+			"mouseenter .ui-menu-item": function( event ) {
+				var target = $( event.currentTarget );
+				// Remove ui-state-active class from siblings of the newly focused menu item
+				// to avoid a jump caused by adjacent elements both having a class with a border
+				target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" );
+				this.focus( event, target );
+			},
+			mouseleave: "collapseAll",
+			"mouseleave .ui-menu": "collapseAll",
+			focus: function( event, keepActiveItem ) {
+				// If there's already an active item, keep it active
+				// If not, activate the first item
+				var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 );
+
+				if ( !keepActiveItem ) {
+					this.focus( event, item );
+				}
+			},
+			blur: function( event ) {
+				this._delay(function() {
+					if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
+						this.collapseAll( event );
+					}
+				});
+			},
+			keydown: "_keydown"
+		});
+
+		this.refresh();
+
+		// Clicks outside of a menu collapse any open menus
+		this._on( this.document, {
+			click: function( event ) {
+				if ( !$( event.target ).closest( ".ui-menu" ).length ) {
+					this.collapseAll( event );
+				}
+
+				// Reset the mouseHandled flag
+				mouseHandled = false;
+			}
+		});
+
+		if ( this.options.trigger ) {
+			this.element.popup({
+				trigger: this.options.trigger,
+				managed: true,
+				focusPopup: $.proxy( function( event, ui ) {
+					this.focus( event, this.element.children( ".ui-menu-item" ).first() );
+					this.element.focus( 1 );
+				}, this)
+			});
+		}
+	},
+
+	_destroy: function() {
+		if ( this.options.trigger ) {
+			this.element.popup( "destroy" );
+		}
+
+		// Destroy (sub)menus
+		this.element
+			.removeAttr( "aria-activedescendant" )
+			.find( ".ui-menu" ).andSelf()
+				.removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" )
+				.removeAttr( "role" )
+				.removeAttr( "tabIndex" )
+				.removeAttr( "aria-labelledby" )
+				.removeAttr( "aria-expanded" )
+				.removeAttr( "aria-hidden" )
+				.removeAttr( "aria-disabled" )
+				.removeUniqueId()
+				.show();
+
+		// Destroy menu items
+		this.element.find( ".ui-menu-item" )
+			.removeClass( "ui-menu-item" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-disabled" )
+			.children( "a" )
+				.removeUniqueId()
+				.removeClass( "ui-corner-all ui-state-hover" )
+				.removeAttr( "tabIndex" )
+				.removeAttr( "role" )
+				.removeAttr( "aria-haspopup" )
+				.children().each( function() {
+					var elem = $( this );
+					if ( elem.data( "ui-menu-submenu-carat" ) ) {
+						elem.remove();
+					}
+				});
+
+		// Destroy menu dividers
+		this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
+	},
+
+	_keydown: function( event ) {
+		var match, prev, character, skip, regex,
+			preventDefault = true;
+
+		function escape( value ) {
+			return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
+		}
+
+		switch ( event.keyCode ) {
+		case $.ui.keyCode.PAGE_UP:
+			this.previousPage( event );
+			break;
+		case $.ui.keyCode.PAGE_DOWN:
+			this.nextPage( event );
+			break;
+		case $.ui.keyCode.HOME:
+			this._move( "first", "first", event );
+			break;
+		case $.ui.keyCode.END:
+			this._move( "last", "last", event );
+			break;
+		case $.ui.keyCode.UP:
+			this.previous( event );
+			break;
+		case $.ui.keyCode.DOWN:
+			this.next( event );
+			break;
+		case $.ui.keyCode.LEFT:
+			this.collapse( event );
+			break;
+		case $.ui.keyCode.RIGHT:
+			if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
+				this.expand( event );
+			}
+			break;
+		case $.ui.keyCode.ENTER:
+		case $.ui.keyCode.SPACE:
+			this._activate( event );
+			break;
+		case $.ui.keyCode.ESCAPE:
+			this.collapse( event );
+			break;
+		default:
+			preventDefault = false;
+			prev = this.previousFilter || "";
+			character = String.fromCharCode( event.keyCode );
+			skip = false;
+
+			clearTimeout( this.filterTimer );
+
+			if ( character === prev ) {
+				skip = true;
+			} else {
+				character = prev + character;
+			}
+
+			regex = new RegExp( "^" + escape( character ), "i" );
+			match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
+				return regex.test( $( this ).children( "a" ).text() );
+			});
+			match = skip && match.index( this.active.next() ) !== -1 ?
+				this.active.nextAll( ".ui-menu-item" ) :
+				match;
+
+			// If no matches on the current filter, reset to the last character pressed
+			// to move down the menu to the first item that starts with that character
+			if ( !match.length ) {
+				character = String.fromCharCode( event.keyCode );
+				regex = new RegExp( "^" + escape( character ), "i" );
+				match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
+					return regex.test( $( this ).children( "a" ).text() );
+				});
+			}
+
+			if ( match.length ) {
+				this.focus( event, match );
+				if ( match.length > 1 ) {
+					this.previousFilter = character;
+					this.filterTimer = this._delay(function() {
+						delete this.previousFilter;
+					}, 1000 );
+				} else {
+					delete this.previousFilter;
+				}
+			} else {
+				delete this.previousFilter;
+			}
+		}
+
+		if ( preventDefault ) {
+			event.preventDefault();
+		}
+	},
+
+	_activate: function( event ) {
+		if ( !this.active.is( ".ui-state-disabled" ) ) {
+			if ( this.active.children( "a[aria-haspopup='true']" ).length ) {
+				this.expand( event );
+			} else {
+				this.select( event );
+			}
+		}
+	},
+
+	refresh: function() {
+		// Initialize nested menus
+		var menus,
+			icon = this.options.icons.submenu,
+			submenus = this.element.find( this.options.menus + ":not(.ui-menu)" )
+				.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
+				.hide()
+				.attr({
+					role: this.options.role,
+					"aria-hidden": "true",
+					"aria-expanded": "false"
+				});
+
+		// Don't refresh list items that are already adapted
+		menus = submenus.add( this.element );
+
+		menus.children( ":not(.ui-menu-item):has(a)" )
+			.addClass( "ui-menu-item" )
+			.attr( "role", "presentation" )
+			.children( "a" )
+				.uniqueId()
+				.addClass( "ui-corner-all" )
+				.attr({
+					tabIndex: -1,
+					role: this._itemRole()
+				});
+
+		// Initialize unlinked menu-items containing spaces and/or dashes only as dividers
+		menus.children( ":not(.ui-menu-item)" ).each(function() {
+			var item = $( this );
+			// hyphen, em dash, en dash
+			if ( !/[^\-—–\s]/.test( item.text() ) ) {
+				item.addClass( "ui-widget-content ui-menu-divider" );
+			}
+		});
+
+		// Add aria-disabled attribute to any disabled menu item
+		menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
+
+		submenus.each(function() {
+			var menu = $( this ),
+				item = menu.prev( "a" ),
+				submenuCarat = $( "<span>" )
+					.addClass( "ui-menu-icon ui-icon " + icon )
+					.data( "ui-menu-submenu-carat", true );
+
+			item
+				.attr( "aria-haspopup", "true" )
+				.prepend( submenuCarat );
+			menu.attr( "aria-labelledby", item.attr( "id" ) );
+		});
+
+		// If the active item has been removed, blur the menu
+		if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
+			this.blur();
+		}
+	},
+
+	_itemRole: function() {
+		return {
+			menu: "menuitem",
+			listbox: "option"
+		}[ this.options.role ];
+	},
+
+	focus: function( event, item ) {
+		var nested, focused;
+		this.blur( event, event && event.type === "focus" );
+
+		this._scrollIntoView( item );
+
+		this.active = item.first();
+		focused = this.active.children( "a" ).addClass( "ui-state-focus" );
+		// Only update aria-activedescendant if there's a role
+		// otherwise we assume focus is managed elsewhere
+		if ( this.options.role ) {
+			this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
+		}
+
+		// Highlight active parent menu item, if any
+		this.active
+			.parent()
+			.closest( ".ui-menu-item" )
+			.children( "a:first" )
+			.addClass( "ui-state-active" );
+
+		if ( event && event.type === "keydown" ) {
+			this._close();
+		} else {
+			this.timer = this._delay(function() {
+				this._close();
+			}, this.delay );
+		}
+
+		nested = item.children( ".ui-menu" );
+		if ( nested.length && ( /^mouse/.test( event.type ) ) ) {
+			this._startOpening(nested);
+		}
+		this.activeMenu = item.parent();
+
+		this._trigger( "focus", event, { item: item } );
+	},
+
+	_scrollIntoView: function( item ) {
+		var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
+		if ( this._hasScroll() ) {
+			borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
+			paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
+			offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
+			scroll = this.activeMenu.scrollTop();
+			elementHeight = this.activeMenu.height();
+			itemHeight = item.height();
+
+			if ( offset < 0 ) {
+				this.activeMenu.scrollTop( scroll + offset );
+			} else if ( offset + itemHeight > elementHeight ) {
+				this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
+			}
+		}
+	},
+
+	blur: function( event, fromFocus ) {
+		if ( !fromFocus ) {
+			clearTimeout( this.timer );
+		}
+
+		if ( !this.active ) {
+			return;
+		}
+
+		this.active.children( "a" ).removeClass( "ui-state-focus" );
+		this.active = null;
+
+		this._trigger( "blur", event, { item: this.active } );
+	},
+
+	_startOpening: function( submenu ) {
+		clearTimeout( this.timer );
+
+		// Don't open if already open fixes a Firefox bug that caused a .5 pixel
+		// shift in the submenu position when mousing over the carat icon
+		if ( submenu.attr( "aria-hidden" ) !== "true" ) {
+			return;
+		}
+
+		this.timer = this._delay(function() {
+			this._close();
+			this._open( submenu );
+		}, this.delay );
+	},
+
+	_open: function( submenu ) {
+		var position = $.extend({
+			of: this.active
+		}, this.options.position );
+
+		clearTimeout( this.timer );
+		this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
+			.hide()
+			.attr( "aria-hidden", "true" );
+
+		submenu
+			.show()
+			.removeAttr( "aria-hidden" )
+			.attr( "aria-expanded", "true" )
+			.position( position );
+	},
+
+	collapseAll: function( event, all ) {
+		clearTimeout( this.timer );
+		this.timer = this._delay(function() {
+			// If we were passed an event, look for the submenu that contains the event
+			var currentMenu = all ? this.element :
+				$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
+
+			// If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
+			if ( !currentMenu.length ) {
+				currentMenu = this.element;
+			}
+
+			this._close( currentMenu );
+
+			this.blur( event );
+			this.activeMenu = currentMenu;
+		}, this.delay );
+	},
+
+	// With no arguments, closes the currently active menu - if nothing is active
+	// it closes all menus.  If passed an argument, it will search for menus BELOW
+	_close: function( startMenu ) {
+		if ( !startMenu ) {
+			startMenu = this.active ? this.active.parent() : this.element;
+		}
+
+		startMenu
+			.find( ".ui-menu" )
+				.hide()
+				.attr( "aria-hidden", "true" )
+				.attr( "aria-expanded", "false" )
+			.end()
+			.find( "a.ui-state-active" )
+				.removeClass( "ui-state-active" );
+	},
+
+	collapse: function( event ) {
+		var newItem = this.active &&
+			this.active.parent().closest( ".ui-menu-item", this.element );
+		if ( newItem && newItem.length ) {
+			this._close();
+			this.focus( event, newItem );
+		}
+	},
+
+	expand: function( event ) {
+		var newItem = this.active &&
+			this.active
+				.children( ".ui-menu " )
+				.children( ".ui-menu-item" )
+				.first();
+
+		if ( newItem && newItem.length ) {
+			this._open( newItem.parent() );
+
+			// Delay so Firefox will not hide activedescendant change in expanding submenu from AT
+			this._delay(function() {
+				this.focus( event, newItem );
+			});
+		}
+	},
+
+	next: function( event ) {
+		this._move( "next", "first", event );
+	},
+
+	previous: function( event ) {
+		this._move( "prev", "last", event );
+	},
+
+	isFirstItem: function() {
+		return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
+	},
+
+	isLastItem: function() {
+		return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
+	},
+
+	_move: function( direction, filter, event ) {
+		var next;
+		if ( this.active ) {
+			if ( direction === "first" || direction === "last" ) {
+				next = this.active
+					[ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
+					.eq( -1 );
+			} else {
+				next = this.active
+					[ direction + "All" ]( ".ui-menu-item" )
+					.eq( 0 );
+			}
+		}
+		if ( !next || !next.length || !this.active ) {
+			next = this.activeMenu.children( ".ui-menu-item" )[ filter ]();
+		}
+
+		this.focus( event, next );
+	},
+
+	nextPage: function( event ) {
+		var item, base, height;
+
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isLastItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.nextAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base - height < 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.children( ".ui-menu-item" )
+				[ !this.active ? "first" : "last" ]() );
+		}
+	},
+
+	previousPage: function( event ) {
+		var item, base, height;
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isFirstItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.prevAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base + height > 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() );
+		}
+	},
+
+	_hasScroll: function() {
+		return this.element.outerHeight() < this.element.prop( "scrollHeight" );
+	},
+
+	select: function( event ) {
+		// TODO: It should never be possible to not have an active item at this
+		// point, but the tests don't trigger mouseenter before click.
+		this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
+		var ui = { item: this.active };
+		if ( !this.active.has( ".ui-menu" ).length ) {
+			this.collapseAll( event, true );
+		}
+		if ( this.options.trigger ) {
+			$( this.options.trigger ).focus( 1 );
+			this.element.popup( "close" );
+		}
+		this._trigger( "select", event, ui );
+	}
+});
+
+}( jQuery ));
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.css
new file mode 100644
index 0000000..8b175f2
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.css
@@ -0,0 +1,15 @@
+/*
+ * jQuery UI Menubar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+.ui-menubar { list-style: none; margin: 0; padding-left: 0; }
+
+.ui-menubar-item { float: left; }
+
+.ui-menubar .ui-button { float: left; font-weight: normal; border-top-width: 0 !important; border-bottom-width: 0 !important; margin: 0; outline: none; }
+.ui-menubar .ui-menubar-link { border-right: 1px dashed transparent; border-left: 1px dashed transparent; }
+
+.ui-menubar .ui-menu { width: 200px; position: absolute; z-index: 9999; font-weight: normal; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.js
new file mode 100644
index 0000000..4936ed4
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.menubar.js
@@ -0,0 +1,327 @@
+/*
+ * jQuery UI Menubar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menubar
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ *	jquery.ui.menu.js
+ */
+(function( $ ) {
+
+// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
+// there has to be just one item that has tabindex
+$.widget( "ui.menubar", {
+	version: "@VERSION",
+	options: {
+		autoExpand: false,
+		buttons: false,
+		items: "li",
+		menuElement: "ul",
+		menuIcon: false,
+		position: {
+			my: "left top",
+			at: "left bottom"
+		}
+	},
+	_create: function() {
+		var that = this;
+		this.menuItems = this.element.children( this.options.items );
+		this.items = this.menuItems.children( "button, a" );
+
+		this.menuItems
+			.addClass( "ui-menubar-item" )
+			.attr( "role", "presentation" );
+		// let only the first item receive focus
+		this.items.slice(1).attr( "tabIndex", -1 );
+
+		this.element
+			.addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+			.attr( "role", "menubar" );
+		this._focusable( this.items );
+		this._hoverable( this.items );
+		this.items.siblings( this.options.menuElement )
+			.menu({
+				position: {
+					within: this.options.position.within
+				},
+				select: function( event, ui ) {
+					ui.item.parents( "ul.ui-menu:last" ).hide();
+					that._close();
+					// TODO what is this targetting? there's probably a better way to access it
+					$(event.target).prev().focus();
+					that._trigger( "select", event, ui );
+				},
+				menus: that.options.menuElement
+			})
+			.hide()
+			.attr({
+				"aria-hidden": "true",
+				"aria-expanded": "false"
+			})
+			// TODO use _on
+			.bind( "keydown.menubar", function( event ) {
+				var menu = $( this );
+				if ( menu.is( ":hidden" ) ) {
+					return;
+				}
+				switch ( event.keyCode ) {
+				case $.ui.keyCode.LEFT:
+					that.previous( event );
+					event.preventDefault();
+					break;
+				case $.ui.keyCode.RIGHT:
+					that.next( event );
+					event.preventDefault();
+					break;
+				}
+			});
+		this.items.each(function() {
+			var input = $(this),
+				// TODO menu var is only used on two places, doesn't quite justify the .each
+				menu = input.next( that.options.menuElement );
+
+			// might be a non-menu button
+			if ( menu.length ) {
+				// TODO use _on
+				input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
+					// ignore triggered focus event
+					if ( event.type === "focus" && !event.originalEvent ) {
+						return;
+					}
+					event.preventDefault();
+					// TODO can we simplify or extractthis check? especially the last two expressions
+					// there's a similar active[0] == menu[0] check in _open
+					if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
+						that._close();
+						return;
+					}
+					if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
+						if( that.options.autoExpand ) {
+							clearTimeout( that.closeTimer );
+						}
+
+						that._open( event, menu );
+					}
+				})
+				// TODO use _on
+				.bind( "keydown", function( event ) {
+					switch ( event.keyCode ) {
+					case $.ui.keyCode.SPACE:
+					case $.ui.keyCode.UP:
+					case $.ui.keyCode.DOWN:
+						that._open( event, $( this ).next() );
+						event.preventDefault();
+						break;
+					case $.ui.keyCode.LEFT:
+						that.previous( event );
+						event.preventDefault();
+						break;
+					case $.ui.keyCode.RIGHT:
+						that.next( event );
+						event.preventDefault();
+						break;
+					}
+				})
+				.attr( "aria-haspopup", "true" );
+
+				// TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
+				if ( that.options.menuIcon ) {
+					input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
+					input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
+				}
+			} else {
+				// TODO use _on
+				input.bind( "click.menubar mouseenter.menubar", function( event ) {
+					if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
+						that._close();
+					}
+				});
+			}
+
+			input
+				.addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
+				.attr( "role", "menuitem" )
+				.wrapInner( "<span class='ui-button-text'></span>" );
+
+			if ( that.options.buttons ) {
+				input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
+			}
+		});
+		that._on( {
+			keydown: function( event ) {
+				if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
+					var active = that.active;
+					that.active.blur();
+					that._close( event );
+					active.prev().focus();
+				}
+			},
+			focusin: function( event ) {
+				clearTimeout( that.closeTimer );
+			},
+			focusout: function( event ) {
+				that.closeTimer = setTimeout( function() {
+					that._close( event );
+				}, 150);
+			},
+			"mouseleave .ui-menubar-item": function( event ) {
+				if ( that.options.autoExpand ) {
+					that.closeTimer = setTimeout( function() {
+						that._close( event );
+					}, 150);
+				}
+			},
+			"mouseenter .ui-menubar-item": function( event ) {
+				clearTimeout( that.closeTimer );
+			}
+		});
+
+		// Keep track of open submenus
+		this.openSubmenus = 0;
+	},
+
+	_destroy : function() {
+		this.menuItems
+			.removeClass( "ui-menubar-item" )
+			.removeAttr( "role" );
+
+		this.element
+			.removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+			.removeAttr( "role" )
+			.unbind( ".menubar" );
+
+		this.items
+			.unbind( ".menubar" )
+			.removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-haspopup" )
+			// TODO unwrap?
+			.children( "span.ui-button-text" ).each(function( i, e ) {
+				var item = $( this );
+				item.parent().html( item.html() );
+			})
+			.end()
+			.children( ".ui-icon" ).remove();
+
+		this.element.find( ":ui-menu" )
+			.menu( "destroy" )
+			.show()
+			.removeAttr( "aria-hidden" )
+			.removeAttr( "aria-expanded" )
+			.removeAttr( "tabindex" )
+			.unbind( ".menubar" );
+	},
+
+	_close: function() {
+		if ( !this.active || !this.active.length ) {
+			return;
+		}
+		this.active
+			.menu( "collapseAll" )
+			.hide()
+			.attr({
+				"aria-hidden": "true",
+				"aria-expanded": "false"
+			});
+		this.active
+			.prev()
+			.removeClass( "ui-state-active" )
+			.removeAttr( "tabIndex" );
+		this.active = null;
+		this.open = false;
+		this.openSubmenus = 0;
+	},
+
+	_open: function( event, menu ) {
+		// on a single-button menubar, ignore reopening the same menu
+		if ( this.active && this.active[0] === menu[0] ) {
+			return;
+		}
+		// TODO refactor, almost the same as _close above, but don't remove tabIndex
+		if ( this.active ) {
+			this.active
+				.menu( "collapseAll" )
+				.hide()
+				.attr({
+					"aria-hidden": "true",
+					"aria-expanded": "false"
+				});
+			this.active
+				.prev()
+				.removeClass( "ui-state-active" );
+		}
+		// set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
+		var button = menu.prev().addClass( "ui-state-active" ).attr( "tabIndex", -1 );
+		this.active = menu
+			.show()
+			.position( $.extend({
+				of: button
+			}, this.options.position ) )
+			.removeAttr( "aria-hidden" )
+			.attr( "aria-expanded", "true" )
+			.menu("focus", event, menu.children( ".ui-menu-item" ).first() )
+			// TODO need a comment here why both events are triggered
+			.focus()
+			.focusin();
+		this.open = true;
+	},
+
+	next: function( event ) {
+		if ( this.open && this.active.data( "menu" ).active.has( ".ui-menu" ).length ) {
+			// Track number of open submenus and prevent moving to next menubar item
+			this.openSubmenus++;
+			return;
+		}
+		this.openSubmenus = 0;
+		this._move( "next", "first", event );
+	},
+
+	previous: function( event ) {
+		if ( this.open && this.openSubmenus ) {
+			// Track number of open submenus and prevent moving to previous menubar item
+			this.openSubmenus--;
+			return;
+		}
+		this.openSubmenus = 0;
+		this._move( "prev", "last", event );
+	},
+
+	_move: function( direction, filter, event ) {
+		var next,
+			wrapItem;
+		if ( this.open ) {
+			next = this.active.closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).first().children( ".ui-menu" ).eq( 0 );
+			wrapItem = this.menuItems[ filter ]().children( ".ui-menu" ).eq( 0 );
+		} else {
+			if ( event ) {
+				next = $( event.target ).closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).children( ".ui-menubar-link" ).eq( 0 );
+				wrapItem = this.menuItems[ filter ]().children( ".ui-menubar-link" ).eq( 0 );
+			} else {
+				next = wrapItem = this.menuItems.children( "a" ).eq( 0 );
+			}
+		}
+
+		if ( next.length ) {
+			if ( this.open ) {
+				this._open( event, next );
+			} else {
+				next.removeAttr( "tabIndex")[0].focus();
+			}
+		} else {
+			if ( this.open ) {
+				this._open( event, wrapItem );
+			} else {
+				wrapItem.removeAttr( "tabIndex")[0].focus();
+			}
+		}
+	}
+});
+
+}( jQuery ));
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.position.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.position.js
new file mode 100644
index 0000000..5b595a8
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.position.js
@@ -0,0 +1,517 @@
+/*!
+ * jQuery UI Position @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/position/
+ */
+(function( $, undefined ) {
+
+$.ui = $.ui || {};
+
+var cachedScrollbarWidth,
+	max = Math.max,
+	abs = Math.abs,
+	round = Math.round,
+	rhorizontal = /left|center|right/,
+	rvertical = /top|center|bottom/,
+	roffset = /[\+\-]\d+%?/,
+	rposition = /^\w+/,
+	rpercent = /%$/,
+	_position = $.fn.position;
+
+function getOffsets( offsets, width, height ) {
+	return [
+		parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
+		parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
+	];
+}
+function parseCss( element, property ) {
+	return parseInt( $.css( element, property ), 10 ) || 0;
+}
+
+$.position = {
+	scrollbarWidth: function() {
+		if ( cachedScrollbarWidth !== undefined ) {
+			return cachedScrollbarWidth;
+		}
+		var w1, w2,
+			div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
+			innerDiv = div.children()[0];
+
+		$( "body" ).append( div );
+		w1 = innerDiv.offsetWidth;
+		div.css( "overflow", "scroll" );
+
+		w2 = innerDiv.offsetWidth;
+
+		if ( w1 === w2 ) {
+			w2 = div[0].clientWidth;
+		}
+
+		div.remove();
+
+		return (cachedScrollbarWidth = w1 - w2);
+	},
+	getScrollInfo: function( within ) {
+		var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ),
+			overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ),
+			hasOverflowX = overflowX === "scroll" ||
+				( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
+			hasOverflowY = overflowY === "scroll" ||
+				( overflowY === "auto" && within.height < within.element[0].scrollHeight );
+		return {
+			width: hasOverflowX ? $.position.scrollbarWidth() : 0,
+			height: hasOverflowY ? $.position.scrollbarWidth() : 0
+		};
+	},
+	getWithinInfo: function( element ) {
+		var withinElement = $( element || window ),
+			isWindow = $.isWindow( withinElement[0] );
+		return {
+			element: withinElement,
+			isWindow: isWindow,
+			offset: withinElement.offset() || { left: 0, top: 0 },
+			scrollLeft: withinElement.scrollLeft(),
+			scrollTop: withinElement.scrollTop(),
+			width: isWindow ? withinElement.width() : withinElement.outerWidth(),
+			height: isWindow ? withinElement.height() : withinElement.outerHeight()
+		};
+	}
+};
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var atOffset, targetWidth, targetHeight, targetOffset, basePosition,
+		target = $( options.of ),
+		within = $.position.getWithinInfo( options.within ),
+		scrollInfo = $.position.getScrollInfo( within ),
+		targetElem = target[0],
+		collision = ( options.collision || "flip" ).split( " " ),
+		offsets = {};
+
+	if ( targetElem.nodeType === 9 ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		targetOffset = { top: 0, left: 0 };
+	} else if ( $.isWindow( targetElem ) ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		targetOffset = { top: target.scrollTop(), left: target.scrollLeft() };
+	} else if ( targetElem.preventDefault ) {
+		// force left top to allow flipping
+		options.at = "left top";
+		targetWidth = targetHeight = 0;
+		targetOffset = { top: targetElem.pageY, left: targetElem.pageX };
+	} else {
+		targetWidth = target.outerWidth();
+		targetHeight = target.outerHeight();
+		targetOffset = target.offset();
+	}
+	// clone to reuse original targetOffset later
+	basePosition = $.extend( {}, targetOffset );
+
+	// force my and at to have valid horizontal and vertical positions
+	// if a value is missing or invalid, it will be converted to center
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[ this ] || "" ).split( " " ),
+			horizontalOffset,
+			verticalOffset;
+
+		if ( pos.length === 1) {
+			pos = rhorizontal.test( pos[ 0 ] ) ?
+				pos.concat( [ "center" ] ) :
+				rvertical.test( pos[ 0 ] ) ?
+					[ "center" ].concat( pos ) :
+					[ "center", "center" ];
+		}
+		pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
+		pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
+
+		// calculate offsets
+		horizontalOffset = roffset.exec( pos[ 0 ] );
+		verticalOffset = roffset.exec( pos[ 1 ] );
+		offsets[ this ] = [
+			horizontalOffset ? horizontalOffset[ 0 ] : 0,
+			verticalOffset ? verticalOffset[ 0 ] : 0
+		];
+
+		// reduce to just the positions without the offsets
+		options[ this ] = [
+			rposition.exec( pos[ 0 ] )[ 0 ],
+			rposition.exec( pos[ 1 ] )[ 0 ]
+		];
+	});
+
+	// normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	if ( options.at[ 0 ] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[ 0 ] === "center" ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[ 1 ] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[ 1 ] === "center" ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
+	basePosition.left += atOffset[ 0 ];
+	basePosition.top += atOffset[ 1 ];
+
+	return this.each(function() {
+		var collisionPosition, using,
+			elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseCss( this, "marginLeft" ),
+			marginTop = parseCss( this, "marginTop" ),
+			collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
+			collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
+			position = $.extend( {}, basePosition ),
+			myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
+
+		if ( options.my[ 0 ] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[ 0 ] === "center" ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[ 1 ] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[ 1 ] === "center" ) {
+			position.top -= elemHeight / 2;
+		}
+
+		position.left += myOffset[ 0 ];
+		position.top += myOffset[ 1 ];
+
+		// if the browser doesn't support fractions, then round for consistent results
+		if ( !$.support.offsetFractions ) {
+			position.left = round( position.left );
+			position.top = round( position.top );
+		}
+
+		collisionPosition = {
+			marginLeft: marginLeft,
+			marginTop: marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[ i ] ] ) {
+				$.ui.position[ collision[ i ] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
+					my: options.my,
+					at: options.at,
+					within: within,
+					elem : elem
+				});
+			}
+		});
+
+		if ( $.fn.bgiframe ) {
+			elem.bgiframe();
+		}
+
+		if ( options.using ) {
+			// adds feedback as second argument to using callback, if present
+			using = function( props ) {
+				var left = targetOffset.left - position.left,
+					right = left + targetWidth - elemWidth,
+					top = targetOffset.top - position.top,
+					bottom = top + targetHeight - elemHeight,
+					feedback = {
+						target: {
+							element: target,
+							left: targetOffset.left,
+							top: targetOffset.top,
+							width: targetWidth,
+							height: targetHeight
+						},
+						element: {
+							element: elem,
+							left: position.left,
+							top: position.top,
+							width: elemWidth,
+							height: elemHeight
+						},
+						horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
+						vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
+					};
+				if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
+					feedback.horizontal = "center";
+				}
+				if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
+					feedback.vertical = "middle";
+				}
+				if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
+					feedback.important = "horizontal";
+				} else {
+					feedback.important = "vertical";
+				}
+				options.using.call( this, props, feedback );
+			};
+		}
+
+		elem.offset( $.extend( position, { using: using } ) );
+	});
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
+				outerWidth = within.width,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = withinOffset - collisionPosLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
+				newOverRight;
+
+			// element is wider than within
+			if ( data.collisionWidth > outerWidth ) {
+				// element is initially over the left side of within
+				if ( overLeft > 0 && overRight <= 0 ) {
+					newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
+					position.left += overLeft - newOverRight;
+				// element is initially over right side of within
+				} else if ( overRight > 0 && overLeft <= 0 ) {
+					position.left = withinOffset;
+				// element is initially over both left and right sides of within
+				} else {
+					if ( overLeft > overRight ) {
+						position.left = withinOffset + outerWidth - data.collisionWidth;
+					} else {
+						position.left = withinOffset;
+					}
+				}
+			// too far left -> align with left edge
+			} else if ( overLeft > 0 ) {
+				position.left += overLeft;
+			// too far right -> align with right edge
+			} else if ( overRight > 0 ) {
+				position.left -= overRight;
+			// adjust based on position and margin
+			} else {
+				position.left = max( position.left - collisionPosLeft, position.left );
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
+				outerHeight = data.within.height,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = withinOffset - collisionPosTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
+				newOverBottom;
+
+			// element is taller than within
+			if ( data.collisionHeight > outerHeight ) {
+				// element is initially over the top of within
+				if ( overTop > 0 && overBottom <= 0 ) {
+					newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
+					position.top += overTop - newOverBottom;
+				// element is initially over bottom of within
+				} else if ( overBottom > 0 && overTop <= 0 ) {
+					position.top = withinOffset;
+				// element is initially over both top and bottom of within
+				} else {
+					if ( overTop > overBottom ) {
+						position.top = withinOffset + outerHeight - data.collisionHeight;
+					} else {
+						position.top = withinOffset;
+					}
+				}
+			// too far up -> align with top
+			} else if ( overTop > 0 ) {
+				position.top += overTop;
+			// too far down -> align with bottom edge
+			} else if ( overBottom > 0 ) {
+				position.top -= overBottom;
+			// adjust based on position and margin
+			} else {
+				position.top = max( position.top - collisionPosTop, position.top );
+			}
+		}
+	},
+	flip: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.left + within.scrollLeft,
+				outerWidth = within.width,
+				offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = collisionPosLeft - offsetLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					data.at[ 0 ] === "right" ?
+						-data.targetWidth :
+						0,
+				offset = -2 * data.offset[ 0 ],
+				newOverRight,
+				newOverLeft;
+
+			if ( overLeft < 0 ) {
+				newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
+				if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+			else if ( overRight > 0 ) {
+				newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
+				if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.top + within.scrollTop,
+				outerHeight = within.height,
+				offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = collisionPosTop - offsetTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
+				top = data.my[ 1 ] === "top",
+				myOffset = top ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					data.at[ 1 ] === "bottom" ?
+						-data.targetHeight :
+						0,
+				offset = -2 * data.offset[ 1 ],
+				newOverTop,
+				newOverBottom;
+			if ( overTop < 0 ) {
+				newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
+				if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+			else if ( overBottom > 0 ) {
+				newOverTop = position.top -  data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
+				if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+		}
+	},
+	flipfit: {
+		left: function() {
+			$.ui.position.flip.left.apply( this, arguments );
+			$.ui.position.fit.left.apply( this, arguments );
+		},
+		top: function() {
+			$.ui.position.flip.top.apply( this, arguments );
+			$.ui.position.fit.top.apply( this, arguments );
+		}
+	}
+};
+
+// fraction support test
+(function () {
+	var testElement, testElementParent, testElementStyle, offsetLeft, i,
+		body = document.getElementsByTagName( "body" )[ 0 ],
+		div = document.createElement( "div" );
+
+	//Create a "fake body" for testing based on method used in jQuery.support
+	testElement = document.createElement( body ? "div" : "body" );
+	testElementStyle = {
+		visibility: "hidden",
+		width: 0,
+		height: 0,
+		border: 0,
+		margin: 0,
+		background: "none"
+	};
+	if ( body ) {
+		$.extend( testElementStyle, {
+			position: "absolute",
+			left: "-1000px",
+			top: "-1000px"
+		});
+	}
+	for ( i in testElementStyle ) {
+		testElement.style[ i ] = testElementStyle[ i ];
+	}
+	testElement.appendChild( div );
+	testElementParent = body || document.documentElement;
+	testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+	div.style.cssText = "position: absolute; left: 10.7432222px;";
+
+	offsetLeft = $( div ).offset().left;
+	$.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
+
+	testElement.innerHTML = "";
+	testElementParent.removeChild( testElement );
+})();
+
+// DEPRECATED
+if ( $.uiBackCompat !== false ) {
+	// offset option
+	(function( $ ) {
+		var _position = $.fn.position;
+		$.fn.position = function( options ) {
+			if ( !options || !options.offset ) {
+				return _position.call( this, options );
+			}
+			var offset = options.offset.split( " " ),
+				at = options.at.split( " " );
+			if ( offset.length === 1 ) {
+				offset[ 1 ] = offset[ 0 ];
+			}
+			if ( /^\d/.test( offset[ 0 ] ) ) {
+				offset[ 0 ] = "+" + offset[ 0 ];
+			}
+			if ( /^\d/.test( offset[ 1 ] ) ) {
+				offset[ 1 ] = "+" + offset[ 1 ];
+			}
+			if ( at.length === 1 ) {
+				if ( /left|center|right/.test( at[ 0 ] ) ) {
+					at[ 1 ] = "center";
+				} else {
+					at[ 1 ] = at[ 0 ];
+					at[ 0 ] = "center";
+				}
+			}
+			return _position.call( this, $.extend( options, {
+				at: at[ 0 ] + offset[ 0 ] + " " + at[ 1 ] + offset[ 1 ],
+				offset: undefined
+			} ) );
+		};
+	}( jQuery ) );
+}
+
+}( jQuery ) );
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.progressbar.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.progressbar.css
new file mode 100644
index 0000000..a8b3d74
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.progressbar.css
@@ -0,0 +1,12 @@
+/*!
+ * jQuery UI Progressbar @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.resizable.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.resizable.css
new file mode 100644
index 0000000..291bdb2
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.resizable.css
@@ -0,0 +1,21 @@
+/*!
+ * jQuery UI Resizable @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.selectable.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.selectable.css
new file mode 100644
index 0000000..e00779d
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.selectable.css
@@ -0,0 +1,11 @@
+/*!
+ * jQuery UI Selectable @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.slider.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.slider.css
new file mode 100644
index 0000000..5e7a973
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.slider.css
@@ -0,0 +1,25 @@
+/*!
+ * jQuery UI Slider @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.spinner.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.spinner.css
new file mode 100644
index 0000000..94b73fa
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.spinner.css
@@ -0,0 +1,24 @@
+/*!
+ * jQuery UI Spinner @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Spinner#theming
+ */
+.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
+.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
+.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; z-index: 100; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
+.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
+.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
+.ui-spinner-up { top: 0; }
+.ui-spinner-down { bottom: 0; }
+
+/* TR overrides */
+span.ui-spinner { background: none; }
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position:-65px -16px;
+}
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tabs.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tabs.css
new file mode 100644
index 0000000..405b693
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tabs.css
@@ -0,0 +1,18 @@
+/*!
+ * jQuery UI Tabs @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 0; margin: 1px .2em 0 0; border-bottom: 0; padding: 0; white-space: nowrap; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-active { margin-bottom: -1px; padding-bottom: 1px; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-tabs-loading a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.theme.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.theme.css
new file mode 100644
index 0000000..d70b9df
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.theme.css
@@ -0,0 +1,247 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(images/ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; }
+.ui-widget-content a { color: #222222/*{fcContent}*/; }
+.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; }
+.ui-widget-header a { color: #222222/*{fcHeader}*/; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; }
+.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 #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; }
+.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #212121/*{fcHover}*/; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -khtml-border-top-left-radius: 4px/*{cornerRadius}*/; border-top-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -khtml-border-top-right-radius: 4px/*{cornerRadius}*/; border-top-right-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-left-radius: 4px/*{cornerRadius}*/; border-bottom-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; }
+.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRad [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tooltip.css b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tooltip.css
new file mode 100644
index 0000000..e293eeb
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.tooltip.css
@@ -0,0 +1,22 @@
+/*!
+ * jQuery UI Tooltip @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+.ui-tooltip {
+	padding:8px;
+	position:absolute;
+	z-index:9999;
+	-o-box-shadow: 0 0 5px #aaa;
+	-moz-box-shadow: 0 0 5px #aaa;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+/* Fades and background-images don't work well together in IE6, drop the image */
+* html .ui-tooltip {
+	background-image: none;
+}
+body .ui-tooltip { border-width:2px; }
diff --git a/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.widget.js b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.widget.js
new file mode 100644
index 0000000..a125dd5
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/jquery-ui-menubar/jquery.ui.widget.js
@@ -0,0 +1,502 @@
+/*!
+ * jQuery UI Widget @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/jQuery.widget/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	slice = Array.prototype.slice,
+	_cleanData = $.cleanData;
+$.cleanData = function( elems ) {
+	for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+		try {
+			$( elem ).triggerHandler( "remove" );
+		// http://bugs.jquery.com/ticket/8235
+		} catch( e ) {}
+	}
+	_cleanData( elems );
+};
+
+$.widget = function( name, base, prototype ) {
+	var fullName, existingConstructor, constructor, basePrototype,
+		namespace = name.split( "." )[ 0 ];
+
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+	// extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+		// copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+		// track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	});
+
+	basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( $.isFunction( value ) ) {
+			prototype[ prop ] = (function() {
+				var _super = function() {
+						return base.prototype[ prop ].apply( this, arguments );
+					},
+					_superApply = function( args ) {
+						return base.prototype[ prop ].apply( this, args );
+					};
+				return function() {
+					var __super = this._super,
+						__superApply = this._superApply,
+						returnValue;
+
+					this._super = _super;
+					this._superApply = _superApply;
+
+					returnValue = value.apply( this, arguments );
+
+					this._super = __super;
+					this._superApply = __superApply;
+
+					return returnValue;
+				};
+			})();
+		}
+	});
+	constructor.prototype = $.widget.extend( basePrototype, {
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: name
+	}, prototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		// TODO remove widgetBaseClass, see #8155
+		widgetBaseClass: fullName,
+		widgetFullName: fullName
+	});
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
+		});
+		// remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+};
+
+$.widget.extend = function( target ) {
+	var input = slice.call( arguments, 1 ),
+		inputIndex = 0,
+		inputLength = input.length,
+		key,
+		value;
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+				target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value;
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.widget.extend.apply( null, [ options ].concat(args) ) :
+			options;
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var methodValue,
+					instance = $.data( this, fullName );
+				if ( !instance ) {
+					return $.error( "cannot call methods on " + name + " prior to initialization; " +
+						"attempted to call method '" + options + "'" );
+				}
+				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
+					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
+				}
+				methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue && methodValue.jquery ?
+						returnValue.pushStack( methodValue.get() ) :
+						methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					new object( options, this );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( options, element ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+	options: {
+		disabled: false,
+
+		// callbacks
+		create: null
+	},
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = uuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+
+		if ( element !== this ) {
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			$.data( element, this.widgetName, this );
+			$.data( element, this.widgetFullName, this );
+			this._on({ remove: "destroy" });
+			this.document = $( element.style ?
+				// element within the document
+				element.ownerDocument :
+				// element is window or document
+				element.document || element );
+			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
+		}
+
+		this._create();
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+	_getCreateOptions: $.noop,
+	_getCreateEventData: $.noop,
+	_create: $.noop,
+	_init: $.noop,
+
+	destroy: function() {
+		this._destroy();
+		// we can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.unbind( this.eventNamespace )
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			.removeData( this.widgetName )
+			.removeData( this.widgetFullName )
+			// support: jquery <1.6.3
+			// http://bugs.jquery.com/ticket/9413
+			.removeData( $.camelCase( this.widgetFullName ) );
+		this.widget()
+			.unbind( this.eventNamespace )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetFullName + "-disabled " +
+				"ui-state-disabled" );
+
+		// clean up events and states
+		this.bindings.unbind( this.eventNamespace );
+		this.hoverable.removeClass( "ui-state-hover" );
+		this.focusable.removeClass( "ui-state-focus" );
+	},
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key,
+			parts,
+			curOption,
+			i;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( value === undefined ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( value === undefined ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+			this.hoverable.removeClass( "ui-state-hover" );
+			this.focusable.removeClass( "ui-state-focus" );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_on: function( element, handlers ) {
+		// no element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+		} else {
+			// accept selectors, DOM elements
+			element = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		var instance = this;
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+				// allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( instance.options.disabled === true ||
+						$( this ).hasClass( "ui-state-disabled" ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^(\w+)\s*(.*)$/ ),
+				eventName = match[1] + instance.eventNamespace,
+				selector = match[2];
+			if ( selector ) {
+				instance.widget().delegate( selector, eventName, handlerProxy );
+			} else {
+				element.bind( eventName, handlerProxy );
+			}
+		});
+	},
+
+	_off: function( element, eventName ) {
+		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
+		element.unbind( eventName ).undelegate( eventName );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-hover" );
+			}
+		});
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-focus" );
+			}
+		});
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+		var hasOptions,
+			effectName = !options ?
+				method :
+				options === true || typeof options === "number" ?
+					defaultEffect :
+					options.effect || defaultEffect;
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+		if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue(function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			});
+		}
+	};
+});
+
+// DEPRECATED
+if ( $.uiBackCompat !== false ) {
+	$.Widget.prototype._getCreateOptions = function() {
+		return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
+	};
+}
+
+})( jQuery );
diff --git a/grails-app/assets/javascripts/vendor/raf/index.js b/grails-app/assets/javascripts/vendor/raf/index.js
new file mode 100644
index 0000000..3d42a3a
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/raf/index.js
@@ -0,0 +1,31 @@
+// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
+
+// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
+
+// MIT license
+
+(function() {
+    var lastTime = 0;
+    var vendors = ['ms', 'moz', 'webkit', 'o'];
+    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
+        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
+                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
+    }
+ 
+    if (!window.requestAnimationFrame)
+        window.requestAnimationFrame = function(callback, element) {
+            var currTime = new Date().getTime();
+            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
+              timeToCall);
+            lastTime = currTime + timeToCall;
+            return id;
+        };
+ 
+    if (!window.cancelAnimationFrame)
+        window.cancelAnimationFrame = function(id) {
+            clearTimeout(id);
+        };
+}());
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-0.11.0.min.js b/grails-app/assets/javascripts/vendor/ui-bootstrap-0.11.0.min.js
new file mode 100644
index 0000000..53fd24e
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-0.11.0.min.js
@@ -0,0 +1,9 @@
+/*
+ * angular-ui-bootstrap
+ * http://angular-ui.github.io/bootstrap/
+
+ * Version: 0.11.0 - 2014-05-01
+ * License: MIT
+ */
+angular.module("ui.bootstrap",["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bo [...]
+})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void  [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4-csp.css b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4-csp.css
new file mode 100755
index 0000000..d772f78
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4-csp.css
@@ -0,0 +1,6 @@
+/* Include this file in your html if you are using the CSP mode. */
+
+.ng-animate.item:not(.left):not(.right) {
+  -webkit-transition: 0s ease-in-out left;
+  transition: 0s ease-in-out left
+}
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.js b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.js
new file mode 100755
index 0000000..b33d84e
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.js
@@ -0,0 +1,5450 @@
+/*
+ * angular-ui-bootstrap
+ * http://angular-ui.github.io/bootstrap/
+
+ * Version: 0.13.4 - 2015-09-03
+ * License: MIT
+ */
+angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.buttons","ui.bootstrap.bindHtml","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.progressbar","ui.bootstrap.pagination","ui.bootstrap.timepicker","ui.bootstrap.typeahead","ui.bootstrap.tabs","ui.bootstrap.popover","ui.bootstrap.tooltip","ui.bootstrap.modal","ui.bootstrap.dropdown","ui.bootstrap.rating","ui.bo [...]
+angular.module('ui.bootstrap.collapse', [])
+
+  .directive('collapse', ['$animate', function($animate) {
+    return {
+      link: function(scope, element, attrs) {
+        function expand() {
+          element.removeClass('collapse')
+            .addClass('collapsing')
+            .attr('aria-expanded', true)
+            .attr('aria-hidden', false);
+
+          $animate.addClass(element, 'in', {
+            to: { height: element[0].scrollHeight + 'px' }
+          }).then(expandDone);
+        }
+
+        function expandDone() {
+          element.removeClass('collapsing');
+          element.css({height: 'auto'});
+        }
+
+        function collapse() {
+          if (!element.hasClass('collapse') && !element.hasClass('in')) {
+            return collapseDone();
+          }
+
+          element
+            // IMPORTANT: The height must be set before adding "collapsing" class.
+            // Otherwise, the browser attempts to animate from height 0 (in
+            // collapsing class) to the given height here.
+            .css({height: element[0].scrollHeight + 'px'})
+            // initially all panel collapse have the collapse class, this removal
+            // prevents the animation from jumping to collapsed state
+            .removeClass('collapse')
+            .addClass('collapsing')
+            .attr('aria-expanded', false)
+            .attr('aria-hidden', true);
+
+          $animate.removeClass(element, 'in', {
+            to: {height: '0'}
+          }).then(collapseDone);
+        }
+
+        function collapseDone() {
+          element.css({height: '0'}); // Required so that collapse works when animation is disabled
+          element.removeClass('collapsing');
+          element.addClass('collapse');
+        }
+
+        scope.$watch(attrs.collapse, function(shouldCollapse) {
+          if (shouldCollapse) {
+            collapse();
+          } else {
+            expand();
+          }
+        });
+      }
+    };
+  }]);
+
+angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
+
+.constant('accordionConfig', {
+  closeOthers: true
+})
+
+.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function($scope, $attrs, accordionConfig) {
+  // This array keeps track of the accordion groups
+  this.groups = [];
+
+  // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
+  this.closeOthers = function(openGroup) {
+    var closeOthers = angular.isDefined($attrs.closeOthers) ?
+      $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
+    if (closeOthers) {
+      angular.forEach(this.groups, function(group) {
+        if (group !== openGroup) {
+          group.isOpen = false;
+        }
+      });
+    }
+  };
+
+  // This is called from the accordion-group directive to add itself to the accordion
+  this.addGroup = function(groupScope) {
+    var that = this;
+    this.groups.push(groupScope);
+
+    groupScope.$on('$destroy', function(event) {
+      that.removeGroup(groupScope);
+    });
+  };
+
+  // This is called from the accordion-group directive when to remove itself
+  this.removeGroup = function(group) {
+    var index = this.groups.indexOf(group);
+    if (index !== -1) {
+      this.groups.splice(index, 1);
+    }
+  };
+
+}])
+
+// The accordion directive simply sets up the directive controller
+// and adds an accordion CSS class to itself element.
+.directive('accordion', function() {
+  return {
+    restrict: 'EA',
+    controller: 'AccordionController',
+    controllerAs: 'accordion',
+    transclude: true,
+    replace: false,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/accordion/accordion.html';
+    }
+  };
+})
+
+// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
+.directive('accordionGroup', function() {
+  return {
+    require: '^accordion',         // We need this directive to be inside an accordion
+    restrict: 'EA',
+    transclude: true,              // It transcludes the contents of the directive into the template
+    replace: true,                // The element containing the directive will be replaced with the template
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/accordion/accordion-group.html';
+    },
+    scope: {
+      heading: '@',               // Interpolate the heading attribute onto this scope
+      isOpen: '=?',
+      isDisabled: '=?'
+    },
+    controller: function() {
+      this.setHeading = function(element) {
+        this.heading = element;
+      };
+    },
+    link: function(scope, element, attrs, accordionCtrl) {
+      accordionCtrl.addGroup(scope);
+
+      scope.openClass = attrs.openClass || 'panel-open';
+      scope.panelClass = attrs.panelClass;
+      scope.$watch('isOpen', function(value) {
+        element.toggleClass(scope.openClass, value);
+        if (value) {
+          accordionCtrl.closeOthers(scope);
+        }
+      });
+
+      scope.toggleOpen = function($event) {
+        if (!scope.isDisabled) {
+          if (!$event || $event.which === 32) {
+            scope.isOpen = !scope.isOpen;
+          }
+        }
+      };
+    }
+  };
+})
+
+// Use accordion-heading below an accordion-group to provide a heading containing HTML
+// <accordion-group>
+//   <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
+// </accordion-group>
+.directive('accordionHeading', function() {
+  return {
+    restrict: 'EA',
+    transclude: true,   // Grab the contents to be used as the heading
+    template: '',       // In effect remove this element!
+    replace: true,
+    require: '^accordionGroup',
+    link: function(scope, element, attr, accordionGroupCtrl, transclude) {
+      // Pass the heading to the accordion-group controller
+      // so that it can be transcluded into the right place in the template
+      // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
+      accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
+    }
+  };
+})
+
+// Use in the accordion-group template to indicate where you want the heading to be transcluded
+// You must provide the property on the accordion-group controller that will hold the transcluded element
+// <div class="accordion-group">
+//   <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
+//   ...
+// </div>
+.directive('accordionTransclude', function() {
+  return {
+    require: '^accordionGroup',
+    link: function(scope, element, attr, controller) {
+      scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
+        if (heading) {
+          element.find('span').html('');
+          element.find('span').append(heading);
+        }
+      });
+    }
+  };
+})
+
+;
+
+angular.module('ui.bootstrap.alert', [])
+
+.controller('AlertController', ['$scope', '$attrs', function($scope, $attrs) {
+  $scope.closeable = !!$attrs.close;
+  this.close = $scope.close;
+}])
+
+.directive('alert', function() {
+  return {
+    controller: 'AlertController',
+    controllerAs: 'alert',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/alert/alert.html';
+    },
+    transclude: true,
+    replace: true,
+    scope: {
+      type: '@',
+      close: '&'
+    }
+  };
+})
+
+.directive('dismissOnTimeout', ['$timeout', function($timeout) {
+  return {
+    require: 'alert',
+    link: function(scope, element, attrs, alertCtrl) {
+      $timeout(function() {
+        alertCtrl.close();
+      }, parseInt(attrs.dismissOnTimeout, 10));
+    }
+  };
+}]);
+
+/**
+* @ngdoc overview
+* @name ui.bootstrap.carousel
+*
+* @description
+* AngularJS version of an image carousel.
+*
+*/
+angular.module('ui.bootstrap.carousel', [])
+.controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) {
+  var self = this,
+    slides = self.slides = $scope.slides = [],
+    NEW_ANIMATE = angular.version.minor >= 4,
+    NO_TRANSITION = 'uib-noTransition',
+    SLIDE_DIRECTION = 'uib-slideDirection',
+    currentIndex = -1,
+    currentInterval, isPlaying;
+  self.currentSlide = null;
+
+  var destroyed = false;
+  /* direction: "prev" or "next" */
+  self.select = $scope.select = function(nextSlide, direction) {
+    var nextIndex = $scope.indexOfSlide(nextSlide);
+    //Decide direction if it's not given
+    if (direction === undefined) {
+      direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
+    }
+    //Prevent this user-triggered transition from occurring if there is already one in progress
+    if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) {
+      goNext(nextSlide, nextIndex, direction);
+    }
+  };
+
+  function goNext(slide, index, direction) {
+    // Scope has been destroyed, stop here.
+    if (destroyed) { return; }
+
+    angular.extend(slide, {direction: direction, active: true});
+    angular.extend(self.currentSlide || {}, {direction: direction, active: false});
+    if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition &&
+      slide.$element && self.slides.length > 1) {
+      slide.$element.data(SLIDE_DIRECTION, slide.direction);
+      if (self.currentSlide && self.currentSlide.$element) {
+        self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction);
+      }
+
+      $scope.$currentTransition = true;
+      if (NEW_ANIMATE) {
+        $animate.on('addClass', slide.$element, function (element, phase) {
+          if (phase === 'close') {
+            $scope.$currentTransition = null;
+            $animate.off('addClass', element);
+          }
+        });
+      } else {
+        slide.$element.one('$animate:close', function closeFn() {
+          $scope.$currentTransition = null;
+        });
+      }
+    }
+
+    self.currentSlide = slide;
+    currentIndex = index;
+
+    //every time you change slides, reset the timer
+    restartTimer();
+  }
+
+  $scope.$on('$destroy', function () {
+    destroyed = true;
+  });
+
+  function getSlideByIndex(index) {
+    if (angular.isUndefined(slides[index].index)) {
+      return slides[index];
+    }
+    var i, len = slides.length;
+    for (i = 0; i < slides.length; ++i) {
+      if (slides[i].index == index) {
+        return slides[i];
+      }
+    }
+  }
+
+  self.getCurrentIndex = function() {
+    if (self.currentSlide && angular.isDefined(self.currentSlide.index)) {
+      return +self.currentSlide.index;
+    }
+    return currentIndex;
+  };
+
+  /* Allow outside people to call indexOf on slides array */
+  $scope.indexOfSlide = function(slide) {
+    return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
+  };
+
+  $scope.next = function() {
+    var newIndex = (self.getCurrentIndex() + 1) % slides.length;
+
+    if (newIndex === 0 && $scope.noWrap()) {
+      $scope.pause();
+      return;
+    }
+
+    return self.select(getSlideByIndex(newIndex), 'next');
+  };
+
+  $scope.prev = function() {
+    var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
+
+    if ($scope.noWrap() && newIndex === slides.length - 1){
+      $scope.pause();
+      return;
+    }
+
+    return self.select(getSlideByIndex(newIndex), 'prev');
+  };
+
+  $scope.isActive = function(slide) {
+     return self.currentSlide === slide;
+  };
+
+  $scope.$watch('interval', restartTimer);
+  $scope.$on('$destroy', resetTimer);
+
+  function restartTimer() {
+    resetTimer();
+    var interval = +$scope.interval;
+    if (!isNaN(interval) && interval > 0) {
+      currentInterval = $interval(timerFn, interval);
+    }
+  }
+
+  function resetTimer() {
+    if (currentInterval) {
+      $interval.cancel(currentInterval);
+      currentInterval = null;
+    }
+  }
+
+  function timerFn() {
+    var interval = +$scope.interval;
+    if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
+      $scope.next();
+    } else {
+      $scope.pause();
+    }
+  }
+
+  $scope.play = function() {
+    if (!isPlaying) {
+      isPlaying = true;
+      restartTimer();
+    }
+  };
+  $scope.pause = function() {
+    if (!$scope.noPause) {
+      isPlaying = false;
+      resetTimer();
+    }
+  };
+
+  self.addSlide = function(slide, element) {
+    slide.$element = element;
+    slides.push(slide);
+    //if this is the first slide or the slide is set to active, select it
+    if(slides.length === 1 || slide.active) {
+      self.select(slides[slides.length-1]);
+      if (slides.length == 1) {
+        $scope.play();
+      }
+    } else {
+      slide.active = false;
+    }
+  };
+
+  self.removeSlide = function(slide) {
+    if (angular.isDefined(slide.index)) {
+      slides.sort(function(a, b) {
+        return +a.index > +b.index;
+      });
+    }
+    //get the index of the slide inside the carousel
+    var index = slides.indexOf(slide);
+    slides.splice(index, 1);
+    if (slides.length > 0 && slide.active) {
+      if (index >= slides.length) {
+        self.select(slides[index-1]);
+      } else {
+        self.select(slides[index]);
+      }
+    } else if (currentIndex > index) {
+      currentIndex--;
+    }
+    
+    //clean the currentSlide when no more slide
+    if (slides.length === 0) {
+      self.currentSlide = null;
+    }
+  };
+
+  $scope.$watch('noTransition', function(noTransition) {
+    $element.data(NO_TRANSITION, noTransition);
+  });
+
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.carousel.directive:carousel
+ * @restrict EA
+ *
+ * @description
+ * Carousel is the outer container for a set of image 'slides' to showcase.
+ *
+ * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
+ * @param {boolean=} noTransition Whether to disable transitions on the carousel.
+ * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <carousel>
+      <slide>
+        <img src="http://placekitten.com/150/150" style="margin:auto;">
+        <div class="carousel-caption">
+          <p>Beautiful!</p>
+        </div>
+      </slide>
+      <slide>
+        <img src="http://placekitten.com/100/150" style="margin:auto;">
+        <div class="carousel-caption">
+          <p>D'aww!</p>
+        </div>
+      </slide>
+    </carousel>
+  </file>
+  <file name="demo.css">
+    .carousel-indicators {
+      top: auto;
+      bottom: 15px;
+    }
+  </file>
+</example>
+ */
+.directive('carousel', [function() {
+  return {
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    controller: 'CarouselController',
+    controllerAs: 'carousel',
+    require: 'carousel',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/carousel/carousel.html';
+    },
+    scope: {
+      interval: '=',
+      noTransition: '=',
+      noPause: '=',
+      noWrap: '&'
+    }
+  };
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.carousel.directive:slide
+ * @restrict EA
+ *
+ * @description
+ * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}.  Must be placed as a child of a carousel element.
+ *
+ * @param {boolean=} active Model binding, whether or not this slide is currently active.
+ * @param {number=} index The index of the slide. The slides will be sorted by this parameter.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+<div ng-controller="CarouselDemoCtrl">
+  <carousel>
+    <slide ng-repeat="slide in slides" active="slide.active" index="$index">
+      <img ng-src="{{slide.image}}" style="margin:auto;">
+      <div class="carousel-caption">
+        <h4>Slide {{$index}}</h4>
+        <p>{{slide.text}}</p>
+      </div>
+    </slide>
+  </carousel>
+  Interval, in milliseconds: <input type="number" ng-model="myInterval">
+  <br />Enter a negative number to stop the interval.
+</div>
+  </file>
+  <file name="script.js">
+function CarouselDemoCtrl($scope) {
+  $scope.myInterval = 5000;
+}
+  </file>
+  <file name="demo.css">
+    .carousel-indicators {
+      top: auto;
+      bottom: 15px;
+    }
+  </file>
+</example>
+*/
+
+.directive('slide', function() {
+  return {
+    require: '^carousel',
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/carousel/slide.html';
+    },
+    scope: {
+      active: '=?',
+      actual: '=?',
+      index: '=?'
+    },
+    link: function (scope, element, attrs, carouselCtrl) {
+      carouselCtrl.addSlide(scope, element);
+      //when the scope is destroyed then remove the slide from the current slides array
+      scope.$on('$destroy', function() {
+        carouselCtrl.removeSlide(scope);
+      });
+
+      scope.$watch('active', function(active) {
+        if (active) {
+          carouselCtrl.select(scope);
+        }
+      });
+    }
+  };
+})
+
+.animation('.item', [
+         '$injector', '$animate',
+function ($injector, $animate) {
+  var NO_TRANSITION = 'uib-noTransition',
+    SLIDE_DIRECTION = 'uib-slideDirection',
+    $animateCss = null;
+
+  if ($injector.has('$animateCss')) {
+    $animateCss = $injector.get('$animateCss');
+  }
+
+  function removeClass(element, className, callback) {
+    element.removeClass(className);
+    if (callback) {
+      callback();
+    }
+  }
+
+  return {
+    beforeAddClass: function (element, className, done) {
+      // Due to transclusion, noTransition property is on parent's scope
+      if (className == 'active' && element.parent() &&
+          !element.parent().data(NO_TRANSITION)) {
+        var stopped = false;
+        var direction = element.data(SLIDE_DIRECTION);
+        var directionClass = direction == 'next' ? 'left' : 'right';
+        var removeClassFn = removeClass.bind(this, element,
+          directionClass + ' ' + direction, done);
+        element.addClass(direction);
+
+        if ($animateCss) {
+          $animateCss(element, {addClass: directionClass})
+            .start()
+            .done(removeClassFn);
+        } else {
+          $animate.addClass(element, directionClass).then(function () {
+            if (!stopped) {
+              removeClassFn();
+            }
+            done();
+          });
+        }
+
+        return function () {
+          stopped = true;
+        };
+      }
+      done();
+    },
+    beforeRemoveClass: function (element, className, done) {
+      // Due to transclusion, noTransition property is on parent's scope
+      if (className === 'active' && element.parent() &&
+          !element.parent().data(NO_TRANSITION)) {
+        var stopped = false;
+        var direction = element.data(SLIDE_DIRECTION);
+        var directionClass = direction == 'next' ? 'left' : 'right';
+        var removeClassFn = removeClass.bind(this, element, directionClass, done);
+
+        if ($animateCss) {
+          $animateCss(element, {addClass: directionClass})
+            .start()
+            .done(removeClassFn);
+        } else {
+          $animate.addClass(element, directionClass).then(function () {
+            if (!stopped) {
+              removeClassFn();
+            }
+            done();
+          });
+        }
+        return function () {
+          stopped = true;
+        };
+      }
+      done();
+    }
+  };
+
+}])
+
+
+;
+
+angular.module('ui.bootstrap.buttons', [])
+
+.constant('buttonConfig', {
+  activeClass: 'active',
+  toggleEvent: 'click'
+})
+
+.controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
+  this.activeClass = buttonConfig.activeClass || 'active';
+  this.toggleEvent = buttonConfig.toggleEvent || 'click';
+}])
+
+.directive('btnRadio', function() {
+  return {
+    require: ['btnRadio', 'ngModel'],
+    controller: 'ButtonsController',
+    controllerAs: 'buttons',
+    link: function(scope, element, attrs, ctrls) {
+      var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      element.find('input').css({display: 'none'});
+
+      //model -> UI
+      ngModelCtrl.$render = function() {
+        element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
+      };
+
+      //ui->model
+      element.bind(buttonsCtrl.toggleEvent, function() {
+        if (attrs.disabled) {
+          return;
+        }
+
+        var isActive = element.hasClass(buttonsCtrl.activeClass);
+
+        if (!isActive || angular.isDefined(attrs.uncheckable)) {
+          scope.$apply(function() {
+            ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
+            ngModelCtrl.$render();
+          });
+        }
+      });
+    }
+  };
+})
+
+.directive('btnCheckbox', ['$document', function($document) {
+  return {
+    require: ['btnCheckbox', 'ngModel'],
+    controller: 'ButtonsController',
+    controllerAs: 'button',
+    link: function(scope, element, attrs, ctrls) {
+      var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      element.find('input').css({display: 'none'});
+
+      function getTrueValue() {
+        return getCheckboxValue(attrs.btnCheckboxTrue, true);
+      }
+
+      function getFalseValue() {
+        return getCheckboxValue(attrs.btnCheckboxFalse, false);
+      }
+
+      function getCheckboxValue(attributeValue, defaultValue) {
+        var val = scope.$eval(attributeValue);
+        return angular.isDefined(val) ? val : defaultValue;
+      }
+
+      //model -> UI
+      ngModelCtrl.$render = function() {
+        element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
+      };
+
+      //ui->model
+      element.bind(buttonsCtrl.toggleEvent, function() {
+        if (attrs.disabled) {
+          return;
+        }
+
+        scope.$apply(function() {
+          ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
+          ngModelCtrl.$render();
+        });
+      });
+
+      //accessibility
+      element.on('keypress', function(e) {
+        if (attrs.disabled || e.which !== 32 || $document[0].activeElement !== element[0]) {
+          return;
+        }
+
+        scope.$apply(function() {
+          ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
+          ngModelCtrl.$render();
+        });
+      });
+    }
+  };
+}]);
+
+angular.module('ui.bootstrap.bindHtml', [])
+
+  .value('$bindHtmlUnsafeSuppressDeprecated', false)
+
+  .directive('bindHtmlUnsafe', ['$log', '$bindHtmlUnsafeSuppressDeprecated', function ($log, $bindHtmlUnsafeSuppressDeprecated) {
+    return function (scope, element, attr) {
+      if (!$bindHtmlUnsafeSuppressDeprecated) {
+        $log.warn('bindHtmlUnsafe is now deprecated. Use ngBindHtml instead');
+      }
+      element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
+      scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
+        element.html(value || '');
+      });
+    };
+  }]);
+angular.module('ui.bootstrap.dateparser', [])
+
+.service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) {
+  // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
+  var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
+
+  this.parsers = {};
+
+  var formatCodeToRegex = {
+    'yyyy': {
+      regex: '\\d{4}',
+      apply: function(value) { this.year = +value; }
+    },
+    'yy': {
+      regex: '\\d{2}',
+      apply: function(value) { this.year = +value + 2000; }
+    },
+    'y': {
+      regex: '\\d{1,4}',
+      apply: function(value) { this.year = +value; }
+    },
+    'MMMM': {
+      regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
+      apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
+    },
+    'MMM': {
+      regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
+      apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
+    },
+    'MM': {
+      regex: '0[1-9]|1[0-2]',
+      apply: function(value) { this.month = value - 1; }
+    },
+    'M': {
+      regex: '[1-9]|1[0-2]',
+      apply: function(value) { this.month = value - 1; }
+    },
+    'dd': {
+      regex: '[0-2][0-9]{1}|3[0-1]{1}',
+      apply: function(value) { this.date = +value; }
+    },
+    'd': {
+      regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
+      apply: function(value) { this.date = +value; }
+    },
+    'EEEE': {
+      regex: $locale.DATETIME_FORMATS.DAY.join('|')
+    },
+    'EEE': {
+      regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
+    },
+    'HH': {
+      regex: '(?:0|1)[0-9]|2[0-3]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'hh': {
+      regex: '0[0-9]|1[0-2]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'H': {
+      regex: '1?[0-9]|2[0-3]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'h': {
+      regex: '[0-9]|1[0-2]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'mm': {
+      regex: '[0-5][0-9]',
+      apply: function(value) { this.minutes = +value; }
+    },
+    'm': {
+      regex: '[0-9]|[1-5][0-9]',
+      apply: function(value) { this.minutes = +value; }
+    },
+    'sss': {
+      regex: '[0-9][0-9][0-9]',
+      apply: function(value) { this.milliseconds = +value; }
+    },
+    'ss': {
+      regex: '[0-5][0-9]',
+      apply: function(value) { this.seconds = +value; }
+    },
+    's': {
+      regex: '[0-9]|[1-5][0-9]',
+      apply: function(value) { this.seconds = +value; }
+    },
+    'a': {
+      regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
+      apply: function(value) {
+        if (this.hours === 12) {
+          this.hours = 0;
+        }
+
+        if (value === 'PM') {
+          this.hours += 12;
+        }
+      }
+    }
+  };
+
+  function createParser(format) {
+    var map = [], regex = format.split('');
+
+    angular.forEach(formatCodeToRegex, function(data, code) {
+      var index = format.indexOf(code);
+
+      if (index > -1) {
+        format = format.split('');
+
+        regex[index] = '(' + data.regex + ')';
+        format[index] = '$'; // Custom symbol to define consumed part of format
+        for (var i = index + 1, n = index + code.length; i < n; i++) {
+          regex[i] = '';
+          format[i] = '$';
+        }
+        format = format.join('');
+
+        map.push({ index: index, apply: data.apply });
+      }
+    });
+
+    return {
+      regex: new RegExp('^' + regex.join('') + '$'),
+      map: orderByFilter(map, 'index')
+    };
+  }
+
+  this.parse = function(input, format, baseDate) {
+    if (!angular.isString(input) || !format) {
+      return input;
+    }
+
+    format = $locale.DATETIME_FORMATS[format] || format;
+    format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
+
+    if (!this.parsers[format]) {
+      this.parsers[format] = createParser(format);
+    }
+
+    var parser = this.parsers[format],
+        regex = parser.regex,
+        map = parser.map,
+        results = input.match(regex);
+
+    if (results && results.length) {
+      var fields, dt;
+      if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
+        fields = {
+          year: baseDate.getFullYear(),
+          month: baseDate.getMonth(),
+          date: baseDate.getDate(),
+          hours: baseDate.getHours(),
+          minutes: baseDate.getMinutes(),
+          seconds: baseDate.getSeconds(),
+          milliseconds: baseDate.getMilliseconds()
+        };
+      } else {
+        if (baseDate) {
+          $log.warn('dateparser:', 'baseDate is not a valid date');
+        }
+        fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
+      }
+
+      for (var i = 1, n = results.length; i < n; i++) {
+        var mapper = map[i-1];
+        if (mapper.apply) {
+          mapper.apply.call(fields, results[i]);
+        }
+      }
+
+      if (isValid(fields.year, fields.month, fields.date)) {
+        dt = new Date(fields.year, fields.month, fields.date,
+          fields.hours, fields.minutes, fields.seconds,
+          fields.milliseconds || 0);
+      }
+
+      return dt;
+    }
+  };
+
+  // Check if date is valid for specific month (and year for February).
+  // Month: 0 = Jan, 1 = Feb, etc
+  function isValid(year, month, date) {
+    if (date < 1) {
+      return false;
+    }
+
+    if (month === 1 && date > 28) {
+      return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
+    }
+
+    if (month === 3 || month === 5 || month === 8 || month === 10) {
+      return date < 31;
+    }
+
+    return true;
+  }
+}]);
+
+angular.module('ui.bootstrap.position', [])
+
+/**
+ * A set of utility methods that can be use to retrieve position of DOM elements.
+ * It is meant to be used where we need to absolute-position DOM elements in
+ * relation to other, existing elements (this is the case for tooltips, popovers,
+ * typeahead suggestions etc.).
+ */
+  .factory('$position', ['$document', '$window', function($document, $window) {
+    function getStyle(el, cssprop) {
+      if (el.currentStyle) { //IE
+        return el.currentStyle[cssprop];
+      } else if ($window.getComputedStyle) {
+        return $window.getComputedStyle(el)[cssprop];
+      }
+      // finally try and get inline style
+      return el.style[cssprop];
+    }
+
+    /**
+     * Checks if a given element is statically positioned
+     * @param element - raw DOM element
+     */
+    function isStaticPositioned(element) {
+      return (getStyle(element, 'position') || 'static' ) === 'static';
+    }
+
+    /**
+     * returns the closest, non-statically positioned parentOffset of a given element
+     * @param element
+     */
+    var parentOffsetEl = function(element) {
+      var docDomEl = $document[0];
+      var offsetParent = element.offsetParent || docDomEl;
+      while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
+        offsetParent = offsetParent.offsetParent;
+      }
+      return offsetParent || docDomEl;
+    };
+
+    return {
+      /**
+       * Provides read-only equivalent of jQuery's position function:
+       * http://api.jquery.com/position/
+       */
+      position: function(element) {
+        var elBCR = this.offset(element);
+        var offsetParentBCR = { top: 0, left: 0 };
+        var offsetParentEl = parentOffsetEl(element[0]);
+        if (offsetParentEl != $document[0]) {
+          offsetParentBCR = this.offset(angular.element(offsetParentEl));
+          offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
+          offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+        }
+
+        var boundingClientRect = element[0].getBoundingClientRect();
+        return {
+          width: boundingClientRect.width || element.prop('offsetWidth'),
+          height: boundingClientRect.height || element.prop('offsetHeight'),
+          top: elBCR.top - offsetParentBCR.top,
+          left: elBCR.left - offsetParentBCR.left
+        };
+      },
+
+      /**
+       * Provides read-only equivalent of jQuery's offset function:
+       * http://api.jquery.com/offset/
+       */
+      offset: function(element) {
+        var boundingClientRect = element[0].getBoundingClientRect();
+        return {
+          width: boundingClientRect.width || element.prop('offsetWidth'),
+          height: boundingClientRect.height || element.prop('offsetHeight'),
+          top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
+          left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+        };
+      },
+
+      /**
+       * Provides coordinates for the targetEl in relation to hostEl
+       */
+      positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
+        var positionStrParts = positionStr.split('-');
+        var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
+
+        var hostElPos,
+          targetElWidth,
+          targetElHeight,
+          targetElPos;
+
+        hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
+
+        targetElWidth = targetEl.prop('offsetWidth');
+        targetElHeight = targetEl.prop('offsetHeight');
+
+        var shiftWidth = {
+          center: function() {
+            return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
+          },
+          left: function() {
+            return hostElPos.left;
+          },
+          right: function() {
+            return hostElPos.left + hostElPos.width;
+          }
+        };
+
+        var shiftHeight = {
+          center: function() {
+            return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
+          },
+          top: function() {
+            return hostElPos.top;
+          },
+          bottom: function() {
+            return hostElPos.top + hostElPos.height;
+          }
+        };
+
+        switch (pos0) {
+          case 'right':
+            targetElPos = {
+              top: shiftHeight[pos1](),
+              left: shiftWidth[pos0]()
+            };
+            break;
+          case 'left':
+            targetElPos = {
+              top: shiftHeight[pos1](),
+              left: hostElPos.left - targetElWidth
+            };
+            break;
+          case 'bottom':
+            targetElPos = {
+              top: shiftHeight[pos0](),
+              left: shiftWidth[pos1]()
+            };
+            break;
+          default:
+            targetElPos = {
+              top: hostElPos.top - targetElHeight,
+              left: shiftWidth[pos1]()
+            };
+            break;
+        }
+
+        return targetElPos;
+      }
+    };
+  }]);
+
+angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
+
+.value('$datepickerSuppressError', false)
+
+.constant('datepickerConfig', {
+  formatDay: 'dd',
+  formatMonth: 'MMMM',
+  formatYear: 'yyyy',
+  formatDayHeader: 'EEE',
+  formatDayTitle: 'MMMM yyyy',
+  formatMonthTitle: 'yyyy',
+  datepickerMode: 'day',
+  minMode: 'day',
+  maxMode: 'year',
+  showWeeks: true,
+  startingDay: 0,
+  yearRange: 20,
+  minDate: null,
+  maxDate: null,
+  shortcutPropagation: false
+})
+
+.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
+  var self = this,
+      ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
+
+  // Modes chain
+  this.modes = ['day', 'month', 'year'];
+
+  // Configuration attributes
+  angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
+                   'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
+    self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
+  });
+
+  // Watchable date attributes
+  angular.forEach(['minDate', 'maxDate'], function(key) {
+    if ($attrs[key]) {
+      $scope.$parent.$watch($parse($attrs[key]), function(value) {
+        self[key] = value ? new Date(value) : null;
+        self.refreshView();
+      });
+    } else {
+      self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
+    }
+  });
+
+  angular.forEach(['minMode', 'maxMode'], function(key) {
+    if ($attrs[key]) {
+      $scope.$parent.$watch($parse($attrs[key]), function(value) {
+        self[key] = angular.isDefined(value) ? value : $attrs[key];
+        $scope[key] = self[key];
+        if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) {
+          $scope.datepickerMode = self[key];
+        }
+      });
+    } else {
+      self[key] = datepickerConfig[key] || null;
+      $scope[key] = self[key];
+    }
+  });
+
+  $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
+  $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
+
+  if (angular.isDefined($attrs.initDate)) {
+    this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
+    $scope.$parent.$watch($attrs.initDate, function(initDate) {
+      if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+        self.activeDate = initDate;
+        self.refreshView();
+      }
+    });
+  } else {
+    this.activeDate = new Date();
+  }
+
+  $scope.isActive = function(dateObject) {
+    if (self.compare(dateObject.date, self.activeDate) === 0) {
+      $scope.activeDateId = dateObject.uid;
+      return true;
+    }
+    return false;
+  };
+
+  this.init = function(ngModelCtrl_) {
+    ngModelCtrl = ngModelCtrl_;
+
+    ngModelCtrl.$render = function() {
+      self.render();
+    };
+  };
+
+  this.render = function() {
+    if (ngModelCtrl.$viewValue) {
+      var date = new Date(ngModelCtrl.$viewValue),
+          isValid = !isNaN(date);
+
+      if (isValid) {
+        this.activeDate = date;
+      } else if (!$datepickerSuppressError) {
+        $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+      }
+    }
+    this.refreshView();
+  };
+
+  this.refreshView = function() {
+    if (this.element) {
+      this._refreshView();
+
+      var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+      ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
+    }
+  };
+
+  this.createDateObject = function(date, format) {
+    var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+    return {
+      date: date,
+      label: dateFilter(date, format),
+      selected: model && this.compare(date, model) === 0,
+      disabled: this.isDisabled(date),
+      current: this.compare(date, new Date()) === 0,
+      customClass: this.customClass(date)
+    };
+  };
+
+  this.isDisabled = function(date) {
+    return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode})));
+  };
+
+  this.customClass = function(date) {
+    return $scope.customClass({date: date, mode: $scope.datepickerMode});
+  };
+
+  // Split array into smaller arrays
+  this.split = function(arr, size) {
+    var arrays = [];
+    while (arr.length > 0) {
+      arrays.push(arr.splice(0, size));
+    }
+    return arrays;
+  };
+
+  // Fix a hard-reprodusible bug with timezones
+  // The bug depends on OS, browser, current timezone and current date
+  // i.e.
+  // var date = new Date(2014, 0, 1);
+  // console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours());
+  // can result in "2013 11 31 23" because of the bug.
+  this.fixTimeZone = function(date) {
+    var hours = date.getHours();
+    date.setHours(hours === 23 ? hours + 2 : 0);
+  };
+
+  $scope.select = function(date) {
+    if ($scope.datepickerMode === self.minMode) {
+      var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
+      dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+      ngModelCtrl.$setViewValue(dt);
+      ngModelCtrl.$render();
+    } else {
+      self.activeDate = date;
+      $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
+    }
+  };
+
+  $scope.move = function(direction) {
+    var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
+        month = self.activeDate.getMonth() + direction * (self.step.months || 0);
+    self.activeDate.setFullYear(year, month, 1);
+    self.refreshView();
+  };
+
+  $scope.toggleMode = function(direction) {
+    direction = direction || 1;
+
+    if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
+      return;
+    }
+
+    $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
+  };
+
+  // Key event mapper
+  $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
+
+  var focusElement = function() {
+    self.element[0].focus();
+  };
+
+  // Listen for focus requests from popup directive
+  $scope.$on('datepicker.focus', focusElement);
+
+  $scope.keydown = function(evt) {
+    var key = $scope.keys[evt.which];
+
+    if (!key || evt.shiftKey || evt.altKey) {
+      return;
+    }
+
+    evt.preventDefault();
+    if (!self.shortcutPropagation) {
+      evt.stopPropagation();
+    }
+
+    if (key === 'enter' || key === 'space') {
+      if (self.isDisabled(self.activeDate)) {
+        return; // do nothing
+      }
+      $scope.select(self.activeDate);
+      focusElement();
+    } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
+      $scope.toggleMode(key === 'up' ? 1 : -1);
+      focusElement();
+    } else {
+      self.handleKeyDown(key, evt);
+      self.refreshView();
+    }
+  };
+}])
+
+.directive('datepicker', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/datepicker/datepicker.html';
+    },
+    scope: {
+      datepickerMode: '=?',
+      dateDisabled: '&',
+      customClass: '&',
+      shortcutPropagation: '&?'
+    },
+    require: ['datepicker', '^ngModel'],
+    controller: 'DatepickerController',
+    controllerAs: 'datepicker',
+    link: function(scope, element, attrs, ctrls) {
+      var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      datepickerCtrl.init(ngModelCtrl);
+    }
+  };
+})
+
+.directive('daypicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/day.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      scope.showWeeks = ctrl.showWeeks;
+
+      ctrl.step = { months: 1 };
+      ctrl.element = element;
+
+      var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+      function getDaysInMonth(year, month) {
+        return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
+      }
+
+      function getDates(startDate, n) {
+        var dates = new Array(n), current = new Date(startDate), i = 0, date;
+        while (i < n) {
+          date = new Date(current);
+          ctrl.fixTimeZone(date);
+          dates[i++] = date;
+          current.setDate(current.getDate() + 1);
+        }
+        return dates;
+      }
+
+      ctrl._refreshView = function() {
+        var year = ctrl.activeDate.getFullYear(),
+          month = ctrl.activeDate.getMonth(),
+          firstDayOfMonth = new Date(year, month, 1),
+          difference = ctrl.startingDay - firstDayOfMonth.getDay(),
+          numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
+          firstDate = new Date(firstDayOfMonth);
+
+        if (numDisplayedFromPreviousMonth > 0) {
+          firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
+        }
+
+        // 42 is the number of days on a six-month calendar
+        var days = getDates(firstDate, 42);
+        for (var i = 0; i < 42; i ++) {
+          days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
+            secondary: days[i].getMonth() !== month,
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.labels = new Array(7);
+        for (var j = 0; j < 7; j++) {
+          scope.labels[j] = {
+            abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
+            full: dateFilter(days[j].date, 'EEEE')
+          };
+        }
+
+        scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
+        scope.rows = ctrl.split(days, 7);
+
+        if (scope.showWeeks) {
+          scope.weekNumbers = [];
+          var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
+              numWeeks = scope.rows.length;
+          for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
+            scope.weekNumbers.push(
+              getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
+          }
+        }
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
+      };
+
+      function getISO8601WeekNumber(date) {
+        var checkDate = new Date(date);
+        checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
+        var time = checkDate.getTime();
+        checkDate.setMonth(0); // Compare with Jan 1
+        checkDate.setDate(1);
+        return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+      }
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getDate();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 7;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 7;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
+          ctrl.activeDate.setMonth(month, 1);
+          date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
+        } else if (key === 'home') {
+          date = 1;
+        } else if (key === 'end') {
+          date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
+        }
+        ctrl.activeDate.setDate(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.directive('monthpicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/month.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      ctrl.step = { years: 1 };
+      ctrl.element = element;
+
+      ctrl._refreshView = function() {
+        var months = new Array(12),
+            year = ctrl.activeDate.getFullYear(),
+            date;
+
+        for (var i = 0; i < 12; i++) {
+          date = new Date(year, i, 1);
+          ctrl.fixTimeZone(date);
+          months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), {
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
+        scope.rows = ctrl.split(months, 3);
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth());
+      };
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getMonth();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 3;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 3;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
+          ctrl.activeDate.setFullYear(year);
+        } else if (key === 'home') {
+          date = 0;
+        } else if (key === 'end') {
+          date = 11;
+        }
+        ctrl.activeDate.setMonth(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.directive('yearpicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/year.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      var range = ctrl.yearRange;
+
+      ctrl.step = { years: range };
+      ctrl.element = element;
+
+      function getStartingYear( year ) {
+        return parseInt((year - 1) / range, 10) * range + 1;
+      }
+
+      ctrl._refreshView = function() {
+        var years = new Array(range), date;
+
+        for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) {
+          date = new Date(start + i, 0, 1);
+          ctrl.fixTimeZone(date);
+          years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), {
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.title = [years[0].label, years[range - 1].label].join(' - ');
+        scope.rows = ctrl.split(years, 5);
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return date1.getFullYear() - date2.getFullYear();
+      };
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getFullYear();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 5;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 5;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
+        } else if (key === 'home') {
+          date = getStartingYear(ctrl.activeDate.getFullYear());
+        } else if (key === 'end') {
+          date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1;
+        }
+        ctrl.activeDate.setFullYear(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.constant('datepickerPopupConfig', {
+  datepickerPopup: 'yyyy-MM-dd',
+  datepickerPopupTemplateUrl: 'template/datepicker/popup.html',
+  datepickerTemplateUrl: 'template/datepicker/datepicker.html',
+  html5Types: {
+    date: 'yyyy-MM-dd',
+    'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
+    'month': 'yyyy-MM'
+  },
+  currentText: 'Today',
+  clearText: 'Clear',
+  closeText: 'Done',
+  closeOnDateSelection: true,
+  appendToBody: false,
+  showButtonBar: true,
+  onOpenFocus: true
+})
+
+.directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout',
+function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
+  return {
+    restrict: 'EA',
+    require: 'ngModel',
+    scope: {
+      isOpen: '=?',
+      currentText: '@',
+      clearText: '@',
+      closeText: '@',
+      dateDisabled: '&',
+      customClass: '&'
+    },
+    link: function(scope, element, attrs, ngModel) {
+      var dateFormat,
+          closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
+          appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody,
+          onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus,
+          datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl,
+          datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl,
+          cache = {};
+
+      scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
+
+      scope.getText = function(key) {
+        return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
+      };
+
+      scope.isDisabled = function(date) {
+        if (date === 'today') {
+          date = new Date();
+        }
+
+        return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) ||
+          (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0));
+      };
+
+      scope.compare = function(date1, date2) {
+        return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
+      };
+
+      var isHtml5DateInput = false;
+      if (datepickerPopupConfig.html5Types[attrs.type]) {
+        dateFormat = datepickerPopupConfig.html5Types[attrs.type];
+        isHtml5DateInput = true;
+      } else {
+        dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup;
+        attrs.$observe('datepickerPopup', function(value, oldValue) {
+            var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
+            // Invalidate the $modelValue to ensure that formatters re-run
+            // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
+            if (newDateFormat !== dateFormat) {
+              dateFormat = newDateFormat;
+              ngModel.$modelValue = null;
+
+              if (!dateFormat) {
+                throw new Error('datepickerPopup must have a date format specified.');
+              }
+            }
+        });
+      }
+
+      if (!dateFormat) {
+        throw new Error('datepickerPopup must have a date format specified.');
+      }
+
+      if (isHtml5DateInput && attrs.datepickerPopup) {
+        throw new Error('HTML5 date input types do not support custom formats.');
+      }
+
+      // popup element used to display calendar
+      var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
+      popupEl.attr({
+        'ng-model': 'date',
+        'ng-change': 'dateSelection(date)',
+        'template-url': datepickerPopupTemplateUrl
+      });
+
+      function cameltoDash(string) {
+        return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
+      }
+
+      // datepicker element
+      var datepickerEl = angular.element(popupEl.children()[0]);
+      datepickerEl.attr('template-url', datepickerTemplateUrl);
+
+      if (isHtml5DateInput) {
+        if (attrs.type === 'month') {
+          datepickerEl.attr('datepicker-mode', '"month"');
+          datepickerEl.attr('min-mode', 'month');
+        }
+      }
+
+      if (attrs.datepickerOptions) {
+        var options = scope.$parent.$eval(attrs.datepickerOptions);
+        if (options && options.initDate) {
+          scope.initDate = options.initDate;
+          datepickerEl.attr('init-date', 'initDate');
+          delete options.initDate;
+        }
+        angular.forEach(options, function(value, option) {
+          datepickerEl.attr( cameltoDash(option), value );
+        });
+      }
+
+      scope.watchData = {};
+      angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
+        if (attrs[key]) {
+          var getAttribute = $parse(attrs[key]);
+          scope.$parent.$watch(getAttribute, function(value) {
+            scope.watchData[key] = value;
+            if (key === 'minDate' || key === 'maxDate') {
+              cache[key] = new Date(value);
+            }
+          });
+          datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
+
+          // Propagate changes from datepicker to outside
+          if (key === 'datepickerMode') {
+            var setAttribute = getAttribute.assign;
+            scope.$watch('watchData.' + key, function(value, oldvalue) {
+              if (angular.isFunction(setAttribute) && value !== oldvalue) {
+                setAttribute(scope.$parent, value);
+              }
+            });
+          }
+        }
+      });
+      if (attrs.dateDisabled) {
+        datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
+      }
+
+      if (attrs.showWeeks) {
+        datepickerEl.attr('show-weeks', attrs.showWeeks);
+      }
+
+      if (attrs.customClass) {
+        datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
+      }
+
+      function parseDate(viewValue) {
+        if (angular.isNumber(viewValue)) {
+          // presumably timestamp to date object
+          viewValue = new Date(viewValue);
+        }
+
+        if (!viewValue) {
+          return null;
+        } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
+          return viewValue;
+        } else if (angular.isString(viewValue)) {
+          var date = dateParser.parse(viewValue, dateFormat, scope.date);
+          if (isNaN(date)) {
+            return undefined;
+          } else {
+            return date;
+          }
+        } else {
+          return undefined;
+        }
+      }
+
+      function validator(modelValue, viewValue) {
+        var value = modelValue || viewValue;
+
+        if (!attrs.ngRequired && !value) {
+          return true;
+        }
+
+        if (angular.isNumber(value)) {
+          value = new Date(value);
+        }
+        if (!value) {
+          return true;
+        } else if (angular.isDate(value) && !isNaN(value)) {
+          return true;
+        } else if (angular.isString(value)) {
+          var date = dateParser.parse(value, dateFormat);
+          return !isNaN(date);
+        } else {
+          return false;
+        }
+      }
+
+      if (!isHtml5DateInput) {
+        // Internal API to maintain the correct ng-invalid-[key] class
+        ngModel.$$parserName = 'date';
+        ngModel.$validators.date = validator;
+        ngModel.$parsers.unshift(parseDate);
+        ngModel.$formatters.push(function(value) {
+          scope.date = value;
+          return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
+        });
+      } else {
+        ngModel.$formatters.push(function(value) {
+          scope.date = value;
+          return value;
+        });
+      }
+
+      // Inner change
+      scope.dateSelection = function(dt) {
+        if (angular.isDefined(dt)) {
+          scope.date = dt;
+        }
+        var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
+        element.val(date);
+        ngModel.$setViewValue(date);
+
+        if (closeOnDateSelection) {
+          scope.isOpen = false;
+          element[0].focus();
+        }
+      };
+
+      // Detect changes in the view from the text box
+      ngModel.$viewChangeListeners.push(function() {
+        scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
+      });
+
+      var documentClickBind = function(event) {
+        if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) {
+          scope.$apply(function() {
+            scope.isOpen = false;
+          });
+        }
+      };
+
+      var inputKeydownBind = function(evt) {
+        if (evt.which === 27 && scope.isOpen) {
+          evt.preventDefault();
+          evt.stopPropagation();
+          scope.$apply(function() {
+            scope.isOpen = false;
+          });
+          element[0].focus();
+        } else if (evt.which === 40 && !scope.isOpen) {
+          evt.preventDefault();
+          evt.stopPropagation();
+          scope.$apply(function() {
+            scope.isOpen = true;
+          });
+        }
+      };
+      element.bind('keydown', inputKeydownBind);
+
+      scope.keydown = function(evt) {
+        if (evt.which === 27) {
+          scope.isOpen = false;
+          element[0].focus();
+        }
+      };
+
+      scope.$watch('isOpen', function(value) {
+        if (value) {
+          scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+          scope.position.top = scope.position.top + element.prop('offsetHeight');
+
+          $timeout(function() {
+            if (onOpenFocus) {
+              scope.$broadcast('datepicker.focus');
+            }
+            $document.bind('click', documentClickBind);
+          }, 0, false);
+        } else {
+          $document.unbind('click', documentClickBind);
+        }
+      });
+
+      scope.select = function(date) {
+        if (date === 'today') {
+          var today = new Date();
+          if (angular.isDate(scope.date)) {
+            date = new Date(scope.date);
+            date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
+          } else {
+            date = new Date(today.setHours(0, 0, 0, 0));
+          }
+        }
+        scope.dateSelection(date);
+      };
+
+      scope.close = function() {
+        scope.isOpen = false;
+        element[0].focus();
+      };
+
+      var $popup = $compile(popupEl)(scope);
+      // Prevent jQuery cache memory leak (template is now redundant after linking)
+      popupEl.remove();
+
+      if (appendToBody) {
+        $document.find('body').append($popup);
+      } else {
+        element.after($popup);
+      }
+
+      scope.$on('$destroy', function() {
+        if (scope.isOpen === true) {
+          if (!$rootScope.$$phase) {
+            scope.$apply(function() {
+              scope.isOpen = false;
+            });
+          }
+        }
+
+        $popup.remove();
+        element.unbind('keydown', inputKeydownBind);
+        $document.unbind('click', documentClickBind);
+      });
+    }
+  };
+}])
+
+.directive('datepickerPopupWrap', function() {
+  return {
+    restrict:'EA',
+    replace: true,
+    transclude: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/datepicker/popup.html';
+    }
+  };
+});
+
+angular.module('ui.bootstrap.progressbar', [])
+
+.constant('progressConfig', {
+  animate: true,
+  max: 100
+})
+
+.value('$progressSuppressWarning', false)
+
+.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
+  var self = this,
+      animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
+
+  this.bars = [];
+  $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
+
+  this.addBar = function(bar, element) {
+    if (!animate) {
+      element.css({'transition': 'none'});
+    }
+
+    this.bars.push(bar);
+
+    bar.max = $scope.max;
+
+    bar.$watch('value', function(value) {
+      bar.recalculatePercentage();
+    });
+
+    bar.recalculatePercentage = function() {
+      bar.percent = +(100 * bar.value / bar.max).toFixed(2);
+
+      var totalPercentage = self.bars.reduce(function(total, bar) {
+        return total + bar.percent;
+      }, 0);
+
+      if (totalPercentage > 100) {
+        bar.percent -= totalPercentage - 100;
+      }
+    };
+
+    bar.$on('$destroy', function() {
+      element = null;
+      self.removeBar(bar);
+    });
+  };
+
+  this.removeBar = function(bar) {
+      this.bars.splice(this.bars.indexOf(bar), 1);
+  };
+
+  $scope.$watch('max', function(max) {
+    self.bars.forEach(function(bar) {
+      bar.max = $scope.max;
+      bar.recalculatePercentage();
+    });
+  });
+}])
+
+.directive('uibProgress', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    require: 'uibProgress',
+    scope: {
+      max: '=?'
+    },
+    templateUrl: 'template/progressbar/progress.html'
+  };
+})
+
+.directive('progress', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    require: 'progress',
+    scope: {
+      max: '=?'
+    },
+    templateUrl: 'template/progressbar/progress.html',
+    link: function() {
+      if ($progressSuppressWarning) {
+        $log.warn('progress is now deprecated. Use uib-progress instead');
+      }
+    }
+  };
+}])
+
+.directive('uibBar', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    require: '^uibProgress',
+    scope: {
+      value: '=',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/bar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      progressCtrl.addBar(scope, element);
+    }
+  };
+})
+
+.directive('bar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    require: '^progress',
+    scope: {
+      value: '=',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/bar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      if ($progressSuppressWarning) {
+        $log.warn('bar is now deprecated. Use uib-bar instead');
+      }
+      progressCtrl.addBar(scope, element);
+    }
+  };
+}])
+
+.directive('progressbar', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    scope: {
+      value: '=',
+      max: '=?',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/progressbar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      progressCtrl.addBar(scope, angular.element(element.children()[0]));
+    }
+  };
+});
+
+angular.module('ui.bootstrap.pagination', [])
+.controller('PaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) {
+  var self = this,
+      ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
+      setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
+
+  this.init = function(ngModelCtrl_, config) {
+    ngModelCtrl = ngModelCtrl_;
+    this.config = config;
+
+    ngModelCtrl.$render = function() {
+      self.render();
+    };
+
+    if ($attrs.itemsPerPage) {
+      $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
+        self.itemsPerPage = parseInt(value, 10);
+        $scope.totalPages = self.calculateTotalPages();
+      });
+    } else {
+      this.itemsPerPage = config.itemsPerPage;
+    }
+
+    $scope.$watch('totalItems', function() {
+      $scope.totalPages = self.calculateTotalPages();
+    });
+
+    $scope.$watch('totalPages', function(value) {
+      setNumPages($scope.$parent, value); // Readonly variable
+
+      if ( $scope.page > value ) {
+        $scope.selectPage(value);
+      } else {
+        ngModelCtrl.$render();
+      }
+    });
+  };
+
+  this.calculateTotalPages = function() {
+    var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
+    return Math.max(totalPages || 0, 1);
+  };
+
+  this.render = function() {
+    $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
+  };
+
+  $scope.selectPage = function(page, evt) {
+    if (evt) {
+      evt.preventDefault();
+    }
+
+    var clickAllowed = !$scope.ngDisabled || !evt;
+    if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
+      if (evt && evt.target) {
+        evt.target.blur();
+      }
+      ngModelCtrl.$setViewValue(page);
+      ngModelCtrl.$render();
+    }
+  };
+
+  $scope.getText = function(key) {
+    return $scope[key + 'Text'] || self.config[key + 'Text'];
+  };
+
+  $scope.noPrevious = function() {
+    return $scope.page === 1;
+  };
+
+  $scope.noNext = function() {
+    return $scope.page === $scope.totalPages;
+  };
+}])
+
+.constant('paginationConfig', {
+  itemsPerPage: 10,
+  boundaryLinks: false,
+  directionLinks: true,
+  firstText: 'First',
+  previousText: 'Previous',
+  nextText: 'Next',
+  lastText: 'Last',
+  rotate: true
+})
+
+.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) {
+  return {
+    restrict: 'EA',
+    scope: {
+      totalItems: '=',
+      firstText: '@',
+      previousText: '@',
+      nextText: '@',
+      lastText: '@',
+      ngDisabled:'='
+    },
+    require: ['pagination', '?ngModel'],
+    controller: 'PaginationController',
+    controllerAs: 'pagination',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/pagination/pagination.html';
+    },
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (!ngModelCtrl) {
+         return; // do nothing if no ng-model
+      }
+
+      // Setup configuration parameters
+      var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
+          rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
+      scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
+      scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
+
+      paginationCtrl.init(ngModelCtrl, paginationConfig);
+
+      if (attrs.maxSize) {
+        scope.$parent.$watch($parse(attrs.maxSize), function(value) {
+          maxSize = parseInt(value, 10);
+          paginationCtrl.render();
+        });
+      }
+
+      // Create page object used in template
+      function makePage(number, text, isActive) {
+        return {
+          number: number,
+          text: text,
+          active: isActive
+        };
+      }
+
+      function getPages(currentPage, totalPages) {
+        var pages = [];
+
+        // Default page limits
+        var startPage = 1, endPage = totalPages;
+        var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
+
+        // recompute if maxSize
+        if (isMaxSized) {
+          if (rotate) {
+            // Current page is displayed in the middle of the visible ones
+            startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
+            endPage   = startPage + maxSize - 1;
+
+            // Adjust if limit is exceeded
+            if (endPage > totalPages) {
+              endPage   = totalPages;
+              startPage = endPage - maxSize + 1;
+            }
+          } else {
+            // Visible pages are paginated with maxSize
+            startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
+
+            // Adjust last page if limit is exceeded
+            endPage = Math.min(startPage + maxSize - 1, totalPages);
+          }
+        }
+
+        // Add page number links
+        for (var number = startPage; number <= endPage; number++) {
+          var page = makePage(number, number, number === currentPage);
+          pages.push(page);
+        }
+
+        // Add links to move between page sets
+        if (isMaxSized && ! rotate) {
+          if (startPage > 1) {
+            var previousPageSet = makePage(startPage - 1, '...', false);
+            pages.unshift(previousPageSet);
+          }
+
+          if (endPage < totalPages) {
+            var nextPageSet = makePage(endPage + 1, '...', false);
+            pages.push(nextPageSet);
+          }
+        }
+
+        return pages;
+      }
+
+      var originalRender = paginationCtrl.render;
+      paginationCtrl.render = function() {
+        originalRender();
+        if (scope.page > 0 && scope.page <= scope.totalPages) {
+          scope.pages = getPages(scope.page, scope.totalPages);
+        }
+      };
+    }
+  };
+}])
+
+.constant('pagerConfig', {
+  itemsPerPage: 10,
+  previousText: '« Previous',
+  nextText: 'Next »',
+  align: true
+})
+
+.directive('pager', ['pagerConfig', function(pagerConfig) {
+  return {
+    restrict: 'EA',
+    scope: {
+      totalItems: '=',
+      previousText: '@',
+      nextText: '@',
+      ngDisabled: '='
+    },
+    require: ['pager', '?ngModel'],
+    controller: 'PaginationController',
+    controllerAs: 'pagination',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/pagination/pager.html';
+    },
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (!ngModelCtrl) {
+         return; // do nothing if no ng-model
+      }
+
+      scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
+      paginationCtrl.init(ngModelCtrl, pagerConfig);
+    }
+  };
+}]);
+
+angular.module('ui.bootstrap.timepicker', [])
+
+.constant('timepickerConfig', {
+  hourStep: 1,
+  minuteStep: 1,
+  showMeridian: true,
+  meridians: null,
+  readonlyInput: false,
+  mousewheel: true,
+  arrowkeys: true,
+  showSpinners: true
+})
+
+.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
+  var selected = new Date(),
+      ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
+      meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
+
+  this.init = function(ngModelCtrl_, inputs) {
+    ngModelCtrl = ngModelCtrl_;
+    ngModelCtrl.$render = this.render;
+
+    ngModelCtrl.$formatters.unshift(function(modelValue) {
+      return modelValue ? new Date(modelValue) : null;
+    });
+
+    var hoursInputEl = inputs.eq(0),
+        minutesInputEl = inputs.eq(1);
+
+    var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
+    if (mousewheel) {
+      this.setupMousewheelEvents(hoursInputEl, minutesInputEl);
+    }
+
+    var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
+    if (arrowkeys) {
+      this.setupArrowkeyEvents(hoursInputEl, minutesInputEl);
+    }
+
+    $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
+    this.setupInputEvents(hoursInputEl, minutesInputEl);
+  };
+
+  var hourStep = timepickerConfig.hourStep;
+  if ($attrs.hourStep) {
+    $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
+      hourStep = parseInt(value, 10);
+    });
+  }
+
+  var minuteStep = timepickerConfig.minuteStep;
+  if ($attrs.minuteStep) {
+    $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
+      minuteStep = parseInt(value, 10);
+    });
+  }
+
+  var min;
+  $scope.$parent.$watch($parse($attrs.min), function(value) {
+    var dt = new Date(value);
+    min = isNaN(dt) ? undefined : dt;
+  });
+
+  var max;
+  $scope.$parent.$watch($parse($attrs.max), function(value) {
+    var dt = new Date(value);
+    max = isNaN(dt) ? undefined : dt;
+  });
+
+  $scope.noIncrementHours = function() {
+    var incrementedSelected = addMinutes(selected, hourStep * 60);
+    return incrementedSelected > max ||
+      (incrementedSelected < selected && incrementedSelected < min);
+  };
+
+  $scope.noDecrementHours = function() {
+    var decrementedSelected = addMinutes(selected, -hourStep * 60);
+    return decrementedSelected < min ||
+      (decrementedSelected > selected && decrementedSelected > max);
+  };
+
+  $scope.noIncrementMinutes = function() {
+    var incrementedSelected = addMinutes(selected, minuteStep);
+    return incrementedSelected > max ||
+      (incrementedSelected < selected && incrementedSelected < min);
+  };
+
+  $scope.noDecrementMinutes = function() {
+    var decrementedSelected = addMinutes(selected, -minuteStep);
+    return decrementedSelected < min ||
+      (decrementedSelected > selected && decrementedSelected > max);
+  };
+
+  $scope.noToggleMeridian = function() {
+    if (selected.getHours() < 13) {
+      return addMinutes(selected, 12 * 60) > max;
+    } else {
+      return addMinutes(selected, -12 * 60) < min;
+    }
+  };
+
+  // 12H / 24H mode
+  $scope.showMeridian = timepickerConfig.showMeridian;
+  if ($attrs.showMeridian) {
+    $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
+      $scope.showMeridian = !!value;
+
+      if (ngModelCtrl.$error.time) {
+        // Evaluate from template
+        var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
+        if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+          selected.setHours(hours);
+          refresh();
+        }
+      } else {
+        updateTemplate();
+      }
+    });
+  }
+
+  // Get $scope.hours in 24H mode if valid
+  function getHoursFromTemplate() {
+    var hours = parseInt($scope.hours, 10);
+    var valid = $scope.showMeridian ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
+    if (!valid) {
+      return undefined;
+    }
+
+    if ($scope.showMeridian) {
+      if (hours === 12) {
+        hours = 0;
+      }
+      if ($scope.meridian === meridians[1]) {
+        hours = hours + 12;
+      }
+    }
+    return hours;
+  }
+
+  function getMinutesFromTemplate() {
+    var minutes = parseInt($scope.minutes, 10);
+    return (minutes >= 0 && minutes < 60) ? minutes : undefined;
+  }
+
+  function pad(value) {
+    return (angular.isDefined(value) && value.toString().length < 2) ? '0' + value : value.toString();
+  }
+
+  // Respond on mousewheel spin
+  this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl) {
+    var isScrollingUp = function(e) {
+      if (e.originalEvent) {
+        e = e.originalEvent;
+      }
+      //pick correct delta variable depending on event
+      var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
+      return (e.detail || delta > 0);
+    };
+
+    hoursInputEl.bind('mousewheel wheel', function(e) {
+      $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
+      e.preventDefault();
+    });
+
+    minutesInputEl.bind('mousewheel wheel', function(e) {
+      $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
+      e.preventDefault();
+    });
+
+  };
+
+  // Respond on up/down arrowkeys
+  this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl) {
+    hoursInputEl.bind('keydown', function(e) {
+      if (e.which === 38) { // up
+        e.preventDefault();
+        $scope.incrementHours();
+        $scope.$apply();
+      } else if (e.which === 40) { // down
+        e.preventDefault();
+        $scope.decrementHours();
+        $scope.$apply();
+      }
+    });
+
+    minutesInputEl.bind('keydown', function(e) {
+      if (e.which === 38) { // up
+        e.preventDefault();
+        $scope.incrementMinutes();
+        $scope.$apply();
+      } else if (e.which === 40) { // down
+        e.preventDefault();
+        $scope.decrementMinutes();
+        $scope.$apply();
+      }
+    });
+  };
+
+  this.setupInputEvents = function(hoursInputEl, minutesInputEl) {
+    if ($scope.readonlyInput) {
+      $scope.updateHours = angular.noop;
+      $scope.updateMinutes = angular.noop;
+      return;
+    }
+
+    var invalidate = function(invalidHours, invalidMinutes) {
+      ngModelCtrl.$setViewValue(null);
+      ngModelCtrl.$setValidity('time', false);
+      if (angular.isDefined(invalidHours)) {
+        $scope.invalidHours = invalidHours;
+      }
+      if (angular.isDefined(invalidMinutes)) {
+        $scope.invalidMinutes = invalidMinutes;
+      }
+    };
+
+    $scope.updateHours = function() {
+      var hours = getHoursFromTemplate(),
+        minutes = getMinutesFromTemplate();
+
+      if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+        selected.setHours(hours);
+        if (selected < min || selected > max) {
+          invalidate(true);
+        } else {
+          refresh('h');
+        }
+      } else {
+        invalidate(true);
+      }
+    };
+
+    hoursInputEl.bind('blur', function(e) {
+      if (!$scope.invalidHours && $scope.hours < 10) {
+        $scope.$apply(function() {
+          $scope.hours = pad($scope.hours);
+        });
+      }
+    });
+
+    $scope.updateMinutes = function() {
+      var minutes = getMinutesFromTemplate(),
+        hours = getHoursFromTemplate();
+
+      if (angular.isDefined(minutes) && angular.isDefined(hours)) {
+        selected.setMinutes(minutes);
+        if (selected < min || selected > max) {
+          invalidate(undefined, true);
+        } else {
+          refresh('m');
+        }
+      } else {
+        invalidate(undefined, true);
+      }
+    };
+
+    minutesInputEl.bind('blur', function(e) {
+      if (!$scope.invalidMinutes && $scope.minutes < 10) {
+        $scope.$apply(function() {
+          $scope.minutes = pad($scope.minutes);
+        });
+      }
+    });
+
+  };
+
+  this.render = function() {
+    var date = ngModelCtrl.$viewValue;
+
+    if (isNaN(date)) {
+      ngModelCtrl.$setValidity('time', false);
+      $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+    } else {
+      if (date) {
+        selected = date;
+      }
+
+      if (selected < min || selected > max) {
+        ngModelCtrl.$setValidity('time', false);
+        $scope.invalidHours = true;
+        $scope.invalidMinutes = true;
+      } else {
+        makeValid();
+      }
+      updateTemplate();
+    }
+  };
+
+  // Call internally when we know that model is valid.
+  function refresh(keyboardChange) {
+    makeValid();
+    ngModelCtrl.$setViewValue(new Date(selected));
+    updateTemplate(keyboardChange);
+  }
+
+  function makeValid() {
+    ngModelCtrl.$setValidity('time', true);
+    $scope.invalidHours = false;
+    $scope.invalidMinutes = false;
+  }
+
+  function updateTemplate(keyboardChange) {
+    var hours = selected.getHours(), minutes = selected.getMinutes();
+
+    if ($scope.showMeridian) {
+      hours = (hours === 0 || hours === 12) ? 12 : hours % 12; // Convert 24 to 12 hour system
+    }
+
+    $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
+    if (keyboardChange !== 'm') {
+      $scope.minutes = pad(minutes);
+    }
+    $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
+  }
+
+  function addMinutes(date, minutes) {
+    var dt = new Date(date.getTime() + minutes * 60000);
+    var newDate = new Date(date);
+    newDate.setHours(dt.getHours(), dt.getMinutes());
+    return newDate;
+  }
+
+  function addMinutesToSelected(minutes) {
+    selected = addMinutes(selected, minutes);
+    refresh();
+  }
+
+  $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
+    $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
+
+  $scope.incrementHours = function() {
+    if (!$scope.noIncrementHours()) {
+      addMinutesToSelected(hourStep * 60);
+    }
+  };
+
+  $scope.decrementHours = function() {
+    if (!$scope.noDecrementHours()) {
+      addMinutesToSelected(-hourStep * 60);
+    }
+  };
+
+  $scope.incrementMinutes = function() {
+    if (!$scope.noIncrementMinutes()) {
+      addMinutesToSelected(minuteStep);
+    }
+  };
+
+  $scope.decrementMinutes = function() {
+    if (!$scope.noDecrementMinutes()) {
+      addMinutesToSelected(-minuteStep);
+    }
+  };
+
+  $scope.toggleMeridian = function() {
+    if (!$scope.noToggleMeridian()) {
+      addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1));
+    }
+  };
+}])
+
+.directive('timepicker', function() {
+  return {
+    restrict: 'EA',
+    require: ['timepicker', '?^ngModel'],
+    controller:'TimepickerController',
+    controllerAs: 'timepicker',
+    replace: true,
+    scope: {},
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/timepicker/timepicker.html';
+    },
+    link: function(scope, element, attrs, ctrls) {
+      var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (ngModelCtrl) {
+        timepickerCtrl.init(ngModelCtrl, element.find('input'));
+      }
+    }
+  };
+});
+
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
+
+/**
+ * A helper service that can parse typeahead's syntax (string provided by users)
+ * Extracted to a separate service for ease of unit testing
+ */
+  .factory('typeaheadParser', ['$parse', function($parse) {
+
+  //                      00000111000000000000022200000000000000003333333333333330000000000044000
+  var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
+
+  return {
+    parse: function(input) {
+      var match = input.match(TYPEAHEAD_REGEXP);
+      if (!match) {
+        throw new Error(
+          'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
+            ' but got "' + input + '".');
+      }
+
+      return {
+        itemName:match[3],
+        source:$parse(match[4]),
+        viewMapper:$parse(match[2] || match[1]),
+        modelMapper:$parse(match[1])
+      };
+    }
+  };
+}])
+
+  .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
+    function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
+    var HOT_KEYS = [9, 13, 27, 38, 40];
+    var eventDebounceTime = 200;
+
+    return {
+      require: ['ngModel', '^?ngModelOptions'],
+      link: function(originalScope, element, attrs, ctrls) {
+        var modelCtrl = ctrls[0];
+        var ngModelOptions = ctrls[1];
+        //SUPPORTED ATTRIBUTES (OPTIONS)
+
+        //minimal no of characters that needs to be entered before typeahead kicks-in
+        var minLength = originalScope.$eval(attrs.typeaheadMinLength);
+        if (!minLength && minLength !== 0) {
+          minLength = 1;
+        }
+
+        //minimal wait time after last character typed before typeahead kicks-in
+        var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
+
+        //should it restrict model values to the ones selected from the popup only?
+        var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+
+        //binding to a variable that indicates if matches are being retrieved asynchronously
+        var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+
+        //a callback executed when a match is selected
+        var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+
+        //should it select highlighted popup value when losing focus?
+        var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
+
+        //binding to a variable that indicates if there were no results after the query is completed
+        var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
+
+        var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
+
+        var appendToBody =  attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+
+        var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
+
+        //If input matches an item of the list exactly, select it automatically
+        var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
+
+        //INTERNAL VARIABLES
+
+        //model setter executed upon match selection
+        var parsedModel = $parse(attrs.ngModel);
+        var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+        var $setModelValue = function(scope, newValue) {
+          if (angular.isFunction(parsedModel(originalScope)) &&
+            ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
+            return invokeModelSetter(scope, {$$$p: newValue});
+          } else {
+            return parsedModel.assign(scope, newValue);
+          }
+        };
+
+        //expressions used by typeahead
+        var parserResult = typeaheadParser.parse(attrs.typeahead);
+
+        var hasFocus;
+
+        //Used to avoid bug in iOS webview where iOS keyboard does not fire
+        //mousedown & mouseup events
+        //Issue #3699
+        var selected;
+
+        //create a child scope for the typeahead directive so we are not polluting original scope
+        //with typeahead-specific data (matches, query etc.)
+        var scope = originalScope.$new();
+        var offDestroy = originalScope.$on('$destroy', function() {
+			    scope.$destroy();
+        });
+        scope.$on('$destroy', offDestroy);
+
+        // WAI-ARIA
+        var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
+        element.attr({
+          'aria-autocomplete': 'list',
+          'aria-expanded': false,
+          'aria-owns': popupId
+        });
+
+        //pop-up element used to display matches
+        var popUpEl = angular.element('<div typeahead-popup></div>');
+        popUpEl.attr({
+          id: popupId,
+          matches: 'matches',
+          active: 'activeIdx',
+          select: 'select(activeIdx)',
+          'move-in-progress': 'moveInProgress',
+          query: 'query',
+          position: 'position'
+        });
+        //custom item template
+        if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
+          popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+        }
+
+        if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
+          popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
+        }
+
+        var resetMatches = function() {
+          scope.matches = [];
+          scope.activeIdx = -1;
+          element.attr('aria-expanded', false);
+        };
+
+        var getMatchId = function(index) {
+          return popupId + '-option-' + index;
+        };
+
+        // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
+        // This attribute is added or removed automatically when the `activeIdx` changes.
+        scope.$watch('activeIdx', function(index) {
+          if (index < 0) {
+            element.removeAttr('aria-activedescendant');
+          } else {
+            element.attr('aria-activedescendant', getMatchId(index));
+          }
+        });
+
+        var inputIsExactMatch = function(inputValue, index) {
+          if (scope.matches.length > index && inputValue) {
+            return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
+          }
+
+          return false;
+        };
+
+        var getMatchesAsync = function(inputValue) {
+          var locals = {$viewValue: inputValue};
+          isLoadingSetter(originalScope, true);
+          isNoResultsSetter(originalScope, false);
+          $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+            //it might happen that several async queries were in progress if a user were typing fast
+            //but we are interested only in responses that correspond to the current view value
+            var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
+            if (onCurrentRequest && hasFocus) {
+              if (matches && matches.length > 0) {
+
+                scope.activeIdx = focusFirst ? 0 : -1;
+                isNoResultsSetter(originalScope, false);
+                scope.matches.length = 0;
+
+                //transform labels
+                for (var i = 0; i < matches.length; i++) {
+                  locals[parserResult.itemName] = matches[i];
+                  scope.matches.push({
+                    id: getMatchId(i),
+                    label: parserResult.viewMapper(scope, locals),
+                    model: matches[i]
+                  });
+                }
+
+                scope.query = inputValue;
+                //position pop-up with matches - we need to re-calculate its position each time we are opening a window
+                //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
+                //due to other elements being rendered
+                recalculatePosition();
+
+                element.attr('aria-expanded', true);
+
+                //Select the single remaining option if user input matches
+                if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
+                  scope.select(0);
+                }
+              } else {
+                resetMatches();
+                isNoResultsSetter(originalScope, true);
+              }
+            }
+            if (onCurrentRequest) {
+              isLoadingSetter(originalScope, false);
+            }
+          }, function() {
+            resetMatches();
+            isLoadingSetter(originalScope, false);
+            isNoResultsSetter(originalScope, true);
+          });
+        };
+
+        // bind events only if appendToBody params exist - performance feature
+        if (appendToBody) {
+          angular.element($window).bind('resize', fireRecalculating);
+          $document.find('body').bind('scroll', fireRecalculating);
+        }
+
+        // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+        var timeoutEventPromise;
+
+        // Default progress type
+        scope.moveInProgress = false;
+
+        function fireRecalculating() {
+          if (!scope.moveInProgress) {
+            scope.moveInProgress = true;
+            scope.$digest();
+          }
+
+          // Cancel previous timeout
+          if (timeoutEventPromise) {
+            $timeout.cancel(timeoutEventPromise);
+          }
+
+          // Debounced executing recalculate after events fired
+          timeoutEventPromise = $timeout(function() {
+            // if popup is visible
+            if (scope.matches.length) {
+              recalculatePosition();
+            }
+
+            scope.moveInProgress = false;
+            scope.$digest();
+          }, eventDebounceTime);
+        }
+
+        // recalculate actual position and set new values to scope
+        // after digest loop is popup in right position
+        function recalculatePosition() {
+          scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+          scope.position.top += element.prop('offsetHeight');
+        }
+
+        resetMatches();
+
+        //we need to propagate user's query so we can higlight matches
+        scope.query = undefined;
+
+        //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+        var timeoutPromise;
+
+        var scheduleSearchWithTimeout = function(inputValue) {
+          timeoutPromise = $timeout(function() {
+            getMatchesAsync(inputValue);
+          }, waitTime);
+        };
+
+        var cancelPreviousTimeout = function() {
+          if (timeoutPromise) {
+            $timeout.cancel(timeoutPromise);
+          }
+        };
+
+        //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
+        //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
+        modelCtrl.$parsers.unshift(function(inputValue) {
+          hasFocus = true;
+
+          if (minLength === 0 || inputValue && inputValue.length >= minLength) {
+            if (waitTime > 0) {
+              cancelPreviousTimeout();
+              scheduleSearchWithTimeout(inputValue);
+            } else {
+              getMatchesAsync(inputValue);
+            }
+          } else {
+            isLoadingSetter(originalScope, false);
+            cancelPreviousTimeout();
+            resetMatches();
+          }
+
+          if (isEditable) {
+            return inputValue;
+          } else {
+            if (!inputValue) {
+              // Reset in case user had typed something previously.
+              modelCtrl.$setValidity('editable', true);
+              return null;
+            } else {
+              modelCtrl.$setValidity('editable', false);
+              return undefined;
+            }
+          }
+        });
+
+        modelCtrl.$formatters.push(function(modelValue) {
+          var candidateViewValue, emptyViewValue;
+          var locals = {};
+
+          // The validity may be set to false via $parsers (see above) if
+          // the model is restricted to selected values. If the model
+          // is set manually it is considered to be valid.
+          if (!isEditable) {
+            modelCtrl.$setValidity('editable', true);
+          }
+
+          if (inputFormatter) {
+            locals.$model = modelValue;
+            return inputFormatter(originalScope, locals);
+          } else {
+            //it might happen that we don't have enough info to properly render input value
+            //we need to check for this situation and simply return model value if we can't apply custom formatting
+            locals[parserResult.itemName] = modelValue;
+            candidateViewValue = parserResult.viewMapper(originalScope, locals);
+            locals[parserResult.itemName] = undefined;
+            emptyViewValue = parserResult.viewMapper(originalScope, locals);
+
+            return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
+          }
+        });
+
+        scope.select = function(activeIdx) {
+          //called from within the $digest() cycle
+          var locals = {};
+          var model, item;
+
+          selected = true;
+          locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
+          model = parserResult.modelMapper(originalScope, locals);
+          $setModelValue(originalScope, model);
+          modelCtrl.$setValidity('editable', true);
+          modelCtrl.$setValidity('parse', true);
+
+          onSelectCallback(originalScope, {
+            $item: item,
+            $model: model,
+            $label: parserResult.viewMapper(originalScope, locals)
+          });
+
+          resetMatches();
+
+          //return focus to the input element if a match was selected via a mouse click event
+          // use timeout to avoid $rootScope:inprog error
+          if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
+            $timeout(function() { element[0].focus(); }, 0, false);
+          }
+        };
+
+        //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
+        element.bind('keydown', function(evt) {
+          //typeahead is open and an "interesting" key was pressed
+          if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
+            return;
+          }
+
+          // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
+          if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
+            resetMatches();
+            scope.$digest();
+            return;
+          }
+
+          evt.preventDefault();
+
+          if (evt.which === 40) {
+            scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
+            scope.$digest();
+
+          } else if (evt.which === 38) {
+            scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
+            scope.$digest();
+
+          } else if (evt.which === 13 || evt.which === 9) {
+            scope.$apply(function () {
+              scope.select(scope.activeIdx);
+            });
+
+          } else if (evt.which === 27) {
+            evt.stopPropagation();
+
+            resetMatches();
+            scope.$digest();
+          }
+        });
+
+        element.bind('blur', function() {
+          if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
+            selected = true;
+            scope.$apply(function() {
+              scope.select(scope.activeIdx);
+            });
+          }
+          hasFocus = false;
+          selected = false;
+        });
+
+        // Keep reference to click handler to unbind it.
+        var dismissClickHandler = function(evt) {
+          // Issue #3973
+          // Firefox treats right click as a click on document
+          if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
+            resetMatches();
+            if (!$rootScope.$$phase) {
+              scope.$digest();
+            }
+          }
+        };
+
+        $document.bind('click', dismissClickHandler);
+
+        originalScope.$on('$destroy', function() {
+          $document.unbind('click', dismissClickHandler);
+          if (appendToBody) {
+            $popup.remove();
+          }
+          // Prevent jQuery cache memory leak
+          popUpEl.remove();
+        });
+
+        var $popup = $compile(popUpEl)(scope);
+
+        if (appendToBody) {
+          $document.find('body').append($popup);
+        } else {
+          element.after($popup);
+        }
+      }
+    };
+
+  }])
+
+  .directive('typeaheadPopup', function() {
+    return {
+      restrict: 'EA',
+      scope: {
+        matches: '=',
+        query: '=',
+        active: '=',
+        position: '&',
+        moveInProgress: '=',
+        select: '&'
+      },
+      replace: true,
+      templateUrl: function(element, attrs) {
+        return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
+      },
+      link: function(scope, element, attrs) {
+        scope.templateUrl = attrs.templateUrl;
+
+        scope.isOpen = function() {
+          return scope.matches.length > 0;
+        };
+
+        scope.isActive = function(matchIdx) {
+          return scope.active == matchIdx;
+        };
+
+        scope.selectActive = function(matchIdx) {
+          scope.active = matchIdx;
+        };
+
+        scope.selectMatch = function(activeIdx) {
+          scope.select({activeIdx:activeIdx});
+        };
+      }
+    };
+  })
+
+  .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
+    return {
+      restrict: 'EA',
+      scope: {
+        index: '=',
+        match: '=',
+        query: '='
+      },
+      link:function(scope, element, attrs) {
+        var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
+        $templateRequest(tplUrl).then(function(tplContent) {
+          $compile(tplContent.trim())(scope, function(clonedElement) {
+            element.replaceWith(clonedElement);
+          });
+        });
+      }
+    };
+  }])
+
+  .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
+    var isSanitizePresent;
+    isSanitizePresent = $injector.has('$sanitize');
+
+    function escapeRegexp(queryToEscape) {
+      // Regex: capture the whole query string and replace it with the string that will be used to match
+      // the results, for example if the capture is "a" the result will be \a
+      return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
+    }
+
+    function containsHtml(matchItem) {
+      return /<.*>/g.test(matchItem);
+    }
+
+    return function(matchItem, query) {
+      if (!isSanitizePresent && containsHtml(matchItem)) {
+        $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+      }
+      matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+      if (!isSanitizePresent) {
+        matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+      }
+      return matchItem;
+    };
+  }]);
+
+
+/**
+ * @ngdoc overview
+ * @name ui.bootstrap.tabs
+ *
+ * @description
+ * AngularJS version of the tabs directive.
+ */
+
+angular.module('ui.bootstrap.tabs', [])
+
+.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
+  var ctrl = this,
+      tabs = ctrl.tabs = $scope.tabs = [];
+
+  ctrl.select = function(selectedTab) {
+    angular.forEach(tabs, function(tab) {
+      if (tab.active && tab !== selectedTab) {
+        tab.active = false;
+        tab.onDeselect();
+        selectedTab.selectCalled = false;
+      }
+    });
+    selectedTab.active = true;
+    // only call select if it has not already been called
+    if (!selectedTab.selectCalled) {
+      selectedTab.onSelect();
+      selectedTab.selectCalled = true;
+    }
+  };
+
+  ctrl.addTab = function addTab(tab) {
+    tabs.push(tab);
+    // we can't run the select function on the first tab
+    // since that would select it twice
+    if (tabs.length === 1 && tab.active !== false) {
+      tab.active = true;
+    } else if (tab.active) {
+      ctrl.select(tab);
+    } else {
+      tab.active = false;
+    }
+  };
+
+  ctrl.removeTab = function removeTab(tab) {
+    var index = tabs.indexOf(tab);
+    //Select a new tab if the tab to be removed is selected and not destroyed
+    if (tab.active && tabs.length > 1 && !destroyed) {
+      //If this is the last tab, select the previous tab. else, the next tab.
+      var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
+      ctrl.select(tabs[newActiveIndex]);
+    }
+    tabs.splice(index, 1);
+  };
+
+  var destroyed;
+  $scope.$on('$destroy', function() {
+    destroyed = true;
+  });
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabset
+ * @restrict EA
+ *
+ * @description
+ * Tabset is the outer container for the tabs directive
+ *
+ * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
+ * @param {boolean=} justified Whether or not to use justified styling for the tabs.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab heading="Tab 1"><b>First</b> Content!</tab>
+      <tab heading="Tab 2"><i>Second</i> Content!</tab>
+    </tabset>
+    <hr />
+    <tabset vertical="true">
+      <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
+      <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
+    </tabset>
+    <tabset justified="true">
+      <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
+      <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tabset', function() {
+  return {
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    scope: {
+      type: '@'
+    },
+    controller: 'TabsetController',
+    templateUrl: 'template/tabs/tabset.html',
+    link: function(scope, element, attrs) {
+      scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
+      scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
+    }
+  };
+})
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tab
+ * @restrict EA
+ *
+ * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
+ * @param {string=} select An expression to evaluate when the tab is selected.
+ * @param {boolean=} active A binding, telling whether or not this tab is selected.
+ * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
+ *
+ * @description
+ * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <div ng-controller="TabsDemoCtrl">
+      <button class="btn btn-small" ng-click="items[0].active = true">
+        Select item 1, using active binding
+      </button>
+      <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
+        Enable/disable item 2, using disabled binding
+      </button>
+      <br />
+      <tabset>
+        <tab heading="Tab 1">First Tab</tab>
+        <tab select="alertMe()">
+          <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
+          Second Tab, with alert callback and html heading!
+        </tab>
+        <tab ng-repeat="item in items"
+          heading="{{item.title}}"
+          disabled="item.disabled"
+          active="item.active">
+          {{item.content}}
+        </tab>
+      </tabset>
+    </div>
+  </file>
+  <file name="script.js">
+    function TabsDemoCtrl($scope) {
+      $scope.items = [
+        { title:"Dynamic Title 1", content:"Dynamic Item 0" },
+        { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
+      ];
+
+      $scope.alertMe = function() {
+        setTimeout(function() {
+          alert("You've selected the alert tab!");
+        });
+      };
+    };
+  </file>
+</example>
+ */
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabHeading
+ * @restrict EA
+ *
+ * @description
+ * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab>
+        <tab-heading><b>HTML</b> in my titles?!</tab-heading>
+        And some content, too!
+      </tab>
+      <tab>
+        <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
+        That's right.
+      </tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tab', ['$parse', '$log', function($parse, $log) {
+  return {
+    require: '^tabset',
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/tabs/tab.html',
+    transclude: true,
+    scope: {
+      active: '=?',
+      heading: '@',
+      onSelect: '&select', //This callback is called in contentHeadingTransclude
+                          //once it inserts the tab's content into the dom
+      onDeselect: '&deselect'
+    },
+    controller: function() {
+      //Empty controller so other directives can require being 'under' a tab
+    },
+    link: function(scope, elm, attrs, tabsetCtrl, transclude) {
+      scope.$watch('active', function(active) {
+        if (active) {
+          tabsetCtrl.select(scope);
+        }
+      });
+
+      scope.disabled = false;
+      if (attrs.disable) {
+        scope.$parent.$watch($parse(attrs.disable), function(value) {
+          scope.disabled = !! value;
+        });
+      }
+
+      // Deprecation support of "disabled" parameter
+      // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
+      // This code is duplicated from the lines above to make it easy to remove once
+      // the feature has been completely deprecated
+      if (attrs.disabled) {
+        $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
+        scope.$parent.$watch($parse(attrs.disabled), function(value) {
+          scope.disabled = !! value;
+        });
+      }
+
+      scope.select = function() {
+        if (!scope.disabled) {
+          scope.active = true;
+        }
+      };
+
+      tabsetCtrl.addTab(scope);
+      scope.$on('$destroy', function() {
+        tabsetCtrl.removeTab(scope);
+      });
+
+      //We need to transclude later, once the content container is ready.
+      //when this link happens, we're inside a tab heading.
+      scope.$transcludeFn = transclude;
+    }
+  };
+}])
+
+.directive('tabHeadingTransclude', function() {
+  return {
+    restrict: 'A',
+    require: '^tab',
+    link: function(scope, elm, attrs, tabCtrl) {
+      scope.$watch('headingElement', function updateHeadingElement(heading) {
+        if (heading) {
+          elm.html('');
+          elm.append(heading);
+        }
+      });
+    }
+  };
+})
+
+.directive('tabContentTransclude', function() {
+  return {
+    restrict: 'A',
+    require: '^tabset',
+    link: function(scope, elm, attrs) {
+      var tab = scope.$eval(attrs.tabContentTransclude);
+
+      //Now our tab is ready to be transcluded: both the tab heading area
+      //and the tab content area are loaded.  Transclude 'em both.
+      tab.$transcludeFn(tab.$parent, function(contents) {
+        angular.forEach(contents, function(node) {
+          if (isTabHeading(node)) {
+            //Let tabHeadingTransclude know.
+            tab.headingElement = node;
+          } else {
+            elm.append(node);
+          }
+        });
+      });
+    }
+  };
+
+  function isTabHeading(node) {
+    return node.tagName && (
+      node.hasAttribute('tab-heading') ||
+      node.hasAttribute('data-tab-heading') ||
+      node.hasAttribute('x-tab-heading') ||
+      node.tagName.toLowerCase() === 'tab-heading' ||
+      node.tagName.toLowerCase() === 'data-tab-heading' ||
+      node.tagName.toLowerCase() === 'x-tab-heading'
+    );
+  }
+});
+
+/**
+ * The following features are still outstanding: popup delay, animation as a
+ * function, placement as a function, inside, support for more triggers than
+ * just mouse enter/leave, and selector delegatation.
+ */
+angular.module( 'ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
+
+.directive('popoverTemplatePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+      originScope: '&' },
+    templateUrl: 'template/popover/popover-template.html'
+  };
+})
+
+.directive('popoverTemplate', ['$tooltip', function($tooltip) {
+  return $tooltip('popoverTemplate', 'popover', 'click', {
+    useContentExp: true
+  });
+}])
+
+.directive('popoverHtmlPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/popover/popover-html.html'
+  };
+})
+
+.directive('popoverHtml', ['$tooltip', function($tooltip) {
+  return $tooltip( 'popoverHtml', 'popover', 'click', {
+    useContentExp: true
+  });
+}])
+
+.directive('popoverPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/popover/popover.html'
+  };
+})
+
+.directive('popover', ['$tooltip', function($tooltip) {
+  return $tooltip( 'popover', 'popover', 'click' );
+}]);
+
+/**
+ * The following features are still outstanding: animation as a
+ * function, placement as a function, inside, support for more triggers than
+ * just mouse enter/leave, html tooltips, and selector delegation.
+ */
+angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+
+/**
+ * The $tooltip service creates tooltip- and popover-like directives as well as
+ * houses global options for them.
+ */
+.provider('$tooltip', function() {
+  // The default options tooltip and popover.
+  var defaultOptions = {
+    placement: 'top',
+    animation: true,
+    popupDelay: 0,
+    useContentExp: false
+  };
+
+  // Default hide triggers for each show trigger
+  var triggerMap = {
+    'mouseenter': 'mouseleave',
+    'click': 'click',
+    'focus': 'blur',
+    'none': ''
+  };
+
+  // The options specified to the provider globally.
+  var globalOptions = {};
+
+  /**
+   * `options({})` allows global configuration of all tooltips in the
+   * application.
+   *
+   *   var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
+   *     // place tooltips left instead of top by default
+   *     $tooltipProvider.options( { placement: 'left' } );
+   *   });
+   */
+	this.options = function(value) {
+		angular.extend(globalOptions, value);
+	};
+
+  /**
+   * This allows you to extend the set of trigger mappings available. E.g.:
+   *
+   *   $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
+   */
+  this.setTriggers = function setTriggers(triggers) {
+    angular.extend(triggerMap, triggers);
+  };
+
+  /**
+   * This is a helper function for translating camel-case to snake-case.
+   */
+  function snake_case(name) {
+    var regexp = /[A-Z]/g;
+    var separator = '-';
+    return name.replace(regexp, function(letter, pos) {
+      return (pos ? separator : '') + letter.toLowerCase();
+    });
+  }
+
+  /**
+   * Returns the actual instance of the $tooltip service.
+   * TODO support multiple triggers
+   */
+  this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) {
+    return function $tooltip(type, prefix, defaultTriggerShow, options) {
+      options = angular.extend({}, defaultOptions, globalOptions, options);
+
+      /**
+       * Returns an object of show and hide triggers.
+       *
+       * If a trigger is supplied,
+       * it is used to show the tooltip; otherwise, it will use the `trigger`
+       * option passed to the `$tooltipProvider.options` method; else it will
+       * default to the trigger supplied to this directive factory.
+       *
+       * The hide trigger is based on the show trigger. If the `trigger` option
+       * was passed to the `$tooltipProvider.options` method, it will use the
+       * mapped trigger from `triggerMap` or the passed trigger if the map is
+       * undefined; otherwise, it uses the `triggerMap` value of the show
+       * trigger; else it will just use the show trigger.
+       */
+      function getTriggers(trigger) {
+        var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
+        var hide = show.map(function(trigger) {
+          return triggerMap[trigger] || trigger;
+        });
+        return {
+          show: show,
+          hide: hide
+        };
+      }
+
+      var directiveName = snake_case(type);
+
+      var startSym = $interpolate.startSymbol();
+      var endSym = $interpolate.endSymbol();
+      var template =
+        '<div '+ directiveName +'-popup '+
+          'title="'+startSym+'title'+endSym+'" '+
+          (options.useContentExp ?
+            'content-exp="contentExp()" ' :
+            'content="'+startSym+'content'+endSym+'" ') +
+          'placement="'+startSym+'placement'+endSym+'" '+
+          'popup-class="'+startSym+'popupClass'+endSym+'" '+
+          'animation="animation" '+
+          'is-open="isOpen"'+
+          'origin-scope="origScope" '+
+          '>'+
+        '</div>';
+
+      return {
+        restrict: 'EA',
+        compile: function(tElem, tAttrs) {
+          var tooltipLinker = $compile( template );
+
+          return function link(scope, element, attrs, tooltipCtrl) {
+            var tooltip;
+            var tooltipLinkedScope;
+            var transitionTimeout;
+            var popupTimeout;
+            var positionTimeout;
+            var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
+            var triggers = getTriggers(undefined);
+            var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
+            var ttScope = scope.$new(true);
+            var repositionScheduled = false;
+            var isOpenExp = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+
+            var positionTooltip = function() {
+              if (!tooltip) { return; }
+
+              if (!positionTimeout) {
+                positionTimeout = $timeout(function() {
+                  // Reset the positioning and box size for correct width and height values.
+                  tooltip.css({ top: 0, left: 0, width: 'auto', height: 'auto' });
+
+                  var ttBox = $position.position(tooltip);
+                  var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
+                  ttCss.top += 'px';
+                  ttCss.left += 'px';
+
+                  ttCss.width = ttBox.width + 'px';
+                  ttCss.height = ttBox.height + 'px';
+
+                  // Now set the calculated positioning and size.
+                  tooltip.css(ttCss);
+
+                  positionTimeout = null;
+
+                }, 0, false);
+              }
+            };
+
+            // Set up the correct scope to allow transclusion later
+            ttScope.origScope = scope;
+
+            // By default, the tooltip is not open.
+            // TODO add ability to start tooltip opened
+            ttScope.isOpen = false;
+
+            function toggleTooltipBind() {
+              if (!ttScope.isOpen) {
+                showTooltipBind();
+              } else {
+                hideTooltipBind();
+              }
+            }
+
+            // Show the tooltip with delay if specified, otherwise show it immediately
+            function showTooltipBind() {
+              if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
+                return;
+              }
+
+              prepareTooltip();
+
+              if (ttScope.popupDelay) {
+                // Do nothing if the tooltip was already scheduled to pop-up.
+                // This happens if show is triggered multiple times before any hide is triggered.
+                if (!popupTimeout) {
+                  popupTimeout = $timeout(show, ttScope.popupDelay, false);
+                }
+              } else {
+                show();
+              }
+            }
+
+            function hideTooltipBind () {
+              hide();
+              if (!$rootScope.$$phase) {
+                $rootScope.$digest();
+              }
+            }
+
+            // Show the tooltip popup element.
+            function show() {
+              popupTimeout = null;
+
+              // If there is a pending remove transition, we must cancel it, lest the
+              // tooltip be mysteriously removed.
+              if (transitionTimeout) {
+                $timeout.cancel(transitionTimeout);
+                transitionTimeout = null;
+              }
+
+              // Don't show empty tooltips.
+              if (!(options.useContentExp ? ttScope.contentExp() : ttScope.content)) {
+                return angular.noop;
+              }
+
+              createTooltip();
+
+              // And show the tooltip.
+              ttScope.isOpen = true;
+              if (isOpenExp) {
+                isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
+              }
+
+              if (!$rootScope.$$phase) {
+                ttScope.$apply(); // digest required as $apply is not called
+              }
+
+              tooltip.css({ display: 'block' });
+
+              positionTooltip();
+            }
+
+            // Hide the tooltip popup element.
+            function hide() {
+              // First things first: we don't show it anymore.
+              ttScope.isOpen = false;
+              if (isOpenExp) {
+                isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
+              }
+
+              //if tooltip is going to be shown after delay, we must cancel this
+              $timeout.cancel(popupTimeout);
+              popupTimeout = null;
+
+              $timeout.cancel(positionTimeout);
+              positionTimeout = null;
+
+              // And now we remove it from the DOM. However, if we have animation, we
+              // need to wait for it to expire beforehand.
+              // FIXME: this is a placeholder for a port of the transitions library.
+              if (ttScope.animation) {
+                if (!transitionTimeout) {
+                  transitionTimeout = $timeout(removeTooltip, 500);
+                }
+              } else {
+                removeTooltip();
+              }
+            }
+
+            function createTooltip() {
+              // There can only be one tooltip element per directive shown at once.
+              if (tooltip) {
+                removeTooltip();
+              }
+              tooltipLinkedScope = ttScope.$new();
+              tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
+                if (appendToBody) {
+                  $document.find('body').append(tooltip);
+                } else {
+                  element.after(tooltip);
+                }
+              });
+
+              if (options.useContentExp) {
+                tooltipLinkedScope.$watch('contentExp()', function(val) {
+                  if (!val && ttScope.isOpen) {
+                    hide();
+                  }
+                });
+
+                tooltipLinkedScope.$watch(function() {
+                  if (!repositionScheduled) {
+                    repositionScheduled = true;
+                    tooltipLinkedScope.$$postDigest(function() {
+                      repositionScheduled = false;
+                      if (ttScope.isOpen) {
+                        positionTooltip();
+                      }
+                    });
+                  }
+                });
+
+              }
+            }
+
+            function removeTooltip() {
+              transitionTimeout = null;
+              if (tooltip) {
+                tooltip.remove();
+                tooltip = null;
+              }
+              if (tooltipLinkedScope) {
+                tooltipLinkedScope.$destroy();
+                tooltipLinkedScope = null;
+              }
+            }
+
+            function prepareTooltip() {
+              prepPopupClass();
+              prepPlacement();
+              prepPopupDelay();
+            }
+
+            ttScope.contentExp = function() {
+              return scope.$eval(attrs[type]);
+            };
+
+            /**
+             * Observe the relevant attributes.
+             */
+            if (!options.useContentExp) {
+              attrs.$observe(type, function(val) {
+                ttScope.content = val;
+
+                if (!val && ttScope.isOpen) {
+                  hide();
+                } else {
+                  positionTooltip();
+                }
+              });
+            }
+
+            attrs.$observe('disabled', function(val) {
+              if (popupTimeout && val) {
+                $timeout.cancel(popupTimeout);
+                popupTimeout = null;
+              }
+
+              if (val && ttScope.isOpen) {
+                hide();
+              }
+            });
+
+            attrs.$observe(prefix + 'Title', function(val) {
+              ttScope.title = val;
+              positionTooltip();
+            });
+
+            attrs.$observe(prefix + 'Placement', function() {
+              if (ttScope.isOpen) {
+                prepPlacement();
+                positionTooltip();
+              }
+            });
+
+            if (isOpenExp) {
+              scope.$watch(isOpenExp, function(val) {
+                if (val !== ttScope.isOpen) {
+                  toggleTooltipBind();
+                }
+              });
+            }
+
+            function prepPopupClass() {
+              ttScope.popupClass = attrs[prefix + 'Class'];
+            }
+
+            function prepPlacement() {
+              var val = attrs[prefix + 'Placement'];
+              ttScope.placement = angular.isDefined(val) ? val : options.placement;
+            }
+
+            function prepPopupDelay() {
+              var val = attrs[prefix + 'PopupDelay'];
+              var delay = parseInt(val, 10);
+              ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+            }
+
+            var unregisterTriggers = function() {
+              triggers.show.forEach(function(trigger) {
+                element.unbind(trigger, showTooltipBind);
+              });
+              triggers.hide.forEach(function(trigger) {
+                element.unbind(trigger, hideTooltipBind);
+              });
+            };
+
+            function prepTriggers() {
+              var val = attrs[prefix + 'Trigger'];
+              unregisterTriggers();
+
+              triggers = getTriggers(val);
+
+              if (triggers.show !== 'none') {
+                triggers.show.forEach(function(trigger, idx) {
+                  // Using raw addEventListener due to jqLite/jQuery bug - #4060
+                  if (trigger === triggers.hide[idx]) {
+                    element[0].addEventListener(trigger, toggleTooltipBind);
+                  } else if (trigger) {
+                    element[0].addEventListener(trigger, showTooltipBind);
+                    element[0].addEventListener(triggers.hide[idx], hideTooltipBind);
+                  }
+                });
+              }
+            }
+            prepTriggers();
+
+            var animation = scope.$eval(attrs[prefix + 'Animation']);
+            ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
+
+            var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
+            appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
+
+            // if a tooltip is attached to <body> we need to remove it on
+            // location change as its parent scope will probably not be destroyed
+            // by the change.
+            if (appendToBody) {
+              scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() {
+                if (ttScope.isOpen) {
+                  hide();
+                }
+              });
+            }
+
+            // Make sure tooltip is destroyed and removed.
+            scope.$on('$destroy', function onDestroyTooltip() {
+              $timeout.cancel(transitionTimeout);
+              $timeout.cancel(popupTimeout);
+              $timeout.cancel(positionTimeout);
+              unregisterTriggers();
+              removeTooltip();
+              ttScope = null;
+            });
+          };
+        }
+      };
+    };
+  }];
+})
+
+// This is mostly ngInclude code but with a custom scope
+.directive('tooltipTemplateTransclude', [
+         '$animate', '$sce', '$compile', '$templateRequest',
+function ($animate ,  $sce ,  $compile ,  $templateRequest) {
+  return {
+    link: function(scope, elem, attrs) {
+      var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
+
+      var changeCounter = 0,
+        currentScope,
+        previousElement,
+        currentElement;
+
+      var cleanupLastIncludeContent = function() {
+        if (previousElement) {
+          previousElement.remove();
+          previousElement = null;
+        }
+        if (currentScope) {
+          currentScope.$destroy();
+          currentScope = null;
+        }
+        if (currentElement) {
+          $animate.leave(currentElement).then(function() {
+            previousElement = null;
+          });
+          previousElement = currentElement;
+          currentElement = null;
+        }
+      };
+
+      scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
+        var thisChangeId = ++changeCounter;
+
+        if (src) {
+          //set the 2nd param to true to ignore the template request error so that the inner
+          //contents and scope can be cleaned up.
+          $templateRequest(src, true).then(function(response) {
+            if (thisChangeId !== changeCounter) { return; }
+            var newScope = origScope.$new();
+            var template = response;
+
+            var clone = $compile(template)(newScope, function(clone) {
+              cleanupLastIncludeContent();
+              $animate.enter(clone, elem);
+            });
+
+            currentScope = newScope;
+            currentElement = clone;
+
+            currentScope.$emit('$includeContentLoaded', src);
+          }, function() {
+            if (thisChangeId === changeCounter) {
+              cleanupLastIncludeContent();
+              scope.$emit('$includeContentError', src);
+            }
+          });
+          scope.$emit('$includeContentRequested', src);
+        } else {
+          cleanupLastIncludeContent();
+        }
+      });
+
+      scope.$on('$destroy', cleanupLastIncludeContent);
+    }
+  };
+}])
+
+/**
+ * Note that it's intentional that these classes are *not* applied through $animate.
+ * They must not be animated as they're expected to be present on the tooltip on
+ * initialization.
+ */
+.directive('tooltipClasses', function() {
+  return {
+    restrict: 'A',
+    link: function(scope, element, attrs) {
+      if (scope.placement) {
+        element.addClass(scope.placement);
+      }
+      if (scope.popupClass) {
+        element.addClass(scope.popupClass);
+      }
+      if (scope.animation()) {
+        element.addClass(attrs.tooltipAnimationClass);
+      }
+    }
+  };
+})
+
+.directive('tooltipPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-popup.html'
+  };
+})
+
+.directive('tooltip', [ '$tooltip', function($tooltip) {
+  return $tooltip('tooltip', 'tooltip', 'mouseenter');
+}])
+
+.directive('tooltipTemplatePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+      originScope: '&' },
+    templateUrl: 'template/tooltip/tooltip-template-popup.html'
+  };
+})
+
+.directive('tooltipTemplate', ['$tooltip', function($tooltip) {
+  return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
+    useContentExp: true
+  });
+}])
+
+.directive('tooltipHtmlPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-html-popup.html'
+  };
+})
+
+.directive('tooltipHtml', ['$tooltip', function($tooltip) {
+  return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
+    useContentExp: true
+  });
+}])
+
+/*
+Deprecated
+*/
+.directive('tooltipHtmlUnsafePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
+  };
+})
+
+.value('tooltipHtmlUnsafeSuppressDeprecated', false)
+.directive('tooltipHtmlUnsafe', [
+          '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
+function($tooltip ,  tooltipHtmlUnsafeSuppressDeprecated ,  $log) {
+  if (!tooltipHtmlUnsafeSuppressDeprecated) {
+    $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
+  }
+  return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter');
+}]);
+
+angular.module('ui.bootstrap.modal', [])
+
+/**
+ * A helper, internal data structure that acts as a map but also allows getting / removing
+ * elements in the LIFO order
+ */
+  .factory('$$stackedMap', function() {
+    return {
+      createNew: function() {
+        var stack = [];
+
+        return {
+          add: function(key, value) {
+            stack.push({
+              key: key,
+              value: value
+            });
+          },
+          get: function(key) {
+            for (var i = 0; i < stack.length; i++) {
+              if (key == stack[i].key) {
+                return stack[i];
+              }
+            }
+          },
+          keys: function() {
+            var keys = [];
+            for (var i = 0; i < stack.length; i++) {
+              keys.push(stack[i].key);
+            }
+            return keys;
+          },
+          top: function() {
+            return stack[stack.length - 1];
+          },
+          remove: function(key) {
+            var idx = -1;
+            for (var i = 0; i < stack.length; i++) {
+              if (key == stack[i].key) {
+                idx = i;
+                break;
+              }
+            }
+            return stack.splice(idx, 1)[0];
+          },
+          removeTop: function() {
+            return stack.splice(stack.length - 1, 1)[0];
+          },
+          length: function() {
+            return stack.length;
+          }
+        };
+      }
+    };
+  })
+
+/**
+ * A helper, internal data structure that stores all references attached to key
+ */
+  .factory('$$multiMap', function() {
+    return {
+      createNew: function() {
+        var map = {};
+
+        return {
+          entries: function() {
+            return Object.keys(map).map(function(key) {
+              return {
+                key: key,
+                value: map[key]
+              };
+            });
+          },
+          get: function(key) {
+            return map[key];
+          },
+          hasKey: function(key) {
+            return !!map[key];
+          },
+          keys: function() {
+            return Object.keys(map);
+          },
+          put: function(key, value) {
+            if (!map[key]) {
+              map[key] = [];
+            }
+
+            map[key].push(value);
+          },
+          remove: function(key, value) {
+            var values = map[key];
+
+            if (!values) {
+              return;
+            }
+
+            var idx = values.indexOf(value);
+
+            if (idx !== -1) {
+              values.splice(idx, 1);
+            }
+
+            if (!values.length) {
+              delete map[key];
+            }
+          }
+        };
+      }
+    };
+  })
+
+/**
+ * A helper directive for the $modal service. It creates a backdrop element.
+ */
+  .directive('modalBackdrop', [
+           '$animate', '$injector', '$modalStack',
+  function($animate ,  $injector,   $modalStack) {
+    var $animateCss = null;
+
+    if ($injector.has('$animateCss')) {
+      $animateCss = $injector.get('$animateCss');
+    }
+
+    return {
+      restrict: 'EA',
+      replace: true,
+      templateUrl: 'template/modal/backdrop.html',
+      compile: function(tElement, tAttrs) {
+        tElement.addClass(tAttrs.backdropClass);
+        return linkFn;
+      }
+    };
+
+    function linkFn(scope, element, attrs) {
+      if (attrs.modalInClass) {
+        if ($animateCss) {
+          $animateCss(element, {
+            addClass: attrs.modalInClass
+          }).start();
+        } else {
+          $animate.addClass(element, attrs.modalInClass);
+        }
+
+        scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+          var done = setIsAsync();
+          if ($animateCss) {
+            $animateCss(element, {
+              removeClass: attrs.modalInClass
+            }).start().then(done);
+          } else {
+            $animate.removeClass(element, attrs.modalInClass).then(done);
+          }
+        });
+      }
+    }
+  }])
+
+  .directive('modalWindow', [
+           '$modalStack', '$q', '$animate', '$injector',
+  function($modalStack ,  $q ,  $animate,   $injector) {
+    var $animateCss = null;
+
+    if ($injector.has('$animateCss')) {
+      $animateCss = $injector.get('$animateCss');
+    }
+
+    return {
+      restrict: 'EA',
+      scope: {
+        index: '@'
+      },
+      replace: true,
+      transclude: true,
+      templateUrl: function(tElement, tAttrs) {
+        return tAttrs.templateUrl || 'template/modal/window.html';
+      },
+      link: function(scope, element, attrs) {
+        element.addClass(attrs.windowClass || '');
+        scope.size = attrs.size;
+
+        scope.close = function(evt) {
+          var modal = $modalStack.getTop();
+          if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
+            evt.preventDefault();
+            evt.stopPropagation();
+            $modalStack.dismiss(modal.key, 'backdrop click');
+          }
+        };
+
+        // This property is only added to the scope for the purpose of detecting when this directive is rendered.
+        // We can detect that by using this property in the template associated with this directive and then use
+        // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
+        scope.$isRendered = true;
+
+        // Deferred object that will be resolved when this modal is render.
+        var modalRenderDeferObj = $q.defer();
+        // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
+        // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
+        attrs.$observe('modalRender', function(value) {
+          if (value == 'true') {
+            modalRenderDeferObj.resolve();
+          }
+        });
+
+        modalRenderDeferObj.promise.then(function() {
+          var animationPromise = null;
+
+          if (attrs.modalInClass) {
+            if ($animateCss) {
+              animationPromise = $animateCss(element, {
+                addClass: attrs.modalInClass
+              }).start();
+            } else {
+              animationPromise = $animate.addClass(element, attrs.modalInClass);
+            }
+
+            scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+              var done = setIsAsync();
+              if ($animateCss) {
+                $animateCss(element, {
+                  removeClass: attrs.modalInClass
+                }).start().then(done);
+              } else {
+                $animate.removeClass(element, attrs.modalInClass).then(done);
+              }
+            });
+          }
+
+
+          $q.when(animationPromise).then(function() {
+            var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
+            /**
+             * Auto-focusing of a freshly-opened modal element causes any child elements
+             * with the autofocus attribute to lose focus. This is an issue on touch
+             * based devices which will show and then hide the onscreen keyboard.
+             * Attempts to refocus the autofocus element via JavaScript will not reopen
+             * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
+             * the modal element if the modal does not contain an autofocus element.
+             */
+            if (inputsWithAutofocus.length) {
+              inputsWithAutofocus[0].focus();
+            } else {
+              element[0].focus();
+            }
+          });
+
+          // Notify {@link $modalStack} that modal is rendered.
+          var modal = $modalStack.getTop();
+          if (modal) {
+            $modalStack.modalRendered(modal.key);
+          }
+        });
+      }
+    };
+  }])
+
+  .directive('modalAnimationClass', [
+    function () {
+      return {
+        compile: function(tElement, tAttrs) {
+          if (tAttrs.modalAnimation) {
+            tElement.addClass(tAttrs.modalAnimationClass);
+          }
+        }
+      };
+    }])
+
+  .directive('modalTransclude', function() {
+    return {
+      link: function($scope, $element, $attrs, controller, $transclude) {
+        $transclude($scope.$parent, function(clone) {
+          $element.empty();
+          $element.append(clone);
+        });
+      }
+    };
+  })
+
+  .factory('$modalStack', [
+             '$animate', '$timeout', '$document', '$compile', '$rootScope',
+             '$q',
+             '$injector',
+             '$$multiMap',
+             '$$stackedMap',
+    function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
+              $q,
+              $injector,
+              $$multiMap,
+              $$stackedMap) {
+      var $animateCss = null;
+
+      if ($injector.has('$animateCss')) {
+        $animateCss = $injector.get('$animateCss');
+      }
+
+      var OPENED_MODAL_CLASS = 'modal-open';
+
+      var backdropDomEl, backdropScope;
+      var openedWindows = $$stackedMap.createNew();
+      var openedClasses = $$multiMap.createNew();
+      var $modalStack = {
+        NOW_CLOSING_EVENT: 'modal.stack.now-closing'
+      };
+
+      //Modal focus behavior
+      var focusableElementList;
+      var focusIndex = 0;
+      var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' +
+        'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
+        'iframe, object, embed, *[tabindex], *[contenteditable=true]';
+
+      function backdropIndex() {
+        var topBackdropIndex = -1;
+        var opened = openedWindows.keys();
+        for (var i = 0; i < opened.length; i++) {
+          if (openedWindows.get(opened[i]).value.backdrop) {
+            topBackdropIndex = i;
+          }
+        }
+        return topBackdropIndex;
+      }
+
+      $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
+        if (backdropScope) {
+          backdropScope.index = newBackdropIndex;
+        }
+      });
+
+      function removeModalWindow(modalInstance, elementToReceiveFocus) {
+        var body = $document.find('body').eq(0);
+        var modalWindow = openedWindows.get(modalInstance).value;
+
+        //clean up the stack
+        openedWindows.remove(modalInstance);
+
+        removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
+          var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
+          openedClasses.remove(modalBodyClass, modalInstance);
+          body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
+        });
+        checkRemoveBackdrop();
+
+        //move focus to specified element if available, or else to body
+        if (elementToReceiveFocus && elementToReceiveFocus.focus) {
+          elementToReceiveFocus.focus();
+        } else {
+          body.focus();
+        }
+      }
+
+      function checkRemoveBackdrop() {
+          //remove backdrop if no longer needed
+          if (backdropDomEl && backdropIndex() == -1) {
+            var backdropScopeRef = backdropScope;
+            removeAfterAnimate(backdropDomEl, backdropScope, function() {
+              backdropScopeRef = null;
+            });
+            backdropDomEl = undefined;
+            backdropScope = undefined;
+          }
+      }
+
+      function removeAfterAnimate(domEl, scope, done) {
+        var asyncDeferred;
+        var asyncPromise = null;
+        var setIsAsync = function() {
+          if (!asyncDeferred) {
+            asyncDeferred = $q.defer();
+            asyncPromise = asyncDeferred.promise;
+          }
+
+          return function asyncDone() {
+            asyncDeferred.resolve();
+          };
+        };
+        scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
+
+        // Note that it's intentional that asyncPromise might be null.
+        // That's when setIsAsync has not been called during the
+        // NOW_CLOSING_EVENT broadcast.
+        return $q.when(asyncPromise).then(afterAnimating);
+
+        function afterAnimating() {
+          if (afterAnimating.done) {
+            return;
+          }
+          afterAnimating.done = true;
+
+          if ($animateCss) {
+            $animateCss(domEl, {
+              event: 'leave'
+            }).start().then(function() {
+              domEl.remove();
+            });
+          } else {
+            $animate.leave(domEl);
+          }
+          scope.$destroy();
+          if (done) {
+            done();
+          }
+        }
+      }
+
+      $document.bind('keydown', function(evt) {
+        if (evt.isDefaultPrevented()) {
+          return evt;
+        }
+
+        var modal = openedWindows.top();
+        if (modal && modal.value.keyboard) {
+          switch (evt.which){
+            case 27: {
+              evt.preventDefault();
+              $rootScope.$apply(function() {
+                $modalStack.dismiss(modal.key, 'escape key press');
+              });
+              break;
+            }
+            case 9: {
+              $modalStack.loadFocusElementList(modal);
+              var focusChanged = false;
+              if (evt.shiftKey) {
+                if ($modalStack.isFocusInFirstItem(evt)) {
+                  focusChanged = $modalStack.focusLastFocusableElement();
+                }
+              } else {
+                if ($modalStack.isFocusInLastItem(evt)) {
+                  focusChanged = $modalStack.focusFirstFocusableElement();
+                }
+              }
+
+              if (focusChanged) {
+                evt.preventDefault();
+                evt.stopPropagation();
+              }
+              break;
+            }
+          }
+        }
+      });
+
+      $modalStack.open = function(modalInstance, modal) {
+        var modalOpener = $document[0].activeElement,
+          modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
+
+        openedWindows.add(modalInstance, {
+          deferred: modal.deferred,
+          renderDeferred: modal.renderDeferred,
+          modalScope: modal.scope,
+          backdrop: modal.backdrop,
+          keyboard: modal.keyboard,
+          openedClass: modal.openedClass
+        });
+
+        openedClasses.put(modalBodyClass, modalInstance);
+
+        var body = $document.find('body').eq(0),
+            currBackdropIndex = backdropIndex();
+
+        if (currBackdropIndex >= 0 && !backdropDomEl) {
+          backdropScope = $rootScope.$new(true);
+          backdropScope.index = currBackdropIndex;
+          var angularBackgroundDomEl = angular.element('<div modal-backdrop="modal-backdrop"></div>');
+          angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
+          if (modal.animation) {
+            angularBackgroundDomEl.attr('modal-animation', 'true');
+          }
+          backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
+          body.append(backdropDomEl);
+        }
+
+        var angularDomEl = angular.element('<div modal-window="modal-window"></div>');
+        angularDomEl.attr({
+          'template-url': modal.windowTemplateUrl,
+          'window-class': modal.windowClass,
+          'size': modal.size,
+          'index': openedWindows.length() - 1,
+          'animate': 'animate'
+        }).html(modal.content);
+        if (modal.animation) {
+          angularDomEl.attr('modal-animation', 'true');
+        }
+
+        var modalDomEl = $compile(angularDomEl)(modal.scope);
+        openedWindows.top().value.modalDomEl = modalDomEl;
+        openedWindows.top().value.modalOpener = modalOpener;
+        body.append(modalDomEl);
+        body.addClass(modalBodyClass);
+
+        $modalStack.clearFocusListCache();
+      };
+
+      function broadcastClosing(modalWindow, resultOrReason, closing) {
+          return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
+      }
+
+      $modalStack.close = function(modalInstance, result) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow && broadcastClosing(modalWindow, result, true)) {
+          modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+          modalWindow.value.deferred.resolve(result);
+          removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+          return true;
+        }
+        return !modalWindow;
+      };
+
+      $modalStack.dismiss = function(modalInstance, reason) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
+          modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+          modalWindow.value.deferred.reject(reason);
+          removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+          return true;
+        }
+        return !modalWindow;
+      };
+
+      $modalStack.dismissAll = function(reason) {
+        var topModal = this.getTop();
+        while (topModal && this.dismiss(topModal.key, reason)) {
+          topModal = this.getTop();
+        }
+      };
+
+      $modalStack.getTop = function() {
+        return openedWindows.top();
+      };
+
+      $modalStack.modalRendered = function(modalInstance) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow) {
+          modalWindow.value.renderDeferred.resolve();
+        }
+      };
+
+      $modalStack.focusFirstFocusableElement = function() {
+        if (focusableElementList.length > 0) {
+          focusableElementList[0].focus();
+          return true;
+        }
+        return false;
+      };
+      $modalStack.focusLastFocusableElement = function() {
+        if (focusableElementList.length > 0) {
+          focusableElementList[focusableElementList.length - 1].focus();
+          return true;
+        }
+        return false;
+      };
+
+      $modalStack.isFocusInFirstItem = function(evt) {
+        if (focusableElementList.length > 0) {
+          return (evt.target || evt.srcElement) == focusableElementList[0];
+        }
+        return false;
+      };
+
+      $modalStack.isFocusInLastItem = function(evt) {
+        if (focusableElementList.length > 0) {
+          return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1];
+        }
+        return false;
+      };
+
+      $modalStack.clearFocusListCache = function() {
+        focusableElementList = [];
+        focusIndex = 0;
+      };
+
+      $modalStack.loadFocusElementList = function(modalWindow) {
+        if (focusableElementList === undefined || !focusableElementList.length0) {
+          if (modalWindow) {
+            var modalDomE1 = modalWindow.value.modalDomEl;
+            if (modalDomE1 && modalDomE1.length) {
+              focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector);
+            }
+          }
+        }
+      };
+
+      return $modalStack;
+    }])
+
+  .provider('$modal', function() {
+    var $modalProvider = {
+      options: {
+        animation: true,
+        backdrop: true, //can also be false or 'static'
+        keyboard: true
+      },
+      $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack',
+        function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) {
+          var $modal = {};
+
+          function getTemplatePromise(options) {
+            return options.template ? $q.when(options.template) :
+              $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);
+          }
+
+          function getResolvePromises(resolves) {
+            var promisesArr = [];
+            angular.forEach(resolves, function(value) {
+              if (angular.isFunction(value) || angular.isArray(value)) {
+                promisesArr.push($q.when($injector.invoke(value)));
+              } else if (angular.isString(value)) {
+                promisesArr.push($q.when($injector.get(value)));
+              } else {
+                promisesArr.push($q.when(value));
+              }
+            });
+            return promisesArr;
+          }
+
+          var promiseChain = null;
+          $modal.getPromiseChain = function() {
+            return promiseChain;
+          };
+
+          $modal.open = function (modalOptions) {
+
+            var modalResultDeferred = $q.defer();
+            var modalOpenedDeferred = $q.defer();
+            var modalRenderDeferred = $q.defer();
+
+            //prepare an instance of a modal to be injected into controllers and returned to a caller
+            var modalInstance = {
+              result: modalResultDeferred.promise,
+              opened: modalOpenedDeferred.promise,
+              rendered: modalRenderDeferred.promise,
+              close: function (result) {
+                return $modalStack.close(modalInstance, result);
+              },
+              dismiss: function (reason) {
+                return $modalStack.dismiss(modalInstance, reason);
+              }
+            };
+
+            //merge and clean up options
+            modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
+            modalOptions.resolve = modalOptions.resolve || {};
+
+            //verify options
+            if (!modalOptions.template && !modalOptions.templateUrl) {
+              throw new Error('One of template or templateUrl options is required.');
+            }
+
+            var templateAndResolvePromise =
+              $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
+
+            // Wait for the resolution of the existing promise chain.
+            // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
+            // Then add to $modalStack and resolve opened.
+            // Finally clean up the chain variable if no subsequent modal has overwritten it.
+            var samePromise;
+            samePromise = promiseChain = $q.all([promiseChain])
+              .then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; })
+              .then(function resolveSuccess(tplAndVars) {
+
+                var modalScope = (modalOptions.scope || $rootScope).$new();
+                modalScope.$close = modalInstance.close;
+                modalScope.$dismiss = modalInstance.dismiss;
+
+                modalScope.$on('$destroy', function() {
+                  if (!modalScope.$$uibDestructionScheduled) {
+                    modalScope.$dismiss('$uibUnscheduledDestruction');
+                  }
+                });
+
+                var ctrlInstance, ctrlLocals = {};
+                var resolveIter = 1;
+
+                //controllers
+                if (modalOptions.controller) {
+                  ctrlLocals.$scope = modalScope;
+                  ctrlLocals.$modalInstance = modalInstance;
+                  angular.forEach(modalOptions.resolve, function(value, key) {
+                    ctrlLocals[key] = tplAndVars[resolveIter++];
+                  });
+
+                  ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
+                  if (modalOptions.controllerAs) {
+                    if (modalOptions.bindToController) {
+                      angular.extend(ctrlInstance, modalScope);
+                    }
+
+                    modalScope[modalOptions.controllerAs] = ctrlInstance;
+                  }
+                }
+
+                $modalStack.open(modalInstance, {
+                  scope: modalScope,
+                  deferred: modalResultDeferred,
+                  renderDeferred: modalRenderDeferred,
+                  content: tplAndVars[0],
+                  animation: modalOptions.animation,
+                  backdrop: modalOptions.backdrop,
+                  keyboard: modalOptions.keyboard,
+                  backdropClass: modalOptions.backdropClass,
+                  windowClass: modalOptions.windowClass,
+                  windowTemplateUrl: modalOptions.windowTemplateUrl,
+                  size: modalOptions.size,
+                  openedClass: modalOptions.openedClass
+                });
+                modalOpenedDeferred.resolve(true);
+
+            }, function resolveError(reason) {
+              modalOpenedDeferred.reject(reason);
+              modalResultDeferred.reject(reason);
+            })
+            .finally(function() {
+              if (promiseChain === samePromise) {
+                promiseChain = null;
+              }
+            });
+
+            return modalInstance;
+          };
+
+          return $modal;
+        }]
+    };
+
+    return $modalProvider;
+  });
+
+angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
+
+.constant('dropdownConfig', {
+  openClass: 'open'
+})
+
+.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
+  var openScope = null;
+
+  this.open = function(dropdownScope) {
+    if (!openScope) {
+      $document.bind('click', closeDropdown);
+      $document.bind('keydown', keybindFilter);
+    }
+
+    if (openScope && openScope !== dropdownScope) {
+      openScope.isOpen = false;
+    }
+
+    openScope = dropdownScope;
+  };
+
+  this.close = function(dropdownScope) {
+    if (openScope === dropdownScope) {
+      openScope = null;
+      $document.unbind('click', closeDropdown);
+      $document.unbind('keydown', keybindFilter);
+    }
+  };
+
+  var closeDropdown = function(evt) {
+    // This method may still be called during the same mouse event that
+    // unbound this event handler. So check openScope before proceeding.
+    if (!openScope) { return; }
+
+    if (evt && openScope.getAutoClose() === 'disabled')  { return ; }
+
+    var toggleElement = openScope.getToggleElement();
+    if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
+      return;
+    }
+
+    var dropdownElement = openScope.getDropdownElement();
+    if (evt && openScope.getAutoClose() === 'outsideClick' &&
+      dropdownElement && dropdownElement[0].contains(evt.target)) {
+      return;
+    }
+
+    openScope.isOpen = false;
+
+    if (!$rootScope.$$phase) {
+      openScope.$apply();
+    }
+  };
+
+  var keybindFilter = function(evt) {
+    if (evt.which === 27) {
+      openScope.focusToggleElement();
+      closeDropdown();
+    } else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) {
+      evt.preventDefault();
+      evt.stopPropagation();
+      openScope.focusDropdownEntry(evt.which);
+    }
+  };
+}])
+
+.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
+  var self = this,
+    scope = $scope.$new(), // create a child scope so we are not polluting original one
+    templateScope,
+    openClass = dropdownConfig.openClass,
+    getIsOpen,
+    setIsOpen = angular.noop,
+    toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
+    appendToBody = false,
+    keynavEnabled = false,
+    selectedOption = null,
+    body = $document.find('body');
+
+  this.init = function(element) {
+    self.$element = element;
+
+    if ($attrs.isOpen) {
+      getIsOpen = $parse($attrs.isOpen);
+      setIsOpen = getIsOpen.assign;
+
+      $scope.$watch(getIsOpen, function(value) {
+        scope.isOpen = !!value;
+      });
+    }
+
+    appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
+    keynavEnabled = angular.isDefined($attrs.keyboardNav);
+
+    if (appendToBody && self.dropdownMenu) {
+      body.append(self.dropdownMenu);
+      body.addClass('dropdown');
+      element.on('$destroy', function handleDestroyEvent() {
+        self.dropdownMenu.remove();
+      });
+    }
+  };
+
+  this.toggle = function(open) {
+    return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
+  };
+
+  // Allow other directives to watch status
+  this.isOpen = function() {
+    return scope.isOpen;
+  };
+
+  scope.getToggleElement = function() {
+    return self.toggleElement;
+  };
+
+  scope.getAutoClose = function() {
+    return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
+  };
+
+  scope.getElement = function() {
+    return self.$element;
+  };
+
+  scope.isKeynavEnabled = function() {
+    return keynavEnabled;
+  };
+
+  scope.focusDropdownEntry = function(keyCode) {
+    var elems = self.dropdownMenu ? //If append to body is used.
+      (angular.element(self.dropdownMenu).find('a')) :
+      (angular.element(self.$element).find('ul').eq(0).find('a'));
+
+    switch (keyCode) {
+      case (40): {
+        if (!angular.isNumber(self.selectedOption)) {
+          self.selectedOption = 0;
+        } else {
+          self.selectedOption = (self.selectedOption === elems.length -1 ?
+            self.selectedOption :
+            self.selectedOption + 1);
+        }
+        break;
+      }
+      case (38): {
+        if (!angular.isNumber(self.selectedOption)) {
+          self.selectedOption = elems.length - 1;
+        } else {
+          self.selectedOption = self.selectedOption === 0 ?
+            0 : self.selectedOption - 1;
+        }
+        break;
+      }
+    }
+    elems[self.selectedOption].focus();
+  };
+
+  scope.getDropdownElement = function() {
+    return self.dropdownMenu;
+  };
+
+  scope.focusToggleElement = function() {
+    if (self.toggleElement) {
+      self.toggleElement[0].focus();
+    }
+  };
+
+  scope.$watch('isOpen', function(isOpen, wasOpen) {
+    if (appendToBody && self.dropdownMenu) {
+      var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
+      var css = {
+        top: pos.top + 'px',
+        display: isOpen ? 'block' : 'none'
+      };
+
+      var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
+      if (!rightalign) {
+        css.left = pos.left + 'px';
+        css.right = 'auto';
+      } else {
+        css.left = 'auto';
+        css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px';
+      }
+
+      self.dropdownMenu.css(css);
+    }
+
+    var openContainer = appendToBody ? body : self.$element;
+
+    $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
+      if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
+        toggleInvoker($scope, { open: !!isOpen });
+      }
+    });
+
+    if (isOpen) {
+      if (self.dropdownMenuTemplateUrl) {
+        $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
+          templateScope = scope.$new();
+          $compile(tplContent.trim())(templateScope, function(dropdownElement) {
+            var newEl = dropdownElement;
+            self.dropdownMenu.replaceWith(newEl);
+            self.dropdownMenu = newEl;
+          });
+        });
+      }
+
+      scope.focusToggleElement();
+      dropdownService.open(scope);
+    } else {
+      if (self.dropdownMenuTemplateUrl) {
+        if (templateScope) {
+          templateScope.$destroy();
+        }
+        var newEl = angular.element('<ul class="dropdown-menu"></ul>');
+        self.dropdownMenu.replaceWith(newEl);
+        self.dropdownMenu = newEl;
+      }
+
+      dropdownService.close(scope);
+      self.selectedOption = null;
+    }
+
+    if (angular.isFunction(setIsOpen)) {
+      setIsOpen($scope, isOpen);
+    }
+  });
+
+  $scope.$on('$locationChangeSuccess', function() {
+    if (scope.getAutoClose() !== 'disabled') {
+      scope.isOpen = false;
+    }
+  });
+
+  var offDestroy = $scope.$on('$destroy', function() {
+    scope.$destroy();
+  });
+  scope.$on('$destroy', offDestroy);
+}])
+
+.directive('dropdown', function() {
+  return {
+    controller: 'DropdownController',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      dropdownCtrl.init( element );
+      element.addClass('dropdown');
+    }
+  };
+})
+
+.directive('dropdownMenu', function() {
+  return {
+    restrict: 'AC',
+    require: '?^dropdown',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      if (!dropdownCtrl) {
+        return;
+      }
+      var tplUrl = attrs.templateUrl;
+      if (tplUrl) {
+        dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
+      }
+      if (!dropdownCtrl.dropdownMenu) {
+        dropdownCtrl.dropdownMenu = element;
+      }
+    }
+  };
+})
+
+.directive('keyboardNav', function() {
+  return {
+    restrict: 'A',
+    require: '?^dropdown',
+    link: function (scope, element, attrs, dropdownCtrl) {
+
+      element.bind('keydown', function(e) {
+        if ([38, 40].indexOf(e.which) !== -1) {
+          e.preventDefault();
+          e.stopPropagation();
+
+          var elems = dropdownCtrl.dropdownMenu.find('a');
+
+          switch (e.which) {
+            case (40): { // Down
+              if (!angular.isNumber(dropdownCtrl.selectedOption)) {
+                dropdownCtrl.selectedOption = 0;
+              } else {
+                dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
+                  dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
+              }
+              break;
+            }
+            case (38): { // Up
+              if (!angular.isNumber(dropdownCtrl.selectedOption)) {
+                dropdownCtrl.selectedOption = elems.length - 1;
+              } else {
+                dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
+                  0 : dropdownCtrl.selectedOption - 1;
+              }
+              break;
+            }
+          }
+          elems[dropdownCtrl.selectedOption].focus();
+        }
+      });
+    }
+  };
+})
+
+.directive('dropdownToggle', function() {
+  return {
+    require: '?^dropdown',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      if (!dropdownCtrl) {
+        return;
+      }
+
+      element.addClass('dropdown-toggle');
+
+      dropdownCtrl.toggleElement = element;
+
+      var toggleDropdown = function(event) {
+        event.preventDefault();
+
+        if (!element.hasClass('disabled') && !attrs.disabled) {
+          scope.$apply(function() {
+            dropdownCtrl.toggle();
+          });
+        }
+      };
+
+      element.bind('click', toggleDropdown);
+
+      // WAI-ARIA
+      element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
+      scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
+        element.attr('aria-expanded', !!isOpen);
+      });
+
+      scope.$on('$destroy', function() {
+        element.unbind('click', toggleDropdown);
+      });
+    }
+  };
+});
+
+angular.module('ui.bootstrap.rating', [])
+
+.constant('ratingConfig', {
+  max: 5,
+  stateOn: null,
+  stateOff: null,
+  titles : ['one', 'two', 'three', 'four', 'five']
+})
+
+.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
+  var ngModelCtrl  = { $setViewValue: angular.noop };
+
+  this.init = function(ngModelCtrl_) {
+    ngModelCtrl = ngModelCtrl_;
+    ngModelCtrl.$render = this.render;
+
+    ngModelCtrl.$formatters.push(function(value) {
+      if (angular.isNumber(value) && value << 0 !== value) {
+        value = Math.round(value);
+      }
+      return value;
+    });
+
+    this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
+    this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
+    var tmpTitles = angular.isDefined($attrs.titles)  ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ;    
+    this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
+      tmpTitles : ratingConfig.titles;
+    
+    var ratingStates = angular.isDefined($attrs.ratingStates) ?
+      $scope.$parent.$eval($attrs.ratingStates) :
+      new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
+    $scope.range = this.buildTemplateObjects(ratingStates);
+  };
+
+  this.buildTemplateObjects = function(states) {
+    for (var i = 0, n = states.length; i < n; i++) {
+      states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
+    }
+    return states;
+  };
+  
+  this.getTitle = function(index) {
+    if (index >= this.titles.length) {
+      return index + 1;
+    } else {
+      return this.titles[index];
+    }
+  };
+  
+  $scope.rate = function(value) {
+    if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
+      ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value);
+      ngModelCtrl.$render();
+    }
+  };
+
+  $scope.enter = function(value) {
+    if (!$scope.readonly) {
+      $scope.value = value;
+    }
+    $scope.onHover({value: value});
+  };
+
+  $scope.reset = function() {
+    $scope.value = ngModelCtrl.$viewValue;
+    $scope.onLeave();
+  };
+
+  $scope.onKeydown = function(evt) {
+    if (/(37|38|39|40)/.test(evt.which)) {
+      evt.preventDefault();
+      evt.stopPropagation();
+      $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
+    }
+  };
+
+  this.render = function() {
+    $scope.value = ngModelCtrl.$viewValue;
+  };
+}])
+
+.directive('rating', function() {
+  return {
+    restrict: 'EA',
+    require: ['rating', 'ngModel'],
+    scope: {
+      readonly: '=?',
+      onHover: '&',
+      onLeave: '&'
+    },
+    controller: 'RatingController',
+    templateUrl: 'template/rating/rating.html',
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+      ratingCtrl.init( ngModelCtrl );
+    }
+  };
+});
+
+angular.module('ui.bootstrap.transition', [])
+
+.value('$transitionSuppressDeprecated', false)
+/**
+ * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
+ * @param  {DOMElement} element  The DOMElement that will be animated.
+ * @param  {string|object|function} trigger  The thing that will cause the transition to start:
+ *   - As a string, it represents the css class to be added to the element.
+ *   - As an object, it represents a hash of style attributes to be applied to the element.
+ *   - As a function, it represents a function to be called that will cause the transition to occur.
+ * @return {Promise}  A promise that is resolved when the transition finishes.
+ */
+.factory('$transition', [
+        '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated',
+function($q ,  $timeout ,  $rootScope ,  $log ,  $transitionSuppressDeprecated) {
+
+  if (!$transitionSuppressDeprecated) {
+    $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.');
+  }
+
+  var $transition = function(element, trigger, options) {
+    options = options || {};
+    var deferred = $q.defer();
+    var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
+
+    var transitionEndHandler = function(event) {
+      $rootScope.$apply(function() {
+        element.unbind(endEventName, transitionEndHandler);
+        deferred.resolve(element);
+      });
+    };
+
+    if (endEventName) {
+      element.bind(endEventName, transitionEndHandler);
+    }
+
+    // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
+    $timeout(function() {
+      if ( angular.isString(trigger) ) {
+        element.addClass(trigger);
+      } else if ( angular.isFunction(trigger) ) {
+        trigger(element);
+      } else if ( angular.isObject(trigger) ) {
+        element.css(trigger);
+      }
+      //If browser does not support transitions, instantly resolve
+      if ( !endEventName ) {
+        deferred.resolve(element);
+      }
+    });
+
+    // Add our custom cancel function to the promise that is returned
+    // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
+    // i.e. it will therefore never raise a transitionEnd event for that transition
+    deferred.promise.cancel = function() {
+      if ( endEventName ) {
+        element.unbind(endEventName, transitionEndHandler);
+      }
+      deferred.reject('Transition cancelled');
+    };
+
+    return deferred.promise;
+  };
+
+  // Work out the name of the transitionEnd event
+  var transElement = document.createElement('trans');
+  var transitionEndEventNames = {
+    'WebkitTransition': 'webkitTransitionEnd',
+    'MozTransition': 'transitionend',
+    'OTransition': 'oTransitionEnd',
+    'transition': 'transitionend'
+  };
+  var animationEndEventNames = {
+    'WebkitTransition': 'webkitAnimationEnd',
+    'MozTransition': 'animationend',
+    'OTransition': 'oAnimationEnd',
+    'transition': 'animationend'
+  };
+  function findEndEventName(endEventNames) {
+    for (var name in endEventNames){
+      if (transElement.style[name] !== undefined) {
+        return endEventNames[name];
+      }
+    }
+  }
+  $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
+  $transition.animationEndEventName = findEndEventName(animationEndEventNames);
+  return $transition;
+}]);
+!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.min.js b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.min.js
new file mode 100755
index 0000000..fdb9726
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-0.13.4.min.js
@@ -0,0 +1,10 @@
+/*
+ * angular-ui-bootstrap
+ * http://angular-ui.github.io/bootstrap/
+
+ * Version: 0.13.4 - 2015-09-03
+ * License: MIT
+ */
+angular.module("ui.bootstrap",["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.buttons","ui.bootstrap.bindHtml","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.progressbar","ui.bootstrap.pagination","ui.bootstrap.timepicker","ui.bootstrap.typeahead","ui.bootstrap.tabs","ui.bootstrap.popover","ui.bootstrap.tooltip","ui.bootstrap.modal","ui.bootstrap.dropdown","ui.bootstrap.rating","ui.boo [...]
+});var p=s.render;s.render=function(){p(),n.page>0&&n.page<=n.totalPages&&(n.pages=l(n.page,n.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(e){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["pager","?ngModel"],controller:"PaginationController",controllerAs:"pagination",templateUrl:function(e,t){return t.templateUrl||"template/pa [...]
+}var $=v?b:p.$element;if(r[t?"addClass":"removeClass"]($,f).then(function(){angular.isDefined(t)&&t!==n&&h(e,{open:!!t})}),t)p.dropdownMenuTemplateUrl&&c(p.dropdownMenuTemplateUrl).then(function(e){u=m.$new(),s(e.trim())(u,function(e){var t=e;p.dropdownMenu.replaceWith(t),p.dropdownMenu=t})}),m.focusToggleElement(),o.open(m);else{if(p.dropdownMenuTemplateUrl){u&&u.$destroy();var w=angular.element('<ul class="dropdown-menu"></ul>');p.dropdownMenu.replaceWith(w),p.dropdownMenu=w}o.close(m) [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.js b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.js
new file mode 100755
index 0000000..222e6e1
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.js
@@ -0,0 +1,5836 @@
+/*
+ * angular-ui-bootstrap
+ * http://angular-ui.github.io/bootstrap/
+
+ * Version: 0.13.4 - 2015-09-03
+ * License: MIT
+ */
+angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.buttons","ui.bootstrap.bindHtml","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.progressbar","ui.bootstrap.pagination","ui.bootstrap.timepicker","ui.bootstrap.typeahead","ui.bootstrap.tabs","ui.bootstrap.popover","ui.bootstrap.tooltip","ui.bootstrap.modal","ui.bootstrap.dropdown","ui.boot [...]
+angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","templat [...]
+angular.module('ui.bootstrap.collapse', [])
+
+  .directive('collapse', ['$animate', function($animate) {
+    return {
+      link: function(scope, element, attrs) {
+        function expand() {
+          element.removeClass('collapse')
+            .addClass('collapsing')
+            .attr('aria-expanded', true)
+            .attr('aria-hidden', false);
+
+          $animate.addClass(element, 'in', {
+            to: { height: element[0].scrollHeight + 'px' }
+          }).then(expandDone);
+        }
+
+        function expandDone() {
+          element.removeClass('collapsing');
+          element.css({height: 'auto'});
+        }
+
+        function collapse() {
+          if (!element.hasClass('collapse') && !element.hasClass('in')) {
+            return collapseDone();
+          }
+
+          element
+            // IMPORTANT: The height must be set before adding "collapsing" class.
+            // Otherwise, the browser attempts to animate from height 0 (in
+            // collapsing class) to the given height here.
+            .css({height: element[0].scrollHeight + 'px'})
+            // initially all panel collapse have the collapse class, this removal
+            // prevents the animation from jumping to collapsed state
+            .removeClass('collapse')
+            .addClass('collapsing')
+            .attr('aria-expanded', false)
+            .attr('aria-hidden', true);
+
+          $animate.removeClass(element, 'in', {
+            to: {height: '0'}
+          }).then(collapseDone);
+        }
+
+        function collapseDone() {
+          element.css({height: '0'}); // Required so that collapse works when animation is disabled
+          element.removeClass('collapsing');
+          element.addClass('collapse');
+        }
+
+        scope.$watch(attrs.collapse, function(shouldCollapse) {
+          if (shouldCollapse) {
+            collapse();
+          } else {
+            expand();
+          }
+        });
+      }
+    };
+  }]);
+
+angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
+
+.constant('accordionConfig', {
+  closeOthers: true
+})
+
+.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function($scope, $attrs, accordionConfig) {
+  // This array keeps track of the accordion groups
+  this.groups = [];
+
+  // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
+  this.closeOthers = function(openGroup) {
+    var closeOthers = angular.isDefined($attrs.closeOthers) ?
+      $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
+    if (closeOthers) {
+      angular.forEach(this.groups, function(group) {
+        if (group !== openGroup) {
+          group.isOpen = false;
+        }
+      });
+    }
+  };
+
+  // This is called from the accordion-group directive to add itself to the accordion
+  this.addGroup = function(groupScope) {
+    var that = this;
+    this.groups.push(groupScope);
+
+    groupScope.$on('$destroy', function(event) {
+      that.removeGroup(groupScope);
+    });
+  };
+
+  // This is called from the accordion-group directive when to remove itself
+  this.removeGroup = function(group) {
+    var index = this.groups.indexOf(group);
+    if (index !== -1) {
+      this.groups.splice(index, 1);
+    }
+  };
+
+}])
+
+// The accordion directive simply sets up the directive controller
+// and adds an accordion CSS class to itself element.
+.directive('accordion', function() {
+  return {
+    restrict: 'EA',
+    controller: 'AccordionController',
+    controllerAs: 'accordion',
+    transclude: true,
+    replace: false,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/accordion/accordion.html';
+    }
+  };
+})
+
+// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
+.directive('accordionGroup', function() {
+  return {
+    require: '^accordion',         // We need this directive to be inside an accordion
+    restrict: 'EA',
+    transclude: true,              // It transcludes the contents of the directive into the template
+    replace: true,                // The element containing the directive will be replaced with the template
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/accordion/accordion-group.html';
+    },
+    scope: {
+      heading: '@',               // Interpolate the heading attribute onto this scope
+      isOpen: '=?',
+      isDisabled: '=?'
+    },
+    controller: function() {
+      this.setHeading = function(element) {
+        this.heading = element;
+      };
+    },
+    link: function(scope, element, attrs, accordionCtrl) {
+      accordionCtrl.addGroup(scope);
+
+      scope.openClass = attrs.openClass || 'panel-open';
+      scope.panelClass = attrs.panelClass;
+      scope.$watch('isOpen', function(value) {
+        element.toggleClass(scope.openClass, value);
+        if (value) {
+          accordionCtrl.closeOthers(scope);
+        }
+      });
+
+      scope.toggleOpen = function($event) {
+        if (!scope.isDisabled) {
+          if (!$event || $event.which === 32) {
+            scope.isOpen = !scope.isOpen;
+          }
+        }
+      };
+    }
+  };
+})
+
+// Use accordion-heading below an accordion-group to provide a heading containing HTML
+// <accordion-group>
+//   <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
+// </accordion-group>
+.directive('accordionHeading', function() {
+  return {
+    restrict: 'EA',
+    transclude: true,   // Grab the contents to be used as the heading
+    template: '',       // In effect remove this element!
+    replace: true,
+    require: '^accordionGroup',
+    link: function(scope, element, attr, accordionGroupCtrl, transclude) {
+      // Pass the heading to the accordion-group controller
+      // so that it can be transcluded into the right place in the template
+      // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
+      accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
+    }
+  };
+})
+
+// Use in the accordion-group template to indicate where you want the heading to be transcluded
+// You must provide the property on the accordion-group controller that will hold the transcluded element
+// <div class="accordion-group">
+//   <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
+//   ...
+// </div>
+.directive('accordionTransclude', function() {
+  return {
+    require: '^accordionGroup',
+    link: function(scope, element, attr, controller) {
+      scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
+        if (heading) {
+          element.find('span').html('');
+          element.find('span').append(heading);
+        }
+      });
+    }
+  };
+})
+
+;
+
+angular.module('ui.bootstrap.alert', [])
+
+.controller('AlertController', ['$scope', '$attrs', function($scope, $attrs) {
+  $scope.closeable = !!$attrs.close;
+  this.close = $scope.close;
+}])
+
+.directive('alert', function() {
+  return {
+    controller: 'AlertController',
+    controllerAs: 'alert',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/alert/alert.html';
+    },
+    transclude: true,
+    replace: true,
+    scope: {
+      type: '@',
+      close: '&'
+    }
+  };
+})
+
+.directive('dismissOnTimeout', ['$timeout', function($timeout) {
+  return {
+    require: 'alert',
+    link: function(scope, element, attrs, alertCtrl) {
+      $timeout(function() {
+        alertCtrl.close();
+      }, parseInt(attrs.dismissOnTimeout, 10));
+    }
+  };
+}]);
+
+/**
+* @ngdoc overview
+* @name ui.bootstrap.carousel
+*
+* @description
+* AngularJS version of an image carousel.
+*
+*/
+angular.module('ui.bootstrap.carousel', [])
+.controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) {
+  var self = this,
+    slides = self.slides = $scope.slides = [],
+    NEW_ANIMATE = angular.version.minor >= 4,
+    NO_TRANSITION = 'uib-noTransition',
+    SLIDE_DIRECTION = 'uib-slideDirection',
+    currentIndex = -1,
+    currentInterval, isPlaying;
+  self.currentSlide = null;
+
+  var destroyed = false;
+  /* direction: "prev" or "next" */
+  self.select = $scope.select = function(nextSlide, direction) {
+    var nextIndex = $scope.indexOfSlide(nextSlide);
+    //Decide direction if it's not given
+    if (direction === undefined) {
+      direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
+    }
+    //Prevent this user-triggered transition from occurring if there is already one in progress
+    if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) {
+      goNext(nextSlide, nextIndex, direction);
+    }
+  };
+
+  function goNext(slide, index, direction) {
+    // Scope has been destroyed, stop here.
+    if (destroyed) { return; }
+
+    angular.extend(slide, {direction: direction, active: true});
+    angular.extend(self.currentSlide || {}, {direction: direction, active: false});
+    if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition &&
+      slide.$element && self.slides.length > 1) {
+      slide.$element.data(SLIDE_DIRECTION, slide.direction);
+      if (self.currentSlide && self.currentSlide.$element) {
+        self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction);
+      }
+
+      $scope.$currentTransition = true;
+      if (NEW_ANIMATE) {
+        $animate.on('addClass', slide.$element, function (element, phase) {
+          if (phase === 'close') {
+            $scope.$currentTransition = null;
+            $animate.off('addClass', element);
+          }
+        });
+      } else {
+        slide.$element.one('$animate:close', function closeFn() {
+          $scope.$currentTransition = null;
+        });
+      }
+    }
+
+    self.currentSlide = slide;
+    currentIndex = index;
+
+    //every time you change slides, reset the timer
+    restartTimer();
+  }
+
+  $scope.$on('$destroy', function () {
+    destroyed = true;
+  });
+
+  function getSlideByIndex(index) {
+    if (angular.isUndefined(slides[index].index)) {
+      return slides[index];
+    }
+    var i, len = slides.length;
+    for (i = 0; i < slides.length; ++i) {
+      if (slides[i].index == index) {
+        return slides[i];
+      }
+    }
+  }
+
+  self.getCurrentIndex = function() {
+    if (self.currentSlide && angular.isDefined(self.currentSlide.index)) {
+      return +self.currentSlide.index;
+    }
+    return currentIndex;
+  };
+
+  /* Allow outside people to call indexOf on slides array */
+  $scope.indexOfSlide = function(slide) {
+    return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
+  };
+
+  $scope.next = function() {
+    var newIndex = (self.getCurrentIndex() + 1) % slides.length;
+
+    if (newIndex === 0 && $scope.noWrap()) {
+      $scope.pause();
+      return;
+    }
+
+    return self.select(getSlideByIndex(newIndex), 'next');
+  };
+
+  $scope.prev = function() {
+    var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
+
+    if ($scope.noWrap() && newIndex === slides.length - 1){
+      $scope.pause();
+      return;
+    }
+
+    return self.select(getSlideByIndex(newIndex), 'prev');
+  };
+
+  $scope.isActive = function(slide) {
+     return self.currentSlide === slide;
+  };
+
+  $scope.$watch('interval', restartTimer);
+  $scope.$on('$destroy', resetTimer);
+
+  function restartTimer() {
+    resetTimer();
+    var interval = +$scope.interval;
+    if (!isNaN(interval) && interval > 0) {
+      currentInterval = $interval(timerFn, interval);
+    }
+  }
+
+  function resetTimer() {
+    if (currentInterval) {
+      $interval.cancel(currentInterval);
+      currentInterval = null;
+    }
+  }
+
+  function timerFn() {
+    var interval = +$scope.interval;
+    if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
+      $scope.next();
+    } else {
+      $scope.pause();
+    }
+  }
+
+  $scope.play = function() {
+    if (!isPlaying) {
+      isPlaying = true;
+      restartTimer();
+    }
+  };
+  $scope.pause = function() {
+    if (!$scope.noPause) {
+      isPlaying = false;
+      resetTimer();
+    }
+  };
+
+  self.addSlide = function(slide, element) {
+    slide.$element = element;
+    slides.push(slide);
+    //if this is the first slide or the slide is set to active, select it
+    if(slides.length === 1 || slide.active) {
+      self.select(slides[slides.length-1]);
+      if (slides.length == 1) {
+        $scope.play();
+      }
+    } else {
+      slide.active = false;
+    }
+  };
+
+  self.removeSlide = function(slide) {
+    if (angular.isDefined(slide.index)) {
+      slides.sort(function(a, b) {
+        return +a.index > +b.index;
+      });
+    }
+    //get the index of the slide inside the carousel
+    var index = slides.indexOf(slide);
+    slides.splice(index, 1);
+    if (slides.length > 0 && slide.active) {
+      if (index >= slides.length) {
+        self.select(slides[index-1]);
+      } else {
+        self.select(slides[index]);
+      }
+    } else if (currentIndex > index) {
+      currentIndex--;
+    }
+    
+    //clean the currentSlide when no more slide
+    if (slides.length === 0) {
+      self.currentSlide = null;
+    }
+  };
+
+  $scope.$watch('noTransition', function(noTransition) {
+    $element.data(NO_TRANSITION, noTransition);
+  });
+
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.carousel.directive:carousel
+ * @restrict EA
+ *
+ * @description
+ * Carousel is the outer container for a set of image 'slides' to showcase.
+ *
+ * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
+ * @param {boolean=} noTransition Whether to disable transitions on the carousel.
+ * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <carousel>
+      <slide>
+        <img src="http://placekitten.com/150/150" style="margin:auto;">
+        <div class="carousel-caption">
+          <p>Beautiful!</p>
+        </div>
+      </slide>
+      <slide>
+        <img src="http://placekitten.com/100/150" style="margin:auto;">
+        <div class="carousel-caption">
+          <p>D'aww!</p>
+        </div>
+      </slide>
+    </carousel>
+  </file>
+  <file name="demo.css">
+    .carousel-indicators {
+      top: auto;
+      bottom: 15px;
+    }
+  </file>
+</example>
+ */
+.directive('carousel', [function() {
+  return {
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    controller: 'CarouselController',
+    controllerAs: 'carousel',
+    require: 'carousel',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/carousel/carousel.html';
+    },
+    scope: {
+      interval: '=',
+      noTransition: '=',
+      noPause: '=',
+      noWrap: '&'
+    }
+  };
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.carousel.directive:slide
+ * @restrict EA
+ *
+ * @description
+ * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}.  Must be placed as a child of a carousel element.
+ *
+ * @param {boolean=} active Model binding, whether or not this slide is currently active.
+ * @param {number=} index The index of the slide. The slides will be sorted by this parameter.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+<div ng-controller="CarouselDemoCtrl">
+  <carousel>
+    <slide ng-repeat="slide in slides" active="slide.active" index="$index">
+      <img ng-src="{{slide.image}}" style="margin:auto;">
+      <div class="carousel-caption">
+        <h4>Slide {{$index}}</h4>
+        <p>{{slide.text}}</p>
+      </div>
+    </slide>
+  </carousel>
+  Interval, in milliseconds: <input type="number" ng-model="myInterval">
+  <br />Enter a negative number to stop the interval.
+</div>
+  </file>
+  <file name="script.js">
+function CarouselDemoCtrl($scope) {
+  $scope.myInterval = 5000;
+}
+  </file>
+  <file name="demo.css">
+    .carousel-indicators {
+      top: auto;
+      bottom: 15px;
+    }
+  </file>
+</example>
+*/
+
+.directive('slide', function() {
+  return {
+    require: '^carousel',
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/carousel/slide.html';
+    },
+    scope: {
+      active: '=?',
+      actual: '=?',
+      index: '=?'
+    },
+    link: function (scope, element, attrs, carouselCtrl) {
+      carouselCtrl.addSlide(scope, element);
+      //when the scope is destroyed then remove the slide from the current slides array
+      scope.$on('$destroy', function() {
+        carouselCtrl.removeSlide(scope);
+      });
+
+      scope.$watch('active', function(active) {
+        if (active) {
+          carouselCtrl.select(scope);
+        }
+      });
+    }
+  };
+})
+
+.animation('.item', [
+         '$injector', '$animate',
+function ($injector, $animate) {
+  var NO_TRANSITION = 'uib-noTransition',
+    SLIDE_DIRECTION = 'uib-slideDirection',
+    $animateCss = null;
+
+  if ($injector.has('$animateCss')) {
+    $animateCss = $injector.get('$animateCss');
+  }
+
+  function removeClass(element, className, callback) {
+    element.removeClass(className);
+    if (callback) {
+      callback();
+    }
+  }
+
+  return {
+    beforeAddClass: function (element, className, done) {
+      // Due to transclusion, noTransition property is on parent's scope
+      if (className == 'active' && element.parent() &&
+          !element.parent().data(NO_TRANSITION)) {
+        var stopped = false;
+        var direction = element.data(SLIDE_DIRECTION);
+        var directionClass = direction == 'next' ? 'left' : 'right';
+        var removeClassFn = removeClass.bind(this, element,
+          directionClass + ' ' + direction, done);
+        element.addClass(direction);
+
+        if ($animateCss) {
+          $animateCss(element, {addClass: directionClass})
+            .start()
+            .done(removeClassFn);
+        } else {
+          $animate.addClass(element, directionClass).then(function () {
+            if (!stopped) {
+              removeClassFn();
+            }
+            done();
+          });
+        }
+
+        return function () {
+          stopped = true;
+        };
+      }
+      done();
+    },
+    beforeRemoveClass: function (element, className, done) {
+      // Due to transclusion, noTransition property is on parent's scope
+      if (className === 'active' && element.parent() &&
+          !element.parent().data(NO_TRANSITION)) {
+        var stopped = false;
+        var direction = element.data(SLIDE_DIRECTION);
+        var directionClass = direction == 'next' ? 'left' : 'right';
+        var removeClassFn = removeClass.bind(this, element, directionClass, done);
+
+        if ($animateCss) {
+          $animateCss(element, {addClass: directionClass})
+            .start()
+            .done(removeClassFn);
+        } else {
+          $animate.addClass(element, directionClass).then(function () {
+            if (!stopped) {
+              removeClassFn();
+            }
+            done();
+          });
+        }
+        return function () {
+          stopped = true;
+        };
+      }
+      done();
+    }
+  };
+
+}])
+
+
+;
+
+angular.module('ui.bootstrap.buttons', [])
+
+.constant('buttonConfig', {
+  activeClass: 'active',
+  toggleEvent: 'click'
+})
+
+.controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
+  this.activeClass = buttonConfig.activeClass || 'active';
+  this.toggleEvent = buttonConfig.toggleEvent || 'click';
+}])
+
+.directive('btnRadio', function() {
+  return {
+    require: ['btnRadio', 'ngModel'],
+    controller: 'ButtonsController',
+    controllerAs: 'buttons',
+    link: function(scope, element, attrs, ctrls) {
+      var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      element.find('input').css({display: 'none'});
+
+      //model -> UI
+      ngModelCtrl.$render = function() {
+        element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
+      };
+
+      //ui->model
+      element.bind(buttonsCtrl.toggleEvent, function() {
+        if (attrs.disabled) {
+          return;
+        }
+
+        var isActive = element.hasClass(buttonsCtrl.activeClass);
+
+        if (!isActive || angular.isDefined(attrs.uncheckable)) {
+          scope.$apply(function() {
+            ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
+            ngModelCtrl.$render();
+          });
+        }
+      });
+    }
+  };
+})
+
+.directive('btnCheckbox', ['$document', function($document) {
+  return {
+    require: ['btnCheckbox', 'ngModel'],
+    controller: 'ButtonsController',
+    controllerAs: 'button',
+    link: function(scope, element, attrs, ctrls) {
+      var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      element.find('input').css({display: 'none'});
+
+      function getTrueValue() {
+        return getCheckboxValue(attrs.btnCheckboxTrue, true);
+      }
+
+      function getFalseValue() {
+        return getCheckboxValue(attrs.btnCheckboxFalse, false);
+      }
+
+      function getCheckboxValue(attributeValue, defaultValue) {
+        var val = scope.$eval(attributeValue);
+        return angular.isDefined(val) ? val : defaultValue;
+      }
+
+      //model -> UI
+      ngModelCtrl.$render = function() {
+        element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
+      };
+
+      //ui->model
+      element.bind(buttonsCtrl.toggleEvent, function() {
+        if (attrs.disabled) {
+          return;
+        }
+
+        scope.$apply(function() {
+          ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
+          ngModelCtrl.$render();
+        });
+      });
+
+      //accessibility
+      element.on('keypress', function(e) {
+        if (attrs.disabled || e.which !== 32 || $document[0].activeElement !== element[0]) {
+          return;
+        }
+
+        scope.$apply(function() {
+          ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
+          ngModelCtrl.$render();
+        });
+      });
+    }
+  };
+}]);
+
+angular.module('ui.bootstrap.bindHtml', [])
+
+  .value('$bindHtmlUnsafeSuppressDeprecated', false)
+
+  .directive('bindHtmlUnsafe', ['$log', '$bindHtmlUnsafeSuppressDeprecated', function ($log, $bindHtmlUnsafeSuppressDeprecated) {
+    return function (scope, element, attr) {
+      if (!$bindHtmlUnsafeSuppressDeprecated) {
+        $log.warn('bindHtmlUnsafe is now deprecated. Use ngBindHtml instead');
+      }
+      element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
+      scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
+        element.html(value || '');
+      });
+    };
+  }]);
+angular.module('ui.bootstrap.dateparser', [])
+
+.service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) {
+  // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
+  var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
+
+  this.parsers = {};
+
+  var formatCodeToRegex = {
+    'yyyy': {
+      regex: '\\d{4}',
+      apply: function(value) { this.year = +value; }
+    },
+    'yy': {
+      regex: '\\d{2}',
+      apply: function(value) { this.year = +value + 2000; }
+    },
+    'y': {
+      regex: '\\d{1,4}',
+      apply: function(value) { this.year = +value; }
+    },
+    'MMMM': {
+      regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
+      apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
+    },
+    'MMM': {
+      regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
+      apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
+    },
+    'MM': {
+      regex: '0[1-9]|1[0-2]',
+      apply: function(value) { this.month = value - 1; }
+    },
+    'M': {
+      regex: '[1-9]|1[0-2]',
+      apply: function(value) { this.month = value - 1; }
+    },
+    'dd': {
+      regex: '[0-2][0-9]{1}|3[0-1]{1}',
+      apply: function(value) { this.date = +value; }
+    },
+    'd': {
+      regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
+      apply: function(value) { this.date = +value; }
+    },
+    'EEEE': {
+      regex: $locale.DATETIME_FORMATS.DAY.join('|')
+    },
+    'EEE': {
+      regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
+    },
+    'HH': {
+      regex: '(?:0|1)[0-9]|2[0-3]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'hh': {
+      regex: '0[0-9]|1[0-2]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'H': {
+      regex: '1?[0-9]|2[0-3]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'h': {
+      regex: '[0-9]|1[0-2]',
+      apply: function(value) { this.hours = +value; }
+    },
+    'mm': {
+      regex: '[0-5][0-9]',
+      apply: function(value) { this.minutes = +value; }
+    },
+    'm': {
+      regex: '[0-9]|[1-5][0-9]',
+      apply: function(value) { this.minutes = +value; }
+    },
+    'sss': {
+      regex: '[0-9][0-9][0-9]',
+      apply: function(value) { this.milliseconds = +value; }
+    },
+    'ss': {
+      regex: '[0-5][0-9]',
+      apply: function(value) { this.seconds = +value; }
+    },
+    's': {
+      regex: '[0-9]|[1-5][0-9]',
+      apply: function(value) { this.seconds = +value; }
+    },
+    'a': {
+      regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
+      apply: function(value) {
+        if (this.hours === 12) {
+          this.hours = 0;
+        }
+
+        if (value === 'PM') {
+          this.hours += 12;
+        }
+      }
+    }
+  };
+
+  function createParser(format) {
+    var map = [], regex = format.split('');
+
+    angular.forEach(formatCodeToRegex, function(data, code) {
+      var index = format.indexOf(code);
+
+      if (index > -1) {
+        format = format.split('');
+
+        regex[index] = '(' + data.regex + ')';
+        format[index] = '$'; // Custom symbol to define consumed part of format
+        for (var i = index + 1, n = index + code.length; i < n; i++) {
+          regex[i] = '';
+          format[i] = '$';
+        }
+        format = format.join('');
+
+        map.push({ index: index, apply: data.apply });
+      }
+    });
+
+    return {
+      regex: new RegExp('^' + regex.join('') + '$'),
+      map: orderByFilter(map, 'index')
+    };
+  }
+
+  this.parse = function(input, format, baseDate) {
+    if (!angular.isString(input) || !format) {
+      return input;
+    }
+
+    format = $locale.DATETIME_FORMATS[format] || format;
+    format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
+
+    if (!this.parsers[format]) {
+      this.parsers[format] = createParser(format);
+    }
+
+    var parser = this.parsers[format],
+        regex = parser.regex,
+        map = parser.map,
+        results = input.match(regex);
+
+    if (results && results.length) {
+      var fields, dt;
+      if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
+        fields = {
+          year: baseDate.getFullYear(),
+          month: baseDate.getMonth(),
+          date: baseDate.getDate(),
+          hours: baseDate.getHours(),
+          minutes: baseDate.getMinutes(),
+          seconds: baseDate.getSeconds(),
+          milliseconds: baseDate.getMilliseconds()
+        };
+      } else {
+        if (baseDate) {
+          $log.warn('dateparser:', 'baseDate is not a valid date');
+        }
+        fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
+      }
+
+      for (var i = 1, n = results.length; i < n; i++) {
+        var mapper = map[i-1];
+        if (mapper.apply) {
+          mapper.apply.call(fields, results[i]);
+        }
+      }
+
+      if (isValid(fields.year, fields.month, fields.date)) {
+        dt = new Date(fields.year, fields.month, fields.date,
+          fields.hours, fields.minutes, fields.seconds,
+          fields.milliseconds || 0);
+      }
+
+      return dt;
+    }
+  };
+
+  // Check if date is valid for specific month (and year for February).
+  // Month: 0 = Jan, 1 = Feb, etc
+  function isValid(year, month, date) {
+    if (date < 1) {
+      return false;
+    }
+
+    if (month === 1 && date > 28) {
+      return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
+    }
+
+    if (month === 3 || month === 5 || month === 8 || month === 10) {
+      return date < 31;
+    }
+
+    return true;
+  }
+}]);
+
+angular.module('ui.bootstrap.position', [])
+
+/**
+ * A set of utility methods that can be use to retrieve position of DOM elements.
+ * It is meant to be used where we need to absolute-position DOM elements in
+ * relation to other, existing elements (this is the case for tooltips, popovers,
+ * typeahead suggestions etc.).
+ */
+  .factory('$position', ['$document', '$window', function($document, $window) {
+    function getStyle(el, cssprop) {
+      if (el.currentStyle) { //IE
+        return el.currentStyle[cssprop];
+      } else if ($window.getComputedStyle) {
+        return $window.getComputedStyle(el)[cssprop];
+      }
+      // finally try and get inline style
+      return el.style[cssprop];
+    }
+
+    /**
+     * Checks if a given element is statically positioned
+     * @param element - raw DOM element
+     */
+    function isStaticPositioned(element) {
+      return (getStyle(element, 'position') || 'static' ) === 'static';
+    }
+
+    /**
+     * returns the closest, non-statically positioned parentOffset of a given element
+     * @param element
+     */
+    var parentOffsetEl = function(element) {
+      var docDomEl = $document[0];
+      var offsetParent = element.offsetParent || docDomEl;
+      while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
+        offsetParent = offsetParent.offsetParent;
+      }
+      return offsetParent || docDomEl;
+    };
+
+    return {
+      /**
+       * Provides read-only equivalent of jQuery's position function:
+       * http://api.jquery.com/position/
+       */
+      position: function(element) {
+        var elBCR = this.offset(element);
+        var offsetParentBCR = { top: 0, left: 0 };
+        var offsetParentEl = parentOffsetEl(element[0]);
+        if (offsetParentEl != $document[0]) {
+          offsetParentBCR = this.offset(angular.element(offsetParentEl));
+          offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
+          offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+        }
+
+        var boundingClientRect = element[0].getBoundingClientRect();
+        return {
+          width: boundingClientRect.width || element.prop('offsetWidth'),
+          height: boundingClientRect.height || element.prop('offsetHeight'),
+          top: elBCR.top - offsetParentBCR.top,
+          left: elBCR.left - offsetParentBCR.left
+        };
+      },
+
+      /**
+       * Provides read-only equivalent of jQuery's offset function:
+       * http://api.jquery.com/offset/
+       */
+      offset: function(element) {
+        var boundingClientRect = element[0].getBoundingClientRect();
+        return {
+          width: boundingClientRect.width || element.prop('offsetWidth'),
+          height: boundingClientRect.height || element.prop('offsetHeight'),
+          top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
+          left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+        };
+      },
+
+      /**
+       * Provides coordinates for the targetEl in relation to hostEl
+       */
+      positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
+        var positionStrParts = positionStr.split('-');
+        var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
+
+        var hostElPos,
+          targetElWidth,
+          targetElHeight,
+          targetElPos;
+
+        hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
+
+        targetElWidth = targetEl.prop('offsetWidth');
+        targetElHeight = targetEl.prop('offsetHeight');
+
+        var shiftWidth = {
+          center: function() {
+            return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
+          },
+          left: function() {
+            return hostElPos.left;
+          },
+          right: function() {
+            return hostElPos.left + hostElPos.width;
+          }
+        };
+
+        var shiftHeight = {
+          center: function() {
+            return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
+          },
+          top: function() {
+            return hostElPos.top;
+          },
+          bottom: function() {
+            return hostElPos.top + hostElPos.height;
+          }
+        };
+
+        switch (pos0) {
+          case 'right':
+            targetElPos = {
+              top: shiftHeight[pos1](),
+              left: shiftWidth[pos0]()
+            };
+            break;
+          case 'left':
+            targetElPos = {
+              top: shiftHeight[pos1](),
+              left: hostElPos.left - targetElWidth
+            };
+            break;
+          case 'bottom':
+            targetElPos = {
+              top: shiftHeight[pos0](),
+              left: shiftWidth[pos1]()
+            };
+            break;
+          default:
+            targetElPos = {
+              top: hostElPos.top - targetElHeight,
+              left: shiftWidth[pos1]()
+            };
+            break;
+        }
+
+        return targetElPos;
+      }
+    };
+  }]);
+
+angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
+
+.value('$datepickerSuppressError', false)
+
+.constant('datepickerConfig', {
+  formatDay: 'dd',
+  formatMonth: 'MMMM',
+  formatYear: 'yyyy',
+  formatDayHeader: 'EEE',
+  formatDayTitle: 'MMMM yyyy',
+  formatMonthTitle: 'yyyy',
+  datepickerMode: 'day',
+  minMode: 'day',
+  maxMode: 'year',
+  showWeeks: true,
+  startingDay: 0,
+  yearRange: 20,
+  minDate: null,
+  maxDate: null,
+  shortcutPropagation: false
+})
+
+.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
+  var self = this,
+      ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
+
+  // Modes chain
+  this.modes = ['day', 'month', 'year'];
+
+  // Configuration attributes
+  angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
+                   'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
+    self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
+  });
+
+  // Watchable date attributes
+  angular.forEach(['minDate', 'maxDate'], function(key) {
+    if ($attrs[key]) {
+      $scope.$parent.$watch($parse($attrs[key]), function(value) {
+        self[key] = value ? new Date(value) : null;
+        self.refreshView();
+      });
+    } else {
+      self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
+    }
+  });
+
+  angular.forEach(['minMode', 'maxMode'], function(key) {
+    if ($attrs[key]) {
+      $scope.$parent.$watch($parse($attrs[key]), function(value) {
+        self[key] = angular.isDefined(value) ? value : $attrs[key];
+        $scope[key] = self[key];
+        if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) {
+          $scope.datepickerMode = self[key];
+        }
+      });
+    } else {
+      self[key] = datepickerConfig[key] || null;
+      $scope[key] = self[key];
+    }
+  });
+
+  $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
+  $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
+
+  if (angular.isDefined($attrs.initDate)) {
+    this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
+    $scope.$parent.$watch($attrs.initDate, function(initDate) {
+      if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+        self.activeDate = initDate;
+        self.refreshView();
+      }
+    });
+  } else {
+    this.activeDate = new Date();
+  }
+
+  $scope.isActive = function(dateObject) {
+    if (self.compare(dateObject.date, self.activeDate) === 0) {
+      $scope.activeDateId = dateObject.uid;
+      return true;
+    }
+    return false;
+  };
+
+  this.init = function(ngModelCtrl_) {
+    ngModelCtrl = ngModelCtrl_;
+
+    ngModelCtrl.$render = function() {
+      self.render();
+    };
+  };
+
+  this.render = function() {
+    if (ngModelCtrl.$viewValue) {
+      var date = new Date(ngModelCtrl.$viewValue),
+          isValid = !isNaN(date);
+
+      if (isValid) {
+        this.activeDate = date;
+      } else if (!$datepickerSuppressError) {
+        $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+      }
+    }
+    this.refreshView();
+  };
+
+  this.refreshView = function() {
+    if (this.element) {
+      this._refreshView();
+
+      var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+      ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
+    }
+  };
+
+  this.createDateObject = function(date, format) {
+    var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
+    return {
+      date: date,
+      label: dateFilter(date, format),
+      selected: model && this.compare(date, model) === 0,
+      disabled: this.isDisabled(date),
+      current: this.compare(date, new Date()) === 0,
+      customClass: this.customClass(date)
+    };
+  };
+
+  this.isDisabled = function(date) {
+    return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode})));
+  };
+
+  this.customClass = function(date) {
+    return $scope.customClass({date: date, mode: $scope.datepickerMode});
+  };
+
+  // Split array into smaller arrays
+  this.split = function(arr, size) {
+    var arrays = [];
+    while (arr.length > 0) {
+      arrays.push(arr.splice(0, size));
+    }
+    return arrays;
+  };
+
+  // Fix a hard-reprodusible bug with timezones
+  // The bug depends on OS, browser, current timezone and current date
+  // i.e.
+  // var date = new Date(2014, 0, 1);
+  // console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours());
+  // can result in "2013 11 31 23" because of the bug.
+  this.fixTimeZone = function(date) {
+    var hours = date.getHours();
+    date.setHours(hours === 23 ? hours + 2 : 0);
+  };
+
+  $scope.select = function(date) {
+    if ($scope.datepickerMode === self.minMode) {
+      var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
+      dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+      ngModelCtrl.$setViewValue(dt);
+      ngModelCtrl.$render();
+    } else {
+      self.activeDate = date;
+      $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
+    }
+  };
+
+  $scope.move = function(direction) {
+    var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
+        month = self.activeDate.getMonth() + direction * (self.step.months || 0);
+    self.activeDate.setFullYear(year, month, 1);
+    self.refreshView();
+  };
+
+  $scope.toggleMode = function(direction) {
+    direction = direction || 1;
+
+    if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
+      return;
+    }
+
+    $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
+  };
+
+  // Key event mapper
+  $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
+
+  var focusElement = function() {
+    self.element[0].focus();
+  };
+
+  // Listen for focus requests from popup directive
+  $scope.$on('datepicker.focus', focusElement);
+
+  $scope.keydown = function(evt) {
+    var key = $scope.keys[evt.which];
+
+    if (!key || evt.shiftKey || evt.altKey) {
+      return;
+    }
+
+    evt.preventDefault();
+    if (!self.shortcutPropagation) {
+      evt.stopPropagation();
+    }
+
+    if (key === 'enter' || key === 'space') {
+      if (self.isDisabled(self.activeDate)) {
+        return; // do nothing
+      }
+      $scope.select(self.activeDate);
+      focusElement();
+    } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
+      $scope.toggleMode(key === 'up' ? 1 : -1);
+      focusElement();
+    } else {
+      self.handleKeyDown(key, evt);
+      self.refreshView();
+    }
+  };
+}])
+
+.directive('datepicker', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/datepicker/datepicker.html';
+    },
+    scope: {
+      datepickerMode: '=?',
+      dateDisabled: '&',
+      customClass: '&',
+      shortcutPropagation: '&?'
+    },
+    require: ['datepicker', '^ngModel'],
+    controller: 'DatepickerController',
+    controllerAs: 'datepicker',
+    link: function(scope, element, attrs, ctrls) {
+      var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      datepickerCtrl.init(ngModelCtrl);
+    }
+  };
+})
+
+.directive('daypicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/day.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      scope.showWeeks = ctrl.showWeeks;
+
+      ctrl.step = { months: 1 };
+      ctrl.element = element;
+
+      var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+      function getDaysInMonth(year, month) {
+        return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
+      }
+
+      function getDates(startDate, n) {
+        var dates = new Array(n), current = new Date(startDate), i = 0, date;
+        while (i < n) {
+          date = new Date(current);
+          ctrl.fixTimeZone(date);
+          dates[i++] = date;
+          current.setDate(current.getDate() + 1);
+        }
+        return dates;
+      }
+
+      ctrl._refreshView = function() {
+        var year = ctrl.activeDate.getFullYear(),
+          month = ctrl.activeDate.getMonth(),
+          firstDayOfMonth = new Date(year, month, 1),
+          difference = ctrl.startingDay - firstDayOfMonth.getDay(),
+          numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
+          firstDate = new Date(firstDayOfMonth);
+
+        if (numDisplayedFromPreviousMonth > 0) {
+          firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
+        }
+
+        // 42 is the number of days on a six-month calendar
+        var days = getDates(firstDate, 42);
+        for (var i = 0; i < 42; i ++) {
+          days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
+            secondary: days[i].getMonth() !== month,
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.labels = new Array(7);
+        for (var j = 0; j < 7; j++) {
+          scope.labels[j] = {
+            abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
+            full: dateFilter(days[j].date, 'EEEE')
+          };
+        }
+
+        scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
+        scope.rows = ctrl.split(days, 7);
+
+        if (scope.showWeeks) {
+          scope.weekNumbers = [];
+          var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
+              numWeeks = scope.rows.length;
+          for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
+            scope.weekNumbers.push(
+              getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
+          }
+        }
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
+      };
+
+      function getISO8601WeekNumber(date) {
+        var checkDate = new Date(date);
+        checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
+        var time = checkDate.getTime();
+        checkDate.setMonth(0); // Compare with Jan 1
+        checkDate.setDate(1);
+        return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+      }
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getDate();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 7;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 7;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
+          ctrl.activeDate.setMonth(month, 1);
+          date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
+        } else if (key === 'home') {
+          date = 1;
+        } else if (key === 'end') {
+          date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
+        }
+        ctrl.activeDate.setDate(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.directive('monthpicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/month.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      ctrl.step = { years: 1 };
+      ctrl.element = element;
+
+      ctrl._refreshView = function() {
+        var months = new Array(12),
+            year = ctrl.activeDate.getFullYear(),
+            date;
+
+        for (var i = 0; i < 12; i++) {
+          date = new Date(year, i, 1);
+          ctrl.fixTimeZone(date);
+          months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), {
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
+        scope.rows = ctrl.split(months, 3);
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth());
+      };
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getMonth();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 3;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 3;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
+          ctrl.activeDate.setFullYear(year);
+        } else if (key === 'home') {
+          date = 0;
+        } else if (key === 'end') {
+          date = 11;
+        }
+        ctrl.activeDate.setMonth(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.directive('yearpicker', ['dateFilter', function(dateFilter) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/datepicker/year.html',
+    require: '^datepicker',
+    link: function(scope, element, attrs, ctrl) {
+      var range = ctrl.yearRange;
+
+      ctrl.step = { years: range };
+      ctrl.element = element;
+
+      function getStartingYear( year ) {
+        return parseInt((year - 1) / range, 10) * range + 1;
+      }
+
+      ctrl._refreshView = function() {
+        var years = new Array(range), date;
+
+        for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) {
+          date = new Date(start + i, 0, 1);
+          ctrl.fixTimeZone(date);
+          years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), {
+            uid: scope.uniqueId + '-' + i
+          });
+        }
+
+        scope.title = [years[0].label, years[range - 1].label].join(' - ');
+        scope.rows = ctrl.split(years, 5);
+      };
+
+      ctrl.compare = function(date1, date2) {
+        return date1.getFullYear() - date2.getFullYear();
+      };
+
+      ctrl.handleKeyDown = function(key, evt) {
+        var date = ctrl.activeDate.getFullYear();
+
+        if (key === 'left') {
+          date = date - 1;   // up
+        } else if (key === 'up') {
+          date = date - 5;   // down
+        } else if (key === 'right') {
+          date = date + 1;   // down
+        } else if (key === 'down') {
+          date = date + 5;
+        } else if (key === 'pageup' || key === 'pagedown') {
+          date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
+        } else if (key === 'home') {
+          date = getStartingYear(ctrl.activeDate.getFullYear());
+        } else if (key === 'end') {
+          date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1;
+        }
+        ctrl.activeDate.setFullYear(date);
+      };
+
+      ctrl.refreshView();
+    }
+  };
+}])
+
+.constant('datepickerPopupConfig', {
+  datepickerPopup: 'yyyy-MM-dd',
+  datepickerPopupTemplateUrl: 'template/datepicker/popup.html',
+  datepickerTemplateUrl: 'template/datepicker/datepicker.html',
+  html5Types: {
+    date: 'yyyy-MM-dd',
+    'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
+    'month': 'yyyy-MM'
+  },
+  currentText: 'Today',
+  clearText: 'Clear',
+  closeText: 'Done',
+  closeOnDateSelection: true,
+  appendToBody: false,
+  showButtonBar: true,
+  onOpenFocus: true
+})
+
+.directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout',
+function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
+  return {
+    restrict: 'EA',
+    require: 'ngModel',
+    scope: {
+      isOpen: '=?',
+      currentText: '@',
+      clearText: '@',
+      closeText: '@',
+      dateDisabled: '&',
+      customClass: '&'
+    },
+    link: function(scope, element, attrs, ngModel) {
+      var dateFormat,
+          closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
+          appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody,
+          onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus,
+          datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl,
+          datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl,
+          cache = {};
+
+      scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
+
+      scope.getText = function(key) {
+        return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
+      };
+
+      scope.isDisabled = function(date) {
+        if (date === 'today') {
+          date = new Date();
+        }
+
+        return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) ||
+          (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0));
+      };
+
+      scope.compare = function(date1, date2) {
+        return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
+      };
+
+      var isHtml5DateInput = false;
+      if (datepickerPopupConfig.html5Types[attrs.type]) {
+        dateFormat = datepickerPopupConfig.html5Types[attrs.type];
+        isHtml5DateInput = true;
+      } else {
+        dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup;
+        attrs.$observe('datepickerPopup', function(value, oldValue) {
+            var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
+            // Invalidate the $modelValue to ensure that formatters re-run
+            // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
+            if (newDateFormat !== dateFormat) {
+              dateFormat = newDateFormat;
+              ngModel.$modelValue = null;
+
+              if (!dateFormat) {
+                throw new Error('datepickerPopup must have a date format specified.');
+              }
+            }
+        });
+      }
+
+      if (!dateFormat) {
+        throw new Error('datepickerPopup must have a date format specified.');
+      }
+
+      if (isHtml5DateInput && attrs.datepickerPopup) {
+        throw new Error('HTML5 date input types do not support custom formats.');
+      }
+
+      // popup element used to display calendar
+      var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
+      popupEl.attr({
+        'ng-model': 'date',
+        'ng-change': 'dateSelection(date)',
+        'template-url': datepickerPopupTemplateUrl
+      });
+
+      function cameltoDash(string) {
+        return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
+      }
+
+      // datepicker element
+      var datepickerEl = angular.element(popupEl.children()[0]);
+      datepickerEl.attr('template-url', datepickerTemplateUrl);
+
+      if (isHtml5DateInput) {
+        if (attrs.type === 'month') {
+          datepickerEl.attr('datepicker-mode', '"month"');
+          datepickerEl.attr('min-mode', 'month');
+        }
+      }
+
+      if (attrs.datepickerOptions) {
+        var options = scope.$parent.$eval(attrs.datepickerOptions);
+        if (options && options.initDate) {
+          scope.initDate = options.initDate;
+          datepickerEl.attr('init-date', 'initDate');
+          delete options.initDate;
+        }
+        angular.forEach(options, function(value, option) {
+          datepickerEl.attr( cameltoDash(option), value );
+        });
+      }
+
+      scope.watchData = {};
+      angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
+        if (attrs[key]) {
+          var getAttribute = $parse(attrs[key]);
+          scope.$parent.$watch(getAttribute, function(value) {
+            scope.watchData[key] = value;
+            if (key === 'minDate' || key === 'maxDate') {
+              cache[key] = new Date(value);
+            }
+          });
+          datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
+
+          // Propagate changes from datepicker to outside
+          if (key === 'datepickerMode') {
+            var setAttribute = getAttribute.assign;
+            scope.$watch('watchData.' + key, function(value, oldvalue) {
+              if (angular.isFunction(setAttribute) && value !== oldvalue) {
+                setAttribute(scope.$parent, value);
+              }
+            });
+          }
+        }
+      });
+      if (attrs.dateDisabled) {
+        datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
+      }
+
+      if (attrs.showWeeks) {
+        datepickerEl.attr('show-weeks', attrs.showWeeks);
+      }
+
+      if (attrs.customClass) {
+        datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
+      }
+
+      function parseDate(viewValue) {
+        if (angular.isNumber(viewValue)) {
+          // presumably timestamp to date object
+          viewValue = new Date(viewValue);
+        }
+
+        if (!viewValue) {
+          return null;
+        } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
+          return viewValue;
+        } else if (angular.isString(viewValue)) {
+          var date = dateParser.parse(viewValue, dateFormat, scope.date);
+          if (isNaN(date)) {
+            return undefined;
+          } else {
+            return date;
+          }
+        } else {
+          return undefined;
+        }
+      }
+
+      function validator(modelValue, viewValue) {
+        var value = modelValue || viewValue;
+
+        if (!attrs.ngRequired && !value) {
+          return true;
+        }
+
+        if (angular.isNumber(value)) {
+          value = new Date(value);
+        }
+        if (!value) {
+          return true;
+        } else if (angular.isDate(value) && !isNaN(value)) {
+          return true;
+        } else if (angular.isString(value)) {
+          var date = dateParser.parse(value, dateFormat);
+          return !isNaN(date);
+        } else {
+          return false;
+        }
+      }
+
+      if (!isHtml5DateInput) {
+        // Internal API to maintain the correct ng-invalid-[key] class
+        ngModel.$$parserName = 'date';
+        ngModel.$validators.date = validator;
+        ngModel.$parsers.unshift(parseDate);
+        ngModel.$formatters.push(function(value) {
+          scope.date = value;
+          return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
+        });
+      } else {
+        ngModel.$formatters.push(function(value) {
+          scope.date = value;
+          return value;
+        });
+      }
+
+      // Inner change
+      scope.dateSelection = function(dt) {
+        if (angular.isDefined(dt)) {
+          scope.date = dt;
+        }
+        var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
+        element.val(date);
+        ngModel.$setViewValue(date);
+
+        if (closeOnDateSelection) {
+          scope.isOpen = false;
+          element[0].focus();
+        }
+      };
+
+      // Detect changes in the view from the text box
+      ngModel.$viewChangeListeners.push(function() {
+        scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
+      });
+
+      var documentClickBind = function(event) {
+        if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) {
+          scope.$apply(function() {
+            scope.isOpen = false;
+          });
+        }
+      };
+
+      var inputKeydownBind = function(evt) {
+        if (evt.which === 27 && scope.isOpen) {
+          evt.preventDefault();
+          evt.stopPropagation();
+          scope.$apply(function() {
+            scope.isOpen = false;
+          });
+          element[0].focus();
+        } else if (evt.which === 40 && !scope.isOpen) {
+          evt.preventDefault();
+          evt.stopPropagation();
+          scope.$apply(function() {
+            scope.isOpen = true;
+          });
+        }
+      };
+      element.bind('keydown', inputKeydownBind);
+
+      scope.keydown = function(evt) {
+        if (evt.which === 27) {
+          scope.isOpen = false;
+          element[0].focus();
+        }
+      };
+
+      scope.$watch('isOpen', function(value) {
+        if (value) {
+          scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+          scope.position.top = scope.position.top + element.prop('offsetHeight');
+
+          $timeout(function() {
+            if (onOpenFocus) {
+              scope.$broadcast('datepicker.focus');
+            }
+            $document.bind('click', documentClickBind);
+          }, 0, false);
+        } else {
+          $document.unbind('click', documentClickBind);
+        }
+      });
+
+      scope.select = function(date) {
+        if (date === 'today') {
+          var today = new Date();
+          if (angular.isDate(scope.date)) {
+            date = new Date(scope.date);
+            date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
+          } else {
+            date = new Date(today.setHours(0, 0, 0, 0));
+          }
+        }
+        scope.dateSelection(date);
+      };
+
+      scope.close = function() {
+        scope.isOpen = false;
+        element[0].focus();
+      };
+
+      var $popup = $compile(popupEl)(scope);
+      // Prevent jQuery cache memory leak (template is now redundant after linking)
+      popupEl.remove();
+
+      if (appendToBody) {
+        $document.find('body').append($popup);
+      } else {
+        element.after($popup);
+      }
+
+      scope.$on('$destroy', function() {
+        if (scope.isOpen === true) {
+          if (!$rootScope.$$phase) {
+            scope.$apply(function() {
+              scope.isOpen = false;
+            });
+          }
+        }
+
+        $popup.remove();
+        element.unbind('keydown', inputKeydownBind);
+        $document.unbind('click', documentClickBind);
+      });
+    }
+  };
+}])
+
+.directive('datepickerPopupWrap', function() {
+  return {
+    restrict:'EA',
+    replace: true,
+    transclude: true,
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/datepicker/popup.html';
+    }
+  };
+});
+
+angular.module('ui.bootstrap.progressbar', [])
+
+.constant('progressConfig', {
+  animate: true,
+  max: 100
+})
+
+.value('$progressSuppressWarning', false)
+
+.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
+  var self = this,
+      animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
+
+  this.bars = [];
+  $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
+
+  this.addBar = function(bar, element) {
+    if (!animate) {
+      element.css({'transition': 'none'});
+    }
+
+    this.bars.push(bar);
+
+    bar.max = $scope.max;
+
+    bar.$watch('value', function(value) {
+      bar.recalculatePercentage();
+    });
+
+    bar.recalculatePercentage = function() {
+      bar.percent = +(100 * bar.value / bar.max).toFixed(2);
+
+      var totalPercentage = self.bars.reduce(function(total, bar) {
+        return total + bar.percent;
+      }, 0);
+
+      if (totalPercentage > 100) {
+        bar.percent -= totalPercentage - 100;
+      }
+    };
+
+    bar.$on('$destroy', function() {
+      element = null;
+      self.removeBar(bar);
+    });
+  };
+
+  this.removeBar = function(bar) {
+      this.bars.splice(this.bars.indexOf(bar), 1);
+  };
+
+  $scope.$watch('max', function(max) {
+    self.bars.forEach(function(bar) {
+      bar.max = $scope.max;
+      bar.recalculatePercentage();
+    });
+  });
+}])
+
+.directive('uibProgress', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    require: 'uibProgress',
+    scope: {
+      max: '=?'
+    },
+    templateUrl: 'template/progressbar/progress.html'
+  };
+})
+
+.directive('progress', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    require: 'progress',
+    scope: {
+      max: '=?'
+    },
+    templateUrl: 'template/progressbar/progress.html',
+    link: function() {
+      if ($progressSuppressWarning) {
+        $log.warn('progress is now deprecated. Use uib-progress instead');
+      }
+    }
+  };
+}])
+
+.directive('uibBar', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    require: '^uibProgress',
+    scope: {
+      value: '=',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/bar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      progressCtrl.addBar(scope, element);
+    }
+  };
+})
+
+.directive('bar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    require: '^progress',
+    scope: {
+      value: '=',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/bar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      if ($progressSuppressWarning) {
+        $log.warn('bar is now deprecated. Use uib-bar instead');
+      }
+      progressCtrl.addBar(scope, element);
+    }
+  };
+}])
+
+.directive('progressbar', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    transclude: true,
+    controller: 'ProgressController',
+    scope: {
+      value: '=',
+      max: '=?',
+      type: '@'
+    },
+    templateUrl: 'template/progressbar/progressbar.html',
+    link: function(scope, element, attrs, progressCtrl) {
+      progressCtrl.addBar(scope, angular.element(element.children()[0]));
+    }
+  };
+});
+
+angular.module('ui.bootstrap.pagination', [])
+.controller('PaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) {
+  var self = this,
+      ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
+      setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
+
+  this.init = function(ngModelCtrl_, config) {
+    ngModelCtrl = ngModelCtrl_;
+    this.config = config;
+
+    ngModelCtrl.$render = function() {
+      self.render();
+    };
+
+    if ($attrs.itemsPerPage) {
+      $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
+        self.itemsPerPage = parseInt(value, 10);
+        $scope.totalPages = self.calculateTotalPages();
+      });
+    } else {
+      this.itemsPerPage = config.itemsPerPage;
+    }
+
+    $scope.$watch('totalItems', function() {
+      $scope.totalPages = self.calculateTotalPages();
+    });
+
+    $scope.$watch('totalPages', function(value) {
+      setNumPages($scope.$parent, value); // Readonly variable
+
+      if ( $scope.page > value ) {
+        $scope.selectPage(value);
+      } else {
+        ngModelCtrl.$render();
+      }
+    });
+  };
+
+  this.calculateTotalPages = function() {
+    var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
+    return Math.max(totalPages || 0, 1);
+  };
+
+  this.render = function() {
+    $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
+  };
+
+  $scope.selectPage = function(page, evt) {
+    if (evt) {
+      evt.preventDefault();
+    }
+
+    var clickAllowed = !$scope.ngDisabled || !evt;
+    if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
+      if (evt && evt.target) {
+        evt.target.blur();
+      }
+      ngModelCtrl.$setViewValue(page);
+      ngModelCtrl.$render();
+    }
+  };
+
+  $scope.getText = function(key) {
+    return $scope[key + 'Text'] || self.config[key + 'Text'];
+  };
+
+  $scope.noPrevious = function() {
+    return $scope.page === 1;
+  };
+
+  $scope.noNext = function() {
+    return $scope.page === $scope.totalPages;
+  };
+}])
+
+.constant('paginationConfig', {
+  itemsPerPage: 10,
+  boundaryLinks: false,
+  directionLinks: true,
+  firstText: 'First',
+  previousText: 'Previous',
+  nextText: 'Next',
+  lastText: 'Last',
+  rotate: true
+})
+
+.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) {
+  return {
+    restrict: 'EA',
+    scope: {
+      totalItems: '=',
+      firstText: '@',
+      previousText: '@',
+      nextText: '@',
+      lastText: '@',
+      ngDisabled:'='
+    },
+    require: ['pagination', '?ngModel'],
+    controller: 'PaginationController',
+    controllerAs: 'pagination',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/pagination/pagination.html';
+    },
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (!ngModelCtrl) {
+         return; // do nothing if no ng-model
+      }
+
+      // Setup configuration parameters
+      var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
+          rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
+      scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
+      scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
+
+      paginationCtrl.init(ngModelCtrl, paginationConfig);
+
+      if (attrs.maxSize) {
+        scope.$parent.$watch($parse(attrs.maxSize), function(value) {
+          maxSize = parseInt(value, 10);
+          paginationCtrl.render();
+        });
+      }
+
+      // Create page object used in template
+      function makePage(number, text, isActive) {
+        return {
+          number: number,
+          text: text,
+          active: isActive
+        };
+      }
+
+      function getPages(currentPage, totalPages) {
+        var pages = [];
+
+        // Default page limits
+        var startPage = 1, endPage = totalPages;
+        var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
+
+        // recompute if maxSize
+        if (isMaxSized) {
+          if (rotate) {
+            // Current page is displayed in the middle of the visible ones
+            startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
+            endPage   = startPage + maxSize - 1;
+
+            // Adjust if limit is exceeded
+            if (endPage > totalPages) {
+              endPage   = totalPages;
+              startPage = endPage - maxSize + 1;
+            }
+          } else {
+            // Visible pages are paginated with maxSize
+            startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
+
+            // Adjust last page if limit is exceeded
+            endPage = Math.min(startPage + maxSize - 1, totalPages);
+          }
+        }
+
+        // Add page number links
+        for (var number = startPage; number <= endPage; number++) {
+          var page = makePage(number, number, number === currentPage);
+          pages.push(page);
+        }
+
+        // Add links to move between page sets
+        if (isMaxSized && ! rotate) {
+          if (startPage > 1) {
+            var previousPageSet = makePage(startPage - 1, '...', false);
+            pages.unshift(previousPageSet);
+          }
+
+          if (endPage < totalPages) {
+            var nextPageSet = makePage(endPage + 1, '...', false);
+            pages.push(nextPageSet);
+          }
+        }
+
+        return pages;
+      }
+
+      var originalRender = paginationCtrl.render;
+      paginationCtrl.render = function() {
+        originalRender();
+        if (scope.page > 0 && scope.page <= scope.totalPages) {
+          scope.pages = getPages(scope.page, scope.totalPages);
+        }
+      };
+    }
+  };
+}])
+
+.constant('pagerConfig', {
+  itemsPerPage: 10,
+  previousText: '« Previous',
+  nextText: 'Next »',
+  align: true
+})
+
+.directive('pager', ['pagerConfig', function(pagerConfig) {
+  return {
+    restrict: 'EA',
+    scope: {
+      totalItems: '=',
+      previousText: '@',
+      nextText: '@',
+      ngDisabled: '='
+    },
+    require: ['pager', '?ngModel'],
+    controller: 'PaginationController',
+    controllerAs: 'pagination',
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/pagination/pager.html';
+    },
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (!ngModelCtrl) {
+         return; // do nothing if no ng-model
+      }
+
+      scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
+      paginationCtrl.init(ngModelCtrl, pagerConfig);
+    }
+  };
+}]);
+
+angular.module('ui.bootstrap.timepicker', [])
+
+.constant('timepickerConfig', {
+  hourStep: 1,
+  minuteStep: 1,
+  showMeridian: true,
+  meridians: null,
+  readonlyInput: false,
+  mousewheel: true,
+  arrowkeys: true,
+  showSpinners: true
+})
+
+.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
+  var selected = new Date(),
+      ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
+      meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
+
+  this.init = function(ngModelCtrl_, inputs) {
+    ngModelCtrl = ngModelCtrl_;
+    ngModelCtrl.$render = this.render;
+
+    ngModelCtrl.$formatters.unshift(function(modelValue) {
+      return modelValue ? new Date(modelValue) : null;
+    });
+
+    var hoursInputEl = inputs.eq(0),
+        minutesInputEl = inputs.eq(1);
+
+    var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
+    if (mousewheel) {
+      this.setupMousewheelEvents(hoursInputEl, minutesInputEl);
+    }
+
+    var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
+    if (arrowkeys) {
+      this.setupArrowkeyEvents(hoursInputEl, minutesInputEl);
+    }
+
+    $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
+    this.setupInputEvents(hoursInputEl, minutesInputEl);
+  };
+
+  var hourStep = timepickerConfig.hourStep;
+  if ($attrs.hourStep) {
+    $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
+      hourStep = parseInt(value, 10);
+    });
+  }
+
+  var minuteStep = timepickerConfig.minuteStep;
+  if ($attrs.minuteStep) {
+    $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
+      minuteStep = parseInt(value, 10);
+    });
+  }
+
+  var min;
+  $scope.$parent.$watch($parse($attrs.min), function(value) {
+    var dt = new Date(value);
+    min = isNaN(dt) ? undefined : dt;
+  });
+
+  var max;
+  $scope.$parent.$watch($parse($attrs.max), function(value) {
+    var dt = new Date(value);
+    max = isNaN(dt) ? undefined : dt;
+  });
+
+  $scope.noIncrementHours = function() {
+    var incrementedSelected = addMinutes(selected, hourStep * 60);
+    return incrementedSelected > max ||
+      (incrementedSelected < selected && incrementedSelected < min);
+  };
+
+  $scope.noDecrementHours = function() {
+    var decrementedSelected = addMinutes(selected, -hourStep * 60);
+    return decrementedSelected < min ||
+      (decrementedSelected > selected && decrementedSelected > max);
+  };
+
+  $scope.noIncrementMinutes = function() {
+    var incrementedSelected = addMinutes(selected, minuteStep);
+    return incrementedSelected > max ||
+      (incrementedSelected < selected && incrementedSelected < min);
+  };
+
+  $scope.noDecrementMinutes = function() {
+    var decrementedSelected = addMinutes(selected, -minuteStep);
+    return decrementedSelected < min ||
+      (decrementedSelected > selected && decrementedSelected > max);
+  };
+
+  $scope.noToggleMeridian = function() {
+    if (selected.getHours() < 13) {
+      return addMinutes(selected, 12 * 60) > max;
+    } else {
+      return addMinutes(selected, -12 * 60) < min;
+    }
+  };
+
+  // 12H / 24H mode
+  $scope.showMeridian = timepickerConfig.showMeridian;
+  if ($attrs.showMeridian) {
+    $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
+      $scope.showMeridian = !!value;
+
+      if (ngModelCtrl.$error.time) {
+        // Evaluate from template
+        var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
+        if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+          selected.setHours(hours);
+          refresh();
+        }
+      } else {
+        updateTemplate();
+      }
+    });
+  }
+
+  // Get $scope.hours in 24H mode if valid
+  function getHoursFromTemplate() {
+    var hours = parseInt($scope.hours, 10);
+    var valid = $scope.showMeridian ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
+    if (!valid) {
+      return undefined;
+    }
+
+    if ($scope.showMeridian) {
+      if (hours === 12) {
+        hours = 0;
+      }
+      if ($scope.meridian === meridians[1]) {
+        hours = hours + 12;
+      }
+    }
+    return hours;
+  }
+
+  function getMinutesFromTemplate() {
+    var minutes = parseInt($scope.minutes, 10);
+    return (minutes >= 0 && minutes < 60) ? minutes : undefined;
+  }
+
+  function pad(value) {
+    return (angular.isDefined(value) && value.toString().length < 2) ? '0' + value : value.toString();
+  }
+
+  // Respond on mousewheel spin
+  this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl) {
+    var isScrollingUp = function(e) {
+      if (e.originalEvent) {
+        e = e.originalEvent;
+      }
+      //pick correct delta variable depending on event
+      var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
+      return (e.detail || delta > 0);
+    };
+
+    hoursInputEl.bind('mousewheel wheel', function(e) {
+      $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
+      e.preventDefault();
+    });
+
+    minutesInputEl.bind('mousewheel wheel', function(e) {
+      $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
+      e.preventDefault();
+    });
+
+  };
+
+  // Respond on up/down arrowkeys
+  this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl) {
+    hoursInputEl.bind('keydown', function(e) {
+      if (e.which === 38) { // up
+        e.preventDefault();
+        $scope.incrementHours();
+        $scope.$apply();
+      } else if (e.which === 40) { // down
+        e.preventDefault();
+        $scope.decrementHours();
+        $scope.$apply();
+      }
+    });
+
+    minutesInputEl.bind('keydown', function(e) {
+      if (e.which === 38) { // up
+        e.preventDefault();
+        $scope.incrementMinutes();
+        $scope.$apply();
+      } else if (e.which === 40) { // down
+        e.preventDefault();
+        $scope.decrementMinutes();
+        $scope.$apply();
+      }
+    });
+  };
+
+  this.setupInputEvents = function(hoursInputEl, minutesInputEl) {
+    if ($scope.readonlyInput) {
+      $scope.updateHours = angular.noop;
+      $scope.updateMinutes = angular.noop;
+      return;
+    }
+
+    var invalidate = function(invalidHours, invalidMinutes) {
+      ngModelCtrl.$setViewValue(null);
+      ngModelCtrl.$setValidity('time', false);
+      if (angular.isDefined(invalidHours)) {
+        $scope.invalidHours = invalidHours;
+      }
+      if (angular.isDefined(invalidMinutes)) {
+        $scope.invalidMinutes = invalidMinutes;
+      }
+    };
+
+    $scope.updateHours = function() {
+      var hours = getHoursFromTemplate(),
+        minutes = getMinutesFromTemplate();
+
+      if (angular.isDefined(hours) && angular.isDefined(minutes)) {
+        selected.setHours(hours);
+        if (selected < min || selected > max) {
+          invalidate(true);
+        } else {
+          refresh('h');
+        }
+      } else {
+        invalidate(true);
+      }
+    };
+
+    hoursInputEl.bind('blur', function(e) {
+      if (!$scope.invalidHours && $scope.hours < 10) {
+        $scope.$apply(function() {
+          $scope.hours = pad($scope.hours);
+        });
+      }
+    });
+
+    $scope.updateMinutes = function() {
+      var minutes = getMinutesFromTemplate(),
+        hours = getHoursFromTemplate();
+
+      if (angular.isDefined(minutes) && angular.isDefined(hours)) {
+        selected.setMinutes(minutes);
+        if (selected < min || selected > max) {
+          invalidate(undefined, true);
+        } else {
+          refresh('m');
+        }
+      } else {
+        invalidate(undefined, true);
+      }
+    };
+
+    minutesInputEl.bind('blur', function(e) {
+      if (!$scope.invalidMinutes && $scope.minutes < 10) {
+        $scope.$apply(function() {
+          $scope.minutes = pad($scope.minutes);
+        });
+      }
+    });
+
+  };
+
+  this.render = function() {
+    var date = ngModelCtrl.$viewValue;
+
+    if (isNaN(date)) {
+      ngModelCtrl.$setValidity('time', false);
+      $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+    } else {
+      if (date) {
+        selected = date;
+      }
+
+      if (selected < min || selected > max) {
+        ngModelCtrl.$setValidity('time', false);
+        $scope.invalidHours = true;
+        $scope.invalidMinutes = true;
+      } else {
+        makeValid();
+      }
+      updateTemplate();
+    }
+  };
+
+  // Call internally when we know that model is valid.
+  function refresh(keyboardChange) {
+    makeValid();
+    ngModelCtrl.$setViewValue(new Date(selected));
+    updateTemplate(keyboardChange);
+  }
+
+  function makeValid() {
+    ngModelCtrl.$setValidity('time', true);
+    $scope.invalidHours = false;
+    $scope.invalidMinutes = false;
+  }
+
+  function updateTemplate(keyboardChange) {
+    var hours = selected.getHours(), minutes = selected.getMinutes();
+
+    if ($scope.showMeridian) {
+      hours = (hours === 0 || hours === 12) ? 12 : hours % 12; // Convert 24 to 12 hour system
+    }
+
+    $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
+    if (keyboardChange !== 'm') {
+      $scope.minutes = pad(minutes);
+    }
+    $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
+  }
+
+  function addMinutes(date, minutes) {
+    var dt = new Date(date.getTime() + minutes * 60000);
+    var newDate = new Date(date);
+    newDate.setHours(dt.getHours(), dt.getMinutes());
+    return newDate;
+  }
+
+  function addMinutesToSelected(minutes) {
+    selected = addMinutes(selected, minutes);
+    refresh();
+  }
+
+  $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
+    $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
+
+  $scope.incrementHours = function() {
+    if (!$scope.noIncrementHours()) {
+      addMinutesToSelected(hourStep * 60);
+    }
+  };
+
+  $scope.decrementHours = function() {
+    if (!$scope.noDecrementHours()) {
+      addMinutesToSelected(-hourStep * 60);
+    }
+  };
+
+  $scope.incrementMinutes = function() {
+    if (!$scope.noIncrementMinutes()) {
+      addMinutesToSelected(minuteStep);
+    }
+  };
+
+  $scope.decrementMinutes = function() {
+    if (!$scope.noDecrementMinutes()) {
+      addMinutesToSelected(-minuteStep);
+    }
+  };
+
+  $scope.toggleMeridian = function() {
+    if (!$scope.noToggleMeridian()) {
+      addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1));
+    }
+  };
+}])
+
+.directive('timepicker', function() {
+  return {
+    restrict: 'EA',
+    require: ['timepicker', '?^ngModel'],
+    controller:'TimepickerController',
+    controllerAs: 'timepicker',
+    replace: true,
+    scope: {},
+    templateUrl: function(element, attrs) {
+      return attrs.templateUrl || 'template/timepicker/timepicker.html';
+    },
+    link: function(scope, element, attrs, ctrls) {
+      var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+
+      if (ngModelCtrl) {
+        timepickerCtrl.init(ngModelCtrl, element.find('input'));
+      }
+    }
+  };
+});
+
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
+
+/**
+ * A helper service that can parse typeahead's syntax (string provided by users)
+ * Extracted to a separate service for ease of unit testing
+ */
+  .factory('typeaheadParser', ['$parse', function($parse) {
+
+  //                      00000111000000000000022200000000000000003333333333333330000000000044000
+  var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
+
+  return {
+    parse: function(input) {
+      var match = input.match(TYPEAHEAD_REGEXP);
+      if (!match) {
+        throw new Error(
+          'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
+            ' but got "' + input + '".');
+      }
+
+      return {
+        itemName:match[3],
+        source:$parse(match[4]),
+        viewMapper:$parse(match[2] || match[1]),
+        modelMapper:$parse(match[1])
+      };
+    }
+  };
+}])
+
+  .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
+    function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
+    var HOT_KEYS = [9, 13, 27, 38, 40];
+    var eventDebounceTime = 200;
+
+    return {
+      require: ['ngModel', '^?ngModelOptions'],
+      link: function(originalScope, element, attrs, ctrls) {
+        var modelCtrl = ctrls[0];
+        var ngModelOptions = ctrls[1];
+        //SUPPORTED ATTRIBUTES (OPTIONS)
+
+        //minimal no of characters that needs to be entered before typeahead kicks-in
+        var minLength = originalScope.$eval(attrs.typeaheadMinLength);
+        if (!minLength && minLength !== 0) {
+          minLength = 1;
+        }
+
+        //minimal wait time after last character typed before typeahead kicks-in
+        var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
+
+        //should it restrict model values to the ones selected from the popup only?
+        var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+
+        //binding to a variable that indicates if matches are being retrieved asynchronously
+        var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+
+        //a callback executed when a match is selected
+        var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+
+        //should it select highlighted popup value when losing focus?
+        var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
+
+        //binding to a variable that indicates if there were no results after the query is completed
+        var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
+
+        var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
+
+        var appendToBody =  attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+
+        var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
+
+        //If input matches an item of the list exactly, select it automatically
+        var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
+
+        //INTERNAL VARIABLES
+
+        //model setter executed upon match selection
+        var parsedModel = $parse(attrs.ngModel);
+        var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+        var $setModelValue = function(scope, newValue) {
+          if (angular.isFunction(parsedModel(originalScope)) &&
+            ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
+            return invokeModelSetter(scope, {$$$p: newValue});
+          } else {
+            return parsedModel.assign(scope, newValue);
+          }
+        };
+
+        //expressions used by typeahead
+        var parserResult = typeaheadParser.parse(attrs.typeahead);
+
+        var hasFocus;
+
+        //Used to avoid bug in iOS webview where iOS keyboard does not fire
+        //mousedown & mouseup events
+        //Issue #3699
+        var selected;
+
+        //create a child scope for the typeahead directive so we are not polluting original scope
+        //with typeahead-specific data (matches, query etc.)
+        var scope = originalScope.$new();
+        var offDestroy = originalScope.$on('$destroy', function() {
+			    scope.$destroy();
+        });
+        scope.$on('$destroy', offDestroy);
+
+        // WAI-ARIA
+        var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
+        element.attr({
+          'aria-autocomplete': 'list',
+          'aria-expanded': false,
+          'aria-owns': popupId
+        });
+
+        //pop-up element used to display matches
+        var popUpEl = angular.element('<div typeahead-popup></div>');
+        popUpEl.attr({
+          id: popupId,
+          matches: 'matches',
+          active: 'activeIdx',
+          select: 'select(activeIdx)',
+          'move-in-progress': 'moveInProgress',
+          query: 'query',
+          position: 'position'
+        });
+        //custom item template
+        if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
+          popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+        }
+
+        if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
+          popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
+        }
+
+        var resetMatches = function() {
+          scope.matches = [];
+          scope.activeIdx = -1;
+          element.attr('aria-expanded', false);
+        };
+
+        var getMatchId = function(index) {
+          return popupId + '-option-' + index;
+        };
+
+        // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
+        // This attribute is added or removed automatically when the `activeIdx` changes.
+        scope.$watch('activeIdx', function(index) {
+          if (index < 0) {
+            element.removeAttr('aria-activedescendant');
+          } else {
+            element.attr('aria-activedescendant', getMatchId(index));
+          }
+        });
+
+        var inputIsExactMatch = function(inputValue, index) {
+          if (scope.matches.length > index && inputValue) {
+            return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
+          }
+
+          return false;
+        };
+
+        var getMatchesAsync = function(inputValue) {
+          var locals = {$viewValue: inputValue};
+          isLoadingSetter(originalScope, true);
+          isNoResultsSetter(originalScope, false);
+          $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+            //it might happen that several async queries were in progress if a user were typing fast
+            //but we are interested only in responses that correspond to the current view value
+            var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
+            if (onCurrentRequest && hasFocus) {
+              if (matches && matches.length > 0) {
+
+                scope.activeIdx = focusFirst ? 0 : -1;
+                isNoResultsSetter(originalScope, false);
+                scope.matches.length = 0;
+
+                //transform labels
+                for (var i = 0; i < matches.length; i++) {
+                  locals[parserResult.itemName] = matches[i];
+                  scope.matches.push({
+                    id: getMatchId(i),
+                    label: parserResult.viewMapper(scope, locals),
+                    model: matches[i]
+                  });
+                }
+
+                scope.query = inputValue;
+                //position pop-up with matches - we need to re-calculate its position each time we are opening a window
+                //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
+                //due to other elements being rendered
+                recalculatePosition();
+
+                element.attr('aria-expanded', true);
+
+                //Select the single remaining option if user input matches
+                if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
+                  scope.select(0);
+                }
+              } else {
+                resetMatches();
+                isNoResultsSetter(originalScope, true);
+              }
+            }
+            if (onCurrentRequest) {
+              isLoadingSetter(originalScope, false);
+            }
+          }, function() {
+            resetMatches();
+            isLoadingSetter(originalScope, false);
+            isNoResultsSetter(originalScope, true);
+          });
+        };
+
+        // bind events only if appendToBody params exist - performance feature
+        if (appendToBody) {
+          angular.element($window).bind('resize', fireRecalculating);
+          $document.find('body').bind('scroll', fireRecalculating);
+        }
+
+        // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+        var timeoutEventPromise;
+
+        // Default progress type
+        scope.moveInProgress = false;
+
+        function fireRecalculating() {
+          if (!scope.moveInProgress) {
+            scope.moveInProgress = true;
+            scope.$digest();
+          }
+
+          // Cancel previous timeout
+          if (timeoutEventPromise) {
+            $timeout.cancel(timeoutEventPromise);
+          }
+
+          // Debounced executing recalculate after events fired
+          timeoutEventPromise = $timeout(function() {
+            // if popup is visible
+            if (scope.matches.length) {
+              recalculatePosition();
+            }
+
+            scope.moveInProgress = false;
+            scope.$digest();
+          }, eventDebounceTime);
+        }
+
+        // recalculate actual position and set new values to scope
+        // after digest loop is popup in right position
+        function recalculatePosition() {
+          scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+          scope.position.top += element.prop('offsetHeight');
+        }
+
+        resetMatches();
+
+        //we need to propagate user's query so we can higlight matches
+        scope.query = undefined;
+
+        //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+        var timeoutPromise;
+
+        var scheduleSearchWithTimeout = function(inputValue) {
+          timeoutPromise = $timeout(function() {
+            getMatchesAsync(inputValue);
+          }, waitTime);
+        };
+
+        var cancelPreviousTimeout = function() {
+          if (timeoutPromise) {
+            $timeout.cancel(timeoutPromise);
+          }
+        };
+
+        //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
+        //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
+        modelCtrl.$parsers.unshift(function(inputValue) {
+          hasFocus = true;
+
+          if (minLength === 0 || inputValue && inputValue.length >= minLength) {
+            if (waitTime > 0) {
+              cancelPreviousTimeout();
+              scheduleSearchWithTimeout(inputValue);
+            } else {
+              getMatchesAsync(inputValue);
+            }
+          } else {
+            isLoadingSetter(originalScope, false);
+            cancelPreviousTimeout();
+            resetMatches();
+          }
+
+          if (isEditable) {
+            return inputValue;
+          } else {
+            if (!inputValue) {
+              // Reset in case user had typed something previously.
+              modelCtrl.$setValidity('editable', true);
+              return null;
+            } else {
+              modelCtrl.$setValidity('editable', false);
+              return undefined;
+            }
+          }
+        });
+
+        modelCtrl.$formatters.push(function(modelValue) {
+          var candidateViewValue, emptyViewValue;
+          var locals = {};
+
+          // The validity may be set to false via $parsers (see above) if
+          // the model is restricted to selected values. If the model
+          // is set manually it is considered to be valid.
+          if (!isEditable) {
+            modelCtrl.$setValidity('editable', true);
+          }
+
+          if (inputFormatter) {
+            locals.$model = modelValue;
+            return inputFormatter(originalScope, locals);
+          } else {
+            //it might happen that we don't have enough info to properly render input value
+            //we need to check for this situation and simply return model value if we can't apply custom formatting
+            locals[parserResult.itemName] = modelValue;
+            candidateViewValue = parserResult.viewMapper(originalScope, locals);
+            locals[parserResult.itemName] = undefined;
+            emptyViewValue = parserResult.viewMapper(originalScope, locals);
+
+            return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
+          }
+        });
+
+        scope.select = function(activeIdx) {
+          //called from within the $digest() cycle
+          var locals = {};
+          var model, item;
+
+          selected = true;
+          locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
+          model = parserResult.modelMapper(originalScope, locals);
+          $setModelValue(originalScope, model);
+          modelCtrl.$setValidity('editable', true);
+          modelCtrl.$setValidity('parse', true);
+
+          onSelectCallback(originalScope, {
+            $item: item,
+            $model: model,
+            $label: parserResult.viewMapper(originalScope, locals)
+          });
+
+          resetMatches();
+
+          //return focus to the input element if a match was selected via a mouse click event
+          // use timeout to avoid $rootScope:inprog error
+          if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
+            $timeout(function() { element[0].focus(); }, 0, false);
+          }
+        };
+
+        //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
+        element.bind('keydown', function(evt) {
+          //typeahead is open and an "interesting" key was pressed
+          if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
+            return;
+          }
+
+          // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
+          if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
+            resetMatches();
+            scope.$digest();
+            return;
+          }
+
+          evt.preventDefault();
+
+          if (evt.which === 40) {
+            scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
+            scope.$digest();
+
+          } else if (evt.which === 38) {
+            scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
+            scope.$digest();
+
+          } else if (evt.which === 13 || evt.which === 9) {
+            scope.$apply(function () {
+              scope.select(scope.activeIdx);
+            });
+
+          } else if (evt.which === 27) {
+            evt.stopPropagation();
+
+            resetMatches();
+            scope.$digest();
+          }
+        });
+
+        element.bind('blur', function() {
+          if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
+            selected = true;
+            scope.$apply(function() {
+              scope.select(scope.activeIdx);
+            });
+          }
+          hasFocus = false;
+          selected = false;
+        });
+
+        // Keep reference to click handler to unbind it.
+        var dismissClickHandler = function(evt) {
+          // Issue #3973
+          // Firefox treats right click as a click on document
+          if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
+            resetMatches();
+            if (!$rootScope.$$phase) {
+              scope.$digest();
+            }
+          }
+        };
+
+        $document.bind('click', dismissClickHandler);
+
+        originalScope.$on('$destroy', function() {
+          $document.unbind('click', dismissClickHandler);
+          if (appendToBody) {
+            $popup.remove();
+          }
+          // Prevent jQuery cache memory leak
+          popUpEl.remove();
+        });
+
+        var $popup = $compile(popUpEl)(scope);
+
+        if (appendToBody) {
+          $document.find('body').append($popup);
+        } else {
+          element.after($popup);
+        }
+      }
+    };
+
+  }])
+
+  .directive('typeaheadPopup', function() {
+    return {
+      restrict: 'EA',
+      scope: {
+        matches: '=',
+        query: '=',
+        active: '=',
+        position: '&',
+        moveInProgress: '=',
+        select: '&'
+      },
+      replace: true,
+      templateUrl: function(element, attrs) {
+        return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
+      },
+      link: function(scope, element, attrs) {
+        scope.templateUrl = attrs.templateUrl;
+
+        scope.isOpen = function() {
+          return scope.matches.length > 0;
+        };
+
+        scope.isActive = function(matchIdx) {
+          return scope.active == matchIdx;
+        };
+
+        scope.selectActive = function(matchIdx) {
+          scope.active = matchIdx;
+        };
+
+        scope.selectMatch = function(activeIdx) {
+          scope.select({activeIdx:activeIdx});
+        };
+      }
+    };
+  })
+
+  .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
+    return {
+      restrict: 'EA',
+      scope: {
+        index: '=',
+        match: '=',
+        query: '='
+      },
+      link:function(scope, element, attrs) {
+        var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
+        $templateRequest(tplUrl).then(function(tplContent) {
+          $compile(tplContent.trim())(scope, function(clonedElement) {
+            element.replaceWith(clonedElement);
+          });
+        });
+      }
+    };
+  }])
+
+  .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
+    var isSanitizePresent;
+    isSanitizePresent = $injector.has('$sanitize');
+
+    function escapeRegexp(queryToEscape) {
+      // Regex: capture the whole query string and replace it with the string that will be used to match
+      // the results, for example if the capture is "a" the result will be \a
+      return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
+    }
+
+    function containsHtml(matchItem) {
+      return /<.*>/g.test(matchItem);
+    }
+
+    return function(matchItem, query) {
+      if (!isSanitizePresent && containsHtml(matchItem)) {
+        $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+      }
+      matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+      if (!isSanitizePresent) {
+        matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+      }
+      return matchItem;
+    };
+  }]);
+
+
+/**
+ * @ngdoc overview
+ * @name ui.bootstrap.tabs
+ *
+ * @description
+ * AngularJS version of the tabs directive.
+ */
+
+angular.module('ui.bootstrap.tabs', [])
+
+.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
+  var ctrl = this,
+      tabs = ctrl.tabs = $scope.tabs = [];
+
+  ctrl.select = function(selectedTab) {
+    angular.forEach(tabs, function(tab) {
+      if (tab.active && tab !== selectedTab) {
+        tab.active = false;
+        tab.onDeselect();
+        selectedTab.selectCalled = false;
+      }
+    });
+    selectedTab.active = true;
+    // only call select if it has not already been called
+    if (!selectedTab.selectCalled) {
+      selectedTab.onSelect();
+      selectedTab.selectCalled = true;
+    }
+  };
+
+  ctrl.addTab = function addTab(tab) {
+    tabs.push(tab);
+    // we can't run the select function on the first tab
+    // since that would select it twice
+    if (tabs.length === 1 && tab.active !== false) {
+      tab.active = true;
+    } else if (tab.active) {
+      ctrl.select(tab);
+    } else {
+      tab.active = false;
+    }
+  };
+
+  ctrl.removeTab = function removeTab(tab) {
+    var index = tabs.indexOf(tab);
+    //Select a new tab if the tab to be removed is selected and not destroyed
+    if (tab.active && tabs.length > 1 && !destroyed) {
+      //If this is the last tab, select the previous tab. else, the next tab.
+      var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
+      ctrl.select(tabs[newActiveIndex]);
+    }
+    tabs.splice(index, 1);
+  };
+
+  var destroyed;
+  $scope.$on('$destroy', function() {
+    destroyed = true;
+  });
+}])
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabset
+ * @restrict EA
+ *
+ * @description
+ * Tabset is the outer container for the tabs directive
+ *
+ * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
+ * @param {boolean=} justified Whether or not to use justified styling for the tabs.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab heading="Tab 1"><b>First</b> Content!</tab>
+      <tab heading="Tab 2"><i>Second</i> Content!</tab>
+    </tabset>
+    <hr />
+    <tabset vertical="true">
+      <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
+      <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
+    </tabset>
+    <tabset justified="true">
+      <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
+      <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tabset', function() {
+  return {
+    restrict: 'EA',
+    transclude: true,
+    replace: true,
+    scope: {
+      type: '@'
+    },
+    controller: 'TabsetController',
+    templateUrl: 'template/tabs/tabset.html',
+    link: function(scope, element, attrs) {
+      scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
+      scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
+    }
+  };
+})
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tab
+ * @restrict EA
+ *
+ * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
+ * @param {string=} select An expression to evaluate when the tab is selected.
+ * @param {boolean=} active A binding, telling whether or not this tab is selected.
+ * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
+ *
+ * @description
+ * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <div ng-controller="TabsDemoCtrl">
+      <button class="btn btn-small" ng-click="items[0].active = true">
+        Select item 1, using active binding
+      </button>
+      <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
+        Enable/disable item 2, using disabled binding
+      </button>
+      <br />
+      <tabset>
+        <tab heading="Tab 1">First Tab</tab>
+        <tab select="alertMe()">
+          <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
+          Second Tab, with alert callback and html heading!
+        </tab>
+        <tab ng-repeat="item in items"
+          heading="{{item.title}}"
+          disabled="item.disabled"
+          active="item.active">
+          {{item.content}}
+        </tab>
+      </tabset>
+    </div>
+  </file>
+  <file name="script.js">
+    function TabsDemoCtrl($scope) {
+      $scope.items = [
+        { title:"Dynamic Title 1", content:"Dynamic Item 0" },
+        { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
+      ];
+
+      $scope.alertMe = function() {
+        setTimeout(function() {
+          alert("You've selected the alert tab!");
+        });
+      };
+    };
+  </file>
+</example>
+ */
+
+/**
+ * @ngdoc directive
+ * @name ui.bootstrap.tabs.directive:tabHeading
+ * @restrict EA
+ *
+ * @description
+ * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
+ *
+ * @example
+<example module="ui.bootstrap">
+  <file name="index.html">
+    <tabset>
+      <tab>
+        <tab-heading><b>HTML</b> in my titles?!</tab-heading>
+        And some content, too!
+      </tab>
+      <tab>
+        <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
+        That's right.
+      </tab>
+    </tabset>
+  </file>
+</example>
+ */
+.directive('tab', ['$parse', '$log', function($parse, $log) {
+  return {
+    require: '^tabset',
+    restrict: 'EA',
+    replace: true,
+    templateUrl: 'template/tabs/tab.html',
+    transclude: true,
+    scope: {
+      active: '=?',
+      heading: '@',
+      onSelect: '&select', //This callback is called in contentHeadingTransclude
+                          //once it inserts the tab's content into the dom
+      onDeselect: '&deselect'
+    },
+    controller: function() {
+      //Empty controller so other directives can require being 'under' a tab
+    },
+    link: function(scope, elm, attrs, tabsetCtrl, transclude) {
+      scope.$watch('active', function(active) {
+        if (active) {
+          tabsetCtrl.select(scope);
+        }
+      });
+
+      scope.disabled = false;
+      if (attrs.disable) {
+        scope.$parent.$watch($parse(attrs.disable), function(value) {
+          scope.disabled = !! value;
+        });
+      }
+
+      // Deprecation support of "disabled" parameter
+      // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
+      // This code is duplicated from the lines above to make it easy to remove once
+      // the feature has been completely deprecated
+      if (attrs.disabled) {
+        $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
+        scope.$parent.$watch($parse(attrs.disabled), function(value) {
+          scope.disabled = !! value;
+        });
+      }
+
+      scope.select = function() {
+        if (!scope.disabled) {
+          scope.active = true;
+        }
+      };
+
+      tabsetCtrl.addTab(scope);
+      scope.$on('$destroy', function() {
+        tabsetCtrl.removeTab(scope);
+      });
+
+      //We need to transclude later, once the content container is ready.
+      //when this link happens, we're inside a tab heading.
+      scope.$transcludeFn = transclude;
+    }
+  };
+}])
+
+.directive('tabHeadingTransclude', function() {
+  return {
+    restrict: 'A',
+    require: '^tab',
+    link: function(scope, elm, attrs, tabCtrl) {
+      scope.$watch('headingElement', function updateHeadingElement(heading) {
+        if (heading) {
+          elm.html('');
+          elm.append(heading);
+        }
+      });
+    }
+  };
+})
+
+.directive('tabContentTransclude', function() {
+  return {
+    restrict: 'A',
+    require: '^tabset',
+    link: function(scope, elm, attrs) {
+      var tab = scope.$eval(attrs.tabContentTransclude);
+
+      //Now our tab is ready to be transcluded: both the tab heading area
+      //and the tab content area are loaded.  Transclude 'em both.
+      tab.$transcludeFn(tab.$parent, function(contents) {
+        angular.forEach(contents, function(node) {
+          if (isTabHeading(node)) {
+            //Let tabHeadingTransclude know.
+            tab.headingElement = node;
+          } else {
+            elm.append(node);
+          }
+        });
+      });
+    }
+  };
+
+  function isTabHeading(node) {
+    return node.tagName && (
+      node.hasAttribute('tab-heading') ||
+      node.hasAttribute('data-tab-heading') ||
+      node.hasAttribute('x-tab-heading') ||
+      node.tagName.toLowerCase() === 'tab-heading' ||
+      node.tagName.toLowerCase() === 'data-tab-heading' ||
+      node.tagName.toLowerCase() === 'x-tab-heading'
+    );
+  }
+});
+
+/**
+ * The following features are still outstanding: popup delay, animation as a
+ * function, placement as a function, inside, support for more triggers than
+ * just mouse enter/leave, and selector delegatation.
+ */
+angular.module( 'ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
+
+.directive('popoverTemplatePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+      originScope: '&' },
+    templateUrl: 'template/popover/popover-template.html'
+  };
+})
+
+.directive('popoverTemplate', ['$tooltip', function($tooltip) {
+  return $tooltip('popoverTemplate', 'popover', 'click', {
+    useContentExp: true
+  });
+}])
+
+.directive('popoverHtmlPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/popover/popover-html.html'
+  };
+})
+
+.directive('popoverHtml', ['$tooltip', function($tooltip) {
+  return $tooltip( 'popoverHtml', 'popover', 'click', {
+    useContentExp: true
+  });
+}])
+
+.directive('popoverPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/popover/popover.html'
+  };
+})
+
+.directive('popover', ['$tooltip', function($tooltip) {
+  return $tooltip( 'popover', 'popover', 'click' );
+}]);
+
+/**
+ * The following features are still outstanding: animation as a
+ * function, placement as a function, inside, support for more triggers than
+ * just mouse enter/leave, html tooltips, and selector delegation.
+ */
+angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+
+/**
+ * The $tooltip service creates tooltip- and popover-like directives as well as
+ * houses global options for them.
+ */
+.provider('$tooltip', function() {
+  // The default options tooltip and popover.
+  var defaultOptions = {
+    placement: 'top',
+    animation: true,
+    popupDelay: 0,
+    useContentExp: false
+  };
+
+  // Default hide triggers for each show trigger
+  var triggerMap = {
+    'mouseenter': 'mouseleave',
+    'click': 'click',
+    'focus': 'blur',
+    'none': ''
+  };
+
+  // The options specified to the provider globally.
+  var globalOptions = {};
+
+  /**
+   * `options({})` allows global configuration of all tooltips in the
+   * application.
+   *
+   *   var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
+   *     // place tooltips left instead of top by default
+   *     $tooltipProvider.options( { placement: 'left' } );
+   *   });
+   */
+	this.options = function(value) {
+		angular.extend(globalOptions, value);
+	};
+
+  /**
+   * This allows you to extend the set of trigger mappings available. E.g.:
+   *
+   *   $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
+   */
+  this.setTriggers = function setTriggers(triggers) {
+    angular.extend(triggerMap, triggers);
+  };
+
+  /**
+   * This is a helper function for translating camel-case to snake-case.
+   */
+  function snake_case(name) {
+    var regexp = /[A-Z]/g;
+    var separator = '-';
+    return name.replace(regexp, function(letter, pos) {
+      return (pos ? separator : '') + letter.toLowerCase();
+    });
+  }
+
+  /**
+   * Returns the actual instance of the $tooltip service.
+   * TODO support multiple triggers
+   */
+  this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) {
+    return function $tooltip(type, prefix, defaultTriggerShow, options) {
+      options = angular.extend({}, defaultOptions, globalOptions, options);
+
+      /**
+       * Returns an object of show and hide triggers.
+       *
+       * If a trigger is supplied,
+       * it is used to show the tooltip; otherwise, it will use the `trigger`
+       * option passed to the `$tooltipProvider.options` method; else it will
+       * default to the trigger supplied to this directive factory.
+       *
+       * The hide trigger is based on the show trigger. If the `trigger` option
+       * was passed to the `$tooltipProvider.options` method, it will use the
+       * mapped trigger from `triggerMap` or the passed trigger if the map is
+       * undefined; otherwise, it uses the `triggerMap` value of the show
+       * trigger; else it will just use the show trigger.
+       */
+      function getTriggers(trigger) {
+        var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
+        var hide = show.map(function(trigger) {
+          return triggerMap[trigger] || trigger;
+        });
+        return {
+          show: show,
+          hide: hide
+        };
+      }
+
+      var directiveName = snake_case(type);
+
+      var startSym = $interpolate.startSymbol();
+      var endSym = $interpolate.endSymbol();
+      var template =
+        '<div '+ directiveName +'-popup '+
+          'title="'+startSym+'title'+endSym+'" '+
+          (options.useContentExp ?
+            'content-exp="contentExp()" ' :
+            'content="'+startSym+'content'+endSym+'" ') +
+          'placement="'+startSym+'placement'+endSym+'" '+
+          'popup-class="'+startSym+'popupClass'+endSym+'" '+
+          'animation="animation" '+
+          'is-open="isOpen"'+
+          'origin-scope="origScope" '+
+          '>'+
+        '</div>';
+
+      return {
+        restrict: 'EA',
+        compile: function(tElem, tAttrs) {
+          var tooltipLinker = $compile( template );
+
+          return function link(scope, element, attrs, tooltipCtrl) {
+            var tooltip;
+            var tooltipLinkedScope;
+            var transitionTimeout;
+            var popupTimeout;
+            var positionTimeout;
+            var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
+            var triggers = getTriggers(undefined);
+            var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
+            var ttScope = scope.$new(true);
+            var repositionScheduled = false;
+            var isOpenExp = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+
+            var positionTooltip = function() {
+              if (!tooltip) { return; }
+
+              if (!positionTimeout) {
+                positionTimeout = $timeout(function() {
+                  // Reset the positioning and box size for correct width and height values.
+                  tooltip.css({ top: 0, left: 0, width: 'auto', height: 'auto' });
+
+                  var ttBox = $position.position(tooltip);
+                  var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
+                  ttCss.top += 'px';
+                  ttCss.left += 'px';
+
+                  ttCss.width = ttBox.width + 'px';
+                  ttCss.height = ttBox.height + 'px';
+
+                  // Now set the calculated positioning and size.
+                  tooltip.css(ttCss);
+
+                  positionTimeout = null;
+
+                }, 0, false);
+              }
+            };
+
+            // Set up the correct scope to allow transclusion later
+            ttScope.origScope = scope;
+
+            // By default, the tooltip is not open.
+            // TODO add ability to start tooltip opened
+            ttScope.isOpen = false;
+
+            function toggleTooltipBind() {
+              if (!ttScope.isOpen) {
+                showTooltipBind();
+              } else {
+                hideTooltipBind();
+              }
+            }
+
+            // Show the tooltip with delay if specified, otherwise show it immediately
+            function showTooltipBind() {
+              if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
+                return;
+              }
+
+              prepareTooltip();
+
+              if (ttScope.popupDelay) {
+                // Do nothing if the tooltip was already scheduled to pop-up.
+                // This happens if show is triggered multiple times before any hide is triggered.
+                if (!popupTimeout) {
+                  popupTimeout = $timeout(show, ttScope.popupDelay, false);
+                }
+              } else {
+                show();
+              }
+            }
+
+            function hideTooltipBind () {
+              hide();
+              if (!$rootScope.$$phase) {
+                $rootScope.$digest();
+              }
+            }
+
+            // Show the tooltip popup element.
+            function show() {
+              popupTimeout = null;
+
+              // If there is a pending remove transition, we must cancel it, lest the
+              // tooltip be mysteriously removed.
+              if (transitionTimeout) {
+                $timeout.cancel(transitionTimeout);
+                transitionTimeout = null;
+              }
+
+              // Don't show empty tooltips.
+              if (!(options.useContentExp ? ttScope.contentExp() : ttScope.content)) {
+                return angular.noop;
+              }
+
+              createTooltip();
+
+              // And show the tooltip.
+              ttScope.isOpen = true;
+              if (isOpenExp) {
+                isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
+              }
+
+              if (!$rootScope.$$phase) {
+                ttScope.$apply(); // digest required as $apply is not called
+              }
+
+              tooltip.css({ display: 'block' });
+
+              positionTooltip();
+            }
+
+            // Hide the tooltip popup element.
+            function hide() {
+              // First things first: we don't show it anymore.
+              ttScope.isOpen = false;
+              if (isOpenExp) {
+                isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
+              }
+
+              //if tooltip is going to be shown after delay, we must cancel this
+              $timeout.cancel(popupTimeout);
+              popupTimeout = null;
+
+              $timeout.cancel(positionTimeout);
+              positionTimeout = null;
+
+              // And now we remove it from the DOM. However, if we have animation, we
+              // need to wait for it to expire beforehand.
+              // FIXME: this is a placeholder for a port of the transitions library.
+              if (ttScope.animation) {
+                if (!transitionTimeout) {
+                  transitionTimeout = $timeout(removeTooltip, 500);
+                }
+              } else {
+                removeTooltip();
+              }
+            }
+
+            function createTooltip() {
+              // There can only be one tooltip element per directive shown at once.
+              if (tooltip) {
+                removeTooltip();
+              }
+              tooltipLinkedScope = ttScope.$new();
+              tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
+                if (appendToBody) {
+                  $document.find('body').append(tooltip);
+                } else {
+                  element.after(tooltip);
+                }
+              });
+
+              if (options.useContentExp) {
+                tooltipLinkedScope.$watch('contentExp()', function(val) {
+                  if (!val && ttScope.isOpen) {
+                    hide();
+                  }
+                });
+
+                tooltipLinkedScope.$watch(function() {
+                  if (!repositionScheduled) {
+                    repositionScheduled = true;
+                    tooltipLinkedScope.$$postDigest(function() {
+                      repositionScheduled = false;
+                      if (ttScope.isOpen) {
+                        positionTooltip();
+                      }
+                    });
+                  }
+                });
+
+              }
+            }
+
+            function removeTooltip() {
+              transitionTimeout = null;
+              if (tooltip) {
+                tooltip.remove();
+                tooltip = null;
+              }
+              if (tooltipLinkedScope) {
+                tooltipLinkedScope.$destroy();
+                tooltipLinkedScope = null;
+              }
+            }
+
+            function prepareTooltip() {
+              prepPopupClass();
+              prepPlacement();
+              prepPopupDelay();
+            }
+
+            ttScope.contentExp = function() {
+              return scope.$eval(attrs[type]);
+            };
+
+            /**
+             * Observe the relevant attributes.
+             */
+            if (!options.useContentExp) {
+              attrs.$observe(type, function(val) {
+                ttScope.content = val;
+
+                if (!val && ttScope.isOpen) {
+                  hide();
+                } else {
+                  positionTooltip();
+                }
+              });
+            }
+
+            attrs.$observe('disabled', function(val) {
+              if (popupTimeout && val) {
+                $timeout.cancel(popupTimeout);
+                popupTimeout = null;
+              }
+
+              if (val && ttScope.isOpen) {
+                hide();
+              }
+            });
+
+            attrs.$observe(prefix + 'Title', function(val) {
+              ttScope.title = val;
+              positionTooltip();
+            });
+
+            attrs.$observe(prefix + 'Placement', function() {
+              if (ttScope.isOpen) {
+                prepPlacement();
+                positionTooltip();
+              }
+            });
+
+            if (isOpenExp) {
+              scope.$watch(isOpenExp, function(val) {
+                if (val !== ttScope.isOpen) {
+                  toggleTooltipBind();
+                }
+              });
+            }
+
+            function prepPopupClass() {
+              ttScope.popupClass = attrs[prefix + 'Class'];
+            }
+
+            function prepPlacement() {
+              var val = attrs[prefix + 'Placement'];
+              ttScope.placement = angular.isDefined(val) ? val : options.placement;
+            }
+
+            function prepPopupDelay() {
+              var val = attrs[prefix + 'PopupDelay'];
+              var delay = parseInt(val, 10);
+              ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+            }
+
+            var unregisterTriggers = function() {
+              triggers.show.forEach(function(trigger) {
+                element.unbind(trigger, showTooltipBind);
+              });
+              triggers.hide.forEach(function(trigger) {
+                element.unbind(trigger, hideTooltipBind);
+              });
+            };
+
+            function prepTriggers() {
+              var val = attrs[prefix + 'Trigger'];
+              unregisterTriggers();
+
+              triggers = getTriggers(val);
+
+              if (triggers.show !== 'none') {
+                triggers.show.forEach(function(trigger, idx) {
+                  // Using raw addEventListener due to jqLite/jQuery bug - #4060
+                  if (trigger === triggers.hide[idx]) {
+                    element[0].addEventListener(trigger, toggleTooltipBind);
+                  } else if (trigger) {
+                    element[0].addEventListener(trigger, showTooltipBind);
+                    element[0].addEventListener(triggers.hide[idx], hideTooltipBind);
+                  }
+                });
+              }
+            }
+            prepTriggers();
+
+            var animation = scope.$eval(attrs[prefix + 'Animation']);
+            ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
+
+            var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
+            appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
+
+            // if a tooltip is attached to <body> we need to remove it on
+            // location change as its parent scope will probably not be destroyed
+            // by the change.
+            if (appendToBody) {
+              scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() {
+                if (ttScope.isOpen) {
+                  hide();
+                }
+              });
+            }
+
+            // Make sure tooltip is destroyed and removed.
+            scope.$on('$destroy', function onDestroyTooltip() {
+              $timeout.cancel(transitionTimeout);
+              $timeout.cancel(popupTimeout);
+              $timeout.cancel(positionTimeout);
+              unregisterTriggers();
+              removeTooltip();
+              ttScope = null;
+            });
+          };
+        }
+      };
+    };
+  }];
+})
+
+// This is mostly ngInclude code but with a custom scope
+.directive('tooltipTemplateTransclude', [
+         '$animate', '$sce', '$compile', '$templateRequest',
+function ($animate ,  $sce ,  $compile ,  $templateRequest) {
+  return {
+    link: function(scope, elem, attrs) {
+      var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
+
+      var changeCounter = 0,
+        currentScope,
+        previousElement,
+        currentElement;
+
+      var cleanupLastIncludeContent = function() {
+        if (previousElement) {
+          previousElement.remove();
+          previousElement = null;
+        }
+        if (currentScope) {
+          currentScope.$destroy();
+          currentScope = null;
+        }
+        if (currentElement) {
+          $animate.leave(currentElement).then(function() {
+            previousElement = null;
+          });
+          previousElement = currentElement;
+          currentElement = null;
+        }
+      };
+
+      scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
+        var thisChangeId = ++changeCounter;
+
+        if (src) {
+          //set the 2nd param to true to ignore the template request error so that the inner
+          //contents and scope can be cleaned up.
+          $templateRequest(src, true).then(function(response) {
+            if (thisChangeId !== changeCounter) { return; }
+            var newScope = origScope.$new();
+            var template = response;
+
+            var clone = $compile(template)(newScope, function(clone) {
+              cleanupLastIncludeContent();
+              $animate.enter(clone, elem);
+            });
+
+            currentScope = newScope;
+            currentElement = clone;
+
+            currentScope.$emit('$includeContentLoaded', src);
+          }, function() {
+            if (thisChangeId === changeCounter) {
+              cleanupLastIncludeContent();
+              scope.$emit('$includeContentError', src);
+            }
+          });
+          scope.$emit('$includeContentRequested', src);
+        } else {
+          cleanupLastIncludeContent();
+        }
+      });
+
+      scope.$on('$destroy', cleanupLastIncludeContent);
+    }
+  };
+}])
+
+/**
+ * Note that it's intentional that these classes are *not* applied through $animate.
+ * They must not be animated as they're expected to be present on the tooltip on
+ * initialization.
+ */
+.directive('tooltipClasses', function() {
+  return {
+    restrict: 'A',
+    link: function(scope, element, attrs) {
+      if (scope.placement) {
+        element.addClass(scope.placement);
+      }
+      if (scope.popupClass) {
+        element.addClass(scope.popupClass);
+      }
+      if (scope.animation()) {
+        element.addClass(attrs.tooltipAnimationClass);
+      }
+    }
+  };
+})
+
+.directive('tooltipPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-popup.html'
+  };
+})
+
+.directive('tooltip', [ '$tooltip', function($tooltip) {
+  return $tooltip('tooltip', 'tooltip', 'mouseenter');
+}])
+
+.directive('tooltipTemplatePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+      originScope: '&' },
+    templateUrl: 'template/tooltip/tooltip-template-popup.html'
+  };
+})
+
+.directive('tooltipTemplate', ['$tooltip', function($tooltip) {
+  return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
+    useContentExp: true
+  });
+}])
+
+.directive('tooltipHtmlPopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-html-popup.html'
+  };
+})
+
+.directive('tooltipHtml', ['$tooltip', function($tooltip) {
+  return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
+    useContentExp: true
+  });
+}])
+
+/*
+Deprecated
+*/
+.directive('tooltipHtmlUnsafePopup', function() {
+  return {
+    restrict: 'EA',
+    replace: true,
+    scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+    templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
+  };
+})
+
+.value('tooltipHtmlUnsafeSuppressDeprecated', false)
+.directive('tooltipHtmlUnsafe', [
+          '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
+function($tooltip ,  tooltipHtmlUnsafeSuppressDeprecated ,  $log) {
+  if (!tooltipHtmlUnsafeSuppressDeprecated) {
+    $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
+  }
+  return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter');
+}]);
+
+angular.module('ui.bootstrap.modal', [])
+
+/**
+ * A helper, internal data structure that acts as a map but also allows getting / removing
+ * elements in the LIFO order
+ */
+  .factory('$$stackedMap', function() {
+    return {
+      createNew: function() {
+        var stack = [];
+
+        return {
+          add: function(key, value) {
+            stack.push({
+              key: key,
+              value: value
+            });
+          },
+          get: function(key) {
+            for (var i = 0; i < stack.length; i++) {
+              if (key == stack[i].key) {
+                return stack[i];
+              }
+            }
+          },
+          keys: function() {
+            var keys = [];
+            for (var i = 0; i < stack.length; i++) {
+              keys.push(stack[i].key);
+            }
+            return keys;
+          },
+          top: function() {
+            return stack[stack.length - 1];
+          },
+          remove: function(key) {
+            var idx = -1;
+            for (var i = 0; i < stack.length; i++) {
+              if (key == stack[i].key) {
+                idx = i;
+                break;
+              }
+            }
+            return stack.splice(idx, 1)[0];
+          },
+          removeTop: function() {
+            return stack.splice(stack.length - 1, 1)[0];
+          },
+          length: function() {
+            return stack.length;
+          }
+        };
+      }
+    };
+  })
+
+/**
+ * A helper, internal data structure that stores all references attached to key
+ */
+  .factory('$$multiMap', function() {
+    return {
+      createNew: function() {
+        var map = {};
+
+        return {
+          entries: function() {
+            return Object.keys(map).map(function(key) {
+              return {
+                key: key,
+                value: map[key]
+              };
+            });
+          },
+          get: function(key) {
+            return map[key];
+          },
+          hasKey: function(key) {
+            return !!map[key];
+          },
+          keys: function() {
+            return Object.keys(map);
+          },
+          put: function(key, value) {
+            if (!map[key]) {
+              map[key] = [];
+            }
+
+            map[key].push(value);
+          },
+          remove: function(key, value) {
+            var values = map[key];
+
+            if (!values) {
+              return;
+            }
+
+            var idx = values.indexOf(value);
+
+            if (idx !== -1) {
+              values.splice(idx, 1);
+            }
+
+            if (!values.length) {
+              delete map[key];
+            }
+          }
+        };
+      }
+    };
+  })
+
+/**
+ * A helper directive for the $modal service. It creates a backdrop element.
+ */
+  .directive('modalBackdrop', [
+           '$animate', '$injector', '$modalStack',
+  function($animate ,  $injector,   $modalStack) {
+    var $animateCss = null;
+
+    if ($injector.has('$animateCss')) {
+      $animateCss = $injector.get('$animateCss');
+    }
+
+    return {
+      restrict: 'EA',
+      replace: true,
+      templateUrl: 'template/modal/backdrop.html',
+      compile: function(tElement, tAttrs) {
+        tElement.addClass(tAttrs.backdropClass);
+        return linkFn;
+      }
+    };
+
+    function linkFn(scope, element, attrs) {
+      if (attrs.modalInClass) {
+        if ($animateCss) {
+          $animateCss(element, {
+            addClass: attrs.modalInClass
+          }).start();
+        } else {
+          $animate.addClass(element, attrs.modalInClass);
+        }
+
+        scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+          var done = setIsAsync();
+          if ($animateCss) {
+            $animateCss(element, {
+              removeClass: attrs.modalInClass
+            }).start().then(done);
+          } else {
+            $animate.removeClass(element, attrs.modalInClass).then(done);
+          }
+        });
+      }
+    }
+  }])
+
+  .directive('modalWindow', [
+           '$modalStack', '$q', '$animate', '$injector',
+  function($modalStack ,  $q ,  $animate,   $injector) {
+    var $animateCss = null;
+
+    if ($injector.has('$animateCss')) {
+      $animateCss = $injector.get('$animateCss');
+    }
+
+    return {
+      restrict: 'EA',
+      scope: {
+        index: '@'
+      },
+      replace: true,
+      transclude: true,
+      templateUrl: function(tElement, tAttrs) {
+        return tAttrs.templateUrl || 'template/modal/window.html';
+      },
+      link: function(scope, element, attrs) {
+        element.addClass(attrs.windowClass || '');
+        scope.size = attrs.size;
+
+        scope.close = function(evt) {
+          var modal = $modalStack.getTop();
+          if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
+            evt.preventDefault();
+            evt.stopPropagation();
+            $modalStack.dismiss(modal.key, 'backdrop click');
+          }
+        };
+
+        // This property is only added to the scope for the purpose of detecting when this directive is rendered.
+        // We can detect that by using this property in the template associated with this directive and then use
+        // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
+        scope.$isRendered = true;
+
+        // Deferred object that will be resolved when this modal is render.
+        var modalRenderDeferObj = $q.defer();
+        // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
+        // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
+        attrs.$observe('modalRender', function(value) {
+          if (value == 'true') {
+            modalRenderDeferObj.resolve();
+          }
+        });
+
+        modalRenderDeferObj.promise.then(function() {
+          var animationPromise = null;
+
+          if (attrs.modalInClass) {
+            if ($animateCss) {
+              animationPromise = $animateCss(element, {
+                addClass: attrs.modalInClass
+              }).start();
+            } else {
+              animationPromise = $animate.addClass(element, attrs.modalInClass);
+            }
+
+            scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+              var done = setIsAsync();
+              if ($animateCss) {
+                $animateCss(element, {
+                  removeClass: attrs.modalInClass
+                }).start().then(done);
+              } else {
+                $animate.removeClass(element, attrs.modalInClass).then(done);
+              }
+            });
+          }
+
+
+          $q.when(animationPromise).then(function() {
+            var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
+            /**
+             * Auto-focusing of a freshly-opened modal element causes any child elements
+             * with the autofocus attribute to lose focus. This is an issue on touch
+             * based devices which will show and then hide the onscreen keyboard.
+             * Attempts to refocus the autofocus element via JavaScript will not reopen
+             * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
+             * the modal element if the modal does not contain an autofocus element.
+             */
+            if (inputsWithAutofocus.length) {
+              inputsWithAutofocus[0].focus();
+            } else {
+              element[0].focus();
+            }
+          });
+
+          // Notify {@link $modalStack} that modal is rendered.
+          var modal = $modalStack.getTop();
+          if (modal) {
+            $modalStack.modalRendered(modal.key);
+          }
+        });
+      }
+    };
+  }])
+
+  .directive('modalAnimationClass', [
+    function () {
+      return {
+        compile: function(tElement, tAttrs) {
+          if (tAttrs.modalAnimation) {
+            tElement.addClass(tAttrs.modalAnimationClass);
+          }
+        }
+      };
+    }])
+
+  .directive('modalTransclude', function() {
+    return {
+      link: function($scope, $element, $attrs, controller, $transclude) {
+        $transclude($scope.$parent, function(clone) {
+          $element.empty();
+          $element.append(clone);
+        });
+      }
+    };
+  })
+
+  .factory('$modalStack', [
+             '$animate', '$timeout', '$document', '$compile', '$rootScope',
+             '$q',
+             '$injector',
+             '$$multiMap',
+             '$$stackedMap',
+    function($animate ,  $timeout ,  $document ,  $compile ,  $rootScope ,
+              $q,
+              $injector,
+              $$multiMap,
+              $$stackedMap) {
+      var $animateCss = null;
+
+      if ($injector.has('$animateCss')) {
+        $animateCss = $injector.get('$animateCss');
+      }
+
+      var OPENED_MODAL_CLASS = 'modal-open';
+
+      var backdropDomEl, backdropScope;
+      var openedWindows = $$stackedMap.createNew();
+      var openedClasses = $$multiMap.createNew();
+      var $modalStack = {
+        NOW_CLOSING_EVENT: 'modal.stack.now-closing'
+      };
+
+      //Modal focus behavior
+      var focusableElementList;
+      var focusIndex = 0;
+      var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' +
+        'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
+        'iframe, object, embed, *[tabindex], *[contenteditable=true]';
+
+      function backdropIndex() {
+        var topBackdropIndex = -1;
+        var opened = openedWindows.keys();
+        for (var i = 0; i < opened.length; i++) {
+          if (openedWindows.get(opened[i]).value.backdrop) {
+            topBackdropIndex = i;
+          }
+        }
+        return topBackdropIndex;
+      }
+
+      $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
+        if (backdropScope) {
+          backdropScope.index = newBackdropIndex;
+        }
+      });
+
+      function removeModalWindow(modalInstance, elementToReceiveFocus) {
+        var body = $document.find('body').eq(0);
+        var modalWindow = openedWindows.get(modalInstance).value;
+
+        //clean up the stack
+        openedWindows.remove(modalInstance);
+
+        removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
+          var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
+          openedClasses.remove(modalBodyClass, modalInstance);
+          body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
+        });
+        checkRemoveBackdrop();
+
+        //move focus to specified element if available, or else to body
+        if (elementToReceiveFocus && elementToReceiveFocus.focus) {
+          elementToReceiveFocus.focus();
+        } else {
+          body.focus();
+        }
+      }
+
+      function checkRemoveBackdrop() {
+          //remove backdrop if no longer needed
+          if (backdropDomEl && backdropIndex() == -1) {
+            var backdropScopeRef = backdropScope;
+            removeAfterAnimate(backdropDomEl, backdropScope, function() {
+              backdropScopeRef = null;
+            });
+            backdropDomEl = undefined;
+            backdropScope = undefined;
+          }
+      }
+
+      function removeAfterAnimate(domEl, scope, done) {
+        var asyncDeferred;
+        var asyncPromise = null;
+        var setIsAsync = function() {
+          if (!asyncDeferred) {
+            asyncDeferred = $q.defer();
+            asyncPromise = asyncDeferred.promise;
+          }
+
+          return function asyncDone() {
+            asyncDeferred.resolve();
+          };
+        };
+        scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
+
+        // Note that it's intentional that asyncPromise might be null.
+        // That's when setIsAsync has not been called during the
+        // NOW_CLOSING_EVENT broadcast.
+        return $q.when(asyncPromise).then(afterAnimating);
+
+        function afterAnimating() {
+          if (afterAnimating.done) {
+            return;
+          }
+          afterAnimating.done = true;
+
+          if ($animateCss) {
+            $animateCss(domEl, {
+              event: 'leave'
+            }).start().then(function() {
+              domEl.remove();
+            });
+          } else {
+            $animate.leave(domEl);
+          }
+          scope.$destroy();
+          if (done) {
+            done();
+          }
+        }
+      }
+
+      $document.bind('keydown', function(evt) {
+        if (evt.isDefaultPrevented()) {
+          return evt;
+        }
+
+        var modal = openedWindows.top();
+        if (modal && modal.value.keyboard) {
+          switch (evt.which){
+            case 27: {
+              evt.preventDefault();
+              $rootScope.$apply(function() {
+                $modalStack.dismiss(modal.key, 'escape key press');
+              });
+              break;
+            }
+            case 9: {
+              $modalStack.loadFocusElementList(modal);
+              var focusChanged = false;
+              if (evt.shiftKey) {
+                if ($modalStack.isFocusInFirstItem(evt)) {
+                  focusChanged = $modalStack.focusLastFocusableElement();
+                }
+              } else {
+                if ($modalStack.isFocusInLastItem(evt)) {
+                  focusChanged = $modalStack.focusFirstFocusableElement();
+                }
+              }
+
+              if (focusChanged) {
+                evt.preventDefault();
+                evt.stopPropagation();
+              }
+              break;
+            }
+          }
+        }
+      });
+
+      $modalStack.open = function(modalInstance, modal) {
+        var modalOpener = $document[0].activeElement,
+          modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
+
+        openedWindows.add(modalInstance, {
+          deferred: modal.deferred,
+          renderDeferred: modal.renderDeferred,
+          modalScope: modal.scope,
+          backdrop: modal.backdrop,
+          keyboard: modal.keyboard,
+          openedClass: modal.openedClass
+        });
+
+        openedClasses.put(modalBodyClass, modalInstance);
+
+        var body = $document.find('body').eq(0),
+            currBackdropIndex = backdropIndex();
+
+        if (currBackdropIndex >= 0 && !backdropDomEl) {
+          backdropScope = $rootScope.$new(true);
+          backdropScope.index = currBackdropIndex;
+          var angularBackgroundDomEl = angular.element('<div modal-backdrop="modal-backdrop"></div>');
+          angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
+          if (modal.animation) {
+            angularBackgroundDomEl.attr('modal-animation', 'true');
+          }
+          backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
+          body.append(backdropDomEl);
+        }
+
+        var angularDomEl = angular.element('<div modal-window="modal-window"></div>');
+        angularDomEl.attr({
+          'template-url': modal.windowTemplateUrl,
+          'window-class': modal.windowClass,
+          'size': modal.size,
+          'index': openedWindows.length() - 1,
+          'animate': 'animate'
+        }).html(modal.content);
+        if (modal.animation) {
+          angularDomEl.attr('modal-animation', 'true');
+        }
+
+        var modalDomEl = $compile(angularDomEl)(modal.scope);
+        openedWindows.top().value.modalDomEl = modalDomEl;
+        openedWindows.top().value.modalOpener = modalOpener;
+        body.append(modalDomEl);
+        body.addClass(modalBodyClass);
+
+        $modalStack.clearFocusListCache();
+      };
+
+      function broadcastClosing(modalWindow, resultOrReason, closing) {
+          return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
+      }
+
+      $modalStack.close = function(modalInstance, result) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow && broadcastClosing(modalWindow, result, true)) {
+          modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+          modalWindow.value.deferred.resolve(result);
+          removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+          return true;
+        }
+        return !modalWindow;
+      };
+
+      $modalStack.dismiss = function(modalInstance, reason) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
+          modalWindow.value.modalScope.$$uibDestructionScheduled = true;
+          modalWindow.value.deferred.reject(reason);
+          removeModalWindow(modalInstance, modalWindow.value.modalOpener);
+          return true;
+        }
+        return !modalWindow;
+      };
+
+      $modalStack.dismissAll = function(reason) {
+        var topModal = this.getTop();
+        while (topModal && this.dismiss(topModal.key, reason)) {
+          topModal = this.getTop();
+        }
+      };
+
+      $modalStack.getTop = function() {
+        return openedWindows.top();
+      };
+
+      $modalStack.modalRendered = function(modalInstance) {
+        var modalWindow = openedWindows.get(modalInstance);
+        if (modalWindow) {
+          modalWindow.value.renderDeferred.resolve();
+        }
+      };
+
+      $modalStack.focusFirstFocusableElement = function() {
+        if (focusableElementList.length > 0) {
+          focusableElementList[0].focus();
+          return true;
+        }
+        return false;
+      };
+      $modalStack.focusLastFocusableElement = function() {
+        if (focusableElementList.length > 0) {
+          focusableElementList[focusableElementList.length - 1].focus();
+          return true;
+        }
+        return false;
+      };
+
+      $modalStack.isFocusInFirstItem = function(evt) {
+        if (focusableElementList.length > 0) {
+          return (evt.target || evt.srcElement) == focusableElementList[0];
+        }
+        return false;
+      };
+
+      $modalStack.isFocusInLastItem = function(evt) {
+        if (focusableElementList.length > 0) {
+          return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1];
+        }
+        return false;
+      };
+
+      $modalStack.clearFocusListCache = function() {
+        focusableElementList = [];
+        focusIndex = 0;
+      };
+
+      $modalStack.loadFocusElementList = function(modalWindow) {
+        if (focusableElementList === undefined || !focusableElementList.length0) {
+          if (modalWindow) {
+            var modalDomE1 = modalWindow.value.modalDomEl;
+            if (modalDomE1 && modalDomE1.length) {
+              focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector);
+            }
+          }
+        }
+      };
+
+      return $modalStack;
+    }])
+
+  .provider('$modal', function() {
+    var $modalProvider = {
+      options: {
+        animation: true,
+        backdrop: true, //can also be false or 'static'
+        keyboard: true
+      },
+      $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack',
+        function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) {
+          var $modal = {};
+
+          function getTemplatePromise(options) {
+            return options.template ? $q.when(options.template) :
+              $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);
+          }
+
+          function getResolvePromises(resolves) {
+            var promisesArr = [];
+            angular.forEach(resolves, function(value) {
+              if (angular.isFunction(value) || angular.isArray(value)) {
+                promisesArr.push($q.when($injector.invoke(value)));
+              } else if (angular.isString(value)) {
+                promisesArr.push($q.when($injector.get(value)));
+              } else {
+                promisesArr.push($q.when(value));
+              }
+            });
+            return promisesArr;
+          }
+
+          var promiseChain = null;
+          $modal.getPromiseChain = function() {
+            return promiseChain;
+          };
+
+          $modal.open = function (modalOptions) {
+
+            var modalResultDeferred = $q.defer();
+            var modalOpenedDeferred = $q.defer();
+            var modalRenderDeferred = $q.defer();
+
+            //prepare an instance of a modal to be injected into controllers and returned to a caller
+            var modalInstance = {
+              result: modalResultDeferred.promise,
+              opened: modalOpenedDeferred.promise,
+              rendered: modalRenderDeferred.promise,
+              close: function (result) {
+                return $modalStack.close(modalInstance, result);
+              },
+              dismiss: function (reason) {
+                return $modalStack.dismiss(modalInstance, reason);
+              }
+            };
+
+            //merge and clean up options
+            modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
+            modalOptions.resolve = modalOptions.resolve || {};
+
+            //verify options
+            if (!modalOptions.template && !modalOptions.templateUrl) {
+              throw new Error('One of template or templateUrl options is required.');
+            }
+
+            var templateAndResolvePromise =
+              $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
+
+            // Wait for the resolution of the existing promise chain.
+            // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
+            // Then add to $modalStack and resolve opened.
+            // Finally clean up the chain variable if no subsequent modal has overwritten it.
+            var samePromise;
+            samePromise = promiseChain = $q.all([promiseChain])
+              .then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; })
+              .then(function resolveSuccess(tplAndVars) {
+
+                var modalScope = (modalOptions.scope || $rootScope).$new();
+                modalScope.$close = modalInstance.close;
+                modalScope.$dismiss = modalInstance.dismiss;
+
+                modalScope.$on('$destroy', function() {
+                  if (!modalScope.$$uibDestructionScheduled) {
+                    modalScope.$dismiss('$uibUnscheduledDestruction');
+                  }
+                });
+
+                var ctrlInstance, ctrlLocals = {};
+                var resolveIter = 1;
+
+                //controllers
+                if (modalOptions.controller) {
+                  ctrlLocals.$scope = modalScope;
+                  ctrlLocals.$modalInstance = modalInstance;
+                  angular.forEach(modalOptions.resolve, function(value, key) {
+                    ctrlLocals[key] = tplAndVars[resolveIter++];
+                  });
+
+                  ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
+                  if (modalOptions.controllerAs) {
+                    if (modalOptions.bindToController) {
+                      angular.extend(ctrlInstance, modalScope);
+                    }
+
+                    modalScope[modalOptions.controllerAs] = ctrlInstance;
+                  }
+                }
+
+                $modalStack.open(modalInstance, {
+                  scope: modalScope,
+                  deferred: modalResultDeferred,
+                  renderDeferred: modalRenderDeferred,
+                  content: tplAndVars[0],
+                  animation: modalOptions.animation,
+                  backdrop: modalOptions.backdrop,
+                  keyboard: modalOptions.keyboard,
+                  backdropClass: modalOptions.backdropClass,
+                  windowClass: modalOptions.windowClass,
+                  windowTemplateUrl: modalOptions.windowTemplateUrl,
+                  size: modalOptions.size,
+                  openedClass: modalOptions.openedClass
+                });
+                modalOpenedDeferred.resolve(true);
+
+            }, function resolveError(reason) {
+              modalOpenedDeferred.reject(reason);
+              modalResultDeferred.reject(reason);
+            })
+            .finally(function() {
+              if (promiseChain === samePromise) {
+                promiseChain = null;
+              }
+            });
+
+            return modalInstance;
+          };
+
+          return $modal;
+        }]
+    };
+
+    return $modalProvider;
+  });
+
+angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
+
+.constant('dropdownConfig', {
+  openClass: 'open'
+})
+
+.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
+  var openScope = null;
+
+  this.open = function(dropdownScope) {
+    if (!openScope) {
+      $document.bind('click', closeDropdown);
+      $document.bind('keydown', keybindFilter);
+    }
+
+    if (openScope && openScope !== dropdownScope) {
+      openScope.isOpen = false;
+    }
+
+    openScope = dropdownScope;
+  };
+
+  this.close = function(dropdownScope) {
+    if (openScope === dropdownScope) {
+      openScope = null;
+      $document.unbind('click', closeDropdown);
+      $document.unbind('keydown', keybindFilter);
+    }
+  };
+
+  var closeDropdown = function(evt) {
+    // This method may still be called during the same mouse event that
+    // unbound this event handler. So check openScope before proceeding.
+    if (!openScope) { return; }
+
+    if (evt && openScope.getAutoClose() === 'disabled')  { return ; }
+
+    var toggleElement = openScope.getToggleElement();
+    if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
+      return;
+    }
+
+    var dropdownElement = openScope.getDropdownElement();
+    if (evt && openScope.getAutoClose() === 'outsideClick' &&
+      dropdownElement && dropdownElement[0].contains(evt.target)) {
+      return;
+    }
+
+    openScope.isOpen = false;
+
+    if (!$rootScope.$$phase) {
+      openScope.$apply();
+    }
+  };
+
+  var keybindFilter = function(evt) {
+    if (evt.which === 27) {
+      openScope.focusToggleElement();
+      closeDropdown();
+    } else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) {
+      evt.preventDefault();
+      evt.stopPropagation();
+      openScope.focusDropdownEntry(evt.which);
+    }
+  };
+}])
+
+.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
+  var self = this,
+    scope = $scope.$new(), // create a child scope so we are not polluting original one
+    templateScope,
+    openClass = dropdownConfig.openClass,
+    getIsOpen,
+    setIsOpen = angular.noop,
+    toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
+    appendToBody = false,
+    keynavEnabled = false,
+    selectedOption = null,
+    body = $document.find('body');
+
+  this.init = function(element) {
+    self.$element = element;
+
+    if ($attrs.isOpen) {
+      getIsOpen = $parse($attrs.isOpen);
+      setIsOpen = getIsOpen.assign;
+
+      $scope.$watch(getIsOpen, function(value) {
+        scope.isOpen = !!value;
+      });
+    }
+
+    appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
+    keynavEnabled = angular.isDefined($attrs.keyboardNav);
+
+    if (appendToBody && self.dropdownMenu) {
+      body.append(self.dropdownMenu);
+      body.addClass('dropdown');
+      element.on('$destroy', function handleDestroyEvent() {
+        self.dropdownMenu.remove();
+      });
+    }
+  };
+
+  this.toggle = function(open) {
+    return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
+  };
+
+  // Allow other directives to watch status
+  this.isOpen = function() {
+    return scope.isOpen;
+  };
+
+  scope.getToggleElement = function() {
+    return self.toggleElement;
+  };
+
+  scope.getAutoClose = function() {
+    return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
+  };
+
+  scope.getElement = function() {
+    return self.$element;
+  };
+
+  scope.isKeynavEnabled = function() {
+    return keynavEnabled;
+  };
+
+  scope.focusDropdownEntry = function(keyCode) {
+    var elems = self.dropdownMenu ? //If append to body is used.
+      (angular.element(self.dropdownMenu).find('a')) :
+      (angular.element(self.$element).find('ul').eq(0).find('a'));
+
+    switch (keyCode) {
+      case (40): {
+        if (!angular.isNumber(self.selectedOption)) {
+          self.selectedOption = 0;
+        } else {
+          self.selectedOption = (self.selectedOption === elems.length -1 ?
+            self.selectedOption :
+            self.selectedOption + 1);
+        }
+        break;
+      }
+      case (38): {
+        if (!angular.isNumber(self.selectedOption)) {
+          self.selectedOption = elems.length - 1;
+        } else {
+          self.selectedOption = self.selectedOption === 0 ?
+            0 : self.selectedOption - 1;
+        }
+        break;
+      }
+    }
+    elems[self.selectedOption].focus();
+  };
+
+  scope.getDropdownElement = function() {
+    return self.dropdownMenu;
+  };
+
+  scope.focusToggleElement = function() {
+    if (self.toggleElement) {
+      self.toggleElement[0].focus();
+    }
+  };
+
+  scope.$watch('isOpen', function(isOpen, wasOpen) {
+    if (appendToBody && self.dropdownMenu) {
+      var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
+      var css = {
+        top: pos.top + 'px',
+        display: isOpen ? 'block' : 'none'
+      };
+
+      var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
+      if (!rightalign) {
+        css.left = pos.left + 'px';
+        css.right = 'auto';
+      } else {
+        css.left = 'auto';
+        css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px';
+      }
+
+      self.dropdownMenu.css(css);
+    }
+
+    var openContainer = appendToBody ? body : self.$element;
+
+    $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
+      if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
+        toggleInvoker($scope, { open: !!isOpen });
+      }
+    });
+
+    if (isOpen) {
+      if (self.dropdownMenuTemplateUrl) {
+        $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
+          templateScope = scope.$new();
+          $compile(tplContent.trim())(templateScope, function(dropdownElement) {
+            var newEl = dropdownElement;
+            self.dropdownMenu.replaceWith(newEl);
+            self.dropdownMenu = newEl;
+          });
+        });
+      }
+
+      scope.focusToggleElement();
+      dropdownService.open(scope);
+    } else {
+      if (self.dropdownMenuTemplateUrl) {
+        if (templateScope) {
+          templateScope.$destroy();
+        }
+        var newEl = angular.element('<ul class="dropdown-menu"></ul>');
+        self.dropdownMenu.replaceWith(newEl);
+        self.dropdownMenu = newEl;
+      }
+
+      dropdownService.close(scope);
+      self.selectedOption = null;
+    }
+
+    if (angular.isFunction(setIsOpen)) {
+      setIsOpen($scope, isOpen);
+    }
+  });
+
+  $scope.$on('$locationChangeSuccess', function() {
+    if (scope.getAutoClose() !== 'disabled') {
+      scope.isOpen = false;
+    }
+  });
+
+  var offDestroy = $scope.$on('$destroy', function() {
+    scope.$destroy();
+  });
+  scope.$on('$destroy', offDestroy);
+}])
+
+.directive('dropdown', function() {
+  return {
+    controller: 'DropdownController',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      dropdownCtrl.init( element );
+      element.addClass('dropdown');
+    }
+  };
+})
+
+.directive('dropdownMenu', function() {
+  return {
+    restrict: 'AC',
+    require: '?^dropdown',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      if (!dropdownCtrl) {
+        return;
+      }
+      var tplUrl = attrs.templateUrl;
+      if (tplUrl) {
+        dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
+      }
+      if (!dropdownCtrl.dropdownMenu) {
+        dropdownCtrl.dropdownMenu = element;
+      }
+    }
+  };
+})
+
+.directive('keyboardNav', function() {
+  return {
+    restrict: 'A',
+    require: '?^dropdown',
+    link: function (scope, element, attrs, dropdownCtrl) {
+
+      element.bind('keydown', function(e) {
+        if ([38, 40].indexOf(e.which) !== -1) {
+          e.preventDefault();
+          e.stopPropagation();
+
+          var elems = dropdownCtrl.dropdownMenu.find('a');
+
+          switch (e.which) {
+            case (40): { // Down
+              if (!angular.isNumber(dropdownCtrl.selectedOption)) {
+                dropdownCtrl.selectedOption = 0;
+              } else {
+                dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
+                  dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
+              }
+              break;
+            }
+            case (38): { // Up
+              if (!angular.isNumber(dropdownCtrl.selectedOption)) {
+                dropdownCtrl.selectedOption = elems.length - 1;
+              } else {
+                dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
+                  0 : dropdownCtrl.selectedOption - 1;
+              }
+              break;
+            }
+          }
+          elems[dropdownCtrl.selectedOption].focus();
+        }
+      });
+    }
+  };
+})
+
+.directive('dropdownToggle', function() {
+  return {
+    require: '?^dropdown',
+    link: function(scope, element, attrs, dropdownCtrl) {
+      if (!dropdownCtrl) {
+        return;
+      }
+
+      element.addClass('dropdown-toggle');
+
+      dropdownCtrl.toggleElement = element;
+
+      var toggleDropdown = function(event) {
+        event.preventDefault();
+
+        if (!element.hasClass('disabled') && !attrs.disabled) {
+          scope.$apply(function() {
+            dropdownCtrl.toggle();
+          });
+        }
+      };
+
+      element.bind('click', toggleDropdown);
+
+      // WAI-ARIA
+      element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
+      scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
+        element.attr('aria-expanded', !!isOpen);
+      });
+
+      scope.$on('$destroy', function() {
+        element.unbind('click', toggleDropdown);
+      });
+    }
+  };
+});
+
+angular.module('ui.bootstrap.rating', [])
+
+.constant('ratingConfig', {
+  max: 5,
+  stateOn: null,
+  stateOff: null,
+  titles : ['one', 'two', 'three', 'four', 'five']
+})
+
+.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
+  var ngModelCtrl  = { $setViewValue: angular.noop };
+
+  this.init = function(ngModelCtrl_) {
+    ngModelCtrl = ngModelCtrl_;
+    ngModelCtrl.$render = this.render;
+
+    ngModelCtrl.$formatters.push(function(value) {
+      if (angular.isNumber(value) && value << 0 !== value) {
+        value = Math.round(value);
+      }
+      return value;
+    });
+
+    this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
+    this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
+    var tmpTitles = angular.isDefined($attrs.titles)  ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ;    
+    this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
+      tmpTitles : ratingConfig.titles;
+    
+    var ratingStates = angular.isDefined($attrs.ratingStates) ?
+      $scope.$parent.$eval($attrs.ratingStates) :
+      new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
+    $scope.range = this.buildTemplateObjects(ratingStates);
+  };
+
+  this.buildTemplateObjects = function(states) {
+    for (var i = 0, n = states.length; i < n; i++) {
+      states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
+    }
+    return states;
+  };
+  
+  this.getTitle = function(index) {
+    if (index >= this.titles.length) {
+      return index + 1;
+    } else {
+      return this.titles[index];
+    }
+  };
+  
+  $scope.rate = function(value) {
+    if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
+      ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value);
+      ngModelCtrl.$render();
+    }
+  };
+
+  $scope.enter = function(value) {
+    if (!$scope.readonly) {
+      $scope.value = value;
+    }
+    $scope.onHover({value: value});
+  };
+
+  $scope.reset = function() {
+    $scope.value = ngModelCtrl.$viewValue;
+    $scope.onLeave();
+  };
+
+  $scope.onKeydown = function(evt) {
+    if (/(37|38|39|40)/.test(evt.which)) {
+      evt.preventDefault();
+      evt.stopPropagation();
+      $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
+    }
+  };
+
+  this.render = function() {
+    $scope.value = ngModelCtrl.$viewValue;
+  };
+}])
+
+.directive('rating', function() {
+  return {
+    restrict: 'EA',
+    require: ['rating', 'ngModel'],
+    scope: {
+      readonly: '=?',
+      onHover: '&',
+      onLeave: '&'
+    },
+    controller: 'RatingController',
+    templateUrl: 'template/rating/rating.html',
+    replace: true,
+    link: function(scope, element, attrs, ctrls) {
+      var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+      ratingCtrl.init( ngModelCtrl );
+    }
+  };
+});
+
+angular.module('ui.bootstrap.transition', [])
+
+.value('$transitionSuppressDeprecated', false)
+/**
+ * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
+ * @param  {DOMElement} element  The DOMElement that will be animated.
+ * @param  {string|object|function} trigger  The thing that will cause the transition to start:
+ *   - As a string, it represents the css class to be added to the element.
+ *   - As an object, it represents a hash of style attributes to be applied to the element.
+ *   - As a function, it represents a function to be called that will cause the transition to occur.
+ * @return {Promise}  A promise that is resolved when the transition finishes.
+ */
+.factory('$transition', [
+        '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated',
+function($q ,  $timeout ,  $rootScope ,  $log ,  $transitionSuppressDeprecated) {
+
+  if (!$transitionSuppressDeprecated) {
+    $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.');
+  }
+
+  var $transition = function(element, trigger, options) {
+    options = options || {};
+    var deferred = $q.defer();
+    var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
+
+    var transitionEndHandler = function(event) {
+      $rootScope.$apply(function() {
+        element.unbind(endEventName, transitionEndHandler);
+        deferred.resolve(element);
+      });
+    };
+
+    if (endEventName) {
+      element.bind(endEventName, transitionEndHandler);
+    }
+
+    // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
+    $timeout(function() {
+      if ( angular.isString(trigger) ) {
+        element.addClass(trigger);
+      } else if ( angular.isFunction(trigger) ) {
+        trigger(element);
+      } else if ( angular.isObject(trigger) ) {
+        element.css(trigger);
+      }
+      //If browser does not support transitions, instantly resolve
+      if ( !endEventName ) {
+        deferred.resolve(element);
+      }
+    });
+
+    // Add our custom cancel function to the promise that is returned
+    // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
+    // i.e. it will therefore never raise a transitionEnd event for that transition
+    deferred.promise.cancel = function() {
+      if ( endEventName ) {
+        element.unbind(endEventName, transitionEndHandler);
+      }
+      deferred.reject('Transition cancelled');
+    };
+
+    return deferred.promise;
+  };
+
+  // Work out the name of the transitionEnd event
+  var transElement = document.createElement('trans');
+  var transitionEndEventNames = {
+    'WebkitTransition': 'webkitTransitionEnd',
+    'MozTransition': 'transitionend',
+    'OTransition': 'oTransitionEnd',
+    'transition': 'transitionend'
+  };
+  var animationEndEventNames = {
+    'WebkitTransition': 'webkitAnimationEnd',
+    'MozTransition': 'animationend',
+    'OTransition': 'oAnimationEnd',
+    'transition': 'animationend'
+  };
+  function findEndEventName(endEventNames) {
+    for (var name in endEventNames){
+      if (transElement.style[name] !== undefined) {
+        return endEventNames[name];
+      }
+    }
+  }
+  $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
+  $transition.animationEndEventName = findEndEventName(animationEndEventNames);
+  return $transition;
+}]);
+
+angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/accordion/accordion-group.html",
+    "<div class=\"panel {{panelClass || 'panel-default'}}\">\n" +
+    "  <div class=\"panel-heading\" ng-keypress=\"toggleOpen($event)\">\n" +
+    "    <h4 class=\"panel-title\">\n" +
+    "      <a href tabindex=\"0\" class=\"accordion-toggle\" ng-click=\"toggleOpen()\" accordion-transclude=\"heading\"><span ng-class=\"{'text-muted': isDisabled}\">{{heading}}</span></a>\n" +
+    "    </h4>\n" +
+    "  </div>\n" +
+    "  <div class=\"panel-collapse collapse\" collapse=\"!isOpen\">\n" +
+    "	  <div class=\"panel-body\" ng-transclude></div>\n" +
+    "  </div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/accordion/accordion.html",
+    "<div class=\"panel-group\" ng-transclude></div>");
+}]);
+
+angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/alert/alert.html",
+    "<div class=\"alert\" ng-class=\"['alert-' + (type || 'warning'), closeable ? 'alert-dismissible' : null]\" role=\"alert\">\n" +
+    "    <button ng-show=\"closeable\" type=\"button\" class=\"close\" ng-click=\"close($event)\">\n" +
+    "        <span aria-hidden=\"true\">×</span>\n" +
+    "        <span class=\"sr-only\">Close</span>\n" +
+    "    </button>\n" +
+    "    <div ng-transclude></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/carousel/carousel.html",
+    "<div ng-mouseenter=\"pause()\" ng-mouseleave=\"play()\" class=\"carousel\" ng-swipe-right=\"prev()\" ng-swipe-left=\"next()\">\n" +
+    "    <ol class=\"carousel-indicators\" ng-show=\"slides.length > 1\">\n" +
+    "        <li ng-repeat=\"slide in slides | orderBy:indexOfSlide track by $index\" ng-class=\"{active: isActive(slide)}\" ng-click=\"select(slide)\"></li>\n" +
+    "    </ol>\n" +
+    "    <div class=\"carousel-inner\" ng-transclude></div>\n" +
+    "    <a class=\"left carousel-control\" ng-click=\"prev()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-left\"></span></a>\n" +
+    "    <a class=\"right carousel-control\" ng-click=\"next()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-right\"></span></a>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/carousel/slide.html",
+    "<div ng-class=\"{\n" +
+    "    'active': active\n" +
+    "  }\" class=\"item text-center\" ng-transclude></div>\n" +
+    "");
+}]);
+
+angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/datepicker/datepicker.html",
+    "<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" +
+    "  <daypicker ng-switch-when=\"day\" tabindex=\"0\"></daypicker>\n" +
+    "  <monthpicker ng-switch-when=\"month\" tabindex=\"0\"></monthpicker>\n" +
+    "  <yearpicker ng-switch-when=\"year\" tabindex=\"0\"></yearpicker>\n" +
+    "</div>");
+}]);
+
+angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/datepicker/day.html",
+    "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" +
+    "  <thead>\n" +
+    "    <tr>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" +
+    "      <th colspan=\"{{::5 + showWeeks}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" +
+    "    </tr>\n" +
+    "    <tr>\n" +
+    "      <th ng-if=\"showWeeks\" class=\"text-center\"></th>\n" +
+    "      <th ng-repeat=\"label in ::labels track by $index\" class=\"text-center\"><small aria-label=\"{{::label.full}}\">{{::label.abbr}}</small></th>\n" +
+    "    </tr>\n" +
+    "  </thead>\n" +
+    "  <tbody>\n" +
+    "    <tr ng-repeat=\"row in rows track by $index\">\n" +
+    "      <td ng-if=\"showWeeks\" class=\"text-center h6\"><em>{{ weekNumbers[$index] }}</em></td>\n" +
+    "      <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" +
+    "        <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default btn-sm\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-muted': dt.secondary, 'text-info': dt.current}\">{{::dt.label}}</span></button>\n" +
+    "      </td>\n" +
+    "    </tr>\n" +
+    "  </tbody>\n" +
+    "</table>\n" +
+    "");
+}]);
+
+angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/datepicker/month.html",
+    "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" +
+    "  <thead>\n" +
+    "    <tr>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" +
+    "      <th><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" +
+    "    </tr>\n" +
+    "  </thead>\n" +
+    "  <tbody>\n" +
+    "    <tr ng-repeat=\"row in rows track by $index\">\n" +
+    "      <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" +
+    "        <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" +
+    "      </td>\n" +
+    "    </tr>\n" +
+    "  </tbody>\n" +
+    "</table>\n" +
+    "");
+}]);
+
+angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/datepicker/popup.html",
+    "<ul class=\"dropdown-menu\" ng-if=\"isOpen\" style=\"display: block\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" ng-keydown=\"keydown($event)\" ng-click=\"$event.stopPropagation()\">\n" +
+    "	<li ng-transclude></li>\n" +
+    "	<li ng-if=\"showButtonBar\" style=\"padding:10px 9px 2px\">\n" +
+    "		<span class=\"btn-group pull-left\">\n" +
+    "			<button type=\"button\" class=\"btn btn-sm btn-info\" ng-click=\"select('today')\" ng-disabled=\"isDisabled('today')\">{{ getText('current') }}</button>\n" +
+    "			<button type=\"button\" class=\"btn btn-sm btn-danger\" ng-click=\"select(null)\">{{ getText('clear') }}</button>\n" +
+    "		</span>\n" +
+    "		<button type=\"button\" class=\"btn btn-sm btn-success pull-right\" ng-click=\"close()\">{{ getText('close') }}</button>\n" +
+    "	</li>\n" +
+    "</ul>\n" +
+    "");
+}]);
+
+angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/datepicker/year.html",
+    "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" +
+    "  <thead>\n" +
+    "    <tr>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" +
+    "      <th colspan=\"3\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" +
+    "      <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" +
+    "    </tr>\n" +
+    "  </thead>\n" +
+    "  <tbody>\n" +
+    "    <tr ng-repeat=\"row in rows track by $index\">\n" +
+    "      <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\">\n" +
+    "        <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" +
+    "      </td>\n" +
+    "    </tr>\n" +
+    "  </tbody>\n" +
+    "</table>\n" +
+    "");
+}]);
+
+angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/progressbar/bar.html",
+    "<div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" style=\"min-width: 0;\" ng-transclude></div>\n" +
+    "");
+}]);
+
+angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/progressbar/progress.html",
+    "<div class=\"progress\" ng-transclude></div>");
+}]);
+
+angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/progressbar/progressbar.html",
+    "<div class=\"progress\">\n" +
+    "  <div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" style=\"min-width: 0;\" ng-transclude></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/pagination/pager.html",
+    "<ul class=\"pager\">\n" +
+    "  <li ng-class=\"{disabled: noPrevious()||ngDisabled, previous: align}\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" +
+    "  <li ng-class=\"{disabled: noNext()||ngDisabled, next: align}\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" +
+    "</ul>\n" +
+    "");
+}]);
+
+angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/pagination/pagination.html",
+    "<ul class=\"pagination\">\n" +
+    "  <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-first\"><a href ng-click=\"selectPage(1, $event)\">{{::getText('first')}}</a></li>\n" +
+    "  <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-prev\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" +
+    "  <li ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active,disabled: ngDisabled&&!page.active}\" class=\"pagination-page\"><a href ng-click=\"selectPage(page.number, $event)\">{{page.text}}</a></li>\n" +
+    "  <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-next\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" +
+    "  <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-last\"><a href ng-click=\"selectPage(totalPages, $event)\">{{::getText('last')}}</a></li>\n" +
+    "</ul>\n" +
+    "");
+}]);
+
+angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/timepicker/timepicker.html",
+    "<table>\n" +
+    "  <tbody>\n" +
+    "    <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" +
+    "      <td><a ng-click=\"incrementHours()\" ng-class=\"{disabled: noIncrementHours()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" +
+    "      <td> </td>\n" +
+    "      <td><a ng-click=\"incrementMinutes()\" ng-class=\"{disabled: noIncrementMinutes()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" +
+    "      <td ng-show=\"showMeridian\"></td>\n" +
+    "    </tr>\n" +
+    "    <tr>\n" +
+    "      <td class=\"form-group\" ng-class=\"{'has-error': invalidHours}\">\n" +
+    "        <input style=\"width:50px;\" type=\"text\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\">\n" +
+    "      </td>\n" +
+    "      <td>:</td>\n" +
+    "      <td class=\"form-group\" ng-class=\"{'has-error': invalidMinutes}\">\n" +
+    "        <input style=\"width:50px;\" type=\"text\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\">\n" +
+    "      </td>\n" +
+    "      <td ng-show=\"showMeridian\"><button type=\"button\" ng-class=\"{disabled: noToggleMeridian()}\" class=\"btn btn-default text-center\" ng-click=\"toggleMeridian()\">{{meridian}}</button></td>\n" +
+    "    </tr>\n" +
+    "    <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" +
+    "      <td><a ng-click=\"decrementHours()\" ng-class=\"{disabled: noDecrementHours()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" +
+    "      <td> </td>\n" +
+    "      <td><a ng-click=\"decrementMinutes()\" ng-class=\"{disabled: noDecrementMinutes()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" +
+    "      <td ng-show=\"showMeridian\"></td>\n" +
+    "    </tr>\n" +
+    "  </tbody>\n" +
+    "</table>\n" +
+    "");
+}]);
+
+angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/typeahead/typeahead-match.html",
+    "<a href tabindex=\"-1\" ng-bind-html=\"match.label | typeaheadHighlight:query\"></a>\n" +
+    "");
+}]);
+
+angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/typeahead/typeahead-popup.html",
+    "<ul class=\"dropdown-menu\" ng-show=\"isOpen() && !moveInProgress\" ng-style=\"{top: position().top+'px', left: position().left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" +
+    "    <li ng-repeat=\"match in matches track by $index\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{::match.id}}\">\n" +
+    "        <div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" +
+    "    </li>\n" +
+    "</ul>\n" +
+    "");
+}]);
+
+angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tabs/tab.html",
+    "<li ng-class=\"{active: active, disabled: disabled}\">\n" +
+    "  <a href ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" +
+    "</li>\n" +
+    "");
+}]);
+
+angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tabs/tabset.html",
+    "<div>\n" +
+    "  <ul class=\"nav nav-{{type || 'tabs'}}\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" +
+    "  <div class=\"tab-content\">\n" +
+    "    <div class=\"tab-pane\" \n" +
+    "         ng-repeat=\"tab in tabs\" \n" +
+    "         ng-class=\"{active: tab.active}\"\n" +
+    "         tab-content-transclude=\"tab\">\n" +
+    "    </div>\n" +
+    "  </div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/popover/popover-html.html",
+    "<div class=\"popover\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"arrow\"></div>\n" +
+    "\n" +
+    "  <div class=\"popover-inner\">\n" +
+    "      <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" +
+    "      <div class=\"popover-content\" ng-bind-html=\"contentExp()\"></div>\n" +
+    "  </div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/popover/popover-template.html",
+    "<div class=\"popover\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"arrow\"></div>\n" +
+    "\n" +
+    "  <div class=\"popover-inner\">\n" +
+    "      <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" +
+    "      <div class=\"popover-content\"\n" +
+    "        tooltip-template-transclude=\"contentExp()\"\n" +
+    "        tooltip-template-transclude-scope=\"originScope()\"></div>\n" +
+    "  </div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/popover/popover.html",
+    "<div class=\"popover\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"arrow\"></div>\n" +
+    "\n" +
+    "  <div class=\"popover-inner\">\n" +
+    "      <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" +
+    "      <div class=\"popover-content\" ng-bind=\"content\"></div>\n" +
+    "  </div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tooltip/tooltip-html-popup.html",
+    "<div class=\"tooltip\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"tooltip-arrow\"></div>\n" +
+    "  <div class=\"tooltip-inner\" ng-bind-html=\"contentExp()\"></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html",
+    "<div class=\"tooltip\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"tooltip-arrow\"></div>\n" +
+    "  <div class=\"tooltip-inner\" bind-html-unsafe=\"content\"></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tooltip/tooltip-popup.html",
+    "<div class=\"tooltip\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"tooltip-arrow\"></div>\n" +
+    "  <div class=\"tooltip-inner\" ng-bind=\"content\"></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/tooltip/tooltip-template-popup.html",
+    "<div class=\"tooltip\"\n" +
+    "  tooltip-animation-class=\"fade\"\n" +
+    "  tooltip-classes\n" +
+    "  ng-class=\"{ in: isOpen() }\">\n" +
+    "  <div class=\"tooltip-arrow\"></div>\n" +
+    "  <div class=\"tooltip-inner\"\n" +
+    "    tooltip-template-transclude=\"contentExp()\"\n" +
+    "    tooltip-template-transclude-scope=\"originScope()\"></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/modal/backdrop.html",
+    "<div class=\"modal-backdrop\"\n" +
+    "     modal-animation-class=\"fade\"\n" +
+    "     modal-in-class=\"in\"\n" +
+    "     ng-style=\"{'z-index': 1040 + (index && 1 || 0) + index*10}\"\n" +
+    "></div>\n" +
+    "");
+}]);
+
+angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/modal/window.html",
+    "<div modal-render=\"{{$isRendered}}\" tabindex=\"-1\" role=\"dialog\" class=\"modal\"\n" +
+    "    modal-animation-class=\"fade\"\n" +
+    "    modal-in-class=\"in\"\n" +
+    "	ng-style=\"{'z-index': 1050 + index*10, display: 'block'}\" ng-click=\"close($event)\">\n" +
+    "    <div class=\"modal-dialog\" ng-class=\"size ? 'modal-' + size : ''\"><div class=\"modal-content\" modal-transclude></div></div>\n" +
+    "</div>\n" +
+    "");
+}]);
+
+angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) {
+  $templateCache.put("template/rating/rating.html",
+    "<span ng-mouseleave=\"reset()\" ng-keydown=\"onKeydown($event)\" tabindex=\"0\" role=\"slider\" aria-valuemin=\"0\" aria-valuemax=\"{{range.length}}\" aria-valuenow=\"{{value}}\">\n" +
+    "    <span ng-repeat-start=\"r in range track by $index\" class=\"sr-only\">({{ $index < value ? '*' : ' ' }})</span>\n" +
+    "    <i ng-repeat-end ng-mouseenter=\"enter($index + 1)\" ng-click=\"rate($index + 1)\" class=\"glyphicon\" ng-class=\"$index < value && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')\" ng-attr-title=\"{{r.title}}\" ></i>\n" +
+    "</span>\n" +
+    "");
+}]);
+!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.min.js b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.min.js
new file mode 100755
index 0000000..6ac9692
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-bootstrap-custom-tpls-0.13.4.min.js
@@ -0,0 +1,10 @@
+/*
+ * angular-ui-bootstrap
+ * http://angular-ui.github.io/bootstrap/
+
+ * Version: 0.13.4 - 2015-09-03
+ * License: MIT
+ */
+angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.buttons","ui.bootstrap.bindHtml","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.progressbar","ui.bootstrap.pagination","ui.bootstrap.timepicker","ui.bootstrap.typeahead","ui.bootstrap.tabs","ui.bootstrap.popover","ui.bootstrap.tooltip","ui.bootstrap.modal","ui.bootstrap.dropdown","ui.boots [...]
+},replace:!0,link:function(n,a,i,o){function r(e,t,n){return{number:e,text:t,active:n}}function l(e,t){var n=[],a=1,i=t,o=angular.isDefined(u)&&t>u;o&&(p?(a=Math.max(e-Math.floor(u/2),1),i=a+u-1,i>t&&(i=t,a=i-u+1)):(a=(Math.ceil(e/u)-1)*u+1,i=Math.min(a+u-1,t)));for(var l=a;i>=l;l++){var s=r(l,l,l===e);n.push(s)}if(o&&!p){if(a>1){var c=r(a-1,"...",!1);n.unshift(c)}if(t>i){var d=r(i+1,"...",!1);n.push(d)}}return n}var s=o[0],c=o[1];if(c){var u=angular.isDefined(i.maxSize)?n.$parent.$eval( [...]
+switch(e){case 40:d.selectedOption=angular.isNumber(d.selectedOption)?d.selectedOption===t.length-1?d.selectedOption:d.selectedOption+1:0;break;case 38:d.selectedOption=angular.isNumber(d.selectedOption)?0===d.selectedOption?0:d.selectedOption-1:t.length-1}t[d.selectedOption].focus()},m.getDropdownElement=function(){return d.dropdownMenu},m.focusToggleElement=function(){d.toggleElement&&d.toggleElement[0].focus()},m.$watch("isOpen",function(t,n){if(v&&d.dropdownMenu){var a=r.positionElem [...]
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-layout.js b/grails-app/assets/javascripts/vendor/ui-layout.js
new file mode 100644
index 0000000..570a6b5
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-layout.js
@@ -0,0 +1,123 @@
+'use strict';
+angular.module('ui.layout', []).controller('uiLayoutCtrl', [
+  '$scope',
+  '$attrs',
+  '$element',
+  function uiLayoutCtrl($scope, $attrs, $element) {
+    return {
+      opts: angular.extend({}, $scope.$eval($attrs.uiLayout), $scope.$eval($attrs.options)),
+      element: $element
+    };
+  }
+]).directive('uiLayout', [
+  '$parse',
+  function ($parse) {
+    var splitBarElem_htmlTemplate = '<div class="stretch ui-splitbar"></div>';
+    return {
+      restrict: 'AE',
+      compile: function compile(tElement, tAttrs) {
+        var _i, _childens = tElement.children(), _child_len = _childens.length;
+        var opts = angular.extend({}, $parse(tAttrs.uiLayout)(), $parse(tAttrs.options)());
+        var isUsingColumnFlow = opts.flow === 'column';
+        tElement.addClass('stretch').addClass('ui-layout-' + (opts.flow || 'row'));
+        for (_i = 0; _i < _child_len; ++_i) {
+          angular.element(_childens[_i]).addClass('stretch');
+        }
+        if (_child_len > 1) {
+          var flowProperty = isUsingColumnFlow ? 'left' : 'top';
+          var oppositeFlowProperty = isUsingColumnFlow ? 'right' : 'bottom';
+          var step = 100 / _child_len;
+          for (_i = 0; _i < _child_len; ++_i) {
+            var area = angular.element(_childens[_i]).css(flowProperty, step * _i + '%').css(oppositeFlowProperty, 100 - step * (_i + 1) + '%');
+            if (_i < _child_len - 1) {
+              var bar = angular.element(splitBarElem_htmlTemplate).css(flowProperty, step * (_i + 1) + '%');
+              area.after(bar);
+            }
+          }
+        }
+      },
+      controller: 'uiLayoutCtrl'
+    };
+  }
+]).directive('uiSplitbar', function () {
+  var htmlElement = angular.element(document.body.parentElement);
+  return {
+    require: '^uiLayout',
+    restrict: 'EAC',
+    link: function (scope, iElement, iAttrs, parentLayout) {
+      var animationFrameRequested, lastX;
+      var _cache = {};
+      var isUsingColumnFlow = parentLayout.opts.flow === 'column';
+      var mouseProperty = isUsingColumnFlow ? 'clientX' : 'clientY';
+      var flowProperty = isUsingColumnFlow ? 'left' : 'top';
+      var oppositeFlowProperty = isUsingColumnFlow ? 'right' : 'bottom';
+      var sizeProperty = isUsingColumnFlow ? 'width' : 'height';
+      var barElm = iElement[0];
+      function _cached_layout_values() {
+        var layout_bb = parentLayout.element[0].getBoundingClientRect();
+        var bar_bb = barElm.getBoundingClientRect();
+        _cache.time = +new Date();
+        _cache.barSize = bar_bb[sizeProperty];
+        _cache.layoutSize = layout_bb[sizeProperty];
+        _cache.layoutOrigine = layout_bb[flowProperty];
+      }
+      function _draw() {
+        var the_pos = (lastX - _cache.layoutOrigine) / _cache.layoutSize * 100;
+        the_pos = Math.min(the_pos, 100 - _cache.barSize / _cache.layoutSize * 100);
+        the_pos = Math.max(the_pos, parseInt(barElm.previousElementSibling.style[flowProperty], 10));
+        if (barElm.nextElementSibling.nextElementSibling) {
+          the_pos = Math.min(the_pos, parseInt(barElm.nextElementSibling.nextElementSibling.style[flowProperty], 10));
+        }
+        barElm.style[flowProperty] = barElm.nextElementSibling.style[flowProperty] = the_pos + '%';
+        barElm.previousElementSibling.style[oppositeFlowProperty] = 100 - the_pos + '%';
+        animationFrameRequested = null;
+      }
+      function _resize(mouseEvent) {
+        lastX = mouseEvent[mouseProperty] || mouseEvent.originalEvent[mouseProperty];
+        if (animationFrameRequested) {
+          window.cancelAnimationFrame(animationFrameRequested);
+        }
+        if (!_cache.time || +new Date() > _cache.time + 1000) {
+          _cached_layout_values();
+        }
+        animationFrameRequested = window.requestAnimationFrame(_draw);
+      }
+      iElement.on('mousedown touchstart', function (e) {
+        e.preventDefault();
+        e.stopPropagation();
+        htmlElement.on('mousemove touchmove', _resize);
+        return false;
+      });
+      htmlElement.on('mouseup touchend', function () {
+        htmlElement.off('mousemove touchmove');
+      });
+    }
+  };
+});
+var lastTime = 0;
+var vendors = [
+    'ms',
+    'moz',
+    'webkit',
+    'o'
+  ];
+for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+  window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+  window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
+}
+if (!window.requestAnimationFrame) {
+  window.requestAnimationFrame = function (callback) {
+    var currTime = new Date().getTime();
+    var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+    var id = window.setTimeout(function () {
+        callback(currTime + timeToCall);
+      }, timeToCall);
+    lastTime = currTime + timeToCall;
+    return id;
+  };
+}
+if (!window.cancelAnimationFrame) {
+  window.cancelAnimationFrame = function (id) {
+    clearTimeout(id);
+  };
+}
\ No newline at end of file
diff --git a/grails-app/assets/javascripts/vendor/ui-layout.min.js b/grails-app/assets/javascripts/vendor/ui-layout.min.js
new file mode 100644
index 0000000..c6e5df5
--- /dev/null
+++ b/grails-app/assets/javascripts/vendor/ui-layout.min.js
@@ -0,0 +1,7 @@
+/**
+ * angular-ui-layout - This directive allows you to split !
+ * @version v0.0.0 - 2014-01-01
+ * @link https://github.com/angular-ui/ui-layout
+ * @license MIT
+ */
+"use strict";angular.module("ui.layout",[]).controller("uiLayoutCtrl",["$scope","$attrs","$element",function(a,b,c){return{opts:angular.extend({},a.$eval(b.uiLayout),a.$eval(b.options)),element:c}}]).directive("uiLayout",["$parse",function(a){var b='<div class="stretch ui-splitbar"></div>';return{restrict:"AE",compile:function(c,d){var e,f=c.children(),g=f.length,h=angular.extend({},a(d.uiLayout)(),a(d.options)()),i="column"===h.flow;for(c.addClass("stretch").addClass("ui-layout-"+(h.flo [...]
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/annotator.css b/grails-app/assets/stylesheets/annotator.css
new file mode 100644
index 0000000..40ca1a0
--- /dev/null
+++ b/grails-app/assets/stylesheets/annotator.css
@@ -0,0 +1,126 @@
+/** Add css rules here for your application. */
+/**
+*= require_self
+*= require bootstrap
+*/
+
+/** Example rules used by the template application (remove for your app) */
+h1 {
+    font-size: 2em;
+    font-weight: bold;
+    color: #777777;
+    margin: 40px 0px 70px;
+    text-align: center;
+}
+
+.sendButton {
+    display: block;
+    font-size: 16pt;
+}
+
+/** Most GWT widgets already have a style name defined */
+.gwt-DialogBox {
+    width: 400px;
+}
+
+.dialogVPanel {
+    margin: 5px;
+}
+
+.serverResponseLabelError {
+    color: red;
+}
+
+#closeButton {
+    margin: 15px 6px 6px;
+}
+
+.details-header {
+    background-color: #cccccc;
+    width: 100%;
+    border-top: 0;
+}
+
+.details-button {
+    display: inline-table;
+    cursor: pointer;
+}
+
+.details-header-title {
+    font-size: larger;
+    width: 20%;
+    margin-left: 10px;
+    text-align: left;
+    font-weight: 200;
+    display: inline-table;
+}
+
+.table-header1 {
+    font-size: large;
+    font-weight: bolder;
+    background-color: #808080;
+}
+
+.detail-table-header1 {
+    font-weight: bolder;
+}
+
+.detail-table-cell {
+    align-content: center;
+    text-align: center;
+}
+
+.selectedTreeItem {
+    background-color: darkgray;
+    color: #808080;
+}
+
+.red {
+    color: red;
+}
+
+.green {
+    color: green;
+}
+
+.gwt-Tree {
+
+}
+
+.gwt-TreeItem {
+    cursor: hand;
+    cursor: pointer;
+}
+
+.gwt-TreeItem-selected {
+    color: black;
+    font-weight: bolder;
+    background-color: darkkhaki;
+}
+
+.dropdown-menu {
+    width: 80px;
+}
+
+.dropdown-menu .item {
+    padding: 5px;
+    width: 80px;
+}
+
+.dropdown-menu .item-selected {
+    background-color: #eee;
+    width: 80px;
+}
+
+.lookup-display {
+    margin-left: 5px;
+    /*margin-right: 5px;*/
+    /*display: inline-table;*/
+    /*background-color: gray;*/
+    /*color: white;*/
+    /*font-size: smaller;*/
+    /*height: 25px;*/
+    /*width: 100px;*/
+}
+
+
diff --git a/grails-app/assets/stylesheets/application.css b/grails-app/assets/stylesheets/application.css
new file mode 100644
index 0000000..d255134
--- /dev/null
+++ b/grails-app/assets/stylesheets/application.css
@@ -0,0 +1,55 @@
+/*
+* This is a manifest file that'll be compiled into application.css, which will include all the files
+* listed below.
+*
+* Any CSS file within this directory can be referenced here using a relative path.
+*
+* You're free to add application-wide styles to this file and they'll appear at the top of the
+* compiled file, but it's generally better to create a new file per style scope.
+*
+*= require main
+*= require mobile
+*= require bootstrap
+*= require font-awesome.min.css
+*= require_self
+*/
+
+.apollo-header{
+   background-color: #396494;
+   padding-left: 12px;
+}
+
+.apollo-dropdown{
+   background-color: #ddd;
+}
+
+
+.navbar {
+    margin-bottom: 0px;
+    min-height: 25px;
+}
+
+
+.btn {
+    padding: 3px;
+}
+
+.apollo-home{
+    background-image: url("images/ApolloLogo_100x36.png");
+}
+
+.info-border{
+    border-radius: 20px;
+    border: solid;
+    border-color: darkgray;
+    border-width: 2px;
+    margin: 2px;
+    padding-left: 4px;
+    padding-right: 4px;
+    padding-top: 1px;
+    padding-bottom: 1px;
+    /*width: 20%;*/
+    float: left;
+    background-color: #eeeeee;
+}
+
diff --git a/grails-app/assets/stylesheets/errors.css b/grails-app/assets/stylesheets/errors.css
new file mode 100644
index 0000000..bdb58bc
--- /dev/null
+++ b/grails-app/assets/stylesheets/errors.css
@@ -0,0 +1,109 @@
+h1, h2 {
+	margin: 10px 25px 5px;
+}
+
+h2 {
+	font-size: 1.1em;
+}
+
+.filename {
+	font-style: italic;
+}
+
+.exceptionMessage {
+	margin: 10px;
+	border: 1px solid #000;
+	padding: 5px;
+	background-color: #E9E9E9;
+}
+
+.stack,
+.snippet {
+	margin: 0 25px 10px;
+}
+
+.stack,
+.snippet {
+	border: 1px solid #ccc;
+	   -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	-webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	        box-shadow: 0 0 2px rgba(0,0,0,0.2);
+}
+
+/* error details */
+.error-details {
+	border-top: 1px solid #FFAAAA;
+	   -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	-webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	        box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	border-bottom: 1px solid #FFAAAA;
+	   -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	-webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	        box-shadow: 0 0 2px rgba(0,0,0,0.2);
+	background-color:#FFF3F3;
+	line-height: 1.5;
+	overflow: hidden;
+	padding: 5px;
+	padding-left:25px;
+}
+
+.error-details dt {
+	clear: left;
+	float: left;
+	font-weight: bold;
+	margin-right: 5px;
+}
+
+.error-details dt:after {
+	content: ":";
+}
+
+.error-details dd {
+	display: block;
+}
+
+/* stack trace */
+.stack {
+	padding: 5px;
+	overflow: auto;
+	height: 150px;
+}
+
+/* code snippet */
+.snippet {
+	background-color: #fff;
+	font-family: monospace;
+}
+
+.snippet .line {
+	display: block;
+}
+
+.snippet .lineNumber {
+	background-color: #ddd;
+	color: #999;
+	display: inline-block;
+	margin-right: 5px;
+	padding: 0 3px;
+	text-align: right;
+	width: 3em;
+}
+
+.snippet .error {
+	background-color: #fff3f3;
+	font-weight: bold;
+}
+
+.snippet .error .lineNumber {
+	background-color: #faa;
+	color: #333;
+	font-weight: bold;
+}
+
+.snippet .line:first-child .lineNumber {
+	padding-top: 5px;
+}
+
+.snippet .line:last-child .lineNumber {
+	padding-bottom: 5px;
+}
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/login.css b/grails-app/assets/stylesheets/login.css
new file mode 100644
index 0000000..f2ad1bd
--- /dev/null
+++ b/grails-app/assets/stylesheets/login.css
@@ -0,0 +1,22 @@
+ at CHARSET "UTF-8";
+
+.user_login {
+	/* width:30%; */
+	margin:5px;
+}
+
+.fieldname {
+	margin-right:5px;
+	font-size: 1.3em;
+}
+
+.input_field {
+	width: 10em;
+	float: right;
+}
+
+.button_login {
+	text-align:center;
+	margin-top:10px;
+	width: 100%;
+}
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/main.css b/grails-app/assets/stylesheets/main.css
new file mode 100644
index 0000000..a94f8a7
--- /dev/null
+++ b/grails-app/assets/stylesheets/main.css
@@ -0,0 +1,649 @@
+/* FONT STACK */
+body,
+input, select, textarea {
+    font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+h1, h2, h3, h4, h5, h6 {
+    line-height: 1.1;
+}
+
+/* BASE LAYOUT */
+
+html {
+    /*background-color: #ddd;*/
+    /*background-image: -moz-linear-gradient(center top, #aaa, #ddd);*/
+    /*background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #aaa), color-stop(1, #ddd));*/
+    /*background-image: linear-gradient(top, #aaa, #ddd);*/
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#aaaaaa', EndColorStr='#dddddd');
+    background-repeat: no-repeat;
+    height: 100%;
+    /* change the box model to exclude the padding from the calculation of 100% height (IE8+) */
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+html.no-cssgradients {
+    background-color: #aaa;
+}
+
+.ie6 html {
+    height: 100%;
+}
+
+html * {
+    margin: 0;
+}
+
+body {
+    background: #ffffff;
+    color: #333333;
+    margin: 0 auto;
+    /*max-width: 960px;*/
+    overflow-x: hidden; /* prevents box-shadow causing a horizontal scrollbar in firefox when viewport < 960px wide */
+    -moz-box-shadow: 0 0 0.3em #255b17;
+    -webkit-box-shadow: 0 0 0.3em #255b17;
+    /*box-shadow: 0 0 0.3em #255b17;*/
+}
+
+#grailsLogo {
+    background-color: #abbf78;
+}
+
+#apolloLogo {
+    background-color: #d3d3d3;
+    /*background-color: #ffffff;*/
+    width: 100%;
+    display: inline;
+    margin-bottom: 20px;
+    /*box-shadow: none;*/
+}
+
+.small-menu {
+    width: 60%;
+    display: inline;
+}
+
+/* replace with .no-boxshadow body if you have modernizr available */
+.ie6 body,
+.ie7 body,
+.ie8 body {
+    border-color: #255b17;
+    border-style: solid;
+    border-width: 0 1px;
+}
+
+.ie6 body {
+    height: 100%;
+}
+
+a:link, a:visited, a:hover {
+    /*color: #48802c;*/
+    color: #0000ff;
+}
+
+a:link, a:visited, a:hover {
+    /*color: #48802c;*/
+    color: lightblue;
+}
+
+a:link, a:visited, a:hover {
+    /*color: #48802c;*/
+    color: #800080;
+}
+
+a:hover, a:active {
+    outline: none; /* prevents outline in webkit on active links but retains it for tab focus */
+}
+
+h1 {
+    color: #48802c;
+    font-weight: normal;
+    font-size: 1.25em;
+    margin: 0.8em 0 0.3em 0;
+}
+
+ul {
+    padding: 0;
+}
+
+img {
+    border: 0;
+}
+
+/* GENERAL */
+
+#grailsLogo a {
+    display: inline-block;
+    margin: 1em;
+}
+
+.content {
+}
+
+.content h1 {
+    border-bottom: 1px solid #CCCCCC;
+    margin: 0.8em 1em 0.3em;
+    padding: 0 0.25em;
+}
+
+.scaffold-list h1 {
+    border: none;
+}
+
+.footer {
+    background: #d3d3d3;
+    color: #000;
+    clear: both;
+    font-size: 0.8em;
+    margin-top: 1.5em;
+    padding: 1em;
+    min-height: 1em;
+}
+
+.footer a {
+    color: #255b17;
+}
+
+.spinner {
+    background: url(../images/spinner.gif) 50% 50% no-repeat transparent;
+    height: 16px;
+    width: 16px;
+    padding: 0.5em;
+    position: absolute;
+    right: 0;
+    top: 0;
+    text-indent: -9999px;
+}
+
+/* NAVIGATION MENU */
+
+.nav {
+    background-color: #efefef;
+    padding: 0.5em 0.75em;
+    /*-moz-box-shadow: 0 0 3px 1px #aaaaaa;*/
+    /*-webkit-box-shadow: 0 0 3px 1px #aaaaaa;*/
+    /*box-shadow: 0 0 3px 1px #aaaaaa;*/
+    /*zoom: 1;*/
+}
+
+.header1{
+    background-color: #ffffff;
+    /*margin-top: 10px;*/
+}
+
+.menu-item{
+    margin-top: 10px;
+}
+
+.nav ul {
+    overflow: hidden;
+    padding-left: 0;
+    zoom: 1;
+}
+
+.nav li {
+    display: block;
+    float: left;
+    list-style-type: none;
+    margin-right: 0.5em;
+    padding: 0;
+}
+
+.nav a {
+    color: #666666;
+    display: block;
+    padding: 0.25em 0.7em;
+    text-decoration: none;
+    -moz-border-radius: 0.3em;
+    -webkit-border-radius: 0.3em;
+    border-radius: 0.3em;
+}
+
+.nav a:active, .nav a:visited {
+    color: #666666;
+}
+
+.nav a:focus, .nav a:hover {
+    background-color: #999999;
+    color: #ffffff;
+    outline: none;
+    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+}
+
+.no-borderradius .nav a:focus, .no-borderradius .nav a:hover {
+    background-color: transparent;
+    color: #444444;
+    text-decoration: underline;
+}
+
+.nav a.home, .nav a.list, .nav a.create {
+    background-position: 0.7em center;
+    background-repeat: no-repeat;
+    text-indent: 25px;
+}
+
+.nav a.home {
+    background-image: url(../images/skin/house.png);
+}
+
+.nav a.list {
+    background-image: url(../images/skin/database_table.png);
+}
+
+.nav a.create {
+    background-image: url(../images/skin/database_add.png);
+}
+
+/* CREATE/EDIT FORMS AND SHOW PAGES */
+
+fieldset,
+.property-list {
+    margin: 0.6em 1.25em 0 1.25em;
+    padding: 0.3em 1.8em 1.25em;
+    position: relative;
+    zoom: 1;
+    border: none;
+}
+
+.property-list .fieldcontain {
+    list-style: none;
+    overflow: hidden;
+    zoom: 1;
+}
+
+.fieldcontain {
+    margin-top: 1em;
+}
+
+.fieldcontain label,
+.fieldcontain .property-label {
+    color: #666666;
+    text-align: right;
+    width: 25%;
+}
+
+.fieldcontain .property-label {
+    float: left;
+}
+
+.fieldcontain .property-value {
+    display: block;
+    margin-left: 27%;
+}
+
+label {
+    cursor: pointer;
+    display: inline-block;
+    margin: 0 0.25em 0 0;
+}
+
+input, select, textarea {
+    background-color: #fcfcfc;
+    border: 1px solid #cccccc;
+    font-size: 1em;
+    padding: 0.2em 0.4em;
+}
+
+select {
+    padding: 0.2em 0.2em 0.2em 0;
+}
+
+select[multiple] {
+    vertical-align: top;
+}
+
+textarea {
+    width: 250px;
+    height: 150px;
+    overflow: auto; /* IE always renders vertical scrollbar without this */
+    vertical-align: top;
+}
+
+input[type=checkbox], input[type=radio] {
+    background-color: transparent;
+    border: 0;
+    padding: 0;
+}
+
+input:focus, select:focus, textarea:focus {
+    background-color: #ffffff;
+    border: 1px solid #eeeeee;
+    outline: 0;
+    -moz-box-shadow: 0 0 0.5em #ffffff;
+    -webkit-box-shadow: 0 0 0.5em #ffffff;
+    box-shadow: 0 0 0.5em #ffffff;
+}
+
+.required-indicator {
+    color: #48802C;
+    display: inline-block;
+    font-weight: bold;
+    margin-left: 0.3em;
+    position: relative;
+    top: 0.1em;
+}
+
+ul.one-to-many {
+    display: inline-block;
+    list-style-position: inside;
+    vertical-align: top;
+}
+
+.ie6 ul.one-to-many, .ie7 ul.one-to-many {
+    display: inline;
+    zoom: 1;
+}
+
+ul.one-to-many li.add {
+    list-style-type: none;
+}
+
+/* EMBEDDED PROPERTIES */
+
+fieldset.embedded {
+    background-color: transparent;
+    border: 1px solid #CCCCCC;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 0;
+    padding-right: 0;
+    -moz-box-shadow: none;
+    -webkit-box-shadow: none;
+    box-shadow: none;
+}
+
+fieldset.embedded legend {
+    margin: 0 1em;
+}
+
+/* MESSAGES AND ERRORS */
+
+.errors,
+.message {
+    font-size: 0.8em;
+    line-height: 2;
+    margin: 1em 2em;
+    padding: 0.25em;
+}
+
+.message {
+    background: #f3f3ff;
+    border: 1px solid #b2d1ff;
+    color: #006dba;
+    -moz-box-shadow: 0 0 0.25em #b2d1ff;
+    -webkit-box-shadow: 0 0 0.25em #b2d1ff;
+    box-shadow: 0 0 0.25em #b2d1ff;
+}
+
+.errors {
+    background: #fff3f3;
+    border: 1px solid #ffaaaa;
+    color: #cc0000;
+    -moz-box-shadow: 0 0 0.25em #ff8888;
+    -webkit-box-shadow: 0 0 0.25em #ff8888;
+    box-shadow: 0 0 0.25em #ff8888;
+}
+
+.errors ul,
+.message {
+    padding: 0;
+}
+
+.errors li {
+    list-style: none;
+    background: transparent url(../images/skin/exclamation.png) 0.5em 50% no-repeat;
+    text-indent: 2.2em;
+}
+
+.message {
+    background: transparent url(../images/skin/information.png) 0.5em 50% no-repeat;
+    text-indent: 2.2em;
+}
+
+/* form fields with errors */
+
+.error input, .error select, .error textarea {
+    background: #fff3f3;
+    border-color: #ffaaaa;
+    color: #cc0000;
+}
+
+.error input:focus, .error select:focus, .error textarea:focus {
+    -moz-box-shadow: 0 0 0.5em #ffaaaa;
+    -webkit-box-shadow: 0 0 0.5em #ffaaaa;
+    box-shadow: 0 0 0.5em #ffaaaa;
+}
+
+/* same effects for browsers that support HTML5 client-side validation (these have to be specified separately or IE will ignore the entire rule) */
+
+input:invalid, select:invalid, textarea:invalid {
+    background: #fff3f3;
+    border-color: #ffaaaa;
+    color: #cc0000;
+}
+
+input:invalid:focus, select:invalid:focus, textarea:invalid:focus {
+    -moz-box-shadow: 0 0 0.5em #ffaaaa;
+    -webkit-box-shadow: 0 0 0.5em #ffaaaa;
+    box-shadow: 0 0 0.5em #ffaaaa;
+}
+
+/* TABLES */
+
+table {
+    border-top: 1px solid #DFDFDF;
+    border-collapse: collapse;
+    width: 100%;
+    margin-bottom: 1em;
+}
+
+tr {
+    border: 0;
+}
+
+tr > td:first-child, tr > th:first-child {
+    padding-left: 1.25em;
+}
+
+tr > td:last-child, tr > th:last-child {
+    padding-right: 1.25em;
+}
+
+td, th {
+    line-height: 1.5em;
+    padding: 0.5em 0.6em;
+    text-align: left;
+    vertical-align: top;
+}
+
+th {
+    background-color: #efefef;
+    background-image: -moz-linear-gradient(top, #ffffff, #eaeaea);
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ffffff), color-stop(1, #eaeaea));
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea');
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea')";
+    color: #666666;
+    font-weight: bold;
+    line-height: 1.7em;
+    padding: 0.2em 0.6em;
+}
+
+thead th {
+    white-space: nowrap;
+}
+
+th a {
+    display: block;
+    text-decoration: none;
+}
+
+th a:link, th a:visited {
+    color: #666666;
+}
+
+th a:hover, th a:focus {
+    color: #333333;
+}
+
+th.sortable a {
+    background-position: right;
+    background-repeat: no-repeat;
+    padding-right: 1.1em;
+}
+
+th.asc a {
+    background-image: url(../images/skin/sorted_asc.gif);
+    background-color: lightcoral;
+    border-radius: 10px;
+    background-position-x: 5px;
+    text-indent: 15px;
+}
+
+th.desc a {
+    background-image: url(../images/skin/sorted_desc.gif);
+    background-color: lightblue;
+    border-radius: 10px;
+    background-position-x: 5px;
+    text-indent: 15px;
+}
+
+.odd {
+    background: #f7f7f7;
+}
+
+.even {
+    background: #ffffff;
+}
+
+tr:hover {
+    background: lightgoldenrodyellow;
+}
+
+/*th:hover, tr:hover {*/
+/*background: #E1F2B6;*/
+/*background: lightgoldenrodyellow;*/
+/*}*/
+
+/* PAGINATION */
+
+.pagination {
+    border-top: 0;
+    margin: 0;
+    padding: 0.3em 0.2em;
+    text-align: center;
+    -moz-box-shadow: 0 0 3px 1px #AAAAAA;
+    -webkit-box-shadow: 0 0 3px 1px #AAAAAA;
+    box-shadow: 0 0 3px 1px #AAAAAA;
+    background-color: #EFEFEF;
+}
+
+.pagination a,
+.pagination .currentStep {
+    color: #666666;
+    display: inline-block;
+    margin: 0 0.1em;
+    padding: 0.25em 0.7em;
+    text-decoration: none;
+    -moz-border-radius: 0.3em;
+    -webkit-border-radius: 0.3em;
+    border-radius: 0.3em;
+}
+
+.pagination a:hover, .pagination a:focus,
+.pagination .currentStep {
+    background-color: #999999;
+    color: #ffffff;
+    outline: none;
+    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+}
+
+.no-borderradius .pagination a:hover, .no-borderradius .pagination a:focus,
+.no-borderradius .pagination .currentStep {
+    background-color: transparent;
+    color: #444444;
+    text-decoration: underline;
+}
+
+/* ACTION BUTTONS */
+
+.buttons {
+    background-color: #efefef;
+    overflow: hidden;
+    padding: 0.3em;
+    -moz-box-shadow: 0 0 3px 1px #aaaaaa;
+    -webkit-box-shadow: 0 0 3px 1px #aaaaaa;
+    box-shadow: 0 0 3px 1px #aaaaaa;
+    margin: 0.1em 0 0 0;
+    border: none;
+}
+
+.buttons input,
+.buttons a {
+    background-color: transparent;
+    border: 0;
+    color: #666666;
+    cursor: pointer;
+    display: inline-block;
+    margin: 0 0.25em 0;
+    overflow: visible;
+    padding: 0.25em 0.7em;
+    text-decoration: none;
+
+    -moz-border-radius: 0.3em;
+    -webkit-border-radius: 0.3em;
+    border-radius: 0.3em;
+}
+
+.buttons input:hover, .buttons input:focus,
+.buttons a:hover, .buttons a:focus {
+    background-color: #999999;
+    color: #ffffff;
+    outline: none;
+    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+    -moz-box-shadow: none;
+    -webkit-box-shadow: none;
+    box-shadow: none;
+}
+
+.no-borderradius .buttons input:hover, .no-borderradius .buttons input:focus,
+.no-borderradius .buttons a:hover, .no-borderradius .buttons a:focus {
+    background-color: transparent;
+    color: #444444;
+    text-decoration: underline;
+}
+
+.buttons .delete, .buttons .edit, .buttons .save {
+    background-position: 0.7em center;
+    background-repeat: no-repeat;
+    text-indent: 25px;
+}
+
+.ie6 .buttons input.delete, .ie6 .buttons input.edit, .ie6 .buttons input.save,
+.ie7 .buttons input.delete, .ie7 .buttons input.edit, .ie7 .buttons input.save {
+    padding-left: 36px;
+}
+
+.buttons .delete {
+    background-image: url(../images/skin/database_delete.png);
+}
+
+.buttons .edit {
+    background-image: url(../images/skin/database_edit.png);
+}
+
+.buttons .save {
+    background-image: url(../images/skin/database_save.png);
+}
+
+a.skip {
+    position: absolute;
+    left: -9999px;
+}
+
+.report-header{
+    /*margin-left: 10px;*/
+    padding-left: 20px;
+}
+
diff --git a/grails-app/assets/stylesheets/mobile.css b/grails-app/assets/stylesheets/mobile.css
new file mode 100644
index 0000000..167f502
--- /dev/null
+++ b/grails-app/assets/stylesheets/mobile.css
@@ -0,0 +1,82 @@
+/* Styles for mobile devices */
+
+ at media screen and (max-width: 480px) {
+	.nav {
+		padding: 0.5em;
+	}
+
+	.nav li {
+		margin: 0 0.5em 0 0;
+		padding: 0.25em;
+	}
+
+	/* Hide individual steps in pagination, just have next & previous */
+	.pagination .step, .pagination .currentStep {
+		display: none;
+	}
+
+	.pagination .prevLink {
+		float: left;
+	}
+
+	.pagination .nextLink {
+		float: right;
+	}
+
+	/* pagination needs to wrap around floated buttons */
+	.pagination {
+		overflow: hidden;
+	}
+
+	/* slightly smaller margin around content body */
+	fieldset,
+	.property-list {
+		padding: 0.3em 1em 1em;
+	}
+
+	input, textarea {
+		width: 100%;
+		   -moz-box-sizing: border-box;
+		-webkit-box-sizing: border-box;
+		    -ms-box-sizing: border-box;
+		        box-sizing: border-box;
+	}
+
+	select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] {
+		width: auto;
+	}
+
+	/* hide all but the first column of list tables */
+	.scaffold-list td:not(:first-child),
+	.scaffold-list th:not(:first-child) {
+		display: none;
+	}
+
+	.scaffold-list thead th {
+		text-align: center;
+	}
+
+	/* stack form elements */
+	.fieldcontain {
+		margin-top: 0.6em;
+	}
+
+	.fieldcontain label,
+	.fieldcontain .property-label,
+	.fieldcontain .property-value {
+		display: block;
+		float: none;
+		margin: 0 0 0.25em 0;
+		text-align: left;
+		width: auto;
+	}
+
+	.errors ul,
+	.message p {
+		margin: 0.5em;
+	}
+
+	.error ul {
+		margin-left: 0;
+	}
+}
diff --git a/grails-app/assets/stylesheets/oldlook.css b/grails-app/assets/stylesheets/oldlook.css
new file mode 100644
index 0000000..4a0c651
--- /dev/null
+++ b/grails-app/assets/stylesheets/oldlook.css
@@ -0,0 +1,171 @@
+/*
+* This is a manifest file that'll be compiled into application.css, which will include all the files
+* listed below.
+*
+* Any CSS file within this directory can be referenced here using a relative path.
+*
+* You're free to add application-wide styles to this file and they'll appear at the top of the
+* compiled file, but it's generally better to create a new file per style scope.
+*
+*= require_self
+*= require bootstrap
+*/
+
+.same-header {
+    background-color: #396494;
+    height: 31px;
+}
+
+.header-item {
+    background-color: #396494;
+    text-decoration-color: #ffffff;
+}
+
+.header-header {
+    color: #ffffff !important;
+}
+
+/*.navbar-custom {*/
+/*background-color: #396494;*/
+/*text-decoration-color: #ffffff;*/
+/*color: #ffffff;*/
+/*height: 25px;*/
+/*}*/
+
+/*.navbar-custom .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {*/
+    /*height: 31px;*/
+    /*background-color: #396494;*/
+    /*text-decoration-color: #ffffff;*/
+    /*color: #ffffff;*/
+/*}*/
+
+.navbar-custom {
+    background-color: #396494;
+    text-decoration-color: #ffffff;
+    color: #ffffff;
+    height: 25px;
+}
+
+table {
+    border-top: 1px solid #DFDFDF;
+    border-collapse: collapse;
+    width: 100%;
+    margin-bottom: 1em;
+}
+
+tr {
+    border: 0;
+}
+
+tr > td:first-child, tr > th:first-child {
+    padding-left: 1.25em;
+}
+
+tr > td:last-child, tr > th:last-child {
+    padding-right: 1.25em;
+}
+
+td, th {
+    line-height: 1.5em;
+    padding: 0.5em 0.6em;
+    text-align: left;
+    vertical-align: top;
+}
+
+th {
+    background-color: #efefef;
+    background-image: -moz-linear-gradient(top, #ffffff, #eaeaea);
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ffffff), color-stop(1, #eaeaea));
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea');
+    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea')";
+    color: #666666;
+    font-weight: bold;
+    line-height: 1.7em;
+    padding: 0.2em 0.6em;
+}
+
+thead th {
+    white-space: nowrap;
+}
+
+th a {
+    display: block;
+    text-decoration: none;
+}
+
+th a:link, th a:visited {
+    color: #666666;
+}
+
+th a:hover, th a:focus {
+    color: #333333;
+}
+
+th.sortable a {
+    background-position: right;
+    background-repeat: no-repeat;
+    padding-right: 1.1em;
+}
+
+th.asc a {
+    background-image: url(../images/skin/sorted_asc.gif);
+}
+
+th.desc a {
+    background-image: url(../images/skin/sorted_desc.gif);
+}
+
+.odd {
+    background: #f7f7f7;
+}
+
+.even {
+    background: #ffffff;
+}
+
+/*.navbar-nav > li > a{ padding: 0px 15px }*/
+/*.navbar-brand{ padding-top: 10px; }*/
+
+
+
+/*.navbar-fixed-top {*/
+/*min-height: 80px;*/
+/*}*/
+
+/*.navbar-nav > li > a {*/
+/*padding-top: 0px;*/
+/*padding-bottom: 0px;*/
+/*line-height: 80px;*/
+/*}*/
+
+/*@media (max-width: 767px) {*/
+/*.navbar-nav > li > a {*/
+/*line-height: 20px;*/
+/*padding-top: 10px;*/
+/*padding-bottom: 10px;}*/
+/*}*/
+
+/*.tnav .navbar .container { height: 28px; }*/
+
+/*.navbar-primary .navbar { background:#9f58b5; border-bottom:none; }*/
+/*.navbar-primary .navbar .nav > li > a {color: #501762;}*/
+/*.navbar-primary .navbar .nav > li > a:hover {color: #fff; background-color: #8e49a3;}*/
+/*.navbar-primary .navbar .nav .active > a,.navbar .nav .active > a:hover {color: #fff; background-color: #501762;}*/
+/*.navbar-primary .navbar .nav li > a .caret, .tnav .navbar .nav li > a:hover .caret {border-top-color: #fff;border-bottom-color: #fff;}*/
+/*.navbar-primary .navbar .nav > li.dropdown.open.active > a:hover {}*/
+/*.navbar-primary .navbar .nav > li.dropdown.open > a {color: #fff;background-color: #9f58b5;border-color: #fff;}*/
+/*.navbar-primary .navbar .nav > li.dropdown.open.active > a:hover .caret, .tnav .navbar .nav > li.dropdown.open > a .caret {border-top-color: #fff;}*/
+/*.navbar-primary .navbar .navbar-brand {color:#fff;}*/
+/*.navbar-primary .navbar .nav.pull-right {margin-left: 10px; margin-right: 0;}*/
+/*.navbar-xs .navbar-primary .navbar { min-height:28px; height: 28px; }*/
+/*.navbar-xs .navbar-primary .navbar .navbar-brand{ padding: 0px 12px;font-size: 16px;line-height: 28px; }*/
+/*.navbar-xs .navbar-primary .navbar .navbar-nav > li > a {  padding-top: 0px; padding-bottom: 0px; line-height: 28px; }*/
+/*.navbar-sm .navbar-primary .navbar { min-height:40px; height: 40px; }*/
+/*.navbar-sm .navbar-primary .navbar .navbar-brand{ padding: 0px 12px;font-size: 16px;line-height: 40px; }*/
+/*.navbar-sm .navbar-primary .navbar .navbar-nav > li > a {  padding-top: 0px; padding-bottom: 0px; line-height: 40px; }*/
+
+/*.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {*/
+/*color: #000;  *//*Sets the text hover color on navbar*/
+/*height: 31px;*/
+/*}*/
+
diff --git a/grails-app/assets/stylesheets/search_sequence.css b/grails-app/assets/stylesheets/search_sequence.css
new file mode 100644
index 0000000..d57580c
--- /dev/null
+++ b/grails-app/assets/stylesheets/search_sequence.css
@@ -0,0 +1,78 @@
+ at CHARSET "UTF-8";
+
+.search_sequence_label {
+	font-size: 1.5em;
+}
+
+.search_sequence_input {
+	width: 50em;
+	height: 3.5em;
+}
+
+.search_sequence_area {
+	margin-bottom: 10px;
+}
+
+.search_all_ref_seqs_area {
+	margin-top: 5px;
+	margin-bottom: 5px;
+}
+
+.search_all_ref_seqs_checkbox {
+	margin-right: 3px;
+}
+
+.search_all_ref_seqs_label {
+	font-size: 1em;
+}
+
+.search_sequence_matches_row:hover {
+	background-color: #000099;
+	color: white;
+}
+
+.search_sequence_matches_generic_field {
+	width: 6.3em;
+	margin-right: 0.2em;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	font-size: 1.3em;
+	background-color: inherit;
+	display: table-cell;
+}
+
+.search_sequence_matches_header {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches_field {
+	margin-top: 0px;
+	margin-bottom: 0px;
+	float: left;
+	overflow: hidden;
+	width: 6em;
+}
+
+.search_sequence_tools {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches {
+	max-height: 25em;
+	overflow: auto;
+}
+
+.search_sequence_matches_row {
+}
+
+.search_sequence_matches_row-firefox {
+	display: table-row;
+}
+
+.search_sequence_message {
+	font-size: 1.3em;
+}
+
+.search_sequence {
+	font-size: 12px;
+}
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/ui-bootstrap-custom-0.13.4-csp.css b/grails-app/assets/stylesheets/ui-bootstrap-custom-0.13.4-csp.css
new file mode 100755
index 0000000..d772f78
--- /dev/null
+++ b/grails-app/assets/stylesheets/ui-bootstrap-custom-0.13.4-csp.css
@@ -0,0 +1,6 @@
+/* Include this file in your html if you are using the CSP mode. */
+
+.ng-animate.item:not(.left):not(.right) {
+  -webkit-transition: 0s ease-in-out left;
+  transition: 0s ease-in-out left
+}
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/ui-layout.css b/grails-app/assets/stylesheets/ui-layout.css
new file mode 100644
index 0000000..a53727b
--- /dev/null
+++ b/grails-app/assets/stylesheets/ui-layout.css
@@ -0,0 +1,49 @@
+
+/**
+  UI.Layout CSS
+*************************************/
+.stretch {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  /* Can be changed by hand ;)*/
+  overflow: auto;
+}
+
+.ui-splitbar{
+  display: -webkit-box;      /* OLD - iOS 6-, Safari 3.1-6 */
+  display: -moz-box;         /* OLD - Firefox 19- (buggy but mostly works) */
+  display: -ms-flexbox;      /* TWEENER - IE 10 */
+  display: -webkit-flex;     /* NEW - Chrome */
+  display: flex;             /* NEW, Spec - Opera 12.1, Firefox 20+ */
+  -webkit-justify-content: center;
+  justify-content: center;
+
+  background-color: #ffffff;
+  right: auto;
+  position: absolute;
+  z-index: 1010;
+}
+
+.ui-layout-row > .ui-splitbar{
+  height: 8px; width: 100%;
+  cursor: row-resize;
+  text-align: center;
+  justify-content: center;
+  align-items: center;
+}
+.ui-layout-column > .ui-splitbar{
+  width: 8px; height: 100%;
+  cursor: col-resize;
+  -webkit-flex-direction: column;
+  flex-direction: column;
+}
+
+.ui-layout-column > .ui-splitbar > a,
+.ui-layout-row > .ui-splitbar > a {
+  color: #fff;
+  cursor: pointer;
+  font-size: 9px;
+}
\ No newline at end of file
diff --git a/grails-app/assets/stylesheets/web_api_stylesheet.css b/grails-app/assets/stylesheets/web_api_stylesheet.css
new file mode 100644
index 0000000..29dfb4c
--- /dev/null
+++ b/grails-app/assets/stylesheets/web_api_stylesheet.css
@@ -0,0 +1,39 @@
+/*body {*/
+/*/!*font-size: 1em;*!/*/
+/*}*/
+
+h3 {
+    /*font-style: bold;*/
+    /*font-size: 150%;*/
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    /*margin-left: 20px;*/
+}
+
+h4 {
+    /*font-style: bold;*/
+    /*font-size: 110%;*/
+    border-bottom-width: 1px;
+    border-bottom-style: solid;
+    /*margin-top: 25px;*/
+}
+
+.code {
+    border-style: dashed;
+    border-width: 1px;
+    margin-bottom: 15px;
+    padding: 5px;
+    /*background: #ffffcc;*/
+    background: lightgrey;
+    white-space: pre;
+    font-size: smaller;
+}
+
+.section {
+    margin-top: 30px;
+    margin-left: 20px;
+}
+
+ul li{
+    margin-left: 20px;
+}
diff --git a/grails-app/conf/AppNavigation.groovy b/grails-app/conf/AppNavigation.groovy
new file mode 100644
index 0000000..f4ec95b
--- /dev/null
+++ b/grails-app/conf/AppNavigation.groovy
@@ -0,0 +1,26 @@
+navigation = {
+    // Declare the "app" scope, used by default in tags
+    app {
+
+        // A nav item pointing to HomeController, using the default action
+        //home()
+        organism(controller:'organism',action: 'list',titleText:'Organisms')
+        sequence(controller:  'sequence',action: 'index',titleText:'Sequences')
+        annotator(controller:'annotator',action: 'index',titleText:'Annotate')
+        user(controller:'user',action: 'permissions',titleText:'User Permissions')
+
+    }
+
+    // Some back-end admin scaffolding stuff in a separate scope
+    //admin {
+        //// Use "list" action as default item, even if its not default action
+        //// and create automatic sub-items for the other actions
+        //books(controller:'bookAdmin', action:'list, create, search')
+        //
+        //// User admin, with default screen using "search" action
+        //users(controller:'userAdmin', action:'search') {
+        //    // Declare action alias so "create" is active for both "create" and "update" actions
+        //    create(action:'create', actionAliases:'update')
+        //}
+    //}
+}
diff --git a/grails-app/conf/BootStrap.groovy b/grails-app/conf/BootStrap.groovy
new file mode 100644
index 0000000..92b7efa
--- /dev/null
+++ b/grails-app/conf/BootStrap.groovy
@@ -0,0 +1,60 @@
+import org.bbop.apollo.FeatureType
+import org.bbop.apollo.Role
+import org.bbop.apollo.UserService
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+
+
+
+class BootStrap {
+
+    def sequenceService
+    def configWrapperService
+    def grailsApplication
+    def featureTypeService
+    def domainMarshallerService
+    def proxyService
+    def userService
+    def phoneHomeService
+
+
+    def init = { servletContext ->
+        log.info "Initializing..."
+        def dataSource = grailsApplication.config.dataSource
+        log.info "Datasource"
+        log.info "Url: ${dataSource.url}"
+        log.info "Driver: ${dataSource.driverClassName}"
+        log.info "Dialect: ${dataSource.dialect}"
+
+        domainMarshallerService.registerObjects()
+        proxyService.initProxies()
+
+
+        SequenceTranslationHandler.spliceDonorSites.addAll(configWrapperService.spliceDonorSites)
+        SequenceTranslationHandler.spliceAcceptorSites.addAll(configWrapperService.spliceAcceptorSites)
+
+        if(FeatureType.count==0){
+            featureTypeService.stubDefaultFeatureTypes()
+        }
+
+        if(Role.count==0){
+            def userRole = new Role(name: UserService.USER).save()
+            userRole.addToPermissions("*:*")
+            userRole.removeFromPermissions("cannedComments:*")
+            userRole.removeFromPermissions("availableStatus:*")
+            userRole.removeFromPermissions("featureType:*")
+            def adminRole = new Role(name: UserService.ADMIN).save()
+            adminRole.addToPermissions("*:*")
+        }
+
+        def admin = grailsApplication.config?.apollo?.admin
+        if(admin){
+            userService.registerAdmin(admin.username,admin.password,admin.firstName,admin.lastName)
+        }
+
+        phoneHomeService.pingServerAsync(org.bbop.apollo.PhoneHomeEnum.START.value)
+
+    }
+    def destroy = {
+        phoneHomeService.pingServer(org.bbop.apollo.PhoneHomeEnum.STOP.value)
+    }
+}
diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy
new file mode 100644
index 0000000..5942e7d
--- /dev/null
+++ b/grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,191 @@
+grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0)
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+grails.project.work.dir = "target/work"
+grails.project.target.level = 1.8
+grails.project.source.level = 1.8
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+def gebVersion = '1.0'
+def seleniumVersion = "2.51.0"
+
+
+//forkConfig = [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+grails.project.fork = [
+        // configure settings for compilation JVM, note that if you alter the Groovy version forked compilation is required
+        //  compile: [maxMemory: 256, minMemory: 64, debug: false, maxPerm: 256, daemon:true],
+
+        test   : false,
+        //run    : false,
+        // configure settings for the test-app JVM, uses the daemon by default
+        //test: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, daemon:true],
+        // configure settings for the run-app JVM
+        run    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+        // configure settings for the run-war JVM
+        war    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+        // configure settings for the Console UI JVM
+        console: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+]
+
+if (System.getProperty("grails.debug")) {
+    //grails.project.fork.war += [debug: true]
+    grails.project.fork.run = false
+    println "Using debug for run"
+}
+
+
+
+grails.project.dependency.resolver = "maven" // or ivy
+grails.project.dependency.resolution = {
+    // inherit Grails' default dependencies
+    inherits("global") {
+        // specify dependency exclusions here; for example, uncomment this to disable ehcache:
+        // excludes 'ehcache'
+    }
+
+    log "error" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+    checksums true // whether to verify checksums on resolve
+    legacyResolve false
+    // whether to do a secondary resolve on plugin installation, not advised and here for backwards compatibility
+
+    repositories {
+
+        grailsPlugins()
+        grailsHome()
+        mavenLocal()
+        grailsCentral()
+        mavenCentral()
+
+        // uncomment these (or add new ones) to enable remote dependency resolution from public Maven repositories
+        //mavenRepo "http://repository.codehaus.org"
+        //mavenRepo "http://download.java.net/maven/2/"
+        //mavenRepo "http://repository.jboss.com/maven2/"
+        //mavenRepo "http://maven.crbs.ucsd.edu/nexus/content/repositories/NIF-snapshot/"
+        //mavenRepo "http://www.biojava.org/download/maven/"
+    }
+
+    dependencies {
+        // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes e.g.
+        runtime 'mysql:mysql-connector-java:5.1.29'
+        runtime 'org.postgresql:postgresql:9.4.1212'
+        compile 'commons-codec:commons-codec:1.2'
+        compile 'commons-collections:commons-collections:3.2.1'
+
+        // HTSJDK
+        compile group: 'com.github.samtools', name: 'htsjdk', version: '2.10.0'
+
+        // svg generation
+        compile group: 'org.apache.xmlgraphics', name: 'batik-svg-dom', version: '1.9'
+        compile group: 'org.apache.xmlgraphics', name: 'batik-svggen', version: '1.7'
+        compile group: 'org.apache.commons', name: 'commons-compress', version: '1.14'
+
+        compile 'org.json:json:20140107'
+        compile 'org.hibernate:hibernate-tools:3.2.0.ga'
+        //compile 'asm:asm:3.1'
+        //compile  'edu.sdsc:scigraph-core:1.1-SNAPSHOT'
+        //compile 'org.biojava:biojava3-core:3.1.0'
+
+        test "org.grails:grails-datastore-test-support:1.0.2-grails-2.4"
+        runtime 'org.grails:grails-datastore-gorm:3.1.5.RELEASE'
+
+//        test "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
+//        test "org.seleniumhq.selenium:selenium-firefox-driver:$seleniumVersion"
+//        test "org.seleniumhq.selenium:selenium-htmlunit-driver:$seleniumVersion"
+//        test "org.seleniumhq.selenium:selenium-support:$seleniumVersion"
+//
+////        test "org.gebish:geb-spock:$gebVersion"
+//        test "org.gebish:geb-spock:$gebVersion"
+        //test "org.spockframework:spock-grails-support:0.7-groovy-2.0"
+
+        // for coveralls
+        build 'org.apache.httpcomponents:httpcore:4.3.2'
+        build 'org.apache.httpcomponents:httpclient:4.3.2'
+        build 'org.apache.httpcomponents:httpmime:4.3.3'
+
+//        compile "org.grails:quartz:1.0.2"
+
+    }
+
+    plugins {
+        // plugins for the build system only
+//        build ':tomcat:7.0.55.2'
+          build ':tomcat:8.0.33'
+//        build ':tomcat:9.0.0.M4.1'
+
+        // plugins for the compile step
+        compile ":rest-api-doc:0.6"
+        compile ":scaffolding:2.1.2"
+        compile ':cache:1.1.8'
+        compile ':cache-ehcache:1.0.5'
+
+        compile ':asset-pipeline:2.1.5'
+        compile ":spring-websocket:1.3.1"
+        compile(":shiro:1.2.1") {
+            excludes([name: 'quartz', group: 'org.opensymphony.quartz'])
+        }
+        compile ":audit-logging:1.0.3"
+
+        // Uncomment these to enable additional asset-pipeline capabilities
+        //compile ":sass-asset-pipeline:1.9.0"
+        //compile ":less-asset-pipeline:1.10.0"
+        //compile ":coffee-asset-pipeline:1.8.0"
+        //compile ":handlebars-asset-pipeline:1.3.0.3"
+
+        // plugins needed at runtime but not for compilation
+        runtime ':hibernate4:4.3.8.1' // or ':hibernate:3.6.10.19'
+        runtime ":database-migration:1.4.1"
+        runtime ":jquery-ui:1.10.4"
+        runtime ":jquery:1.11.1"
+
+        // https://github.com/groovydev/twitter-bootstrap-grails-plugin/blob/master/README.md
+        runtime ':twitter-bootstrap:3.3.5'
+        //compile ":angularjs:1.0.0"
+        //compile ":dojo:1.7.2.0"
+        //compile ":platform-core:1.0.0"
+
+        //runtime ":resources:1.2.13"
+        //build ":extended-dependency-manager:0.5.5"
+
+        //compile ":gwt:1.0" , {
+        //    transitive=true
+        //}
+        compile ":yammer-metrics:3.0.1-2"
+        compile "org.grails.plugins:quartz2:2.1.6.2"
+
+
+        //compile ":joda-time:1.4"
+        // TODO: re-add when ready to install functional tests
+//        test    ":geb:$gebVersion"
+//        test "org.grails.plugins:geb:$gebVersion"
+//        test 'com.github.detro:phantomjsdriver:1.2.0'
+
+
+//        grails.plugin.location.'chado-grails' = "../chado-grails"
+//        grails.plugin.location.'test-plugin' = "../test-plugin"
+//        runtime ":chado:0.1"
+//        compile ":test-plugin:0.1"
+//        compile ":chado-plugin:0.1"
+
+        // remember to sync rest
+        runtime ":rest-client-builder:2.1.1"
+        // for coveralls: https://github.com/agorapulse/grails-coveralls
+        build(':coveralls:0.1.3', ':rest-client-builder:2.1.1') {
+            export = false
+        }
+        test(':code-coverage:2.0.3-3') {
+            export = false
+        }
+    }
+}
+
+//gwt.compile.args = {
+//    arg(value: '-strict')
+//    arg(value: '-XjsInteropMode')
+//    arg(value: 'JS')
+//}
+//
+//gwt {
+//    version="2.7.0"
+//    gin.version = '2.1.2'
+//}
+
diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy
new file mode 100644
index 0000000..0fd87bb
--- /dev/null
+++ b/grails-app/conf/Config.groovy
@@ -0,0 +1,435 @@
+// locations to search for config files that get merged into the main config;
+// config files can be ConfigSlurper scripts, Java properties files, or classes
+// in the classpath in ConfigSlurper format
+
+// if (System.properties["${appName}.config.location"]) {
+//    grails.config.locations << "file:" + System.properties["${appName}.config.location"]
+// }
+
+extraSrcDirs = "$basedir/src/gwt/org.bbop.apollo.gwt.shared"
+eventCompileStart = {
+    projectCompiler.srcDirectories << extraSrcDirs
+}
+
+grails.config.locations = [
+        "file:./${appName}-config.groovy"        // dev only
+        , "classpath:${appName}-config.groovy"    // for production deployment
+        , "classpath:${appName}-config.properties"
+]
+
+// if (System.properties["${appName}.config.location"]) {
+//    grails.config.locations << "file:" + System.properties["${appName}.config.location"]
+// }
+
+
+grails.project.groupId = appName // change this to alter the default package name and Maven publishing destination
+
+// The ACCEPT header will not be used for content negotiation for user agents containing the following strings (defaults to the 4 major rendering engines)
+grails.mime.disable.accept.header.userAgents = ['Gecko', 'WebKit', 'Presto', 'Trident']
+grails.mime.types = [ // the first one is the default format
+                      all          : '*/*', // 'all' maps to '*' or the first available format in withFormat
+                      atom         : 'application/atom+xml',
+                      css          : 'text/css',
+                      csv          : 'text/csv',
+                      form         : 'application/x-www-form-urlencoded',
+                      html         : ['text/html', 'application/xhtml+xml'],
+                      js           : 'text/javascript',
+                      json         : ['application/json', 'text/json'],
+                      multipartForm: 'multipart/form-data',
+                      rss          : 'application/rss+xml',
+                      text         : 'text/plain',
+                      hal          : ['application/hal+json', 'application/hal+xml'],
+                      xml          : ['text/xml', 'application/xml']
+]
+
+// URL Mapping Cache Max Size, defaults to 5000
+//grails.urlmapping.cache.maxsize = 1000
+
+// Legacy setting for codec used to encode data with ${}
+grails.views.default.codec = "html"
+
+// The default scope for controllers. May be prototype, session or singleton.
+// If unspecified, controllers are prototype scoped.
+grails.controllers.defaultScope = 'singleton'
+
+// GSP settings
+grails {
+    views {
+        gsp {
+            encoding = 'UTF-8'
+            htmlcodec = 'xml' // use xml escaping instead of HTML4 escaping
+            codecs {
+                expression = 'html' // escapes values inside ${}
+                scriptlet = 'html' // escapes output from scriptlets in GSPs
+                taglib = 'none' // escapes output from taglibs
+                staticparts = 'none' // escapes output from static template parts
+            }
+        }
+        // escapes all not-encoded output at final stage of outputting
+        // filteringCodecForContentType.'text/html' = 'html'
+    }
+}
+
+
+grails.converters.encoding = "UTF-8"
+// scaffolding templates configuration
+grails.scaffolding.templates.domainSuffix = 'Instance'
+
+// Set to false to use the new Grails 1.2 JSONBuilder in the render method
+grails.json.legacy.builder = false
+// enabled native2ascii conversion of i18n properties files
+grails.enable.native2ascii = false
+// packages to include in Spring bean scanning
+grails.spring.bean.packages = []
+// whether to disable processing of multi part requests
+grails.web.disable.multipart = false
+
+// request parameters to mask when logging exceptions
+grails.exceptionresolver.params.exclude = ['password']
+
+// configure auto-caching of queries by default (if false you can cache individual queries with 'cache: true')
+grails.hibernate.cache.queries = false
+
+// configure passing transaction's read-only attribute to Hibernate session, queries and criterias
+// set "singleSession = false" OSIV mode in hibernate configuration after enabling
+grails.hibernate.pass.readonly = false
+// configure passing read-only to OSIV session by default, requires "singleSession = false" OSIV mode
+grails.hibernate.osiv.readonly = false
+
+grails.cache.config = {
+    // avoid ehcache naming conflict to run multiple WA instances
+    provider {
+        name "ehcache-apollo-" + (new Date().format("yyyyMMddHHmmss"))
+    }
+    cache {
+        enabled = true
+        name 'globalcache'
+        eternal false
+        overflowToDisk true
+        maxElementsInMemory 10000
+        maxElementsOnDisk 10000000
+    }
+    defaultCache {
+        maxElementsInMemory 10000
+        eternal false
+        timeToIdleSeconds 120
+        timeToLiveSeconds 120
+        overflowToDisk true
+        maxElementsOnDisk 10000000
+        diskPersistent false
+        diskExpiryThreadIntervalSeconds 120
+        memoryStoreEvictionPolicy 'LRU'
+    }
+}
+
+
+
+environments {
+    development {
+        grails.logging.jul.usebridge = true
+        grails.assets.minifyJs = false
+        grails.assets.minifyCss = false
+        grails.assets.enableSourceMaps = true
+        grails.assets.bundle = false
+    }
+    test {
+        grails.assets.minifyJs = false
+        grails.assets.minifyCss = false
+        grails.assets.enableSourceMaps = true
+        grails.assets.bundle = false
+    }
+    production {
+        grails.logging.jul.usebridge = false
+        grails.assets.minifyJs = false
+        grails.assets.minifyCss = false
+        grails.assets.enableSourceMaps = true
+        grails.assets.bundle = false
+    }
+}
+
+// log4j configuration
+log4j.main = {
+    // log errors from dependencies
+    error 'org.codehaus.groovy.grails.web.servlet',        // controllers
+            'org.codehaus.groovy.grails.web.pages',          // GSP
+            'org.codehaus.groovy.grails.web.sitemesh',       // layouts
+            'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+            'org.codehaus.groovy.grails.web.mapping',        // URL mapping
+            'org.codehaus.groovy.grails.commons',            // core / classloading
+            'org.codehaus.groovy.grails.plugins',            // plugins
+            'org.codehaus.groovy.grails.orm.hibernate',      // hibernate integration
+            'org.springframework',
+            'org.hibernate',
+            'net.sf.ehcache.hibernate'
+
+    // enable logging of our webapollo instance (uncomment debug for extensive output)
+    warn 'grails.app'
+//    debug 'grails.app'
+//    debug 'liquibase'
+//    debug 'org.bbop.apollo'
+
+    // more find grained logging
+    //trace 'org.hibernate.type'
+    //debug 'org.hibernate.SQL'
+    //debug 'grails.app'
+    //debug 'grails.app.controllers.org.bbop.apollo'
+    //debug 'grails.app.controllers.org.bbop.apollo.JbrowseController'
+    //debug 'grails.app.services.org.bbop.apollo.FeatureService'
+    info 'grails.app.controllers.org.bbop.apollo.GroupController'
+    info 'grails.app.controllers.org.bbop.apollo.UserController'
+    //info 'grails.app.services'
+    //debug 'grails.app.jobs'
+    //debug 'grails.app.taglib'
+    //debug 'grails.app.controllers'
+    //debug 'grails.app.services'
+}
+
+//grails.gorm.default.constraints = {
+//    '*'(nullable: true)
+//}
+//grails.datastore.gorm.GormInstanceApi.copy = cloneForDomains ;
+grails.gorm.failOnError = true
+grails.tomcat.nio = true
+grails.tomcat.scan.enabled = true
+
+// default apollo settings
+apollo {
+    default_minimum_intron_size = 1
+    history_size = 0
+    overlapper_class = "org.bbop.apollo.sequence.OrfOverlapper"
+    track_name_comparator = "/config/track_name_comparator.js"
+    use_cds_for_new_transcripts = false
+    feature_has_dbxrefs = true
+    feature_has_attributes = true
+    feature_has_pubmed_ids = true
+    feature_has_go_ids = true
+    feature_has_comments = true
+    feature_has_status = true
+    export_subfeatures_attr = false
+    user_pure_memory_store = true
+    is_partial_translation_allowed = false // unused so far
+    export_subfeature_attrs = false
+
+    // used for uploading
+    common_data_directory = "/opt/apollo"
+
+    // settings for Chado export
+    // set chado_export_fasta_for_sequence if you want the reference sequence FASTA to be exported into the database
+    // Note: Enabling this feature can be memory intensive
+    chado_export_fasta_for_sequence = false
+    // set chado_export_fasta_for_cds if you want the CDS FASTA to be exported into the database
+    chado_export_fasta_for_cds = false
+    only_owners_delete = false
+
+    // this is the default
+    // other translation codes are of the form ncbi_KEY_translation_table.txt
+    // under the web-app/translation_tables  directory
+    // to add your own add them to that directory and over-ride the translation code here
+    get_translation_code = 1
+    proxies = [
+            [
+                    referenceUrl : 'http://golr.geneontology.org/select',
+                    targetUrl    : 'http://golr.geneontology.org/select',
+                    active       : true,
+                    fallbackOrder: 0,
+                    replace      : false
+            ]
+            ,
+            [
+                    referenceUrl : 'http://golr.geneontology.org/select',
+                    targetUrl    : 'http://golr.berkeleybop.org/select',
+                    active       : false,
+                    fallbackOrder: 1,
+                    replace      : false
+            ]
+            ,
+            [
+                    referenceUrl : 'http://golr.geneontology.org/select',
+                    targetUrl    : 'http://golr.berkeleybop.org/solr/select',
+                    active       : false,
+                    fallbackOrder: 2,
+                    replace      : false
+            ]
+    ]
+    sequence_search_tools = [
+            blat_nuc : [
+                    search_exe  : "/usr/local/bin/blat",
+                    search_class: "org.bbop.apollo.sequence.search.blat.BlatCommandLineNucleotideToNucleotide",
+                    name        : "Blat nucleotide",
+                    params      : ""
+            ],
+            blat_prot: [
+                    search_exe  : "/usr/local/bin/blat",
+                    search_class: "org.bbop.apollo.sequence.search.blat.BlatCommandLineProteinToNucleotide",
+                    name        : "Blat protein",
+                    params      : ""
+                    //tmp_dir: "/opt/apollo/tmp" optional param
+            ]
+    ]
+    data_adapters = [[
+                             permission   : 1,
+                             key          : "GFF3",
+                             data_adapters: [[
+                                                     permission: 1,
+                                                     key       : "Only GFF3",
+                                                     options   : "output=file&format=gzip&type=GFF3&exportGff3Fasta=false"
+                                             ],
+                                             [
+                                                     permission: 1,
+                                                     key       : "GFF3 with FASTA",
+                                                     options   : "output=file&format=gzip&type=GFF3&exportGff3Fasta=true"
+                                             ]]
+                     ],
+                     [
+                             permission   : 1,
+                             key          : "FASTA",
+                             data_adapters: [[
+                                                     permission: 1,
+                                                     key       : "peptide",
+                                                     options   : "output=file&format=gzip&type=FASTA&seqType=peptide"
+                                             ],
+                                             [
+                                                     permission: 1,
+                                                     key       : "cDNA",
+                                                     options   : "output=file&format=gzip&type=FASTA&seqType=cdna"
+                                             ]
+                                             ,
+                                             [
+                                                     permission: 1,
+                                                     key       : "CDS",
+                                                     options   : "output=file&format=gzip&type=FASTA&seqType=cds"
+                                             ]
+                                             ,
+                                             [
+                                                     permission: 1,
+                                                     key       : "highlighted region",
+                                                     options   : "output=file&format=gzip&type=FASTA&seqType=genomic"
+                                             ]
+                             ]
+
+                     ]]
+
+    // TODO: should come from config or via preferences database
+    splice_donor_sites = ["GT"]
+    splice_acceptor_sites = ["AG"]
+    gff3.source = "."
+    bootstrap = false
+
+    info_editor = {
+        feature_types = "default"
+        attributes = true
+        dbxrefs = true
+        pubmed_ids = true
+        go_ids = true
+        comments = true
+    }
+
+    // customize admin tab on annotator panel with these links
+    administrativePanel = [
+            ['label': "Canned Comments", 'link': "/cannedComment/"]
+            , ['label': "Canned Key", 'link': "/cannedKey/"]
+            , ['label': "Canned Values", 'link': "/cannedValue/"]
+            , ['label': "Feature Types", 'link': "/featureType/"]
+            , ['label': "Statuses", 'link': "/availableStatus/"]
+            , ['label': "Proxies", 'link': "/proxy/"]
+            , ['label': "Report::Organisms", 'link': "/organism/report/", 'type': "report"]
+            , ['label': "Report::Sequences", 'link': "/sequence/report/", 'type': "report"]
+            , ['label': "Report::Annotator", 'link': "/annotator/report/", 'type': "report"]
+            , ['label': "Report::Changes", 'link': "/featureEvent/report/", 'type': "report"]
+            , ['label': "System Info", 'link': "/home/systemInfo/", 'type': "report"]
+            , ['label': "Performance Metrics", 'link': "/home/metrics/", 'type': "report"]
+            , ['label': "WebServices", 'link': "/WebServices/", 'type': "report"]
+    ]
+
+    // over-ride in apollo-config.groovy to add extra tabs
+    extraTabs = [
+//            ['title': 'extra1', 'url': 'http://localhost:8080/apollo/annotator/report/'],
+//            ['title': 'extra2', 'content': '<b>Some content</b><a href="http://google.com" target="_blank">Google</a>']
+    ]
+
+
+    authentications = [
+            ["name"     : "Username Password Authenticator",
+             "className": "usernamePasswordAuthenticatorService",
+             "active"   : true,
+            ]
+            ,
+            ["name"     : "Remote User Authenticator",
+             "className": "remoteUserAuthenticatorService",
+             "active"   : false,
+            ]
+    ]
+
+    // comment out if you don't want this to be reported
+    google_analytics = ["UA-62921593-1"]
+
+    phone {
+        phoneHome = true
+        url = "https://s3.amazonaws.com/"
+        bucketPrefix = "apollo-usage-"
+        fileName = "ping.json"
+    }
+}
+
+
+grails.plugin.databasemigration.updateOnStart = true
+grails.plugin.databasemigration.updateOnStartFileNames = ['changelog.groovy']
+
+// from: http://grails.org/plugin/audit-logging
+auditLog {
+    //note, this disables the audit log
+    disabled = true
+    //verbose = true // verbosely log all changed values to db
+    logIds = true  // log db-ids of associated objects.
+
+}
+
+// Default JBrowse configuration
+jbrowse {
+    git {
+        url = "https://github.com/gmod/jbrowse"
+//        branch = "master"
+        tag = "d3827c4e81054ef65933ca792d228f786d849ba4"
+        alwaysPull = false
+        alwaysRecheck = false
+    }
+//    url {
+//        // always use dev for apollo
+//        url = "http://jbrowse.org/wordpress/wp-content/plugins/download-monitor/download.php?id=102"
+//        type ="zip"
+//        fileName = "JBrowse-1.12.0-dev"
+//    }
+//
+//	// Warning: We are still testing the performance of the NeatFeatures plugins in combination with Apollo.
+//	// We advise caution if enabling these plugins with Apollo until this process is finalized.
+    plugins {
+        WebApollo {
+            included = true
+        }
+//        NeatHTMLFeatures{
+//            included = true
+//            linearGradient = 0
+//        }
+//        NeatCanvasFeatures{
+//            included = true
+//        }
+        RegexSequenceSearch {
+            included = true
+        }
+        HideTrackLabels {
+            included = true
+        }
+//        MyVariantInfo {
+//            git = 'https://github.com/GMOD/myvariantviewer'
+//            branch = 'master'
+//            alwaysRecheck = "true"
+//            alwaysPull = "true"
+//        }
+//        SashimiPlot {
+//            git = 'https://github.com/cmdcolin/sashimiplot'
+//            branch = 'master'
+//            alwaysPull = "true"
+//        }
+    }
+
+}
diff --git a/grails-app/conf/DataSource.groovy b/grails-app/conf/DataSource.groovy
new file mode 100644
index 0000000..cdd1e60
--- /dev/null
+++ b/grails-app/conf/DataSource.groovy
@@ -0,0 +1,107 @@
+dataSource {
+    pooled = true
+    jmxExport = true
+    driverClassName = "org.h2.Driver"
+    username = "sa"
+    password = ""
+}
+//hibernate {
+//    cache.use_second_level_cache=true
+//    cache.use_query_cache=true
+//    cache.provider_class='org.hibernate.cache.EhCacheProvider'
+//    cache.region.factory_class = 'grails.plugin.cache.ehcache.hibernate.BeanEhcacheRegionFactory4' // For Hibernate 4.0 and higher
+//
+//}
+
+hibernate {
+    cache.use_second_level_cache = true
+    cache.use_query_cache = true
+//    cache.region.factory_class = 'org.hibernate.cache.SingletonEhCacheRegionFactory' // Hibernate 3
+    cache.region.factory_class = 'org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory' // Hibernate 4
+    singleSession = true // configure OSIV singleSession mode
+    flush.mode = 'manual' // OSIV session flush mode outside of transactional context
+}
+
+// environment specific settings
+environments {
+    development {
+        dataSource {
+            //dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+        }
+//        dataSource_chado{
+//            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+//            url = "jdbc:h2:mem:chadoDevDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+//        }
+    }
+    test {
+        dataSource {
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            dialect = "org.bbop.apollo.ImprovedH2Dialect"
+            url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+        }
+//        dataSource_chado {
+//            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+//            dialect = "org.bbop.apollo.ImprovedH2Dialect"
+//            url = "jdbc:h2:mem:chadoTestDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+//        }
+    }
+    production {
+        dataSource {
+            dbCreate = "update"
+            // NOTE: Not to be used for production.  Please see:  http://genomearchitect.readthedocs.io/en/latest/Configure/
+            // you should copy over sample-XXX-config.groovy
+//            url = "jdbc:h2:/tmp/prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+            url = ""
+            properties {
+               // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+               jmxEnabled = true
+               initialSize = 5
+               maxActive = 50
+               minIdle = 5
+               maxIdle = 25
+               maxWait = 10000
+               maxAge = 10 * 60000
+               timeBetweenEvictionRunsMillis = 5000
+               minEvictableIdleTimeMillis = 60000
+               validationQuery = "SELECT 1"
+               validationQueryTimeout = 3
+               validationInterval = 15000
+               testOnBorrow = true
+               testWhileIdle = true
+               testOnReturn = false
+               jdbcInterceptors = "ConnectionState"
+               defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+            }
+        }
+//        dataSource_chado {
+//            dbCreate = "update"
+//            // NOTE: Not to be used for production.  Please see:  http://genomearchitect.readthedocs.io/en/latest/Configure/
+//            // you should copy over sample-XXX-config.groovy
+////            url = "jdbc:h2:/tmp/chadoProdDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+//            url = ""
+//            properties {
+//                // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+//                jmxEnabled = true
+//                initialSize = 5
+//                maxActive = 50
+//                minIdle = 5
+//                maxIdle = 25
+//                maxWait = 10000
+//                maxAge = 10 * 60000
+//                timeBetweenEvictionRunsMillis = 5000
+//                minEvictableIdleTimeMillis = 60000
+//                validationQuery = "SELECT 1"
+//                validationQueryTimeout = 3
+//                validationInterval = 15000
+//                testOnBorrow = true
+//                testWhileIdle = true
+//                testOnReturn = false
+//                jdbcInterceptors = "ConnectionState"
+//                defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+//            }
+//        }
+    }
+}
+
diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy
new file mode 100644
index 0000000..fa3a4c7
--- /dev/null
+++ b/grails-app/conf/UrlMappings.groovy
@@ -0,0 +1,132 @@
+class UrlMappings {
+
+    static mappings = {
+        "/$controller/$action?/$id?(.$format)?" {
+            constraints {
+                // apply constraints here
+            }
+        }
+
+
+        "/"(redirect: '/annotator/index')
+        "500"(view: '/error')
+        "/menu"(view: '/menu')
+        "/version.jsp"(controller: 'annotator', view: "version")
+        "/${clientToken}/version.jsp"(controller: 'annotator', view: "version")
+
+        "/track/nclist/${organismString}/${trackName}/${sequence}:${fmin}..${fmax}.json"(controller: "track", action: "nclist")
+        "/track/nclist/${organismString}/${trackName}/?loc=${sequence}:${fmin}..${fmax}"(controller: "track", action: "nclist")
+
+//        "/trackForName/${organismString}/${trackName}/${sequence}/${featureName}.json"(controller: "track", action: "jsonName")
+        "/track/${organismString}/${trackName}/${sequence}/${featureName}.${type}"(controller: "track", action: "featuresByName",[params:params])
+        "/track/${organismString}/${trackName}/${sequence}:${fmin}..${fmax}.${type}"(controller: "track", action: "featuresByLocation",[params:params])
+        "/track/${organismString}/${trackName}/?loc=${sequence}:${fmin}..${fmax}.${type}"(controller: "track", action: "featuresByLocation",[params:params])
+        "/track/cache/clear/${organismName}/${trackName}"(controller: "track", action: "clearTrackCache")
+        "/track/cache/clear/${organismName}"(controller: "track", action: "clearOrganismCache")
+
+        "/sequence/${organismString}/?loc=${sequenceName}:${fmin}..${fmax}"(controller: "sequence", action: "sequenceByLocation",[params:params])
+        "/sequence/${organismString}/${sequenceName}:${fmin}..${fmax}"(controller: "sequence", action: "sequenceByLocation",[params:params])
+        "/sequence/${organismString}/${sequenceName}/${featureName}.${type}"(controller: "sequence", action: "sequenceByName",[params:params])
+        "/sequence/cache/clear/${organismName}/${sequenceName}"(controller: "sequence", action: "clearSequenceCache")
+        "/sequence/cache/clear/${organismName}"(controller: "sequence", action: "clearOrganismCache")
+
+        // TODO: remove if we merge with the JSON
+        "/track/biolink/${organismString}/${trackName}/${sequence}:${fmin}..${fmax}.biolink"(controller: "track", action: "biolink")
+        "/track/biolink/${organismString}/${trackName}/?loc=${sequence}:${fmin}..${fmax}"(controller: "track", action: "biolink")
+
+        // set this routing here
+        "/jbrowse/"(controller: "jbrowse", action: "indexRouter", params:params)
+        "/${clientToken}/jbrowse/"(controller: "jbrowse", action: "indexRouter", params:params)
+        "/${clientToken}/jbrowse/index.html"(controller: "jbrowse", action: "indexRouter", params:params)
+        "/${clientToken}/jbrowse/${path}**" {
+            controller= "jbrowse"
+            action= "passthrough"
+            prefix= "jbrowse"
+//            permanent = true
+        }
+        "/${clientToken}/stomp/${path}**" {
+            controller= "jbrowse"
+            action= "passthrough"
+            prefix= "stomp"
+//            permanent = true
+        }
+
+        "/${clientToken}/jbrowse/data/${path}"(controller: "jbrowse", action: "data")
+        "/${clientToken}/jbrowse/data/${path}**"(controller: "jbrowse", action: "data")
+        "/${clientToken}/jbrowse/data/trackList.json"(controller:"jbrowse", action: "trackList")
+        "/jbrowse/data/${path}"(controller: "jbrowse", action: "data")
+        "/jbrowse/data/${path}**"(controller: "jbrowse", action: "data")
+        "/jbrowse/data/trackList.json"(controller:"jbrowse", action: "trackList")
+
+
+
+        "/${clientToken}/AnnotationEditorService"(controller:"annotationEditor",action: "handleOperation",params:params)
+        "/Login"(controller:"login",action: "handleOperation",params:params)
+        "/${clientToken}/Login"(controller:"login",action: "handleOperation",params:params)
+        "/${clientToken}/sequence/lookupSequenceByNameAndOrganism"(controller:"sequence",action: "lookupSequenceByNameAndOrganism", params:params)
+
+        "/proxy/request/${url}"(controller:"proxy", action: "request")
+        "/${clientToken}/proxy/request/${url}"(controller:"proxy", action: "request")
+
+        "/ProxyService"(controller:"ncbiProxyService",action: "index",params:params)
+        "/${clientToken}/ProxyService"(controller:"ncbiProxyService",action: "index",params:params)
+
+        "/IOService"(controller:"IOService",action: "handleOperation",params:params)
+        "/${clientToken}/IOService"(controller:"IOService",action: "handleOperation",params:params)
+
+        "/IOService/download"(controller:"IOService",action: "download", params:params)
+        "/${clientToken}/IOService/download"(controller:"IOService",action: "download", params:params)
+
+        "/jbrowse/web_services/api"(controller:"annotationEditor",action: "web_services", params:params)
+        "/jbrowse/web_services/api"(controller:"webServices",action: "index", params:params)
+        "/${clientToken}/jbrowse/web_services/api"(controller:"webServices",action: "index", params:params)
+
+        // if all else fails
+        // TODO: pass all of these into the same function and remap from there
+        "/${clientToken}/${path}**.bw" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "bw"
+        }
+        "/${clientToken}/${path}**.bai" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "bai"
+        }
+        "/${clientToken}/${path}**.bam" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "bam"
+        }
+//        "/${clientToken}/${path}**.conf" {
+//            controller= "jbrowse"
+//            action= "data"
+//            fileType = "conf"
+//        }
+        "/${clientToken}/${path}**.gtf" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "gtf"
+        }
+        "/${clientToken}/${path}**.vcf.gz.tbi" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "vcf.gz.tbi"
+        }
+        "/${clientToken}/${path}**.vcf.gz" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "vcf.gz"
+        }
+        "/${clientToken}/${path}**.gff3" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "gff3"
+        }
+        "/${clientToken}/${path}**.gff" {
+            controller= "jbrowse"
+            action= "data"
+            fileType = "gff"
+        }
+    }
+}
diff --git a/grails-app/conf/org/bbop/apollo/SecurityFilters.groovy b/grails-app/conf/org/bbop/apollo/SecurityFilters.groovy
new file mode 100644
index 0000000..bf10e24
--- /dev/null
+++ b/grails-app/conf/org/bbop/apollo/SecurityFilters.groovy
@@ -0,0 +1,100 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.subject.Subject
+
+class SecurityFilters {
+
+    def permissionService
+
+    def filters = {
+
+        // TODO: this is the right way to do this as it uses proper forwarding, but
+        // the user object lacks permissions
+//        all(uri: "/**") {
+//            before = {
+//                // Ignore direct views (e.g. the default main index page).
+//                if (!controllerName) return true
+//                // Access control by convention.
+//                accessControl(auth:false)
+//            }
+//        }
+
+        // TODO: route more controllers through here
+        all(controller: '*', action: '*') {
+            before = {
+                if (controllerName == "organism"
+                        || controllerName == "home"
+                        || controllerName == "cannedKey"
+                        || controllerName == "cannedValue"
+                        || controllerName == "cannedComment"
+                        || controllerName == "availableStatus"
+                        || controllerName == "proxy"
+                        || controllerName == "featureType"
+                        || actionName == "report"
+                        || actionName == "loadLink"
+                ) {
+                    try {
+                        log.debug "apollo filter ${controllerName}::${actionName}"
+                        Subject subject = SecurityUtils.getSubject();
+                        if (!subject.isAuthenticated()) {
+
+                            // we don't try to add this here
+                            if (permissionService.authenticateWithToken(request)) {
+                                if (params.targetUri) {
+                                    redirect(uri: params.targetUri)
+                                }
+                                return true
+                            } else {
+                                log.warn "Authentication failed"
+                                def targetUri = "/${controllerName}/${actionName}"
+                                int paramCount = 0
+                                def paramString = ""
+                                for(p in params){
+                                    if(p.key!="controller" && p.key!="action"){
+                                        paramString += paramCount==0 ? "?" : "&"
+                                        String key = p.key
+                                        String value = p.value
+                                        // the ?loc is incorrect
+                                        // https://github.com/GMOD/Apollo/issues/1371
+                                        // ?ov/Apollo-staging/someanimal/jbrowse/?loc= -> ?loc=
+                                        if(p.key.contains("?loc")){
+                                            def lastIndex = p.key.lastIndexOf("loc")
+                                            key = p.key.substring(lastIndex)
+                                        }
+                                        else
+                                        if(p.key.startsWith("addStores")){
+                                            value = URLEncoder.encode(value,"UTF-8")
+                                        }
+                                        paramString += key +"="+ value
+                                        ++paramCount
+                                    }
+
+                                }
+                                // https://github.com/GMOD/Apollo/issues/1371
+                                // ?ov/Apollo-staging/someanimal/jbrowse/?loc= -> ?loc=
+                                // if it contains two question marks with no equals in-between, then fix it
+                                // paramString seems to be getting extra data on it via the paramString
+//                                int indexOfLoc = paramString.indexOf("?loc=")
+//                                int numberOfStartParams = paramString.findAll("\\?").size()
+//                                if (indexOfLoc > 0 && numberOfStartParams>1) {
+//                                    paramString = paramString.substring(indexOfLoc)
+//                                }
+                                targetUri = targetUri + paramString
+                                redirect(uri: "/auth/login?targetUri=${targetUri}")
+                                return false
+                            }
+                        }
+                    }
+                    catch (Exception e) {
+                        render([error: e.message] as JSON)
+                        return false
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy
new file mode 100644
index 0000000..fa95006
--- /dev/null
+++ b/grails-app/conf/spring/resources.groovy
@@ -0,0 +1,3 @@
+// Place your Spring DSL code here
+beans = {
+}
diff --git a/grails-app/controllers/org/bbop/apollo/AbstractApolloController.groovy b/grails-app/controllers/org/bbop/apollo/AbstractApolloController.groovy
new file mode 100644
index 0000000..ed532bc
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/AbstractApolloController.groovy
@@ -0,0 +1,48 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+abstract class AbstractApolloController {
+
+    public static String REST_OPERATION = "operation"
+    public static String REST_TRACK = "track"
+    public static String REST_FEATURES = "features"
+    public static REST_USERNAME = "username"
+    public static REST_PERMISSION = "permission"
+    public static REST_DATA_ADAPTER = "data_adapter"
+    public static REST_DATA_ADAPTERS = "data_adapters"
+    public static REST_KEY = "key"
+    public static REST_OPTIONS = "options"
+    public static REST_TRANSLATION_TABLE = "translation_table"
+
+    protected String underscoreToCamelCase(String underscore) {
+        if (!underscore || underscore.isAllWhitespace()) {
+            return ''
+        }
+        return underscore.replaceAll(/_\w/) { it[1].toUpperCase() }
+    }
+
+
+    protected JSONObject createJSONFeatureContainer(JSONObject... features) throws JSONException {
+        JSONObject jsonFeatureContainer = new JSONObject();
+        JSONArray jsonFeatures = new JSONArray();
+        jsonFeatureContainer.put(FeatureStringEnum.FEATURES.value, jsonFeatures);
+        for (JSONObject feature : features) {
+            jsonFeatures.put(feature);
+        }
+        return jsonFeatureContainer;
+    }
+
+    protected def findPost() {
+        for (p in params) {
+            String key = p.key
+            if (key.contains("operation")) {
+                return (JSONObject) JSON.parse(key)
+            }
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/AnnotationEditorController.groovy b/grails-app/controllers/org/bbop/apollo/AnnotationEditorController.groovy
new file mode 100644
index 0000000..a89b633
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/AnnotationEditorController.groovy
@@ -0,0 +1,1291 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import groovy.json.JsonBuilder
+import org.apache.shiro.SecurityUtils
+import org.bbop.apollo.event.AnnotationEvent
+import org.bbop.apollo.event.AnnotationListener
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.TranslationTable
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.http.HttpStatus
+import org.springframework.messaging.handler.annotation.MessageMapping
+import org.springframework.messaging.handler.annotation.SendTo
+
+import java.lang.reflect.InvocationTargetException
+import java.nio.charset.Charset
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.security.Principal
+
+import static grails.async.Promises.task
+
+/**
+ * From the WA1 AnnotationEditorService class.
+ *
+ * This code primarily provides integration with genomic editing functionality visible in the JBrowse window.
+ */
+ at RestApi(name = "Annotation Services", description = "Methods for running the annotation engine")
+class AnnotationEditorController extends AbstractApolloController implements AnnotationListener {
+
+
+    def featureService
+    def sequenceService
+    def configWrapperService
+    def featureRelationshipService
+    def featurePropertyService
+    def requestHandlingService
+    def transcriptService
+    def exonService
+    def permissionService
+    def preferenceService
+    def sequenceSearchService
+    def featureEventService
+    def annotationEditorService
+    def organismService
+    def brokerMessagingTemplate
+
+
+    def index() {
+        log.debug "bang "
+    }
+
+    // Map the operation specified in the URL to a controller
+    def handleOperation(String track, String operation) {
+        JSONObject postObject = findPost()
+        operation = postObject.get(REST_OPERATION)
+        def mappedAction = underscoreToCamelCase(operation)
+        log.debug "handleOperation ${params.controller} ${operation} -> ${mappedAction}"
+        forward action: "${mappedAction}", params: [data: postObject]
+    }
+
+    /**
+     * @return
+     */
+    @Timed
+    def getUserPermission() {
+        log.debug "getUserPermission ${params.data}"
+        JSONObject returnObject = permissionService.handleInput(request, params)
+
+        String username = SecurityUtils.subject.principal
+        if (username) {
+            int permission = PermissionEnum.NONE.value
+
+            User user = User.findByUsername(username)
+            log.debug "getting user permission for ${user}, returnObject"
+//            Organism organism = preferenceService.getOrganismFromPreferences(user,null,returnObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+            Organism organism = preferenceService.getCurrentOrganismForCurrentUser(returnObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+            if (!organism) {
+                log.error "somehow no organism shown, getting for all"
+            }
+            Map<String, Integer> permissions
+            List<PermissionEnum> permissionEnumList = permissionService.getOrganismPermissionsForUser(organism, user)
+            permission = permissionService.findHighestEnumValue(permissionEnumList)
+            permissions = new HashMap<>()
+            permissions.put(username, permission)
+            permissions = permissionService.getPermissionsForUser(user)
+            if (permissions) {
+                session.setAttribute("permissions", permissions);
+            }
+            if (permissions.values().size() > 0) {
+                permission = permissions.values().iterator().next();
+            }
+            returnObject.put(REST_PERMISSION, permission)
+            returnObject.put(REST_USERNAME, username)
+            render returnObject
+        } else {
+            def errorMessage = [message: "You must first login before editing"]
+            response.status = 401
+            render errorMessage as JSON
+        }
+    }
+
+    //TODO: parse permissions
+    def getDataAdapters() {
+        log.debug "getDataAdapters"
+        JSONObject returnObject = permissionService.handleInput(request, params)
+        def set = configWrapperService.getDataAdapterTools()
+
+        def obj = new JsonBuilder(set)
+        def jre = ["data_adapters": obj.content]
+        render jre as JSON
+    }
+
+    @Timed
+    def getHistoryForFeatures() {
+        log.debug "getHistoryForFeatures ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        inputObject.put(FeatureStringEnum.USERNAME.value, SecurityUtils.subject.principal)
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        permissionService.checkPermissions(inputObject, PermissionEnum.READ)
+
+        JSONObject historyContainer = createJSONFeatureContainer();
+        historyContainer = featureEventService.generateHistory(historyContainer, featuresArray)
+
+        render historyContainer as JSON
+    }
+
+
+    @RestApiMethod(description = "Returns a translation table as JSON", path = "/annotationEditor/getTranslationTable", verb = RestApiVerb.POST)
+    @RestApiParams(params = [])
+    def getTranslationTable() {
+        log.debug "getTranslationTable"
+        JSONObject returnObject = permissionService.handleInput(request, params)
+        log.debug "return object ${returnObject as JSON}"
+        Organism organism = preferenceService.getCurrentOrganismForCurrentUser(returnObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+        log.debug "has organism ${organism}"
+        // use the over-wridden one
+        TranslationTable translationTable = organismService.getTranslationTable(organism)
+
+        JSONObject ttable = new JSONObject()
+        for (Map.Entry<String, String> t : translationTable.getTranslationTable().entrySet()) {
+            ttable.put(t.getKey(), t.getValue())
+        }
+        returnObject.put(REST_TRANSLATION_TABLE, ttable)
+        render returnObject
+    }
+
+
+    @RestApiMethod(description = "Add non-coding genomic feature", path = "/annotationEditor/addFeature", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "suppressHistory", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress the history of this operation")
+            , @RestApiParam(name = "suppressEvents", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress instant update of the user interface")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ])
+    def addFeature() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addFeature(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set exon feature boundaries", path = "/annotationEditor/setExonBoundaries", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "suppressHistory", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress the history of this operation")
+            , @RestApiParam(name = "suppressEvents", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress instant update of the user interface")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def setExonBoundaries() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setExonBoundaries(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+
+    @RestApiMethod(description = "Add an exon", path = "/annotationEditor/addExon", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "suppressHistory", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress the history of this operation")
+            , @RestApiParam(name = "suppressEvents", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress instant update of the user interface")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def addExon() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addExon(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+
+    @RestApiMethod(description = "Add comments", path = "/annotationEditor/addComments", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects ('uniquename' required) that include an added 'comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def addComments() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addComments(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Delete comments", path = "/annotationEditor/deleteComments", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects ('uniquename' required) that include an added 'comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def deleteComments() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteComments(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+
+    @RestApiMethod(description = "Update comments", path = "/annotationEditor/updateComments", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects ('uniquename' required) that include an added 'old_comments','new_comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def updateComments() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.updateComments(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+
+    @RestApiMethod(description = "Get comments", path = "/annotationEditor/getComments", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects ('uniquename' required) JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ]
+    )
+    def getComments() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.READ)) {
+            render requestHandlingService.getComments(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Add transcript", path = "/annotationEditor/addTranscript", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "suppressHistory", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress the history of this operation")
+            , @RestApiParam(name = "suppressEvents", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress instant update of the user interface")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy")
+    ])
+    def addTranscript() {
+        try {
+            log.debug "addTranscript ${params}"
+            JSONObject inputObject = permissionService.handleInput(request, params)
+            if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+                render requestHandlingService.addTranscript(inputObject)
+            } else {
+                render status: HttpStatus.UNAUTHORIZED
+            }
+        }
+        catch (Exception e) {
+            def error = [error: e.message]
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Duplicate transcript", path = "/annotationEditor/duplicateTranscript", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "suppressHistory", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress the history of this operation")
+            , @RestApiParam(name = "suppressEvents", type = "boolean", paramType = RestApiParamType.QUERY, description = "Suppress instant update of the user interface")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing a single JSONObject feature that contains 'uniquename'")
+    ])
+    def duplicateTranscript() {
+        log.debug "duplicateTranscript ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.duplicateTranscript(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set translation start", path = "/annotationEditor/setTranslationStart", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmin':12}}")
+    ])
+    def setTranslationStart() {
+        log.debug "setTranslationStart ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setTranslationStart(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set translation end", path = "/annotationEditor/setTranslationEnd", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmax':12}}")
+    ])
+    def setTranslationEnd() {
+        log.debug "setTranslationEnd ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setTranslationEnd(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set longest ORF", path = "/annotationEditor/setLongestOrf", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234'}")
+    ])
+    def setLongestOrf() {
+        log.debug "setLongestORF ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setLongestOrf(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set boundaries of genomic feature", path = "/annotationEditor/setBoundaries", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing feature objects with the location object defined {'uniquename':'ABCD-1234','location':{'fmin':2,'fmax':12}}")
+    ])
+    def setBoundaries() {
+        log.debug "setBoundaries ${params}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setBoundaries(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Get all annotated features for a sequence", path = "/annotationEditor/getFeatures", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+    ])
+    def getFeatures() {
+        JSONObject returnObject = permissionService.handleInput(request, params)
+        try {
+            permissionService.checkPermissions(returnObject, PermissionEnum.READ)
+            render requestHandlingService.getFeatures(returnObject)
+        } catch (e) {
+            def error = [error: 'problem getting features: ' + e.fillInStackTrace()]
+            render error as JSON
+            log.error(error.error)
+        }
+    }
+
+    @Timed
+    def getInformation() {
+        JSONObject featureContainer = createJSONFeatureContainer();
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.checkPermissions(PermissionEnum.WRITE)) {
+            render new JSONObject() as JSON
+            return
+        }
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        for (int i = 0; i < featuresArray.size(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value);
+            Feature gbolFeature = Feature.findByUniqueName(uniqueName)
+            JSONObject info = new JSONObject();
+            info.put(FeatureStringEnum.UNIQUENAME.value, uniqueName);
+            info.put("time_accessioned", gbolFeature.lastUpdated)
+            info.put("owner", gbolFeature.owner ? gbolFeature.owner.username : "N/A");
+            String parentIds = "";
+            featureRelationshipService.getParentForFeature(gbolFeature).each {
+                if (parentIds.length() > 0) {
+                    parentIds += ", ";
+                }
+                parentIds += it.getUniqueName();
+            }
+            if (parentIds.length() > 0) {
+                info.put("parent_ids", parentIds);
+            }
+            def featureProperties = featurePropertyService.getNonReservedProperties(gbolFeature);
+            featureProperties.each {
+                info.put(it.tag, it.value);
+            }
+            featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(info);
+        }
+
+        render featureContainer
+    }
+
+
+    @RestApiMethod(description = "Get sequence alterations for a given sequence", path = "/annotationEditor/getSequenceAlterations", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+    ])
+    @Timed
+    def getSequenceAlterations() {
+        JSONObject returnObject = permissionService.handleInput(request, params)
+        Sequence sequence = permissionService.checkPermissions(returnObject, PermissionEnum.READ)
+        JSONArray jsonFeatures = new JSONArray()
+        returnObject.put(FeatureStringEnum.FEATURES.value, jsonFeatures)
+
+        List<SequenceAlteration> sequenceAlterationList = Feature.executeQuery("select f from Feature f join f.featureLocations fl join fl.sequence s where s = :sequence and f.class in :sequenceTypes"
+                , [sequence: sequence, sequenceTypes: requestHandlingService.viewableAlterations])
+        for (SequenceAlteration alteration : sequenceAlterationList) {
+            jsonFeatures.put(featureService.convertFeatureToJSON(alteration, true));
+        }
+
+        render returnObject
+    }
+
+    /**
+     * @deprecated This will likely be removed
+     * @return
+     */
+//    def getOrganism() {
+//        JSONObject inputObject = permissionService.handleInput(request, params)
+//        Organism organism = preferenceService.getCurrentOrganismForCurrentUser(inputObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+//        if (organism) {
+//            render organism as JSON
+//        } else {
+//            render new JSONObject()
+//        }
+//    }
+
+    def getAnnotationInfoEditorConfiguration() {
+        JSONObject annotationInfoEditorConfigContainer = new JSONObject();
+        JSONArray annotationInfoEditorConfigs = new JSONArray();
+        annotationInfoEditorConfigContainer.put(FeatureStringEnum.ANNOTATION_INFO_EDITOR_CONFIGS.value, annotationInfoEditorConfigs);
+        JSONObject annotationInfoEditorConfig = new JSONObject();
+        annotationInfoEditorConfigs.put(annotationInfoEditorConfig);
+
+        annotationInfoEditorConfig.put(FeatureStringEnum.HASDBXREFS.value, configWrapperService.hasDbxrefs());
+        annotationInfoEditorConfig.put(FeatureStringEnum.HASATTRIBUTES.value, configWrapperService.hasAttributes());
+        annotationInfoEditorConfig.put(FeatureStringEnum.HASPUBMEDIDS.value, configWrapperService.hasPubmedIds());
+        annotationInfoEditorConfig.put(FeatureStringEnum.HASGOIDS.value, configWrapperService.hasGoIds());
+        annotationInfoEditorConfig.put(FeatureStringEnum.HASCOMMENTS.value, configWrapperService.hasComments());
+        JSONArray supportedTypes = new JSONArray();
+        supportedTypes.add(FeatureStringEnum.DEFAULT.value)
+        annotationInfoEditorConfig.put(FeatureStringEnum.SUPPORTED_TYPES.value, supportedTypes);
+        log.debug "return config ${annotationInfoEditorConfigContainer}"
+        render annotationInfoEditorConfigContainer
+    }
+
+    @RestApiMethod(description = "Set name of a feature", path = "/annotationEditor/setName", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','name':'gene01'}")
+    ])
+    def setName() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setName(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set description for a feature", path = "/annotationEditor/setDescription", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','description':'some descriptive test'}")
+    ])
+    def setDescription() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setDescription(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set symbol of a feature", path = "/annotationEditor/setSymbol", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','symbol':'Pax6a'}")
+    ])
+    def setSymbol() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setSymbol(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set status of a feature", path = "/annotationEditor/setStatus", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','status':'existing-status-string'}.  Available status found here: /availableStatus/ ")
+    ])
+    def setStatus() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setStatus(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Add attribute (key,value pair) to feature", path = "/annotationEditor/addAttribute", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','non_reserved_properties':[{'tag':'clockwork','value':'orange'},{'tag':'color','value':'purple'}]}.  Available status found here: /availableStatus/ ")
+    ])
+    def addAttribute() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addNonReservedProperties(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Delete attribute (key,value pair) for feature", path = "/annotationEditor/deleteAttribute", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','non_reserved_properties':[{'tag':'clockwork','value':'orange'},{'tag':'color','value':'purple'}]}.  Available status found here: /availableStatus/ ")
+    ])
+    def deleteAttribute() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteNonReservedProperties(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Update attribute (key,value pair) for feature", path = "/annotationEditor/updateAttribute", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','old_non_reserved_properties':[{'color': 'red'}], 'new_non_reserved_properties': [{'color': 'green'}]}.")
+    ])
+    def updateAttribute() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.updateNonReservedProperties(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Add dbxref (db,id pair) to feature", path = "/annotationEditor/addDbxref", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','dbxrefs': [{'db': 'PMID', 'accession': '19448641'}]}.")
+    ])
+    def addDbxref() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addNonPrimaryDbxrefs(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Update dbxrefs (db,id pairs) for a feature", path = "/annotationEditor/updateDbxref", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','old_dbxrefs': [{'db': 'PMID', 'accession': '19448641'}], 'new_dbxrefs': [{'db': 'PMID', 'accession': '19448642'}]}.")
+    ])
+    def updateDbxref() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.updateNonPrimaryDbxrefs(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Delete dbxrefs (db,id pairs) for a feature", path = "/annotationEditor/deleteDbxref", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','dbxrefs': [{'db': 'PMID', 'accession': '19448641'}]}.")
+    ])
+    def deleteDbxref() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteNonPrimaryDbxrefs(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Set readthrough stop codon", path = "/annotationEditor/setReadthroughStopCodon", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with one feature object {'uniquename':'ABCD-1234'}")
+    ])
+    def setReadthroughStopCodon() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.setReadthroughStopCodon(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Add sequence alteration", path = "/annotationEditor/addSequenceAlteration", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with Sequence Alteration (Insertion, Deletion, Substituion) objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/")
+    ])
+    def addSequenceAlteration() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.addSequenceAlteration(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Delete sequence alteration", path = "/annotationEditor/deleteSequenceAlteration", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with Sequence Alteration identified by unique names {'uniquename':'ABC123'}")
+    ])
+    def deleteSequenceAlteration() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteSequenceAlteration(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Flip strand", path = "/annotationEditor/flipStrand", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with with objects of features defined as {'uniquename':'ABC123'}")
+    ])
+    def flipStrand() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.flipStrand(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Merge exons", path = "/annotationEditor/mergeExons", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with with two objects of referred to as defined as {'uniquename':'ABC123'}")
+    ])
+    def mergeExons() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.mergeExons(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Split exons", path = "/annotationEditor/splitExon", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing feature objects with the location object defined {'uniquename':'ABCD-1234','location':{'fmin':2,'fmax':12}}")
+    ])
+    def splitExon() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.splitExon(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+
+    @RestApiMethod(description = "Delete feature", path = "/annotationEditor/deleteFeature", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of features objects to delete defined by unique name {'uniquename':'ABC123'}")
+    ])
+    def deleteFeature() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteFeature(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Delete exons", path = "/annotationEditor/deleteExon", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of features objects, where the first is the parent transcript and the remaining are exons all defined by a unique name {'uniquename':'ABC123'}")
+    ])
+    def deleteExon() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.deleteExon(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Make intron", path = "/annotationEditor/makeIntron", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmin':12}}")
+    ])
+    def makeIntron() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.makeIntron(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Split transcript", path = "/annotationEditor/splitTranscript", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with with two exon objects referred to their unique names {'uniquename':'ABC123'}")
+    ])
+    def splitTranscript() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.splitTranscript(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Merge transcripts", path = "/annotationEditor/mergeTranscripts", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray with with two transcript objects referred to their unique names {'uniquename':'ABC123'}")
+    ])
+    def mergeTranscripts() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (permissionService.hasPermissions(inputObject, PermissionEnum.WRITE)) {
+            render requestHandlingService.mergeTranscripts(inputObject)
+        } else {
+            render status: HttpStatus.UNAUTHORIZED
+        }
+    }
+
+    @RestApiMethod(description = "Get sequences for features", path = "/annotationEditor/getSequences", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sequence name")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism ID or common name")
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of features objects to export defined by a unique name {'uniquename':'ABC123'}")
+    ])
+    def getSequence() {
+        log.debug "getSequence ${params.data}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(inputObject, PermissionEnum.EXPORT)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        JSONObject featureContainer = createJSONFeatureContainer()
+        JSONObject sequenceObject = sequenceService.getSequenceForFeatures(inputObject)
+        featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(sequenceObject)
+        render featureContainer
+    }
+
+    @RestApiMethod(description = "Get sequences search tools", path = "/annotationEditor/getSequenceSearchTools")
+    def getSequenceSearchTools() {
+        log.debug "getSequenceSearchTools ${params.data}"
+        def set = configWrapperService.getSequenceSearchTools()
+        def obj = new JsonBuilder(set)
+        def jre = ["sequence_search_tools": obj.content]
+        render jre as JSON
+    }
+
+    @RestApiMethod(description = "Get canned comments", path = "/annotationEditor/getCannedComments", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+    ])
+    def getCannedComments() {
+        log.debug "sequenceSearch ${params.data}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(inputObject, PermissionEnum.READ)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        render CannedComment.listOrderByComment() as JSON
+    }
+
+    @RestApiMethod(description = "Search sequences", path = "/annotationEditor/searchSequences", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "client_token", type = "string", paramType = RestApiParamType.QUERY, description = "Organism ID/Name or Client-generated ")
+            , @RestApiParam(name = "search", type = "JSONObject", paramType = RestApiParamType.QUERY, description = "{'key':'blat','residues':'ATACTAGAGATAC':'database_id':'abc123'}")
+    ])
+    def searchSequence() {
+        log.debug "sequenceSearch ${params.data}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(inputObject, PermissionEnum.READ)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        Organism organism = preferenceService.getCurrentOrganismForCurrentUser(inputObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+        log.debug "Organism to string:  ${organism as JSON}"
+        render sequenceSearchService.searchSequence(inputObject, organism.getBlatdb())
+    }
+
+
+    @RestApiMethod(description = "Get gff3", path = "/annotationEditor/getGff3", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "features", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "JSONArray of features objects to export defined by a unique name {'uniquename':'ABC123'}")
+    ])
+    def getGff3() {
+        log.debug "getGff3 ${params.data}"
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(inputObject, PermissionEnum.EXPORT)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        try {
+            File outputFile = File.createTempFile("feature", ".gff3");
+            sequenceService.getGff3ForFeature(inputObject, outputFile)
+            Charset encoding = Charset.defaultCharset()
+            byte[] encoded = Files.readAllBytes(Paths.get(outputFile.getAbsolutePath()))
+            String gff3String = new String(encoded, encoding)
+            outputFile.delete() // deleting temp file
+            render gff3String
+
+        } catch (IOException e) {
+            log.debug("Cannot create a temp file for 'get GFF3' operation")
+            e.printStackTrace()
+        }
+    }
+
+    @Timed
+    def getAnnotationInfoEditorData() {
+        Sequence sequence
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        try {
+            sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        } catch (e) {
+            log.error(e)
+            render new JSONObject() as JSON
+            return
+        }
+
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject returnObject = createJSONFeatureContainer()
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            log.debug "input json feature ${jsonFeature}"
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+//            JSONObject newFeature = featureService.convertFeatureToJSON(feature, false)
+            JSONObject newFeature = new JSONObject()
+
+            if (feature.symbol) {
+                newFeature.put(FeatureStringEnum.SYMBOL.value, feature.symbol)
+            }
+            if (feature.description) {
+                newFeature.put(FeatureStringEnum.DESCRIPTION.value, feature.description)
+            }
+
+            jsonFeature.put(FeatureStringEnum.ID.value, feature.id);
+            if (feature.getName() != null) {
+                newFeature.put(FeatureStringEnum.NAME.value, feature.getName());
+            }
+            newFeature.put(FeatureStringEnum.ID.value, feature.id)
+            newFeature.put(FeatureStringEnum.OWNER.value, featureService.generateOwnerString(feature));
+            newFeature.put(FeatureStringEnum.DATE_CREATION.value, feature.dateCreated.time);
+            newFeature.put(FeatureStringEnum.DATE_LAST_MODIFIED.value, feature.lastUpdated.time);
+            newFeature.put(FeatureStringEnum.TYPE.value, featureService.generateJSONFeatureStringForType(feature.ontologyId));
+
+            if (feature.featureLocation) {
+                newFeature.put(FeatureStringEnum.SEQUENCE.value, feature.featureLocation.sequence.name);
+            }
+
+
+            List<FeatureType> featureTypeList = FeatureType.findAllByOntologyId(feature.ontologyId)
+
+            if (AvailableStatus.count > 0) {
+                if(feature.status){
+                    newFeature.put(FeatureStringEnum.STATUS.value, feature.status.value)
+                }
+                JSONArray availableStatuses = new JSONArray()
+                newFeature.put(FeatureStringEnum.AVAILABLE_STATUSES.value,availableStatuses)
+
+                List<AvailableStatus> availableStatusList = new ArrayList<>()
+                if (featureTypeList) {
+                    availableStatusList.addAll(AvailableStatus.executeQuery("select cc from AvailableStatus cc join cc.featureTypes ft where ft in (:featureTypeList)", [featureTypeList: featureTypeList]))
+                }
+                availableStatusList.addAll(AvailableStatus.executeQuery("select cc from AvailableStatus cc where cc.featureTypes is empty"))
+
+//                // if there are organism filters for these statuses for this organism, then apply them
+                List<AvailableStatusOrganismFilter> availableStatusOrganismFilters = AvailableStatusOrganismFilter.findAllByAvailableStatusInList(availableStatusList)
+                if (availableStatusOrganismFilters) {
+                    AvailableStatusOrganismFilter.findAllByOrganismAndAvailableStatusInList(sequence.organism, availableStatusList).each {
+                        availableStatuses.put(it.availableStatus.value)
+                        availableStatusList.remove(it.availableStatus)
+                    }
+                    availableStatusList.each {
+                        availableStatuses.put(it.value)
+                    }
+                }
+//                // otherwise ignore them
+                else {
+                    availableStatusList.each {
+                        availableStatuses.put(it.value)
+                    }
+                }
+
+            }
+
+
+            // TODO: add the rest of the attributes
+            if (configWrapperService.hasAttributes()) {
+                JSONArray properties = new JSONArray();
+                newFeature.put(FeatureStringEnum.NON_RESERVED_PROPERTIES.value, properties);
+                for (FeatureProperty property : featurePropertyService.getNonReservedProperties(feature)) {
+                    JSONObject jsonProperty = new JSONObject();
+                    jsonProperty.put(FeatureStringEnum.TAG.value, property.getTag());
+                    jsonProperty.put(FeatureStringEnum.VALUE.value, property.getValue());
+                    properties.put(jsonProperty);
+                }
+
+
+
+                List<CannedKey> cannedKeyList = new ArrayList<>()
+                JSONArray cannedKeys = new JSONArray();
+                newFeature.put(FeatureStringEnum.CANNED_KEYS.value, cannedKeys);
+                if (featureTypeList) {
+                    cannedKeyList.addAll(CannedKey.executeQuery("select cc from CannedKey cc join cc.featureTypes ft where ft in (:featureTypeList)", [featureTypeList: featureTypeList]))
+                }
+                cannedKeyList.addAll(CannedKey.executeQuery("select cc from CannedKey cc where cc.featureTypes is empty"))
+
+//                // if there are organism filters for these canned comments for this organism, then apply them
+                List<CannedKeyOrganismFilter> cannedKeyOrganismFilters = CannedKeyOrganismFilter.findAllByCannedKeyInList(cannedKeyList)
+                if (cannedKeyOrganismFilters) {
+                    CannedKeyOrganismFilter.findAllByOrganismAndCannedKeyInList(sequence.organism, cannedKeyList).each {
+                        cannedKeys.put(it.cannedKey.label)
+                    }
+                }
+//                // otherwise ignore them
+                else {
+                    cannedKeyList.each {
+                        cannedKeys.put(it.label)
+                    }
+                }
+
+                // handle canned Values
+                List<CannedValue> cannedValueList = new ArrayList<>()
+                JSONArray cannedValues = new JSONArray();
+                newFeature.put(FeatureStringEnum.CANNED_VALUES.value, cannedValues);
+                if (featureTypeList) {
+                    cannedValueList.addAll(CannedValue.executeQuery("select cc from CannedValue cc join cc.featureTypes ft where ft in (:featureTypeList)", [featureTypeList: featureTypeList]))
+                }
+                cannedValueList.addAll(CannedValue.executeQuery("select cc from CannedValue cc where cc.featureTypes is empty"))
+
+//                // if there are organism filters for these canned comments for this organism, then apply them
+                List<CannedValueOrganismFilter> cannedValueOrganismFilters = CannedValueOrganismFilter.findAllByCannedValueInList(cannedValueList)
+                if (cannedValueOrganismFilters) {
+                    CannedValueOrganismFilter.findAllByOrganismAndCannedValueInList(sequence.organism, cannedValueList).each {
+                        cannedValues.put(it.cannedValue.label)
+                    }
+                }
+//                // otherwise ignore them
+                else {
+                    cannedValueList.each {
+                        cannedValues.put(it.label)
+                    }
+                }
+            }
+            if (configWrapperService.hasDbxrefs() || configWrapperService.hasPubmedIds() || configWrapperService.hasGoIds()) {
+                JSONArray dbxrefs = new JSONArray();
+                newFeature.put(FeatureStringEnum.DBXREFS.value, dbxrefs);
+                for (DBXref dbxref : feature.featureDBXrefs) {
+                    JSONObject jsonDbxref = new JSONObject();
+                    jsonDbxref.put(FeatureStringEnum.DB.value, dbxref.getDb().getName());
+                    jsonDbxref.put(FeatureStringEnum.ACCESSION.value, dbxref.getAccession());
+                    dbxrefs.put(jsonDbxref);
+                }
+            }
+            if (configWrapperService.hasComments()) {
+                JSONArray comments = new JSONArray();
+                newFeature.put(FeatureStringEnum.COMMENTS.value, comments);
+                for (Comment comment : featurePropertyService.getComments(feature)) {
+                    comments.put(comment.value);
+                }
+
+                JSONArray cannedComments = new JSONArray();
+                newFeature.put(FeatureStringEnum.CANNED_COMMENTS.value, cannedComments);
+
+                List<CannedComment> cannedCommentList = new ArrayList<>()
+                if (featureTypeList) {
+                    cannedCommentList.addAll(CannedComment.executeQuery("select cc from CannedComment cc join cc.featureTypes ft where ft in (:featureTypeList)", [featureTypeList: featureTypeList]))
+                }
+                cannedCommentList.addAll(CannedComment.executeQuery("select cc from CannedComment cc where cc.featureTypes is empty"))
+
+                // if there are organism filters for these canned comments for this organism, then apply them
+                List<CannedCommentOrganismFilter> cannedCommentOrganismFilters = CannedCommentOrganismFilter.findAllByCannedCommentInList(cannedCommentList)
+                if (cannedCommentOrganismFilters) {
+                    CannedCommentOrganismFilter.findAllByOrganismAndCannedCommentInList(sequence.organism, cannedCommentList).each {
+                        cannedComments.put(it.cannedComment.comment)
+                    }
+                }
+                // otherwise ignore them
+                else {
+                    cannedCommentList.each {
+                        cannedComments.put(it.comment)
+                    }
+                }
+            }
+
+            returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(newFeature);
+        }
+
+
+        render returnObject
+    }
+
+
+    @MessageMapping("/AnnotationNotification")
+    @SendTo("/topic/AnnotationNotification")
+    @Timed
+    protected String annotationEditor(String inputString, Principal principal) {
+        inputString = annotationEditorService.cleanJSONString(inputString)
+        JSONObject rootElement = (JSONObject) JSON.parse(inputString)
+        rootElement.put(FeatureStringEnum.USERNAME.value, principal.name)
+
+        String operation = ((JSONObject) rootElement).get(REST_OPERATION)
+
+        String operationName = underscoreToCamelCase(operation)
+        log.debug "operationName: ${operationName}"
+        def p = task {
+            switch (operationName) {
+                case "logout":
+                    SecurityUtils.subject.logout()
+                    break
+                case "setToDownstreamDonor": requestHandlingService.setDonor(rootElement, false)
+                    break
+                case "setToUpstreamDonor": requestHandlingService.setDonor(rootElement, true)
+                    break
+                case "setToDownstreamAcceptor": requestHandlingService.setAcceptor(rootElement, false)
+                    break
+                case "setToUpstreamAcceptor": requestHandlingService.setAcceptor(rootElement, true)
+                    break
+                default:
+                    boolean foundMethod = false
+                    String returnString = null
+                    requestHandlingService.getClass().getMethods().each { method ->
+                        if (method.name == operationName) {
+                            foundMethod = true
+                            log.debug "found the method ${operationName}"
+                            Feature.withNewSession {
+                                try {
+                                    returnString = method.invoke(requestHandlingService, rootElement)
+                                } catch (e) {
+                                    log.error("CAUGHT ERROR through websocket call: " + e)
+                                    if (e instanceof InvocationTargetException || !e.message) {
+                                        log.error("THROWING PARENT ERROR instead through reflection: " + e.getCause())
+                                        return sendError(e.getCause(), principal?.name)
+                                    } else {
+                                        return sendError(e, principal?.name)
+                                    }
+                                }
+                            }
+                            return returnString
+                        }
+                    }
+                    if (foundMethod) {
+                        return returnString
+                    } else {
+                        log.error "METHOD NOT found ${operationName}"
+                        throw new AnnotationException("Operation ${operationName} not found")
+                    }
+                    break
+            }
+        }
+        try {
+            def results = p.get()
+            return results
+        } catch (Exception ae) {
+            // TODO: should be returning nothing, but then broadcasting specifically to this user
+            log.error("Error for user ${principal?.name} when exexecting ${inputString}" + ae?.message)
+            return sendError(ae, principal.name)
+        }
+
+    }
+
+// TODO: handle errors without broadcasting
+    protected def sendError(Throwable exception, String username) {
+        log.error "exception ${exception}"
+        log.error "exception message ${exception.message}"
+        log.error "username ${username}"
+
+        JSONObject errorObject = new JSONObject()
+        errorObject.put(REST_OPERATION, FeatureStringEnum.ERROR.name())
+        errorObject.put(FeatureStringEnum.ERROR_MESSAGE.value, exception.message)
+        errorObject.put(FeatureStringEnum.USERNAME.value, username)
+
+        def destination = "/topic/AnnotationNotification/user/" + username
+        log.error "error destination message: ${destination}"
+        brokerMessagingTemplate.convertAndSend(destination, exception.message ?: exception.fillInStackTrace().fillInStackTrace())
+
+        return errorObject.toString()
+    }
+
+
+    @SendTo("/topic/AnnotationNotification")
+    protected String sendAnnotationEvent(String returnString) {
+        log.debug "sendAnnotationEvent ${returnString?.size()}"
+        return returnString
+    }
+
+    synchronized void handleChangeEvent(AnnotationEvent... events) {
+        log.debug "handleChangeEvent ${events.length}"
+        if (events.length == 0) {
+            return;
+        }
+        JSONArray operations = new JSONArray();
+        for (AnnotationEvent event : events) {
+            JSONObject features = event.getFeatures();
+            try {
+                features.put("operation", event.getOperation().name());
+                features.put("sequenceAlterationEvent", event.isSequenceAlterationEvent());
+                operations.put(features);
+            }
+            catch (JSONException e) {
+                log.error("error handling change event ${event}: ${e}")
+            }
+        }
+
+        sendAnnotationEvent(operations.toString())
+
+    }
+
+
+    def web_services() {
+        render view: "/web_services"
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/AnnotatorController.groovy b/grails-app/controllers/org/bbop/apollo/AnnotatorController.groovy
new file mode 100644
index 0000000..61d6cfb
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/AnnotatorController.groovy
@@ -0,0 +1,507 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.bbop.apollo.event.AnnotationEvent
+import org.bbop.apollo.gwt.shared.ClientTokenGenerator
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.report.AnnotatorSummary
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.hibernate.FetchMode
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.http.HttpStatus
+
+/**
+ * This is server-side code supporting the high-level functionality of the GWT AnnotatorPanel class.
+ */
+class AnnotatorController {
+
+    def featureService
+    def requestHandlingService
+    def permissionService
+    def annotatorService
+    def preferenceService
+    def reportService
+    def featureRelationshipService
+    def configWrapperService
+
+    private List<String> reservedList = ["loc",
+                                         FeatureStringEnum.CLIENT_TOKEN.value,
+                                         FeatureStringEnum.ORGANISM.value,
+                                         "action",
+                                         "controller",
+                                         "format"]
+
+    /**
+     * This is a public method, but is really used only internally.
+     *
+     * Loads the shared link and moves over:
+     * http://localhost:8080/apollo/annotator/loadLink?loc=chrII:302089..337445&organism=23357&highlight=0&tracklist=0&tracks=Reference%20sequence,User-created%20Annotations&clientToken=12312321
+     * @return
+     */
+    def loadLink() {
+        String clientToken
+        try {
+            if (params.containsKey(FeatureStringEnum.CLIENT_TOKEN.value)) {
+                clientToken = params[FeatureStringEnum.CLIENT_TOKEN.value]
+            } else {
+                clientToken = ClientTokenGenerator.generateRandomString()
+                log.debug 'generating client token on the backend: ' + clientToken
+            }
+            Organism organism
+            // check organism first
+            if (params.containsKey(FeatureStringEnum.ORGANISM.value)) {
+                String organismString = params[FeatureStringEnum.ORGANISM.value]
+                organism = preferenceService.getOrganismForTokenInDB(organismString)
+            }
+            organism = organism ?: preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+            def allowedOrganisms = permissionService.getOrganisms(permissionService.currentUser)
+            if (!allowedOrganisms) {
+                throw new RuntimeException("User does have permissions to access any organisms.")
+            }
+
+            if(params.uuid){
+                Feature feature = Feature.findByUniqueName(params.uuid)
+                FeatureLocation featureLocation = feature.featureLocation
+                params.loc = featureLocation.sequence.name + ":" + featureLocation.fmin +".." + featureLocation.fmax
+                organism = featureLocation.sequence.organism
+            }
+
+            if (!allowedOrganisms.contains(organism)) {
+                log.error("Can not load organism ${organism?.commonName} so loading ${allowedOrganisms.first().commonName} instead.")
+                params.loc = null
+                organism = allowedOrganisms.first()
+            }
+
+            log.debug "loading organism: ${organism}"
+            preferenceService.setCurrentOrganism(permissionService.currentUser, organism, clientToken)
+            if (params.loc) {
+                String location = params.loc
+                String[] splitString = location.split(":")
+                log.debug "splitString : ${splitString}"
+                String sequenceString = splitString[0]
+                Sequence sequence = Sequence.findByOrganismAndName(organism, sequenceString)
+                String[] minMax = splitString[1].split("\\.\\.")
+
+                log.debug "minMax: ${minMax}"
+                int fmin, fmax
+                try {
+                    fmin = minMax[0] as Integer
+                    fmax = minMax[1] as Integer
+                } catch (e) {
+                    log.error "error parsing ${e}"
+                    fmin = sequence.start
+                    fmax = sequence.end
+                }
+                log.debug "fmin ${fmin} . . fmax ${fmax} . . ${sequence}"
+
+                preferenceService.setCurrentSequenceLocation(sequence.name, fmin, fmax, clientToken)
+            }
+
+        } catch (e) {
+            log.error "problem parsing the string ${e}"
+        }
+
+        String queryParamString = ""
+        def keyList = []
+        // this fixes a bug in addStores being duplicated or processed incorrectly
+        for (p in params) {
+            // if "addStores is not being processed correclty, this will fix it
+            if (p.key.startsWith("addStores=")) {
+                if (!p.value) {
+                    queryParamString += "&" + p.key
+                    keyList << "addStores"
+                }
+            } else if (!reservedList.contains(p.key) && !keyList.contains(p.key)) {
+                queryParamString += "&" + p
+                keyList << p.key
+            }
+        }
+
+        // for some reason the addTracks requires the context path, which seems to be an obscure bug in grails
+        if (queryParamString.contains("addTracks")) {
+            redirect uri: "${request.contextPath}/annotator/index?clientToken=" + clientToken + queryParamString
+        } else {
+            redirect uri: "/annotator/index?clientToken=" + clientToken + queryParamString
+        }
+
+    }
+
+    /**
+     * Loads the main annotator panel.
+     */
+    @NotTransactional
+    def index() {
+        log.debug "loading the index"
+        String uuid = UUID.randomUUID().toString()
+        String clientToken = params.containsKey(FeatureStringEnum.CLIENT_TOKEN.value) ? params.get(FeatureStringEnum.CLIENT_TOKEN.value) : null
+        [userKey: uuid, clientToken: clientToken]
+    }
+
+    @NotTransactional
+    def getExtraTabs(){
+        def extraTabs = configWrapperService.extraTabs
+        render extraTabs as JSON
+    }
+
+    @NotTransactional
+    def adminPanel() {
+        if (permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)) {
+            def administativePanel = grailsApplication.config.apollo.administrativePanel
+            [links: administativePanel]
+        } else {
+            render text: "Unauthorized"
+        }
+    }
+
+    /**
+     * updates shallow properties of gene / feature
+     * @return
+     */
+    @RestApiMethod(description = "Update shallow feature properties", path = "/annotator/updateFeature", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "uniquename", type = "string", paramType = RestApiParamType.QUERY, description = "Uniquename (UUID) of the feature we are editing")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Updated feature name")
+            , @RestApiParam(name = "symbol", type = "string", paramType = RestApiParamType.QUERY, description = "Updated feature symbol")
+            , @RestApiParam(name = "description", type = "string", paramType = RestApiParamType.QUERY, description = "Updated feature description")
+    ]
+    )
+    @Transactional
+    def updateFeature() {
+        log.debug "updateFeature ${params.data}"
+        JSONObject data = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(data, PermissionEnum.WRITE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        Feature feature = Feature.findByUniqueName(data.uniquename)
+
+        feature.name = data.name
+        feature.symbol = data.symbol
+        feature.description = data.description
+
+        feature.save(flush: true, failOnError: true)
+
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        if (feature instanceof Gene) {
+            List<Feature> childFeatures = feature.parentFeatureRelationships*.childFeature
+            for (childFeature in childFeatures) {
+                JSONObject jsonFeature = featureService.convertFeatureToJSON(childFeature, false)
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(jsonFeature)
+            }
+        } else {
+            JSONObject jsonFeature = featureService.convertFeatureToJSON(feature, false)
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(jsonFeature)
+        }
+
+        Sequence sequence = feature?.featureLocation?.sequence
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: updateFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+                , sequenceAlterationEvent: false
+        )
+        requestHandlingService.fireAnnotationEvent(annotationEvent)
+
+        render updateFeatureContainer
+    }
+
+
+    @RestApiMethod(description = "Update exon boundaries", path = "/annotator/setExonBoundaries", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "uniquename", type = "string", paramType = RestApiParamType.QUERY, description = "Uniquename (UUID) of the exon we are editing")
+            , @RestApiParam(name = "fmin", type = "int", paramType = RestApiParamType.QUERY, description = "fmin for Exon Location")
+            , @RestApiParam(name = "fmax", type = "int", paramType = RestApiParamType.QUERY, description = "fmax for Exon Location")
+            , @RestApiParam(name = "strand", type = "int", paramType = RestApiParamType.QUERY, description = "strand for Feature Location 1 or -1")
+    ]
+    )
+    def setExonBoundaries() {
+        JSONObject data = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(data, PermissionEnum.WRITE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+
+        JSONObject jsonObject = new JSONObject()
+        JSONObject featureObject = new JSONObject()
+        JSONArray featuresArray = new JSONArray()
+        featureObject.put(FeatureStringEnum.UNIQUENAME.value, data.uniquename)
+        JSONObject featureLocationObject = new JSONObject()
+        featureLocationObject.put(FeatureStringEnum.FMIN.value, data.fmin)
+        featureLocationObject.put(FeatureStringEnum.FMAX.value, data.fmax)
+        featureLocationObject.put(FeatureStringEnum.STRAND.value, data.strand)
+        featureObject.put(FeatureStringEnum.LOCATION.value, featureLocationObject)
+
+        featuresArray.add(featureObject)
+
+        jsonObject.put("features", featuresArray)
+        jsonObject.put(FeatureStringEnum.CLIENT_TOKEN.value, data.clientToken)
+        jsonObject.put(FeatureStringEnum.TRACK.value, data.track)
+        jsonObject.put("operation", "set_exon_boundaries")
+        jsonObject.put(FeatureStringEnum.USERNAME.value, data.username)
+
+        return requestHandlingService.setExonBoundaries(jsonObject)
+    }
+
+    private JSONObject createJSONFeatureContainer(JSONObject... features) throws JSONException {
+        JSONObject jsonFeatureContainer = new JSONObject();
+        JSONArray jsonFeatures = new JSONArray();
+        jsonFeatureContainer.put(FeatureStringEnum.FEATURES.value, jsonFeatures);
+        for (JSONObject feature : features) {
+            jsonFeatures.put(feature);
+        }
+        return jsonFeatureContainer;
+    }
+
+    /**
+     * Not really setup for a REST service as is specific to the Annotator Panel interface.
+     * If a user has read permissions this method will work.
+     * @param sequenceName
+     * @param request
+     * @param annotationName
+     * @param type
+     * @param user
+     * @param offset
+     * @param max
+     * @param sortorder
+     * @param sort
+     * @return
+     */
+    def findAnnotationsForSequence(String sequenceName, String request, String annotationName, String type, String user, Integer offset, Integer max, String sortorder, String sort, String clientToken) {
+        try {
+            JSONObject returnObject = createJSONFeatureContainer()
+            returnObject.clientToken = clientToken
+            if (sequenceName && !Sequence.countByName(sequenceName)) return
+
+            if (sequenceName) {
+                returnObject.track = sequenceName
+            }
+
+            Sequence sequenceObj = permissionService.checkPermissions(returnObject, PermissionEnum.READ)
+            Organism organism = sequenceObj.organism
+            Integer index = Integer.parseInt(request)
+
+            List<String> viewableTypes
+
+            if (type) {
+                viewableTypes = new ArrayList<>()
+                switch (type) {
+                    case "Gene": viewableTypes.add(Gene.class.canonicalName)
+                        break
+                    case "Pseudogene": viewableTypes.add(Pseudogene.class.canonicalName)
+                        break
+                    case "repeat_region": viewableTypes.add(RepeatRegion.class.canonicalName)
+                        break
+                    case "transposable_element": viewableTypes.add(TransposableElement.class.canonicalName)
+                        break
+                    default:
+                        log.info "Type not found for annotation filter '${type}'"
+                        viewableTypes = requestHandlingService.viewableAnnotationList
+                        break
+                }
+            } else {
+                viewableTypes = requestHandlingService.viewableAnnotationList
+            }
+
+            long start = System.currentTimeMillis()
+            log.debug "${sort} ${sortorder}"
+
+            //use two step query. step 1 gets genes in a page
+            def pagination = Feature.createCriteria().list(max: max, offset: offset) {
+                featureLocations {
+                    if (sequenceName) {
+                        eq('sequence', sequenceObj)
+                    }
+                    if (sort == "length") {
+                        order('length', sortorder)
+                    }
+                    sequence {
+                        if (sort == "sequence") {
+                            order('name', sortorder)
+                        }
+                        eq('organism', organism)
+                    }
+                }
+                if (sort == "name") {
+                    order('name', sortorder)
+                }
+                if (sort == "date") {
+                    order('lastUpdated', sortorder)
+                }
+                if (annotationName) {
+                    ilike('name', '%' + annotationName + '%')
+                }
+                'in'('class', viewableTypes)
+            }
+
+            //step 2 does a distinct query with extra attributes added in
+            def features = pagination.size() == 0 ? [] : Feature.createCriteria().listDistinct {
+                'in'('id', pagination.collect { it.id })
+                featureLocations {
+                    if (sort == "length") {
+                        order('length', sortorder)
+                    }
+                    sequence {
+                        if (sort == "sequence") {
+                            order('name', sortorder)
+                        }
+                    }
+                }
+                if (sort == "name") {
+                    order('name', sortorder)
+                }
+                if (sort == "date") {
+                    order('lastUpdated', sortorder)
+                }
+                fetchMode 'owners', FetchMode.JOIN
+                fetchMode 'featureLocations', FetchMode.JOIN
+                fetchMode 'featureLocations.sequence', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.parentFeature', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.parentFeatureRelationships', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.parentFeatureRelationships.childFeature', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.parentFeatureRelationships.childFeature.featureLocations', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.parentFeatureRelationships.childFeature.featureLocations.sequence', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.childFeatureRelationships', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.featureLocations', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.featureLocations.sequence', FetchMode.JOIN
+                fetchMode 'parentFeatureRelationships.childFeature.owners', FetchMode.JOIN
+            }
+            long durationInMilliseconds = System.currentTimeMillis() - start;
+            log.debug "criteria query ${durationInMilliseconds}"
+
+            start = System.currentTimeMillis();
+            for (Feature feature in features) {
+                JSONObject featureObject = featureService.convertFeatureToJSONLite(feature, false, 0)
+                returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureObject)
+            }
+            durationInMilliseconds = System.currentTimeMillis() - start;
+            log.debug "convert to json ${durationInMilliseconds}"
+
+            returnObject.put(FeatureStringEnum.REQUEST_INDEX.getValue(), index + 1)
+            returnObject.put(FeatureStringEnum.ANNOTATION_COUNT.value, pagination.totalCount)
+
+            render returnObject
+        }
+        catch (PermissionException e) {
+            def error = [error: e.message]
+            log.warn "Permission exception: " + e.message
+            render error as JSON
+        }
+        catch (Exception e) {
+            def error = [error: e.message]
+            log.error e.message
+            e.printStackTrace()
+            render error as JSON
+        }
+
+    }
+
+    /**
+     * This is a public passthrough to version
+     */
+    def version() {}
+
+    /**
+     * This is a very specific method for the GWT interface.
+     * An additional method should be added.
+     *
+     * AnnotatorService.getAppState() throws an exception and returns an empty JSON string
+     * if the user has insufficient permissions.
+     */
+    @Transactional
+    def getAppState() {
+        preferenceService.evaluateSaves(true)
+        render annotatorService.getAppState(params.get(FeatureStringEnum.CLIENT_TOKEN.value).toString()) as JSON
+    }
+
+    /**
+     */
+    @Transactional
+    def setCurrentOrganism(Organism organismInstance) {
+        // set the current organism
+        preferenceService.setCurrentOrganism(permissionService.currentUser, organismInstance, params[FeatureStringEnum.CLIENT_TOKEN.value] as String)
+        session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, organismInstance.directory)
+
+        if (!permissionService.checkPermissions(PermissionEnum.READ)) {
+            flash.message = permissionService.getInsufficientPermissionMessage(PermissionEnum.READ)
+            redirect(uri: "/auth/login")
+            return
+        }
+
+        render annotatorService.getAppState(params[FeatureStringEnum.CLIENT_TOKEN.value] as String) as JSON
+    }
+
+    /**
+     */
+    @Transactional
+    def setCurrentSequence(Sequence sequenceInstance) {
+        if (!permissionService.checkPermissions(PermissionEnum.READ)) {
+            flash.message = permissionService.getInsufficientPermissionMessage(PermissionEnum.READ)
+            redirect(uri: "/auth/login")
+            return
+        }
+        // set the current organism and sequence Id (if both)
+        preferenceService.setCurrentSequence(permissionService.currentUser, sequenceInstance, params[FeatureStringEnum.CLIENT_TOKEN.value] as String)
+        session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, sequenceInstance.organism.directory)
+
+        render annotatorService.getAppState(params[FeatureStringEnum.CLIENT_TOKEN.value] as String) as JSON
+    }
+
+    def notAuthorized() {
+        log.error "not authorized"
+    }
+
+    /**
+     * Permissions handled upstream
+     * @param max
+     * @return
+     */
+    def report(Integer max) {
+        List<AnnotatorSummary> annotatorSummaryList = new ArrayList<>()
+        params.max = Math.min(max ?: 20, 100)
+
+        List<User> annotators = User.list(params)
+
+        annotators.each {
+            annotatorSummaryList.add(reportService.generateAnnotatorSummary(it, true))
+        }
+
+        render view: "report", model: [annotatorInstanceList: annotatorSummaryList, annotatorInstanceCount: User.count]
+    }
+
+    def detail(User user) {
+        if (!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)) {
+            flash.message = permissionService.getInsufficientPermissionMessage(PermissionEnum.ADMINISTRATE)
+            redirect(uri: "/auth/login")
+            return
+        }
+        render view: "detail", model: [annotatorInstance: reportService.generateAnnotatorSummary(user)]
+    }
+
+    def ping() {
+        log.debug "Ping: Evaluating Saves"
+        preferenceService.evaluateSaves()
+        if (permissionService.checkPermissions(PermissionEnum.READ)) {
+            log.debug("permissions checked and alive")
+            render new JSONObject() as JSON
+        } else {
+            log.error("User does not have permissions for the site")
+            redirect(uri: "/auth/login")
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/AuthController.groovy b/grails-app/controllers/org/bbop/apollo/AuthController.groovy
new file mode 100644
index 0000000..9100f8c
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/AuthController.groovy
@@ -0,0 +1,97 @@
+package org.bbop.apollo
+
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.AuthenticationException
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.web.util.SavedRequest
+import org.apache.shiro.web.util.WebUtils
+
+class AuthController {
+
+
+    def permissionService
+    def preferenceService
+
+    def index = { redirect(action: "login", params: params) }
+
+    def login = {
+        WebUtils.saveRequest(request)
+        return [ username: params.username, rememberMe: (params.rememberMe != null), targetUri: params.targetUri ]
+    }
+
+    def signIn = {
+        def authToken = new UsernamePasswordToken(params.username, params.password as String)
+
+        // Support for "remember me"
+        if (params.rememberMe) {
+            authToken.rememberMe = true
+        }
+        
+        // If a controller redirected to this page, redirect back
+        // to it. Otherwise redirect to the root URI.
+        def targetUri = params.targetUri ?: "/"
+        
+        // Handle requests saved by Shiro filters.
+        SavedRequest savedRequest = WebUtils.getSavedRequest(request)
+        if (savedRequest) {
+            if(savedRequest.queryString && savedRequest.queryString.startsWith("targetUri=")){
+                targetUri = savedRequest.queryString.substring("targetUri=".size())
+            }
+            else{
+                targetUri = savedRequest.requestURI - request.contextPath
+                if (savedRequest.queryString) {
+                    targetUri = targetUri + '?' + savedRequest.queryString
+                }
+            }
+        }
+        
+        try{
+            // Perform the actual login. An AuthenticationException
+            // will be thrown if the username is unrecognised or the
+            // password is incorrect.
+            permissionService.authenticateWithToken(authToken,request)
+//            SecurityUtils.subject.login(authToken)
+            if(targetUri){
+                log.info "Redirecting to '${targetUri}'."
+
+                redirect(uri: targetUri)
+                return
+            }
+        }
+        catch (AuthenticationException ex){
+            // Authentication failed, so display the appropriate message
+            // on the login page.
+            log.info "Authentication failure for user '${params.username}'."
+            flash.message = message(code: "login.failed")
+
+            // Keep the username and "remember me" setting so that the
+            // user doesn't have to enter them again.
+            def m = [ username: params.username ]
+            if (params.rememberMe) {
+                m["rememberMe"] = true
+            }
+
+            // Remember the target URI too.
+            if (params.targetUri) {
+                m["targetUri"] = params.targetUri
+            }
+
+            // Now redirect back to the login page.
+            redirect(action: "login", params: m)
+        }
+    }
+
+    def signOut = {
+        preferenceService.evaluateSaves(true)
+        // Log the user out of the application.
+        SecurityUtils.subject?.logout()
+        webRequest.getCurrentRequest().session = null
+
+        // For now, redirect back to the home page.
+        redirect(uri: "/")
+    }
+
+    def unauthorized = {
+//        render "You do not have permission to access this page."
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/AvailableStatusController.groovy b/grails-app/controllers/org/bbop/apollo/AvailableStatusController.groovy
new file mode 100644
index 0000000..805595f
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/AvailableStatusController.groovy
@@ -0,0 +1,329 @@
+package org.bbop.apollo
+
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import static org.springframework.http.HttpStatus.*
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+ at RestApi(name = "Available Status Services", description = "Methods for managing available statuses")
+ at Transactional(readOnly = true)
+class AvailableStatusController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+
+    def beforeInterceptor = {
+        if(!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)){
+            forward action: "notAuthorized", controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+        def availableStatuss = AvailableStatus.list(params)
+        def organismFilterMap = [:]
+        AvailableStatusOrganismFilter.findAllByAvailableStatusInList(availableStatuss).each() {
+            List filterList = organismFilterMap.containsKey(it.availableStatus) ? organismFilterMap.get(it.availableStatus) : []
+            filterList.add(it)
+            organismFilterMap[it.availableStatus] = filterList
+        }
+        respond availableStatuss, model: [availableStatusInstanceCount: AvailableStatus.count(), organismFilters: organismFilterMap]
+    }
+
+    def show(AvailableStatus availableStatusInstance) {
+        respond availableStatusInstance, model: [organismFilters: AvailableStatusOrganismFilter.findAllByAvailableStatus(availableStatusInstance)]
+    }
+
+    def create() {
+        respond new AvailableStatus(params)
+    }
+
+    @Transactional
+    def save(AvailableStatus availableStatusInstance) {
+        if (availableStatusInstance == null) {
+            notFound()
+            return
+        }
+
+        if (availableStatusInstance.hasErrors()) {
+            respond availableStatusInstance.errors, view:'create'
+            return
+        }
+
+
+        availableStatusInstance.save()
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new AvailableStatusOrganismFilter(
+                    organism: organism,
+                    availableStatus: availableStatusInstance
+            ).save()
+        }
+
+        availableStatusInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'availableStatus.label', default: 'AvailableStatus'), availableStatusInstance.id])
+                redirect availableStatusInstance
+            }
+            '*' { respond availableStatusInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(AvailableStatus availableStatusInstance) {
+        respond availableStatusInstance, model: [organismFilters: AvailableStatusOrganismFilter.findAllByAvailableStatus(availableStatusInstance)]
+    }
+
+    @Transactional
+    def update(AvailableStatus availableStatusInstance) {
+        if (availableStatusInstance == null) {
+            notFound()
+            return
+        }
+
+        if (availableStatusInstance.hasErrors()) {
+            respond availableStatusInstance.errors, view:'edit'
+            return
+        }
+
+        availableStatusInstance.save()
+
+        AvailableStatusOrganismFilter.deleteAll(AvailableStatusOrganismFilter.findAllByAvailableStatus(availableStatusInstance))
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new AvailableStatusOrganismFilter(
+                    organism: organism,
+                    availableStatus: availableStatusInstance
+            ).save()
+        }
+
+        availableStatusInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'AvailableStatus.label', default: 'AvailableStatus'), availableStatusInstance.id])
+                redirect availableStatusInstance
+            }
+            '*'{ respond availableStatusInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(AvailableStatus availableStatusInstance) {
+
+        if (availableStatusInstance == null) {
+            notFound()
+            return
+        }
+
+        availableStatusInstance.delete flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'AvailableStatus.label', default: 'AvailableStatus'), availableStatusInstance.id])
+                redirect action:"index", method:"GET"
+            }
+            '*'{ render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'availableStatus.label', default: 'AvailableStatus'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*'{ render status: NOT_FOUND }
+        }
+    }
+
+    @RestApiMethod(description = "Create status", path = "/availableStatus/createStatus", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "value", type = "string", paramType = RestApiParamType.QUERY, description = "Status name to add")
+    ]
+    )
+    @Transactional
+    def createStatus() {
+        JSONObject statusJson = permissionService.handleInput(request, params)
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(statusJson))) {
+                if (!statusJson.value) {
+                    throw new Exception('empty fields detected')
+                }
+
+                log.debug "Adding ${statusJson.value}"
+                AvailableStatus status = new AvailableStatus(
+                        value: statusJson.value
+                ).save(flush: true)
+
+                render status as JSON
+            } else {
+                def error = [error: 'not authorized to add AvailableStatus']
+                render error as JSON
+                log.error(error.error)
+            }
+        } catch (e) {
+            def error = [error: 'problem saving AvailableStatus: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Update status", path = "/availableStatus/updateStatus", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Status ID to update (or specify the old_value)")
+            , @RestApiParam(name = "old_value", type = "string", paramType = RestApiParamType.QUERY, description = "Status name to update")
+            , @RestApiParam(name = "new_value", type = "string", paramType = RestApiParamType.QUERY, description = "Status name to change to (the only editable option)")
+    ]
+    )
+    @Transactional
+    def updateStatus() {
+        try {
+            JSONObject statusJson = permissionService.handleInput(request, params)
+            log.debug "Updating status ${statusJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(statusJson))) {
+                if (!statusJson.new_value) {
+                    throw new Exception('empty fields detected')
+                }
+
+                log.debug "status ID: ${statusJson.id}"
+                AvailableStatus status = AvailableStatus.findById(statusJson.id) ?: AvailableStatus.findByValue(statusJson.old_value)
+
+                if (!status) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to update the status")
+                    render jsonObject as JSON
+                    return
+                }
+
+                status.value = statusJson.new_value
+                status.save(flush: true)
+
+                log.info "Success updating status: ${status.id}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to edit status']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem editing status: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Remove a status", path = "/availableStatus/deleteStatus", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Status ID to remove (or specify the name)")
+            , @RestApiParam(name = "value", type = "string", paramType = RestApiParamType.QUERY, description = "Status name to delete")
+    ])
+    @Transactional
+    def deleteStatus() {
+        try {
+            JSONObject statusJson = permissionService.handleInput(request, params)
+            log.debug "Deleting status ${statusJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(statusJson))) {
+
+                AvailableStatus status = AvailableStatus.findById(statusJson.id) ?: AvailableStatus.findByValue(statusJson.value)
+
+                if (!status) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the status")
+                    render jsonObject as JSON
+                    return
+                }
+
+                status.delete()
+
+                log.info "Success deleting status: ${statusJson}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to delete status']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem deleting status: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Returns a JSON array of all statuses, or optionally, gets information about a specific status", path = "/availableStatus/showStatus", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Status ID to show (or specify a name)")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Status name to show")
+    ])
+    @Transactional
+    def showStatus() {
+        try {
+            JSONObject statusJson = permissionService.handleInput(request, params)
+            log.debug "Showing status ${statusJson}"
+            if (!permissionService.hasGlobalPermissions(statusJson, PermissionEnum.ADMINISTRATE)) {
+                render status: UNAUTHORIZED
+                return
+            }
+
+            if (statusJson.id || statusJson.name) {
+                AvailableStatus status = AvailableStatus.findById(statusJson.id) ?: AvailableStatus.findByValue(statusJson.name)
+
+                if (!status) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the status")
+                    render jsonObject as JSON
+                    return
+                }
+
+                log.info "Success showing status: ${statusJson}"
+                render status as JSON
+            }
+            else {
+                def statuses = AvailableStatus.all
+
+                log.info "Success showing all statuses"
+                render statuses as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem showing statuses: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/CannedCommentController.groovy b/grails-app/controllers/org/bbop/apollo/CannedCommentController.groovy
new file mode 100644
index 0000000..f598d5c
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/CannedCommentController.groovy
@@ -0,0 +1,335 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+import static org.springframework.http.HttpStatus.*
+
+ at RestApi(name = "Canned Comments Services", description = "Methods for managing canned comments")
+ at Transactional(readOnly = true)
+class CannedCommentController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+
+    def beforeInterceptor = {
+        if (!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)) {
+            forward action: "notAuthorized", controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+        def cannedComments = CannedComment.list(params)
+        def organismFilterMap = [:]
+        CannedCommentOrganismFilter.findAllByCannedCommentInList(cannedComments).each() {
+            List filterList = organismFilterMap.containsKey(it.cannedComment) ? organismFilterMap.get(it.cannedComment) : []
+            filterList.add(it)
+            organismFilterMap[it.cannedComment] = filterList
+        }
+        respond cannedComments, model: [cannedCommentInstanceCount: CannedComment.count(), organismFilters: organismFilterMap]
+    }
+
+    def show(CannedComment cannedCommentInstance) {
+        respond cannedCommentInstance, model: [organismFilters: CannedCommentOrganismFilter.findAllByCannedComment(cannedCommentInstance)]
+    }
+
+    def create() {
+        respond new CannedComment(params)
+    }
+
+    @Transactional
+    def save(CannedComment cannedCommentInstance) {
+        if (cannedCommentInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedCommentInstance.hasErrors()) {
+            respond cannedCommentInstance.errors, view: 'create'
+            return
+        }
+
+
+        cannedCommentInstance.save()
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedCommentOrganismFilter(
+                    organism: organism,
+                    cannedComment: cannedCommentInstance
+            ).save()
+        }
+
+        cannedCommentInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'cannedComment.label', default: 'CannedComment'), cannedCommentInstance.id])
+                redirect cannedCommentInstance
+            }
+            '*' { respond cannedCommentInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(CannedComment cannedCommentInstance) {
+        respond cannedCommentInstance, model: [organismFilters: CannedCommentOrganismFilter.findAllByCannedComment(cannedCommentInstance)]
+    }
+
+    @Transactional
+    def update(CannedComment cannedCommentInstance) {
+        if (cannedCommentInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedCommentInstance.hasErrors()) {
+            respond cannedCommentInstance.errors, view: 'edit'
+            return
+        }
+
+        cannedCommentInstance.save()
+
+        CannedCommentOrganismFilter.deleteAll(CannedCommentOrganismFilter.findAllByCannedComment(cannedCommentInstance))
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedCommentOrganismFilter(
+                    organism: organism,
+                    cannedComment: cannedCommentInstance
+            ).save()
+        }
+
+        cannedCommentInstance.save(flush: true)
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'CannedComment.label', default: 'CannedComment'), cannedCommentInstance.id])
+                redirect cannedCommentInstance
+            }
+            '*' { respond cannedCommentInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(CannedComment cannedCommentInstance) {
+
+        if (cannedCommentInstance == null) {
+            notFound()
+            return
+        }
+
+        cannedCommentInstance.delete flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'CannedComment.label', default: 'CannedComment'), cannedCommentInstance.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'cannedComment.label', default: 'CannedComment'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NOT_FOUND }
+        }
+    }
+
+    @RestApiMethod(description = "Create canned comment", path = "/cannedComment/createComment", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "comment", type = "string", paramType = RestApiParamType.QUERY, description = "Canned comment to add")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def createComment() {
+        JSONObject commentJson = permissionService.handleInput(request, params)
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(commentJson))) {
+                if (!commentJson.comment) {
+                    throw new Exception('empty fields detected')
+                }
+
+                if (!commentJson.metadata) {
+                    commentJson.metadata = ""
+                }
+
+                log.debug "Adding canned comment ${commentJson.comment}"
+                CannedComment comment = new CannedComment(
+                        comment: commentJson.comment,
+                        metadata: commentJson.metadata
+                ).save(flush: true)
+
+                render comment as JSON
+            } else {
+                def error = [error: 'not authorized to add CannedComment']
+                render error as JSON
+                log.error(error.error)
+            }
+        } catch (e) {
+            def error = [error: 'problem saving CannedComment: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Update canned comment", path = "/cannedComment/updateComment", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned comment ID to update (or specify the old_comment)")
+            , @RestApiParam(name = "old_comment", type = "string", paramType = RestApiParamType.QUERY, description = "Canned comment to update")
+            , @RestApiParam(name = "new_comment", type = "string", paramType = RestApiParamType.QUERY, description = "Canned comment to change to (the only editable option)")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def updateComment() {
+        try {
+            JSONObject commentJson = permissionService.handleInput(request, params)
+            log.debug "Updating canned comment ${commentJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(commentJson))) {
+
+                log.debug "Canned comment ID: ${commentJson.id}"
+                CannedComment comment = CannedComment.findById(commentJson.id) ?: CannedComment.findByComment(commentJson.old_comment)
+
+                if (!comment) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to update the canned comment")
+                    render jsonObject as JSON
+                    return
+                }
+
+                comment.comment = commentJson.new_comment
+
+                if (commentJson.metadata) {
+                    comment.metadata = commentJson.metadata
+                }
+
+                comment.save(flush: true)
+
+                log.info "Success updating canned comment: ${comment.id}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to edit canned comment']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem editing canned comment: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Remove a canned comment", path = "/cannedComment/deleteComment", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned comment ID to remove (or specify the name)")
+            , @RestApiParam(name = "comment", type = "string", paramType = RestApiParamType.QUERY, description = "Canned comment to delete")
+    ])
+    @Transactional
+    def deleteComment() {
+        try {
+            JSONObject commentJson = permissionService.handleInput(request, params)
+            log.debug "Deleting canned comment ${commentJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(commentJson))) {
+
+                CannedComment comment = CannedComment.findById(commentJson.id) ?: CannedComment.findByComment(commentJson.comment)
+
+                if (!comment) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned comment")
+                    render jsonObject as JSON
+                    return
+                }
+
+                comment.delete()
+
+                log.info "Success deleting canned comment: ${commentJson}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to delete canned comment']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem deleting canned comment: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Returns a JSON array of all canned comments, or optionally, gets information about a specific canned comment", path = "/cannedComment/showComment", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Comment ID to show (or specify a comment)")
+            , @RestApiParam(name = "comment", type = "string", paramType = RestApiParamType.QUERY, description = "Comment to show")
+    ])
+    @Transactional
+    def showComment() {
+        try {
+            JSONObject commentJson = permissionService.handleInput(request, params)
+            log.debug "Showing canned comment ${commentJson}"
+            if (!permissionService.hasGlobalPermissions(commentJson, PermissionEnum.ADMINISTRATE)) {
+                render status: UNAUTHORIZED
+                return
+            }
+
+            if (commentJson.id || commentJson.comment) {
+                CannedComment comment = CannedComment.findById(commentJson.id) ?: CannedComment.findByComment(commentJson.comment)
+
+                if (!comment) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned comments")
+                    render jsonObject as JSON
+                    return
+                }
+
+                log.info "Success showing comment: ${commentJson}"
+                render comment as JSON
+            } else {
+                def comments = CannedComment.all
+
+                log.info "Success showing all canned comments"
+                render comments as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem showing canned comments: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/CannedKeyController.groovy b/grails-app/controllers/org/bbop/apollo/CannedKeyController.groovy
new file mode 100644
index 0000000..049e84f
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/CannedKeyController.groovy
@@ -0,0 +1,337 @@
+package org.bbop.apollo
+
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import static org.springframework.http.HttpStatus.*
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+ at RestApi(name = "Canned Keys Services", description = "Methods for managing canned keys")
+ at Transactional(readOnly = true)
+class CannedKeyController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+
+    def beforeInterceptor = {
+        if(!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)){
+            forward action: "notAuthorized", controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+
+        def cannedKeys = CannedKey.list(params)
+        def organismFilterMap = [:]
+        CannedKeyOrganismFilter.findAllByCannedKeyInList(cannedKeys).each() {
+            List filterList = organismFilterMap.containsKey(it.cannedKey) ? organismFilterMap.get(it.cannedKey) : []
+            filterList.add(it)
+            organismFilterMap[it.cannedKey] = filterList
+        }
+        respond cannedKeys, model: [cannedKeyInstanceCount: CannedKey.count(), organismFilters: organismFilterMap]
+    }
+
+    def show(CannedKey cannedKeyInstance) {
+        respond cannedKeyInstance, model: [organismFilters: CannedKeyOrganismFilter.findAllByCannedKey(cannedKeyInstance)]
+    }
+
+    def create() {
+        respond new CannedKey(params)
+    }
+
+    @Transactional
+    def save(CannedKey cannedKeyInstance) {
+        if (cannedKeyInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedKeyInstance.hasErrors()) {
+            respond cannedKeyInstance.errors, view:'create'
+            return
+        }
+
+        cannedKeyInstance.save()
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedKeyOrganismFilter(
+                    organism: organism,
+                    cannedKey: cannedKeyInstance
+            ).save()
+        }
+
+        cannedKeyInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'cannedKey.label', default: 'CannedKey'), cannedKeyInstance.id])
+                redirect cannedKeyInstance
+            }
+            '*' { respond cannedKeyInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(CannedKey cannedKeyInstance) {
+        respond cannedKeyInstance
+    }
+
+    @Transactional
+    def update(CannedKey cannedKeyInstance) {
+        if (cannedKeyInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedKeyInstance.hasErrors()) {
+            respond cannedKeyInstance.errors, view:'edit'
+            return
+        }
+
+        cannedKeyInstance.save()
+
+        CannedKeyOrganismFilter.deleteAll(CannedKeyOrganismFilter.findAllByCannedKey(cannedKeyInstance))
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedKeyOrganismFilter(
+                    organism: organism,
+                    cannedKey: cannedKeyInstance
+            ).save()
+        }
+
+        cannedKeyInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'CannedKey.label', default: 'CannedKey'), cannedKeyInstance.id])
+                redirect cannedKeyInstance
+            }
+            '*'{ respond cannedKeyInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(CannedKey cannedKeyInstance) {
+
+        if (cannedKeyInstance == null) {
+            notFound()
+            return
+        }
+
+        cannedKeyInstance.delete flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'CannedKey.label', default: 'CannedKey'), cannedKeyInstance.id])
+                redirect action:"index", method:"GET"
+            }
+            '*'{ render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'cannedKey.label', default: 'CannedKey'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*'{ render status: NOT_FOUND }
+        }
+    }
+
+    @RestApiMethod(description = "Create canned key", path = "/cannedKey/createKey", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "key", type = "string", paramType = RestApiParamType.QUERY, description = "Canned key to add")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def createKey() {
+        JSONObject keyJson = permissionService.handleInput(request, params)
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(keyJson))) {
+                if (!keyJson.key) {
+                    throw new Exception('empty fields detected')
+                }
+
+                if (!keyJson.metadata) {
+                    keyJson.metadata = ""
+                }
+
+                log.debug "Adding canned key ${keyJson.key}"
+                CannedKey key = new CannedKey(
+                        label: keyJson.key,
+                        metadata: keyJson.metadata
+                ).save(flush: true)
+
+                render key as JSON
+            } else {
+                def error = [error: 'not authorized to add CannedKey']
+                render error as JSON
+                log.error(error.error)
+            }
+        } catch (e) {
+            def error = [error: 'problem saving CannedKey: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Update canned key", path = "/cannedKey/updateKey", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned key ID to update (or specify the old_key)")
+            , @RestApiParam(name = "old_key", type = "string", paramType = RestApiParamType.QUERY, description = "Canned key to update")
+            , @RestApiParam(name = "new_key", type = "string", paramType = RestApiParamType.QUERY, description = "Canned key to change to (the only editable option)")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def updateKey() {
+        try {
+            JSONObject keyJson = permissionService.handleInput(request, params)
+            log.debug "Updating canned key ${keyJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(keyJson))) {
+
+                log.debug "Canned key ID: ${keyJson.id}"
+                CannedKey key = CannedKey.findById(keyJson.id) ?: CannedKey.findByLabel(keyJson.old_key)
+
+                if (!key) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to update the canned key")
+                    render jsonObject as JSON
+                    return
+                }
+
+                key.label = keyJson.new_key
+
+                if (keyJson.metadata) {
+                    key.metadata = keyJson.metadata
+                }
+
+                key.save(flush: true)
+
+                log.info "Success updating canned key: ${key.id}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to edit canned key']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem editing canned key: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Remove a canned key", path = "/cannedKey/deleteKey", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned key ID to remove (or specify the name)")
+            , @RestApiParam(name = "key", type = "string", paramType = RestApiParamType.QUERY, description = "Canned key to delete")
+    ])
+    @Transactional
+    def deleteKey() {
+        try {
+            JSONObject keyJson = permissionService.handleInput(request, params)
+            log.debug "Deleting canned key ${keyJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(keyJson))) {
+
+                CannedKey key = CannedKey.findById(keyJson.id) ?: CannedKey.findByLabel(keyJson.key)
+
+                if (!key) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned key")
+                    render jsonObject as JSON
+                    return
+                }
+
+                key.delete()
+
+                log.info "Success deleting canned key: ${keyJson}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to delete canned key']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem deleting canned key: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Returns a JSON array of all canned keys, or optionally, gets information about a specific canned key", path = "/cannedKey/showKey", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Key ID to show (or specify a key)")
+            , @RestApiParam(name = "key", type = "string", paramType = RestApiParamType.QUERY, description = "Key to show")
+    ])
+    @Transactional
+    def showKey() {
+        try {
+            JSONObject keyJson = permissionService.handleInput(request, params)
+            log.debug "Showing canned key ${keyJson}"
+            if (!permissionService.hasGlobalPermissions(keyJson, PermissionEnum.ADMINISTRATE)) {
+                render status: UNAUTHORIZED
+                return
+            }
+
+            if (keyJson.id || keyJson.key) {
+                CannedKey key = CannedKey.findById(keyJson.id) ?: CannedKey.findByLabel(keyJson.old_key)
+
+                if (!key) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned keys")
+                    render jsonObject as JSON
+                    return
+                }
+
+                log.info "Success showing key: ${keyJson}"
+                render key as JSON
+            }
+            else {
+                def keys = CannedKey.all
+
+                log.info "Success showing all canned keys"
+                render keys as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem showing canned keys: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/CannedValueController.groovy b/grails-app/controllers/org/bbop/apollo/CannedValueController.groovy
new file mode 100644
index 0000000..304865f
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/CannedValueController.groovy
@@ -0,0 +1,334 @@
+package org.bbop.apollo
+
+
+import grails.converters.JSON
+import static org.springframework.http.HttpStatus.*
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+ at RestApi(name = "Canned Values Services", description = "Methods for managing canned values")
+ at Transactional(readOnly = true)
+class CannedValueController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+
+    def beforeInterceptor = {
+        if(!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)){
+            forward action: "notAuthorized", controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+        def cannedValues = CannedValue.list(params)
+        def organismFilterMap = [:]
+        CannedValueOrganismFilter.findAllByCannedValueInList(cannedValues).each() {
+            List filterList = organismFilterMap.containsValue(it.cannedValue) ? organismFilterMap.get(it.cannedValue) : []
+            filterList.add(it)
+            organismFilterMap[it.cannedValue] = filterList
+        }
+        respond cannedValues, model: [cannedValueInstanceCount: CannedValue.count(), organismFilters: organismFilterMap]
+    }
+
+    def show(CannedValue cannedValueInstance) {
+        respond cannedValueInstance, model: [organismFilters: CannedValueOrganismFilter.findAllByCannedValue(cannedValueInstance)]
+    }
+
+    def create() {
+        respond new CannedValue(params)
+    }
+
+    @Transactional
+    def save(CannedValue cannedValueInstance) {
+        if (cannedValueInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedValueInstance.hasErrors()) {
+            respond cannedValueInstance.errors, view:'create'
+            return
+        }
+
+        cannedValueInstance.save()
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedValueOrganismFilter(
+                    organism: organism,
+                    cannedValue: cannedValueInstance
+            ).save()
+        }
+
+        cannedValueInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'cannedValue.label', default: 'CannedValue'), cannedValueInstance.id])
+                redirect cannedValueInstance
+            }
+            '*' { respond cannedValueInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(CannedValue cannedValueInstance) {
+        respond cannedValueInstance
+    }
+
+    @Transactional
+    def update(CannedValue cannedValueInstance) {
+        if (cannedValueInstance == null) {
+            notFound()
+            return
+        }
+
+        if (cannedValueInstance.hasErrors()) {
+            respond cannedValueInstance.errors, view:'edit'
+            return
+        }
+
+        cannedValueInstance.save()
+
+        CannedValueOrganismFilter.deleteAll(CannedValueOrganismFilter.findAllByCannedValue(cannedValueInstance))
+
+        if (params.organisms instanceof String) {
+            params.organisms = [params.organisms]
+        }
+
+        params?.organisms.each {
+            Organism organism = Organism.findById(it)
+            new CannedValueOrganismFilter(
+                    organism: organism,
+                    cannedValue: cannedValueInstance
+            ).save()
+        }
+
+        cannedValueInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'CannedValue.label', default: 'CannedValue'), cannedValueInstance.id])
+                redirect cannedValueInstance
+            }
+            '*'{ respond cannedValueInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(CannedValue cannedValueInstance) {
+
+        if (cannedValueInstance == null) {
+            notFound()
+            return
+        }
+
+        cannedValueInstance.delete flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'CannedValue.label', default: 'CannedValue'), cannedValueInstance.id])
+                redirect action:"index", method:"GET"
+            }
+            '*'{ render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'cannedValue.label', default: 'CannedValue'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*'{ render status: NOT_FOUND }
+        }
+    }
+
+    @RestApiMethod(description = "Create canned value", path = "/cannedValue/createValue", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "value", type = "string", paramType = RestApiParamType.QUERY, description = "Canned value to add")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def createValue() {
+        JSONObject valueJson = permissionService.handleInput(request, params)
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(valueJson))) {
+                if (!valueJson.value) {
+                    throw new Exception('empty fields detected')
+                }
+
+                if (!valueJson.metadata) {
+                    valueJson.metadata = ""
+                }
+
+                log.debug "Adding canned value ${valueJson.value}"
+                CannedValue value = new CannedValue(
+                        label: valueJson.value,
+                        metadata: valueJson.metadata
+                ).save(flush: true)
+
+                render value as JSON
+            } else {
+                def error = [error: 'not authorized to add CannedValue']
+                render error as JSON
+                log.error(error.error)
+            }
+        } catch (e) {
+            def error = [error: 'problem saving CannedValue: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Update canned value", path = "/cannedValue/updateValue", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned value ID to update (or specify the old_value)")
+            , @RestApiParam(name = "old_value", type = "string", paramType = RestApiParamType.QUERY, description = "Canned value to update")
+            , @RestApiParam(name = "new_value", type = "string", paramType = RestApiParamType.QUERY, description = "Canned value to change to (the only editable option)")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "Optional additional information")
+    ]
+    )
+    @Transactional
+    def updateValue() {
+        try {
+            JSONObject valueJson = permissionService.handleInput(request, params)
+            log.debug "Updating canned value ${valueJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(valueJson))) {
+
+                log.debug "Canned value ID: ${valueJson.id}"
+                CannedValue value = CannedValue.findById(valueJson.id) ?: CannedValue.findByLabel(valueJson.old_value)
+
+                if (!value) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to update the canned value")
+                    render jsonObject as JSON
+                    return
+                }
+
+                value.label = valueJson.new_value
+
+                if (valueJson.metadata) {
+                    value.metadata = valueJson.metadata
+                }
+
+                value.save(flush: true)
+
+                log.info "Success updating canned value: ${value.id}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to edit canned value']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem editing canned value: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Remove a canned value", path = "/cannedValue/deleteValue", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Canned value ID to remove (or specify the name)")
+            , @RestApiParam(name = "value", type = "string", paramType = RestApiParamType.QUERY, description = "Canned value to delete")
+    ])
+    @Transactional
+    def deleteValue() {
+        try {
+            JSONObject valueJson = permissionService.handleInput(request, params)
+            log.debug "Deleting canned value ${valueJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(valueJson))) {
+
+                CannedValue value = CannedValue.findById(valueJson.id) ?: CannedValue.findByLabel(valueJson.value)
+
+                if (!value) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned value")
+                    render jsonObject as JSON
+                    return
+                }
+
+                value.delete()
+
+                log.info "Success deleting canned value: ${valueJson}"
+                render new JSONObject() as JSON
+            } else {
+                def error = [error: 'not authorized to delete canned value']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem deleting canned value: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Returns a JSON array of all canned values, or optionally, gets information about a specific canned value", path = "/cannedValue/showValue", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Value ID to show (or specify a value)")
+            , @RestApiParam(name = "value", type = "string", paramType = RestApiParamType.QUERY, description = "Value to show")
+    ])
+    @Transactional
+    def showValue() {
+        try {
+            JSONObject valueJson = permissionService.handleInput(request, params)
+            log.debug "Showing canned value ${valueJson}"
+            if (!permissionService.hasGlobalPermissions(valueJson, PermissionEnum.ADMINISTRATE)) {
+                render status: UNAUTHORIZED
+                return
+            }
+
+            if (valueJson.id || valueJson.value) {
+                CannedValue value = CannedValue.findById(valueJson.id) ?: CannedValue.findByLabel(valueJson.old_value)
+
+                if (!value) {
+                    JSONObject jsonObject = new JSONObject()
+                    jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the canned values")
+                    render jsonObject as JSON
+                    return
+                }
+
+                log.info "Success showing value: ${valueJson}"
+                render value as JSON
+            }
+            else {
+                def values = CannedValue.all
+
+                log.info "Success showing all canned values"
+                render values as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem showing canned values: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/FeatureEventController.groovy b/grails-app/controllers/org/bbop/apollo/FeatureEventController.groovy
new file mode 100644
index 0000000..cee217e
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/FeatureEventController.groovy
@@ -0,0 +1,301 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+import static org.springframework.http.HttpStatus.*
+
+ at Transactional(readOnly = true)
+class FeatureEventController {
+
+    static final String DAY_DATE_FORMAT = 'yyyy-MM-dd'
+    static final String FULL_DATE_FORMAT = DAY_DATE_FORMAT + ' HH:mm:ss'
+
+    def requestHandlingService
+    def permissionService
+
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    /**
+     * Returns a JSON representation of all "current" Genome Annotations before or after a given date.
+     *
+     * @param date
+     * @param beforeDate
+     * @return
+     */
+    @RestApiMethod(description = "Returns a JSON representation of all current Annotations before or after a given date.", path = "/featureEvent/findChanges", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "date", type = "Date", paramType = RestApiParamType.QUERY, description = "Date to query yyyy-MM-dd:HH:mm:ss or yyyy-MM-dd")
+            , @RestApiParam(name = "afterDate", type = "Boolean", paramType = RestApiParamType.QUERY, description = "Search after or on the given date.")
+            , @RestApiParam(name = "beforeDate", type = "Boolean", paramType = RestApiParamType.QUERY, description = "Search before or on the given date.")
+            , @RestApiParam(name = "max", type = "Integer", paramType = RestApiParamType.QUERY, description = "Max to return")
+            , @RestApiParam(name = "sort", type = "String", paramType = RestApiParamType.QUERY, description = "Sort parameter (lastUpdated).  See FeatureEvent object/table.")
+            , @RestApiParam(name = "order", type = "String", paramType = RestApiParamType.QUERY, description = "desc/asc sort order by sort param")
+    ])
+    def findChanges() {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(inputObject, org.bbop.apollo.gwt.shared.PermissionEnum.ADMINISTRATE)) {
+            render status: org.springframework.http.HttpStatus.UNAUTHORIZED
+            return
+        }
+        String date = inputObject.date
+        Boolean afterDate = inputObject.afterDate
+        Date compareDate = Date.parse(date.contains(":") ? FULL_DATE_FORMAT : DAY_DATE_FORMAT, date)
+        params.max = params.max ?: 200
+
+        def c = FeatureEvent.createCriteria()
+
+        def list = c.list(max: params.max, offset: params.offset) {
+            eq('current', true)
+            if (afterDate) {
+                ge('lastUpdated', compareDate)
+            } else {
+                le('lastUpdated', compareDate)
+            }
+            order(params.sort ?: "lastUpdated", params.order ?: "desc")
+        }
+
+        JSONArray returnList = new JSONArray()
+
+        list.each { FeatureEvent featureEvent ->
+            JSONArray entry = JSON.parse(featureEvent.newFeaturesJsonArray) as JSONArray
+            returnList.add(entry)
+        }
+
+        render returnList as JSON
+    }
+
+    /**
+     * Permissions handled upstream
+     * @param max
+     * @return
+     */
+    def report(Integer max) {
+
+        params.max = Math.min(max ?: 15, 100)
+
+        def c = Feature.createCriteria()
+
+        def list = c.list(max: params.max, offset: params.offset) {
+            if (params.sort == "owners") {
+                owners {
+                    order('username', params.order)
+                }
+            }
+            if (params.sort == "sequencename") {
+                featureLocations {
+                    sequence {
+                        order('name', params.order)
+                    }
+                }
+            } else if (params.sort == "name") {
+                order('name', params.order)
+            } else if (params.sort == "cvTerm") {
+                order('class', params.order)
+            } else if (params.sort == "organism") {
+                featureLocations {
+                    sequence {
+                        organism {
+                            order('commonName', params.order)
+                        }
+                    }
+                }
+            }
+            else if (params.sort == "lastUpdated") {
+                order('lastUpdated', params.order)
+            }
+            else if (params.sort == "dateCreated") {
+                order('dateCreated', params.order)
+            }
+
+            if (params.ownerName && params.ownerName != "null") {
+                owners {
+                    ilike('username', '%' + params.ownerName + '%')
+                }
+            }
+            if (params.featureType && params.featureType != "null") {
+                ilike('class', '%' + params.featureType)
+            }
+            if (params.organismName && params.organismName != "null") {
+                featureLocations {
+                    sequence {
+                        organism {
+                            eq('commonName', params.organismName)
+                        }
+                    }
+                }
+            }
+            if (params.sequenceName && params.sequenceName != "null") {
+                featureLocations {
+                    sequence {
+                        ilike('name', '%' + params.sequenceName + '%')
+                    }
+                }
+            }
+
+            if (params.afterDate) {
+                Calendar calendar = GregorianCalendar.getInstance()
+                calendar.setTime(params.afterDate)
+                calendar.set(Calendar.HOUR,0)
+                calendar.set(Calendar.MINUTE,0)
+                calendar.set(Calendar.SECOND,0)
+                gte('lastUpdated', calendar.getTime())
+            }
+            if (params.beforeDate) {
+                Date beforeDate = params.beforeDate
+                // set the before date to the very end of day
+                Calendar calendar = GregorianCalendar.getInstance()
+                calendar.setTime(params.beforeDate)
+                calendar.set(Calendar.HOUR,23)
+                calendar.set(Calendar.MINUTE,59)
+                calendar.set(Calendar.SECOND,59)
+                lte('lastUpdated', calendar.getTime())
+            }
+            log.debug "afterDateDate ${params.afterDateDate}"
+            log.debug "beforeDate ${params.beforeDate}"
+
+
+            if (params.dateCreatedAfterDate) {
+                Calendar calendar = GregorianCalendar.getInstance()
+                calendar.setTime(params.dateCreatedAfterDate)
+                calendar.set(Calendar.HOUR,0)
+                calendar.set(Calendar.MINUTE,0)
+                calendar.set(Calendar.SECOND,0)
+                gte('dateCreated', calendar.getTime())
+            }
+            if (params.dateCreatedBeforeDate) {
+                Date dateCreatedBeforeDate = params.dateCreatedBeforeDate
+                // set the before date to the very end of day
+                Calendar calendar = GregorianCalendar.getInstance()
+                calendar.setTime(params.dateCreatedBeforeDate)
+                calendar.set(Calendar.HOUR,23)
+                calendar.set(Calendar.MINUTE,59)
+                calendar.set(Calendar.SECOND,59)
+                lte('dateCreated', calendar.getTime())
+            }
+            log.debug "dateCreatedAfterDateDate ${params.dateCreatedAfterDateDate}"
+            log.debug "dateCreatedBeforeDate ${params.dateCreatedBeforeDate}"
+
+
+            'in'('class', requestHandlingService.viewableAnnotationList)
+        }
+
+        def filters = [organismName: params.organismName, featureType: params.featureType, ownerName: params.ownerName]
+
+        def featureTypes = []
+        RequestHandlingService.viewableAnnotationTypesList.each() {
+            featureTypes << it.substring(it.lastIndexOf(".") + 1)
+        }.sort()
+
+        Date today = new Date()
+        Date veryOldDate = today.minus(20 * 365)  // 20 years back
+        Date beforeDate = params.beforeDate ?: today
+        Date afterDate = params.afterDate ?: veryOldDate
+        Date dateCreatedBeforeDate = params.dateCreatedBeforeDate ?: today
+        Date dateCreatedAfterDate = params.dateCreatedAfterDate ?: veryOldDate
+
+        render view: "report", model: [dateCreatedAfterDate: dateCreatedAfterDate, dateCreatedBeforeDate: dateCreatedBeforeDate,afterDate: afterDate, beforeDate: beforeDate, sequenceName: params.sequenceName, features: list, featureCount: list.totalCount, organismName: params.organismName, featureTypes: featureTypes, featureType: params.featureType, ownerName: params.ownerName, filters: filters, sort: params.sort]
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+        respond FeatureEvent.list(params), model: [featureEventInstanceCount: FeatureEvent.count()]
+    }
+
+    def show(FeatureEvent featureEventInstance) {
+        respond featureEventInstance
+    }
+
+    def create() {
+        respond new FeatureEvent(params)
+    }
+
+    @Transactional
+    def save(FeatureEvent featureEventInstance) {
+        if (featureEventInstance == null) {
+            notFound()
+            return
+        }
+
+        if (featureEventInstance.hasErrors()) {
+            respond featureEventInstance.errors, view: 'create'
+            return
+        }
+
+        featureEventInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'featureEvent.label', default: 'FeatureEvent'), featureEventInstance.id])
+                redirect featureEventInstance
+            }
+            '*' { respond featureEventInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(FeatureEvent featureEventInstance) {
+        respond featureEventInstance
+    }
+
+    @Transactional
+    def update(FeatureEvent featureEventInstance) {
+        if (featureEventInstance == null) {
+            notFound()
+            return
+        }
+
+        if (featureEventInstance.hasErrors()) {
+            respond featureEventInstance.errors, view: 'edit'
+            return
+        }
+
+        featureEventInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'FeatureEvent.label', default: 'FeatureEvent'), featureEventInstance.id])
+                redirect featureEventInstance
+            }
+            '*' { respond featureEventInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(FeatureEvent featureEventInstance) {
+
+        if (featureEventInstance == null) {
+            notFound()
+            return
+        }
+
+        featureEventInstance.delete flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'FeatureEvent.label', default: 'FeatureEvent'), featureEventInstance.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'featureEvent.label', default: 'FeatureEvent'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NOT_FOUND }
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/FeatureTypeController.groovy b/grails-app/controllers/org/bbop/apollo/FeatureTypeController.groovy
new file mode 100644
index 0000000..34d2ac3
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/FeatureTypeController.groovy
@@ -0,0 +1,113 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.gwt.shared.PermissionEnum
+
+import static org.springframework.http.HttpStatus.*
+import grails.transaction.Transactional
+
+ at Transactional(readOnly = true)
+class FeatureTypeController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+
+    def beforeInterceptor = {
+        if(!permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)){
+            forward action: "notAuthorized" ,controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 30, 200)
+        respond FeatureType.list(params), model:[featureTypeInstanceCount: FeatureType.count()]
+    }
+
+    def show(FeatureType featureTypeInstance) {
+        respond featureTypeInstance
+    }
+
+    def create() {
+        respond new FeatureType(params)
+    }
+
+    @Transactional
+    def save(FeatureType featureTypeInstance) {
+        if (featureTypeInstance == null) {
+            notFound()
+            return
+        }
+
+        if (featureTypeInstance.hasErrors()) {
+            respond featureTypeInstance.errors, view:'create'
+            return
+        }
+
+        featureTypeInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'featureType.label', default: 'FeatureType'), featureTypeInstance.id])
+                redirect featureTypeInstance
+            }
+            '*' { respond featureTypeInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(FeatureType featureTypeInstance) {
+        respond featureTypeInstance
+    }
+
+    @Transactional
+    def update(FeatureType featureTypeInstance) {
+        if (featureTypeInstance == null) {
+            notFound()
+            return
+        }
+
+        if (featureTypeInstance.hasErrors()) {
+            respond featureTypeInstance.errors, view:'edit'
+            return
+        }
+
+        featureTypeInstance.save flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'FeatureType.label', default: 'FeatureType'), featureTypeInstance.id])
+                redirect featureTypeInstance
+            }
+            '*'{ respond featureTypeInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(FeatureType featureTypeInstance) {
+
+        if (featureTypeInstance == null) {
+            notFound()
+            return
+        }
+
+        featureTypeInstance.delete flush:true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'FeatureType.label', default: 'FeatureType'), featureTypeInstance.id])
+                redirect action:"index", method:"GET"
+            }
+            '*'{ render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'featureType.label', default: 'FeatureType'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*'{ render status: NOT_FOUND }
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/GroupController.groovy b/grails-app/controllers/org/bbop/apollo/GroupController.groovy
new file mode 100644
index 0000000..07cf5b7
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/GroupController.groovy
@@ -0,0 +1,377 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.http.HttpStatus
+
+ at RestApi(name = "Group Services", description = "Methods for managing groups")
+class GroupController {
+
+    def permissionService
+    def preferenceService
+
+    @RestApiMethod(description = "Get organism permissions for group", path = "/group/getOrganismPermissionsForGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Group ID (or specify the name)")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Group name")
+    ]
+    )
+    def getOrganismPermissionsForGroup() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        UserGroup group = UserGroup.findById(dataObject.groupId)
+        if (!group) {
+            group = UserGroup.findByName(dataObject.name)
+        }
+        if (!group) {
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to get organism permissions")
+            render jsonObject as JSON
+            return
+        }
+
+        List<GroupOrganismPermission> groupOrganismPermissions = GroupOrganismPermission.findAllByGroup(group)
+        render groupOrganismPermissions as JSON
+    }
+
+    @RestApiMethod(description = "Load all groups", path = "/group/loadGroups", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "groupId", type = "long", paramType = RestApiParamType.QUERY, description = "Optional only load a specific groupId")
+    ])
+    def loadGroups() {
+        try {
+            log.debug "loadGroups"
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            JSONArray returnArray = new JSONArray()
+            def allowableOrganisms = permissionService.getOrganisms((User) permissionService.currentUser)
+
+            Map<String, List<GroupOrganismPermission>> groupOrganismPermissionMap = new HashMap<>()
+
+            List<GroupOrganismPermission> groupOrganismPermissionList = GroupOrganismPermission.findAllByOrganismInList(allowableOrganisms as List)
+            for (GroupOrganismPermission groupOrganismPermission in groupOrganismPermissionList) {
+                List<GroupOrganismPermission> groupOrganismPermissionListTemp = groupOrganismPermissionMap.get(groupOrganismPermission.group.name)
+                if (groupOrganismPermissionListTemp == null) {
+                    groupOrganismPermissionListTemp = new ArrayList<>()
+                }
+                groupOrganismPermissionListTemp.add(groupOrganismPermission)
+                groupOrganismPermissionMap.put(groupOrganismPermission.group.name, groupOrganismPermissionListTemp)
+            }
+
+
+            def groups = dataObject.groupId ? [UserGroup.findById(dataObject.groupId)] : UserGroup.all
+            groups.each {
+                def groupObject = new JSONObject()
+                groupObject.id = it.id
+                groupObject.name = it.name
+                groupObject.public = it.isPublicGroup()
+                groupObject.numberOfUsers = it.users?.size()
+
+                JSONArray userArray = new JSONArray()
+                it.users.each { user ->
+                    JSONObject userObject = new JSONObject()
+                    userObject.id = user.id
+                    userObject.email = user.username
+                    userObject.firstName = user.firstName
+                    userObject.lastName = user.lastName
+
+                    userArray.add(userObject)
+                }
+                groupObject.users = userArray
+
+                // add organism permissions
+                JSONArray organismPermissionsArray = new JSONArray()
+                def groupOrganismPermissionList3 = groupOrganismPermissionMap.get(it.name)
+                List<Long> organismsWithPermissions = new ArrayList<>()
+                for (GroupOrganismPermission groupOrganismPermission in groupOrganismPermissionList3) {
+                    if (allowableOrganisms.contains(groupOrganismPermission.organism)) {
+                        JSONObject organismJSON = new JSONObject()
+                        organismJSON.organism = groupOrganismPermission.organism.commonName
+                        organismJSON.permissions = groupOrganismPermission.permissions
+                        organismJSON.permissionArray = groupOrganismPermission.permissionValues
+                        organismJSON.groupId = groupOrganismPermission.groupId
+                        organismJSON.id = groupOrganismPermission.id
+                        organismPermissionsArray.add(organismJSON)
+                        organismsWithPermissions.add(groupOrganismPermission.organism.id)
+                    }
+                }
+
+                Set<Organism> organismList = allowableOrganisms.findAll() {
+                    !organismsWithPermissions.contains(it.id)
+                }
+
+                for (Organism organism in organismList) {
+                    JSONObject organismJSON = new JSONObject()
+                    organismJSON.organism = organism.commonName
+                    organismJSON.permissions = "[]"
+                    organismJSON.permissionArray = new JSONArray()
+                    organismJSON.groupId = it.id
+                    organismPermissionsArray.add(organismJSON)
+                }
+
+
+                groupObject.organismPermissions = organismPermissionsArray
+                returnArray.put(groupObject)
+            }
+
+            render returnArray as JSON
+        }
+        catch (Exception e) {
+            response.status = HttpStatus.INTERNAL_SERVER_ERROR.value()
+            def error = [error: e.message]
+            log.error error
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Create group", path = "/group/createGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Group name to add")
+    ]
+    )
+    @Transactional
+    def createGroup() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        log.info "Creating group"
+
+        UserGroup group = new UserGroup(
+                name: dataObject.name
+        ).save(flush: true)
+
+        log.info "Added group ${group.name}"
+
+        render group as JSON
+
+    }
+
+    @RestApiMethod(description = "Delete a group", path = "/group/deleteGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Group ID to remove (or specify the name)")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Group name to remove")
+    ]
+    )
+    @Transactional
+    def deleteGroup() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED.value()
+            return
+        }
+        log.info "Removing group"
+        UserGroup group = UserGroup.findById(dataObject.id)
+        if (!group) {
+            group = UserGroup.findByName(dataObject.name)
+        }
+        if (!group) {
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the group")
+            render jsonObject as JSON
+            return
+        }
+        List<User> users = group.users as List
+        users.each { it ->
+            it.removeFromUserGroups(group)
+            it.save()
+        }
+
+        def groupOrganismPermissions = GroupOrganismPermission.findAllByGroup(group)
+        GroupOrganismPermission.deleteAll(groupOrganismPermissions)
+
+        log.info "Removing group ${group.name}"
+
+        group.save(flush: true)
+        group.delete(flush: true)
+
+
+        render new JSONObject() as JSON
+    }
+
+    @RestApiMethod(description = "Update group", path = "/group/updateGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Group ID to update")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Group name to change to (the only editable optoin)")
+    ]
+    )
+    @Transactional
+    def updateGroup() {
+        log.info "Updating group"
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED.value()
+            return
+        }
+        UserGroup group = UserGroup.findById(dataObject.id)
+        // the only thing that can really change
+        log.info "Updated group ${group.name} to use name ${dataObject.name}"
+        group.name = dataObject.name
+
+        group.save(flush: true)
+    }
+
+    /**
+     * Only changing one of the boolean permissions
+     * @return
+     */
+    @RestApiMethod(description = "Update organism permission", path = "/group/updateOrganismPermission", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "groupId", type = "long", paramType = RestApiParamType.QUERY, description = "Group ID to modify permissions for (must provide this or 'name')")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "Group name to modify permissions for (must provide this or 'groupId')")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name")
+
+            , @RestApiParam(name = "ADMINISTRATE", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has administrative and all lesser (including user/group) privileges for the organism")
+            , @RestApiParam(name = "WRITE", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has write and all lesser privileges for the organism")
+            , @RestApiParam(name = "EXPORT", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has export and all lesser privileges for the organism")
+            , @RestApiParam(name = "READ", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has read and all lesser privileges for the organism")
+    ]
+    )
+    @Transactional
+    def updateOrganismPermission() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED.value()
+            return
+        }
+        log.info "Trying to update group organism permissions"
+        GroupOrganismPermission groupOrganismPermission = GroupOrganismPermission.findById(dataObject.id)
+
+
+        UserGroup group
+        if (dataObject.groupId) {
+            group = UserGroup.findById(dataObject.groupId as Long)
+        }
+        if (!group) {
+            group = UserGroup.findByName(dataObject.name)
+        }
+        if (!group) {
+            render([(FeatureStringEnum.ERROR.value): "Failed to find group for ${dataObject.name} and ${dataObject.groupId}"] as JSON)
+            return
+        }
+
+        log.debug "Finding organism by ${dataObject.organism}"
+        Organism organism = preferenceService.getOrganismForTokenInDB(dataObject.organism)
+        if (!organism) {
+            render([(FeatureStringEnum.ERROR.value): "Failed to find organism for ${dataObject.organism}"] as JSON)
+            return
+        }
+
+
+        log.debug "found ${groupOrganismPermission}"
+        if (!groupOrganismPermission) {
+            groupOrganismPermission = GroupOrganismPermission.findByGroupAndOrganism(group, organism)
+        }
+
+        if (!groupOrganismPermission) {
+            log.debug "creating new permissions! "
+            groupOrganismPermission = new GroupOrganismPermission(
+                    group: group
+                    , organism: organism
+                    , permissions: "[]"
+                    , permissionArray: new JSONArray()
+            ).save(insert: true)
+            log.debug "created new permissions! "
+        }
+
+
+
+        JSONArray permissionsArray = new JSONArray()
+        if (dataObject.getBoolean(PermissionEnum.ADMINISTRATE.name())) {
+            permissionsArray.add(PermissionEnum.ADMINISTRATE.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.WRITE.name())) {
+            permissionsArray.add(PermissionEnum.WRITE.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.EXPORT.name())) {
+            permissionsArray.add(PermissionEnum.EXPORT.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.READ.name())) {
+            permissionsArray.add(PermissionEnum.READ.name())
+        }
+
+        if(permissionsArray.size()==0){
+            groupOrganismPermission.delete(flush: true)
+            render groupOrganismPermission as JSON
+            return
+        }
+
+
+        groupOrganismPermission.permissions = permissionsArray.toString()
+        groupOrganismPermission.save(flush: true)
+
+        log.info "Updated permissions for group ${group.name} and organism ${organism?.commonName} and permissions ${permissionsArray?.toString()}"
+
+        render groupOrganismPermission as JSON
+
+    }
+
+    @RestApiMethod(description = "Update group membership", path = "/group/updateMembership", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "groupId", type = "long", paramType = RestApiParamType.QUERY, description = "Group ID to alter membership of")
+            , @RestApiParam(name = "users", type = "JSONArray", paramType = RestApiParamType.QUERY, description = "A JSON array of strings of emails of users the now belong to the group")
+    ]
+    )
+    @Transactional
+    def updateMembership() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED.value()
+            return
+        }
+        log.info "Trying to update user group membership"
+        UserGroup groupInstance = UserGroup.findById(dataObject.groupId)
+        List<User> oldUsers = groupInstance.users as List
+        List<String> usernames = dataObject.users
+        List<User> newUsers = User.findAllByUsernameInList(usernames)
+
+        List<User> usersToAdd = newUsers - oldUsers
+        List<User> usersToRemove = oldUsers - newUsers
+
+        usersToAdd.each {
+            groupInstance.addToUsers(it)
+            it.addToUserGroups(groupInstance)
+            it.save()
+        }
+
+        usersToRemove.each {
+            groupInstance.removeFromUsers(it)
+            it.removeFromUserGroups(groupInstance)
+            it.save()
+        }
+
+        groupInstance.save(flush: true)
+
+        log.info "Updated group ${groupInstance.name} membership setting users ${newUsers.join(' ')}"
+
+        render loadGroups() as JSON
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/HomeController.groovy b/grails-app/controllers/org/bbop/apollo/HomeController.groovy
new file mode 100644
index 0000000..04d221e
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/HomeController.groovy
@@ -0,0 +1,127 @@
+package org.bbop.apollo
+
+import grails.plugins.rest.client.RestBuilder
+import grails.plugins.rest.client.RestResponse
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.report.PerformanceMetric
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+
+class HomeController {
+
+    def permissionService
+
+    /**
+     * Permissions handled upstream
+     * @return
+     */
+    @Timed(name = "SystemInfo")
+    def systemInfo() {
+        Map<String, String> runtimeMapInstance = new HashMap<>()
+        Map<String, String> servletMapInstance = new HashMap<>()
+        Map<String, String> javaMapInstance = new HashMap<>()
+
+        javaMapInstance.putAll(System.getenv())
+
+        servletContext.getAttributeNames().each {
+            servletMapInstance.put(it, servletContext.getAttribute(it))
+        }
+
+        runtimeMapInstance.put("Available processors", "" + Runtime.getRuntime().availableProcessors())
+        runtimeMapInstance.put("Free memory", Runtime.getRuntime().freeMemory() / 1E6 + " MB")
+        runtimeMapInstance.put("Max memory", "" + Runtime.getRuntime().maxMemory() / 1E6 + " MB")
+        runtimeMapInstance.put("Total memory", "" + Runtime.getRuntime().totalMemory() / 1E6 + " MB")
+
+//        servletContext
+        render view: "systemInfo", model: [javaMapInstance: javaMapInstance, servletMapInstance: servletMapInstance, runtimeMapInstance: runtimeMapInstance]
+    }
+
+    private String getMethodName(String timerName) {
+        return timerName.substring(timerName.lastIndexOf(".") + 1).replaceAll("Timer", "")
+    }
+
+    private String getClassName(String timerName) {
+        return timerName.substring("org.bbop.apollo.".length(), timerName.lastIndexOf("."))
+    }
+
+    /**
+     * Permissions handled upstream
+     * @return
+     */
+    def metrics() {
+        def link = createLink(absolute: true, action: "metrics", controller: "metrics")
+        RestBuilder rest = new RestBuilder()
+        RestResponse response = rest.get(link)
+        JSONObject timerObjects = (response.json as JSONObject).getJSONObject("timers")
+
+        List<PerformanceMetric> performanceMetricList = new ArrayList<>()
+        Long countTotal = 0
+        Double meanTotal = 0
+        Double totalTotal = 0
+
+        for (String timerName : timerObjects.keySet()) {
+//            JSONObject jsonObject = timerObjects.getJSONObject(i).getJSONObject("timers")
+            PerformanceMetric metric = new PerformanceMetric()
+            metric.className = getClassName(timerName)
+            metric.methodName = getMethodName(timerName)
+            JSONObject timerData = timerObjects.getJSONObject(timerName)
+            metric.count = timerData.getInt("count")
+            metric.min = timerData.getDouble("min")
+            metric.max = timerData.getDouble("max")
+            metric.mean = timerData.getDouble("mean")
+            metric.stddev = timerData.getDouble("stddev")
+            metric.total = metric.count * metric.mean
+
+            countTotal += metric.count
+            meanTotal += metric.mean
+            totalTotal += metric.total
+
+            performanceMetricList.add(metric)
+        }
+
+        performanceMetricList.eachWithIndex { it,index ->
+            it.totalPercent =  it.total /  totalTotal
+        }
+
+//        http://localhost:8080/apollo/metrics/metrics?pretty=true
+        performanceMetricList.sort(true) { a, b ->
+            b.mean <=> a.mean ?: b.count <=> a.count ?: b.total <=> a.total ?: b.totalPercent <=> a.totalPercent
+        }
+
+
+        render view: "metrics", model: [performanceMetricList: performanceMetricList, countTotal: countTotal, meanTotal: performanceMetricList ? meanTotal / countTotal : 0 , totalTime: totalTotal]
+    }
+
+    def downloadReport() {
+
+        String returnString = "class,method,total,count,mean,max,min,stddev\n"
+        def link = createLink(absolute: true, action: "metrics", controller: "metrics")
+        RestBuilder rest = new RestBuilder()
+        RestResponse restResponse = rest.get(link)
+        JSONObject timerObjects = (restResponse.json as JSONObject).getJSONObject("timers")
+        for (String timerName : timerObjects.keySet()) {
+            returnString += getClassName(timerName) +","+getMethodName(timerName)+","
+            JSONObject timerData = timerObjects.getJSONObject(timerName)
+            returnString += timerData.getInt("count") * timerData.getDouble("mean")
+            returnString += ","
+            returnString += timerData.getInt("count")
+            returnString += ","
+            returnString += timerData.getDouble("min")
+            returnString += ","
+            returnString += timerData.getDouble("max")
+            returnString += ","
+            returnString += timerData.getDouble("mean")
+            returnString += ","
+            returnString += timerData.getDouble("stddev")
+            returnString += "\n"
+        }
+
+        String fileName = "apollo-performance-metrics-${new Date().format('dd-MMM-yyyy')}"
+        response.setHeader "Content-disposition", "attachment; filename=${fileName}.csv"
+        response.setHeader("x-filename", "${fileName}.csv")
+        response.contentType = 'text/csv'
+        response.outputStream << returnString
+        response.outputStream.flush()
+        response.outputStream.close()
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/IOServiceController.groovy b/grails-app/controllers/org/bbop/apollo/IOServiceController.groovy
new file mode 100644
index 0000000..c408833
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/IOServiceController.groovy
@@ -0,0 +1,251 @@
+package org.bbop.apollo
+
+import com.google.common.base.Splitter
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.sequence.DownloadFile
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.http.HttpStatus
+
+import java.util.zip.GZIPOutputStream
+
+ at RestApi(name = "IO Services", description = "Methods for bulk importing and exporting sequence data")
+class IOServiceController extends AbstractApolloController {
+
+    def sequenceService
+    def featureService
+    def gff3HandlerService
+    def fastaHandlerService
+    def chadoHandlerService
+    def preferenceService
+    def permissionService
+    def configWrapperService
+    def requestHandlingService
+
+    // fileMap of uuid / filename
+    // see #464
+    private Map<String, DownloadFile> fileMap = new HashMap<>()
+
+    def index() {}
+
+    def handleOperation(String track, String operation) {
+        log.debug "Requested parameterMap: ${request.parameterMap.keySet()}"
+        log.debug "upstream params: ${params}"
+        JSONObject postObject = findPost()
+        def mappedAction = underscoreToCamelCase(operation)
+        forward action: "${mappedAction}", params: params
+    }
+
+    @RestApiMethod(description = "Write out genomic data.  An example script is used in the https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/groovy/get_gff3.groovy"
+            , path = "/IOService/write", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+
+            , @RestApiParam(name = "type", type = "string", paramType = RestApiParamType.QUERY, description = "Type of annotated genomic features to export 'FASTA','GFF3','CHADO'.")
+
+            , @RestApiParam(name = "seqType", type = "string", paramType = RestApiParamType.QUERY, description = "Type of output sequence 'peptide','cds','cdna','genomic'.")
+            , @RestApiParam(name = "format", type = "string", paramType = RestApiParamType.QUERY, description = "'gzip' or 'text'")
+            , @RestApiParam(name = "sequences", type = "string", paramType = RestApiParamType.QUERY, description = "Names of references sequences to add (default is all).")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "Name of organism that sequences belong to (will default to last organism).")
+            , @RestApiParam(name = "output", type = "string", paramType = RestApiParamType.QUERY, description = "Output method 'file','text'")
+            , @RestApiParam(name = "exportAllSequences", type = "boolean", paramType = RestApiParamType.QUERY, description = "Export all reference sequences for an organism (over-rides 'sequences')")
+            , @RestApiParam(name = "exportGff3Fasta", type = "boolean", paramType = RestApiParamType.QUERY, description = "Export reference sequence when exporting GFF3 annotations.")
+            , @RestApiParam(name = "region", type = "String", paramType = RestApiParamType.QUERY, description = "Highlighted genomic region to export in form sequence:min..max  e.g., chr3:1001..1034")
+    ]
+    )
+    @Timed
+    def write() {
+        try {
+            long current = System.currentTimeMillis()
+            JSONObject dataObject = permissionService.handleInput(request,params)
+            if (!permissionService.hasPermissions(dataObject, PermissionEnum.READ)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            String typeOfExport = dataObject.type
+            String sequenceType = dataObject.seqType
+            Boolean exportAllSequences = dataObject.exportAllSequences ? Boolean.valueOf(dataObject.exportAllSequences) : false
+            Boolean exportGff3Fasta = dataObject.exportGff3Fasta ? Boolean.valueOf(dataObject.exportGff3Fasta) : false
+            String output = dataObject.output
+            String adapter = dataObject.adapter
+            String format = dataObject.format
+            String region = dataObject.region
+            def sequences = dataObject.sequences // can be array or string
+            Organism organism = dataObject.organism ? preferenceService.getOrganismForTokenInDB(dataObject.organism) : preferenceService.getCurrentOrganismForCurrentUser(dataObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+
+
+            def st = System.currentTimeMillis()
+            def queryParams = [viewableAnnotationList: requestHandlingService.viewableAnnotationList, organism: organism]
+            if(exportAllSequences){
+                sequences = []
+            }
+            if (sequences) {
+                queryParams.sequences = sequences
+            }
+            // caputures 3 level indirection, joins feature locations only. joining other things slows it down
+            def genes = Gene.executeQuery("select distinct f from Gene f join fetch f.featureLocations fl join fetch f.parentFeatureRelationships pr join fetch pr.childFeature child join fetch child.featureLocations join fetch child.childFeatureRelationships join fetch child.parentFeatureRelationships cpr join fetch cpr.childFeature subchild join fetch subchild.featureLocations join fetch subchild.childFeatureRelationships left join fetch subchild.parentFeatureRelationships where fl.sequ [...]
+            // captures rest of feats
+            def otherFeats = Feature.createCriteria().list() {
+                featureLocations {
+                    sequence {
+                        eq('organism', organism)
+                        if (sequences) {
+                            'in'('name', sequences)
+                        }
+                    }
+                }
+                'in'('class', requestHandlingService.viewableAlterations + requestHandlingService.viewableAnnotationFeatureList)
+            }
+            log.debug "${otherFeats}"
+            def features = genes + otherFeats
+
+            log.debug "IOService query: ${System.currentTimeMillis() - st}ms"
+
+            def sequenceList = Sequence.createCriteria().list() {
+                eq('organism', organism)
+                if (sequences) {
+                    'in'('name', sequences)
+                }
+            }
+            File outputFile = File.createTempFile("Annotations", "." + typeOfExport.toLowerCase())
+            String fileName
+
+            if (typeOfExport == FeatureStringEnum.TYPE_GFF3.getValue()) {
+                // adding sequence alterations to list of features to export
+                if (!exportAllSequences  && sequences != null && !(sequences.class == JSONArray.class)) {
+                    fileName = "Annotations-" + sequences + "." + typeOfExport.toLowerCase() + (format == "gzip" ? ".gz" : "")
+                } else {
+                    fileName = "Annotations" + "." + typeOfExport.toLowerCase() + (format == "gzip" ? ".gz" : "")
+                }
+                // call gff3HandlerService
+                if (exportGff3Fasta) {
+                    gff3HandlerService.writeFeaturesToText(outputFile.path, features, grailsApplication.config.apollo.gff3.source as String, true, sequenceList)
+                } else {
+                    gff3HandlerService.writeFeaturesToText(outputFile.path, features, grailsApplication.config.apollo.gff3.source as String)
+                }
+            } else if (typeOfExport == FeatureStringEnum.TYPE_FASTA.getValue()) {
+                if (!exportAllSequences  && sequences != null && !(sequences.class == JSONArray.class)) {
+                    String regionString = (region && adapter == FeatureStringEnum.HIGHLIGHTED_REGION.value) ? region : ""
+                    fileName = "Annotations-" + sequences + "${regionString}." + sequenceType + "." + typeOfExport.toLowerCase() + (format == "gzip" ? ".gz" : "")
+                } else {
+                    fileName = "Annotations" + "." + sequenceType + "." + typeOfExport.toLowerCase() + (format == "gzip" ? ".gz" : "")
+                }
+
+                // call fastaHandlerService
+                if(region && adapter == FeatureStringEnum.HIGHLIGHTED_REGION.value){
+                    String track = region.split(":")[0]
+                    String locationString = region.split(":")[1]
+                    Integer min = locationString.split("\\.\\.")[0] as Integer
+                    Integer max = locationString.split("\\.\\.")[1] as Integer
+                    // its an exclusive fmin, so must subtract one
+                    --min
+                    Sequence sequence = Sequence.findByOrganismAndName(organism,track)
+
+                    String defline = String.format(">Genomic region %s - %s\n",region,sequence.organism.commonName);
+                    String genomicSequence = defline
+                    genomicSequence += Splitter.fixedLength(FastaHandlerService.NUM_RESIDUES_PER_LINE).split(sequenceService.getGenomicResiduesFromSequenceWithAlterations(sequence,min,max, Strand.POSITIVE)).join("\n")
+                    outputFile.text = genomicSequence
+                }
+                else{
+                    fastaHandlerService.writeFeatures(features, sequenceType, ["name"] as Set, outputFile.path, FastaHandlerService.Mode.WRITE, FastaHandlerService.Format.TEXT,region)
+                }
+            } else if (typeOfExport == FeatureStringEnum.TYPE_CHADO.getValue()) {
+                if (sequences) {
+                    render chadoHandlerService.writeFeatures(organism, sequenceList, features)
+                } else {
+                    render chadoHandlerService.writeFeatures(organism, [], features, exportAllSequences)
+                }
+                 return // ??
+            }
+
+            //generating a html fragment with the link for download that can be rendered on client side
+            String uuidString = UUID.randomUUID().toString()
+            DownloadFile downloadFile = new DownloadFile(
+                    uuid: uuidString
+                    , path: outputFile.path
+                    , fileName: fileName
+            )
+            log.debug "${uuidString}"
+            fileMap.put(uuidString, downloadFile)
+
+            if (output == "file") {
+
+                def jsonObject = [
+                        "uuid"      : uuidString,
+                        "exportType": typeOfExport,
+                        "seqType"   : sequenceType,
+                        "format"    : format,
+                        "filename"  : fileName
+                ]
+                render jsonObject as JSON
+            } else {
+                render text: outputFile.text
+            }
+            log.debug "Total IOService export time ${System.currentTimeMillis() - current}ms"
+        }
+        catch (Exception e) {
+            def error = [error: e.message]
+            e.printStackTrace()
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "This is used to retrieve the a download link once the write operation was initialized using output: file."
+            , path = "/IOService/download", verb = RestApiVerb.POST
+    )
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "uuid", type = "string", paramType = RestApiParamType.QUERY, description = "UUID that holds the key to the stored download.")
+            , @RestApiParam(name = "format", type = "string", paramType = RestApiParamType.QUERY, description = "'gzip' or 'text'")
+    ]
+    )
+    @Timed
+    def download() {
+        String uuid = params.uuid
+        DownloadFile downloadFile = fileMap.remove(uuid)
+        def file
+        if (downloadFile) {
+            file = new File(downloadFile.path)
+            if (!file.exists()) {
+                render text: "Error: file does not exist"
+                return
+            }
+        } else {
+            render text: "Error: uuid did not map to file. Please try to re-download"
+            return
+        }
+
+        response.setHeader("Content-disposition", "attachment; filename=${downloadFile.fileName}")
+        if (params.format == "gzip") {
+            new GZIPOutputStream(response.outputStream).withWriter { it << file.text }
+//            def output = new BufferedOutputStream(new GZIPOutputStream(response.outputStream))
+//            output << file.text
+        } else {
+            def outputStream = response.outputStream
+            outputStream << file.text
+            outputStream.flush()
+            outputStream.close()
+        }
+
+        file.delete()
+    }
+
+    def chadoExportStatus() {
+        JSONObject returnObject = new JSONObject()
+        returnObject.export_status = configWrapperService.hasChadoDataSource().toString()
+        render returnObject
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/JbrowseController.groovy b/grails-app/controllers/org/bbop/apollo/JbrowseController.groovy
new file mode 100644
index 0000000..0a78e47
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/JbrowseController.groovy
@@ -0,0 +1,575 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import liquibase.util.file.FilenameUtils
+import org.apache.shiro.SecurityUtils
+import org.bbop.apollo.gwt.shared.ClientTokenGenerator
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.Range
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import javax.servlet.http.HttpServletResponse
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+
+import static org.springframework.http.HttpStatus.NOT_FOUND
+
+//@CompileStatic
+class JbrowseController {
+
+    private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
+
+    def grailsApplication
+    def sequenceService
+    def permissionService
+    def preferenceService
+    def jbrowseService
+    def servletContext
+    def configWrapperService
+    def trackService
+
+    def chooseOrganismForJbrowse() {
+        [organisms: Organism.findAllByPublicMode(true, [sort: 'commonName', order: 'asc']), flash: [message: params.error]]
+    }
+
+
+    def indexRouter() {
+        log.debug "indexRouter ${params}"
+        log.debug "path ${params.path}"
+        log.debug "request path: ${request.requestURL}"
+
+        def paramList = []
+        String clientToken = params[FeatureStringEnum.CLIENT_TOKEN.value]
+        params.each { entry ->
+            if (entry.key != "action" && entry.key != "controller" && entry.key != "organism") {
+                paramList.add(entry.key + "=" + entry.value)
+            }
+        }
+        // case 3 - validated login (just read from preferences, then
+        if (permissionService.currentUser && clientToken) {
+//            Organism organism = preferenceService.getOrganismForToken(clientToken)
+            Organism organism = preferenceService.getOrganismForTokenInDB(clientToken)
+            if(organism){
+                // we need to generate a client_token and do a redirection
+                paramList = paramList.findAll(){
+                    !it.startsWith(FeatureStringEnum.CLIENT_TOKEN.value)
+                }
+                clientToken = ClientTokenGenerator.generateRandomString()
+                preferenceService.setCurrentOrganism(permissionService.currentUser, organism, clientToken)
+                String paramString = ""
+                paramList.each {
+                    if(it.toString().startsWith("addStore")){
+                        paramString += URLEncoder.encode(it.toString(),"UTF-8")+"&"
+                    }
+                    else{
+                        paramString += it + "&"
+                    }
+                }
+                String uriString = createLink(url: "/${clientToken}/jbrowse/index.html?${paramString}")
+                redirect(uri:  uriString)
+                return
+            }
+            else{
+                organism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+            }
+            def availableOrganisms = permissionService.getOrganisms(permissionService.currentUser)
+            if(!availableOrganisms){
+                String urlString = "/jbrowse/index.html?${paramList.join("&")}"
+                String username = permissionService.currentUser.username
+                SecurityUtils.subject.logout()
+                forward(controller: "jbrowse", action: "chooseOrganismForJbrowse", params: [urlString: urlString, error: "User '${username}' lacks permissions to view or edit the annotations of any organism."])
+                return
+            }
+            if(!availableOrganisms.contains(organism)){
+                log.warn "Organism '${organism?.commonName}' is not viewable by this user so viewing ${availableOrganisms.first().commonName} instead."
+                organism = availableOrganisms.first()
+            }
+            if(organism && clientToken){
+                preferenceService.setCurrentOrganism(permissionService.currentUser, organism, clientToken)
+            }
+            File file = new File(servletContext.getRealPath("/jbrowse/index.html"))
+            render file.text
+            return
+        }
+//        // case 1 - anonymous login with organism ID, show organism
+        else {
+            log.debug "organism ID specified: ${clientToken}"
+
+            if (clientToken) {
+                Organism organism = preferenceService.getOrganismForToken(clientToken)
+                if (!organism) {
+                    String urlString = "/jbrowse/index.html?${paramList.join("&")}"
+                    forward(controller: "jbrowse", action: "chooseOrganismForJbrowse", params: [urlString: urlString, error: "Unable to find organism for '${clientToken}'"])
+                    return
+                }
+                // only show if public, otherwise will go to the end and force a login
+                if(organism.publicMode) {
+                    def session = request.getSession(true)
+                    session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, organism.directory)
+                    session.setAttribute(FeatureStringEnum.ORGANISM_ID.value, organism.id)
+                    session.setAttribute(FeatureStringEnum.ORGANISM_NAME.value, organism.commonName)
+                    // create an anonymous login
+                    File file = new File(servletContext.getRealPath("/jbrowse/index.html") as String)
+                    render file.text
+                    return
+                }
+            }
+
+
+        }
+
+        // case 2 - anonymous login with-OUT organism ID, show organism list
+        paramList.add("organism=${clientToken}")
+        String urlString = "/jbrowse/index.html?${paramList.join("&")}"
+        forward(controller: "jbrowse", action: "chooseOrganismForJbrowse", params: [urlString: urlString])
+    }
+
+    private String getDirectoryFromSession(String clientToken) {
+        String directory = request.session.getAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value)
+        if (!directory) {
+            Organism organism = preferenceService.getOrganismForToken(clientToken)
+            if (organism) {
+                def session = request.getSession(true)
+                session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, organism.directory)
+                session.setAttribute(FeatureStringEnum.ORGANISM_ID.value, organism.id)
+                session.setAttribute(FeatureStringEnum.ORGANISM_NAME.value, organism.commonName)
+                session.setAttribute(FeatureStringEnum.CLIENT_TOKEN.value, clientToken)
+                return organism.directory
+            }
+        }
+        return directory
+    }
+
+    /**
+     * @param clientToken
+     * @return
+     */
+    private String getJBrowseDirectoryForSession(String clientToken) {
+        log.debug "current user? ${permissionService.currentUser}"
+        if (!permissionService.currentUser) {
+            return getDirectoryFromSession(clientToken)
+        }
+
+        String thisToken = request.session.getAttribute(FeatureStringEnum.CLIENT_TOKEN.value)
+        request.session.setAttribute(FeatureStringEnum.CLIENT_TOKEN.value, clientToken)
+
+        log.debug "getting organism for client token ${clientToken}"
+        Organism currentOrganism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+        log.debug "got organism ${currentOrganism} for client token ${clientToken}"
+        String organismJBrowseDirectory = currentOrganism.directory
+        if (!organismJBrowseDirectory) {
+            for (Organism organism in Organism.all) {
+                // load if not
+                if (!organism.sequences) {
+                    sequenceService.loadRefSeqs(organism)
+                }
+
+                if (organism.sequences) {
+                    User user = permissionService.currentUser
+                    UserOrganismPreference userOrganismPreference = UserOrganismPreference.findByUserAndOrganism(user, organism, [max: 1, sort: "lastUpdated", order: "desc"])
+                    Sequence sequence =  Sequence.findAllByOrganism(organism,[sort:"end",order:"desc",max: 1]).first()
+                    if (userOrganismPreference == null) {
+                        userOrganismPreference = new UserOrganismPreference(
+                                user: user
+                                , organism: organism
+                                , sequence: sequence
+                                , currentOrganism: true
+                        ).save(insert: true, flush: true)
+                    } else {
+                        userOrganismPreference.sequence = sequence
+                        userOrganismPreference.currentOrganism = true
+                        userOrganismPreference.save()
+                    }
+
+                    organismJBrowseDirectory = organism.directory
+                    session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, organismJBrowseDirectory)
+                    session.setAttribute(FeatureStringEnum.SEQUENCE_NAME.value, sequence.name)
+                    session.setAttribute(FeatureStringEnum.ORGANISM_ID.value, sequence.organismId)
+                    session.setAttribute(FeatureStringEnum.ORGANISM.value, sequence.organism.commonName)
+                    return organismJBrowseDirectory
+                }
+            }
+        }
+        return organismJBrowseDirectory
+    }
+
+    /**
+     * Handles data directory serving for jbrowse
+     */
+    def data() {
+        String dataDirectory = getJBrowseDirectoryForSession(params.get(FeatureStringEnum.CLIENT_TOKEN.value).toString())
+        log.debug "data directory: ${dataDirectory}"
+        String dataFileName = dataDirectory + "/" + params.path
+        dataFileName += params.fileType ? ".${params.fileType}" : ""
+        String fileName = FilenameUtils.getName(params.path)
+        File file = new File(dataFileName)
+
+        // see https://github.com/GMOD/Apollo/issues/1448
+        if (!file.exists() && jbrowseService.hasOverlappingDirectory(dataDirectory,params.path)) {
+            log.debug "params.path: ${params.path} directory ${dataDirectory}"
+            String newPath = jbrowseService.fixOverlappingPath(dataDirectory,params.path)
+            dataFileName = newPath
+            dataFileName += params.fileType ? ".${params.fileType}" : ""
+            file = new File(dataFileName)
+        }
+
+        if (!file.exists()) {
+            Organism currentOrganism = preferenceService.getCurrentOrganismForCurrentUser(params.get(FeatureStringEnum.CLIENT_TOKEN.value).toString())
+            File extendedOrganismDataDirectory = new File(configWrapperService.commonDataDirectory + File.separator + currentOrganism.id + "-" + currentOrganism.commonName)
+
+            if (extendedOrganismDataDirectory.exists()) {
+                log.debug"track found in common data directory ${extendedOrganismDataDirectory.absolutePath}"
+                String newPath = extendedOrganismDataDirectory.getCanonicalPath() + File.separator + params.path
+                dataFileName = newPath
+                dataFileName += params.fileType ? ".${params.fileType}" : ""
+                file = new File(dataFileName)
+                log.debug"data file name: ${dataFileName}"
+            }
+
+            if (!file.exists()) {
+                log.error("File not found: " + dataFileName)
+                response.setStatus(HttpServletResponse.SC_NOT_FOUND)
+                return
+            }
+        }
+
+
+        String mimeType = getServletContext().getMimeType(fileName);
+        if (!mimeType) {
+            log.debug("No input MIME type of " + fileName);
+            if (fileName.endsWith(".json") || params.format == "json") {
+                mimeType = "application/json";
+                response.setContentType(mimeType);
+            } else if (fileName.endsWith(".bam")
+                    || fileName.endsWith(".bw")
+                    || fileName.endsWith(".bai")
+                    || fileName.endsWith(".conf")
+                    || fileName.endsWith(".csv")
+            ) {
+                mimeType = "text/plain";
+            } else if (fileName.endsWith(".tbi")) {
+                mimeType = "application/x-gzip";
+            } else {
+                log.info("Could not get MIME type of " + fileName + " falling back to text/plain");
+                mimeType = "text/plain";
+            }
+            if (fileName.endsWith("jsonz") || fileName.endsWith("txtz")) {
+                response.setHeader 'Content-Encoding', 'x-gzip'
+            }
+        }
+
+
+
+        if (isCacheableFile(fileName)) {
+            String eTag = createHashFromFile(file);
+            String dateString = formatLastModifiedDate(file);
+
+            response.setHeader("ETag", eTag);
+            response.setHeader("Last-Modified", dateString);
+        }
+
+        String range = request.getHeader("range");
+        long length = file.length();
+        Range full = new Range(0, length - 1, length);
+
+        List<Range> ranges = new ArrayList<Range>();
+
+        // from http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html#sublong
+        if (range != null) {
+
+            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
+            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*\$")) {
+                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
+                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                return;
+            } else {
+                // If any valid If-Range header, then process each part of byte range.
+
+                if (ranges.isEmpty()) {
+                    for (String part : range.substring(6).split(",")) {
+                        // Assuming a file with length of 100, the following examples returns bytes at:
+                        // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
+                        long start = sublong(part, 0, part.indexOf("-"));
+                        long end = sublong(part, part.indexOf("-") + 1, part.length());
+
+                        if (start == -1) {
+                            start = length - end;
+                            end = length - 1;
+                        } else if (end == -1 || end > length - 1) {
+                            end = length - 1;
+                        }
+
+                        // Check if Range is syntactically valid. If not, then return 416.
+                        if (start > end) {
+                            response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
+                            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+                            return;
+                        }
+                        ranges.add(new Range(start, end, length));
+                    }
+                }
+            }
+
+        }
+
+        response.setContentType(mimeType);
+        if (ranges.isEmpty() || ranges.get(0) == full) {
+            // Set content size
+            response.setContentLength((int) file.length());
+
+            // Open the file and output streams
+            FileInputStream inputStream = new FileInputStream(file);
+            OutputStream out = response.getOutputStream();
+
+            // Copy the contents of the file to the output stream
+            byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+            int count = 0;
+            while ((count = inputStream.read(buf)) >= 0) {
+                out.write(buf, 0, count);
+            }
+            inputStream.close();
+            out.close();
+        } else if (ranges.size() == 1) {
+            // Return single part of file.
+            Range r = ranges.get(0);
+            response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
+            response.setHeader("Content-Length", String.valueOf(r.length));
+            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
+
+            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
+
+            OutputStream output = response.getOutputStream();
+            byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+            long count = r.start;
+            try {
+
+                // Copy single part range.
+                long ret = bis.skip(r.start);
+                if (ret != r.start) {
+                    log.error("Failed to read range request!");
+                    bis.close();
+                    output.close();
+                    return;
+                }
+
+                while (count < r.end) {
+                    int bret = bis.read(buf, 0, DEFAULT_BUFFER_SIZE);
+                    if (bret != -1) {
+                        output.write(buf, 0, bret);
+                        count += bret;
+                    } else break;
+                }
+
+            } catch (Exception e) {
+                log.error(e.message);
+                e.printStackTrace();
+            }
+
+            output.close();
+            bis.close();
+
+        }
+
+    }
+
+
+    def trackList() {
+        String clientToken = params.get(FeatureStringEnum.CLIENT_TOKEN.value)
+        log.debug "track list client token: ${clientToken}"
+        String dataDirectory = getJBrowseDirectoryForSession(clientToken)
+        log.debug "got data directory of . . . ? ${dataDirectory}"
+        String absoluteFilePath = dataDirectory + "/trackList.json"
+        File file = new File(absoluteFilePath);
+        def mimeType = "application/json";
+        response.setContentType(mimeType);
+        Long id
+
+        if (!file.exists()) {
+            log.warn("Could not get for name and path: ${absoluteFilePath}");
+            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+            render status: NOT_FOUND
+            return;
+        }
+
+        // add datasets to the configuration
+        JSONObject jsonObject = JSON.parse(file.text) as JSONObject
+
+        Organism currentOrganism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+        if (currentOrganism != null) {
+            jsonObject.put("dataset_id", currentOrganism.id)
+        } else {
+            id = request.session.getAttribute(FeatureStringEnum.ORGANISM_ID.value);
+            jsonObject.put("dataset_id", id);
+        }
+        List<Organism> list = permissionService.getOrganismsForCurrentUser()
+        JSONObject organismObjectContainer = new JSONObject()
+        for (organism in list) {
+            JSONObject organismObject = new JSONObject()
+            organismObject.put("name", organism.commonName)
+            String url = "javascript:window.top.location.href = '../../annotator/loadLink?"
+            url += "organism=" + organism.getId();
+            url += "&highlight=0";
+            url += "&clientToken="+clientToken;
+            url += "&tracks='";
+            organismObject.put("url", url)
+            organismObjectContainer.put(organism.id, organismObject)
+        }
+
+        if (list.size() == 0) {
+            JSONObject organismObject = new JSONObject()
+            organismObject.put("name", currentOrganism.commonName)
+            organismObject.put("url", "#")
+            organismObjectContainer.put(id, organismObject)
+        }
+
+        jsonObject.put("datasets", organismObjectContainer)
+
+        if (jsonObject.include == null) jsonObject.put("include", new JSONArray())
+        jsonObject.include.add("../plugins/WebApollo/json/annot.json")
+
+        def plugins = grailsApplication.config.jbrowse?.plugins
+        // not sure if I do it this way or via the include
+        if (plugins) {
+            def pluginKeys = []
+            if (!jsonObject.plugins) {
+                jsonObject.plugins = new JSONArray()
+            } else {
+                for (int i = 0; i < jsonObject.plugins.size(); i++) {
+                    if(jsonObject.plugins[i] instanceof JSONObject){
+                        pluginKeys.add(jsonObject.plugins[i].name)
+                    }
+                    else
+                    if(jsonObject.plugins[i] instanceof String){
+                        pluginKeys.add(jsonObject.plugins[i])
+                    }
+                }
+            }
+            // add core plugin: https://github.com/GMOD/jbrowse/blob/master/src/JBrowse/Browser.js#L244
+            pluginKeys.add("RegexSequenceSearch")
+            for (plugin in plugins) {
+                if (!pluginKeys.contains(plugin.key)) {
+                    pluginKeys.add(plugin.key)
+                    JSONObject pluginObject = new JSONObject()
+                    pluginObject.name = plugin.key
+                    pluginObject.location = "./plugins/${plugin.key}"
+                    pluginObject.putAll(plugin.value)
+                    jsonObject.plugins.add(pluginObject)
+                    log.info "Loading plugin: ${pluginObject.name} details: ${pluginObject as JSON}"
+                }
+            }
+        }
+
+        trackService.removeIncludedPlugins(jsonObject.plugins)
+
+        // add extendedTrackList.json, if available
+        if (!currentOrganism.dataAddedViaWebServices) {
+            println "${configWrapperService.commonDataDirectory + File.separator + currentOrganism.id + "-" + currentOrganism.commonName + File.separator + OrganismController.EXTENDED_TRACKLIST}"
+            File extendedTrackListFile = new File(configWrapperService.commonDataDirectory + File.separator + currentOrganism.id + "-" + currentOrganism.commonName + File.separator + OrganismController.EXTENDED_TRACKLIST)
+            if (extendedTrackListFile.exists()) {
+                log.debug "augmenting track JSON Object with extendedTrackList.json contents"
+                JSONObject extendedTrackListObject = JSON.parse(extendedTrackListFile.text) as JSONObject
+                jsonObject.getJSONArray("tracks").addAll(extendedTrackListObject.getJSONArray("tracks"))
+            }
+        }
+
+        response.outputStream << jsonObject.toString()
+        response.outputStream.close()
+    }
+
+    private static boolean isCacheableFile(String fileName) {
+        if (fileName.endsWith(".txt") || fileName.endsWith("txtz") || fileName.endsWith("csv")) {
+            return true;
+        }
+        if (fileName.endsWith(".json") || fileName.endsWith("jsonz")) {
+            String[] names = fileName.split("\\/");
+            String requestName = names[names.length - 1];
+            return requestName.startsWith("lf-");
+        }
+
+        return false;
+    }
+
+    private static String formatLastModifiedDate(File file) {
+        DateFormat simpleDateFormat = SimpleDateFormat.getDateInstance();
+        return simpleDateFormat.format(new Date(file.lastModified()));
+    }
+
+    private static String createHashFromFile(File file) {
+        String fileName = file.getName();
+        long length = file.length();
+        long lastModified = file.lastModified();
+        return fileName + "_" + length + "_" + lastModified;
+    }
+
+    /**
+     * Returns a substring of the given string value from the given begin index to the given end
+     * index as a long. If the substring is empty, then -1 will be returned
+     *
+     * @param value The string value to return a substring as long for.
+     * @param beginIndex The begin index of the substring to be returned as long.
+     * @param endIndex The end index of the substring to be returned as long.
+     * @return A substring of the given string value as long or -1 if substring is empty.
+     */
+    private static long sublong(String value, int beginIndex, int endIndex) {
+        String substring = value.substring(beginIndex, endIndex);
+        return (substring.length() > 0) ? Long.parseLong(substring) : -1;
+    }
+
+    def passthrough() {
+        String dataFileName = params.prefix + "/" + params.path
+        String fileName = FilenameUtils.getName(params.path)
+        // have to prefix with a "/"
+        if(!dataFileName.startsWith("/")){
+            dataFileName = "/" + dataFileName
+        }
+        File file = new File(servletContext.getRealPath(dataFileName))
+
+        if (!file.exists()) {
+            Organism currentOrganism = preferenceService.getCurrentOrganismForCurrentUser(params.get(FeatureStringEnum.CLIENT_TOKEN.value).toString())
+            File extendedOrganismDataDirectory = new File(configWrapperService.commonDataDirectory + File.separator + currentOrganism.id + "-" + currentOrganism.commonName)
+            if (extendedOrganismDataDirectory.exists()) {
+                log.debug"track found in common data directory ${extendedOrganismDataDirectory.absolutePath}"
+                String newPath = extendedOrganismDataDirectory.getCanonicalPath() + File.separator + params.path
+                dataFileName = newPath
+                dataFileName += params.fileType ? ".${params.fileType}" : ""
+                file = new File(dataFileName)
+                log.debug"data file name: ${dataFileName}"
+            }
+
+            if (!file.exists()) {
+                log.error("File not found: " + dataFileName)
+                response.setStatus(HttpServletResponse.SC_NOT_FOUND)
+                return
+            }
+        }
+
+        String mimeType = getServletContext().getMimeType(fileName);
+
+        String eTag = createHashFromFile(file);
+        String dateString = formatLastModifiedDate(file);
+
+//        if (isCacheableFile(fileName)) {
+            response.setHeader("ETag", eTag);
+            response.setHeader("Last-Modified", dateString);
+//        }
+        
+        response.setContentType(mimeType);
+        // Set content size
+        response.setContentLength((int) file.length());
+
+        // Open the file and output streams
+        FileInputStream inputStream = new FileInputStream(file);
+        OutputStream out = response.getOutputStream();
+
+        // Copy the contents of the file to the output stream
+        byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+        int count = 0
+        while ((count = inputStream.read(buf)) >= 0) {
+            out.write(buf, 0, count)
+        }
+        inputStream.close()
+        out.close()
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/LoginController.groovy b/grails-app/controllers/org/bbop/apollo/LoginController.groovy
new file mode 100644
index 0000000..91bff81
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/LoginController.groovy
@@ -0,0 +1,239 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.AuthenticationException
+import org.apache.shiro.authc.IncorrectCredentialsException
+import org.apache.shiro.authc.UnknownAccountException
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.session.Session
+import org.apache.shiro.subject.Subject
+import org.apache.shiro.web.util.SavedRequest
+import org.apache.shiro.web.util.WebUtils
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import javax.servlet.http.HttpServletResponse
+
+class LoginController extends AbstractApolloController {
+
+    def permissionService
+    def userService
+    def brokerMessagingTemplate
+
+    def index() {}
+
+
+    def handleOperation(String track, String operation) {
+        JSONObject postObject = findPost()
+        if(postObject?.containsKey(REST_OPERATION)){
+            operation = postObject.get(REST_OPERATION)
+        }
+        log.info "updated operation: ${operation}"
+        if(!operation){
+            forward action: "doLogin"
+            return
+        }
+        def mappedAction = underscoreToCamelCase(operation)
+        log.debug "${operation} -> ${mappedAction}"
+        forward action: "${mappedAction}",  params: [data: postObject]
+    }
+
+
+    def doLogin(){
+        log.debug "creating login popup"
+    }
+
+    private void sendError(HttpServletResponse response, Exception e) throws IOException {
+        try {
+            response.setContentType("application/json");
+            response.setCharacterEncoding("UTF-8");
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            response.getWriter().write(new JSONObject().put("error", e.getMessage()).toString());
+        }
+        catch (JSONException e2) {
+            log.error("Error sending error: ${e2}")
+        }
+    }
+
+    def registerAdmin(){
+        if(User.count > 0 && !permissionService.isAdmin()){
+            log.error "Can only register admins if no users ${User.count} or user is admin"
+            throw new AnnotationException("Can only register admins if no users ${User.count} or user is admin")
+        }
+        def jsonObj = request.JSON
+        if(!jsonObj){
+            jsonObj = JSON.parse(params.data)
+            log.debug "jsonObj ${jsonObj}"
+        }
+
+        log.debug "register -> the jsonObj ${jsonObj}"
+        userService.registerAdmin(jsonObj)
+
+
+        return login()
+    }
+
+    /**
+     * @return
+     */
+    def login(){
+        def jsonObj
+        try {
+            jsonObj = request.JSON
+            if(!jsonObj){
+                jsonObj = JSON.parse(params.data)
+                log.debug "jsonObj ${jsonObj}"
+            }
+            log.debug "login -> the jsonObj ${jsonObj}"
+            String username = jsonObj.username
+            String password = jsonObj.password
+            Boolean rememberMe = jsonObj.rememberMe
+
+            def authToken = new UsernamePasswordToken(username, password as String)
+
+            // Support for "remember me"
+            if (rememberMe) {
+                authToken.rememberMe = true
+            }
+            log.debug "rememberMe: ${rememberMe}"
+            log.debug "authToken : ${authToken.rememberMe}"
+
+            // If a controller redirected to this page, redirect back
+            // to it. Otherwise redirect to the root URI.
+            def targetUri = params.targetUri ?: "/"
+
+            // Handle requests saved by Shiro filters.
+            SavedRequest savedRequest = WebUtils.getSavedRequest(request)
+            if (savedRequest) {
+                targetUri = savedRequest.requestURI - request.contextPath
+                if (savedRequest.queryString){
+                  targetUri = targetUri + '?' + savedRequest.queryString
+                }
+            }
+
+
+            // Perform the actual login. An AuthenticationException
+            // will be thrown if the username is unrecognised or the
+            // password is incorrect.
+            Subject subject = SecurityUtils.getSubject();
+            Session session = subject.getSession(true);
+            if(!permissionService.authenticateWithToken(authToken,request)){
+                throw new IncorrectCredentialsException("Bad credentaisl for user ${username}")
+            }
+//            subject.login(authToken)
+            log.debug "IS AUTHENTICATED: " + subject.isAuthenticated()
+            log.debug "SESSION ${session}"
+            log.debug "LOGIN SESSION ${SecurityUtils.subject.getSession(false).id}"
+
+            session.setAttribute("username", username);
+            session.setAttribute("permissions", new HashMap<String, Integer>());
+
+            User user = User.findByUsername(username)
+
+
+            Map<String, Integer> permissions = permissionService.getPermissionsForUser(user)
+            if(permissions){
+                session.setAttribute("permissions", permissions);
+            }
+
+            if(targetUri.length()>2){
+                redirect(uri: targetUri)
+            }
+            else{
+                render new JSONObject() as JSON
+            }
+        } catch(IncorrectCredentialsException ex) {
+            // Keep the username and "remember me" setting so that the
+            // user doesn't have to enter them again.
+            def m = [ username: jsonObj.username ]
+            if (jsonObj.rememberMe) {
+                m["rememberMe"] = true
+            }
+
+            // Remember the target URI too.
+            if (jsonObj.targetUri) {
+                m["targetUri"] = jsonObj.targetUri
+            }
+            m.error="Incorrect login"
+            // Now redirect back to the login page.
+            //redirect(action: "login", params: m)
+            render m as JSON
+        } catch(UnknownAccountException ex) {
+
+            def m = [ username: jsonObj.username ]
+            if (jsonObj.rememberMe) {
+                m["rememberMe"] = true
+            }
+
+            // Remember the target URI too.
+            if (jsonObj.targetUri) {
+                m["targetUri"] = jsonObj.targetUri
+            }
+            m.error="Unknown account"
+            render m as JSON
+
+        } catch ( AuthenticationException ae ) {
+
+            def m = [ username: jsonObj.username ]
+            if (jsonObj.rememberMe) {
+                m["rememberMe"] = true
+            }
+
+            // Remember the target URI too.
+            if (jsonObj.targetUri) {
+                m["targetUri"] = jsonObj.targetUri
+            }
+            m.error="Unknown authentication error"
+            render m as JSON
+            //unexpected condition - error?
+        }
+        catch ( Exception e ) {
+            def error=[error: e.message]
+            render error as JSON
+        }
+    }
+
+
+    def logout(){
+        log.debug "LOGOUT SESSION ${SecurityUtils?.subject?.getSession(false)?.id}"
+        log.debug "logging out with params: ${params}"
+        // have to retrive the username first
+        String username = SecurityUtils.subject.principal
+        log.debug "sending logout"
+        sendLogout(username,params.get(FeatureStringEnum.CLIENT_TOKEN.value).toString())
+        log.debug "sent logout"
+        sleep(1000)
+        log.debug "doing local logout"
+        SecurityUtils.subject.logout()
+        sleep(1000)
+        log.debug "logged out"
+        if(params.targetUri){
+            redirect(uri:"/auth/login?targetUri=${params.targetUri}")
+        }
+        else{
+            render new JSONObject() as JSON
+        }
+    }
+
+    def sendLogout(String username,String clientToken) {
+        User user = User.findByUsername(username)
+        log.debug "sending logout for ${user} via ${username} with ${clientToken}"
+        JSONObject jsonObject = new JSONObject()
+        if(!user){
+            log.error("Already logged out or user not found: ${username}")
+            return jsonObject.toString()
+        }
+        jsonObject.put(FeatureStringEnum.USERNAME.value,username)
+        jsonObject.put(FeatureStringEnum.CLIENT_TOKEN.value,clientToken)
+        jsonObject.put(REST_OPERATION,"logout")
+        log.debug "sending to: '/topic/AnnotationNotification/user/' + ${user.username}"
+        try {
+            brokerMessagingTemplate.convertAndSend "/topic/AnnotationNotification/user/" + username, jsonObject.toString()
+        } catch (e) {
+            log.error("working?: "+e)
+        }
+        return jsonObject.toString()
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/NcbiProxyServiceController.groovy b/grails-app/controllers/org/bbop/apollo/NcbiProxyServiceController.groovy
new file mode 100644
index 0000000..ec13148
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/NcbiProxyServiceController.groovy
@@ -0,0 +1,46 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import javax.xml.parsers.DocumentBuilder
+import javax.xml.parsers.DocumentBuilderFactory
+
+class NcbiProxyServiceController {
+
+    private static String SEARCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
+    // only returns title if xml, not json
+    private static String FETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?retmode=xml";
+    /**
+     * Request URL:http://demo.genomearchitect.org/WebApolloDemoStaging/ProxyService?proxy=eutils&operation=fetch&db=pubmed&id=PMC3013679
+
+     * @return
+     */
+    def index() {
+        String db = params.db
+        String id = params.id
+        String operation = params.operation
+        String urlString
+
+        switch (operation) {
+            case "search":
+                urlString = SEARCH_URL + "?db=" + db + "&term=" + id + "[uid]"
+                break;
+            case "fetch":
+                urlString = FETCH_URL + "&db=" + db + "&id=" + id + "";
+//                urlString = FETCH_URL + "?db=" + db + "&id=" + id+"&retmode=json"
+                break;
+            default:
+                throw new AnnotationException("EUtils operation ${operation} unknown")
+                break;
+        }
+
+        URL url = new URL(urlString)
+//        String returnText = url.text
+
+        // TODO: make
+        DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+        JSONObject returnObject = FormatUtil.convertFromXMLToJSON(docBuilder.parse(url.openStream()))
+        render returnObject as JSON
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/OrganismController.groovy b/grails-app/controllers/org/bbop/apollo/OrganismController.groovy
new file mode 100644
index 0000000..b45a3e4
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/OrganismController.groovy
@@ -0,0 +1,1087 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.report.OrganismSummary
+import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.web.multipart.commons.CommonsMultipartFile
+
+import javax.servlet.http.HttpServletResponse
+
+import static org.springframework.http.HttpStatus.NOT_FOUND
+
+ at RestApi(name = "Organism Services", description = "Methods for managing organisms")
+ at Transactional(readOnly = true)
+class OrganismController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+    static final String TRACKLIST = "trackList.json"
+    static final String EXTENDED_TRACKLIST = "extendedTrackList.json"
+
+    def sequenceService
+    def permissionService
+    def requestHandlingService
+    def preferenceService
+    def organismService
+    def reportService
+    def configWrapperService
+    def trackService
+    def fileService
+
+
+    @RestApiMethod(description = "Remove an organism", path = "/organism/deleteOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "json", paramType = RestApiParamType.QUERY, description = "Pass an Organism JSON object with an 'id' that corresponds to the organism to be removed")
+    ])
+    @Transactional
+    def deleteOrganism() {
+
+        try {
+            JSONObject organismJson = permissionService.handleInput(request, params)
+            log.debug "deleteOrganism ${organismJson}"
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(organismJson))) {
+
+                log.debug "organism ID: ${organismJson.id} vs ${organismJson.organism}"
+                Organism organism = Organism.findById(organismJson.id as Long) ?: Organism.findByCommonName(organismJson.organism)
+                if (organism) {
+                    UserOrganismPreference.deleteAll(UserOrganismPreference.findAllByOrganism(organism))
+                    OrganismFilter.deleteAll(OrganismFilter.findAllByOrganism(organism))
+                    organism.delete()
+                    log.info "Success deleting organism: ${organismJson.organism}"
+                } else {
+                    log.error "Organism ${organismJson.organism} not found"
+                }
+                render findAllOrganisms()
+            } else {
+                def error = [error: 'not authorized to delete organism']
+                log.error(error.error)
+                render error as JSON
+            }
+        }
+        catch (Exception e) {
+            def error = [error: 'problem deleting organism: ' + e]
+            log.error(error.error)
+            render error as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Delete an organism along with its data directory and returns a JSON object containing properties of the deleted organism", path = "/organism/deleteOrganismWithSequence", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "ID or commonName that can be used to uniquely identify an organism")
+    ])
+    @Transactional
+    def deleteOrganismWithSequence() {
+
+        JSONObject requestObject = permissionService.handleInput(request, params)
+        JSONObject responseObject = new JSONObject()
+        log.debug "deleteOrganism ${requestObject}"
+
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(requestObject))) {
+                Organism organism = preferenceService.getOrganismForTokenInDB(requestObject.organism)
+                if (organism) {
+                    boolean dataAddedViaWebServices = organism.dataAddedViaWebServices == null ? false : organism.dataAddedViaWebServices
+                    String organismDirectory = organism.directory
+                    def organismAsJSON = organism as JSON
+                    UserOrganismPreference.deleteAll(UserOrganismPreference.findAllByOrganism(organism))
+                    OrganismFilter.deleteAll(OrganismFilter.findAllByOrganism(organism))
+                    organism.delete()
+
+                    if (dataAddedViaWebServices) {
+                        log.debug "organism ${organism.id} was added via web services;"
+                        File dataDirectory = new File(organismDirectory)
+                        if (dataDirectory.deleteDir()) {
+                            log.info "dataDirectory: ${organismDirectory} deleted successfully."
+                        } else {
+                            log.error "Could not delete data directory: ${organismDirectory}."
+                            responseObject.put("warn", "Could not delete data directory: ${organismDirectory}")
+                        }
+                    } else {
+                        log.warn "organism ${organism.id} was not added via web services; Organism deleted but cannot delete data directory ${organismDirectory}"
+                        responseObject.put("warn", "Organism ${organism.id} was not added via web services; Organism deleted but cannot delete data directory ${organismDirectory}.")
+                        String extendedDataDirectoryName = configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + organism.commonName
+                        File extendedDataDirectory = new File(extendedDataDirectoryName)
+                        if (extendedDataDirectory.exists()) {
+                            log.info "Extended data directory found: ${extendedDataDirectoryName}"
+                            if (extendedDataDirectory.deleteDir()) {
+                                log.info "extended data directory found and deleted"
+                            } else {
+                                log.error "Extended data directory found but could not be deleted"
+                                responseObject.put("warn", responseObject.get("warn") + " Extended data directory found but could not be deleted.")
+                            }
+                        }
+                    }
+                    //render organismAsJSON
+                    responseObject.put("organism", JSON.parse(organismAsJSON.toString()) as JSONObject)
+                    log.info "Success deleting organism: ${requestObject.organism}"
+                } else {
+                    log.error "Organism: ${requestObject.organism} not found"
+                    responseObject.put("error", "Organism: ${requestObject.organism} not found.")
+                }
+            } else {
+                log.error "username not authorized to delete organism"
+                responseObject.put("error", "username not authorized to delete organism.")
+            }
+        } catch (Exception e) {
+            log.error(e.message)
+            responseObject.put("error", e.message)
+        }
+
+        render responseObject as JSON
+    }
+
+    @RestApiMethod(description = "Remove features from an organism", path = "/organism/deleteOrganismFeatures", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "json", paramType = RestApiParamType.QUERY, description = "ID or commonName that can be used to uniquely identify an organism.")
+    ])
+    @NotTransactional
+    def deleteOrganismFeatures() {
+        JSONObject organismJson = permissionService.handleInput(request, params)
+        if (organismJson.username == "" || organismJson.organism == "" || organismJson.password == "") {
+            def error = ['error': 'Empty fields in request JSON']
+            render error as JSON
+            log.error(error.error)
+            return
+        }
+        try {
+            if (!permissionService.hasPermissions(organismJson, PermissionEnum.ADMINISTRATE)) {
+                def error = [error: 'not authorized to delete all features from organism']
+                log.error(error.error)
+                render error as JSON
+                return
+            }
+
+            Organism organism = Organism.findByCommonName(organismJson.organism)
+
+            if (!organism) {
+                organism = Organism.findById(organismJson.organism)
+            }
+
+            if (!organism) {
+                throw new Exception("Can not find organism for ${organismJson.organism} to remove features of")
+            }
+
+            organismService.deleteAllFeaturesForOrganism(organism)
+            render [:] as JSON
+        }
+        catch (e) {
+            def error = [error: 'problem removing organism features for organism: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Adds an organism returning a JSON array of all organisms", path = "/organism/addOrganismWithSequence", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "species", type = "string", paramType = RestApiParamType.QUERY, description = "species name")
+            , @RestApiParam(name = "genus", type = "string", paramType = RestApiParamType.QUERY, description = "species genus")
+            , @RestApiParam(name = "blatdb", type = "string", paramType = RestApiParamType.QUERY, description = "filesystem path for a BLAT database (e.g. a .2bit file)")
+            , @RestApiParam(name = "publicMode", type = "boolean", paramType = RestApiParamType.QUERY, description = "a flag for whether the organism appears as in the public genomes list")
+            , @RestApiParam(name = "commonName", type = "string", paramType = RestApiParamType.QUERY, description = "commonName for an organism")
+            , @RestApiParam(name = "nonDefaultTranslationTable", type = "string", paramType = RestApiParamType.QUERY, description = "non-default translation table")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "organism metadata")
+            , @RestApiParam(name = "organismData", type = "file", paramType = RestApiParamType.QUERY, description = "zip or tar.gz compressed data directory")
+    ])
+    @Transactional
+    def addOrganismWithSequence() {
+
+        JSONObject returnObject = new JSONObject()
+        String directoryName
+        JSONObject requestObject = permissionService.handleInput(request, params)
+        String clientToken = requestObject.getString(FeatureStringEnum.CLIENT_TOKEN.value)
+        CommonsMultipartFile sequenceDataFile = request.getFile(FeatureStringEnum.ORGANISM_DATA.value)
+
+        if (!requestObject.containsKey(FeatureStringEnum.ORGANISM_NAME.value)) {
+            returnObject.put("error", "/addOrganismWithSequence requires '${FeatureStringEnum.ORGANISM_NAME.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        if (sequenceDataFile == null) {
+            returnObject.put("error", "/addOrganismWithSequence requires '${FeatureStringEnum.ORGANISM_DATA.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(requestObject))) {
+                log.debug "User is admin"
+                def organism = new Organism(
+                        commonName: requestObject.get(FeatureStringEnum.ORGANISM_NAME.value),
+                        directory: configWrapperService.commonDataDirectory,
+                        blatdb: requestObject.blatdb ?: "",
+                        genus: requestObject.genus ?: "",
+                        species: requestObject.species ?: "",
+                        metadata: requestObject.metadata ?: "",
+                        publicMode: requestObject.publicMode ?: false,
+                        dataAddedViaWebServices: true
+                ).save(failOnError: true, flush: true, insert: true)
+                directoryName = configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + requestObject.get(FeatureStringEnum.ORGANISM_NAME.value)
+                File directory = new File(directoryName)
+
+                if (directory.mkdirs()) {
+                    log.debug "Successfully created directory ${directoryName}"
+                    File archiveFile = new File(sequenceDataFile.getOriginalFilename())
+                    sequenceDataFile.transferTo(archiveFile)
+                    try {
+                        fileService.decompress(archiveFile, configWrapperService.commonDataDirectory, organism.id + "-" + requestObject.get(FeatureStringEnum.ORGANISM_NAME.value), true)
+                        log.debug "Adding ${requestObject.get(FeatureStringEnum.ORGANISM_NAME.value)} with directory: ${directoryName}"
+                        organism.directory = directoryName
+                        organism.save()
+                        sequenceService.loadRefSeqs(organism)
+                        preferenceService.setCurrentOrganism(permissionService.getCurrentUser(requestObject), organism, clientToken)
+                        findAllOrganisms()
+                    }
+                    catch (IOException e) {
+                        log.error e.printStackTrace()
+                        returnObject.put("error", e.message)
+                        organism.delete()
+                    }
+                } else {
+                    log.error "Could not create ${directoryName}"
+                    returnObject.put("error", "Could not create ${directoryName}.")
+                    organism.delete()
+                }
+            } else {
+                log.error "username ${requestObject.get(FeatureStringEnum.USERNAME.value)} is not authorized to add organisms"
+                returnObject.put("error", "username ${requestObject.get(FeatureStringEnum.USERNAME.value)} is not authorized to add organisms.")
+            }
+        } catch (Exception e) {
+            log.error e.printStackTrace()
+            returnObject.put("error", e.message)
+        }
+
+        render returnObject as JSON
+    }
+
+    @RestApiMethod(description = "Adds a track to an existing organism returning a JSON object containing all tracks for the current organism.", path = "/organism/addTrackToOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "ID or commonName that can be used to uniquely identify an organism")
+            , @RestApiParam(name = "trackData", type = "string", paramType = RestApiParamType.QUERY, description = "zip or tar.gz compressed track data")
+            , @RestApiParam(name = "trackFile", type = "string", paramType = RestApiParamType.QUERY, description = "track file (*.bam, *.vcf, *.bw)")
+            , @RestApiParam(name = "trackFileIndex", type = "string", paramType = RestApiParamType.QUERY, description = "index (*.bai, *.tbi)")
+            , @RestApiParam(name = "trackConfig", type = "string", paramType = RestApiParamType.QUERY, description = "Track configuration (JBrowse JSON)")
+    ])
+    @Transactional
+    def addTrackToOrganism() {
+
+        JSONObject returnObject = new JSONObject()
+        JSONObject requestObject = permissionService.handleInput(request, params)
+
+
+        if (!requestObject.containsKey(FeatureStringEnum.ORGANISM.value)) {
+            returnObject.put("error", "/addTrackToOrganism requires '${FeatureStringEnum.ORGANISM.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        if (requestObject.containsKey(FeatureStringEnum.TRACK_DATA.value) && requestObject.containsKey("trackFile")) {
+            returnObject.put("error", "Both 'trackData' and 'trackFile' specified; /addTrackToOrganism requires either '${FeatureStringEnum.TRACK_DATA.value}' or 'trackFile'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        if (!requestObject.containsKey(FeatureStringEnum.TRACK_DATA.value) && !requestObject.containsKey("trackFile")) {
+            returnObject.put("error", "/addTrackToOrganism requires either '${FeatureStringEnum.TRACK_DATA.value}' or 'trackFile'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        if (!requestObject.containsKey(FeatureStringEnum.TRACK_CONFIG.value)) {
+            returnObject.put("error", "/addTrackToOrganism requires '${FeatureStringEnum.TRACK_CONFIG.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        JSONObject trackConfigObject
+        try {
+            trackConfigObject = JSON.parse(params.get(FeatureStringEnum.TRACK_CONFIG.value)) as JSONObject
+        } catch (ConverterException ce) {
+            log.error ce.message
+            returnObject.put("error", ce.message)
+            render returnObject as JSON
+            return
+        }
+
+        if (!trackConfigObject.containsKey(FeatureStringEnum.LABEL.value) || !trackConfigObject.containsKey(FeatureStringEnum.URL_TEMPLATE.value)) {
+            log.error "trackConfig requires '${FeatureStringEnum.LABEL.value}' and '${FeatureStringEnum.URL_TEMPLATE.value}'"
+            returnObject.put("error", "trackConfig requires '${FeatureStringEnum.LABEL.value}' and '${FeatureStringEnum.URL_TEMPLATE.value}'.")
+            render returnObject as JSON
+            return
+        }
+
+        try {
+            permissionService.checkPermissions(requestObject, PermissionEnum.ADMINISTRATE)
+            log.debug "user ${requestObject.get(FeatureStringEnum.USERNAME.value)} is admin"
+            Organism organism = preferenceService.getOrganismForTokenInDB(requestObject.get(FeatureStringEnum.ORGANISM.value))
+
+            if (organism) {
+                log.debug "Adding track to organism: ${organism.commonName}"
+                String organismDirectoryName = organism.directory
+                File organismDirectory = new File(organismDirectoryName)
+                File commonDataDirectory = new File(configWrapperService.commonDataDirectory)
+
+                CommonsMultipartFile trackDataFile = request.getFile(FeatureStringEnum.TRACK_DATA.value)
+                CommonsMultipartFile trackFile = request.getFile("trackFile")
+                CommonsMultipartFile trackFileIndex = request.getFile("trackFileIndex")
+
+                if (organismDirectory.getParentFile().getCanonicalPath() == commonDataDirectory.getCanonicalPath()) {
+                    // organism data is in common data directory
+                    log.debug "organism data is in common data directory"
+                    File trackListJsonFile = new File(organism.directory + File.separator + TRACKLIST)
+                    JSONObject trackListObject = JSON.parse(trackListJsonFile.text)
+                    JSONArray tracksArray = trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+
+                    if (trackDataFile) {
+                        // check if track exists in trackList.json
+                        if (trackService.findTrackFromArray(tracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value)) == null) {
+                            // add track config to trackList.json
+                            tracksArray.add(trackConfigObject)
+                            // unpack track data into organism directory
+                            File archiveFile = new File(trackDataFile.getOriginalFilename())
+                            trackDataFile.transferTo(archiveFile)
+                            try {
+                                String urlTemplate = trackConfigObject.get(FeatureStringEnum.URL_TEMPLATE.value)
+                                String trackDirectoryName = urlTemplate.split("/").first()
+                                String path = organismDirectoryName + File.separator + trackDirectoryName
+                                fileService.decompress(archiveFile, path, trackConfigObject.get(FeatureStringEnum.LABEL.value), true)
+
+                                // write to trackList.json
+                                def trackListJsonWriter = trackListJsonFile.newWriter()
+                                trackListJsonWriter << trackListObject.toString(4)
+                                trackListJsonWriter.close()
+                                returnObject.put(FeatureStringEnum.TRACKS.value, tracksArray)
+                            }
+                            catch (IOException e) {
+                                log.error e.printStackTrace()
+                                returnObject.put("error", e.message)
+                            }
+                        } else {
+                            log.error "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}"
+                            returnObject.put("error", "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}.")
+                        }
+                    } else {
+                        // trackDataFile is null; use data from trackFile and trackFileIndex, if available
+                        if (trackFile) {
+                            if (trackService.findTrackFromArray(tracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value)) == null) {
+                                // add track config to trackList.json
+                                tracksArray.add(trackConfigObject)
+                                try {
+                                    String urlTemplate = trackConfigObject.get(FeatureStringEnum.URL_TEMPLATE.value)
+                                    String trackDirectoryName = urlTemplate.split("/").first()
+                                    String path = organismDirectoryName + File.separator + trackDirectoryName
+                                    fileService.store(trackFile, path)
+                                    if (trackFileIndex) {
+                                        fileService.store(trackFileIndex, path)
+                                    }
+
+                                    // write to trackList.json
+                                    def trackListJsonWriter = trackListJsonFile.newWriter()
+                                    trackListJsonWriter << trackListObject.toString(4)
+                                    trackListJsonWriter.close()
+                                    returnObject.put(FeatureStringEnum.TRACKS.value, tracksArray)
+                                }
+                                catch (IOException e) {
+                                    log.error e.printStackTrace()
+                                    returnObject.put("error", e.message)
+                                }
+                            } else {
+                                log.error "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}"
+                                returnObject.put("error", "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}.")
+                            }
+                        }
+                    }
+                } else {
+                    // organism data is somewhere on the server where we don't want to modify anything
+                    File trackListJsonFile = new File(organism.directory + File.separator + TRACKLIST)
+                    JSONObject trackListObject = JSON.parse(trackListJsonFile.text)
+                    JSONArray tracksArray = trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+                    if (trackService.findTrackFromArray(tracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value)) != null) {
+                        log.error "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}"
+                        returnObject.put("error", "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${organism.directory}/${TRACKLIST}.")
+                    } else {
+                        String extendedDirectoryName = configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + organism.commonName
+                        File extendedDirectory = new File(extendedDirectoryName)
+                        if (extendedDirectory.exists()) {
+                            // extended organism directory present in common data directory
+                            log.debug "extended organism directory ${extendedDirectoryName} present in common data directory"
+                        } else {
+                            // make a new extended organism directory in common data directory
+                            log.debug "creating extended organism directory ${extendedDirectoryName} in common data directory"
+                            if (extendedDirectory.mkdirs()) {
+                                // write extendedTrackList.json
+                                File extendedTrackListJsonFile = new File(extendedDirectoryName + File.separator + EXTENDED_TRACKLIST)
+                                def trackListJsonWriter = extendedTrackListJsonFile.newWriter()
+                                trackListJsonWriter << "{'${FeatureStringEnum.TRACKS.value}':[]}"
+                                trackListJsonWriter.close()
+                            } else {
+                                log.error "Cannot create directory ${extendedDirectoryName}"
+                                returnObject.put("error", "Cannot create directory ${extendedDirectoryName}.")
+                            }
+                        }
+
+                        if (trackDataFile) {
+                            File extendedTrackListJsonFile = new File(extendedDirectoryName + File.separator + EXTENDED_TRACKLIST)
+                            JSONObject extendedTrackListObject = JSON.parse(extendedTrackListJsonFile.text)
+                            JSONArray extendedTracksArray = extendedTrackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+                            // check if track exists in extendedTrackList.json
+                            if (trackService.findTrackFromArray(extendedTracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value)) != null) {
+                                log.error "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${extendedDirectoryName}/${EXTENDED_TRACKLIST}"
+                                returnObject.put("error", "an entry for track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' already exists in ${extendedDirectoryName}/${EXTENDED_TRACKLIST}.")
+                            } else {
+                                // add track config to extendedTrackList.json
+                                extendedTracksArray.add(trackConfigObject)
+                                // unpack track data into organism directory
+                                File archiveFile = new File(trackDataFile.getOriginalFilename())
+                                trackDataFile.transferTo(archiveFile)
+                                try {
+                                    String urlTemplate = trackConfigObject.get(FeatureStringEnum.URL_TEMPLATE.value)
+                                    String trackDirectoryName = urlTemplate.split("/").first()
+                                    String path = extendedDirectoryName + File.separator + trackDirectoryName
+                                    fileService.decompress(archiveFile, path, trackConfigObject.get(FeatureStringEnum.LABEL.value), true)
+
+                                    // write to trackList.json
+                                    def trackListJsonWriter = extendedTrackListJsonFile.newWriter()
+                                    trackListJsonWriter << extendedTrackListObject.toString(4)
+                                    trackListJsonWriter.close()
+                                    returnObject.put(FeatureStringEnum.TRACKS.value, tracksArray + extendedTracksArray)
+                                }
+                                catch (IOException e) {
+                                    log.error e.printStackTrace()
+                                    returnObject.put("error", e.message)
+                                }
+                            }
+                        }
+                    }
+                }
+            } else {
+                log.error "Organism not found"
+                returnObject.put("error", "Organism not found.")
+            }
+
+        } catch (e) {
+            log.error e.message
+            returnObject.put("error", e.message)
+        }
+
+        render returnObject as JSON
+    }
+
+    @RestApiMethod(description = "Deletes a track from an existing organism and returns a JSON object of the deleted track's configuration", path = "/organism/deleteTrackFromOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "ID or commonName that can be used to uniquely identify an organism")
+            , @RestApiParam(name = "trackLabel", type = "string", paramType = RestApiParamType.QUERY, description = "Track label corresponding to the track that is to be deleted")
+    ])
+    @Transactional
+    def deleteTrackFromOrganism() {
+
+        JSONObject returnObject = new JSONObject()
+
+        try {
+            JSONObject requestObject = permissionService.handleInput(request, params)
+            if (!requestObject.containsKey(FeatureStringEnum.ORGANISM.value)) {
+                returnObject.put("error", "/deleteTrackFromOrganism requires '${FeatureStringEnum.ORGANISM.value}'.")
+                response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+                render returnObject as JSON
+                return
+            }
+
+            if (!requestObject.containsKey(FeatureStringEnum.TRACK_LABEL.value)) {
+                returnObject.put("error", "/deleteTrackFromOrganism requires '${FeatureStringEnum.TRACK_LABEL.value}'.")
+                response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+                render returnObject as JSON
+                return
+            }
+
+            String trackLabel = requestObject.get(FeatureStringEnum.TRACK_LABEL.value)
+            permissionService.checkPermissions(requestObject, PermissionEnum.ADMINISTRATE)
+            log.debug "user ${requestObject.get(FeatureStringEnum.USERNAME.value)} is admin"
+            Organism organism = preferenceService.getOrganismForTokenInDB(requestObject.get(FeatureStringEnum.ORGANISM.value))
+
+            if (organism) {
+                log.debug "organism ${organism}"
+                File trackListJsonFile = new File(organism.trackList)
+                JSONObject trackListObject = JSON.parse(trackListJsonFile.text) as JSONObject
+                JSONObject trackObject = trackService.findTrackFromArray(trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value), trackLabel)
+
+                if (trackObject == null) {
+                    // track not found in trackList.json
+                    log.debug "Track with label '${trackLabel}' not found; searching in extendedTrackList.json"
+                    File extendedTrackListJsonFile = new File(configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + organism.commonName + File.separator + EXTENDED_TRACKLIST)
+                    if (extendedTrackListJsonFile.exists()) {
+                        JSONObject extendedTrackListObject = JSON.parse(extendedTrackListJsonFile.text) as JSONObject
+                        trackObject = trackService.findTrackFromArray(extendedTrackListObject.getJSONArray(FeatureStringEnum.TRACKS.value), trackLabel)
+                        if (trackObject == null) {
+                            // track not found
+                            log.error "Track with label '${trackLabel}' not found"
+                            returnObject.put("error", "Track with label '${trackLabel}' not found.")
+                        } else {
+                            log.debug "Track with label '${trackLabel}' found; removing from extendedTrackList.json"
+                            extendedTrackListObject.getJSONArray(FeatureStringEnum.TRACKS.value).remove(trackObject)
+                            String urlTemplate = trackObject.get(FeatureStringEnum.URL_TEMPLATE.value)
+                            String trackDirectory = urlTemplate.split("/").first()
+                            File trackDir = new File(configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + organism.commonName + File.separator + trackDirectory + File.separator + trackObject.get(FeatureStringEnum.LABEL.value))
+                            if (trackDir.exists()) {
+                                log.debug "Deleting ${trackDir.getAbsolutePath()}"
+                                if (trackDir.deleteDir()) {
+                                    // updating extendedTrackList.json
+                                    def trackListJsonWriter = extendedTrackListJsonFile.newWriter()
+                                    trackListJsonWriter << extendedTrackListObject.toString(4)
+                                    trackListJsonWriter.close()
+                                }
+                            } else {
+                                log.error "track directory ${trackDir.getAbsolutePath()} not found"
+                                returnObject.put("error", "Track with label '${trackLabel}' removed from config but track directory not found.")
+                                // updating extendedTrackList.json
+                                def trackListJsonWriter = extendedTrackListJsonFile.newWriter()
+                                trackListJsonWriter << extendedTrackListObject.toString(4)
+                                trackListJsonWriter.close()
+                            }
+                            returnObject.put("track", trackObject)
+                        }
+                    } else {
+                        log.error "Track with label '${trackLabel}' not found"
+                        returnObject.put("error", "Track with label '${trackLabel}' not found.")
+                    }
+                } else {
+                    // track found in trackList.json
+                    log.debug "track with label '${trackLabel}' found in trackList.json"
+                    if (organism.dataAddedViaWebServices) {
+                        log.debug "organism data was added via web services; thus can remove the track"
+                        // track can be deleted since the organism and all subsequent tracks were added via web services
+                        trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value).remove(trackObject)
+                        String urlTemplate = trackObject.get(FeatureStringEnum.URL_TEMPLATE.value)
+                        String trackDirectory = urlTemplate.split("/").first()
+                        File trackDir = new File(organism.directory + File.separator + trackDirectory + File.separator + trackObject.get(FeatureStringEnum.LABEL.value))
+                        if (trackDir.exists()) {
+                            log.debug "Deleting ${trackDir.getAbsolutePath()}"
+                            if (trackDir.deleteDir()) {
+                                // updating trackList.json
+                                def trackListJsonWriter = trackListJsonFile.newWriter()
+                                trackListJsonWriter << trackListObject.toString(4)
+                                trackListJsonWriter.close()
+                            }
+                        } else {
+                            log.error "track directory ${trackDir.getAbsolutePath()} not found"
+                            returnObject.put("error", "Track with label '${trackLabel}' removed from config but track directory not found.")
+                            // updating trackList.json
+                            def trackListJsonWriter = trackListJsonFile.newWriter()
+                            trackListJsonWriter << trackListObject.toString(4)
+                            trackListJsonWriter.close()
+                        }
+                        returnObject.put("track", trackObject)
+                    } else {
+                        // cannot delete track since its part of the main data directory
+                        log.error "Track with label '${trackLabel}' found but is part of the main data directory and cannot be deleted."
+                        returnObject.put("error", "Track with label '${trackLabel}' found but is part of the main data directory and cannot be deleted.")
+                    }
+                }
+            } else {
+                log.error("Organism not found")
+                returnObject.put("error", "Organism not found.")
+            }
+        } catch (Exception e) {
+            log.error(e.message)
+            returnObject.put("error", e.message)
+        }
+
+        render returnObject as JSON
+    }
+
+    @RestApiMethod(description = "Update a track in an existing organism returning a JSON object containing old and new track configurations", path = "/organism/updateTrackForOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "ID or commonName that can be used to uniquely identify an organism")
+            , @RestApiParam(name = "trackConfig", type = "string", paramType = RestApiParamType.QUERY, description = "Track configuration (JBrowse JSON)")
+    ])
+    @Transactional
+    def updateTrackForOrganism() {
+
+        JSONObject returnObject = new JSONObject()
+        JSONObject requestObject = permissionService.handleInput(request, params)
+
+
+        if (!requestObject.containsKey(FeatureStringEnum.ORGANISM.value)) {
+            returnObject.put("error", "/updateTrackForOrganism requires '${FeatureStringEnum.ORGANISM.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        if (!requestObject.containsKey(FeatureStringEnum.TRACK_CONFIG.value)) {
+            returnObject.put("error", "/updateTrackForOrganism requires '${FeatureStringEnum.TRACK_CONFIG.value}'.")
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
+            render returnObject as JSON
+            return
+        }
+
+        JSONObject trackConfigObject
+        try {
+            trackConfigObject = JSON.parse(params.get(FeatureStringEnum.TRACK_CONFIG.value)) as JSONObject
+        } catch (ConverterException ce) {
+            log.error ce.message
+            returnObject.put("error", ce.message)
+            render returnObject as JSON
+            return
+        }
+
+        if (!trackConfigObject.containsKey(FeatureStringEnum.LABEL.value) || !trackConfigObject.containsKey(FeatureStringEnum.URL_TEMPLATE.value)) {
+            log.error "trackConfig requires both '${FeatureStringEnum.LABEL.value}' and '${FeatureStringEnum.URL_TEMPLATE.value}'."
+            returnObject.put("error", "trackConfig requires both '${FeatureStringEnum.LABEL.value}' and '${FeatureStringEnum.URL_TEMPLATE.value}'.")
+            render returnObject as JSON
+            return
+        }
+
+        try {
+            permissionService.checkPermissions(requestObject, PermissionEnum.ADMINISTRATE)
+            log.debug "user ${requestObject.get(FeatureStringEnum.USERNAME.value)} is admin"
+            Organism organism = preferenceService.getOrganismForTokenInDB(requestObject.get(FeatureStringEnum.ORGANISM.value))
+
+            if (organism) {
+                String organismDirectoryName = organism.directory
+                File organismDirectory = new File(organismDirectoryName)
+                File commonDataDirectory = new File(configWrapperService.commonDataDirectory)
+
+                if (organismDirectory.getParentFile().getAbsolutePath() == commonDataDirectory.getAbsolutePath()) {
+                    // organism data is in common data directory
+                    log.debug "organism data is in common data directory"
+                    File trackListJsonFile = new File(organism.directory + File.separator + TRACKLIST)
+                    JSONObject trackListObject = JSON.parse(trackListJsonFile.text)
+                    JSONArray tracksArray = trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+                    // check if track exists in trackList.json
+                    JSONObject trackObject = trackService.findTrackFromArray(tracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value))
+                    if (trackObject == null) {
+                        log.error "Cannot find track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'"
+                        returnObject.put("error", "Cannot find track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'.")
+                    } else {
+                        // replaces track config
+                        tracksArray.remove(trackObject)
+                        tracksArray.add(trackConfigObject)
+
+                        // write to trackList.json
+                        def trackListJsonWriter = trackListJsonFile.newWriter()
+                        trackListJsonWriter << trackListObject.toString(4)
+                        trackListJsonWriter.close()
+
+                        returnObject.put("oldTrackConfig", trackObject)
+                        returnObject.put("newTrackConfig", trackConfigObject)
+                    }
+                } else {
+                    // organism data is somewhere on the server where we don't want to modify anything
+                    log.debug "organism data is somewhere on the FS"
+                    File trackListJsonFile = new File(organism.directory + File.separator + TRACKLIST)
+                    JSONObject trackListObject = JSON.parse(trackListJsonFile.text)
+                    JSONArray tracksArray = trackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+                    // check if track exists in trackList.json
+                    JSONObject trackObject = trackService.findTrackFromArray(tracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value))
+                    if (trackObject != null) {
+                        // cannot update track config
+                        log.error "Track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' found but is part of the main data directory and cannot be updated."
+                        returnObject.put("error", "Track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}' found but is part of the main data directory and cannot be updated.")
+                    } else {
+                        String extendedDirectoryName = configWrapperService.commonDataDirectory + File.separator + organism.id + "-" + organism.commonName
+                        File extendedDirectory = new File(extendedDirectoryName)
+                        if (extendedDirectory.exists()) {
+                            // extended organism directory present in common data directory
+                            log.debug "extended organism directory ${extendedDirectoryName} present in common data directory"
+                            File extendedTrackListJsonFile = new File(extendedDirectoryName + File.separator + EXTENDED_TRACKLIST)
+                            JSONObject extendedTrackListObject = JSON.parse(extendedTrackListJsonFile.text)
+                            JSONArray extendedTracksArray = extendedTrackListObject.getJSONArray(FeatureStringEnum.TRACKS.value)
+                            // check if track exists in extendedTrackList.json
+                            trackObject = trackService.findTrackFromArray(extendedTracksArray, trackConfigObject.get(FeatureStringEnum.LABEL.value))
+                            if (trackObject == null) {
+                                log.error "Cannot find track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'"
+                                returnObject.put("error", "Cannot find track with label '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'.")
+                            } else {
+                                // replaces track config
+                                extendedTracksArray.remove(trackObject)
+                                extendedTracksArray.add(trackConfigObject)
+
+                                // write to trackList.json
+                                def extendedTrackListJsonWriter = extendedTrackListJsonFile.newWriter()
+                                extendedTrackListJsonWriter << extendedTrackListObject.toString(4)
+                                extendedTrackListJsonWriter.close()
+
+                                returnObject.put("oldTrackConfig", trackObject)
+                                returnObject.put("newTrackConfig", trackConfigObject)
+                            }
+                        } else {
+                            log.error "Extended organism directory does not exist; Cannot find track with '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'."
+                            returnObject.put("error", "Cannot find track with '${trackConfigObject.get(FeatureStringEnum.LABEL.value)}'.")
+                        }
+                    }
+                }
+            } else {
+                log.error "Organism not found"
+                returnObject.put("error", "Organism not found.")
+            }
+        } catch (Exception e) {
+            log.error e.message
+            returnObject.put("error", e.message)
+        }
+
+        render returnObject as JSON
+    }
+
+    @RestApiMethod(description = "Adds an organism returning a JSON array of all organisms", path = "/organism/addOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "directory", type = "string", paramType = RestApiParamType.QUERY, description = "Filesystem path for the organisms data directory (required)")
+            , @RestApiParam(name = "commonName", type = "string", paramType = RestApiParamType.QUERY, description = "A name used for the organism")
+            , @RestApiParam(name = "species", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Species name")
+            , @RestApiParam(name = "genus", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Species genus")
+            , @RestApiParam(name = "blatdb", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Filesystem path for a BLAT database (e.g. a .2bit file)")
+            , @RestApiParam(name = "publicMode", type = "boolean", paramType = RestApiParamType.QUERY, description = "(optional) A flag for whether the organism appears as in the public genomes list (default false)")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Organism metadata")
+            , @RestApiParam(name = "returnAllOrganisms", type = "boolean", paramType = RestApiParamType.QUERY, description = "(optional) Return all organisms (true / false) (default true)")
+    ])
+    @Transactional
+    def addOrganism() {
+        JSONObject organismJson = permissionService.handleInput(request, params)
+        String clientToken = organismJson.getString(FeatureStringEnum.CLIENT_TOKEN.value)
+        try {
+            if (permissionService.isUserAdmin(permissionService.getCurrentUser(organismJson))) {
+                if (organismJson.get("commonName") == "" || organismJson.get("directory") == "") {
+                    throw new Exception('empty fields detected')
+                }
+
+                log.debug "Adding ${organismJson.publicMode}"
+                Organism organism = new Organism(
+                        commonName: organismJson.commonName
+                        , directory: organismJson.directory
+                        , blatdb: organismJson.blatdb
+                        , species: organismJson.species
+                        , genus: organismJson.genus
+                        , metadata: organismJson.metadata
+                        , nonDefaultTranslationTable: organismJson.nonDefaultTranslationTable ?: null
+                        , publicMode: organismJson.publicMode ?: false
+                )
+                log.debug "organism ${organism as JSON}"
+
+                if (checkOrganism(organism)) {
+                    organism.save(failOnError: true, flush: true, insert: true)
+                }
+
+                // send file using:
+//            curl \
+                //  -F "userid=1" \
+                //  -F "filecomment=This is an image file" \
+                //  -F "sequenceData=@/home/user1/Desktop/jbrowse/sample/seq.zip" \
+                //  localhost:8080/apollo
+//                if (request.getFile("sequenceData)")) {
+//
+//                }
+                sequenceService.loadRefSeqs(organism)
+
+                preferenceService.setCurrentOrganism(permissionService.getCurrentUser(organismJson), organism, clientToken)
+                Boolean returnAllOrganisms = organismJson.returnAllOrganisms ? Boolean.valueOf(organismJson.returnAllOrganisms): true
+
+                render returnAllOrganisms ? findAllOrganisms() : new JSONArray()
+
+            } else {
+                def error = [error: 'not authorized to add organism']
+                render error as JSON
+                log.error(error.error)
+            }
+        } catch (e) {
+            def error = [error: 'problem saving organism: ' + e]
+            render error as JSON
+            e.printStackTrace()
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Finds sequences for a given organism and returns a JSON object including the username, organism and a JSONArray of sequences", path = "/organism/getSequencesForOrganism", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "Common name or ID for the organism")
+    ])
+    def getSequencesForOrganism() {
+        JSONObject organismJson = permissionService.handleInput(request, params)
+        if (organismJson.username == "" || organismJson.organism == "" || organismJson.password == "") {
+            render(['error': 'Empty fields in request JSON'] as JSON)
+            return
+        }
+
+        List<Sequence> sequenceList
+
+        Organism organism = Organism.findByCommonName(organismJson.organism)
+        if (!organism) {
+            organism = Organism.findById(organismJson.organism)
+        }
+        if (!organism) {
+            def error = ['error': 'Organism not found ' + organismJson.organism]
+            render error as JSON
+            log.error(error.error)
+            return
+        }
+
+
+        if (permissionService.findHighestOrganismPermissionForUser(organism, permissionService.getCurrentUser(organismJson)).rank >= PermissionEnum.EXPORT.rank) {
+            def c = Sequence.createCriteria()
+            sequenceList = c.list {
+                eq('organism', organism)
+            }
+            log.debug "Sequence list fetched at getSequencesForOrganism: ${sequenceList}"
+        } else {
+            def error = ['error': 'Username ' + organismJson.username + ' does not have export permissions for organism ' + organismJson.organism]
+            render error as JSON
+            log.error(error.error)
+            return
+        }
+
+        render([username: organismJson.username, organism: organismJson.organism, sequences: sequenceList] as JSON)
+    }
+
+    private boolean checkOrganism(Organism organism) {
+        File directory = new File(organism.directory)
+        File trackListFile = new File(organism.getTrackList())
+        File refSeqFile = new File(organism.getRefseqFile())
+
+        if (!directory.exists() || !directory.isDirectory()) {
+            organism.valid = false
+            throw new Exception("Invalid directory specified: " + directory.absolutePath)
+        } else if (!trackListFile.exists()) {
+            organism.valid = false
+            throw new Exception("Track file does not exists: " + trackListFile.absolutePath)
+        } else if (!refSeqFile.exists()) {
+            organism.valid = false
+            throw new Exception("Reference sequence file does not exists: " + refSeqFile.absolutePath)
+        } else {
+            organism.valid = true
+        }
+        return organism.valid
+    }
+
+
+    @RestApiMethod(description = "Adds an organism returning a JSON array of all organisms", path = "/organism/updateOrganismInfo", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "unique id of organism to change")
+            , @RestApiParam(name = "directory", type = "string", paramType = RestApiParamType.QUERY, description = "filesystem path for the organisms data directory (required)")
+            , @RestApiParam(name = "species", type = "string", paramType = RestApiParamType.QUERY, description = "species name")
+            , @RestApiParam(name = "genus", type = "string", paramType = RestApiParamType.QUERY, description = "species genus")
+            , @RestApiParam(name = "blatdb", type = "string", paramType = RestApiParamType.QUERY, description = "filesystem path for a BLAT database (e.g. a .2bit file)")
+            , @RestApiParam(name = "publicMode", type = "boolean", paramType = RestApiParamType.QUERY, description = "a flag for whether the organism appears as in the public genomes list")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "a common name used for the organism")
+            , @RestApiParam(name = "nonDefaultTranslationTable", type = "string", paramType = RestApiParamType.QUERY, description = "non-default translation table")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "organism metadata")
+    ])
+    @Transactional
+    def updateOrganismInfo() {
+        try {
+            JSONObject organismJson = permissionService.handleInput(request, params)
+            permissionService.checkPermissions(organismJson, PermissionEnum.ADMINISTRATE)
+            Organism organism = Organism.findById(organismJson.id)
+            if (organism) {
+                log.debug "Updating organism info ${organismJson as JSON}"
+                organism.commonName = organismJson.name
+                organism.blatdb = organismJson.blatdb ?: null
+                organism.species = organismJson.species ?: null
+                organism.genus = organismJson.genus ?: null
+                organism.metadata = organismJson.metadata ?: null
+                organism.directory = organismJson.directory
+                organism.publicMode = organismJson.publicMode ?: false
+                organism.nonDefaultTranslationTable = organismJson.nonDefaultTranslationTable ?: null
+
+                if (checkOrganism(organism)) {
+                    organism.save(flush: true, insert: false, failOnError: true)
+                } else {
+                    throw new Exception("Bad organism directory: " + organism.directory)
+                }
+            } else {
+                throw new Exception('organism not found')
+            }
+            render findAllOrganisms() as JSON
+        }
+        catch (e) {
+            def error = [error: 'problem saving organism: ' + e]
+            render error as JSON
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Update organism metadata", path = "/organism/updateOrganismMetadata", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "unique id of organism to change")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "organism metadata")
+    ])
+    @Transactional
+    def updateOrganismMetadata() {
+        log.debug "updating organism metadata ${params}"
+        try {
+            JSONObject organismJson = permissionService.handleInput(request, params)
+            permissionService.checkPermissions(organismJson, PermissionEnum.ADMINISTRATE)
+            Organism organism = Organism.findById(organismJson.id)
+            if (organism) {
+                log.debug "Updating organism metadata ${organismJson as JSON}"
+                organism.metadata = organismJson.metadata
+                organism.save(flush: true, insert: false, failOnError: true)
+            } else {
+                throw new Exception('Organism not found')
+            }
+            render new JSONObject() as JSON
+        }
+        catch (e) {
+            def error = [error: 'problem saving organism: ' + e]
+            render error as JSON
+            log.error(error.error)
+        }
+    }
+
+    @RestApiMethod(description = "Returns a JSON array of all organisms, or optionally, gets information about a specific organism", path = "/organism/findAllOrganisms", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) ID or commonName that can be used to uniquely identify an organism")
+    ])
+    def findAllOrganisms() {
+        try {
+            JSONObject requestObject = permissionService.handleInput(request, params)
+            List<Organism> organismList = []
+
+            if (requestObject.organism) {
+                log.debug "finding info for specific organism"
+                Organism organism = Organism.findByCommonName(requestObject.organism)
+                if (!organism) organism = Organism.findById(requestObject.organism)
+                if (!organism) {
+                    render([error: "Organism not found"] as JSON)
+                    return
+                }
+                List<PermissionEnum> permissionEnumList = permissionService.getOrganismPermissionsForUser(organism, permissionService.getCurrentUser(requestObject))
+                if (permissionService.findHighestEnum(permissionEnumList)?.rank > PermissionEnum.NONE.rank) {
+                    organismList.add(organism)
+                }
+            } else {
+                log.debug "finding all info"
+                if (permissionService.isAdmin()) {
+                    organismList = Organism.all
+                } else {
+                    organismList = permissionService.getOrganismsForCurrentUser(requestObject)
+                }
+            }
+
+            if (!organismList) {
+                def error = [error: 'Not authorized for any organisms']
+                render error as JSON
+                return
+            }
+
+            UserOrganismPreference userOrganismPreference = UserOrganismPreference.findByUserAndCurrentOrganism(permissionService.getCurrentUser(requestObject), true, [max: 1, sort: "lastUpdated", order: "desc"])
+            Long defaultOrganismId = userOrganismPreference ? userOrganismPreference.organism.id : null
+
+            JSONArray jsonArray = new JSONArray()
+            for (Organism organism in organismList) {
+
+                def c = Feature.createCriteria()
+
+                def list = c.list {
+                    featureLocations {
+                        sequence {
+                            eq('organism', organism)
+                        }
+                    }
+                    'in'('class', requestHandlingService.viewableAnnotationList)
+                }
+                log.debug "${list}"
+                Integer annotationCount = list.size()
+                Integer sequenceCount = Sequence.countByOrganism(organism)
+
+                JSONObject jsonObject = [
+                        id             : organism.id,
+                        commonName     : organism.commonName,
+                        blatdb         : organism.blatdb,
+                        directory      : organism.directory,
+                        annotationCount: annotationCount,
+                        sequences      : sequenceCount,
+                        genus          : organism.genus,
+                        species        : organism.species,
+                        valid          : organism.valid,
+                        publicMode     : organism.publicMode,
+                        nonDefaultTranslationTable : organism.nonDefaultTranslationTable,
+                        metadata       : organism.metadata,
+                        currentOrganism: defaultOrganismId != null ? organism.id == defaultOrganismId : false
+                ] as JSONObject
+                jsonArray.add(jsonObject)
+            }
+            render jsonArray as JSON
+        }
+        catch (Exception e) {
+            e.printStackTrace()
+            def error = [error: e.message]
+            render error as JSON
+        }
+    }
+
+    /**
+     * Permissions handled upstream
+     * @return
+     */
+    def report() {
+        Map<Organism, OrganismSummary> organismSummaryListInstance = new TreeMap<>(new Comparator<Organism>() {
+            @Override
+            int compare(Organism o1, Organism o2) {
+                return o1.commonName <=> o2.commonName
+            }
+        })
+
+        // global version
+        OrganismSummary organismSummaryInstance = reportService.generateAllFeatureSummary()
+
+
+        Organism.listOrderByCommonName().each { organism ->
+            OrganismSummary thisOrganismSummaryInstance = reportService.generateOrganismSummary(organism)
+            organismSummaryListInstance.put(organism, thisOrganismSummaryInstance)
+        }
+
+
+        respond organismSummaryInstance, model: [organismSummaries: organismSummaryListInstance]
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'organism.label', default: 'Organism'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NOT_FOUND }
+        }
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/ProxyController.groovy b/grails-app/controllers/org/bbop/apollo/ProxyController.groovy
new file mode 100644
index 0000000..524aa3c
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/ProxyController.groovy
@@ -0,0 +1,152 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+
+import static org.springframework.http.HttpStatus.*
+
+ at Transactional(readOnly = true)
+class ProxyController {
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def permissionService
+    def proxyService
+
+    def beforeInterceptor = {
+        if (actionName != "request" && !permissionService.checkPermissions(PermissionEnum.ADMINISTRATE)) {
+            forward action: "notAuthorized", controller: "annotator"
+            return
+        }
+    }
+
+    def index(Integer max) {
+        params.max = Math.min(max ?: 10, 100)
+        respond Proxy.list(params), model: [proxyInstanceCount: Proxy.count()]
+    }
+
+    def show(Proxy proxyInstance) {
+        respond proxyInstance
+    }
+
+    def create() {
+        params.active = true
+        respond new Proxy(params)
+    }
+
+    /**
+     * @return
+     */
+    @Transactional
+    def request(String url) {
+        // only a logged-in user can use the proxy
+        User currentUser = permissionService.currentUser
+        if (!currentUser) {
+            log.warn "Attempting to proxy ${url} without a logged-in user"
+            render status: UNAUTHORIZED
+            return
+        }
+        String referenceUrl = URLDecoder.decode(url, "UTF-8")
+        Proxy proxy = proxyService.findProxyForUrl(referenceUrl)
+
+
+        if (!proxy) {
+            log.error "Proxy not found for ${referenceUrl}.  Please add a proxy (see the config guide)."
+            render status: NOT_FOUND
+            return
+        }
+
+        log.info "using proxy ${proxy?.targetUrl}"
+
+        String targetUrl = proxy ? proxy.targetUrl : referenceUrl
+
+        targetUrl += "?" + request.queryString
+        log.debug "target url: ${targetUrl}"
+        URL returnUrl = new URL(targetUrl)
+
+        log.debug "input URI ${request.requestURI}"
+        log.info "request url ${referenceUrl}?${request.getQueryString()}"
+        log.info "return url: ${returnUrl}"
+        render text: returnUrl.text
+    }
+
+
+    @Transactional
+    def save(Proxy proxyInstance) {
+        if (proxyInstance == null) {
+            notFound()
+            return
+        }
+
+        if (proxyInstance.hasErrors()) {
+            respond proxyInstance.errors, view: 'create'
+            return
+        }
+
+        proxyInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.created.message', args: [message(code: 'proxy.label', default: 'Proxy'), proxyInstance.id])
+                redirect proxyInstance
+            }
+            '*' { respond proxyInstance, [status: CREATED] }
+        }
+    }
+
+    def edit(Proxy proxyInstance) {
+        respond proxyInstance
+    }
+
+    @Transactional
+    def update(Proxy proxyInstance) {
+        if (proxyInstance == null) {
+            notFound()
+            return
+        }
+
+        if (proxyInstance.hasErrors()) {
+            respond proxyInstance.errors, view: 'edit'
+            return
+        }
+
+        proxyInstance.save flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.updated.message', args: [message(code: 'Proxy.label', default: 'Proxy'), proxyInstance.id])
+                redirect proxyInstance
+            }
+            '*' { respond proxyInstance, [status: OK] }
+        }
+    }
+
+    @Transactional
+    def delete(Proxy proxyInstance) {
+
+        if (proxyInstance == null) {
+            notFound()
+            return
+        }
+
+        proxyInstance.delete flush: true
+
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.deleted.message', args: [message(code: 'Proxy.label', default: 'Proxy'), proxyInstance.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NO_CONTENT }
+        }
+    }
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'proxy.label', default: 'Proxy'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NOT_FOUND }
+        }
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/SequenceController.groovy b/grails-app/controllers/org/bbop/apollo/SequenceController.groovy
new file mode 100644
index 0000000..6d22b99
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/SequenceController.groovy
@@ -0,0 +1,380 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.session.Session
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+import org.bbop.apollo.report.SequenceSummary
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+import javax.servlet.http.HttpServletResponse
+
+import static org.springframework.http.HttpStatus.NOT_FOUND
+
+ at RestApi(name = "Sequence Services", description = "Methods for retrieving sequence data")
+ at Transactional(readOnly = true)
+class SequenceController {
+
+
+    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
+
+    def sequenceService
+    def featureService
+    def requestHandlingService
+    def transcriptService
+    def permissionService
+    def preferenceService
+    def reportService
+
+    def permissions() {}
+
+    def beforeInterceptor = {
+        if (params.action == "sequenceByName"
+                || params.action == "sequenceByLocation"
+        ) {
+            response.setHeader("Access-Control-Allow-Origin", "*")
+        }
+    }
+
+    @Transactional
+    def setCurrentSequenceLocation(String name, Integer start, Integer end) {
+
+        try {
+            UserOrganismPreferenceDTO userOrganismPreference = preferenceService.setCurrentSequenceLocation(name, start, end, params[FeatureStringEnum.CLIENT_TOKEN.value].toString())
+            if (params.suppressOutput) {
+                render new JSONObject() as JSON
+            } else {
+                render userOrganismPreference.sequence as JSON
+            }
+        } catch (NumberFormatException e) {
+            //  we can ignore this specific exception as null is an acceptable value for start / end
+        }
+        catch (Exception e) {
+            def error = [error: e.message]
+            log.error e.message
+            render error as JSON
+        }
+    }
+
+    @Transactional
+    def setCurrentSequenceForNameAndOrganism(Organism organism) {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        Sequence sequence = Sequence.findByNameAndOrganism(inputObject.sequenceName, organism)
+        setCurrentSequence(sequence)
+    }
+
+    /**
+     * ID is the organism ID
+     * Sequence is the default sequence name
+     *
+     * If no sequence name is set, pull the preferences, otherwise just choose a random one.
+     * @param id
+     * @param sequenceName
+     * @return
+     */
+
+    @Transactional
+    def setCurrentSequence(Sequence sequenceInstance) {
+        JSONObject inputObject = permissionService.handleInput(request, params)
+        String token = inputObject.getString(FeatureStringEnum.CLIENT_TOKEN.value)
+        Organism organism = sequenceInstance.organism
+
+        User currentUser = permissionService.currentUser
+        UserOrganismPreferenceDTO userOrganismPreference = preferenceService.setCurrentSequence(currentUser, sequenceInstance, token)
+
+        Session session = SecurityUtils.subject.getSession(false)
+        session.setAttribute(FeatureStringEnum.DEFAULT_SEQUENCE_NAME.value, sequenceInstance.name)
+        session.setAttribute(FeatureStringEnum.SEQUENCE_NAME.value, sequenceInstance.name)
+        session.setAttribute(FeatureStringEnum.ORGANISM_JBROWSE_DIRECTORY.value, organism.directory)
+        session.setAttribute(FeatureStringEnum.ORGANISM_ID.value, sequenceInstance.organismId)
+
+        JSONObject sequenceObject = new JSONObject()
+        sequenceObject.put("id", sequenceInstance.id)
+        sequenceObject.put("name", sequenceInstance.name)
+        sequenceObject.put("length", sequenceInstance.length)
+        sequenceObject.put("start", sequenceInstance.start)
+        sequenceObject.put("end", sequenceInstance.end)
+        sequenceObject.startBp = userOrganismPreference.startbp
+        sequenceObject.endBp = userOrganismPreference.endbp
+
+        render sequenceObject as JSON
+    }
+
+
+    @Transactional
+    def loadSequences(Organism organism) {
+        if (!organism.sequences) {
+            sequenceService.loadRefSeqs(organism)
+        }
+
+        JSONArray sequenceArray = new JSONArray()
+        for (Sequence sequence in organism.sequences) {
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put("id", sequence.id)
+            jsonObject.put("name", sequence.name)
+            jsonObject.put("length", sequence.length)
+            jsonObject.put("start", sequence.start)
+            jsonObject.put("end", sequence.end)
+            sequenceArray.put(jsonObject)
+        }
+
+        render sequenceArray as JSON
+    }
+
+
+    protected void notFound() {
+        request.withFormat {
+            form multipartForm {
+                flash.message = message(code: 'default.not.found.message', args: [message(code: 'sequence.label', default: 'Sequence'), params.id])
+                redirect action: "index", method: "GET"
+            }
+            '*' { render status: NOT_FOUND }
+        }
+    }
+
+
+    @Transactional
+    def lookupSequenceByName(String q, String clientToken) {
+        Organism organism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+        def sequences = Sequence.findAllByNameIlikeAndOrganism(q + "%", organism, ["sort": "name", "order": "asc", "max": 20]).collect() {
+            it.name
+        }
+        render sequences as JSON
+    }
+
+    /**
+     * @deprecated TODO: will be removed as standalone will likely not be supported in the future.
+     * @return
+     */
+    def lookupSequenceByNameAndOrganism(String clientToken) {
+        JSONObject j;
+        for (k in params) {
+            j = JSON.parse(k.key)
+            break;
+        }
+        def organism
+        if (!j.name || !j.organism) {
+            organism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+        } else {
+            organism = Organism.findById(j.organism)
+        }
+        def seqid = j.name
+        def sequenceId = Sequence.findByNameAndOrganism(seqid, organism).id
+        JSONObject jsonObject = new JSONObject()
+        jsonObject.put(FeatureStringEnum.ID.value, sequenceId)
+        jsonObject.put(FeatureStringEnum.ORGANISM_ID.value, organism.id)
+        render jsonObject as JSON
+    }
+
+    @Transactional
+    def getSequences(String name, Integer start, Integer length, String sort, Boolean asc, Integer minFeatureLength, Integer maxFeatureLength, String clientToken) {
+        try {
+            Organism organism = preferenceService.getCurrentOrganismForCurrentUser(clientToken)
+
+            if (!organism) {
+                render([] as JSON)
+                return
+            }
+            def sequences = Sequence.createCriteria().list() {
+                if (name) {
+                    ilike('name', '%' + name + '%')
+                }
+                eq('organism', organism)
+                gt('length', minFeatureLength ?: 0)
+                lt('length', maxFeatureLength ?: Integer.MAX_VALUE)
+                if (sort == "length") {
+                    order('length', asc ? "asc" : "desc")
+                }
+                if (sort == "name") {
+                    order('name', asc ? "asc" : "desc")
+                }
+            }
+            def sequenceCounts = Feature.executeQuery("select fl.sequence.name, count(fl.sequence) from Feature f join f.featureLocations fl where fl.sequence.organism = :organism and fl.sequence.length < :maxFeatureLength and fl.sequence.length > :minFeatureLength and f.class in :viewableAnnotationList group by fl.sequence.name", [minFeatureLength: minFeatureLength ?: 0, maxFeatureLength: maxFeatureLength ?: Integer.MAX_VALUE, viewableAnnotationList: requestHandlingService.viewableAnnot [...]
+            def map = [:]
+            sequenceCounts.each {
+                map[it[0]] = it[1]
+            }
+            def results = sequences.collect { s ->
+                [id: s.id, length: s.length, start: s.start, end: s.end, count: map[s.name] ?: 0, name: s.name, sequenceCount: sequences.size()]
+            }
+            if (sort == "count") {
+                results = results.sort { it.count }
+                if (!asc) {
+                    results = results.reverse()
+                }
+            }
+            render results ? results[start..Math.min(start + length - 1, results.size() - 1)] as JSON : new JSONObject() as JSON
+        }
+        catch (PermissionException e) {
+            def error = [error: "Error: " + e]
+            render error as JSON
+        }
+    }
+
+    /**
+     * Permissions handled upstream
+     * @param organism
+     * @param max
+     * @return
+     */
+    def report(Organism organism, Integer max) {
+        organism = organism ?: Organism.first()
+        params.max = Math.min(max ?: 20, 100)
+
+        List<SequenceSummary> sequenceInstanceList = new ArrayList<>()
+        List<Sequence> sequences = Sequence.findAllByOrganism(organism, params)
+
+        sequences.each {
+            sequenceInstanceList.add(reportService.generateSequenceSummary(it))
+        }
+
+        int sequenceInstanceCount = Sequence.countByOrganism(organism)
+        render view: "report", model: [sequenceInstanceList: sequenceInstanceList, organism: organism, sequenceInstanceCount: sequenceInstanceCount]
+    }
+
+    @RestApiMethod(description = "Get sequence data within a range", path = "/sequence/<organism name>/<sequence name>:<fmin>..<fmax>?ignoreCache=<ignoreCache>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismString", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name or ID(required)")
+            , @RestApiParam(name = "sequenceName", type = "string", paramType = RestApiParamType.QUERY, description = "Sequence name(required)")
+            , @RestApiParam(name = "fmin", type = "integer", paramType = RestApiParamType.QUERY, description = "Minimum range(required)")
+            , @RestApiParam(name = "fmax", type = "integer", paramType = RestApiParamType.QUERY, description = "Maximum range (required)")
+            , @RestApiParam(name = "ignoreCache", type = "boolean", paramType = RestApiParamType.QUERY, description = "(default false).  Use cache for request if available.")
+    ])
+    @Transactional
+    String sequenceByLocation(String organismString, String sequenceName, int fmin, int fmax) {
+
+        Boolean ignoreCache = params.ignoreCache != null ? Boolean.valueOf(params.ignoreCache) : false
+        Map paramMap = new TreeMap<>()
+
+        if (!ignoreCache) {
+            String responseString = sequenceService.checkCache(organismString, sequenceName, fmin, fmax,  paramMap)
+            if (responseString) {
+                render responseString
+                return
+            }
+        }
+
+        Organism organism = Organism.findByCommonName(organismString) ?: Organism.findById(organismString as Long)
+        Sequence sequence = Sequence.findByNameAndOrganism(sequenceName, organism)
+
+        Strand strand = params.strand ? Strand.getStrandForValue(params.strand as Integer) : Strand.POSITIVE
+        String sequenceString = sequenceService.getGenomicResiduesFromSequenceWithAlterations(sequence, fmin, fmax, strand)
+        sequenceService.cacheRequest(sequenceString, organismString, sequenceName, fmin, fmax,  paramMap)
+        render sequenceString
+
+    }
+
+    @RestApiMethod(description = "Get sequence data as for a selected name", path = "/sequence/<organism name>/<sequence name>/<feature name>.<type>?ignoreCache=<ignoreCache>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismString", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name or ID (required)")
+            , @RestApiParam(name = "sequenceName", type = "string", paramType = RestApiParamType.QUERY, description = "Sequence name (required)")
+            , @RestApiParam(name = "featureName", type = "string", paramType = RestApiParamType.QUERY, description = "The uniqueName (UUID) or given name of the feature (typically transcript) of the element to retrieve sequence from")
+            , @RestApiParam(name = "type", type = "string", paramType = RestApiParamType.QUERY, description = "(default genomic) Return type: genomic, cds, cdna, peptide")
+            , @RestApiParam(name = "ignoreCache", type = "boolean", paramType = RestApiParamType.QUERY, description = "(default false).  Use cache for request if available.")
+    ])
+    @Transactional
+    String sequenceByName(String organismString, String sequenceName, String featureName, String type) {
+
+        Boolean ignoreCache = params.ignoreCache != null ? Boolean.valueOf(params.ignoreCache) : false
+        Map paramMap = new TreeMap<>()
+        paramMap.put("name", featureName)
+
+
+        if (!ignoreCache) {
+            String responseString = sequenceService.checkCache(organismString, sequenceName, featureName, type, paramMap)
+            if (responseString) {
+                render responseString
+                return
+            }
+        }
+
+        Feature feature = Feature.findByUniqueName(featureName)
+        if (!feature) {
+            def features = Feature.findAllByName(featureName)
+
+            for (int i = 0; i < features.size() && !feature; i++) {
+                Feature f = features.get(i)
+                Sequence s = f.featureLocation.sequence
+                if (f.featureLocation.sequence.name == sequenceName
+                        && (s.organism.commonName == organismString || s.organism.id == organismString)
+                ) {
+                    feature = f
+                }
+            }
+        }
+
+        if (feature) {
+            String sequenceString = sequenceService.getSequenceForFeature(feature, type)
+            if(sequenceString?.trim()){
+                render sequenceString
+                sequenceService.cacheRequest(sequenceString, organismString, sequenceName, featureName, type, paramMap)
+                return
+            }
+        }
+        response.status = 404
+    }
+
+    @RestApiMethod(description = "Remove sequence cache for an organism and sequence", path = "/sequence/cache/clear/<organism name>/<sequence name>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismName", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name (required)")
+            , @RestApiParam(name = "sequenceName", type = "string", paramType = RestApiParamType.QUERY, description = "Sequence name (required)")
+    ])
+    @Transactional
+    def clearSequenceCache(String organismName, String sequenceName) {
+        if (!checkPermission(organismName)) return
+        int removed = SequenceCache.countByOrganismNameAndSequenceName(organismName, sequenceName)
+        SequenceCache.deleteAll(SequenceCache.findAllByOrganismNameAndSequenceName(organismName, sequenceName))
+        render new JSONObject(removed: removed) as JSON
+    }
+
+    @RestApiMethod(description = "Remove sequence cache for an organism", path = "/sequence/cache/clear/<organism name>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismName", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name (required) or 'ALL' if admin")
+    ])
+    @Transactional
+    def clearOrganismCache(String organismName) {
+        if (organismName.toLowerCase().equals("all") && permissionService.isAdmin()) {
+            log.info "Deleting cache for all organisms"
+            JSONArray jsonArray = new JSONArray()
+            Organism.all.each { organism ->
+                int removed = SequenceCache.countByOrganismName(organism.commonName)
+                SequenceCache.deleteAll(SequenceCache.findAllByOrganismName(organism.commonName))
+                JSONObject jsonObject = new JSONObject(name: organism.commonName, removed: removed) as JSONObject
+                jsonArray.add(jsonObject)
+            }
+
+            render jsonArray as JSON
+        } else {
+            log.info "Deleting cache for ${organismName}"
+            if (!checkPermission(organismName)) return
+            int removed = SequenceCache.countByOrganismName(organismName)
+            SequenceCache.deleteAll(SequenceCache.findAllByOrganismName(organismName))
+            render new JSONObject(removed: removed) as JSON
+        }
+
+    }
+
+    def checkPermission(String organismString) {
+        Organism organism = preferenceService.getOrganismForToken(organismString)
+        if (organism.publicMode || permissionService.checkPermissions(PermissionEnum.READ)) {
+            return true
+        } else {
+            // not accessible to the public
+            response.status = HttpServletResponse.SC_FORBIDDEN
+            render ""
+            return false
+        }
+
+    }
+}
diff --git a/grails-app/controllers/org/bbop/apollo/TrackController.groovy b/grails-app/controllers/org/bbop/apollo/TrackController.groovy
new file mode 100644
index 0000000..a35dc45
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/TrackController.groovy
@@ -0,0 +1,309 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.sequence.SequenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+
+import javax.servlet.http.HttpServletResponse
+
+ at RestApi(name = "Track Services", description = "Methods for retrieving track data")
+ at Transactional(readOnly = true)
+class TrackController {
+
+    def preferenceService
+    def permissionService
+    def trackService
+    def grailsApplication
+    def svgService
+
+
+    def beforeInterceptor = {
+        if (params.action == "featuresByName"
+                || params.action == "featuresByLocation"
+        ) {
+            response.setHeader("Access-Control-Allow-Origin", "*")
+        }
+    }
+
+    /**
+     * Just a convenience method
+     * @param trackName
+     * @param organismString
+     * @param sequence
+     * @return
+     */
+    def trackData(String organismString, String trackName, String sequence) {
+        JSONObject jsonObject = trackService.getTrackData(trackName, organismString, sequence)
+        render jsonObject as JSON
+    }
+
+    @RestApiMethod(description = "Remove track cache for an organism and track", path = "/track/cache/clear/<organism name>/<track name>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismName", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name (required)")
+            , @RestApiParam(name = "trackName", type = "string", paramType = RestApiParamType.QUERY, description = "Track name (required)")
+    ])
+    @Transactional
+    def clearTrackCache(String organismName, String trackName) {
+        if (!checkPermission(organismName)) return
+        int removed = TrackCache.countByOrganismNameAndTrackName(organismName, trackName)
+        TrackCache.deleteAll(TrackCache.findAllByOrganismNameAndTrackName(organismName, trackName))
+        render new JSONObject(removed: removed) as JSON
+    }
+
+    @RestApiMethod(description = "Remove track cache for an organism", path = "/track/cache/clear/<organism name>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismName", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name (required) or 'ALL' if admin")
+    ])
+    @Transactional
+    def clearOrganismCache(String organismName) {
+        if (organismName.toLowerCase().equals("all") && permissionService.isAdmin()) {
+            log.info "Deleting cache for all organisms"
+            JSONArray jsonArray = new JSONArray()
+            Organism.all.each { organism ->
+                int removed = TrackCache.countByOrganismName(organism.commonName)
+                TrackCache.deleteAll(TrackCache.findAllByOrganismName(organism.commonName))
+                JSONObject jsonObject = new JSONObject(name: organism.commonName, removed: removed) as JSONObject
+                jsonArray.add(jsonObject)
+            }
+
+            render jsonArray as JSON
+        } else {
+            log.info "Deleting cache for ${organismName}"
+            if (!checkPermission(organismName)) return
+            int removed = TrackCache.countByOrganismName(organismName)
+            TrackCache.deleteAll(TrackCache.findAllByOrganismName(organismName))
+            render new JSONObject(removed: removed) as JSON
+        }
+
+    }
+
+
+    @RestApiMethod(description = "Get track data as an JSON within but only for the selected name", path = "/track/<organism name>/<track name>/<sequence name>/<feature name>.<type>?ignoreCache=<ignoreCache>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismString", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name or ID(required)")
+            , @RestApiParam(name = "trackName", type = "string", paramType = RestApiParamType.QUERY, description = "Track name(required)")
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "Sequence name(required)")
+            , @RestApiParam(name = "featureName", type = "string", paramType = RestApiParamType.QUERY, description = "If top-level feature 'id' matches, then annotate with 'selected'=1")
+            , @RestApiParam(name = "ignoreCache", type = "boolean", paramType = RestApiParamType.QUERY, description = "(default false).  Use cache for request if available.")
+            , @RestApiParam(name = "flatten", type = "string", paramType = RestApiParamType.QUERY, description = "Brings nested top-level components to the root level.  If not provided or 'false' it will not flatten.  Default is 'gene'." )
+            , @RestApiParam(name = "type", type = "json/svg", paramType = RestApiParamType.QUERY, description = ".json or .svg")
+    ])
+    @Transactional
+    def featuresByName(String organismString, String trackName, String sequence, String featureName, String type) {
+        if (!checkPermission(organismString)) return
+
+        Boolean ignoreCache = params.ignoreCache != null ? Boolean.valueOf(params.ignoreCache) : false
+        Map paramMap = new TreeMap<>()
+        paramMap.put("name", featureName)
+        String flatten = params.flatten != null ? params.flatten : 'gene'
+        flatten = flatten == 'false' ? '' : flatten
+        paramMap.put("onlySelected", true)
+        if (!ignoreCache) {
+            String responseString = trackService.checkCache(organismString, trackName, sequence, featureName, type, paramMap)
+            if (responseString) {
+                if (type == "json") {
+                    render JSON.parse(responseString)  as JSON
+                    return
+                }
+                else
+                if (type == "svg") {
+                    render responseString
+                    return
+                }
+            }
+        }
+
+        Organism organism = preferenceService.getOrganismForToken(organismString)
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                organismCommonName: organism.commonName
+                , trackName: trackName
+                , sequenceName: sequence
+        )
+        JSONArray renderedArray
+        try {
+            JSONArray filteredList = trackService.getNCList(trackName, organismString, sequence, -1, -1)
+            renderedArray = trackService.convertAllNCListToObject(filteredList, sequenceDTO)
+        } catch (FileNotFoundException fnfe) {
+            log.warn(fnfe.message)
+            response.status = 404
+            return
+        }
+
+        JSONArray returnArray = new JSONArray()
+        for (JSONObject returnObject in renderedArray) {
+            // only set if true?
+            returnObject.id = createLink(absolute: true, uri: "/track/${organism.commonName}/${trackName}/${sequence}/${featureName}.json")
+            if (returnObject?.name == featureName) {
+                returnObject.selected = true
+                returnArray.add(returnObject)
+            }
+        }
+
+        if(flatten){
+            returnArray  = trackService.flattenArray(returnArray,flatten)
+        }
+
+        if (type == "json") {
+            trackService.cacheRequest(returnArray.toString(), organismString, trackName, sequence, featureName, type, paramMap)
+            render returnArray as JSON
+        } else if (type == "svg") {
+            String xmlString = svgService.renderSVGFromJSONArray(returnArray)
+            trackService.cacheRequest(xmlString, organismString, trackName, sequence, featureName, type, paramMap)
+            render xmlString
+        }
+
+    }
+
+    private static Set<String> getNames(def name){
+        Set<String> nameSet = new HashSet<>()
+        if(name){
+            if(name instanceof String[]){
+                name.each {
+                    nameSet.add(it)
+                }
+            }
+            else
+            if(name instanceof String){
+                nameSet.add(name)
+            }
+        }
+        return nameSet
+    }
+
+    @RestApiMethod(description = "Get track data as an JSON within an range", path = "/track/<organism name>/<track name>/<sequence name>:<fmin>..<fmax>.<type>?name=<name>&onlySelected=<onlySelected>&ignoreCache=<ignoreCache>", verb = RestApiVerb.GET)
+    @RestApiParams(params = [
+            @RestApiParam(name = "organismString", type = "string", paramType = RestApiParamType.QUERY, description = "Organism common name or ID(required)")
+            , @RestApiParam(name = "trackName", type = "string", paramType = RestApiParamType.QUERY, description = "Track name(required)")
+            , @RestApiParam(name = "sequence", type = "string", paramType = RestApiParamType.QUERY, description = "Sequence name(required)")
+            , @RestApiParam(name = "fmin", type = "integer", paramType = RestApiParamType.QUERY, description = "Minimum range(required)")
+            , @RestApiParam(name = "fmax", type = "integer", paramType = RestApiParamType.QUERY, description = "Maximum range (required)")
+            , @RestApiParam(name = "name", type = "string / string[]", paramType = RestApiParamType.QUERY, description = "If top-level feature 'name' matches, then annotate with 'selected'=true.  Multiple names can be passed in.")
+            , @RestApiParam(name = "onlySelected", type = "string", paramType = RestApiParamType.QUERY, description = "(default false).  If 'selected'!=1 one, then exclude.")
+            , @RestApiParam(name = "ignoreCache", type = "boolean", paramType = RestApiParamType.QUERY, description = "(default false).  Use cache for request if available.")
+            , @RestApiParam(name = "flatten", type = "string", paramType = RestApiParamType.QUERY, description = "Brings nested top-level components to the root level.  If not provided or 'false' it will not flatten.  Default is 'gene'.")
+            , @RestApiParam(name = "type", type = "string", paramType = RestApiParamType.QUERY, description = ".json or .svg")
+    ])
+    @Transactional
+    def featuresByLocation(String organismString, String trackName, String sequence, Long fmin, Long fmax, String type) {
+        if (!checkPermission(organismString)) return
+
+        Set<String> nameSet = getNames(params.name ? params.name : "")
+        Boolean onlySelected = params.onlySelected != null ? params.onlySelected : false
+        String flatten = params.flatten != null ? params.flatten : 'gene'
+        flatten = flatten == 'false' ? '' : flatten
+        Boolean ignoreCache = params.ignoreCache != null ? Boolean.valueOf(params.ignoreCache) : false
+        Map paramMap = new TreeMap<>()
+        paramMap.put("type", type)
+        if (nameSet) {
+            paramMap.put("name", nameSet)
+            paramMap.put("onlySelected", onlySelected)
+        }
+        if (!ignoreCache) {
+            String responseString = trackService.checkCache(organismString, trackName, sequence, fmin, fmax, type, paramMap)
+            if (responseString) {
+                if (type == "json") {
+                    render JSON.parse(responseString) as JSON
+                    return
+                } else if (type == "svg") {
+                    render responseString
+                    return
+                }
+            }
+        }
+        JSONArray renderedArray
+        Organism organism = preferenceService.getOrganismForToken(organismString)
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                organismCommonName: organism.commonName
+                , trackName: trackName
+                , sequenceName: sequence
+        )
+        try {
+            JSONArray filteredList = trackService.getNCList(trackName, organismString, sequence, fmin, fmax)
+            renderedArray = trackService.convertAllNCListToObject(filteredList, sequenceDTO)
+        } catch (FileNotFoundException fnfe) {
+            log.warn(fnfe.message)
+            response.status = 404
+            return
+        }
+
+        if (flatten) {
+            renderedArray = trackService.flattenArray(renderedArray, flatten)
+        }
+
+        JSONArray returnArray = new JSONArray()
+        for (JSONObject returnObject in renderedArray) {
+            // only set if true?
+            if (returnObject.name) {
+                returnObject.id = createLink(absolute: true, uri: "/track/${organism.commonName}/${trackName}/${sequence}/${returnObject.name}.json")
+            }
+            if (nameSet) {
+                if (returnObject.name && nameSet.contains(returnObject?.name)) {
+                    returnObject.selected = true
+                    if (onlySelected) {
+                        returnArray.add(returnObject)
+                    }
+                }
+            }
+        }
+
+        if (onlySelected) {
+            renderedArray = returnArray
+        }
+
+        if (type == "json") {
+            trackService.cacheRequest(renderedArray.toString(), organismString, trackName, sequence, fmin, fmax, type, paramMap)
+            render renderedArray as JSON
+        } else if (type == "svg") {
+            String xmlString = svgService.renderSVGFromJSONArray(returnArray)
+            trackService.cacheRequest(xmlString, organismString, trackName, sequence, fmin, fmax, type, paramMap)
+            render xmlString
+        }
+    }
+
+    def biolink(String organismString, String trackName, String sequence, Long fmin, Long fmax) {
+        if (!checkPermission(organismString)) return
+        JSONArray filteredList = trackService.getNCList(trackName, organismString, sequence, fmin, fmax)
+        JSONObject renderdObject = trackService.getNCListAsBioLink(filteredList)
+        render renderdObject as JSON
+    }
+
+/**
+ *
+ * @param trackName
+ * @param organism
+ * @param sequence
+ * @param fmin
+ * @param fmax
+ * @return
+ */
+// TODO: this is just for debuggin
+// track < organism ID or name > / <track name > /  < sequence name > / min / max
+    def nclist(String organismString, String trackName, String sequence, Long fmin, Long fmax) {
+        if (!checkPermission(organismString)) return
+        JSONArray filteredList = trackService.getNCList(trackName, organismString, sequence, fmin, fmax)
+        render filteredList as JSON
+    }
+
+    def checkPermission(String organismString) {
+        Organism organism = preferenceService.getOrganismForToken(organismString)
+        if (organism.publicMode || permissionService.checkPermissions(PermissionEnum.READ)) {
+            return true
+        } else {
+            // not accessible to the public
+            response.status = HttpServletResponse.SC_FORBIDDEN
+            render ""
+            return false
+        }
+
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/UserController.groovy b/grails-app/controllers/org/bbop/apollo/UserController.groovy
new file mode 100644
index 0000000..ebae52c
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/UserController.groovy
@@ -0,0 +1,578 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.crypto.hash.Sha256Hash
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.restapidoc.annotation.RestApi
+import org.restapidoc.annotation.RestApiMethod
+import org.restapidoc.annotation.RestApiParam
+import org.restapidoc.annotation.RestApiParams
+import org.restapidoc.pojo.RestApiParamType
+import org.restapidoc.pojo.RestApiVerb
+import org.springframework.http.HttpStatus
+
+ at RestApi(name = "User Services", description = "Methods for managing users")
+ at Transactional(readOnly = true)
+class UserController {
+
+    def permissionService
+    def preferenceService
+    def userService
+
+
+    @RestApiMethod(description = "Load all users and their permissions", path = "/user/loadUsers", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "userId", type = "long / string", paramType = RestApiParamType.QUERY, description = "Optionally only user a specific userId as an integer database id or a username string")
+            , @RestApiParam(name = "start", type = "long / string", paramType = RestApiParamType.QUERY, description = "(optional) Result start / offset")
+            , @RestApiParam(name = "length", type = "long / string", paramType = RestApiParamType.QUERY, description = "(optional) Result length")
+            , @RestApiParam(name = "name", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Search name")
+            , @RestApiParam(name = "sortColumn", type = "string", paramType = RestApiParamType.QUERY, description = "(optional) Sort column, default 'name'")
+            , @RestApiParam(name = "sortAscending", type = "boolean", paramType = RestApiParamType.QUERY, description = "(optional) Sort column is ascending if true (default false)")
+            , @RestApiParam(name = "omitEmptyOrganisms", type = "boolean", paramType = RestApiParamType.QUERY, description = "(optional) Omits empty organism permissions from return (default false)")
+    ])
+    def loadUsers() {
+        try {
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            JSONArray returnArray = new JSONArray()
+            if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+
+            def allowableOrganisms = permissionService.getOrganisms(permissionService.currentUser)
+
+            List<String> allUserGroups = UserGroup.all.name
+            Map<String, List<UserOrganismPermission>> userOrganismPermissionMap = new HashMap<>()
+            List<UserOrganismPermission> userOrganismPermissionList = UserOrganismPermission.findAllByOrganismInList(allowableOrganisms as List)
+            for (UserOrganismPermission userOrganismPermission in userOrganismPermissionList) {
+                List<UserOrganismPermission> userOrganismPermissionListTemp = userOrganismPermissionMap.get(userOrganismPermission.user.username)
+                if (userOrganismPermissionListTemp == null) {
+                    userOrganismPermissionListTemp = new ArrayList<>()
+                }
+                userOrganismPermissionListTemp.add(userOrganismPermission)
+                userOrganismPermissionMap.put(userOrganismPermission.user.username, userOrganismPermissionListTemp)
+            }
+
+            def c = User.createCriteria()
+            def offset = dataObject.start ?: 0
+            def maxResults = dataObject.length ?: Integer.MAX_VALUE
+            def searchName = dataObject.name ?: null
+            def sortName = dataObject.sortColumn ?: 'name'
+            def sortAscending = dataObject.sortAscending ?: true
+            def omitEmptyOrganisms = dataObject.omitEmptyOrganisms!=null ? dataObject.omitEmptyOrganisms : false
+
+            def users = c.list(max: maxResults, offset: offset) {
+                if (dataObject.userId && dataObject.userId in Integer) {
+                    eq('id', (Long) dataObject.userId)
+                }
+                if (dataObject.userId && dataObject.userId in String) {
+                    eq('username', dataObject.userId)
+                }
+                if (searchName) {
+                    or {
+                        ilike('firstName', '%' + searchName + '%')
+                        ilike('lastName', '%' + searchName + '%')
+                        ilike('username', '%' + searchName + '%')
+                    }
+                }
+                if(sortName){
+                    switch(sortName){
+                        case "name":
+                            order('firstName', sortAscending?"asc":"desc")
+                            order('lastName', sortAscending?"asc":"desc")
+                            break
+                        case "email":
+                            order('username', sortAscending?"asc":"desc")
+                            break
+                    }
+                }
+            }.unique { a, b ->
+                a.id <=> b.id
+            }
+
+            int userCount = User.withCriteria{
+                if (dataObject.userId && dataObject.userId in Integer) {
+                    eq('id', (Long) dataObject.userId)
+                }
+                if (dataObject.userId && dataObject.userId in String) {
+                    eq('username', dataObject.userId)
+                }
+                if (searchName) {
+                    or {
+                        ilike('firstName', '%' + searchName + '%')
+                        ilike('lastName', '%' + searchName + '%')
+                        ilike('username', '%' + searchName + '%')
+                    }
+                }
+            }.unique { a, b ->
+                a.id <=> b.id
+            }.size()
+
+            users.each {
+                def userObject = new JSONObject()
+
+                userObject.userId = it.id
+                userObject.username = it.username
+                userObject.firstName = it.firstName
+                userObject.lastName = it.lastName
+                Role role = userService.getHighestRole(it)
+                userObject.role = role?.name
+
+
+                JSONArray groupsArray = new JSONArray()
+                List<String> groupsForUser = new ArrayList<>()
+                for (group in it.userGroups) {
+                    JSONObject groupJson = new JSONObject()
+                    groupsForUser.add(group.name)
+                    groupJson.put("name", group.name)
+                    groupsArray.add(groupJson)
+                }
+                userObject.groups = groupsArray
+
+
+                JSONArray availableGroupsArray = new JSONArray()
+                List<String> availableGroups = allUserGroups - groupsForUser
+                for (group in availableGroups) {
+                    JSONObject groupJson = new JSONObject()
+                    groupJson.put("name", group)
+                    availableGroupsArray.add(groupJson)
+                }
+                userObject.availableGroups = availableGroupsArray
+
+                // organism permissions
+                JSONArray organismPermissionsArray = new JSONArray()
+                def userOrganismPermissionList3 = userOrganismPermissionMap.get(it.username)
+                List<Long> organismsWithPermissions = new ArrayList<>()
+                log.debug "number of groups for user: ${userOrganismPermissionList3?.size()} for ${it.username}"
+                for (UserOrganismPermission userOrganismPermission in userOrganismPermissionList3) {
+                    if (userOrganismPermission.organism in allowableOrganisms) {
+                        JSONObject organismJSON = new JSONObject()
+                        organismJSON.put("organism", userOrganismPermission.organism.commonName)
+                        organismJSON.put("permissions", userOrganismPermission.permissions)
+                        organismJSON.put("permissionArray", userOrganismPermission.permissionValues)
+                        organismJSON.put("userId", userOrganismPermission.userId)
+                        organismJSON.put("id", userOrganismPermission.id)
+                        organismPermissionsArray.add(organismJSON)
+                        organismsWithPermissions.add(userOrganismPermission.organism.id)
+                    }
+                }
+
+                // if an organism has permissions
+                Set<Organism> organismList = allowableOrganisms.findAll() {
+                    !organismsWithPermissions.contains(it.id)
+                }
+
+                if(!omitEmptyOrganisms){
+                    for (Organism organism in organismList) {
+                        JSONObject organismJSON = new JSONObject()
+                        organismJSON.put("organism", organism.commonName)
+                        organismJSON.put("permissions", "[]")
+                        organismJSON.put("permissionArray", new JSONArray())
+                        organismJSON.put("userId", it.id)
+                        organismPermissionsArray.add(organismJSON)
+                    }
+                }
+
+
+                userObject.organismPermissions = organismPermissionsArray
+
+                // could probably be done in a separate object
+                userObject.userCount = userCount
+                userObject.searchName = searchName
+
+                returnArray.put(userObject)
+            }
+
+            render returnArray as JSON
+        }
+        catch (Exception e) {
+            response.status = HttpStatus.INTERNAL_SERVER_ERROR.value()
+            def error = [error: e.message]
+            log.error error
+            render error as JSON
+        }
+    }
+
+    @Transactional
+    def checkLogin() {
+        def currentUser = permissionService.currentUser
+        preferenceService.evaluateSaves(true)
+
+        // grab from session
+        if (!currentUser) {
+            def authToken = null
+            if (request.getParameter("username")) {
+                String username = request.getParameter("username")
+                String password = request.getParameter("password")
+                authToken = new UsernamePasswordToken(username, password)
+            }
+
+            if (permissionService.authenticateWithToken(request)) {
+                currentUser = permissionService.currentUser
+            } else {
+                log.error("Failed to authenticate")
+            }
+        }
+
+        if (currentUser) {
+            UserOrganismPreference userOrganismPreference
+            try {
+                // sets it by default
+                userOrganismPreference = preferenceService.getCurrentOrganismPreferenceInDB(params[FeatureStringEnum.CLIENT_TOKEN.value])
+            } catch (e) {
+                log.error(e)
+            }
+
+            def userObject = userService.convertUserToJson(currentUser)
+
+            if ((!userOrganismPreference || !permissionService.hasAnyPermissions(currentUser)) && !permissionService.isUserAdmin(currentUser)) {
+                userObject.put(FeatureStringEnum.ERROR.value, "You do not have access to any organism on this server.  Please contact your administrator.")
+            } else if (userOrganismPreference) {
+                userObject.put("tracklist", userOrganismPreference.nativeTrackList)
+            }
+
+            render userObject as JSON
+        } else {
+            def userObject = new JSONObject()
+            userObject.put(FeatureStringEnum.HAS_USERS.value, User.count > 0)
+            render userObject as JSON
+        }
+    }
+
+
+    @Transactional
+    def updateTrackListPreference() {
+        try {
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            if (!permissionService.hasPermissions(dataObject, PermissionEnum.READ)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            log.info "updateTrackListPreference"
+
+            UserOrganismPreference uop = preferenceService.getCurrentOrganismPreferenceInDB(dataObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+
+            uop.nativeTrackList = dataObject.get("tracklist")
+            uop.save(flush: true)
+            log.info "Added userOrganismPreference ${uop.nativeTrackList}"
+            render new JSONObject() as JSON
+        }
+        catch (Exception e) {
+            log.error "${e.message}"
+            render([error: e.message]) as JSON
+        }
+    }
+
+
+    @RestApiMethod(description = "Add user to group", path = "/user/addUserToGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "group", type = "string", paramType = RestApiParamType.QUERY, description = "Group name")
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User id")
+            , @RestApiParam(name = "user", type = "email", paramType = RestApiParamType.QUERY, description = "User email/username (supplied if user id unknown)")
+    ])
+    @Transactional
+    def addUserToGroup() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        log.info "Adding user to group"
+        UserGroup userGroup = UserGroup.findByName(dataObject.group)
+        User user = dataObject.userId ? User.findById(dataObject.userId) : User.findByUsername(dataObject.user)
+        user.addToUserGroups(userGroup)
+        user.save(flush: true)
+        log.info "Added user ${user.username} to group ${userGroup.name}"
+        render new JSONObject() as JSON
+    }
+
+    @RestApiMethod(description = "Remove user from group", path = "/user/removeUserFromGroup", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "group", type = "string", paramType = RestApiParamType.QUERY, description = "Group name")
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User id")
+            , @RestApiParam(name = "user", type = "email", paramType = RestApiParamType.QUERY, description = "User email/username (supplied if user id unknown)")
+    ])
+    @Transactional
+    def removeUserFromGroup() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        log.info "Removing user from group"
+        UserGroup userGroup = UserGroup.findByName(dataObject.group)
+        User user = dataObject.userId ? User.findById(dataObject.userId) : User.findByUsername(dataObject.user)
+        user.removeFromUserGroups(userGroup)
+        user.save(flush: true)
+        log.info "Removed user ${user.username} from group ${userGroup.name}"
+        render new JSONObject() as JSON
+    }
+
+    @RestApiMethod(description = "Create user", path = "/user/createUser", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "email", type = "email", paramType = RestApiParamType.QUERY, description = "Email of the user to add")
+            , @RestApiParam(name = "firstName", type = "string", paramType = RestApiParamType.QUERY, description = "First name of user to add")
+            , @RestApiParam(name = "lastName", type = "string", paramType = RestApiParamType.QUERY, description = "Last name of user to add")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "User metadata (optional)")
+            , @RestApiParam(name = "role", type = "string", paramType = RestApiParamType.QUERY, description = "User role USER / ADMIN (optional: default USER) ")
+            , @RestApiParam(name = "newPassword", type = "string", paramType = RestApiParamType.QUERY, description = "Password of user to add")
+    ])
+    @Transactional
+    def createUser() {
+        try {
+            log.info "Creating user"
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            if (User.findByUsername(dataObject.email) != null) {
+                JSONObject error = new JSONObject()
+                error.put(FeatureStringEnum.ERROR.value, "User already exists. Please enter a new username")
+                render error.toString()
+                return
+            }
+
+            User user = new User(
+                    firstName: dataObject.firstName
+                    , lastName: dataObject.lastName
+                    , username: dataObject.email
+                    , metadata: dataObject.metadata ?: null
+                    , passwordHash: new Sha256Hash(dataObject.newPassword ?: dataObject.password).toHex()
+            )
+            user.save(insert: true)
+
+            String roleString = dataObject.role ?: UserService.USER
+            Role role = Role.findByName(roleString.toUpperCase())
+            if (!role) {
+                role = Role.findByName(UserService.USER)
+            }
+            log.debug "adding role: ${role}"
+            user.addToRoles(role)
+            role.addToUsers(user)
+            role.save()
+            user.save(flush: true)
+
+            log.info "Added user ${user.username} with role ${role.name}"
+
+            render new JSONObject() as JSON
+        } catch (e) {
+            log.error(e.fillInStackTrace())
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to add the user " + e.message)
+            render jsonObject as JSON
+        }
+
+    }
+
+    @RestApiMethod(description = "Delete user", path = "/user/deleteUser", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User ID to delete")
+            , @RestApiParam(name = "userToDelete", type = "email", paramType = RestApiParamType.QUERY, description = "Username (email) to delete")
+    ])
+    @Transactional
+    def deleteUser() {
+        try {
+            log.info "Removing user"
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            if (!permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            User user = null
+            if (dataObject.has('userId')) {
+                user = User.findById(dataObject.userId)
+            }
+            // to support the webservice
+            if (!user && dataObject.has("userToDelete")) {
+                user = User.findByUsername(dataObject.userToDelete)
+            }
+            user.userGroups.each { it ->
+                it.removeFromUsers(user)
+            }
+            FeatureEvent.deleteAll(FeatureEvent.findAllByEditor(user))
+            UserTrackPermission.deleteAll(UserTrackPermission.findAllByUser(user))
+            UserOrganismPermission.deleteAll(UserOrganismPermission.findAllByUser(user))
+            UserOrganismPreference.deleteAll(UserOrganismPreference.findAllByUser(user))
+            user.delete(flush: true)
+
+            log.info "Removed user ${user.username}"
+
+            render new JSONObject() as JSON
+        } catch (e) {
+            log.error(e.fillInStackTrace())
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to delete the user " + e.message)
+            render jsonObject as JSON
+        }
+    }
+
+    @RestApiMethod(description = "Update user", path = "/user/updateUser", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User ID to update")
+            , @RestApiParam(name = "email", type = "email", paramType = RestApiParamType.QUERY, description = "Email of the user to update")
+            , @RestApiParam(name = "firstName", type = "string", paramType = RestApiParamType.QUERY, description = "First name of user to update")
+            , @RestApiParam(name = "lastName", type = "string", paramType = RestApiParamType.QUERY, description = "Last name of user to update")
+            , @RestApiParam(name = "metadata", type = "string", paramType = RestApiParamType.QUERY, description = "User metadata (optional)")
+            , @RestApiParam(name = "newPassword", type = "string", paramType = RestApiParamType.QUERY, description = "Password of user to update")
+    ]
+    )
+    @Transactional
+    def updateUser() {
+        try {
+            log.info "Updating user"
+            JSONObject dataObject = permissionService.handleInput(request, params)
+            if (!permissionService.sameUser(dataObject, request) && !permissionService.hasGlobalPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+                render status: HttpStatus.UNAUTHORIZED
+                return
+            }
+            User user = User.findById(dataObject.userId)
+            user.firstName = dataObject.firstName
+            user.lastName = dataObject.lastName
+            user.username = dataObject.email
+            user.metadata = dataObject.metadata ?: null
+
+            if (dataObject.newPassword) {
+                user.passwordHash = new Sha256Hash(dataObject.newPassword).toHex()
+            }
+
+            String roleString = dataObject.role
+            Role currentRole = userService.getHighestRole(user)
+
+            if (!currentRole || !roleString.equalsIgnoreCase(currentRole.name)) {
+                if (currentRole) {
+                    user.removeFromRoles(currentRole)
+                }
+                Role role = Role.findByName(roleString.toUpperCase())
+                user.addToRoles(role)
+            }
+            log.info "Updated user"
+            user.save(flush: true)
+            render new JSONObject() as JSON
+        } catch (e) {
+            log.error(e.fillInStackTrace())
+            JSONObject jsonObject = new JSONObject()
+            jsonObject.put(FeatureStringEnum.ERROR.value, "Failed to update the user " + e.message)
+            render jsonObject as JSON
+        }
+
+    }
+
+    @RestApiMethod(description = "Get organism permissions for user, returns an array of permission objects", path = "/user/getOrganismPermissionsForUser", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User ID to fetch")
+    ])
+    def getOrganismPermissionsForUser() {
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        User user = User.findById(dataObject.userId)
+
+        List<UserOrganismPermission> userOrganismPermissionList = UserOrganismPermission.findAllByUser(user)
+
+        render userOrganismPermissionList as JSON
+    }
+
+    /**
+     * Only changing one of the boolean permissions
+     * @return
+     */
+    @RestApiMethod(description = "Update organism permissions", path = "/user/updateOrganismPermission", verb = RestApiVerb.POST)
+    @RestApiParams(params = [
+            @RestApiParam(name = "username", type = "email", paramType = RestApiParamType.QUERY)
+            , @RestApiParam(name = "password", type = "password", paramType = RestApiParamType.QUERY)
+
+            , @RestApiParam(name = "userId", type = "long", paramType = RestApiParamType.QUERY, description = "User ID to modify permissions for")
+            , @RestApiParam(name = "user", type = "email", paramType = RestApiParamType.QUERY, description = "(Optional) user email of the user to modify permissions for if User ID is not provided")
+            , @RestApiParam(name = "organism", type = "string", paramType = RestApiParamType.QUERY, description = "Name of organism to update")
+            , @RestApiParam(name = "id", type = "long", paramType = RestApiParamType.QUERY, description = "Permission ID to update (can get from userId/organism instead)")
+
+            , @RestApiParam(name = "ADMINISTRATE", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has administrative and all lesser (including user/group) privileges for the organism")
+            , @RestApiParam(name = "WRITE", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has write and all lesser privileges for the organism")
+            , @RestApiParam(name = "EXPORT", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has export and all lesser privileges for the organism")
+            , @RestApiParam(name = "READ", type = "boolean", paramType = RestApiParamType.QUERY, description = "Indicate if user has read and all lesser privileges for the organism")
+    ])
+    @Transactional
+    def updateOrganismPermission() {
+        log.info "Updating organism permissions"
+        JSONObject dataObject = permissionService.handleInput(request, params)
+        if (!permissionService.hasPermissions(dataObject, PermissionEnum.ADMINISTRATE)) {
+            render status: HttpStatus.UNAUTHORIZED
+            return
+        }
+        UserOrganismPermission userOrganismPermission = UserOrganismPermission.findById(dataObject.id)
+
+        User user = dataObject.userId ? User.findById(dataObject.userId as Long) : User.findByUsername(dataObject.user)
+
+        log.debug "Finding organism by ${dataObject.organism}"
+        Organism organism = preferenceService.getOrganismForTokenInDB(dataObject.organism)
+        if (!organism) {
+            render([(FeatureStringEnum.ERROR.value): "Failed to find organism with ${dataObject.organism}"] as JSON)
+            return
+        }
+        if (!user) {
+            log.error("Failed to find user with ${dataObject.userId} OR ${dataObject.user}")
+            render([(FeatureStringEnum.ERROR.value): "Failed to find user with ${dataObject.userId} OR ${dataObject.user}"] as JSON)
+        }
+        log.debug "found ${userOrganismPermission}"
+        if (!userOrganismPermission) {
+            userOrganismPermission = UserOrganismPermission.findByUserAndOrganism(user, organism)
+        }
+
+        if (!userOrganismPermission) {
+            log.debug "creating new permissions! "
+            userOrganismPermission = new UserOrganismPermission(
+                    user: user
+                    , organism: organism
+                    , permissions: "[]"
+            ).save(insert: true)
+            log.debug "created new permissions! "
+        }
+
+
+        JSONArray permissionsArray = new JSONArray()
+        if (dataObject.getBoolean(PermissionEnum.ADMINISTRATE.name())) {
+            permissionsArray.add(PermissionEnum.ADMINISTRATE.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.WRITE.name())) {
+            permissionsArray.add(PermissionEnum.WRITE.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.EXPORT.name())) {
+            permissionsArray.add(PermissionEnum.EXPORT.name())
+        }
+        if (dataObject.getBoolean(PermissionEnum.READ.name())) {
+            permissionsArray.add(PermissionEnum.READ.name())
+        }
+
+        if (permissionsArray.size() == 0) {
+            userOrganismPermission.delete(flush: true)
+            render userOrganismPermission as JSON
+            return
+        }
+
+        userOrganismPermission.permissions = permissionsArray.toString()
+        userOrganismPermission.save(flush: true)
+        log.info "Updated organism permissions for user ${user.username} and organism ${organism.commonName} and permissions ${permissionsArray.toString()}"
+        render userOrganismPermission as JSON
+
+
+    }
+
+}
diff --git a/grails-app/controllers/org/bbop/apollo/WebServicesController.groovy b/grails-app/controllers/org/bbop/apollo/WebServicesController.groovy
new file mode 100644
index 0000000..29ea44a
--- /dev/null
+++ b/grails-app/controllers/org/bbop/apollo/WebServicesController.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+class WebServicesController {
+
+    def grailsApplication
+
+    def index() {
+
+    }
+
+    def api() {
+        // get restapidoc.json from the asset file-system
+
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Analysis.groovy b/grails-app/domain/org/bbop/apollo/Analysis.groovy
new file mode 100644
index 0000000..6c775c6
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Analysis.groovy
@@ -0,0 +1,65 @@
+package org.bbop.apollo
+
+class Analysis {
+
+    static constraints = {
+    }
+
+    String name;
+    String description;
+    String program;
+    String programVersion;
+    String algorithm;
+    String sourceName;
+    String sourceVersion;
+    String sourceURI;
+    Date timeExecuted;
+
+    static hasMany = [
+            analysisFeatures : AnalysisFeature
+            ,analysisProperties : AnalysisProperty
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Analysis castOther = ( Analysis ) other;
+
+        return ( (this.getProgram()==castOther.getProgram()) || ( this.getProgram()!=null && castOther.getProgram()!=null && this.getProgram().equals(castOther.getProgram()) ) ) && ( (this.getProgramVersion()==castOther.getProgramVersion()) || ( this.getProgramVersion()!=null && castOther.getProgramVersion()!=null && this.getProgramVersion().equals(castOther.getProgramVersion()) ) ) && ( (this.getSourceName()==castOther.getSourceName()) || ( this.getSourceName()!=null && castOther.getSou [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+
+
+        result = 37 * result + ( getProgram() == null ? 0 : this.getProgram().hashCode() );
+        result = 37 * result + ( getProgramVersion() == null ? 0 : this.getProgramVersion().hashCode() );
+
+        result = 37 * result + ( getSourceName() == null ? 0 : this.getSourceName().hashCode() );
+
+
+
+
+
+        return result;
+    }
+
+    public Analysis generateClone() {
+        Analysis cloned = new Analysis();
+        cloned.name = this.name;
+        cloned.description = this.description;
+        cloned.program = this.program;
+        cloned.programVersion = this.programVersion;
+        cloned.algorithm = this.algorithm;
+        cloned.sourceName = this.sourceName;
+        cloned.sourceVersion = this.sourceVersion;
+        cloned.sourceURI = this.sourceURI;
+        cloned.timeExecuted = this.timeExecuted;
+        cloned.analysisFeatures = this.analysisFeatures;
+        cloned.analysisProperties = this.analysisProperties;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/AnalysisFeature.groovy b/grails-app/domain/org/bbop/apollo/AnalysisFeature.groovy
new file mode 100644
index 0000000..6237d63
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/AnalysisFeature.groovy
@@ -0,0 +1,46 @@
+package org.bbop.apollo
+
+class AnalysisFeature {
+
+    static constraints = {
+    }
+
+    Analysis analysis;
+    Feature feature;
+    Double rawScore;
+    Double normalizedScore;
+    Double significance;
+    Double identity;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        AnalysisFeature castOther = ( AnalysisFeature ) other;
+
+        return ( (this.getAnalysis()==castOther.getAnalysis()) || ( this.getAnalysis()!=null && castOther.getAnalysis()!=null && this.getAnalysis().equals(castOther.getAnalysis()) ) ) && ( (this.getFeature()==castOther.getFeature()) || ( this.getFeature()!=null && castOther.getFeature()!=null && this.getFeature().equals(castOther.getFeature()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getAnalysis() == null ? 0 : this.getAnalysis().hashCode() );
+        result = 37 * result + ( getFeature() == null ? 0 : this.getFeature().hashCode() );
+
+
+
+
+        return result;
+    }
+
+    public AnalysisFeature generateClone() {
+        AnalysisFeature cloned = new AnalysisFeature();
+        cloned.analysis = this.analysis;
+        cloned.feature = this.feature;
+        cloned.rawScore = this.rawScore;
+        cloned.normalizedScore = this.normalizedScore;
+        cloned.significance = this.significance;
+        cloned.identity = this.identity;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/AnalysisProperty.groovy b/grails-app/domain/org/bbop/apollo/AnalysisProperty.groovy
new file mode 100644
index 0000000..49ec37d
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/AnalysisProperty.groovy
@@ -0,0 +1,38 @@
+package org.bbop.apollo
+
+class AnalysisProperty {
+
+    static constraints = {
+    }
+
+    Analysis analysis;
+    CVTerm type;
+    String value;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        AnalysisProperty castOther = ( AnalysisProperty ) other;
+
+        return ( (this.getAnalysis()==castOther.getAnalysis()) || ( this.getAnalysis()!=null && castOther.getAnalysis()!=null && this.getAnalysis().equals(castOther.getAnalysis()) ) ) && ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getValue()==castOther.getValue()) || ( this.getValue()!=null && castOther.getValue()!=null && this.getValue().equals(castOther.getValue()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getAnalysis() == null ? 0 : this.getAnalysis().hashCode() );
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getValue() == null ? 0 : this.getValue().hashCode() );
+        return result;
+    }
+
+    public AnalysisProperty generateClone() {
+        AnalysisProperty cloned = new AnalysisProperty();
+        cloned.analysis = this.analysis;
+        cloned.type = this.type;
+        cloned.value = this.value;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/AvailableStatus.groovy b/grails-app/domain/org/bbop/apollo/AvailableStatus.groovy
new file mode 100644
index 0000000..4e886e5
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/AvailableStatus.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo
+
+class AvailableStatus {
+
+    static constraints = {
+//        label nullable: true
+        value nullable: false
+    }
+
+//    String label
+    String value
+
+    static hasMany = [
+            featureTypes: FeatureType
+    ]
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/AvailableStatusOrganismFilter.groovy b/grails-app/domain/org/bbop/apollo/AvailableStatusOrganismFilter.groovy
new file mode 100644
index 0000000..784a911
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/AvailableStatusOrganismFilter.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class AvailableStatusOrganismFilter extends OrganismFilter {
+
+    AvailableStatus availableStatus
+
+    static constraints = {
+        availableStatus nullable: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/BiologicalRegion.groovy b/grails-app/domain/org/bbop/apollo/BiologicalRegion.groovy
new file mode 100644
index 0000000..6dc93d8
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/BiologicalRegion.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ * AbstractSingleLocationBioFeature
+ */
+abstract class BiologicalRegion extends Region{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0001411"// XX:NNNNNNN
+
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/CDS.groovy b/grails-app/domain/org/bbop/apollo/CDS.groovy
new file mode 100644
index 0000000..8bc52ad
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CDS.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+/**
+ * NOTE: superclass is NOT region . . .
+ */
+class CDS extends TranscriptRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000316"// XX:NNNNNNN
+    static String cvTerm = "CDS"
+    static String alternateCvTerm = "CDS"
+}
diff --git a/grails-app/domain/org/bbop/apollo/CV.groovy b/grails-app/domain/org/bbop/apollo/CV.groovy
new file mode 100644
index 0000000..bd07737
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CV.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+class CV {
+
+    static constraints = {
+    }
+
+     String name;
+     String definition;
+
+     static hasMany = [
+             cvterms: CVTerm
+     ]
+}
diff --git a/grails-app/domain/org/bbop/apollo/CVTerm.groovy b/grails-app/domain/org/bbop/apollo/CVTerm.groovy
new file mode 100644
index 0000000..c399aa3
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CVTerm.groovy
@@ -0,0 +1,68 @@
+package org.bbop.apollo
+
+class CVTerm {
+
+    static constraints = {
+    }
+    CV cv;
+    DBXref dbxref;
+    String name;
+    String definition;
+    int isObsolete;
+    int isRelationshipType;
+
+    static hasMany = [
+            childCVTermRelationships   : CVTermRelationship
+            , parentCVTermRelationships: CVTermRelationship
+            , parentCVTermPaths        : CVTermPath
+            , childCVTermPaths         : CVTermPath
+    ]
+
+    static mappedBy = [
+            parentCVTermRelationships : "objectCVTerm"
+            , childCVTermRelationships: "subjectCVTerm"
+            , parentCVTermPaths       : "subjectCVTerm"
+            , childCVTermPaths        : "objectCVTerm"
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        CVTerm castOther = (CVTerm) other;
+
+        return ((this.getCv() == castOther.getCv()) || (this.getCv() != null && castOther.getCv() != null && this.getCv().equals(castOther.getCv()))) && ((this.getName() == castOther.getName()) || (this.getName() != null && castOther.getName() != null && this.getName().equals(castOther.getName()))) && (this.getIsObsolete() == castOther.getIsObsolete());
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + (getCv() == null ? 0 : this.getCv().hashCode());
+
+        result = 37 * result + (getName() == null ? 0 : this.getName().hashCode());
+
+        result = 37 * result + this.getIsObsolete();
+
+
+
+
+
+        return result;
+    }
+
+    public CVTerm generateClone() {
+        CVTerm cloned = new CVTerm();
+        cloned.cv = this.cv;
+        cloned.dbxref = this.dbxref;
+        cloned.name = this.name;
+        cloned.definition = this.definition;
+        cloned.isObsolete = this.isObsolete;
+        cloned.isRelationshipType = this.isRelationshipType;
+        cloned.childCVTermRelationships = this.childCVTermRelationships;
+        cloned.parentCVTermRelationships = this.parentCVTermRelationships;
+        cloned.parentCVTermPaths = this.parentCVTermPaths;
+        cloned.childCVTermPaths = this.childCVTermPaths;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/CVTermPath.groovy b/grails-app/domain/org/bbop/apollo/CVTermPath.groovy
new file mode 100644
index 0000000..78dd697
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CVTermPath.groovy
@@ -0,0 +1,45 @@
+package org.bbop.apollo
+
+class CVTermPath {
+
+    static constraints = {
+    }
+
+     CVTerm type;
+     CVTerm subjectCVTerm;
+     CVTerm objectCVTerm;
+     CV cv;
+     Integer pathDistance;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        CVTermPath castOther = ( CVTermPath ) other;
+
+        return ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getSubjectCVTerm()==castOther.getSubjectCVTerm()) || ( this.getSubjectCVTerm()!=null && castOther.getSubjectCVTerm()!=null && this.getSubjectCVTerm().equals(castOther.getSubjectCVTerm()) ) ) && ( (this.getObjectCVTerm()==castOther.getObjectCVTerm()) || ( this.getObjectCVTerm()!=null && castOther.getObjectCVTerm()!=null  [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getSubjectCVTerm() == null ? 0 : this.getSubjectCVTerm().hashCode() );
+
+        result = 37 * result + ( getObjectCVTerm() == null ? 0 : this.getObjectCVTerm().hashCode() );
+        result = 37 * result + ( getPathDistance() == null ? 0 : this.getPathDistance().hashCode() );
+        return result;
+    }
+
+    public CVTermPath generateClone() {
+        CVTermPath cloned = new CVTermPath();
+        cloned.type = this.type;
+        cloned.subjectCVTerm = this.subjectCVTerm;
+        cloned.cv = this.cv;
+        cloned.objectCVTerm = this.objectCVTerm;
+        cloned.pathDistance = this.pathDistance;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/CVTermRelationship.groovy b/grails-app/domain/org/bbop/apollo/CVTermRelationship.groovy
new file mode 100644
index 0000000..fce33a8
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CVTermRelationship.groovy
@@ -0,0 +1,39 @@
+package org.bbop.apollo
+
+class CVTermRelationship {
+
+    static constraints = {
+    }
+
+     CVTerm type;
+     CVTerm subjectCVTerm;
+     CVTerm objectCVTerm;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        CVTermRelationship castOther = ( CVTermRelationship ) other;
+
+        return ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getSubjectCVTerm()==castOther.getSubjectCVTerm()) || ( this.getSubjectCVTerm()!=null && castOther.getSubjectCVTerm()!=null && this.getSubjectCVTerm().equals(castOther.getSubjectCVTerm()) ) ) && ( (this.getObjectCVTerm()==castOther.getObjectCVTerm()) || ( this.getObjectCVTerm()!=null && castOther.getObjectCVTerm()!=null  [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getSubjectCVTerm() == null ? 0 : this.getSubjectCVTerm().hashCode() );
+        result = 37 * result + ( getObjectCVTerm() == null ? 0 : this.getObjectCVTerm().hashCode() );
+        return result;
+    }
+
+    public CVTermRelationship generateClone() {
+        CVTermRelationship cloned = new CVTermRelationship();
+        cloned.type = this.type;
+        cloned.subjectCVTerm = this.subjectCVTerm;
+        cloned.objectCVTerm = this.objectCVTerm;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedComment.groovy b/grails-app/domain/org/bbop/apollo/CannedComment.groovy
new file mode 100644
index 0000000..b074a56
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedComment.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+class CannedComment {
+
+    static constraints = {
+        comment nullable: false
+        metadata nullable: true
+    }
+
+    String comment
+    String metadata
+
+    static hasMany = [
+            featureTypes: FeatureType
+    ]
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedCommentOrganismFilter.groovy b/grails-app/domain/org/bbop/apollo/CannedCommentOrganismFilter.groovy
new file mode 100644
index 0000000..5119087
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedCommentOrganismFilter.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class CannedCommentOrganismFilter extends OrganismFilter {
+
+    CannedComment cannedComment
+
+    static constraints = {
+        cannedComment nullable: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedKey.groovy b/grails-app/domain/org/bbop/apollo/CannedKey.groovy
new file mode 100644
index 0000000..d3c843d
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedKey.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+class CannedKey {
+
+    static constraints = {
+        label nullable: false
+        metadata nullable: true
+    }
+
+    String label
+    String metadata
+
+    static hasMany = [
+        featureTypes: FeatureType
+    ]
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedKeyOrganismFilter.groovy b/grails-app/domain/org/bbop/apollo/CannedKeyOrganismFilter.groovy
new file mode 100644
index 0000000..ac91011
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedKeyOrganismFilter.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class CannedKeyOrganismFilter extends OrganismFilter {
+
+    CannedKey cannedKey
+
+    static constraints = {
+        cannedKey nullable: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedValue.groovy b/grails-app/domain/org/bbop/apollo/CannedValue.groovy
new file mode 100644
index 0000000..f47625a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedValue.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+class CannedValue {
+
+    static constraints = {
+        label nullable: false
+        metadata nullable: true
+    }
+
+    String label
+    String metadata
+
+    static hasMany = [
+        featureTypes: FeatureType
+    ]
+}
diff --git a/grails-app/domain/org/bbop/apollo/CannedValueOrganismFilter.groovy b/grails-app/domain/org/bbop/apollo/CannedValueOrganismFilter.groovy
new file mode 100644
index 0000000..100325e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CannedValueOrganismFilter.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class CannedValueOrganismFilter extends OrganismFilter {
+
+    CannedValue cannedValue
+
+    static constraints = {
+        cannedValue nullable: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Chromosome.groovy b/grails-app/domain/org/bbop/apollo/Chromosome.groovy
new file mode 100644
index 0000000..5bf0c2a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Chromosome.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * NOTE: superclass is NOT region . . .
+ */
+class Chromosome extends BiologicalRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000340"// XX:NNNNNNN
+    static String cvTerm = "Chromosome"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Comment.groovy b/grails-app/domain/org/bbop/apollo/Comment.groovy
new file mode 100644
index 0000000..a578a5c
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Comment.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class Comment extends FeatureProperty implements Ontological{
+
+    static constraints = {
+    }
+
+    static String cvTerm = "Comment"
+    static String ontologyId = "Comment" // TODO: not in the SO
+}
diff --git a/grails-app/domain/org/bbop/apollo/Contig.groovy b/grails-app/domain/org/bbop/apollo/Contig.groovy
new file mode 100644
index 0000000..b19ad4b
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Contig.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+/**
+ * NOTE: superclass is NOT region . . .
+ */
+class Contig extends Region{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000149"// XX:NNNNNNN
+//    static String cvTerm = "Match"// may have a link
+    static String cvTerm = "Contig"
+}
diff --git a/grails-app/domain/org/bbop/apollo/CustomDomainMapping.groovy b/grails-app/domain/org/bbop/apollo/CustomDomainMapping.groovy
new file mode 100644
index 0000000..8db6e17
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CustomDomainMapping.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+class CustomDomainMapping {
+
+    String cvTerm
+    String alternateCvTerm
+    String ontologyId
+    Boolean isTranscript
+
+    static constraints = {
+        cvTerm nullable: false, blank: false
+        alternateCvTerm nullable: true
+        ontologyId nullable: false, blank: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/CustomFeature.groovy b/grails-app/domain/org/bbop/apollo/CustomFeature.groovy
new file mode 100644
index 0000000..ca390db
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CustomFeature.groovy
@@ -0,0 +1,66 @@
+package org.bbop.apollo
+
+/**
+ * Used to over-ride custom features
+ */
+class CustomFeature extends Feature implements ConfigurableFeature{
+
+    String metaData // JSON MetaData .. or whaever
+
+    String customCvTerm
+    String customAlternateCvTerm
+    String customOntologyId
+    String customClassName
+
+    static constraints = {
+        metaData nullable: true, blank: true
+        customCvTerm nullable: false
+        customAlternateCvTerm nullable: true
+        customOntologyId nullable: false
+        customClassName nullable: false
+    }
+
+    static mapping = {
+        metaData type: "text"
+    }
+
+    @Override
+    String getCvTerm() {
+        return customCvTerm
+    }
+
+    @Override
+    String getOntologyId() {
+        return customOntologyId
+    }
+
+    @Override
+    String getAlternateCvTerm() {
+        return customAlternateCvTerm
+    }
+
+    @Override
+    String getClassName() {
+        return customClassName
+    }
+
+    @Override
+    void setClassName(String className) {
+        customClassName = className
+    }
+
+    @Override
+    void setCvTerm(String cvTerm) {
+        customCvTerm = cvTerm
+    }
+
+    @Override
+    void setAlternateCvTerm(String alternateCvTerm) {
+        customAlternateCvTerm = alternateCvTerm
+    }
+
+    @Override
+    void setOntologyId(String ontologyId) {
+        customOntologyId = ontologyId
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/CustomTranscript.groovy b/grails-app/domain/org/bbop/apollo/CustomTranscript.groovy
new file mode 100644
index 0000000..4167fb5
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/CustomTranscript.groovy
@@ -0,0 +1,67 @@
+package org.bbop.apollo
+
+/**
+ * Transcript type meant to be extensible
+ *
+ */
+class CustomTranscript extends Transcript implements ConfigurableFeature{
+
+    String metaData // JSON MetaData .. or whaever
+
+    String customCvTerm
+    String customAlternateCvTerm
+    String customOntologyId
+    String customClassName
+
+    static constraints = {
+        metaData nullable: true, blank: true
+        customCvTerm nullable: false
+        customAlternateCvTerm nullable: true
+        customOntologyId nullable: false
+        customClassName nullable: false
+    }
+
+    static mapping = {
+        metaData type: "text"
+    }
+
+    @Override
+    String getCvTerm() {
+        return customCvTerm
+    }
+
+    @Override
+    String getOntologyId() {
+        return customOntologyId
+    }
+
+    @Override
+    String getAlternateCvTerm() {
+        return customAlternateCvTerm
+    }
+
+    @Override
+    String getClassName() {
+        return customClassName
+    }
+
+    @Override
+    void setClassName(String className) {
+        customClassName = className
+    }
+
+    @Override
+    void setCvTerm(String cvTerm) {
+        customCvTerm = cvTerm
+    }
+
+    @Override
+    void setAlternateCvTerm(String alternateCvTerm) {
+        customAlternateCvTerm = alternateCvTerm
+    }
+
+    @Override
+    void setOntologyId(String ontologyId) {
+        customOntologyId = ontologyId
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/DB.groovy b/grails-app/domain/org/bbop/apollo/DB.groovy
new file mode 100644
index 0000000..47332c6
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/DB.groovy
@@ -0,0 +1,44 @@
+package org.bbop.apollo
+
+class DB {
+
+    static constraints = {
+        name nullable: false, unique: true
+        urlPrefix nullable: true
+        url nullable: true
+        description nullable: true
+    }
+
+     String name;
+     String description;
+     String urlPrefix;
+     String url;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        DB castOther = ( DB ) other;
+
+        return ( (this.getName()==castOther.getName()) || ( this.getName()!=null && castOther.getName()!=null && this.getName().equals(castOther.getName()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getName() == null ? 0 : this.getName().hashCode() );
+
+
+
+        return result;
+    }
+
+    public DB generateClone() {
+        DB cloned = new DB();
+        cloned.name = this.name;
+        cloned.description = this.description;
+        cloned.urlPrefix = this.urlPrefix;
+        cloned.url = this.url;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/DBXref.groovy b/grails-app/domain/org/bbop/apollo/DBXref.groovy
new file mode 100644
index 0000000..155bee8
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/DBXref.groovy
@@ -0,0 +1,66 @@
+package org.bbop.apollo
+
+class DBXref {
+
+    static constraints = {
+        db nullable: false
+        accession nullable: false
+        version nullable: true
+        description nullable: true
+    }
+
+    String version;
+    DB db;
+    String accession;
+    String description;
+
+    static hasMany = [
+            dbxrefProperties : DBXrefProperty
+    ]
+
+    boolean equals(other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+
+        DBXref dbXref = (DBXref) other
+
+//        if (accession != dbXref.accession) return false
+//        if (db != dbXref.db) return false
+//        if (version != dbXref.version) return false
+        DBXref castOther = ( DBXref ) other;
+//
+        return ( (this.getVersion()==castOther.getVersion()) || ( this.getVersion()!=null && castOther.getVersion()!=null && this.getVersion().equals(castOther.getVersion()) ) ) && ( (this.getDb()==castOther.getDb()) || ( this.getDb()!=null && castOther.getDb()!=null && this.getDb().equals(castOther.getDb()) ) ) && ( (this.getAccession()==castOther.getAccession()) || ( this.getAccession()!=null && castOther.getAccession()!=null && this.getAccession().equals(castOther.getAccession()) ) );
+    }
+
+
+//    public boolean equals(Object other) {
+//        if ( (this == other ) ) return true;
+//        if ( (other == null ) ) return false;
+//        if ( !(other instanceof DBXref) ) return false;
+//        DBXref castOther = ( DBXref ) other;
+//
+//        return ( (this.getVersion()==castOther.getVersion()) || ( this.getVersion()!=null && castOther.getVersion()!=null && this.getVersion().equals(castOther.getVersion()) ) ) && ( (this.getDb()==castOther.getDb()) || ( this.getDb()!=null && castOther.getDb()!=null && this.getDb().equals(castOther.getDb()) ) ) && ( (this.getAccession()==castOther.getAccession()) || ( this.getAccession()!=null && castOther.getAccession()!=null && this.getAccession().equals(castOther.getAccession()) ) );
+//    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getVersion() == null ? 0 : this.getVersion().hashCode() );
+        result = 37 * result + ( getDb() == null ? 0 : this.getDb().hashCode() );
+        result = 37 * result + ( getAccession() == null ? 0 : this.getAccession().hashCode() );
+
+
+        return result;
+    }
+
+    public DBXref generateClone() {
+        DBXref cloned = new DBXref();
+        cloned.db = this.db;
+        cloned.accession = this.accession;
+        cloned.description = this.description;
+        cloned.dbxrefProperties = this.dbxrefProperties;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/DBXrefProperty.groovy b/grails-app/domain/org/bbop/apollo/DBXrefProperty.groovy
new file mode 100644
index 0000000..42c027c
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/DBXrefProperty.groovy
@@ -0,0 +1,40 @@
+package org.bbop.apollo
+
+class DBXrefProperty {
+
+    static constraints = {
+    }
+
+     CVTerm type;
+     DBXref dbxref;
+     String value;
+     int rank;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        DBXrefProperty castOther = ( DBXrefProperty ) other;
+
+        return ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getDbxref()==castOther.getDbxref()) || ( this.getDbxref()!=null && castOther.getDbxref()!=null && this.getDbxref().equals(castOther.getDbxref()) ) ) && (this.getRank()==castOther.getRank());
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getDbxref() == null ? 0 : this.getDbxref().hashCode() );
+
+        result = 37 * result + this.getRank();
+        return result;
+    }
+
+    public DBXrefProperty generateClone() {
+        DBXrefProperty cloned = new DBXrefProperty();
+        cloned.type = this.type;
+        cloned.dbxref = this.dbxref;
+        cloned.value = this.value;
+        cloned.rank = this.rank;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/DataAdapter.groovy b/grails-app/domain/org/bbop/apollo/DataAdapter.groovy
new file mode 100644
index 0000000..74264d0
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/DataAdapter.groovy
@@ -0,0 +1,40 @@
+package org.bbop.apollo
+
+class DataAdapter {
+
+
+    static constraints = {
+        key nullable: false
+        permission nullable: false
+        options nullable: true
+        implementationClass nullable: true
+        tempDirectory nullable: true
+        exportSourceGenomicSequence nullable: true
+        source nullable: true
+        featureTypeString nullable: true
+    }
+
+    static hasMany = [
+            dataAdapters: DataAdapter
+    ]
+
+    String key
+    String implementationClass
+    String permission
+    String options
+
+    String tempDirectory
+
+    // gff3 stuff
+    // value to use in the source column
+    String source
+    Boolean exportSourceGenomicSequence
+
+    // feature stuff
+    // [{featureType:"sequence:mRNA"},{featureType:"sequence:transcript"}]
+    String featureTypeString
+
+    static mapping = {
+        key column: "data_adapter_key"
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Deletion.groovy b/grails-app/domain/org/bbop/apollo/Deletion.groovy
new file mode 100644
index 0000000..d62f520
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Deletion.groovy
@@ -0,0 +1,19 @@
+package org.bbop.apollo
+
+class Deletion extends SequenceAlteration{
+
+    Integer deletionLength
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "Deletion"
+    static String ontologyId = "SO:0000159"
+    static String alternateCvTerm = "deletion"
+
+    @Override
+    int getOffset() {
+        // TODO: add offset
+        return deletionLength
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Environment.groovy b/grails-app/domain/org/bbop/apollo/Environment.groovy
new file mode 100644
index 0000000..7e5b57f
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Environment.groovy
@@ -0,0 +1,42 @@
+package org.bbop.apollo
+
+class Environment {
+
+    static constraints = {
+    }
+
+    String uniquename;
+    String description;
+
+    static hasMany = [
+           environmentCVTerms  : EnvironmentCVTerm
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Environment castOther = ( Environment ) other;
+
+        return ( (this.getUniquename()==castOther.getUniquename()) || ( this.getUniquename()!=null && castOther.getUniquename()!=null && this.getUniquename().equals(castOther.getUniquename()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getUniquename() == null ? 0 : this.getUniquename().hashCode() );
+
+
+        return result;
+    }
+
+    public Environment generateClone() {
+        Environment cloned = new Environment();
+        cloned.uniquename = this.uniquename;
+        cloned.description = this.description;
+        cloned.environmentCVTerms = this.environmentCVTerms;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/EnvironmentCVTerm.groovy b/grails-app/domain/org/bbop/apollo/EnvironmentCVTerm.groovy
new file mode 100644
index 0000000..3a885b1
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/EnvironmentCVTerm.groovy
@@ -0,0 +1,35 @@
+package org.bbop.apollo
+
+class EnvironmentCVTerm {
+
+    static constraints = {
+    }
+
+    Environment environment;
+    CVTerm cvterm;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        EnvironmentCVTerm castOther = ( EnvironmentCVTerm ) other;
+
+        return ( (this.getEnvironment()==castOther.getEnvironment()) || ( this.getEnvironment()!=null && castOther.getEnvironment()!=null && this.getEnvironment().equals(castOther.getEnvironment()) ) ) && ( (this.getCvterm()==castOther.getCvterm()) || ( this.getCvterm()!=null && castOther.getCvterm()!=null && this.getCvterm().equals(castOther.getCvterm()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getEnvironment() == null ? 0 : this.getEnvironment().hashCode() );
+        result = 37 * result + ( getCvterm() == null ? 0 : this.getCvterm().hashCode() );
+        return result;
+    }
+
+    public EnvironmentCVTerm generateClone() {
+        EnvironmentCVTerm cloned = new EnvironmentCVTerm();
+        cloned.environment = this.environment;
+        cloned.cvterm = this.cvterm;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Exon.groovy b/grails-app/domain/org/bbop/apollo/Exon.groovy
new file mode 100644
index 0000000..9f1ce7d
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Exon.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+/**
+ * NOTE: superclass is NOT region . . .
+ */
+class Exon extends TranscriptRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000147"// XX:NNNNNNN
+    static String cvTerm = "Exon"
+    static String alternateCvTerm = "exon"
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Feature.groovy b/grails-app/domain/org/bbop/apollo/Feature.groovy
new file mode 100644
index 0000000..1c94578
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Feature.groovy
@@ -0,0 +1,167 @@
+package org.bbop.apollo
+
+class Feature implements Ontological{
+
+    static auditable = true
+
+    static constraints = {
+        name nullable: false
+        uniqueName nullable: false
+        dbxref nullable: true
+        sequenceLength nullable: true
+        md5checksum nullable: true
+        isAnalysis nullable: true
+        isObsolete nullable: true
+        dateCreated nullable: true
+        lastUpdated nullable: true
+        symbol nullable: true // TODO: should be false and unique per organism
+        description nullable: true
+        status nullable: true
+    }
+
+    String symbol
+    String description
+    DBXref dbxref;
+    String name;
+    String uniqueName;
+    Integer sequenceLength;
+    String md5checksum;
+    Status status
+    boolean isAnalysis;
+    boolean isObsolete;
+    Date dateCreated;
+    Date lastUpdated ;
+
+    static hasMany = [
+            featureLocations: FeatureLocation
+            ,featureGenotypes: FeatureGenotype
+            ,parentFeatureRelationships: FeatureRelationship  // relationships where I am the parent feature relationship
+            ,childFeatureRelationships: FeatureRelationship // relationships where I am the child feature relationship
+            ,featureCVTerms: FeatureCVTerm
+            ,featureSynonyms: FeatureSynonym
+            ,featureDBXrefs: DBXref
+            ,featurePublications: Publication
+            ,featurePhenotypes: Phenotype
+            ,featureProperties: FeatureProperty
+            ,synonyms: Synonym
+            ,owners:User
+    ]
+
+    static mappedBy = [
+            parentFeatureRelationships: "parentFeature",
+            childFeatureRelationships: "childFeature",
+            featureGenotypes: "feature",
+            featureLocations: "feature"
+    ]
+    
+    static mapping = {
+            childFeatureRelationships cascade: 'all-delete-orphan'
+            parentFeatureRelationships cascade: 'all-delete-orphan'
+            featureLocations cascade: 'all-delete-orphan' // lazy: false  since most / all feature locations have a single element join is more efficient
+            name type: 'text'
+            description type: 'text'
+    }
+
+
+    static belongsTo = [
+            User
+    ]
+    
+    public User getOwner(){
+        if(owners?.size()>0){
+            return owners.iterator().next()
+        }
+        return null
+    }
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Feature castOther = ( Feature ) other;
+
+        return  (this?.ontologyId==castOther?.ontologyId) \
+                   &&  (this?.getUniqueName()==castOther?.getUniqueName())
+    }
+
+    public int hashCode() {
+        int result = 17;
+        result = 37 * result + ( ontologyId == null ? 0 : this.ontologyId.hashCode() );
+        result = 37 * result + ( getUniqueName() == null ? 0 : this.getUniqueName().hashCode() );
+        return result;
+    }
+
+    public Feature generateClone() {
+        Feature cloned = this.getClass().newInstance()
+        cloned.dbxref = this.dbxref;
+        cloned.name = this.name;
+        cloned.uniqueName = this.uniqueName;
+        cloned.sequenceLength = this.sequenceLength;
+        cloned.md5checksum = this.md5checksum;
+        cloned.isAnalysis = this.isAnalysis;
+        cloned.isObsolete = this.isObsolete;
+        cloned.dateCreated = this.dateCreated;
+        cloned.lastUpdated = this.lastUpdated;
+        cloned.featureLocations = this.featureLocations;
+        cloned.featureGenotypes = this.featureGenotypes;
+        cloned.parentFeatureRelationships = this.parentFeatureRelationships;
+        cloned.childFeatureRelationships = this.childFeatureRelationships;
+        cloned.featureCVTerms = this.featureCVTerms;
+        cloned.featureSynonyms = this.featureSynonyms;
+        cloned.featureDBXrefs = this.featureDBXrefs;
+        cloned.featurePublications = this.featurePublications;
+        cloned.featurePhenotypes = this.featurePhenotypes;
+        cloned.featureProperties = this.featureProperties;
+        return cloned;
+    }
+
+
+
+    /** Convenience method for retrieving the location.  Assumes that it only contains a single
+     *  location so it returns the first (and hopefully only) location from the collection of
+     *  locations.  Returns <code>null</code> if none are found.
+     *
+     * @return FeatureLocation of this object
+     */
+    public FeatureLocation getFeatureLocation() {
+        Collection<FeatureLocation> locs = getFeatureLocations();
+        return locs ? locs.first() : null
+    }
+
+
+    /** Get the length of this feature.
+     *
+     * @return Length of feature
+     */
+    public int getLength() {
+        return getFeatureLocation().calculateLength()
+    }
+
+    public Integer getFmin(){
+        featureLocation.fmin
+    }
+
+    public Integer getFmax(){
+        featureLocation.fmax
+    }
+
+    public Integer getStrand(){
+        featureLocation.strand
+    }
+
+
+    @Override
+    public String toString() {
+        return "Feature{" +
+                "id=" + id +
+                ", symbol='" + symbol + '\'' +
+                ", description='" + description + '\'' +
+                ", name='" + name + '\'' +
+                ", uniqueName='" + uniqueName + '\'' +
+                ", sequenceLength=" + sequenceLength +
+                ", status=" + status +
+                ", dateCreated=" + dateCreated +
+                ", lastUpdated=" + lastUpdated +
+                '}';
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureAttribute.groovy b/grails-app/domain/org/bbop/apollo/FeatureAttribute.groovy
new file mode 100644
index 0000000..7977a8b
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureAttribute.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class FeatureAttribute extends SequenceAttribute{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000733"// XX:NNNNNNN
+
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureCVTerm.groovy b/grails-app/domain/org/bbop/apollo/FeatureCVTerm.groovy
new file mode 100644
index 0000000..5f544a1
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureCVTerm.groovy
@@ -0,0 +1,50 @@
+package org.bbop.apollo
+
+
+class FeatureCVTerm {
+
+    static constraints = {
+    }
+
+    Publication publication;
+    Feature feature;
+    CVTerm cvterm;
+    boolean isNot;
+    int rank;
+
+    static hasMany = [
+            featureCVTermPublications: Publication
+            ,featureCVTermDBXrefs: DBXref
+    ]
+
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureCVTerm castOther = ( FeatureCVTerm ) other;
+
+        return ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPublication().equals(castOther.getPublication()) ) ) && ( (this.getFeature()==castOther.getFeature()) || ( this.getFeature()!=null && castOther.getFeature()!=null && this.getFeature().equals(castOther.getFeature()) ) ) && ( (this.getCvterm()==castOther.getCvterm()) || ( this.getCvterm()!=null && castOther.getCvterm()!=null && this.getCvterm( [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + ( getFeature() == null ? 0 : this.getFeature().hashCode() );
+        result = 37 * result + ( getCvterm() == null ? 0 : this.getCvterm().hashCode() );
+        result = 37 * result + this.getRank();
+        return result;
+    }
+
+    public FeatureCVTerm generateClone() {
+        FeatureCVTerm cloned = new FeatureCVTerm();
+        cloned.publication = this.publication;
+        cloned.feature = this.feature;
+        cloned.cvterm = this.cvterm;
+        cloned.isNot = this.isNot;
+        cloned.rank = this.rank;
+        cloned.featureCVTermPublications = this.featureCVTermPublications;
+        cloned.featureCVTermDBXrefs = this.featureCVTermDBXrefs;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureEvent.groovy b/grails-app/domain/org/bbop/apollo/FeatureEvent.groovy
new file mode 100644
index 0000000..3297b75
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureEvent.groovy
@@ -0,0 +1,63 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.history.FeatureOperation
+
+//import org.bbop.apollo.history.FeatureOperation
+
+class FeatureEvent {
+
+
+    Date dateCreated
+    Date lastUpdated
+    User editor
+    FeatureOperation operation
+    Boolean current
+
+    String name  // this is the name of the top-level feature (typically gene) during this event
+    String originalJsonCommand
+    String newFeaturesJsonArray
+    String oldFeaturesJsonArray
+
+
+    Long parentId // will be the same, unless split, then parent name may be different
+    Long parentMergeId // the name of the parent merged from
+    String uniqueName // from original top-level feature
+    Long childId // will be the same, unless merged, then name will change
+    Long childSplitId // on a split, then the name will change
+
+    static constraints = {
+        editor nullable: true
+        originalJsonCommand nullable: true
+        newFeaturesJsonArray nullable: true
+        oldFeaturesJsonArray nullable: true
+        name nullable: false, blank: false
+
+        uniqueName nullable: false, blank: false
+        parentId nullable: true, blank: false
+        parentMergeId nullable: true, blank: false
+        childId nullable: true, blank: false
+        childSplitId  nullable: true, blank: false
+    }
+
+    static mapping = {
+        uniqueName index: "feature_uniqueName"
+        name type: "text"
+        originalJsonCommand type: "text"
+        newFeaturesJsonArray type: "text"
+        oldFeaturesJsonArray type: "text"
+    }
+
+//    static hasOne = [
+//            feature : Feature
+//    ]
+
+//    static belongsTo = [
+//            feature: Feature
+//    ]
+
+//    static hasMany = [
+//            newFeatures: Feature
+//            ,oldFeatures: Feature
+//    ]
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureGenotype.groovy b/grails-app/domain/org/bbop/apollo/FeatureGenotype.groovy
new file mode 100644
index 0000000..12fdbbd
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureGenotype.groovy
@@ -0,0 +1,49 @@
+package org.bbop.apollo
+
+class FeatureGenotype {
+
+    static constraints = {
+    }
+
+    Integer featureGenotypeId;
+    Genotype genotype;
+    Feature feature;
+    CVTerm cvterm;
+    Feature chromosomeFeature;
+    int rank;
+    int cgroup;
+
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureGenotype castOther = ( FeatureGenotype ) other;
+
+        return ( (this.getGenotype()==castOther.getGenotype()) || ( this.getGenotype()!=null && castOther.getGenotype()!=null && this.getGenotype().equals(castOther.getGenotype()) ) ) && ( (this.getFeature()==castOther.getFeature()) || ( this.getFeature()!=null && castOther.getFeature()!=null && this.getFeature().equals(castOther.getFeature()) ) ) && ( (this.getCvterm()==castOther.getCvterm()) || ( this.getCvterm()!=null && castOther.getCvterm()!=null && this.getCvterm().equals(castOther [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getGenotype() == null ? 0 : this.getGenotype().hashCode() );
+        result = 37 * result + ( getFeature() == null ? 0 : this.getFeature().hashCode() );
+        result = 37 * result + ( getCvterm() == null ? 0 : this.getCvterm().hashCode() );
+        result = 37 * result + ( getChromosomeFeature() == null ? 0 : this.getChromosomeFeature().hashCode() );
+        result = 37 * result + this.getRank();
+        result = 37 * result + this.getCgroup();
+        return result;
+    }
+
+    public FeatureGenotype generateClone() {
+        FeatureGenotype cloned = new FeatureGenotype();
+        cloned.genotype = this.genotype;
+        cloned.feature = this.feature;
+        cloned.cvterm = this.cvterm;
+        cloned.chromosomeFeature = this.chromosomeFeature;
+        cloned.rank = this.rank;
+        cloned.cgroup = this.cgroup;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureLazyResidues.groovy b/grails-app/domain/org/bbop/apollo/FeatureLazyResidues.groovy
new file mode 100644
index 0000000..35b2b18
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureLazyResidues.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+/**
+ * Top level track feature
+ */
+class FeatureLazyResidues extends Feature{
+
+
+    static constraints = {
+        fmin nullable: false
+        fmax nullable: false
+    }
+
+    Integer fmin;
+    Integer fmax;
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureLocation.groovy b/grails-app/domain/org/bbop/apollo/FeatureLocation.groovy
new file mode 100644
index 0000000..9da3777
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureLocation.groovy
@@ -0,0 +1,86 @@
+package org.bbop.apollo
+
+class FeatureLocation {
+    static mapping = {
+        length formula: 'FMAX-FMIN'
+    }
+    static constraints = {
+        feature nullable: false
+        fmin nullable: false
+        fmax nullable: false
+        sequence nullable: false
+        length nullable: true
+        isFminPartial nullable: true
+        isFmaxPartial nullable: true
+        strand nullable: true
+        phase nullable: true
+        residueInfo nullable: true
+        locgroup nullable: true
+        rank nullable: true
+    }
+
+    Feature feature
+    Integer fmin
+    Integer length
+    boolean isFminPartial
+    Integer fmax
+    boolean isFmaxPartial
+    Integer strand
+    Integer phase
+    String residueInfo
+    int locgroup
+    int rank
+    Sequence sequence
+
+    static belongsTo = [Feature]
+
+
+    static hasMany = [
+            featureLocationPublications: Publication
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureLocation castOther = (FeatureLocation) other;
+
+        return ((this.getFeature() == castOther.getFeature()) || (this.getFeature() != null && castOther.getFeature() != null && this.getFeature().equals(castOther.getFeature()))) && (this.getLocgroup() == castOther.getLocgroup()) && (this.getRank() == castOther.getRank());
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+        result = 37 * result + (getFeature() == null ? 0 : this.getFeature().hashCode());
+
+        result = 37 * result + this.getLocgroup();
+        result = 37 * result + this.getRank();
+
+        return result;
+    }
+
+    /**
+     * We use this as an artificial accessor in case the property has not been calculatd
+     * @return
+     */
+    public Integer calculateLength(){
+        return fmax-fmin
+    }
+    public FeatureLocation generateClone() {
+        FeatureLocation cloned = new FeatureLocation();
+        cloned.sequence = this.sequence;
+        cloned.feature = this.feature;
+        cloned.fmin = this.fmin;
+        cloned.isFminPartial = this.isFminPartial;
+        cloned.fmax = this.fmax;
+        cloned.isFmaxPartial = this.isFmaxPartial;
+        cloned.strand = this.strand;
+        cloned.phase = this.phase;
+        cloned.residueInfo = this.residueInfo;
+        cloned.locgroup = this.locgroup;
+        cloned.rank = this.rank;
+        cloned.featureLocationPublications = this.featureLocationPublications;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureProperty.groovy b/grails-app/domain/org/bbop/apollo/FeatureProperty.groovy
new file mode 100644
index 0000000..580a138
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureProperty.groovy
@@ -0,0 +1,59 @@
+package org.bbop.apollo
+
+class FeatureProperty implements Ontological{
+
+    static auditable =  true
+
+    private static final String TAG_VALUE_DELIMITER = "=";
+
+    static constraints = {
+        type nullable: true
+//        feature nullable: true
+        value nullable: false // unique? . . or unique for feature?
+        rank nullable: true
+        tag nullable: true,blank: false  // this will be null for generic properties
+    }
+
+//    Integer featurePropertyId;
+    CVTerm type;
+    // I think a FeatureProperty can be associated with more than one
+    Feature feature;
+    String tag;
+    String value;
+    int rank;
+
+    static hasMany = [
+            featurePropertyPublications :  Publication
+    ]
+
+    static belongsTo = [
+            Feature
+    ]
+
+    static mapping = {
+        value type: 'text'
+    }
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureProperty castOther = ( FeatureProperty ) other;
+
+        if(castOther?.id == this?.id) return true
+        if(castOther?.rank != this?.rank) return false
+        if(castOther?.value != this?.value) return false
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getValue() == null ? 0 : this.getValue().hashCode() );
+
+        result = 37 * result + this.getRank();
+
+        return result;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureRelationship.groovy b/grails-app/domain/org/bbop/apollo/FeatureRelationship.groovy
new file mode 100644
index 0000000..3dc3aff
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureRelationship.groovy
@@ -0,0 +1,52 @@
+package org.bbop.apollo
+
+class FeatureRelationship implements  Ontological{
+
+    static auditable =  true
+
+    static constraints = {
+        rank nullable: true
+        value nullable: true
+    }
+
+    Feature parentFeature;
+    Feature childFeature;
+    String value; // unused, but could be used like metadata (strength / quality of connection)
+    int rank;
+    static String ontologyId = "part_of"
+    
+    static hasMany = [
+            featureRelationshipProperties : FeatureProperty
+            ,featureRelationshipPublications: Publication
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureRelationship castOther = ( FeatureRelationship ) other;
+        if(this?.id == castOther?.id) return true
+
+        return  this.parentFeature ==castOther.parentFeature  \
+                && this.childFeature ==  castOther.childFeature
+    }
+
+    public int hashCode() {
+        int result = 17;
+        result = 37 * result + ( parentFeature == null ? 0 : this.parentFeature.hashCode() );
+        result = 37 * result + ( childFeature == null ? 0 : this.childFeature.hashCode() );
+        result = 37 * result + this.rank;
+        return result;
+    }
+
+    public FeatureRelationship generateClone() {
+        FeatureRelationship cloned = new FeatureRelationship();
+        cloned.parentFeature = this.parentFeature;
+        cloned.childFeature = this.childFeature;
+        cloned.value = this.value;
+        cloned.rank = this.rank;
+        cloned.featureRelationshipProperties = this.featureRelationshipProperties;
+        cloned.featureRelationshipPublications = this.featureRelationshipPublications;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureSynonym.groovy b/grails-app/domain/org/bbop/apollo/FeatureSynonym.groovy
new file mode 100644
index 0000000..b21a1fa
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureSynonym.groovy
@@ -0,0 +1,45 @@
+package org.bbop.apollo
+
+
+class FeatureSynonym {
+
+    static constraints = {
+    }
+
+    Publication publication;
+    Feature feature;
+    Synonym synonym;
+    boolean isCurrent;
+    boolean isInternal;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        FeatureSynonym castOther = ( FeatureSynonym ) other;
+
+        return ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPublication().equals(castOther.getPublication()) ) ) && ( (this.getFeature()==castOther.getFeature()) || ( this.getFeature()!=null && castOther.getFeature()!=null && this.getFeature().equals(castOther.getFeature()) ) ) && ( (this.getSynonym()==castOther.getSynonym()) || ( this.getSynonym()!=null && castOther.getSynonym()!=null && this.getSyn [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + ( getFeature() == null ? 0 : this.getFeature().hashCode() );
+        result = 37 * result + ( getSynonym() == null ? 0 : this.getSynonym().hashCode() );
+
+
+        return result;
+    }
+
+    public FeatureSynonym generateClone() {
+        FeatureSynonym cloned = new FeatureSynonym();
+        cloned.publication = this.publication;
+        cloned.feature = this.feature;
+        cloned.synonym = this.synonym;
+        cloned.isCurrent = this.isCurrent;
+        cloned.isInternal = this.isInternal;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/FeatureType.groovy b/grails-app/domain/org/bbop/apollo/FeatureType.groovy
new file mode 100644
index 0000000..5cf8178
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/FeatureType.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo
+
+class FeatureType {
+
+    static constraints = {
+    }
+
+    String name
+    String display
+    String type
+    String ontologyId
+}
diff --git a/grails-app/domain/org/bbop/apollo/Frameshift.groovy b/grails-app/domain/org/bbop/apollo/Frameshift.groovy
new file mode 100644
index 0000000..9fec8c3
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Frameshift.groovy
@@ -0,0 +1,33 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+abstract class Frameshift extends TranscriptAttribute{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000865"// XX:NNNNNNN
+
+    // add convenience methods
+    /** Get the coordinate for the frameshift.
+     *
+     * @return Coordinate for the frameshift
+     */
+    public int getCoordinate() {
+        return Integer.parseInt(getValue());
+    }
+
+    /** Returns whether this frameshift is in the plus translational direction.
+     *
+     * @return true if the frameshift is in the plus translational direction
+     */
+    public abstract boolean isPlusFrameshift();
+
+    /** Get the frameshift value.
+     *
+     * @return Frameshift value
+     */
+    public abstract int getFrameshiftValue();
+}
diff --git a/grails-app/domain/org/bbop/apollo/Gene.groovy b/grails-app/domain/org/bbop/apollo/Gene.groovy
new file mode 100644
index 0000000..b4eda02
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Gene.groovy
@@ -0,0 +1,19 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ * AbstractSingleLocationBioFeature
+ */
+class Gene extends BiologicalRegion{
+
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000704"// XX:NNNNNNN
+    static String cvTerm = "Gene"// may have a link
+    static String alternateCvTerm = "gene"
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/GeneMemberRegion.groovy b/grails-app/domain/org/bbop/apollo/GeneMemberRegion.groovy
new file mode 100644
index 0000000..718e6c6
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/GeneMemberRegion.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ * AbstractSingleLocationBioFeature
+ */
+class GeneMemberRegion extends BiologicalRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000831"// XX:NNNNNNN
+}
diff --git a/grails-app/domain/org/bbop/apollo/Genotype.groovy b/grails-app/domain/org/bbop/apollo/Genotype.groovy
new file mode 100644
index 0000000..862e3e9
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Genotype.groovy
@@ -0,0 +1,51 @@
+package org.bbop.apollo
+
+
+class Genotype {
+
+    static constraints = {
+    }
+
+    Integer genotypeId;
+    String name;
+    String uniqueName;
+    String description;
+
+    static hasMany = [
+           phenotypeDescriptions  : PhenotypeDescription
+            ,featureGenotypes: FeatureGenotype
+            ,phenotypeStatements : PhenotypeStatement
+    ]
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Genotype castOther = ( Genotype ) other;
+
+        return ( (this.getUniqueName()==castOther.getUniqueName()) || ( this.getUniqueName()!=null && castOther.getUniqueName()!=null && this.getUniqueName().equals(castOther.getUniqueName()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+
+        result = 37 * result + ( getUniqueName() == null ? 0 : this.getUniqueName().hashCode() );
+
+
+
+
+        return result;
+    }
+
+    public Genotype generateClone() {
+        Genotype cloned = new Genotype();
+        cloned.name = this.name;
+        cloned.uniqueName = this.uniqueName;
+        cloned.description = this.description;
+        cloned.phenotypeDescriptions = this.phenotypeDescriptions;
+        cloned.featureGenotypes = this.featureGenotypes;
+        cloned.phenotypeStatements = this.phenotypeStatements;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/GroupOrganismPermission.groovy b/grails-app/domain/org/bbop/apollo/GroupOrganismPermission.groovy
new file mode 100644
index 0000000..24b16f8
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/GroupOrganismPermission.groovy
@@ -0,0 +1,31 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONArray
+
+class GroupOrganismPermission extends GroupPermission{
+
+    String permissions // JSONArray wrapping PermissionEnum
+
+    List<String> getPermissionValues(){
+        def returnList = []
+        if(permissions){
+            JSONArray jsonArray = JSON.parse(permissions) as JSONArray
+            for(int i = 0  ; i < jsonArray.size() ; i++){
+                returnList << jsonArray.getString(i)
+            }
+        }
+
+        return returnList
+    }
+
+
+    static belongsTo = [ Organism, UserGroup ]
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/GroupPermission.groovy b/grails-app/domain/org/bbop/apollo/GroupPermission.groovy
new file mode 100644
index 0000000..8592479
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/GroupPermission.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+class GroupPermission extends Permission{
+
+    UserGroup group
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/GroupTrackPermission.groovy b/grails-app/domain/org/bbop/apollo/GroupTrackPermission.groovy
new file mode 100644
index 0000000..0d2719e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/GroupTrackPermission.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+class GroupTrackPermission extends GroupPermission{
+
+    String trackVisibilities // JSON representation (name:'',visible:t/f)
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Insertion.groovy b/grails-app/domain/org/bbop/apollo/Insertion.groovy
new file mode 100644
index 0000000..f192aec
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Insertion.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo
+
+class Insertion extends SequenceAlteration{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "Insertion"
+    static String ontologyId = "SO:0000667"
+    static String alternateCvTerm = "insertion"
+
+    @Override
+    int getOffset() {
+        return alterationResidue.length()
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Intron.groovy b/grails-app/domain/org/bbop/apollo/Intron.groovy
new file mode 100644
index 0000000..a20400e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Intron.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+/**
+ * NOTE: superclass is NOT region . . .
+ */
+class Intron extends TranscriptRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000188"// XX:NNNNNNN
+//    static String cvTerm = "Match"// may have a link
+    static String cvTerm = "Intron"
+}
diff --git a/grails-app/domain/org/bbop/apollo/MRNA.groovy b/grails-app/domain/org/bbop/apollo/MRNA.groovy
new file mode 100644
index 0000000..af19b9f
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/MRNA.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class MRNA extends Transcript{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "MRNA"
+    static String ontologyId = "SO:0000234"
+    static String alternateCvTerm = "mRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Match.groovy b/grails-app/domain/org/bbop/apollo/Match.groovy
new file mode 100644
index 0000000..242af37
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Match.groovy
@@ -0,0 +1,76 @@
+package org.bbop.apollo
+
+/**
+ * In the ontology, this is a "is_a" relationship ... not sure if it makes sense to keep it that way, though
+ */
+class Match extends Region{
+
+    static constraints = {
+    }
+
+    // added
+    AnalysisFeature analysisFeature;
+
+    static String ontologyId = "SO:0000343"// XX:NNNNNNN
+    static String cvTerm = "Match"// may have a link
+
+    // add convenience methods
+
+    def setQueryFeatureLocation(int fmin, int fmax, int strand, Feature source) {
+        FeatureLocation loc = analysisFeature.getQueryFeatureLocation();
+        boolean needToAdd = false;
+        if (loc == null) {
+            loc = new FeatureLocation();
+            needToAdd = true;
+        }
+        loc.setRank(0);
+        loc.setFmin(fmin);
+        loc.setFmax(fmax);
+        loc.setStrand(strand);
+        if (needToAdd) {
+            analysisFeature.getFeatureLocations().add(loc);
+        }
+    }
+
+    def setSubjectFeatureLocation(int fmin, int fmax, int strand, Feature source) {
+        FeatureLocation loc = getSubjectFeatureLocation();
+        boolean needToAdd = false;
+        if (loc == null) {
+            loc = new FeatureLocation();
+            needToAdd = true;
+        }
+        loc.setRank(1);
+        loc.setFmin(fmin);
+        loc.setFmax(fmax);
+        loc.setStrand(strand);
+        if (needToAdd) {
+            analysisFeature.getFeatureLocations().add(loc);
+            getFeatureLocations().add(loc);
+        }
+    }
+    def setIdentity(double identity) {
+        if (analysisFeature == null) {
+            return;
+        }
+        analysisFeature.setIdentity(identity);
+    }
+
+
+    def getQueryFeatureLocation() {
+        for (FeatureLocation loc : getFeatureLocations()) {
+            if (loc.getRank() == 0) {
+                return loc;
+            }
+        }
+        return null;
+    }
+
+    def getSubjectFeatureLocation() {
+        for (FeatureLocation loc : getFeatureLocations()) {
+            if (loc.getRank() == 1) {
+                return loc;
+            }
+        }
+        return null;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/MiRNA.groovy b/grails-app/domain/org/bbop/apollo/MiRNA.groovy
new file mode 100644
index 0000000..56201ec
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/MiRNA.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class MiRNA extends NcRNA{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "MiRNA"
+    static String ontologyId = "SO:0000276"
+    static String alternateCvTerm = "miRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Minus1Frameshift.groovy b/grails-app/domain/org/bbop/apollo/Minus1Frameshift.groovy
new file mode 100644
index 0000000..7af25da
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Minus1Frameshift.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class Minus1Frameshift extends Frameshift{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000866"// XX:NNNNNNN
+    static String cvTerm = "Minus1Frameshift"
+
+    // add convenience methods
+    @Override
+    boolean isPlusFrameshift() {
+        return false
+    }
+
+    @Override
+    int getFrameshiftValue() {
+        return 1
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Minus2Frameshift.groovy b/grails-app/domain/org/bbop/apollo/Minus2Frameshift.groovy
new file mode 100644
index 0000000..cacc44a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Minus2Frameshift.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class Minus2Frameshift extends Frameshift{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000867"// XX:NNNNNNN
+    static String cvTerm = "Minus2Frameshift"
+
+    // add convenience methods
+    @Override
+    boolean isPlusFrameshift() {
+        return false
+    }
+
+    @Override
+    int getFrameshiftValue() {
+        return 2
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/NcRNA.groovy b/grails-app/domain/org/bbop/apollo/NcRNA.groovy
new file mode 100644
index 0000000..ab7b994
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/NcRNA.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class NcRNA extends Transcript{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "NcRNA"
+    static String ontologyId = "SO:0000655"
+    static String alternateCvTerm = "ncRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/NonCanonicalFivePrimeSpliceSite.groovy b/grails-app/domain/org/bbop/apollo/NonCanonicalFivePrimeSpliceSite.groovy
new file mode 100644
index 0000000..1456c88
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/NonCanonicalFivePrimeSpliceSite.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+class NonCanonicalFivePrimeSpliceSite extends SpliceSite{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "NonCanonicalFivePrimeSpliceSite"
+    static String ontologyId = "SO:0000679"
+
+
+    static String alternateCvTerm = "non_canonical_five_prime_splice_site"
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/NonCanonicalThreePrimeSpliceSite.groovy b/grails-app/domain/org/bbop/apollo/NonCanonicalThreePrimeSpliceSite.groovy
new file mode 100644
index 0000000..8ce0738
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/NonCanonicalThreePrimeSpliceSite.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+class NonCanonicalThreePrimeSpliceSite extends SpliceSite{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "NonCanonicalThreePrimeSpliceSite"
+    static String ontologyId = "SO:0000678"
+
+
+    static String alternateCvTerm = "non_canonical_three_prime_splice_site"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Operation.groovy b/grails-app/domain/org/bbop/apollo/Operation.groovy
new file mode 100644
index 0000000..494d83a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Operation.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.operation.OperationEnum
+
+/**
+ * We store operation attributes as JSON
+ */
+class Operation {
+   
+    OperationEnum operationType
+    String featureUniqueName
+    // if not self
+//    private Map<String, Object> attributes;
+    
+    String attributes;
+   
+//    List<Feature> oldFeatures
+    String oldFeatures;
+    
+//    List<Feature> newFeatures
+    String newFeatures;
+
+
+    static mapping = {
+        attributes type: 'text'
+        oldFeatures type: 'text'
+        newFeatures type: 'text'
+    }
+
+    static constraints = {
+        attributes nullable: true, blank: false
+        newFeatures nullable: true, blank: false
+        oldFeatures nullable: true, blank: false
+    }
+    
+}
diff --git a/grails-app/domain/org/bbop/apollo/Organism.groovy b/grails-app/domain/org/bbop/apollo/Organism.groovy
new file mode 100644
index 0000000..0dd58be
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Organism.groovy
@@ -0,0 +1,76 @@
+package org.bbop.apollo
+
+
+import groovy.transform.EqualsAndHashCode
+ at EqualsAndHashCode
+class Organism {
+
+    static auditable = true
+
+    static constraints = {
+        comment nullable: true
+        abbreviation nullable: true
+        species nullable: true
+        genus nullable: true
+        valid nullable: true
+        blatdb nullable: true
+        metadata nullable: true
+        commonName nullable: false
+        genomeFasta nullable: true
+        genomeFastaIndex nullable: true
+        nonDefaultTranslationTable nullable: true,blank: false
+        dataAddedViaWebServices nullable: true
+    }
+
+    String abbreviation;
+    String genus;
+    String species;
+    String commonName;
+    String comment;
+    Boolean valid;
+    boolean publicMode;
+    String blatdb;
+    String directory
+    String genomeFasta
+    String genomeFastaIndex
+    String nonDefaultTranslationTable
+    String metadata
+    Boolean dataAddedViaWebServices
+
+    static hasMany = [
+            organismProperties: OrganismProperty
+            , organismDBXrefs : OrganismDBXref
+            , sequences       : Sequence
+            , userPermissions : UserOrganismPermission
+            , groupPermissions: GroupOrganismPermission
+    ]
+
+    String getTrackList() {
+        if (!directory) {
+            return null
+        } else {
+            return directory + "/trackList.json"
+        }
+    }
+
+    String getRefseqFile() {
+        if (!directory) {
+            return null
+        } else {
+            return directory + "/seq/refSeqs.json"
+        }
+    }
+
+    String getGenomeFastaFileName() {
+        return genomeFasta ? directory + File.separator + genomeFasta : null
+    }
+
+    String getGenomeFastaIndexFileName() {
+        return genomeFastaIndex ? directory + File.separator + genomeFastaIndex : null
+    }
+
+    static mapping = {
+        publicMode defaultValue: true
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/OrganismDBXref.groovy b/grails-app/domain/org/bbop/apollo/OrganismDBXref.groovy
new file mode 100644
index 0000000..701349d
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/OrganismDBXref.groovy
@@ -0,0 +1,35 @@
+package org.bbop.apollo
+
+class OrganismDBXref {
+
+    static constraints = {
+    }
+
+     DBXref dbxref;
+     Organism organism;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        OrganismDBXref castOther = ( OrganismDBXref ) other;
+
+        return ( (this.getDbxref()==castOther.getDbxref()) || ( this.getDbxref()!=null && castOther.getDbxref()!=null && this.getDbxref().equals(castOther.getDbxref()) ) ) && ( (this.getOrganism()==castOther.getOrganism()) || ( this.getOrganism()!=null && castOther.getOrganism()!=null && this.getOrganism().equals(castOther.getOrganism()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getDbxref() == null ? 0 : this.getDbxref().hashCode() );
+        result = 37 * result + ( getOrganism() == null ? 0 : this.getOrganism().hashCode() );
+        return result;
+    }
+
+    public OrganismDBXref generateClone() {
+        OrganismDBXref cloned = new OrganismDBXref();
+        cloned.dbxref = this.dbxref;
+        cloned.organism = this.organism;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/OrganismFilter.groovy b/grails-app/domain/org/bbop/apollo/OrganismFilter.groovy
new file mode 100644
index 0000000..ba16b2c
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/OrganismFilter.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+abstract class OrganismFilter {
+
+    Organism organism
+
+    static constraints = {
+        organism nullable: false
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/OrganismProperty.groovy b/grails-app/domain/org/bbop/apollo/OrganismProperty.groovy
new file mode 100644
index 0000000..802af52
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/OrganismProperty.groovy
@@ -0,0 +1,54 @@
+package org.bbop.apollo
+
+class OrganismProperty {
+
+    static constraints = {
+    }
+
+    Integer organismId;
+    String abbreviation;
+    String genus;
+    String species;
+    String commonName;
+    String comment;
+
+    static hasMany = [
+        organismProperties : OrganismProperty
+        ,organismDBXrefs : OrganismDBXref
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        OrganismProperty castOther = ( OrganismProperty ) other;
+
+        return ( (this.getGenus()==castOther.getGenus()) || ( this.getGenus()!=null && castOther.getGenus()!=null && this.getGenus().equals(castOther.getGenus()) ) ) && ( (this.getSpecies()==castOther.getSpecies()) || ( this.getSpecies()!=null && castOther.getSpecies()!=null && this.getSpecies().equals(castOther.getSpecies()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+
+        result = 37 * result + ( getGenus() == null ? 0 : this.getGenus().hashCode() );
+        result = 37 * result + ( getSpecies() == null ? 0 : this.getSpecies().hashCode() );
+
+
+
+
+        return result;
+    }
+
+    public Organism generateClone() {
+        Organism cloned = new Organism();
+        cloned.abbreviation = this.abbreviation;
+        cloned.genus = this.genus;
+        cloned.species = this.species;
+        cloned.commonName = this.commonName;
+        cloned.comment = this.comment;
+        cloned.organismProperties = this.organismProperties;
+        cloned.organismDBXrefs = this.organismDBXrefs;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/PartOf.groovy b/grails-app/domain/org/bbop/apollo/PartOf.groovy
new file mode 100644
index 0000000..f76b794
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PartOf.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class PartOf implements Ontological{
+
+    static constraints = {
+    }
+
+    static String cvTerm = "PartOf"
+    static String ontologyId = "part_of"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Permission.groovy b/grails-app/domain/org/bbop/apollo/Permission.groovy
new file mode 100644
index 0000000..7c900b4
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Permission.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+abstract class Permission {
+    
+    Organism organism
+
+    static constraints = {
+        
+    }
+
+    static mapping = {
+//        trackNames type: "text"
+    }
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Phenotype.groovy b/grails-app/domain/org/bbop/apollo/Phenotype.groovy
new file mode 100644
index 0000000..d1e8771
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Phenotype.groovy
@@ -0,0 +1,56 @@
+package org.bbop.apollo
+class Phenotype {
+
+    static constraints = {
+    }
+
+    CVTerm attribute;
+    CVTerm cvalue;
+    CVTerm assay;
+    CVTerm observable;
+    String uniqueName;
+    String value;
+
+    static hasMany = [
+            phenotypeCVTerms : CVTerm
+            ,phenotypeStatements: PhenotypeStatement
+            ,features: Feature
+    ]
+
+    static belongsTo = [
+            Feature
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Phenotype castOther = ( Phenotype ) other;
+
+        return ( (this.getUniqueName()==castOther.getUniqueName()) || ( this.getUniqueName()!=null && castOther.getUniqueName()!=null && this.getUniqueName().equals(castOther.getUniqueName()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getUniqueName() == null ? 0 : this.getUniqueName().hashCode() );
+
+
+        return result;
+    }
+
+    public Phenotype generateClone() {
+        Phenotype cloned = new Phenotype();
+        cloned.attribute = this.attribute;
+        cloned.cvalue = this.cvalue;
+        cloned.assay = this.assay;
+        cloned.observable = this.observable;
+        cloned.uniqueName = this.uniqueName;
+        cloned.value = this.value;
+        cloned.phenotypeCVTerms = this.phenotypeCVTerms;
+        cloned.phenotypeStatements = this.phenotypeStatements;
+        cloned.features = this.features
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/PhenotypeDescription.groovy b/grails-app/domain/org/bbop/apollo/PhenotypeDescription.groovy
new file mode 100644
index 0000000..6652e88
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PhenotypeDescription.groovy
@@ -0,0 +1,44 @@
+package org.bbop.apollo
+
+class PhenotypeDescription {
+
+    static constraints = {
+    }
+
+    Environment environment;
+    CVTerm type;
+    Publication publication;
+    Genotype genotype;
+    String description;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        PhenotypeDescription castOther = ( PhenotypeDescription ) other;
+
+        return ( (this.getEnvironment()==castOther.getEnvironment()) || ( this.getEnvironment()!=null && castOther.getEnvironment()!=null && this.getEnvironment().equals(castOther.getEnvironment()) ) ) && ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPubli [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getEnvironment() == null ? 0 : this.getEnvironment().hashCode() );
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + ( getGenotype() == null ? 0 : this.getGenotype().hashCode() );
+
+        return result;
+    }
+
+    public PhenotypeDescription generateClone() {
+        PhenotypeDescription cloned = new PhenotypeDescription();
+        cloned.environment = this.environment;
+        cloned.type = this.type;
+        cloned.publication = this.publication;
+        cloned.genotype = this.genotype;
+        cloned.description = this.description;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/PhenotypeStatement.groovy b/grails-app/domain/org/bbop/apollo/PhenotypeStatement.groovy
new file mode 100644
index 0000000..b5d9534
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PhenotypeStatement.groovy
@@ -0,0 +1,46 @@
+package org.bbop.apollo
+
+class PhenotypeStatement {
+
+    static constraints = {
+    }
+
+
+    Environment environment;
+    CVTerm type;
+    Publication publication;
+    Genotype genotype;
+    Phenotype phenotype;
+
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        PhenotypeStatement castOther = ( PhenotypeStatement ) other;
+
+        return ( (this.getEnvironment()==castOther.getEnvironment()) || ( this.getEnvironment()!=null && castOther.getEnvironment()!=null && this.getEnvironment().equals(castOther.getEnvironment()) ) ) && ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPubli [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getEnvironment() == null ? 0 : this.getEnvironment().hashCode() );
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + ( getGenotype() == null ? 0 : this.getGenotype().hashCode() );
+        result = 37 * result + ( getPhenotype() == null ? 0 : this.getPhenotype().hashCode() );
+        return result;
+    }
+
+    public PhenotypeStatement generateClone() {
+        PhenotypeStatement cloned = new PhenotypeStatement();
+        cloned.environment = this.environment;
+        cloned.type = this.type;
+        cloned.publication = this.publication;
+        cloned.genotype = this.genotype;
+        cloned.phenotype = this.phenotype;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Plus1Frameshift.groovy b/grails-app/domain/org/bbop/apollo/Plus1Frameshift.groovy
new file mode 100644
index 0000000..fe7383b
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Plus1Frameshift.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class Plus1Frameshift extends Frameshift{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000868"// XX:NNNNNNN
+    static String cvTerm = "Plus1Frameshift"
+
+    // add convenience methods
+    @Override
+    boolean isPlusFrameshift() {
+        return true
+    }
+
+    @Override
+    int getFrameshiftValue() {
+        return 1
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Plus2Frameshift.groovy b/grails-app/domain/org/bbop/apollo/Plus2Frameshift.groovy
new file mode 100644
index 0000000..b6ff5af
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Plus2Frameshift.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class Plus2Frameshift extends Frameshift{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000860"// XX:NNNNNNN
+    static String cvTerm = "Plus2Frameshift"
+
+    // add convenience methods
+    @Override
+    boolean isPlusFrameshift() {
+        return true
+    }
+
+    @Override
+    int getFrameshiftValue() {
+        return 2
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Preference.groovy b/grails-app/domain/org/bbop/apollo/Preference.groovy
new file mode 100644
index 0000000..ce74afb
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Preference.groovy
@@ -0,0 +1,18 @@
+package org.bbop.apollo
+
+class Preference {
+
+    static constraints = {
+        name nullable: true ,blank: false
+        domain nullable: true ,blank: false
+        preferencesString nullable: true, blank: false
+        clientToken nullable: false, blank: false
+    }
+
+    String name
+    String domain  // if we want to filter for a user / group domain
+    String preferencesString // can embed JSONObject
+    String clientToken // this is a token from the browser
+    Date dateCreated
+    Date lastUpdated
+}
diff --git a/grails-app/domain/org/bbop/apollo/Proxy.groovy b/grails-app/domain/org/bbop/apollo/Proxy.groovy
new file mode 100644
index 0000000..90bc7c5
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Proxy.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+class Proxy {
+
+    static constraints = {
+        fallbackOrder nullable: true
+        lastSuccess nullable: true
+        lastFail nullable: true
+        referenceUrl url: true, nullable: false,blank: false
+        targetUrl url: true, nullable: false,blank: false
+        active nullable: false
+    }
+
+    static mapping= {
+        active defaultValue: true
+    }
+
+    String referenceUrl
+    String targetUrl
+    Boolean active
+    Integer fallbackOrder
+    Date lastSuccess
+    Date lastFail
+}
diff --git a/grails-app/domain/org/bbop/apollo/Pseudogene.groovy b/grails-app/domain/org/bbop/apollo/Pseudogene.groovy
new file mode 100644
index 0000000..c25c959
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Pseudogene.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ * AbstractSingleLocationBioFeature
+ */
+class Pseudogene extends Gene{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000336"// XX:NNNNNNN
+    static String cvTerm = "Pseudogene"// may have a link
+
+    static String alternateCvTerm = "pseudogene"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Publication.groovy b/grails-app/domain/org/bbop/apollo/Publication.groovy
new file mode 100644
index 0000000..1421157
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Publication.groovy
@@ -0,0 +1,90 @@
+package org.bbop.apollo
+
+class Publication {
+
+    static constraints = {
+    }
+
+    CVTerm type;
+    String title;
+    String volumeTitle;
+    String volume;
+    String seriesName;
+    String issue;
+    String publicationYear;
+    String pages;
+    String miniReference;
+    String uniqueName;
+    Boolean isObsolete;
+    String publisher;
+    String publicationPlace;
+
+
+    static hasMany = [
+            childPublicationRelationships : PublicationRelationship
+            ,parentPublicationRelationships : PublicationRelationship
+            ,publicationAuthors: PublicationAuthor
+            ,publicationDBXrefs: PublicationDBXref
+    ]
+
+    static mappedBy = [
+            childPublicationRelationships :"subjectPublication"
+            ,parentPublicationRelationships : "objectPublication"
+    ]
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Publication castOther = ( Publication ) other;
+
+        return ( (this.getUniqueName()==castOther.getUniqueName()) || ( this.getUniqueName()!=null && castOther.getUniqueName()!=null && this.getUniqueName().equals(castOther.getUniqueName()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+
+
+
+
+
+
+
+
+
+        result = 37 * result + ( getUniqueName() == null ? 0 : this.getUniqueName().hashCode() );
+
+
+
+
+
+
+
+        return result;
+    }
+
+    public Publication generateClone() {
+        Publication cloned = new Publication();
+        cloned.type = this.type;
+        cloned.title = this.title;
+        cloned.volumeTitle = this.volumeTitle;
+        cloned.volume = this.volume;
+        cloned.seriesName = this.seriesName;
+        cloned.issue = this.issue;
+        cloned.publicationYear = this.publicationYear;
+        cloned.pages = this.pages;
+        cloned.miniReference = this.miniReference;
+        cloned.uniqueName = this.uniqueName;
+        cloned.isObsolete = this.isObsolete;
+        cloned.publisher = this.publisher;
+        cloned.publicationPlace = this.publicationPlace;
+        cloned.childPublicationRelationships = this.childPublicationRelationships;
+        cloned.publicationAuthors = this.publicationAuthors;
+        cloned.publicationDBXrefs = this.publicationDBXrefs;
+        cloned.parentPublicationRelationships = this.parentPublicationRelationships;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/PublicationAuthor.groovy b/grails-app/domain/org/bbop/apollo/PublicationAuthor.groovy
new file mode 100644
index 0000000..9fd5daa
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PublicationAuthor.groovy
@@ -0,0 +1,47 @@
+package org.bbop.apollo
+
+class PublicationAuthor {
+
+    static constraints = {
+    }
+
+    Publication publication;
+    int rank;
+    Boolean editor;
+    String surname;
+    String givenNames;
+    String suffix;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        PublicationAuthor castOther = ( PublicationAuthor ) other;
+
+        return ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPublication().equals(castOther.getPublication()) ) ) && (this.getRank()==castOther.getRank());
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + this.getRank();
+
+
+
+
+        return result;
+    }
+
+    public PublicationAuthor generateClone() {
+        PublicationAuthor cloned = new PublicationAuthor();
+        cloned.publication = this.publication;
+        cloned.rank = this.rank;
+        cloned.editor = this.editor;
+        cloned.surname = this.surname;
+        cloned.givenNames = this.givenNames;
+        cloned.suffix = this.suffix;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/PublicationDBXref.groovy b/grails-app/domain/org/bbop/apollo/PublicationDBXref.groovy
new file mode 100644
index 0000000..41bc393
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PublicationDBXref.groovy
@@ -0,0 +1,40 @@
+package org.bbop.apollo
+
+class PublicationDBXref {
+
+    static constraints = {
+    }
+
+
+    Publication publication;
+    DBXref dbxref;
+    boolean isCurrent;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        PublicationDBXref castOther = ( PublicationDBXref ) other;
+
+        return ( (this.getPublication()==castOther.getPublication()) || ( this.getPublication()!=null && castOther.getPublication()!=null && this.getPublication().equals(castOther.getPublication()) ) ) && ( (this.getDbxref()==castOther.getDbxref()) || ( this.getDbxref()!=null && castOther.getDbxref()!=null && this.getDbxref().equals(castOther.getDbxref()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getPublication() == null ? 0 : this.getPublication().hashCode() );
+        result = 37 * result + ( getDbxref() == null ? 0 : this.getDbxref().hashCode() );
+
+        return result;
+    }
+
+    public PublicationDBXref generateClone() {
+        PublicationDBXref cloned = new PublicationDBXref();
+        cloned.publication = this.publication;
+        cloned.dbxref = this.dbxref;
+        cloned.isCurrent = this.isCurrent;
+        return cloned;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/PublicationRelationship.groovy b/grails-app/domain/org/bbop/apollo/PublicationRelationship.groovy
new file mode 100644
index 0000000..f472697
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/PublicationRelationship.groovy
@@ -0,0 +1,37 @@
+package org.bbop.apollo
+
+class PublicationRelationship {
+
+    static constraints = {
+    }
+
+    Publication subjectPublication;
+    Publication objectPublication;
+    CVTerm type;
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        PublicationRelationship castOther = (PublicationRelationship) other;
+
+        return ((this.getSubjectPublication() == castOther.getSubjectPublication()) || (this.getSubjectPublication() != null && castOther.getSubjectPublication() != null && this.getSubjectPublication().equals(castOther.getSubjectPublication()))) && ((this.getType() == castOther.getType()) || (this.getType() != null && castOther.getType() != null && this.getType().equals(castOther.getType()))) && ((this.getObjectPublication() == castOther.getObjectPublication()) || (this.getObjectPublicat [...]
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + (getSubjectPublication() == null ? 0 : this.getSubjectPublication().hashCode());
+        result = 37 * result + (getType() == null ? 0 : this.getType().hashCode());
+        result = 37 * result + (getObjectPublication() == null ? 0 : this.getObjectPublication().hashCode());
+        return result;
+    }
+
+    public PublicationRelationship generateClone() {
+        PublicationRelationship cloned = new PublicationRelationship();
+        cloned.subjectPublication = this.subjectPublication;
+        cloned.type = this.type;
+        cloned.objectPublication = this.objectPublication;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/RRNA.groovy b/grails-app/domain/org/bbop/apollo/RRNA.groovy
new file mode 100644
index 0000000..7af42c3
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/RRNA.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class RRNA extends NcRNA{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "RRNA"
+    static String ontologyId = "SO:0000252"
+    static String alternateCvTerm = "rRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Region.groovy b/grails-app/domain/org/bbop/apollo/Region.groovy
new file mode 100644
index 0000000..bad7055
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Region.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+/**
+ * In the ontology, this is a "is_a" relationship . .. not sure if it makes sense to keep it that way, though
+ */
+class Region extends SequenceFeature {
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000001"// XX:NNNNNNN
+    static String cvTerm = "Region"// may have a link
+
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/RepeatRegion.groovy b/grails-app/domain/org/bbop/apollo/RepeatRegion.groovy
new file mode 100644
index 0000000..120573e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/RepeatRegion.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * From Scaffold
+ */
+class RepeatRegion extends BiologicalRegion{
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "RepeatRegion"
+    static String ontologyId = "SO:0000657"
+    static String alternateCvTerm = "repeat_region"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Role.groovy b/grails-app/domain/org/bbop/apollo/Role.groovy
new file mode 100644
index 0000000..eebadd1
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Role.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+/**
+ * These are global roles.
+ */
+class Role {
+    String name
+
+    static hasMany = [ users: User, permissions: String ]
+    static belongsTo = User
+
+    static constraints = {
+        name(nullable: false, blank: false, unique: true)
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/SearchTool.groovy b/grails-app/domain/org/bbop/apollo/SearchTool.groovy
new file mode 100644
index 0000000..ddf4b63
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SearchTool.groovy
@@ -0,0 +1,19 @@
+package org.bbop.apollo
+
+class SearchTool {
+
+    static constraints = {
+    }
+
+    String key
+    String implementationClass
+    String binaryPath
+    String tmpDir
+    String databasePath
+    String options
+    boolean removeTempDirectory
+    static mapping = {
+        key column: "search_key"
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Sequence.groovy b/grails-app/domain/org/bbop/apollo/Sequence.groovy
new file mode 100644
index 0000000..65acc58
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Sequence.groovy
@@ -0,0 +1,39 @@
+package org.bbop.apollo
+
+class Sequence {
+
+    static auditable = true
+
+    static constraints = {
+        name nullable: false
+        start nullable: false
+        end nullable: false
+        organism nullable: true
+        seqChunkSize nullable: true
+    }
+
+
+    // feature locations instead of features
+    static hasMany = [
+        featureLocations: FeatureLocation,
+        sequenceChunks: SequenceChunk
+    ]
+
+    static mapping = {
+        cache usage: 'read-only'
+        end column: 'sequence_end'
+        start column: 'sequence_start'
+        featureLocations cascade: 'all-delete-orphan'
+        sequenceChunks cascade: 'all-delete-orphan'
+    }
+
+    static belongsTo = [Organism]
+
+
+    String name
+    Organism organism
+    Integer length
+    Integer seqChunkSize
+    Integer start
+    Integer end
+}
diff --git a/grails-app/domain/org/bbop/apollo/SequenceAlteration.groovy b/grails-app/domain/org/bbop/apollo/SequenceAlteration.groovy
new file mode 100644
index 0000000..1a5f464
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SequenceAlteration.groovy
@@ -0,0 +1,24 @@
+package org.bbop.apollo
+
+class SequenceAlteration extends SequenceFeature{
+
+
+
+    static constraints = {
+        alterationResidue nullable: true
+    }
+
+    String alterationResidue
+
+    static String cvTerm  = "SequenceAlteration"
+    static String ontologyId = "SO:0001059"
+
+    /** Get the offset added by the sequence alteration.
+     *
+     * @return Offset added by the sequence alteration
+     */
+    public int getOffset() {
+        return 0;
+    }
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/SequenceAttribute.groovy b/grails-app/domain/org/bbop/apollo/SequenceAttribute.groovy
new file mode 100644
index 0000000..d5bcec4
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SequenceAttribute.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class SequenceAttribute extends FeatureProperty implements Ontological {
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000400"// XX:NNNNNNN
+
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/SequenceCache.groovy b/grails-app/domain/org/bbop/apollo/SequenceCache.groovy
new file mode 100644
index 0000000..e7153ea
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SequenceCache.groovy
@@ -0,0 +1,32 @@
+package org.bbop.apollo
+
+class SequenceCache {
+
+    String sequenceName
+    String organismName
+    String type
+
+    Long fmin
+    Long fmax
+    String featureName
+    String paramMap
+
+    String response // JSON response
+
+    static constraints = {
+        sequenceName nullable: false, blank: false
+        organismName nullable: false, blank: false
+        type nullable: true, blank: false
+
+        fmin nullable: true
+        fmax nullable: true
+        featureName nullable: true
+        paramMap nullable: true, blank: true
+    }
+
+    static mapping = {
+        response type: "text"
+        sequenceName type: "text"
+        paramMap type: "text"
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/SequenceChunk.groovy b/grails-app/domain/org/bbop/apollo/SequenceChunk.groovy
new file mode 100644
index 0000000..e1c9d48
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SequenceChunk.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+class SequenceChunk {
+
+    static constraints = {
+    }
+
+    static mapping = {
+        residue type: "text"
+        cache usage: 'read-only'
+    }
+
+    Sequence sequence
+    int chunkNumber
+    String residue
+}
diff --git a/grails-app/domain/org/bbop/apollo/SequenceFeature.groovy b/grails-app/domain/org/bbop/apollo/SequenceFeature.groovy
new file mode 100644
index 0000000..d40a8af
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SequenceFeature.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class SequenceFeature extends Feature implements Ontological {
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000110"// XX:NNNNNNN
+
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/ServerData.groovy b/grails-app/domain/org/bbop/apollo/ServerData.groovy
new file mode 100644
index 0000000..8a0b822
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/ServerData.groovy
@@ -0,0 +1,21 @@
+package org.bbop.apollo
+
+class ServerData {
+
+    static constraints = {
+        name unique: true
+    }
+
+    String name
+    Date dateCreated
+    Date lastUpdated
+
+    String getName(){
+        if(!name){
+            name = "ApolloSever-${org.bbop.apollo.gwt.shared.ClientTokenGenerator.generateRandomString()}"
+        }
+        return name
+    }
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/SnRNA.groovy b/grails-app/domain/org/bbop/apollo/SnRNA.groovy
new file mode 100644
index 0000000..19dbabb
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SnRNA.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo
+
+class SnRNA extends Transcript{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "SnRNA"
+    static String ontologyId = "SO:0000274"
+    static String alternateCvTerm = "snRNA"
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/SnoRNA.groovy b/grails-app/domain/org/bbop/apollo/SnoRNA.groovy
new file mode 100644
index 0000000..667bff3
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SnoRNA.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class SnoRNA extends NcRNA{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "SnoRNA"
+    static String ontologyId = "SO:0000275"
+    static String alternateCvTerm = "snoRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/SpliceSite.groovy b/grails-app/domain/org/bbop/apollo/SpliceSite.groovy
new file mode 100644
index 0000000..fde3afc
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SpliceSite.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+class SpliceSite extends TranscriptRegion{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "Chromosome"
+    static String ontologyId = "SO:0000162"
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/Status.groovy b/grails-app/domain/org/bbop/apollo/Status.groovy
new file mode 100644
index 0000000..92a2cf7
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Status.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+
+class Status extends FeatureProperty{
+
+    static constraints = {
+    }
+
+    static String cvTerm = "Status"
+    static String ontologyId = "Status"
+//    static String ontologyId = "SO:0000905" // Note the same as the SO version
+}
diff --git a/grails-app/domain/org/bbop/apollo/StopCodonReadThrough.groovy b/grails-app/domain/org/bbop/apollo/StopCodonReadThrough.groovy
new file mode 100644
index 0000000..dee105e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/StopCodonReadThrough.groovy
@@ -0,0 +1,19 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ * Maps to: ReadthroughStopCodon
+ */
+
+// its unclear how this should be handled properly as its closer to a CDS
+//class StopCodonReadThrough extends CDS{
+class StopCodonReadThrough extends Feature implements Ontological {
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000883"// XX:NNNNNNN
+    //static String cvTerm = "ReadthroughStopCodon"
+    static String cvTerm = "stop_codon_read_through"
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/Substitution.groovy b/grails-app/domain/org/bbop/apollo/Substitution.groovy
new file mode 100644
index 0000000..79aed45
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Substitution.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+class Substitution extends SequenceAlteration{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "Substitution"
+    static String ontologyId = "SO:1000002"
+    static String alternateCvTerm = "substitution"
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/SuperContig.groovy b/grails-app/domain/org/bbop/apollo/SuperContig.groovy
new file mode 100644
index 0000000..96b426a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/SuperContig.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+/**
+ * From Scaffold
+ */
+class SuperContig extends Region{
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "Chromosome"
+    static String ontologyId = "SO:0000148"
+}
diff --git a/grails-app/domain/org/bbop/apollo/Synonym.groovy b/grails-app/domain/org/bbop/apollo/Synonym.groovy
new file mode 100644
index 0000000..630051a
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Synonym.groovy
@@ -0,0 +1,39 @@
+package org.bbop.apollo
+
+class Synonym {
+
+    static constraints = {
+    }
+
+
+    CVTerm type;
+    String name;
+    String synonymSGML;
+
+
+    public boolean equals(Object other) {
+        if (this.is(other)) return true
+        if (getClass() != other.class) return false
+        Synonym castOther = ( Synonym ) other;
+
+        return ( (this.getType()==castOther.getType()) || ( this.getType()!=null && castOther.getType()!=null && this.getType().equals(castOther.getType()) ) ) && ( (this.getName()==castOther.getName()) || ( this.getName()!=null && castOther.getName()!=null && this.getName().equals(castOther.getName()) ) );
+    }
+
+    public int hashCode() {
+        int result = 17;
+
+
+        result = 37 * result + ( getType() == null ? 0 : this.getType().hashCode() );
+        result = 37 * result + ( getName() == null ? 0 : this.getName().hashCode() );
+
+        return result;
+    }
+
+    public Synonym generateClone() {
+        Synonym cloned = new Synonym();
+        cloned.type = this.type;
+        cloned.name = this.name;
+        cloned.synonymSGML = this.synonymSGML;
+        return cloned;
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/TRNA.groovy b/grails-app/domain/org/bbop/apollo/TRNA.groovy
new file mode 100644
index 0000000..f3fe125
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/TRNA.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo
+
+class TRNA extends NcRNA{
+
+    static constraints = {
+    }
+
+    static String cvTerm =  "TRNA"
+    static String ontologyId = "SO:0000253"
+
+    static String alternateCvTerm = "tRNA"
+}
diff --git a/grails-app/domain/org/bbop/apollo/TrackCache.groovy b/grails-app/domain/org/bbop/apollo/TrackCache.groovy
new file mode 100644
index 0000000..905e03e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/TrackCache.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo
+
+class TrackCache {
+
+    String trackName
+    String sequenceName
+    String organismName
+    String type
+
+
+    Long fmin
+    Long fmax
+    String featureName
+    String paramMap
+
+    String response // JSON response
+
+    static constraints = {
+        trackName nullable: false, blank: false
+        sequenceName nullable: false, blank: false
+        organismName nullable: false, blank: false
+        type nullable: true, blank: false
+
+        fmin nullable: true
+        fmax nullable: true
+        featureName nullable: true
+        paramMap nullable: true, blank: true
+    }
+
+    static mapping = {
+        response type: "text"
+        trackName type: "text"
+        sequenceName type: "text"
+        paramMap type: "text"
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/Transcript.groovy b/grails-app/domain/org/bbop/apollo/Transcript.groovy
new file mode 100644
index 0000000..efbb88b
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/Transcript.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ * AbstractSingleLocationBioFeature
+ */
+class Transcript extends GeneMemberRegion{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000673"// XX:NNNNNNN
+    static String cvTerm = "Transcript"// may have a link
+    static String alternateCvTerm = "transcript"
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/TranscriptAttribute.groovy b/grails-app/domain/org/bbop/apollo/TranscriptAttribute.groovy
new file mode 100644
index 0000000..9ef2f44
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/TranscriptAttribute.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+/**
+ * Note: top-level in the sequence ontology
+ */
+class TranscriptAttribute extends FeatureAttribute{
+
+    static constraints = {
+    }
+
+    static String ontologyId = "SO:0000237"// XX:NNNNNNN
+
+    // add convenience methods
+}
diff --git a/grails-app/domain/org/bbop/apollo/TranscriptRegion.groovy b/grails-app/domain/org/bbop/apollo/TranscriptRegion.groovy
new file mode 100644
index 0000000..4c4033e
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/TranscriptRegion.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+/**
+ * Inherited from here:
+ */
+class TranscriptRegion extends BiologicalRegion{
+
+    static constraints = {
+    }
+
+
+    static String ontologyId = "SO:0000833"  // XX:NNNNNNN
+}
diff --git a/grails-app/domain/org/bbop/apollo/TransposableElement.groovy b/grails-app/domain/org/bbop/apollo/TransposableElement.groovy
new file mode 100644
index 0000000..fffca22
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/TransposableElement.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+class TransposableElement extends BiologicalRegion{
+
+
+    static constraints = {
+    }
+
+    static String cvTerm  = "TransposableElement"
+    static String ontologyId = "SO:0000101"
+
+    static String alternateCvTerm = "transposable_element"
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/User.groovy b/grails-app/domain/org/bbop/apollo/User.groovy
new file mode 100644
index 0000000..c07b6fb
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/User.groovy
@@ -0,0 +1,75 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+/**
+ * Maps to CVTerm Owner, no Ontology term
+ */
+class User implements Ontological {
+
+
+    static auditable = true
+
+    // TODO: username should be mapped to "value" of FeatureProperty
+    String username
+    String passwordHash
+    String firstName
+    String lastName
+    String metadata // this is JSON metadata
+
+    static String cvTerm = "Owner"
+    static String ontologyId = "Owner"
+
+    static hasMany = [roles: Role, userGroups: UserGroup]
+
+    static belongsTo = [
+            UserGroup
+    ]
+
+
+    static constraints = {
+        username(nullable: false, blank: false, unique: true)
+        passwordHash(display: false, blank: false, null: false,minSize: 5)
+        metadata(display: false, blank: true,nullable: true)
+    }
+
+    static mapping = {
+        table "grails_user"
+//        password column: "grails_password"
+    }
+
+    private void validateMetaData(){
+        // resets bad JSON
+        if(!metadata || !metadata.startsWith("{") || !metadata.endsWith("}")){
+            metadata = "{}"
+        }
+    }
+
+    JSONObject addMetaData(String key,String value){
+        validateMetaData()
+        JSONObject jsonObject = JSON.parse(metadata) as JSONObject
+        jsonObject.put(key,value)
+        metadata = jsonObject.toString()
+        return jsonObject
+    }
+
+    def getMetaData(String key){
+        validateMetaData()
+        JSONObject jsonObject = JSON.parse(metadata) as JSONObject
+        return jsonObject.containsKey(key) ? jsonObject.get(key) : null
+    }
+
+    JSONObject getMetaDataObject(){
+        validateMetaData()
+        return JSON.parse(metadata) as JSONObject
+    }
+
+    def removeMetaData(String key){
+        validateMetaData()
+        JSONObject jsonObject = JSON.parse(metadata) as JSONObject
+        String value = jsonObject.remove(key)
+        metadata = jsonObject.toString()
+        return value
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserGroup.groovy b/grails-app/domain/org/bbop/apollo/UserGroup.groovy
new file mode 100644
index 0000000..c4fd8cb
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserGroup.groovy
@@ -0,0 +1,14 @@
+package org.bbop.apollo
+
+class UserGroup {
+
+    static constraints = {
+    }
+
+    static hasMany = [
+            users: User
+    ]
+
+    String name
+    boolean publicGroup = false
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserOrganismPermission.groovy b/grails-app/domain/org/bbop/apollo/UserOrganismPermission.groovy
new file mode 100644
index 0000000..60067aa
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserOrganismPermission.groovy
@@ -0,0 +1,32 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONArray
+
+class UserOrganismPermission extends UserPermission{
+    
+    String permissions // JSONArray wrapping PermissionEnum
+
+    List<String> getPermissionValues(){
+        def returnList = []
+        if(permissions){
+            JSONArray jsonArray = JSON.parse(permissions) as JSONArray
+            for(int i = 0  ; i < jsonArray.size() ; i++){
+                returnList << jsonArray.getString(i)
+            }
+        }
+
+        return returnList
+    }
+    
+    static belongsTo = [ Organism ]
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+    
+
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserOrganismPreference.groovy b/grails-app/domain/org/bbop/apollo/UserOrganismPreference.groovy
new file mode 100644
index 0000000..51b1ec7
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserOrganismPreference.groovy
@@ -0,0 +1,41 @@
+package org.bbop.apollo
+
+class UserOrganismPreference extends UserPreference{
+
+    static constraints = {
+        organism nullable: false
+        currentOrganism nullable: false
+        nativeTrackList nullable: true
+        sequence nullable: true, blank: false
+        startbp nullable: true, blank: false
+        endbp nullable: true, blank: false
+    }
+
+    Organism organism
+    Boolean currentOrganism  // this means the "active" client token
+    Boolean nativeTrackList
+    Sequence sequence
+    Integer startbp
+    Integer endbp
+
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (!(o instanceof UserOrganismPreference)) return false
+
+        UserOrganismPreference that = (UserOrganismPreference) o
+
+        if (id != that.id) return false
+        if (organism != that.organism) return false
+        if (sequence != that.sequence) return false
+
+        return true
+    }
+
+    int hashCode() {
+        int result
+        result = organism.hashCode()
+        result = 31 * result + sequence.hashCode()
+        result = 31 * result + id.hashCode()
+        return result
+    }
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserPermission.groovy b/grails-app/domain/org/bbop/apollo/UserPermission.groovy
new file mode 100644
index 0000000..ab5e09b
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserPermission.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+class UserPermission extends Permission{
+    
+    User user
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+    
+    
+
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserPreference.groovy b/grails-app/domain/org/bbop/apollo/UserPreference.groovy
new file mode 100644
index 0000000..9a4f7eb
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserPreference.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+class UserPreference extends Preference{
+
+    static constraints = {
+        user nullable: false
+    }
+
+    User user
+}
diff --git a/grails-app/domain/org/bbop/apollo/UserTrackPermission.groovy b/grails-app/domain/org/bbop/apollo/UserTrackPermission.groovy
new file mode 100644
index 0000000..26e3dfe
--- /dev/null
+++ b/grails-app/domain/org/bbop/apollo/UserTrackPermission.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo
+
+class UserTrackPermission extends UserPermission{
+
+    String trackVisibilities // JSON representation (name:'',visible:t/f)
+
+    static constraints = {
+    }
+
+    static mapping = {
+    }
+
+}
diff --git a/grails-app/domain/org/gmod/chado/Chadoprop.groovy b/grails-app/domain/org/gmod/chado/Chadoprop.groovy
new file mode 100644
index 0000000..222b45f
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Chadoprop.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class Chadoprop {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    String value
+    Integer rank
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "chadoprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id"]
+        rank unique: ["cvterm"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cv.groovy b/grails-app/domain/org/gmod/chado/Cv.groovy
new file mode 100644
index 0000000..0b0b43c
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cv.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class Cv {
+
+    String name
+    String definition
+
+    static hasMany = [cvprops    : Cvprop,
+                      cvtermpaths: Cvtermpath,
+                      cvterms    : Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cv_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name unique: true
+        definition nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cvprop.groovy b/grails-app/domain/org/gmod/chado/Cvprop.groovy
new file mode 100644
index 0000000..6096bb7
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cvprop.groovy
@@ -0,0 +1,25 @@
+package org.gmod.chado
+
+class Cvprop {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    String value
+    Integer rank
+    Cv cv
+    Cvterm type
+
+    static belongsTo = [Cv, Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "cv_id"]
+        rank unique: ["type", "cv"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cvterm.groovy b/grails-app/domain/org/gmod/chado/Cvterm.groovy
new file mode 100644
index 0000000..b579b81
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cvterm.groovy
@@ -0,0 +1,157 @@
+package org.gmod.chado
+
+class Cvterm {
+
+    String name
+    String definition
+    Integer isObsolete
+    Integer isRelationshiptype
+    Dbxref dbxref
+    Cv cv
+
+    static hasMany = [
+//			acquisitionRelationships: AcquisitionRelationship,
+//	                  acquisitionprops: Acquisitionprop,
+//analysisfeatureprops            : Analysisfeatureprop,
+//analysisprops                   : Analysisprop,
+//	                  arraydesignprops: Arraydesignprop,
+//	                  arraydesignsForPlatformtypeId: Arraydesign,
+//	                  arraydesignsForSubstratetypeId: Arraydesign,
+//	                  assayprops: Assayprop,
+//	                  biomaterialRelationships: BiomaterialRelationship,
+//	                  biomaterialTreatments: BiomaterialTreatment,
+//	                  biomaterialprops: Biomaterialprop,
+//	                  cellLineCvtermprops: CellLineCvtermprop,
+//	                  cellLineCvterms: CellLineCvterm,
+//	                  cellLineRelationships: CellLineRelationship,
+//	                  cellLineprops: CellLineprop,
+chadoprops                      : Chadoprop,
+//contactRelationships            : ContactRelationship,
+//contacts                        : Contact,
+//	                  controls: Control,
+cvprops                         : Cvprop,
+cvtermDbxrefs                   : CvtermDbxref,
+cvtermRelationshipsForObjectId  : CvtermRelationship,
+cvtermRelationshipsForSubjectId : CvtermRelationship,
+cvtermRelationshipsForTypeId    : CvtermRelationship,
+cvtermpathsForObjectId          : Cvtermpath,
+cvtermpathsForSubjectId         : Cvtermpath,
+cvtermpathsForTypeId            : Cvtermpath,
+cvtermpropsForCvtermId          : Cvtermprop,
+cvtermpropsForTypeId            : Cvtermprop,
+cvtermsynonymsForCvtermId       : Cvtermsynonym,
+cvtermsynonymsForTypeId         : Cvtermsynonym,
+dbxrefprops                     : Dbxrefprop,
+//	                  elementRelationships: ElementRelationship,
+//	                  elementresultRelationships: ElementresultRelationship,
+//	                  elements: Element,
+environmentCvterms              : EnvironmentCvterm,
+expressionCvtermprops           : ExpressionCvtermprop,
+expressionCvtermsForCvtermId    : ExpressionCvterm,
+expressionCvtermsForCvtermTypeId: ExpressionCvterm,
+expressionprops                 : Expressionprop,
+featureCvtermprops              : FeatureCvtermprop,
+featureCvterms                  : FeatureCvterm,
+featureExpressionprops          : FeatureExpressionprop,
+featureGenotypes                : FeatureGenotype,
+featurePubprops                 : FeaturePubprop,
+featureRelationshipprops        : FeatureRelationshipprop,
+featureRelationships            : FeatureRelationship,
+featuremaps                     : Featuremap,
+featureprops                    : Featureprop,
+features                        : Feature,
+genotypeprops                   : Genotypeprop,
+genotypes                       : Genotype,
+//libraries                       : Library,
+//libraryCvterms                  : LibraryCvterm,
+//libraryprops                    : Libraryprop,
+//ndExperimentStockprops          : NdExperimentStockprop,
+//ndExperimentStocks              : NdExperimentStock,
+//ndExperimentprops               : NdExperimentprop,
+//ndExperiments                   : NdExperiment,
+//ndGeolocationprops              : NdGeolocationprop,
+//ndProtocolReagents              : NdProtocolReagent,
+//ndProtocolprops                 : NdProtocolprop,
+//ndProtocols                     : NdProtocol,
+//ndReagentRelationships          : NdReagentRelationship,
+//ndReagentprops                  : NdReagentprop,
+//ndReagents                      : NdReagent,
+organismprops                   : Organismprop,
+phendescs                       : Phendesc,
+phenotypeComparisonCvterms      : PhenotypeComparisonCvterm,
+phenotypeCvterms                : PhenotypeCvterm,
+phenotypesForAssayId            : Phenotype,
+phenotypesForAttrId             : Phenotype,
+phenotypesForCvalueId           : Phenotype,
+phenotypesForObservableId       : Phenotype,
+phenstatements                  : Phenstatement,
+phylonodeRelationships          : PhylonodeRelationship,
+phylonodeprops                  : Phylonodeprop,
+phylonodes                      : Phylonode,
+phylotrees                      : Phylotree,
+//projectRelationships            : ProjectRelationship,
+//projectprops                    : Projectprop,
+//	                  protocolparamsForDatatypeId: Protocolparam,
+//	                  protocolparamsForUnittypeId: Protocolparam,
+//	                  protocols: Protocol,
+pubRelationships                : PubRelationship,
+pubprops                        : Pubprop,
+pubs                            : Pub,
+//	                  quantificationRelationships: QuantificationRelationship,
+//	                  quantificationprops: Quantificationprop,
+//stockCvtermprops                : StockCvtermprop,
+//stockCvterms                    : StockCvterm,
+//stockDbxrefprops                : StockDbxrefprop,
+//stockRelationshipCvterms        : StockRelationshipCvterm,
+//stockRelationships              : StockRelationship,
+//stockcollectionprops            : Stockcollectionprop,
+//stockcollections                : Stockcollection,
+//stockprops                      : Stockprop,
+//stocks                          : Stock,
+//	                  studydesignprops: Studydesignprop,
+//	                  studyfactors: Studyfactor,
+//	                  studypropFeatures: StudypropFeature,
+//	                  studyprops: Studyprop,
+synonyms                        : Synonym
+//	                  treatments: Treatment
+    ]
+    static belongsTo = [Cv, Dbxref]
+
+    // TODO you have multiple hasMany references for class(es) [Arraydesign, CvtermRelationship, Cvtermpath, Cvtermprop, Cvtermsynonym, ExpressionCvterm, Phenotype, Protocolparam]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [
+//			arraydesignsForPlatformtypeId: "TODO",
+//	                   arraydesignsForSubstratetypeId: "TODO",
+cvtermRelationshipsForObjectId  : "object",
+cvtermRelationshipsForSubjectId : "subject",
+cvtermRelationshipsForTypeId    : "type",
+cvtermpathsForObjectId          : "object",
+cvtermpathsForSubjectId         : "subject",
+cvtermpathsForTypeId            : "type",
+cvtermpropsForCvtermId          : "cvterm",
+cvtermpropsForTypeId            : "type",
+cvtermsynonymsForCvtermId       : "cvterm",
+cvtermsynonymsForTypeId         : "type",
+expressionCvtermsForCvtermId    : "cvterm",
+//expressionCvtermsForCvtermTypeId: "cvtermByCvtermTypeId",
+phenotypesForAssayId            : "assay",
+phenotypesForAttrId             : "attr",
+phenotypesForCvalueId           : "cvalue",
+phenotypesForObservableId       : "observable"
+//	                   protocolparamsForDatatypeId: "TODO",
+//	                   protocolparamsForUnittypeId: "TODO"
+    ]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvterm_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name maxSize: 1024
+        definition nullable: true
+//		isObsolete unique: ["cv_id", "name"]
+//        isObsolete unique: ["cv", "name"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/CvtermDbxref.groovy b/grails-app/domain/org/gmod/chado/CvtermDbxref.groovy
new file mode 100644
index 0000000..f5c1054
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/CvtermDbxref.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class CvtermDbxref {
+
+    Integer isForDefinition
+    Dbxref dbxref
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm, Dbxref]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvterm_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/CvtermRelationship.groovy b/grails-app/domain/org/gmod/chado/CvtermRelationship.groovy
new file mode 100644
index 0000000..163d1cd
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/CvtermRelationship.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class CvtermRelationship {
+
+    Cvterm subject
+    Cvterm object
+    Cvterm type
+
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvterm_relationship_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cvtermpath.groovy b/grails-app/domain/org/gmod/chado/Cvtermpath.groovy
new file mode 100644
index 0000000..25bbaa5
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cvtermpath.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class Cvtermpath {
+
+    Integer pathdistance
+    Cv cv
+    Cvterm subject
+    Cvterm object
+    Cvterm type
+
+    static belongsTo = [Cv, Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvtermpath_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		pathdistance nullable: true, unique: ["type_id", "object_id", "subject_id"]
+        pathdistance nullable: true, unique: ["cv", "subject", "object"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cvtermprop.groovy b/grails-app/domain/org/gmod/chado/Cvtermprop.groovy
new file mode 100644
index 0000000..2e0655d
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cvtermprop.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class Cvtermprop {
+
+    String value
+    Integer rank
+    Cvterm cvterm
+    Cvterm type
+
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvtermprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["value", "type_id", "cvterm_id"]
+        rank unique: ["value", "type", "cvterm"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Cvtermsynonym.groovy b/grails-app/domain/org/gmod/chado/Cvtermsynonym.groovy
new file mode 100644
index 0000000..86ebad5
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Cvtermsynonym.groovy
@@ -0,0 +1,21 @@
+package org.gmod.chado
+
+class Cvtermsynonym {
+
+    String synonym
+    Cvterm cvterm
+    Cvterm type
+
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "cvtermsynonym_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		synonym maxSize: 1024, unique: ["cvterm_id"]
+        synonym maxSize: 1024, unique: ["cvterm"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Db.groovy b/grails-app/domain/org/gmod/chado/Db.groovy
new file mode 100644
index 0000000..87a2779
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Db.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class Db {
+
+    String name
+    String description
+    String urlprefix
+    String url
+
+    static hasMany = [dbxrefs: Dbxref]
+
+    static mapping = {
+        datasource "chado"
+        id column: "db_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name unique: true
+        description nullable: true
+        urlprefix nullable: true
+        url nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Dbxref.groovy b/grails-app/domain/org/gmod/chado/Dbxref.groovy
new file mode 100644
index 0000000..22de890
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Dbxref.groovy
@@ -0,0 +1,48 @@
+package org.gmod.chado
+
+class Dbxref {
+
+    String accession
+    String version
+    String description
+//    Serializable searchableAccession
+    Db db
+
+    static hasMany = [
+//			arraydesigns: Arraydesign,
+//	                  assays: Assay,
+//	                  biomaterialDbxrefs: BiomaterialDbxref,
+//	                  biomaterials: Biomaterial,
+//	                  cellLineDbxrefs: CellLineDbxref,
+cvtermDbxrefs           : CvtermDbxref,
+cvterms                 : Cvterm,
+dbxrefprops             : Dbxrefprop,
+//	                  elements: Element,
+featureCvtermDbxrefs    : FeatureCvtermDbxref,
+featureDbxrefs          : FeatureDbxref,
+features                : Feature,
+//libraryDbxrefs          : LibraryDbxref,
+//ndExperimentDbxrefs     : NdExperimentDbxref,
+//ndExperimentStockDbxrefs: NdExperimentStockDbxref,
+organismDbxrefs         : OrganismDbxref,
+phylonodeDbxrefs        : PhylonodeDbxref,
+phylotrees              : Phylotree,
+//	                  protocols: Protocol,
+pubDbxrefs              : PubDbxref,
+//stockDbxrefs            : StockDbxref,
+//stocks                  : Stock
+//	                  studies: Study
+    ]
+    static belongsTo = [Db]
+
+    static mapping = {
+        datasource "chado"
+        id column: "dbxref_id", generator: "increment"
+    }
+
+    static constraints = {
+        description nullable: true
+//        searchableAccession nullable: true
+        version nullable: true  // this is against Chado specification in http://gmod.org/wiki/Chado_Tables#Table:_dbxref
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Dbxrefprop.groovy b/grails-app/domain/org/gmod/chado/Dbxrefprop.groovy
new file mode 100644
index 0000000..b2c3410
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Dbxrefprop.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class Dbxrefprop {
+
+    String value
+    Integer rank
+    Dbxref dbxref
+    Cvterm type
+
+    static belongsTo = [Cvterm, Dbxref]
+
+    static mapping = {
+        datasource "chado"
+        id column: "dbxrefprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["type_id", "dbxref_id"]
+        rank unique: ["type", "dbxref"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Eimage.groovy b/grails-app/domain/org/gmod/chado/Eimage.groovy
new file mode 100644
index 0000000..6888144
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Eimage.groovy
@@ -0,0 +1,21 @@
+package org.gmod.chado
+
+class Eimage {
+
+    String eimageData
+    String eimageType
+    String imageUri
+
+    static hasMany = [expressionImages: ExpressionImage]
+
+    static mapping = {
+        datasource "chado"
+        id column: "eimage_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        eimageData nullable: true
+        imageUri nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Environment.groovy b/grails-app/domain/org/gmod/chado/Environment.groovy
new file mode 100644
index 0000000..a9c6f19
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Environment.groovy
@@ -0,0 +1,30 @@
+package org.gmod.chado
+
+class Environment {
+
+    String uniquename
+    String description
+
+    static hasMany = [environmentCvterms                   : EnvironmentCvterm,
+                      phendescs                            : Phendesc,
+                      phenotypeComparisonsForEnvironment1Id: PhenotypeComparison,
+                      phenotypeComparisonsForEnvironment2Id: PhenotypeComparison,
+                      phenstatements                       : Phenstatement]
+
+    // TODO you have multiple hasMany references for class(es) [PhenotypeComparison]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    // TODO: double-chkeck
+    static mappedBy = [phenotypeComparisonsForEnvironment1Id: "environment1",
+                       phenotypeComparisonsForEnvironment2Id: "environment2"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "environment_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        uniquename unique: true
+        description nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/EnvironmentCvterm.groovy b/grails-app/domain/org/gmod/chado/EnvironmentCvterm.groovy
new file mode 100644
index 0000000..20cf1e0
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/EnvironmentCvterm.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class EnvironmentCvterm {
+
+    Environment environment
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm, Environment]
+
+    static mapping = {
+        datasource "chado"
+        id column: "environment_cvterm_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Expression.groovy b/grails-app/domain/org/gmod/chado/Expression.groovy
new file mode 100644
index 0000000..636411c
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Expression.groovy
@@ -0,0 +1,26 @@
+package org.gmod.chado
+
+class Expression {
+
+    String uniquename
+    String md5checksum
+    String description
+
+    static hasMany = [expressionCvterms : ExpressionCvterm,
+                      expressionImages  : ExpressionImage,
+                      expressionPubs    : ExpressionPub,
+                      expressionprops   : Expressionprop,
+                      featureExpressions: FeatureExpression]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expression_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        uniquename unique: true
+        md5checksum nullable: true, maxSize: 32
+        description nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/ExpressionCvterm.groovy b/grails-app/domain/org/gmod/chado/ExpressionCvterm.groovy
new file mode 100644
index 0000000..ece7a3a
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/ExpressionCvterm.groovy
@@ -0,0 +1,18 @@
+package org.gmod.chado
+
+class ExpressionCvterm {
+
+    Integer rank
+    Cvterm cvterm
+    Expression expression
+    Cvterm cvtermType
+
+    static hasMany = [expressionCvtermprops: ExpressionCvtermprop]
+    static belongsTo = [Cvterm, Expression]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expression_cvterm_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/ExpressionCvtermprop.groovy b/grails-app/domain/org/gmod/chado/ExpressionCvtermprop.groovy
new file mode 100644
index 0000000..96afc82
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/ExpressionCvtermprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class ExpressionCvtermprop {
+
+    String value
+    Integer rank
+    ExpressionCvterm expressionCvterm
+    Cvterm type
+
+    static belongsTo = [Cvterm, ExpressionCvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expression_cvtermprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "expression_cvterm_id"]
+        rank unique: ["type", "expressionCvterm"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/ExpressionImage.groovy b/grails-app/domain/org/gmod/chado/ExpressionImage.groovy
new file mode 100644
index 0000000..9af693a
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/ExpressionImage.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class ExpressionImage {
+
+    Expression expression
+    Eimage eimage
+
+    static belongsTo = [Eimage, Expression]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expression_image_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/ExpressionPub.groovy b/grails-app/domain/org/gmod/chado/ExpressionPub.groovy
new file mode 100644
index 0000000..3bee499
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/ExpressionPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class ExpressionPub {
+
+    Pub pub
+    Expression expression
+
+    static belongsTo = [Expression, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expression_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Expressionprop.groovy b/grails-app/domain/org/gmod/chado/Expressionprop.groovy
new file mode 100644
index 0000000..4a6c030
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Expressionprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class Expressionprop {
+
+    String value
+    Integer rank
+    Expression expression
+    Cvterm type
+
+    static belongsTo = [Cvterm, Expression]
+
+    static mapping = {
+        datasource "chado"
+        id column: "expressionprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "expression_id"]
+        rank unique: ["type", "expression"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Feature.groovy b/grails-app/domain/org/gmod/chado/Feature.groovy
new file mode 100644
index 0000000..9637738
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Feature.groovy
@@ -0,0 +1,81 @@
+package org.gmod.chado
+
+import java.sql.Timestamp
+
+class Feature {
+
+    String name
+    String uniquename
+    String residues
+    Integer seqlen
+    String md5checksum
+    Boolean isAnalysis
+    Boolean isObsolete
+    Date timeaccessioned
+    Date timelastmodified
+//    Serializable searchableName
+    Dbxref dbxref
+    Organism organism
+    Cvterm type
+
+    static hasMany = [
+//            analysisfeatures                : Analysisfeature,
+//	                  cellLineFeatures: CellLineFeature,
+//	                  elements: Element,
+                      featureCvterms                  : FeatureCvterm,
+                      featureDbxrefs                  : FeatureDbxref,
+                      featureExpressions              : FeatureExpression,
+                      featureGenotypesForChromosomeId : FeatureGenotype,
+                      featureGenotypesForFeatureId    : FeatureGenotype,
+                      featurePhenotypes               : FeaturePhenotype,
+                      featurePubs                     : FeaturePub,
+                      featureRelationshipsForObjectId : FeatureRelationship,
+                      featureRelationshipsForSubjectId: FeatureRelationship,
+                      featureSynonyms                 : FeatureSynonym,
+                      featurelocsForFeatureId         : Featureloc,
+                      featurelocsForSrcfeatureId      : Featureloc,
+                      featureposesForFeatureId        : Featurepos,
+                      featureposesForMapFeatureId     : Featurepos,
+                      featureprops                    : Featureprop,
+                      featurerangesForFeatureId       : Featurerange,
+                      featurerangesForLeftendfId      : Featurerange,
+                      featurerangesForLeftstartfId    : Featurerange,
+                      featurerangesForRightendfId     : Featurerange,
+                      featurerangesForRightstartfId   : Featurerange,
+//                      libraryFeatures                 : LibraryFeature,
+                      phylonodes                      : Phylonode
+//	                  studypropFeatures: StudypropFeature
+    ]
+    static belongsTo = [Cvterm, Dbxref, Organism]
+
+    // TODO you have multiple hasMany references for class(es) [FeatureGenotype, FeatureRelationship, Featureloc, Featurepos, Featurerange]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [featureGenotypesForChromosomeId : "chromosome",
+                       featureGenotypesForFeatureId    : "feature",
+                       featureRelationshipsForObjectId : "object",
+                       featureRelationshipsForSubjectId: "subject",
+                       featurelocsForFeatureId         : "feature",
+                       featurelocsForSrcfeatureId      : "srcfeature",
+                       featureposesForFeatureId        : "feature",
+                       featureposesForMapFeatureId     : "mapFeature",
+                       featurerangesForFeatureId       : "feature",
+                       featurerangesForLeftendfId      : "leftendf",
+                       featurerangesForLeftstartfId    : "leftstartf",
+                       featurerangesForRightendfId     : "rightendf",
+                       featurerangesForRightstartfId   : "rightstartf"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name nullable: true
+        residues nullable: true
+        seqlen nullable: true
+        md5checksum nullable: true, maxSize: 32
+//        searchableName nullable: true
+        dbxref nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureContains.groovy b/grails-app/domain/org/gmod/chado/FeatureContains.groovy
new file mode 100644
index 0000000..9b2607c
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureContains.groovy
@@ -0,0 +1,38 @@
+package org.gmod.chado
+
+import org.apache.commons.lang.builder.EqualsBuilder
+import org.apache.commons.lang.builder.HashCodeBuilder
+
+class FeatureContains implements Serializable {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    Integer subjectId
+    Integer objectId
+
+    int hashCode() {
+        def builder = new HashCodeBuilder()
+        builder.append subjectId
+        builder.append objectId
+        builder.toHashCode()
+    }
+
+    boolean equals(other) {
+        if (other == null) return false
+        def builder = new EqualsBuilder()
+        builder.append subjectId, other.subjectId
+        builder.append objectId, other.objectId
+        builder.isEquals()
+    }
+
+    static mapping = {
+        datasource "chado"
+        id composite: ["subjectId", "objectId"]
+        version false
+    }
+
+    static constraints = {
+        subjectId nullable: true
+        objectId nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureCvterm.groovy b/grails-app/domain/org/gmod/chado/FeatureCvterm.groovy
new file mode 100644
index 0000000..ba5674d
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureCvterm.groovy
@@ -0,0 +1,26 @@
+package org.gmod.chado
+
+class FeatureCvterm {
+
+    Boolean isNot
+    Integer rank
+    Pub pub
+    Feature feature
+    Cvterm cvterm
+
+    static hasMany = [featureCvtermDbxrefs: FeatureCvtermDbxref,
+                      featureCvtermPubs   : FeatureCvtermPub,
+                      featureCvtermprops  : FeatureCvtermprop]
+    static belongsTo = [Cvterm, Feature, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_cvterm_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["pub_id", "cvterm_id", "feature_id"]
+        rank unique: ["pub", "cvterm", "feature"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureCvtermDbxref.groovy b/grails-app/domain/org/gmod/chado/FeatureCvtermDbxref.groovy
new file mode 100644
index 0000000..8e8e033
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureCvtermDbxref.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeatureCvtermDbxref {
+
+    Dbxref dbxref
+    FeatureCvterm featureCvterm
+
+    static belongsTo = [Dbxref, FeatureCvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_cvterm_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureCvtermPub.groovy b/grails-app/domain/org/gmod/chado/FeatureCvtermPub.groovy
new file mode 100644
index 0000000..191628f
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureCvtermPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeatureCvtermPub {
+
+    Pub pub
+    FeatureCvterm featureCvterm
+
+    static belongsTo = [FeatureCvterm, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_cvterm_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureCvtermprop.groovy b/grails-app/domain/org/gmod/chado/FeatureCvtermprop.groovy
new file mode 100644
index 0000000..ddf2cac
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureCvtermprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class FeatureCvtermprop {
+
+    String value
+    Integer rank
+    FeatureCvterm featureCvterm
+    Cvterm type
+
+    static belongsTo = [Cvterm, FeatureCvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_cvtermprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "feature_cvterm_id"]
+        rank unique: ["type", "featureCvterm"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureDbxref.groovy b/grails-app/domain/org/gmod/chado/FeatureDbxref.groovy
new file mode 100644
index 0000000..8a078d8
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureDbxref.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class FeatureDbxref {
+
+    Boolean isCurrent
+    Feature feature
+    Dbxref dbxref
+
+    static belongsTo = [Dbxref, Feature]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureExpression.groovy b/grails-app/domain/org/gmod/chado/FeatureExpression.groovy
new file mode 100644
index 0000000..7771181
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureExpression.groovy
@@ -0,0 +1,17 @@
+package org.gmod.chado
+
+class FeatureExpression {
+
+    Pub pub
+    Feature feature
+    Expression expression
+
+    static hasMany = [featureExpressionprops: FeatureExpressionprop]
+    static belongsTo = [Expression, Feature, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_expression_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureExpressionprop.groovy b/grails-app/domain/org/gmod/chado/FeatureExpressionprop.groovy
new file mode 100644
index 0000000..42bec47
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureExpressionprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class FeatureExpressionprop {
+
+    String value
+    Integer rank
+    FeatureExpression featureExpression
+    Cvterm type
+
+    static belongsTo = [Cvterm, FeatureExpression]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_expressionprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "feature_expression_id"]
+        rank unique: ["type", "featureExpression"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureGenotype.groovy b/grails-app/domain/org/gmod/chado/FeatureGenotype.groovy
new file mode 100644
index 0000000..f9850cd
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureGenotype.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class FeatureGenotype {
+
+    Integer rank
+    Integer cgroup
+    Feature feature
+    Genotype genotype
+    Cvterm cvterm
+    Feature chromosome
+
+    static belongsTo = [Cvterm, Feature, Genotype]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_genotype_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		cgroup unique: ["rank", "chromosome_id", "cvterm_id", "genotype_id", "feature_id"]
+        cgroup unique: ["rank", "chromosome", "cvterm", "genotype", "feature"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureIntersection.groovy b/grails-app/domain/org/gmod/chado/FeatureIntersection.groovy
new file mode 100644
index 0000000..bbf6aed
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureIntersection.groovy
@@ -0,0 +1,58 @@
+package org.gmod.chado
+
+import org.apache.commons.lang.builder.EqualsBuilder
+import org.apache.commons.lang.builder.HashCodeBuilder
+
+class FeatureIntersection implements Serializable {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    Integer subjectId
+    Integer objectId
+    Integer srcfeatureId
+    Short subjectStrand
+    Short objectStrand
+    Integer fmin
+    Integer fmax
+
+    int hashCode() {
+        def builder = new HashCodeBuilder()
+        builder.append subjectId
+        builder.append objectId
+        builder.append srcfeatureId
+        builder.append subjectStrand
+        builder.append objectStrand
+        builder.append fmin
+        builder.append fmax
+        builder.toHashCode()
+    }
+
+    boolean equals(other) {
+        if (other == null) return false
+        def builder = new EqualsBuilder()
+        builder.append subjectId, other.subjectId
+        builder.append objectId, other.objectId
+        builder.append srcfeatureId, other.srcfeatureId
+        builder.append subjectStrand, other.subjectStrand
+        builder.append objectStrand, other.objectStrand
+        builder.append fmin, other.fmin
+        builder.append fmax, other.fmax
+        builder.isEquals()
+    }
+
+    static mapping = {
+        datasource "chado"
+        id composite: ["subjectId", "objectId", "srcfeatureId", "subjectStrand", "objectStrand", "fmin", "fmax"]
+        version false
+    }
+
+    static constraints = {
+        subjectId nullable: true
+        objectId nullable: true
+        srcfeatureId nullable: true
+        subjectStrand nullable: true
+        objectStrand nullable: true
+        fmin nullable: true
+        fmax nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturePhenotype.groovy b/grails-app/domain/org/gmod/chado/FeaturePhenotype.groovy
new file mode 100644
index 0000000..4f1141d
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturePhenotype.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeaturePhenotype {
+
+    Feature feature
+    Phenotype phenotype
+
+    static belongsTo = [Feature, Phenotype]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_phenotype_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturePub.groovy b/grails-app/domain/org/gmod/chado/FeaturePub.groovy
new file mode 100644
index 0000000..18e5c64
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturePub.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class FeaturePub {
+
+    Pub pub
+    Feature feature
+
+    static hasMany = [featurePubprops: FeaturePubprop]
+    static belongsTo = [Feature, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturePubprop.groovy b/grails-app/domain/org/gmod/chado/FeaturePubprop.groovy
new file mode 100644
index 0000000..e0abb19
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturePubprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class FeaturePubprop {
+
+    String value
+    Integer rank
+    FeaturePub featurePub
+    Cvterm type
+
+    static belongsTo = [Cvterm, FeaturePub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_pubprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "feature_pub_id"]
+        rank unique: ["type", "featurePub"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureRelationship.groovy b/grails-app/domain/org/gmod/chado/FeatureRelationship.groovy
new file mode 100644
index 0000000..5b25699
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureRelationship.groovy
@@ -0,0 +1,26 @@
+package org.gmod.chado
+
+class FeatureRelationship {
+
+    String value
+    Integer rank
+    Feature object
+    Feature subject
+    Cvterm type
+
+    static hasMany = [featureRelationshipPubs : FeatureRelationshipPub,
+                      featureRelationshipprops: FeatureRelationshipprop]
+    static belongsTo = [Cvterm, Feature]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_relationship_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "object_id", "subject_id"]
+        rank unique: ["type", "object", "subject"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureRelationshipPub.groovy b/grails-app/domain/org/gmod/chado/FeatureRelationshipPub.groovy
new file mode 100644
index 0000000..721e450
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureRelationshipPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeatureRelationshipPub {
+
+    Pub pub
+    FeatureRelationship featureRelationship
+
+    static belongsTo = [FeatureRelationship, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_relationship_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureRelationshipprop.groovy b/grails-app/domain/org/gmod/chado/FeatureRelationshipprop.groovy
new file mode 100644
index 0000000..f4646c4
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureRelationshipprop.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class FeatureRelationshipprop {
+
+    String value
+    Integer rank
+    FeatureRelationship featureRelationship
+    Cvterm type
+
+    static hasMany = [featureRelationshippropPubs: FeatureRelationshippropPub]
+    static belongsTo = [Cvterm, FeatureRelationship]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_relationshipprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "feature_relationship_id"]
+        rank unique: ["type", "featureRelationship"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureRelationshippropPub.groovy b/grails-app/domain/org/gmod/chado/FeatureRelationshippropPub.groovy
new file mode 100644
index 0000000..58b12ff
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureRelationshippropPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeatureRelationshippropPub {
+
+    Pub pub
+    FeatureRelationshipprop featureRelationshipprop
+
+    static belongsTo = [FeatureRelationshipprop, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_relationshipprop_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeatureSynonym.groovy b/grails-app/domain/org/gmod/chado/FeatureSynonym.groovy
new file mode 100644
index 0000000..550eec0
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeatureSynonym.groovy
@@ -0,0 +1,18 @@
+package org.gmod.chado
+
+class FeatureSynonym {
+
+    Boolean isCurrent
+    Boolean isInternal
+    Pub pub
+    Feature feature
+    Synonym synonym
+
+    static belongsTo = [Feature, Pub, Synonym]
+
+    static mapping = {
+        datasource "chado"
+        id column: "feature_synonym_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Featureloc.groovy b/grails-app/domain/org/gmod/chado/Featureloc.groovy
new file mode 100644
index 0000000..a0057d2
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Featureloc.groovy
@@ -0,0 +1,35 @@
+package org.gmod.chado
+
+class Featureloc {
+
+    Integer fmin
+    Boolean isFminPartial
+    Integer fmax
+    Boolean isFmaxPartial
+    Short strand
+    Integer phase
+    String residueInfo
+    Integer locgroup
+    Integer rank
+    Feature srcfeature
+    Feature feature
+
+    static hasMany = [featurelocPubs: FeaturelocPub]
+    static belongsTo = [Feature]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featureloc_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        fmin nullable: true
+        fmax nullable: true
+        strand nullable: true
+        phase nullable: true
+        residueInfo nullable: true
+//		rank unique: ["locgroup", "feature_id"]
+        rank unique: ["locgroup", "feature"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturelocPub.groovy b/grails-app/domain/org/gmod/chado/FeaturelocPub.groovy
new file mode 100644
index 0000000..4c36485
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturelocPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeaturelocPub {
+
+    Pub pub
+    Featureloc featureloc
+
+    static belongsTo = [Featureloc, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featureloc_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Featuremap.groovy b/grails-app/domain/org/gmod/chado/Featuremap.groovy
new file mode 100644
index 0000000..0ac316e
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Featuremap.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class Featuremap {
+
+    String name
+    String description
+    Cvterm unittype
+
+    static hasMany = [featuremapPubs: FeaturemapPub,
+                      featureposes  : Featurepos,
+                      featureranges : Featurerange]
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featuremap_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name nullable: true, unique: true
+        description nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturemapPub.groovy b/grails-app/domain/org/gmod/chado/FeaturemapPub.groovy
new file mode 100644
index 0000000..38ca785
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturemapPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeaturemapPub {
+
+    Pub pub
+    Featuremap featuremap
+
+    static belongsTo = [Featuremap, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featuremap_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Featurepos.groovy b/grails-app/domain/org/gmod/chado/Featurepos.groovy
new file mode 100644
index 0000000..fb354d5
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Featurepos.groovy
@@ -0,0 +1,21 @@
+package org.gmod.chado
+
+class Featurepos {
+
+    Double mappos
+    Feature feature
+    Featuremap featuremap
+    Feature mapFeature
+
+    static belongsTo = [Feature, Featuremap]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featurepos_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        mappos scale: 17
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Featureprop.groovy b/grails-app/domain/org/gmod/chado/Featureprop.groovy
new file mode 100644
index 0000000..0e49867
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Featureprop.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class Featureprop {
+
+    String value
+    Integer rank
+    Feature feature
+    Cvterm type
+
+    static hasMany = [featurepropPubs: FeaturepropPub]
+    static belongsTo = [Cvterm, Feature]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featureprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "feature_id"]
+        rank unique: ["type", "feature"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturepropPub.groovy b/grails-app/domain/org/gmod/chado/FeaturepropPub.groovy
new file mode 100644
index 0000000..d80d1f6
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturepropPub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class FeaturepropPub {
+
+    Pub pub
+    Featureprop featureprop
+
+    static belongsTo = [Featureprop, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featureprop_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Featurerange.groovy b/grails-app/domain/org/gmod/chado/Featurerange.groovy
new file mode 100644
index 0000000..0bf5861
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Featurerange.groovy
@@ -0,0 +1,26 @@
+package org.gmod.chado
+
+class Featurerange {
+
+    String rangestr
+    Feature feature
+    Featuremap featuremap
+    Feature rightendf
+    Feature leftstartf
+    Feature leftendf
+    Feature rightstartf
+
+    static belongsTo = [Feature, Featuremap]
+
+    static mapping = {
+        datasource "chado"
+        id column: "featurerange_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        rangestr nullable: true
+        leftendf nullable: true
+        rightstartf nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/FeaturesetMeets.groovy b/grails-app/domain/org/gmod/chado/FeaturesetMeets.groovy
new file mode 100644
index 0000000..0f6b03c
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/FeaturesetMeets.groovy
@@ -0,0 +1,38 @@
+package org.gmod.chado
+
+import org.apache.commons.lang.builder.EqualsBuilder
+import org.apache.commons.lang.builder.HashCodeBuilder
+
+class FeaturesetMeets implements Serializable {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    Integer subjectId
+    Integer objectId
+
+    int hashCode() {
+        def builder = new HashCodeBuilder()
+        builder.append subjectId
+        builder.append objectId
+        builder.toHashCode()
+    }
+
+    boolean equals(other) {
+        if (other == null) return false
+        def builder = new EqualsBuilder()
+        builder.append subjectId, other.subjectId
+        builder.append objectId, other.objectId
+        builder.isEquals()
+    }
+
+    static mapping = {
+        datasource "chado"
+        id composite: ["subjectId", "objectId"]
+        version false
+    }
+
+    static constraints = {
+        subjectId nullable: true
+        objectId nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Genotype.groovy b/grails-app/domain/org/gmod/chado/Genotype.groovy
new file mode 100644
index 0000000..57cbed1
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Genotype.groovy
@@ -0,0 +1,35 @@
+package org.gmod.chado
+
+class Genotype {
+
+    String name
+    String uniquename
+    String description
+
+    static hasMany = [featureGenotypes                  : FeatureGenotype,
+                      genotypeprops                     : Genotypeprop,
+//                      ndExperimentGenotypes             : NdExperimentGenotype,
+                      phendescs                         : Phendesc,
+                      phenotypeComparisonsForGenotype1Id: PhenotypeComparison,
+                      phenotypeComparisonsForGenotype2Id: PhenotypeComparison,
+                      phenstatements                    : Phenstatement
+//                      stockGenotypes                    : StockGenotype
+    ]
+
+    // TODO you have multiple hasMany references for class(es) [PhenotypeComparison]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [phenotypeComparisonsForGenotype1Id: "genotype1",
+                       phenotypeComparisonsForGenotype2Id: "genotype2"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "genotype_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name nullable: true
+        uniquename unique: true
+        description nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Genotypeprop.groovy b/grails-app/domain/org/gmod/chado/Genotypeprop.groovy
new file mode 100644
index 0000000..199cf96
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Genotypeprop.groovy
@@ -0,0 +1,24 @@
+package org.gmod.chado
+
+class Genotypeprop {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    String value
+    Integer rank
+    Genotype genotype
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm, Genotype]
+
+    static mapping = {
+        datasource "chado"
+        id column: "genotypeprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+        rank unique: ["cvterm", "genotype"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Organism.groovy b/grails-app/domain/org/gmod/chado/Organism.groovy
new file mode 100644
index 0000000..4febb90
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Organism.groovy
@@ -0,0 +1,36 @@
+package org.gmod.chado
+
+class Organism {
+
+    String abbreviation
+    String genus
+    String species
+    String commonName
+    String comment
+
+    static hasMany = [
+//            analysisOrganisms   : AnalysisOrganism,
+//	                  biomaterials: Biomaterial,
+//	                  cellLines: CellLine,
+                      features            : Feature,
+//                      libraries           : Library,
+                      organismDbxrefs     : OrganismDbxref,
+                      organismprops       : Organismprop,
+                      phenotypeComparisons: PhenotypeComparison,
+                      phylonodeOrganisms  : PhylonodeOrganism,
+//                      stocks              : Stock
+    ]
+
+    static mapping = {
+        datasource "chado"
+        id column: "organism_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        abbreviation nullable: true
+        species unique: ["genus"]
+        commonName nullable: true
+        comment nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/OrganismDbxref.groovy b/grails-app/domain/org/gmod/chado/OrganismDbxref.groovy
new file mode 100644
index 0000000..3b8d1f1
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/OrganismDbxref.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class OrganismDbxref {
+
+    Dbxref dbxref
+    Organism organism
+
+    static belongsTo = [Dbxref, Organism]
+
+    static mapping = {
+        datasource "chado"
+        id column: "organism_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/OrganismFeatureCount.groovy b/grails-app/domain/org/gmod/chado/OrganismFeatureCount.groovy
new file mode 100644
index 0000000..09cde44
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/OrganismFeatureCount.groovy
@@ -0,0 +1,58 @@
+package org.gmod.chado
+
+import org.apache.commons.lang.builder.EqualsBuilder
+import org.apache.commons.lang.builder.HashCodeBuilder
+
+class OrganismFeatureCount implements Serializable {
+    // Note: This domain class is not part of any Chado module
+    // Probably an artifact of the reverse engineering script.
+    // TODO: Remove if not needed.
+    Integer organismId
+    String genus
+    String species
+    String commonName
+    Integer numFeatures
+    Integer cvtermId
+    String featureType
+
+    int hashCode() {
+        def builder = new HashCodeBuilder()
+        builder.append organismId
+        builder.append genus
+        builder.append species
+        builder.append commonName
+        builder.append numFeatures
+        builder.append cvtermId
+        builder.append featureType
+        builder.toHashCode()
+    }
+
+    boolean equals(other) {
+        if (other == null) return false
+        def builder = new EqualsBuilder()
+        builder.append organismId, other.organismId
+        builder.append genus, other.genus
+        builder.append species, other.species
+        builder.append commonName, other.commonName
+        builder.append numFeatures, other.numFeatures
+        builder.append cvtermId, other.cvtermId
+        builder.append featureType, other.featureType
+        builder.isEquals()
+    }
+
+    static mapping = {
+        datasource "chado"
+        id composite: ["organismId", "genus", "species", "commonName", "numFeatures", "cvtermId", "featureType"]
+        version false
+    }
+
+    static constraints = {
+        organismId nullable: true
+        genus nullable: true
+        species nullable: true
+        commonName nullable: true
+        numFeatures nullable: true
+        cvtermId nullable: true
+        featureType nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Organismprop.groovy b/grails-app/domain/org/gmod/chado/Organismprop.groovy
new file mode 100644
index 0000000..c6e7e5e
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Organismprop.groovy
@@ -0,0 +1,23 @@
+package org.gmod.chado
+
+class Organismprop {
+
+    String value
+    Integer rank
+    Organism organism
+    Cvterm type
+
+    static belongsTo = [Cvterm, Organism]
+
+    static mapping = {
+        datasource "chado"
+        id column: "organismprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        value nullable: true
+//		rank unique: ["type_id", "organism_id"]
+        rank unique: ["type", "organism"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phendesc.groovy b/grails-app/domain/org/gmod/chado/Phendesc.groovy
new file mode 100644
index 0000000..548645e
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phendesc.groovy
@@ -0,0 +1,18 @@
+package org.gmod.chado
+
+class Phendesc {
+
+    String description
+    Pub pub
+    Environment environment
+    Genotype genotype
+    Cvterm type
+
+    static belongsTo = [Cvterm, Environment, Genotype, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phendesc_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phenotype.groovy b/grails-app/domain/org/gmod/chado/Phenotype.groovy
new file mode 100644
index 0000000..09b03f3
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phenotype.groovy
@@ -0,0 +1,41 @@
+package org.gmod.chado
+
+class Phenotype {
+
+    String uniquename
+    String name
+    String value
+    Cvterm observable
+    Cvterm attr
+    Cvterm cvalue
+    Cvterm assay
+
+    static hasMany = [featurePhenotypes                  : FeaturePhenotype,
+//                      ndExperimentPhenotypes             : NdExperimentPhenotype,
+                      phenotypeComparisonsForPhenotype1Id: PhenotypeComparison,
+                      phenotypeComparisonsForPhenotype2Id: PhenotypeComparison,
+                      phenotypeCvterms                   : PhenotypeCvterm,
+                      phenstatements                     : Phenstatement]
+    static belongsTo = [Cvterm]
+
+    // TODO you have multiple hasMany references for class(es) [PhenotypeComparison]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [phenotypeComparisonsForPhenotype1Id: "phenotype1",
+                       phenotypeComparisonsForPhenotype2Id: "phenotype2"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phenotype_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        uniquename unique: true
+        name nullable: true
+        value nullable: true
+        observable nullable: true
+        attr nullable: true
+        cvalue nullable: true
+        assay nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhenotypeComparison.groovy b/grails-app/domain/org/gmod/chado/PhenotypeComparison.groovy
new file mode 100644
index 0000000..d7c4ef3
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhenotypeComparison.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class PhenotypeComparison {
+
+    Pub pub
+    Environment environment1
+    Environment environment2
+    Organism organism
+    Phenotype phenotype1
+    Phenotype phenotype2
+    Genotype genotype1
+    Genotype genotype2
+
+    static hasMany = [phenotypeComparisonCvterms: PhenotypeComparisonCvterm]
+    static belongsTo = [Environment, Genotype, Organism, Phenotype, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phenotype_comparison_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhenotypeComparisonCvterm.groovy b/grails-app/domain/org/gmod/chado/PhenotypeComparisonCvterm.groovy
new file mode 100644
index 0000000..ae4ccbd
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhenotypeComparisonCvterm.groovy
@@ -0,0 +1,17 @@
+package org.gmod.chado
+
+class PhenotypeComparisonCvterm {
+
+    Integer rank
+    Pub pub
+    PhenotypeComparison phenotypeComparison
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm, PhenotypeComparison, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phenotype_comparison_cvterm_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhenotypeCvterm.groovy b/grails-app/domain/org/gmod/chado/PhenotypeCvterm.groovy
new file mode 100644
index 0000000..2ff8f12
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhenotypeCvterm.groovy
@@ -0,0 +1,21 @@
+package org.gmod.chado
+
+class PhenotypeCvterm {
+
+    Integer rank
+    Cvterm cvterm
+    Phenotype phenotype
+
+    static belongsTo = [Cvterm, Phenotype]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phenotype_cvterm_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["cvterm_id", "phenotype_id"]
+        rank unique: ["cvterm", "phenotype"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phenstatement.groovy b/grails-app/domain/org/gmod/chado/Phenstatement.groovy
new file mode 100644
index 0000000..6fe6506
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phenstatement.groovy
@@ -0,0 +1,18 @@
+package org.gmod.chado
+
+class Phenstatement {
+
+    Pub pub
+    Environment environment
+    Genotype genotype
+    Phenotype phenotype
+    Cvterm type
+
+    static belongsTo = [Cvterm, Environment, Genotype, Phenotype, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phenstatement_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phylonode.groovy b/grails-app/domain/org/gmod/chado/Phylonode.groovy
new file mode 100644
index 0000000..2bd6013
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phylonode.groovy
@@ -0,0 +1,42 @@
+package org.gmod.chado
+
+class Phylonode {
+
+    Integer leftIdx
+    Integer rightIdx
+    String label
+    Double distance
+    Feature feature
+    Phylotree phylotree
+    Phylonode parentPhylonode
+    Cvterm type
+
+    static hasMany = [phylonodeDbxrefs                  : PhylonodeDbxref,
+                      phylonodeOrganisms                : PhylonodeOrganism,
+                      phylonodePubs                     : PhylonodePub,
+                      phylonodeRelationshipsForObjectId : PhylonodeRelationship,
+                      phylonodeRelationshipsForSubjectId: PhylonodeRelationship,
+                      phylonodeprops                    : Phylonodeprop,
+                      phylonodes                        : Phylonode]
+    static belongsTo = [Cvterm, Feature, Phylotree, Phylonode]
+
+    // TODO you have multiple hasMany references for class(es) [PhylonodeRelationship]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [phylonodeRelationshipsForObjectId : "object",
+                       phylonodeRelationshipsForSubjectId: "subject"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonode_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		leftIdx unique: ["phylotree_id"]
+//		rightIdx unique: ["phylotree_id"]
+        leftIdx unique: ["phylotree"]
+        rightIdx unique: ["phylotree"]
+        label nullable: true
+        distance nullable: true, scale: 17
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhylonodeDbxref.groovy b/grails-app/domain/org/gmod/chado/PhylonodeDbxref.groovy
new file mode 100644
index 0000000..7645a44
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhylonodeDbxref.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class PhylonodeDbxref {
+
+    Dbxref dbxref
+    Phylonode phylonode
+
+    static belongsTo = [Dbxref, Phylonode]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonode_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhylonodeOrganism.groovy b/grails-app/domain/org/gmod/chado/PhylonodeOrganism.groovy
new file mode 100644
index 0000000..d99f746
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhylonodeOrganism.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class PhylonodeOrganism {
+
+    Organism organism
+    Phylonode phylonode
+
+    static belongsTo = [Organism, Phylonode]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonode_organism_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhylonodePub.groovy b/grails-app/domain/org/gmod/chado/PhylonodePub.groovy
new file mode 100644
index 0000000..98014da
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhylonodePub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class PhylonodePub {
+
+    Pub pub
+    Phylonode phylonode
+
+    static belongsTo = [Phylonode, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonode_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhylonodeRelationship.groovy b/grails-app/domain/org/gmod/chado/PhylonodeRelationship.groovy
new file mode 100644
index 0000000..adf4b6b
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhylonodeRelationship.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class PhylonodeRelationship {
+
+    Integer rank
+    Phylotree phylotree
+    Phylonode object
+    Phylonode subject
+    Cvterm type
+
+    static belongsTo = [Cvterm, Phylonode, Phylotree]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonode_relationship_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        rank nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phylonodeprop.groovy b/grails-app/domain/org/gmod/chado/Phylonodeprop.groovy
new file mode 100644
index 0000000..ceb8237
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phylonodeprop.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class Phylonodeprop {
+
+    String value
+    Integer rank
+    Phylonode phylonode
+    Cvterm type
+
+    static belongsTo = [Cvterm, Phylonode]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylonodeprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["value", "type_id", "phylonode_id"]
+        rank unique: ["value", "type", "phylonode"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Phylotree.groovy b/grails-app/domain/org/gmod/chado/Phylotree.groovy
new file mode 100644
index 0000000..2254aa0
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Phylotree.groovy
@@ -0,0 +1,27 @@
+package org.gmod.chado
+
+class Phylotree {
+
+    String name
+    String comment
+    Dbxref dbxref
+//    Analysis analysis
+    Cvterm type
+
+    static hasMany = [phylonodeRelationships: PhylonodeRelationship,
+                      phylonodes            : Phylonode,
+                      phylotreePubs         : PhylotreePub]
+//    static belongsTo = [Analysis, Cvterm, Dbxref]
+    static belongsTo = [Cvterm, Dbxref]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylotree_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        name nullable: true
+        comment nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PhylotreePub.groovy b/grails-app/domain/org/gmod/chado/PhylotreePub.groovy
new file mode 100644
index 0000000..27baeb7
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PhylotreePub.groovy
@@ -0,0 +1,15 @@
+package org.gmod.chado
+
+class PhylotreePub {
+
+    Pub pub
+    Phylotree phylotree
+
+    static belongsTo = [Phylotree, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "phylotree_pub_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Pub.groovy b/grails-app/domain/org/gmod/chado/Pub.groovy
new file mode 100644
index 0000000..58e6485
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Pub.groovy
@@ -0,0 +1,89 @@
+package org.gmod.chado
+
+class Pub {
+
+    String title
+    String volumetitle
+    String volume
+    String seriesName
+    String issue
+    String pyear
+    String pages
+    String miniref
+    String uniquename
+    Boolean isObsolete
+    String publisher
+    String pubplace
+    Cvterm type
+
+    static hasMany = [
+//			cellLineCvterms: CellLineCvterm,
+//	                  cellLineFeatures: CellLineFeature,
+//	                  cellLineLibraries: CellLineLibrary,
+//	                  cellLinePubs: CellLinePub,
+//	                  cellLineSynonyms: CellLineSynonym,
+//	                  cellLinepropPubs: CellLinepropPub,
+expressionPubs              : ExpressionPub,
+featureCvtermPubs           : FeatureCvtermPub,
+featureCvterms              : FeatureCvterm,
+featureExpressions          : FeatureExpression,
+featurePubs                 : FeaturePub,
+featureRelationshipPubs     : FeatureRelationshipPub,
+featureRelationshippropPubs : FeatureRelationshippropPub,
+featureSynonyms             : FeatureSynonym,
+featurelocPubs              : FeaturelocPub,
+featuremapPubs              : FeaturemapPub,
+featurepropPubs             : FeaturepropPub,
+//libraryCvterms              : LibraryCvterm,
+//libraryPubs                 : LibraryPub,
+//librarySynonyms             : LibrarySynonym,
+//librarypropPubs             : LibrarypropPub,
+//ndExperimentPubs            : NdExperimentPub,
+phendescs                   : Phendesc,
+phenotypeComparisonCvterms  : PhenotypeComparisonCvterm,
+phenotypeComparisons        : PhenotypeComparison,
+phenstatements              : Phenstatement,
+phylonodePubs               : PhylonodePub,
+phylotreePubs               : PhylotreePub,
+//projectPubs                 : ProjectPub,
+//	                  protocols: Protocol,
+pubDbxrefs                  : PubDbxref,
+pubRelationshipsForObjectId : PubRelationship,
+pubRelationshipsForSubjectId: PubRelationship,
+pubauthors                  : Pubauthor,
+pubprops                    : Pubprop,
+//stockCvterms                : StockCvterm,
+//stockPubs                   : StockPub,
+//stockRelationshipCvterms    : StockRelationshipCvterm,
+//stockRelationshipPubs       : StockRelationshipPub,
+//stockpropPubs               : StockpropPub
+//	                  studies: Study
+    ]
+    static belongsTo = [Cvterm]
+
+    // TODO you have multiple hasMany references for class(es) [PubRelationship]
+    //      so you'll need to disambiguate them with the 'mappedBy' property:
+    static mappedBy = [pubRelationshipsForObjectId : "object",
+                       pubRelationshipsForSubjectId: "subject"]
+
+    static mapping = {
+        datasource "chado"
+        id column: "pub_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+        title nullable: true
+        volumetitle nullable: true
+        volume nullable: true
+        seriesName nullable: true
+        issue nullable: true
+        pyear nullable: true
+        pages nullable: true
+        miniref nullable: true
+        uniquename unique: true
+        isObsolete nullable: true
+        publisher nullable: true
+        pubplace nullable: true
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PubDbxref.groovy b/grails-app/domain/org/gmod/chado/PubDbxref.groovy
new file mode 100644
index 0000000..e93f7ed
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PubDbxref.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class PubDbxref {
+
+    Boolean isCurrent
+    Pub pub
+    Dbxref dbxref
+
+    static belongsTo = [Dbxref, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "pub_dbxref_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/PubRelationship.groovy b/grails-app/domain/org/gmod/chado/PubRelationship.groovy
new file mode 100644
index 0000000..bec08d2
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/PubRelationship.groovy
@@ -0,0 +1,16 @@
+package org.gmod.chado
+
+class PubRelationship {
+
+    Pub object
+    Pub subject
+    Cvterm cvterm
+
+    static belongsTo = [Cvterm, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "pub_relationship_id", generator: "increment"
+        version false
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Pubauthor.groovy b/grails-app/domain/org/gmod/chado/Pubauthor.groovy
new file mode 100644
index 0000000..b064009
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Pubauthor.groovy
@@ -0,0 +1,28 @@
+package org.gmod.chado
+
+class Pubauthor {
+
+    Integer rank
+    Boolean editor
+    String surname
+    String givennames
+    String suffix
+    Pub pub
+
+    static belongsTo = [Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "pubauthor_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank unique: ["pub_id"]
+        rank unique: ["pub"]
+        editor nullable: true
+        surname maxSize: 100
+        givennames nullable: true, maxSize: 100
+        suffix nullable: true, maxSize: 100
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Pubprop.groovy b/grails-app/domain/org/gmod/chado/Pubprop.groovy
new file mode 100644
index 0000000..0552864
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Pubprop.groovy
@@ -0,0 +1,22 @@
+package org.gmod.chado
+
+class Pubprop {
+
+    String value
+    Integer rank
+    Pub pub
+    Cvterm type
+
+    static belongsTo = [Cvterm, Pub]
+
+    static mapping = {
+        datasource "chado"
+        id column: "pubprop_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//		rank nullable: true, unique: ["type_id", "pub_id"]
+        rank nullable: true, unique: ["type", "pub"]
+    }
+}
diff --git a/grails-app/domain/org/gmod/chado/Synonym.groovy b/grails-app/domain/org/gmod/chado/Synonym.groovy
new file mode 100644
index 0000000..6586241
--- /dev/null
+++ b/grails-app/domain/org/gmod/chado/Synonym.groovy
@@ -0,0 +1,26 @@
+package org.gmod.chado
+
+class Synonym {
+
+    String name
+    String synonymSgml
+//    Serializable searchableSynonymSgml
+    Cvterm type
+
+    static hasMany = [
+//			cellLineSynonyms: CellLineSynonym,
+featureSynonyms: FeatureSynonym
+//librarySynonyms: LibrarySynonym
+    ]
+    static belongsTo = [Cvterm]
+
+    static mapping = {
+        datasource "chado"
+        id column: "synonym_id", generator: "increment"
+        version false
+    }
+
+    static constraints = {
+//        searchableSynonymSgml nullable: true
+    }
+}
diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties
new file mode 100644
index 0000000..0c9d7ee
--- /dev/null
+++ b/grails-app/i18n/messages.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}]
+default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL
+default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number
+default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address
+default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}]
+default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}]
+default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}]
+default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}]
+default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}]
+default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}]
+default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation
+default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}]
+default.blank.message=Property [{0}] of class [{1}] cannot be blank
+default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}]
+default.null.message=Property [{0}] of class [{1}] cannot be null
+default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique
+
+default.paginate.prev=Previous
+default.paginate.next=Next
+default.boolean.true=True
+default.boolean.false=False
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} created
+default.updated.message={0} {1} updated
+default.deleted.message={0} {1} deleted
+default.not.deleted.message={0} {1} could not be deleted
+default.not.found.message={0} not found with id {1}
+default.optimistic.locking.failure=Another user has updated this {0} while you were editing
+
+default.home.label=Home
+default.list.label={0} List
+default.add.label=Add {0}
+default.new.label=New {0}
+default.create.label=Create {0}
+default.show.label=Show {0}
+default.edit.label=Edit {0}
+
+default.button.create.label=Create
+default.button.edit.label=Edit
+default.button.update.label=Update
+default.button.delete.label=Delete
+default.button.delete.confirm.message=Are you sure?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Property {0} must be a valid URL
+typeMismatch.java.net.URI=Property {0} must be a valid URI
+typeMismatch.java.util.Date=Property {0} must be a valid Date
+typeMismatch.java.lang.Double=Property {0} must be a valid number
+typeMismatch.java.lang.Integer=Property {0} must be a valid number
+typeMismatch.java.lang.Long=Property {0} must be a valid number
+typeMismatch.java.lang.Short=Property {0} must be a valid number
+typeMismatch.java.math.BigDecimal=Property {0} must be a valid number
+typeMismatch.java.math.BigInteger=Property {0} must be a valid number
diff --git a/grails-app/i18n/messages_cs_CZ.properties b/grails-app/i18n/messages_cs_CZ.properties
new file mode 100644
index 0000000..7345531
--- /dev/null
+++ b/grails-app/i18n/messages_cs_CZ.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] neodpovídá požadovanému vzoru [{3}]
+default.invalid.url.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní URL
+default.invalid.creditCard.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní číslo kreditní karty
+default.invalid.email.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není validní emailová adresa
+default.invalid.range.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není v povoleném rozmezí od [{3}] do [{4}]
+default.invalid.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není v povoleném rozmezí od [{3}] do [{4}]
+default.invalid.max.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] překračuje maximální povolenou hodnotu [{3}]
+default.invalid.min.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] je menší než minimální povolená hodnota [{3}]
+default.invalid.max.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] překračuje maximální velikost [{3}]
+default.invalid.min.size.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] je menší než minimální velikost [{3}]
+default.invalid.validator.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] neprošla validací
+default.not.inlist.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] není obsažena v seznamu [{3}]
+default.blank.message=Položka [{0}] třídy [{1}] nemůže být prázdná
+default.not.equal.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] nemůže být stejná jako [{3}]
+default.null.message=Položka [{0}] třídy [{1}] nemůže být prázdná
+default.not.unique.message=Položka [{0}] třídy [{1}] o hodnotě [{2}] musí být unikátní
+
+default.paginate.prev=Předcházející
+default.paginate.next=Následující
+default.boolean.true=Pravda
+default.boolean.false=Nepravda
+default.date.format=dd. MM. yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} vytvořeno
+default.updated.message={0} {1} aktualizováno
+default.deleted.message={0} {1} smazáno
+default.not.deleted.message={0} {1} nelze smazat
+default.not.found.message={0} nenalezen s id {1}
+default.optimistic.locking.failure=Jiný uživatel aktualizoval záznam {0}, právě když byl vámi editován
+
+default.home.label=Domů
+default.list.label={0} Seznam
+default.add.label=Přidat {0}
+default.new.label=Nový {0}
+default.create.label=Vytvořit {0}
+default.show.label=Ukázat {0}
+default.edit.label=Editovat {0}
+
+default.button.create.label=Vytvoř
+default.button.edit.label=Edituj
+default.button.update.label=Aktualizuj
+default.button.delete.label=Smaž
+default.button.delete.confirm.message=Jste si jistý?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Položka {0} musí být validní URL
+typeMismatch.java.net.URI=Položka {0} musí být validní URI
+typeMismatch.java.util.Date=Položka {0} musí být validní datum
+typeMismatch.java.lang.Double=Položka {0} musí být validní desetinné číslo
+typeMismatch.java.lang.Integer=Položka {0} musí být validní číslo
+typeMismatch.java.lang.Long=Položka {0} musí být validní číslo
+typeMismatch.java.lang.Short=Položka {0} musí být validní číslo
+typeMismatch.java.math.BigDecimal=Položka {0} musí být validní číslo
+typeMismatch.java.math.BigInteger=Položka {0} musí být validní číslo
diff --git a/grails-app/i18n/messages_da.properties b/grails-app/i18n/messages_da.properties
new file mode 100644
index 0000000..858b229
--- /dev/null
+++ b/grails-app/i18n/messages_da.properties
@@ -0,0 +1,56 @@
+default.doesnt.match.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke mønsteret [{3}]
+default.invalid.url.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig URL
+default.invalid.creditCard.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke et gyldigt kreditkortnummer
+default.invalid.email.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig e-mail adresse
+default.invalid.range.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for intervallet fra  [{3}] til [{4}]
+default.invalid.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for størrelsen fra [{3}] til [{4}]
+default.invalid.max.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale værdi [{3}]
+default.invalid.min.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale værdi [{3}]
+default.invalid.max.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale størrelse på [{3}]
+default.invalid.min.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale størrelse på [{3}]
+default.invalid.validator.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke den brugerdefinerede validering
+default.not.inlist.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] findes ikke i listen [{3}]
+default.blank.message=Feltet [{0}] i klassen [{1}] kan ikke være tom
+default.not.equal.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] må ikke være [{3}]
+default.null.message=Feltet [{0}] i klassen [{1}] kan ikke være null
+default.not.unique.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] skal være unik
+
+default.paginate.prev=Forrige
+default.paginate.next=Næste
+default.boolean.true=Sand
+default.boolean.false=Falsk
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} oprettet
+default.updated.message={0} {1} opdateret
+default.deleted.message={0} {1} slettet
+default.not.deleted.message={0} {1} kunne ikke slettes
+default.not.found.message={0} med id {1} er ikke fundet
+default.optimistic.locking.failure=En anden bruger har opdateret denne {0} imens du har lavet rettelser
+
+default.home.label=Hjem
+default.list.label={0} Liste
+default.add.label=Tilføj {0}
+default.new.label=Ny {0}
+default.create.label=Opret {0}
+default.show.label=Vis {0}
+default.edit.label=Ret {0}
+
+default.button.create.label=Opret
+default.button.edit.label=Ret
+default.button.update.label=Opdater
+default.button.delete.label=Slet
+default.button.delete.confirm.message=Er du sikker?
+
+# Databindingsfejl. Brug "typeMismatch.$className.$propertyName for at passe til en given klasse (f.eks typeMismatch.Book.author)
+typeMismatch.java.net.URL=Feltet {0} skal være en valid URL
+typeMismatch.java.net.URI=Feltet {0} skal være en valid URI
+typeMismatch.java.util.Date=Feltet {0} skal være en valid Dato
+typeMismatch.java.lang.Double=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Integer=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Long=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Short=Feltet {0} skal være et valid tal
+typeMismatch.java.math.BigDecimal=Feltet {0} skal være et valid tal
+typeMismatch.java.math.BigInteger=Feltet {0} skal være et valid tal
+
diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties
new file mode 100644
index 0000000..0f7bfe9
--- /dev/null
+++ b/grails-app/i18n/messages_de.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] entspricht nicht dem vorgegebenen Muster [{3}]
+default.invalid.url.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige URL
+default.invalid.creditCard.message=Das Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige Kreditkartennummer
+default.invalid.email.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige E-Mail Adresse
+default.invalid.range.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}]
+default.invalid.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}]
+default.invalid.max.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist größer als der Höchstwert von [{3}]
+default.invalid.min.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist kleiner als der Mindestwert von [{3}]
+default.invalid.max.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] übersteigt den Höchstwert von [{3}]
+default.invalid.min.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] unterschreitet den Mindestwert von [{3}]
+default.invalid.validator.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist ungültig
+default.not.inlist.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht in der Liste [{3}] enthalten.
+default.blank.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht leer sein
+default.not.equal.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nicht gleich [{3}] sein
+default.null.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht null sein
+default.not.unique.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nur einmal vorkommen
+
+default.paginate.prev=Vorherige
+default.paginate.next=Nächste
+default.boolean.true=Wahr
+default.boolean.false=Falsch
+default.date.format=dd.MM.yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} wurde angelegt
+default.updated.message={0} {1} wurde geändert
+default.deleted.message={0} {1} wurde gelöscht
+default.not.deleted.message={0} {1} konnte nicht gelöscht werden
+default.not.found.message={0} mit der id {1} wurde nicht gefunden
+default.optimistic.locking.failure=Ein anderer Benutzer hat das {0} Object geändert während Sie es bearbeitet haben
+
+default.home.label=Home
+default.list.label={0} Liste
+default.add.label={0} hinzufügen
+default.new.label={0} anlegen
+default.create.label={0} anlegen
+default.show.label={0} anzeigen
+default.edit.label={0} bearbeiten
+
+default.button.create.label=Anlegen
+default.button.edit.label=Bearbeiten
+default.button.update.label=Aktualisieren
+default.button.delete.label=Löschen
+default.button.delete.confirm.message=Sind Sie sicher?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Die Eigenschaft {0} muss eine gültige URL sein
+typeMismatch.java.net.URI=Die Eigenschaft {0} muss eine gültige URI sein
+typeMismatch.java.util.Date=Die Eigenschaft {0} muss ein gültiges Datum sein
+typeMismatch.java.lang.Double=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Integer=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Long=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Short=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.math.BigDecimal=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.math.BigInteger=Die Eigenschaft {0} muss eine gültige Zahl sein
diff --git a/grails-app/i18n/messages_es.properties b/grails-app/i18n/messages_es.properties
new file mode 100644
index 0000000..f0ede53
--- /dev/null
+++ b/grails-app/i18n/messages_es.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no corresponde al patrón [{3}]
+default.invalid.url.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una URL válida
+default.invalid.creditCard.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es un número de tarjeta de crédito válida
+default.invalid.email.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una dirección de correo electrónico válida
+default.invalid.range.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el rango válido de [{3}] a [{4}]
+default.invalid.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el tamaño válido de [{3}] a [{4}]
+default.invalid.max.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el valor máximo [{3}]
+default.invalid.min.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menos que el valor mínimo [{3}]
+default.invalid.max.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el tamaño máximo de [{3}]
+default.invalid.min.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menor que el tamaño mínimo de [{3}]
+default.invalid.validator.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es válido
+default.not.inlist.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no esta contenido dentro de la lista [{3}]
+default.blank.message=La propiedad [{0}] de la clase [{1}] no puede ser vacía
+default.not.equal.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no puede igualar a [{3}]
+default.null.message=La propiedad [{0}] de la clase [{1}] no puede ser nulo
+default.not.unique.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] debe ser única
+
+default.paginate.prev=Anterior
+default.paginate.next=Siguiente
+default.boolean.true=Verdadero
+default.boolean.false=Falso
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} creado
+default.updated.message={0} {1} actualizado
+default.deleted.message={0} {1} eliminado
+default.not.deleted.message={0} {1} no puede eliminarse
+default.not.found.message=No se encuentra {0} con id {1}
+default.optimistic.locking.failure=Mientras usted editaba, otro usuario ha actualizado su {0}
+
+default.home.label=Principal
+default.list.label={0} Lista
+default.add.label=Agregar {0}
+default.new.label=Nuevo {0}
+default.create.label=Crear {0}
+default.show.label=Mostrar {0}
+default.edit.label=Editar {0}
+
+default.button.create.label=Crear
+default.button.edit.label=Editar
+default.button.update.label=Actualizar
+default.button.delete.label=Eliminar
+default.button.delete.confirm.message=¿Está usted seguro?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=La propiedad {0} debe ser una URL válida
+typeMismatch.java.net.URI=La propiedad {0} debe ser una URI válida
+typeMismatch.java.util.Date=La propiedad {0} debe ser una fecha válida
+typeMismatch.java.lang.Double=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Integer=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Long=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Short=La propiedad {0} debe ser un número válido
+typeMismatch.java.math.BigDecimal=La propiedad {0} debe ser un número válido
+typeMismatch.java.math.BigInteger=La propiedad {0} debe ser un número válido
\ No newline at end of file
diff --git a/grails-app/i18n/messages_fr.properties b/grails-app/i18n/messages_fr.properties
new file mode 100644
index 0000000..5572164
--- /dev/null
+++ b/grails-app/i18n/messages_fr.properties
@@ -0,0 +1,19 @@
+default.doesnt.match.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne correspond pas au pattern [{3}]
+default.invalid.url.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une URL valide
+default.invalid.creditCard.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte de crédit valide
+default.invalid.email.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide
+default.invalid.range.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}]
+default.invalid.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}]
+default.invalid.max.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}]
+default.invalid.min.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}]
+default.invalid.max.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}]
+default.invalid.min.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}]
+default.invalid.validator.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas valide
+default.not.inlist.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne fait pas partie de la liste [{3}]
+default.blank.message=La propriété [{0}] de la classe [{1}] ne peut pas être vide
+default.not.equal.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne peut pas être égale à [{3}]
+default.null.message=La propriété [{0}] de la classe [{1}] ne peut pas être nulle
+default.not.unique.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] doit être unique
+
+default.paginate.prev=Précédent
+default.paginate.next=Suivant
diff --git a/grails-app/i18n/messages_it.properties b/grails-app/i18n/messages_it.properties
new file mode 100644
index 0000000..a90f1c7
--- /dev/null
+++ b/grails-app/i18n/messages_it.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non corrisponde al pattern [{3}]
+default.invalid.url.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un URL valido
+default.invalid.creditCard.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un numero di carta di credito valido
+default.invalid.email.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un indirizzo email valido
+default.invalid.range.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo valido da [{3}] a [{4}]
+default.invalid.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo di dimensioni valide da [{3}] a [{4}]
+default.invalid.max.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}]
+default.invalid.min.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}]
+default.invalid.max.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}]
+default.invalid.min.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}]
+default.invalid.validator.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è valida
+default.not.inlist.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è contenuta nella lista [{3}]
+default.blank.message=La proprietà [{0}] della classe [{1}] non può essere vuota
+default.not.equal.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non può essere uguale a [{3}]
+default.null.message=La proprietà [{0}] della classe [{1}] non può essere null
+default.not.unique.message=La proprietà [{0}] della classe [{1}] con valore [{2}] deve essere unica
+
+default.paginate.prev=Precedente
+default.paginate.next=Successivo
+default.boolean.true=Vero
+default.boolean.false=Falso
+default.date.format=dd/MM/yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} creato
+default.updated.message={0} {1} aggiornato
+default.deleted.message={0} {1} eliminato
+default.not.deleted.message={0} {1} non può essere eliminato
+default.not.found.message={0} non trovato con id {1}
+default.optimistic.locking.failure=Un altro utente ha aggiornato questo {0} mentre si era in modifica
+
+default.home.label=Home
+default.list.label={0} Elenco
+default.add.label=Aggiungi {0}
+default.new.label=Nuovo {0}
+default.create.label=Crea {0}
+default.show.label=Mostra {0}
+default.edit.label=Modifica {0}
+
+default.button.create.label=Crea
+default.button.edit.label=Modifica
+default.button.update.label=Aggiorna
+default.button.delete.label=Elimina
+default.button.delete.confirm.message=Si è sicuri?
+
+# Data binding errors. Usa "typeMismatch.$className.$propertyName per la personalizzazione (es typeMismatch.Book.author)
+typeMismatch.java.net.URL=La proprietà {0} deve essere un URL valido
+typeMismatch.java.net.URI=La proprietà {0} deve essere un URI valido
+typeMismatch.java.util.Date=La proprietà {0} deve essere una data valida
+typeMismatch.java.lang.Double=La proprietà {0} deve essere un numero valido
+typeMismatch.java.lang.Integer=La proprietà {0} deve essere un numero valido
+typeMismatch.java.lang.Long=La proprietà {0} deve essere un numero valido
+typeMismatch.java.lang.Short=La proprietà {0} deve essere un numero valido
+typeMismatch.java.math.BigDecimal=La proprietà {0} deve essere un numero valido
+typeMismatch.java.math.BigInteger=La proprietà {0} deve essere un numero valido
diff --git a/grails-app/i18n/messages_ja.properties b/grails-app/i18n/messages_ja.properties
new file mode 100644
index 0000000..f716d75
--- /dev/null
+++ b/grails-app/i18n/messages_ja.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]パターンと一致していません。
+default.invalid.url.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なURLではありません。
+default.invalid.creditCard.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なクレジットカード番号ではありません。
+default.invalid.email.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、有効なメールアドレスではありません。
+default.invalid.range.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]範囲内を指定してください。
+default.invalid.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]以内を指定してください。
+default.invalid.max.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。
+default.invalid.min.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。
+default.invalid.max.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。
+default.invalid.min.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。
+default.invalid.validator.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、カスタムバリデーションを通過できません。
+default.not.inlist.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]リスト内に存在しません。
+default.blank.message=[{1}]クラスのプロパティ[{0}]の空白は許可されません。
+default.not.equal.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]と同等ではありません。
+default.null.message=[{1}]クラスのプロパティ[{0}]にnullは許可されません。
+default.not.unique.message=クラス[{1}]プロパティ[{0}]の値[{2}]は既に使用されています。
+
+default.paginate.prev=戻る
+default.paginate.next=次へ
+default.boolean.true=はい
+default.boolean.false=いいえ
+default.date.format=yyyy/MM/dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0}(id:{1})を作成しました。
+default.updated.message={0}(id:{1})を更新しました。
+default.deleted.message={0}(id:{1})を削除しました。
+default.not.deleted.message={0}(id:{1})は削除できませんでした。
+default.not.found.message={0}(id:{1})は見つかりませんでした。
+default.optimistic.locking.failure=この{0}は編集中に他のユーザによって先に更新されています。
+
+default.home.label=ホーム
+default.list.label={0}リスト
+default.add.label={0}を追加
+default.new.label={0}を新規作成
+default.create.label={0}を作成
+default.show.label={0}詳細
+default.edit.label={0}を編集
+
+default.button.create.label=作成
+default.button.edit.label=編集
+default.button.update.label=更新
+default.button.delete.label=削除
+default.button.delete.confirm.message=本当に削除してよろしいですか?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL={0}は有効なURLでなければなりません。
+typeMismatch.java.net.URI={0}は有効なURIでなければなりません。
+typeMismatch.java.util.Date={0}は有効な日付でなければなりません。
+typeMismatch.java.lang.Double={0}は有効な数値でなければなりません。
+typeMismatch.java.lang.Integer={0}は有効な数値でなければなりません。
+typeMismatch.java.lang.Long={0}は有効な数値でなければなりません。
+typeMismatch.java.lang.Short={0}は有効な数値でなければなりません。
+typeMismatch.java.math.BigDecimal={0}は有効な数値でなければなりません。
+typeMismatch.java.math.BigInteger={0}は有効な数値でなければなりません。
diff --git a/grails-app/i18n/messages_nb.properties b/grails-app/i18n/messages_nb.properties
new file mode 100644
index 0000000..47a8a1a
--- /dev/null
+++ b/grails-app/i18n/messages_nb.properties
@@ -0,0 +1,56 @@
+default.doesnt.match.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overholder ikke mønsteret [{3}]
+default.invalid.url.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke en gyldig URL
+default.invalid.creditCard.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke et gyldig kredittkortnummer
+default.invalid.email.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke en gyldig epostadresse
+default.invalid.range.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke innenfor intervallet [{3}] til [{4}]
+default.invalid.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er ikke innenfor intervallet [{3}] til [{4}]
+default.invalid.max.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overstiger maksimumsverdien på [{3}]
+default.invalid.min.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er under minimumsverdien på [{3}]
+default.invalid.max.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overstiger maksimumslengden på [{3}]
+default.invalid.min.size.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] er kortere enn minimumslengden på [{3}]
+default.invalid.validator.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] overholder ikke den brukerdefinerte valideringen
+default.not.inlist.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] finnes ikke i listen [{3}]
+default.blank.message=Feltet [{0}] i klassen [{1}] kan ikke være tom
+default.not.equal.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] kan ikke være [{3}]
+default.null.message=Feltet [{0}] i klassen [{1}] kan ikke være null
+default.not.unique.message=Feltet [{0}] i klassen [{1}] med verdien [{2}] må være unik
+
+default.paginate.prev=Forrige
+default.paginate.next=Neste
+default.boolean.true=Ja
+default.boolean.false=Nei
+default.date.format=dd.MM.yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} opprettet
+default.updated.message={0} {1} oppdatert
+default.deleted.message={0} {1} slettet
+default.not.deleted.message={0} {1} kunne ikke slettes
+default.not.found.message={0} med id {1} ble ikke funnet
+default.optimistic.locking.failure=En annen bruker har oppdatert denne {0} mens du redigerte
+
+default.home.label=Hjem
+default.list.label={0}liste
+default.add.label=Legg til {0}
+default.new.label=Ny {0}
+default.create.label=Opprett {0}
+default.show.label=Vis {0}
+default.edit.label=Endre {0}
+
+default.button.create.label=Opprett
+default.button.edit.label=Endre
+default.button.update.label=Oppdater
+default.button.delete.label=Slett
+default.button.delete.confirm.message=Er du sikker?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Feltet {0} må være en gyldig URL
+typeMismatch.java.net.URI=Feltet {0} må være en gyldig URI
+typeMismatch.java.util.Date=Feltet {0} må være en gyldig dato
+typeMismatch.java.lang.Double=Feltet {0} må være et gyldig tall
+typeMismatch.java.lang.Integer=Feltet {0} må være et gyldig heltall
+typeMismatch.java.lang.Long=Feltet {0} må være et gyldig heltall
+typeMismatch.java.lang.Short=Feltet {0} må være et gyldig heltall
+typeMismatch.java.math.BigDecimal=Feltet {0} må være et gyldig tall
+typeMismatch.java.math.BigInteger=Feltet {0} må være et gyldig heltall
+
diff --git a/grails-app/i18n/messages_nl.properties b/grails-app/i18n/messages_nl.properties
new file mode 100644
index 0000000..cd5cc94
--- /dev/null
+++ b/grails-app/i18n/messages_nl.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet overeen met het vereiste patroon [{3}]
+default.invalid.url.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldige URL
+default.invalid.creditCard.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig credit card nummer
+default.invalid.email.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig e-mailadres
+default.invalid.range.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige waardenreeks van [{3}] tot [{4}]
+default.invalid.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige grootte van [{3}] tot [{4}]
+default.invalid.max.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumwaarde [{3}]
+default.invalid.min.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan de minimumwaarde [{3}]
+default.invalid.max.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumgrootte van [{3}]
+default.invalid.min.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan minimumgrootte van [{3}]
+default.invalid.validator.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is niet geldig
+default.not.inlist.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet voor in de lijst [{3}]
+default.blank.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn
+default.not.equal.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] mag niet gelijk zijn aan [{3}]
+default.null.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn
+default.not.unique.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] moet uniek zijn
+
+default.paginate.prev=Vorige
+default.paginate.next=Volgende
+default.boolean.true=Ja
+default.boolean.false=Nee
+default.date.format=dd-MM-yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} ingevoerd
+default.updated.message={0} {1} gewijzigd
+default.deleted.message={0} {1} verwijderd
+default.not.deleted.message={0} {1} kon niet worden verwijderd
+default.not.found.message={0} met id {1} kon niet worden gevonden
+default.optimistic.locking.failure=Een andere gebruiker heeft deze {0} al gewijzigd
+
+default.home.label=Home
+default.list.label={0} Overzicht
+default.add.label=Toevoegen {0}
+default.new.label=Invoeren {0}
+default.create.label=Invoeren {0}
+default.show.label=Details {0}
+default.edit.label=Wijzigen {0}
+
+default.button.create.label=Invoeren
+default.button.edit.label=Wijzigen
+default.button.update.label=Opslaan
+default.button.delete.label=Verwijderen
+default.button.delete.confirm.message=Weet je het zeker?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Attribuut {0} is geen geldige URL
+typeMismatch.java.net.URI=Attribuut {0} is geen geldige URI
+typeMismatch.java.util.Date=Attribuut {0} is geen geldige datum
+typeMismatch.java.lang.Double=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Integer=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Long=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Short=Attribuut {0} is geen geldig nummer
+typeMismatch.java.math.BigDecimal=Attribuut {0} is geen geldig nummer
+typeMismatch.java.math.BigInteger=Attribuut {0} is geen geldig nummer
diff --git a/grails-app/i18n/messages_pl.properties b/grails-app/i18n/messages_pl.properties
new file mode 100644
index 0000000..959296c
--- /dev/null
+++ b/grails-app/i18n/messages_pl.properties
@@ -0,0 +1,59 @@
+#
+# Translated by Matthias Hryniszak - padcom at gmail.com
+#
+
+default.doesnt.match.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie pasuje do wymaganego wzorca [{3}]
+default.invalid.url.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest niepoprawnym adresem URL
+default.invalid.creditCard.message=Właściwość [{0}] klasy [{1}] with value [{2}] nie jest poprawnym numerem karty kredytowej
+default.invalid.email.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie jest poprawnym adresem e-mail
+default.invalid.range.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się zakładanym zakresie od [{3}] do [{4}]
+default.invalid.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się w zakładanym zakresie rozmiarów od [{3}] do [{4}]
+default.invalid.max.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] przekracza maksymalną wartość [{3}]
+default.invalid.min.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest mniejsza niż minimalna wartość [{3}]
+default.invalid.max.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] przekracza maksymalny rozmiar [{3}]
+default.invalid.min.size.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] jest mniejsza niż minimalny rozmiar [{3}]
+default.invalid.validator.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie spełnia założonych niestandardowych warunków
+default.not.inlist.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie zawiera się w liście [{3}]
+default.blank.message=Właściwość [{0}] klasy [{1}] nie może być pusta
+default.not.equal.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] nie może równać się [{3}]
+default.null.message=Właściwość [{0}] klasy [{1}] nie może być null
+default.not.unique.message=Właściwość [{0}] klasy [{1}] o wartości [{2}] musi być unikalna
+
+default.paginate.prev=Poprzedni
+default.paginate.next=Następny
+default.boolean.true=Prawda
+default.boolean.false=Fałsz
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message=Utworzono {0} {1}
+default.updated.message=Zaktualizowano {0} {1}
+default.deleted.message=Usunięto {0} {1}
+default.not.deleted.message={0} {1} nie mógł zostać usunięty
+default.not.found.message=Nie znaleziono {0} o id {1}
+default.optimistic.locking.failure=Inny użytkownik zaktualizował ten obiekt {0} w trakcie twoich zmian
+
+default.home.label=Strona domowa
+default.list.label=Lista {0}
+default.add.label=Dodaj {0}
+default.new.label=Utwórz {0}
+default.create.label=Utwórz {0}
+default.show.label=Pokaż {0}
+default.edit.label=Edytuj {0}
+
+default.button.create.label=Utwórz
+default.button.edit.label=Edytuj
+default.button.update.label=Zaktualizuj
+default.button.delete.label=Usuń
+default.button.delete.confirm.message=Czy jesteś pewien?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Właściwość {0} musi być poprawnym adresem URL
+typeMismatch.java.net.URI=Właściwość {0} musi być poprawnym adresem URI
+typeMismatch.java.util.Date=Właściwość {0} musi być poprawną datą
+typeMismatch.java.lang.Double=Właściwość {0} musi być poprawnyą liczbą
+typeMismatch.java.lang.Integer=Właściwość {0} musi być poprawnyą liczbą
+typeMismatch.java.lang.Long=Właściwość {0} musi być poprawnyą liczbą
+typeMismatch.java.lang.Short=Właściwość {0} musi być poprawnyą liczbą
+typeMismatch.java.math.BigDecimal=Właściwość {0} musi być poprawnyą liczbą
+typeMismatch.java.math.BigInteger=Właściwość {0} musi być poprawnyą liczbą
diff --git a/grails-app/i18n/messages_pt_BR.properties b/grails-app/i18n/messages_pt_BR.properties
new file mode 100644
index 0000000..0c368f2
--- /dev/null
+++ b/grails-app/i18n/messages_pt_BR.properties
@@ -0,0 +1,59 @@
+#
+# Translated by Lucas Teixeira - lucastex at gmail.com
+#
+
+default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atende ao padrão definido [{3}]
+default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é uma URL válida
+default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito
+default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido.
+default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está entre a faixa de valores válida de [{3}] até [{4}]
+default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está na faixa de tamanho válida de [{3}] até [{4}]
+default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapass o valor máximo [{3}]
+default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}]
+default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}]
+default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}]
+default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação
+default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um valor dentre os permitidos na lista [{3}]
+default.blank.message=O campo [{0}] da classe [{1}] não pode ficar em branco
+default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}]
+default.null.message=O campo [{0}] da classe [{1}] não pode ser vazia
+default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único
+
+default.paginate.prev=Anterior
+default.paginate.next=Próximo
+default.boolean.true=Sim
+default.boolean.false=Não
+default.date.format=dd/MM/yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} criado
+default.updated.message={0} {1} atualizado
+default.deleted.message={0} {1} removido
+default.not.deleted.message={0} {1} não pode ser removido
+default.not.found.message={0} não foi encontrado com id {1}
+default.optimistic.locking.failure=Outro usuário atualizou este [{0}] enquanto você tentou salvá-lo
+
+default.home.label=Principal
+default.list.label={0} Listagem
+default.add.label=Adicionar {0}
+default.new.label=Novo {0}
+default.create.label=Criar {0}
+default.show.label=Ver {0}
+default.edit.label=Editar {0}
+
+default.button.create.label=Criar
+default.button.edit.label=Editar
+default.button.update.label=Alterar
+default.button.delete.label=Remover
+default.button.delete.confirm.message=Tem certeza?
+
+# Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para customizar (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=O campo {0} deve ser uma URL válida.
+typeMismatch.java.net.URI=O campo {0} deve ser uma URI válida.
+typeMismatch.java.util.Date=O campo {0} deve ser uma data válida
+typeMismatch.java.lang.Double=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Long=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Short=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido.
\ No newline at end of file
diff --git a/grails-app/i18n/messages_pt_PT.properties b/grails-app/i18n/messages_pt_PT.properties
new file mode 100644
index 0000000..a386070
--- /dev/null
+++ b/grails-app/i18n/messages_pt_PT.properties
@@ -0,0 +1,34 @@
+#
+# translation by miguel.ping at gmail.com, based on pt_BR translation by Lucas Teixeira - lucastex at gmail.com
+#
+
+default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não corresponde ao padrão definido [{3}]
+default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um URL válido
+default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito
+default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido.
+default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está dentro dos limites de valores válidos de [{3}] a [{4}]
+default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] está fora dos limites de tamanho válido de [{3}] a [{4}]
+default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o valor máximo [{3}]
+default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}]
+default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}]
+default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}]
+default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação
+default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não se encontra nos valores permitidos da lista [{3}]
+default.blank.message=O campo [{0}] da classe [{1}] não pode ser vazio
+default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}]
+default.null.message=O campo [{0}] da classe [{1}] não pode ser vazio
+default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único
+
+default.paginate.prev=Anterior
+default.paginate.next=Próximo
+
+# Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para personalizar(eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=O campo {0} deve ser um URL válido.
+typeMismatch.java.net.URI=O campo {0} deve ser um URI válido.
+typeMismatch.java.util.Date=O campo {0} deve ser uma data válida
+typeMismatch.java.lang.Double=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Long=O campo {0} deve ser um número valido.
+typeMismatch.java.lang.Short=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido.
diff --git a/grails-app/i18n/messages_ru.properties b/grails-app/i18n/messages_ru.properties
new file mode 100644
index 0000000..53a4bdc
--- /dev/null
+++ b/grails-app/i18n/messages_ru.properties
@@ -0,0 +1,31 @@
+default.doesnt.match.message=Значение [{2}] поля [{0}] класса [{1}] не соответствует образцу [{3}]
+default.invalid.url.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым URL-адресом
+default.invalid.creditCard.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым номером кредитной карты
+default.invalid.email.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым e-mail адресом
+default.invalid.range.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в допустимый интервал от [{3}] до [{4}]
+default.invalid.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) не попадает в допустимый интервал от [{3}] до [{4}]
+default.invalid.max.message=Значение [{2}] поля [{0}] класса [{1}] больше чем максимально допустимое значение [{3}]
+default.invalid.min.message=Значение [{2}] поля [{0}] класса [{1}] меньше чем минимально допустимое значение [{3}]
+default.invalid.max.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) больше чем максимально допустимый размер [{3}]
+default.invalid.min.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) меньше чем минимально допустимый размер [{3}]
+default.invalid.validator.message=Значение [{2}] поля [{0}] класса [{1}] не допустимо
+default.not.inlist.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в список допустимых значений [{3}]
+default.blank.message=Поле [{0}] класса [{1}] не может быть пустым
+default.not.equal.message=Значение [{2}] поля [{0}] класса [{1}] не может быть равно [{3}]
+default.null.message=Поле [{0}] класса [{1}] не может иметь значение null
+default.not.unique.message=Значение [{2}] поля [{0}] класса [{1}] должно быть уникальным
+
+default.paginate.prev=Предыдушая страница
+default.paginate.next=Следующая страница
+
+# Ошибки при присвоении данных. Для точной настройки для полей классов используйте
+# формат "typeMismatch.$className.$propertyName" (например, typeMismatch.Book.author)
+typeMismatch.java.net.URL=Значение поля {0} не является допустимым URL
+typeMismatch.java.net.URI=Значение поля {0} не является допустимым URI
+typeMismatch.java.util.Date=Значение поля {0} не является допустимой датой
+typeMismatch.java.lang.Double=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Integer=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Long=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Short=Значение поля {0} не является допустимым числом
+typeMismatch.java.math.BigDecimal=Значение поля {0} не является допустимым числом
+typeMismatch.java.math.BigInteger=Значение поля {0} не является допустимым числом
diff --git a/grails-app/i18n/messages_sv.properties b/grails-app/i18n/messages_sv.properties
new file mode 100644
index 0000000..61899d7
--- /dev/null
+++ b/grails-app/i18n/messages_sv.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Attributet [{0}] för klassen [{1}] med värde [{2}] matchar inte mot uttrycket [{3}]
+default.invalid.url.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte en giltig URL
+default.invalid.creditCard.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte ett giltigt kreditkortsnummer
+default.invalid.email.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte en giltig e-postadress
+default.invalid.range.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte inom intervallet [{3}] till [{4}]
+default.invalid.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] har en storlek som inte är inom [{3}] till [{4}]
+default.invalid.max.message=Attributet [{0}] för klassen [{1}] med värde [{2}] överskrider maxvärdet [{3}]
+default.invalid.min.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är mindre än minimivärdet [{3}]
+default.invalid.max.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] överskrider maxstorleken [{3}]
+default.invalid.min.size.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är mindre än minimistorleken [{3}]
+default.invalid.validator.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte giltigt enligt anpassad regel
+default.not.inlist.message=Attributet [{0}] för klassen [{1}] med värde [{2}] är inte giltigt, måste vara ett av [{3}]
+default.blank.message=Attributet [{0}] för klassen [{1}] får inte vara tomt
+default.not.equal.message=Attributet [{0}] för klassen [{1}] med värde [{2}] får inte vara lika med [{3}]
+default.null.message=Attributet [{0}] för klassen [{1}] får inte vara tomt
+default.not.unique.message=Attributet [{0}] för klassen [{1}] med värde [{2}] måste vara unikt
+
+default.paginate.prev=Föregående
+default.paginate.next=Nästa
+default.boolean.true=Sant
+default.boolean.false=Falskt
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} skapades
+default.updated.message={0} {1} uppdaterades
+default.deleted.message={0} {1} borttagen
+default.not.deleted.message={0} {1} kunde inte tas bort
+default.not.found.message={0} med id {1} kunde inte hittas
+default.optimistic.locking.failure=En annan användare har uppdaterat det här {0} objektet medan du redigerade det
+
+default.home.label=Hem
+default.list.label= {0} - Lista
+default.add.label=Lägg till {0}
+default.new.label=Skapa {0}
+default.create.label=Skapa {0}
+default.show.label=Visa {0}
+default.edit.label=Ändra {0}
+
+default.button.create.label=Skapa
+default.button.edit.label=Ändra
+default.button.update.label=Uppdatera
+default.button.delete.label=Ta bort
+default.button.delete.confirm.message=Är du säker?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Värdet för {0} måste vara en giltig URL
+typeMismatch.java.net.URI=Värdet för {0} måste vara en giltig URI
+typeMismatch.java.util.Date=Värdet {0} måste vara ett giltigt datum
+typeMismatch.java.lang.Double=Värdet {0} måste vara ett giltigt nummer
+typeMismatch.java.lang.Integer=Värdet {0} måste vara ett giltigt heltal
+typeMismatch.java.lang.Long=Värdet {0} måste vara ett giltigt heltal
+typeMismatch.java.lang.Short=Värdet {0} måste vara ett giltigt heltal
+typeMismatch.java.math.BigDecimal=Värdet {0} måste vara ett giltigt nummer
+typeMismatch.java.math.BigInteger=Värdet {0} måste vara ett giltigt heltal
\ No newline at end of file
diff --git a/grails-app/i18n/messages_th.properties b/grails-app/i18n/messages_th.properties
new file mode 100644
index 0000000..fd0dbb6
--- /dev/null
+++ b/grails-app/i18n/messages_th.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบที่กำหนดไว้ใน [{3}]
+default.invalid.url.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบ URL
+default.invalid.creditCard.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบหมายเลขบัตรเครดิต
+default.invalid.email.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบอีเมล์
+default.invalid.range.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีค่าที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}]
+default.invalid.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีขนาดที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}]
+default.invalid.max.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าเกิดกว่าค่ามากสุด [{3}]
+default.invalid.min.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าน้อยกว่าค่าต่ำสุด  [{3}]
+default.invalid.max.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดเกินกว่าขนาดมากสุดของ [{3}]
+default.invalid.min.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดต่ำกว่าขนาดต่ำสุดของ  [{3}]
+default.invalid.validator.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ผ่านการทวนสอบค่าที่ตั้งขึ้น
+default.not.inlist.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้อยู่ในรายการต่อไปนี้  [{3}]
+default.blank.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็นค่าว่างได้
+default.not.equal.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่สามารถเท่ากับ [{3}] ได้
+default.null.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็น null ได้
+default.not.unique.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] จะต้องไม่ซ้ำ (unique)
+
+default.paginate.prev=ก่อนหน้า
+default.paginate.next=ถัดไป
+default.boolean.true=จริง
+default.boolean.false=เท็จ
+default.date.format=dd-MM-yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message=สร้าง {0} {1} เรียบร้อยแล้ว
+default.updated.message=ปรับปรุง {0} {1} เรียบร้อยแล้ว
+default.deleted.message=ลบ {0} {1} เรียบร้อยแล้ว
+default.not.deleted.message=ไม่สามารถลบ {0} {1}
+default.not.found.message=ไม่พบ {0} ด้วย id {1} นี้
+default.optimistic.locking.failure=มีผู้ใช้ท่านอื่นปรับปรุง {0} ขณะที่คุณกำลังแก้ไขข้อมูลอยู่
+
+default.home.label=หน้าแรก
+default.list.label=รายการ {0}
+default.add.label=เพิ่ม {0}
+default.new.label=สร้าง {0} ใหม่
+default.create.label=สร้าง {0}
+default.show.label=แสดง {0}
+default.edit.label=แก้ไข {0}
+
+default.button.create.label=สร้าง
+default.button.edit.label=แก้ไข
+default.button.update.label=ปรับปรุง
+default.button.delete.label=ลบ
+default.button.delete.confirm.message=คุณแน่ใจหรือไม่ ?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=คุณสมบัติ '{0}' จะต้องเป็นค่า URL ที่ถูกต้อง
+typeMismatch.java.net.URI=คุณสมบัติ '{0}' จะต้องเป็นค่า URI ที่ถูกต้อง
+typeMismatch.java.util.Date=คุณสมบัติ '{0}' จะต้องมีค่าเป็นวันที่
+typeMismatch.java.lang.Double=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Double
+typeMismatch.java.lang.Integer=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Integer
+typeMismatch.java.lang.Long=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Long
+typeMismatch.java.lang.Short=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Short
+typeMismatch.java.math.BigDecimal=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigDecimal
+typeMismatch.java.math.BigInteger=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigInteger
diff --git a/grails-app/i18n/messages_zh_CN.properties b/grails-app/i18n/messages_zh_CN.properties
new file mode 100644
index 0000000..b89bc93
--- /dev/null
+++ b/grails-app/i18n/messages_zh_CN.properties
@@ -0,0 +1,18 @@
+default.blank.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3A\u7A7A
+default.doesnt.match.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E\u5B9A\u4E49\u7684\u6A21\u5F0F [{3}]\u4E0D\u5339\u914D
+default.invalid.creditCard.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684\u4FE1\u7528\u5361\u53F7
+default.invalid.email.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
+default.invalid.max.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927
+default.invalid.max.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927
+default.invalid.min.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F
+default.invalid.min.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F
+default.invalid.range.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] )
+default.invalid.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] )
+default.invalid.url.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL
+default.invalid.validator.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u672A\u80FD\u901A\u8FC7\u81EA\u5B9A\u4E49\u7684\u9A8C\u8BC1
+default.not.equal.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E[{3}]\u4E0D\u76F8\u7B49
+default.not.inlist.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5217\u8868\u7684\u53D6\u503C\u8303\u56F4\u5185
+default.not.unique.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u5FC5\u987B\u662F\u552F\u4E00\u7684
+default.null.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3Anull
+default.paginate.next=\u4E0B\u9875
+default.paginate.prev=\u4E0A\u9875
diff --git a/grails-app/i18n/shiro.properties b/grails-app/i18n/shiro.properties
new file mode 100644
index 0000000..ddba8d9
--- /dev/null
+++ b/grails-app/i18n/shiro.properties
@@ -0,0 +1 @@
+login.failed = Invalid username and/or password
diff --git a/grails-app/jobs/org/bbop/apollo/CleanupPreferencesJob.groovy b/grails-app/jobs/org/bbop/apollo/CleanupPreferencesJob.groovy
new file mode 100644
index 0000000..a026e57
--- /dev/null
+++ b/grails-app/jobs/org/bbop/apollo/CleanupPreferencesJob.groovy
@@ -0,0 +1,18 @@
+package org.bbop.apollo
+
+
+class CleanupPreferencesJob {
+
+    def preferenceService
+
+    static triggers = {
+//      simple repeatInterval: 30000l // execute job every 30 seconds for testing
+//        simple repeatInterval: 24 * 60 * 60 * 1000l ,startDelay: 5 * 60 * 1000l // execute job once a day, delays 5 minutes
+        simple repeatInterval: 8 * 60 * 60 * 1000l ,startDelay: 5 * 60 * 1000l // execute job three times a day, delays 5 minutes
+    }
+
+    def execute() {
+        // execute job
+        preferenceService.removeStalePreferences()
+    }
+}
diff --git a/grails-app/jobs/org/bbop/apollo/PhoneHomeJob.groovy b/grails-app/jobs/org/bbop/apollo/PhoneHomeJob.groovy
new file mode 100644
index 0000000..3078145
--- /dev/null
+++ b/grails-app/jobs/org/bbop/apollo/PhoneHomeJob.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+
+
+class PhoneHomeJob {
+
+    def phoneHomeService
+    def configWrapperService
+
+    static triggers = {
+//      simple repeatInterval: 5000l // execute job once a day
+        simple repeatInterval: 24 * 60 * 60 * 1000l // execute job once a day
+    }
+
+    def execute() {
+        // execute job
+        def map = ["num_users":User.count.toString(),"num_organisms": Organism.count.toString()]
+        phoneHomeService.pingServer(PhoneHomeEnum.RUNNING.value,map)
+    }
+}
diff --git a/grails-app/migrations/changelog-2_0_0.groovy b/grails-app/migrations/changelog-2_0_0.groovy
new file mode 100644
index 0000000..a6d4bfc
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_0.groovy
@@ -0,0 +1,2192 @@
+databaseChangeLog = {
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-1") {
+		createTable(tableName: "analysis") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "analysisPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "algorithm", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "program", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "program_version", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "source_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "sourceuri", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "source_version", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "time_executed", type: "timestamp") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-2") {
+		createTable(tableName: "analysis_feature") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "analysis_featPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "analysis_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "identity", type: "double") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "normalized_score", type: "double") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "raw_score", type: "double") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "significance", type: "double") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-3") {
+		createTable(tableName: "analysis_property") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "analysis_propPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "analysis_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "value", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-4") {
+		createTable(tableName: "audit_log") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "audit_logPK")
+			}
+
+			column(name: "actor", type: "varchar(255)")
+
+			column(name: "class_name", type: "varchar(255)")
+
+			column(name: "date_created", type: "timestamp") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "event_name", type: "varchar(255)")
+
+			column(name: "last_updated", type: "timestamp") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "new_value", type: "varchar(255)")
+
+			column(name: "old_value", type: "varchar(255)")
+
+			column(name: "persisted_object_id", type: "varchar(255)")
+
+			column(name: "persisted_object_version", type: "bigint")
+
+			column(name: "property_name", type: "varchar(255)")
+
+			column(name: "uri", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-5") {
+		createTable(tableName: "available_status") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "available_staPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "value", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-6") {
+		createTable(tableName: "canned_comment") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "canned_commenPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "comment", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "metadata", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-7") {
+		createTable(tableName: "canned_comment_feature_type") {
+			column(name: "canned_comment_feature_types_id", type: "bigint")
+
+			column(name: "feature_type_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-8") {
+		createTable(tableName: "custom_domain_mapping") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "custom_domainPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "alternate_cv_term", type: "varchar(255)")
+
+			column(name: "cv_term", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_transcript", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "ontology_id", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-9") {
+		createTable(tableName: "cv") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "cvPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "definition", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-10") {
+		createTable(tableName: "cvterm") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "cvtermPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cv_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "dbxref_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "definition", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_obsolete", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_relationship_type", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-11") {
+		createTable(tableName: "cvterm_path") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "cvterm_pathPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cv_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvterm_path_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "objectcvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "path_distance", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "subjectcvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-12") {
+		createTable(tableName: "cvterm_relationship") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "cvterm_relatiPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvterm_relationship_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "objectcvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "subjectcvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-13") {
+		createTable(tableName: "data_adapter") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "data_adapterPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "export_source_genomic_sequence", type: "boolean")
+
+			column(name: "feature_type_string", type: "varchar(255)")
+
+			column(name: "implementation_class", type: "varchar(255)")
+
+			column(name: "data_adapter_key", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "options", type: "varchar(255)")
+
+			column(name: "permission", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "source", type: "varchar(255)")
+
+			column(name: "temp_directory", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-14") {
+		createTable(tableName: "data_adapter_data_adapter") {
+			column(name: "data_adapter_data_adapters_id", type: "bigint")
+
+			column(name: "data_adapter_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-15") {
+		createTable(tableName: "db") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "dbPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)")
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "url", type: "varchar(255)")
+
+			column(name: "url_prefix", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-16") {
+		createTable(tableName: "dbxref") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "dbxrefPK")
+			}
+
+			column(name: "version", type: "varchar(255)")
+
+			column(name: "accession", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "db_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-17") {
+		createTable(tableName: "dbxref_property") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "dbxref_properPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "dbxref_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "dbxref_property_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "value", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-18") {
+		createTable(tableName: "environment") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "environmentPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "environment_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "uniquename", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-19") {
+		createTable(tableName: "environmentcvterm") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "environmentcvPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "environment_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "environment_cvterm_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-20") {
+		createTable(tableName: "feature") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "featurePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "date_created", type: "timestamp")
+
+			column(name: "dbxref_id", type: "bigint")
+
+			column(name: "description", type: "varchar(255)")
+
+			column(name: "is_analysis", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_obsolete", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "last_updated", type: "timestamp")
+
+			column(name: "md5checksum", type: "varchar(255)")
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "sequence_length", type: "integer")
+
+			column(name: "status_id", type: "bigint")
+
+			column(name: "symbol", type: "varchar(255)")
+
+			column(name: "unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "class", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "alteration_residue", type: "varchar(255)")
+
+			column(name: "deletion_length", type: "integer")
+
+			column(name: "alternate_cv_term", type: "varchar(255)")
+
+			column(name: "class_name", type: "varchar(255)")
+
+			column(name: "custom_alternate_cv_term", type: "varchar(255)")
+
+			column(name: "custom_class_name", type: "varchar(255)")
+
+			column(name: "custom_cv_term", type: "varchar(255)")
+
+			column(name: "custom_ontology_id", type: "varchar(255)")
+
+			column(name: "cv_term", type: "varchar(255)")
+
+			column(name: "meta_data", type: "longvarchar")
+
+			column(name: "ontology_id", type: "varchar(255)")
+
+			column(name: "analysis_feature_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-21") {
+		createTable(tableName: "feature_dbxref") {
+			column(name: "feature_featuredbxrefs_id", type: "bigint")
+
+			column(name: "dbxref_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-22") {
+		createTable(tableName: "feature_event") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_eventPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "child_id", type: "bigint")
+
+			column(name: "child_split_id", type: "bigint")
+
+			column(name: "current", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "date_created", type: "timestamp") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "editor_id", type: "bigint")
+
+			column(name: "last_updated", type: "timestamp") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "new_features_json_array", type: "longvarchar")
+
+			column(name: "old_features_json_array", type: "longvarchar")
+
+			column(name: "operation", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "original_json_command", type: "longvarchar")
+
+			column(name: "parent_id", type: "bigint")
+
+			column(name: "parent_merge_id", type: "bigint")
+
+			column(name: "unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-23") {
+		createTable(tableName: "feature_feature_phenotypes") {
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "phenotype_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-24") {
+		createTable(tableName: "feature_genotype") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_genotPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cgroup", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "chromosome_feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_genotype_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genotype_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-25") {
+		createTable(tableName: "feature_grails_user") {
+			column(name: "feature_owners_id", type: "bigint")
+
+			column(name: "user_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-26") {
+		createTable(tableName: "feature_location") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_locatPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "fmax", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "fmin", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_fmax_partial", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_fmin_partial", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "locgroup", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "phase", type: "integer")
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "residue_info", type: "varchar(255)")
+
+			column(name: "sequence_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "strand", type: "integer")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-27") {
+		createTable(tableName: "feature_location_publication") {
+			column(name: "feature_location_feature_location_publications_id", type: "bigint")
+
+			column(name: "publication_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-28") {
+		createTable(tableName: "feature_property") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_propePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "tag", type: "varchar(255)")
+
+			column(name: "type_id", type: "bigint")
+
+			column(name: "value", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "class", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-29") {
+		createTable(tableName: "feature_property_publication") {
+			column(name: "feature_property_feature_property_publications_id", type: "bigint")
+
+			column(name: "publication_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-30") {
+		createTable(tableName: "feature_publication") {
+			column(name: "feature_feature_publications_id", type: "bigint")
+
+			column(name: "publication_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-31") {
+		createTable(tableName: "feature_relationship") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_relatPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "child_feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "parent_feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "value", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-32") {
+		createTable(tableName: "feature_relationship_feature_property") {
+			column(name: "feature_relationship_feature_relationship_properties_id", type: "bigint")
+
+			column(name: "feature_property_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-33") {
+		createTable(tableName: "feature_relationship_publication") {
+			column(name: "feature_relationship_feature_relationship_publications_id", type: "bigint")
+
+			column(name: "publication_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-34") {
+		createTable(tableName: "feature_synonym") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_synonPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_current", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_internal", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "synonym_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_synonyms_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-35") {
+		createTable(tableName: "feature_type") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "feature_typePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "display", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "ontology_id", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-36") {
+		createTable(tableName: "featurecvterm") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "featurecvtermPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvterm_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "feature_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_not", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-37") {
+		createTable(tableName: "featurecvterm_dbxref") {
+			column(name: "featurecvterm_featurecvtermdbxrefs_id", type: "bigint")
+
+			column(name: "dbxref_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-38") {
+		createTable(tableName: "featurecvterm_publication") {
+			column(name: "featurecvterm_featurecvterm_publications_id", type: "bigint")
+
+			column(name: "publication_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-39") {
+		createTable(tableName: "genome") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "genomePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "directory", type: "varchar(255)")
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-40") {
+		createTable(tableName: "genotype") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "genotypePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genotype_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-41") {
+		createTable(tableName: "grails_user") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "grails_userPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "first_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "last_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "password_hash", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "username", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-42") {
+		createTable(tableName: "grails_user_roles") {
+			column(name: "user_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "role_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-43") {
+		createTable(tableName: "operation") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "operationPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "attributes", type: "longvarchar")
+
+			column(name: "feature_unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "new_features", type: "longvarchar")
+
+			column(name: "old_features", type: "longvarchar")
+
+			column(name: "operation_type", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-44") {
+		createTable(tableName: "organism") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "organismPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "abbreviation", type: "varchar(255)")
+
+			column(name: "blatdb", type: "varchar(255)")
+
+			column(name: "comment", type: "varchar(255)")
+
+			column(name: "common_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "directory", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genus", type: "varchar(255)")
+
+			column(name: "species", type: "varchar(255)")
+
+			column(name: "valid", type: "boolean")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-45") {
+		createTable(tableName: "organism_organism_property") {
+			column(name: "organism_organism_properties_id", type: "bigint")
+
+			column(name: "organism_property_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-46") {
+		createTable(tableName: "organism_property") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "organism_propPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "abbreviation", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "comment", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "common_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genus", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "organism_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "species", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-47") {
+		createTable(tableName: "organism_property_organism_property") {
+			column(name: "organism_property_organism_properties_id", type: "bigint")
+
+			column(name: "organism_property_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-48") {
+		createTable(tableName: "organism_property_organismdbxref") {
+			column(name: "organism_property_organismdbxrefs_id", type: "bigint")
+
+			column(name: "organismdbxref_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-49") {
+		createTable(tableName: "organismdbxref") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "organismdbxrePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "dbxref_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "organism_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "organism_dbxref_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-50") {
+		createTable(tableName: "part_of") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "part_ofPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-51") {
+		createTable(tableName: "permission") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "permissionPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "organism_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "class", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "group_id", type: "bigint")
+
+			column(name: "permissions", type: "varchar(255)")
+
+			column(name: "track_visibilities", type: "varchar(255)")
+
+			column(name: "user_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-52") {
+		createTable(tableName: "phenotype") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "phenotypePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "assay_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "attribute_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "cvalue_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "observable_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "value", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-53") {
+		createTable(tableName: "phenotype_cvterm") {
+			column(name: "phenotype_phenotypecvterms_id", type: "bigint")
+
+			column(name: "cvterm_id", type: "bigint")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-54") {
+		createTable(tableName: "phenotype_description") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "phenotype_desPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "description", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "environment_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genotype_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "phenotype_description_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-55") {
+		createTable(tableName: "phenotype_statement") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "phenotype_staPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "environment_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "genotype_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "phenotype_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-56") {
+		createTable(tableName: "preference") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "preferencePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "domain", type: "varchar(255)")
+
+			column(name: "name", type: "varchar(255)")
+
+			column(name: "preferences_string", type: "varchar(255)")
+
+			column(name: "class", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "user_id", type: "bigint")
+
+			column(name: "current_organism", type: "boolean")
+
+			column(name: "endbp", type: "integer")
+
+			column(name: "organism_id", type: "bigint")
+
+			column(name: "sequence_id", type: "bigint")
+
+			column(name: "startbp", type: "integer")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-57") {
+		createTable(tableName: "publication") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "publicationPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_obsolete", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "issue", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "mini_reference", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "pages", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_place", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_year", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publisher", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "series_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "title", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "unique_name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "volume", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "volume_title", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-58") {
+		createTable(tableName: "publication_author") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "publication_aPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "editor", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "given_names", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_author_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "rank", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "suffix", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "surname", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-59") {
+		createTable(tableName: "publication_relationship") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "publication_rPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "object_publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_relationship_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "subject_publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-60") {
+		createTable(tableName: "publicationdbxref") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "publicationdbPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "dbxref_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "is_current", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publication_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "publicationdbxref_id", type: "integer") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-61") {
+		createTable(tableName: "role") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "rolePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-62") {
+		createTable(tableName: "role_permissions") {
+			column(name: "role_id", type: "bigint")
+
+			column(name: "permissions_string", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-63") {
+		createTable(tableName: "search_tool") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "search_toolPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "binary_path", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "database_path", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "implementation_class", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "search_key", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "options", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "remove_temp_directory", type: "boolean") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "tmp_dir", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-64") {
+		createTable(tableName: "sequence") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "sequencePK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "sequence_end", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "length", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "organism_id", type: "bigint")
+
+			column(name: "ref_seq_file", type: "varchar(255)")
+
+			column(name: "seq_chunk_prefix", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "seq_chunk_size", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "sequence_directory", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "splice_acceptor", type: "varchar(255)")
+
+			column(name: "splice_donor_site", type: "varchar(255)")
+
+			column(name: "sequence_start", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "translation_table_location", type: "varchar(255)")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-65") {
+		createTable(tableName: "sequence_chunk") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "sequence_chunPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "chunk_number", type: "integer") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "residue", type: "longvarchar") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "sequence_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-66") {
+		createTable(tableName: "synonym") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "synonymPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "synonymsgml", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "type_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-67") {
+		createTable(tableName: "user_group") {
+			column(autoIncrement: "true", name: "id", type: "bigint") {
+				constraints(nullable: "false", primaryKey: "true", primaryKeyName: "user_groupPK")
+			}
+
+			column(name: "version", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "name", type: "varchar(255)") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "public_group", type: "boolean") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-68") {
+		createTable(tableName: "user_group_users") {
+			column(name: "user_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+
+			column(name: "user_group_id", type: "bigint") {
+				constraints(nullable: "false")
+			}
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-69") {
+		addPrimaryKey(columnNames: "feature_id, phenotype_id", tableName: "feature_feature_phenotypes")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-70") {
+		addPrimaryKey(columnNames: "user_id, role_id", tableName: "grails_user_roles")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-71") {
+		addPrimaryKey(columnNames: "user_group_id, user_id", tableName: "user_group_users")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-179") {
+		createIndex(indexName: "UK_3mkho1p232es23vtaqp0obydb", tableName: "db", unique: "true") {
+			column(name: "name")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-180") {
+		createIndex(indexName: "name_uniq_1445456090031", tableName: "db", unique: "true") {
+			column(name: "name")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-181") {
+		createIndex(indexName: "UK_rdmcxcj6i53kfjmb9j811ta1m", tableName: "grails_user", unique: "true") {
+			column(name: "username")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-182") {
+		createIndex(indexName: "username_uniq_1445456090054", tableName: "grails_user", unique: "true") {
+			column(name: "username")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-183") {
+		createIndex(indexName: "UK_8sewwnpamngi6b1dwaa88askk", tableName: "role", unique: "true") {
+			column(name: "name")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-184") {
+		createIndex(indexName: "name_uniq_1445456090064", tableName: "role", unique: "true") {
+			column(name: "name")
+		}
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-72") {
+		addForeignKeyConstraint(baseColumnNames: "analysis_id", baseTableName: "analysis_feature", constraintName: "FK_8m30ycwh545b4aoxor9sbk1oq", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "analysis", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-73") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "analysis_feature", constraintName: "FK_l94xl424xp988f06gr2b3t5tw", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-74") {
+		addForeignKeyConstraint(baseColumnNames: "analysis_id", baseTableName: "analysis_property", constraintName: "FK_38g8n4bitmdwkrcs217uexrwx", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "analysis", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-75") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "analysis_property", constraintName: "FK_9o7xs7saygim8y0sm4ostvpc1", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-76") {
+		addForeignKeyConstraint(baseColumnNames: "canned_comment_feature_types_id", baseTableName: "canned_comment_feature_type", constraintName: "FK_8l290fdei9m707s7ngn712sts", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "canned_comment", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-77") {
+		addForeignKeyConstraint(baseColumnNames: "feature_type_id", baseTableName: "canned_comment_feature_type", constraintName: "FK_es9vpf57b7a14sv803xy64k8h", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_type", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-78") {
+		addForeignKeyConstraint(baseColumnNames: "cv_id", baseTableName: "cvterm", constraintName: "FK_oksfqluv12ktmut9s6o9jla7a", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cv", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-79") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "cvterm", constraintName: "FK_6d097oy44230tuoo8lb8dkkcp", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-80") {
+		addForeignKeyConstraint(baseColumnNames: "cv_id", baseTableName: "cvterm_path", constraintName: "FK_ke2nrw91sxil8mv7osgv83pw1", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cv", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-81") {
+		addForeignKeyConstraint(baseColumnNames: "objectcvterm_id", baseTableName: "cvterm_path", constraintName: "FK_tlfh10092i00g6rlv589naqy5", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-82") {
+		addForeignKeyConstraint(baseColumnNames: "subjectcvterm_id", baseTableName: "cvterm_path", constraintName: "FK_nq02ir0qeydr5tj3071k9gl7b", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-83") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "cvterm_path", constraintName: "FK_jaqi1bk3t2c0m3pybmparp856", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-84") {
+		addForeignKeyConstraint(baseColumnNames: "objectcvterm_id", baseTableName: "cvterm_relationship", constraintName: "FK_r1o1rnfnsf7oipuv1h7h1fln7", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-85") {
+		addForeignKeyConstraint(baseColumnNames: "subjectcvterm_id", baseTableName: "cvterm_relationship", constraintName: "FK_pwxrfyx6rqu5krq4nj5wa3u4f", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-86") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "cvterm_relationship", constraintName: "FK_ob1d0vrfaix8b28j4tvilqnyv", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-87") {
+		addForeignKeyConstraint(baseColumnNames: "data_adapter_data_adapters_id", baseTableName: "data_adapter_data_adapter", constraintName: "FK_321276juoco9ijc32gxeo7mi9", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "data_adapter", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-88") {
+		addForeignKeyConstraint(baseColumnNames: "data_adapter_id", baseTableName: "data_adapter_data_adapter", constraintName: "FK_c5a2cdstwj0ydibnu567urh7q", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "data_adapter", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-89") {
+		addForeignKeyConstraint(baseColumnNames: "db_id", baseTableName: "dbxref", constraintName: "FK_np3tfcu9g867to3qux6raf9y8", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "db", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-90") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "dbxref_property", constraintName: "FK_3p1ssctww083s0tt65mmm64uo", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-91") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "dbxref_property", constraintName: "FK_t6ojbvugx8kou45oklsie3rt5", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-92") {
+		addForeignKeyConstraint(baseColumnNames: "cvterm_id", baseTableName: "environmentcvterm", constraintName: "FK_tql9djnqw1d7migfndoj3lrph", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-93") {
+		addForeignKeyConstraint(baseColumnNames: "environment_id", baseTableName: "environmentcvterm", constraintName: "FK_rrwb96jjqgtg077yv8pbim3jj", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "environment", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-94") {
+		addForeignKeyConstraint(baseColumnNames: "analysis_feature_id", baseTableName: "feature", constraintName: "FK_7huaou2aj3ac3oa49c1e0nhlm", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "analysis_feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-95") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "feature", constraintName: "FK_hc4vrafs0ws7ugdkp0n6u3xdo", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-96") {
+		addForeignKeyConstraint(baseColumnNames: "status_id", baseTableName: "feature", constraintName: "FK_kfq8esgv3in8wxml2x36f2md", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-97") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "feature_dbxref", constraintName: "FK_n6n7lheb1qkmlde8u6gvvjxne", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-98") {
+		addForeignKeyConstraint(baseColumnNames: "feature_featuredbxrefs_id", baseTableName: "feature_dbxref", constraintName: "FK_1mrfkxbb3n7fhjxcrkxappdn8", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-99") {
+		addForeignKeyConstraint(baseColumnNames: "editor_id", baseTableName: "feature_event", constraintName: "FK_35nc3xd2axx6fwyap4bjkt09u", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-100") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "feature_feature_phenotypes", constraintName: "FK_aqr7eiyx6puju6elciwubbwmo", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-101") {
+		addForeignKeyConstraint(baseColumnNames: "phenotype_id", baseTableName: "feature_feature_phenotypes", constraintName: "FK_dy5g29heir5ic3d36okyuihho", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "phenotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-102") {
+		addForeignKeyConstraint(baseColumnNames: "chromosome_feature_id", baseTableName: "feature_genotype", constraintName: "FK_hak8r429shmpho06rbyvwnmt0", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-103") {
+		addForeignKeyConstraint(baseColumnNames: "cvterm_id", baseTableName: "feature_genotype", constraintName: "FK_b42u9iq4kuqe5ay544do81n32", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-104") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "feature_genotype", constraintName: "FK_cm3gqs38fa2lpllgoum8n4kgn", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-105") {
+		addForeignKeyConstraint(baseColumnNames: "genotype_id", baseTableName: "feature_genotype", constraintName: "FK_736wxgjs6pip5212ash5i68p", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "genotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-106") {
+		addForeignKeyConstraint(baseColumnNames: "feature_owners_id", baseTableName: "feature_grails_user", constraintName: "FK_4dgbhgiw0vb9hqy2k5fqg3neh", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-107") {
+		addForeignKeyConstraint(baseColumnNames: "user_id", baseTableName: "feature_grails_user", constraintName: "FK_lflwbgxduee8ljjwe5rfbdil2", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-108") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "feature_location", constraintName: "FK_qml7xp9f5uojcw7jwdxcb35le", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-109") {
+		addForeignKeyConstraint(baseColumnNames: "sequence_id", baseTableName: "feature_location", constraintName: "FK_dhnrehn3tj85m2j9c0m4md3f4", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "sequence", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-110") {
+		addForeignKeyConstraint(baseColumnNames: "feature_location_feature_location_publications_id", baseTableName: "feature_location_publication", constraintName: "FK_jquf8fftudekrwgx1e870jy43", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_location", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-111") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "feature_location_publication", constraintName: "FK_n4lr2f61atuxmm8cb90qtkojq", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-112") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "feature_property", constraintName: "FK_jpvdxc57abfiridcr57x8130", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-113") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "feature_property", constraintName: "FK_36e638geg9tew42b1mp2ehff", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-114") {
+		addForeignKeyConstraint(baseColumnNames: "feature_property_feature_property_publications_id", baseTableName: "feature_property_publication", constraintName: "FK_86law9p6s1pbt02n3mltkcqwh", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-115") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "feature_property_publication", constraintName: "FK_j4dnb11fi9vcvrdjo5m352pyq", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-116") {
+		addForeignKeyConstraint(baseColumnNames: "feature_feature_publications_id", baseTableName: "feature_publication", constraintName: "FK_qolh5l4blkx8vfmwcl7f3woan", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-117") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "feature_publication", constraintName: "FK_580odgbjowisfshvk82rfjri2", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-118") {
+		addForeignKeyConstraint(baseColumnNames: "child_feature_id", baseTableName: "feature_relationship", constraintName: "FK_8jm56covt0m7m0m191bc5jseh", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-119") {
+		addForeignKeyConstraint(baseColumnNames: "parent_feature_id", baseTableName: "feature_relationship", constraintName: "FK_72kmd92rdc6gne0nrh026o1j0", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-120") {
+		addForeignKeyConstraint(baseColumnNames: "feature_property_id", baseTableName: "feature_relationship_feature_property", constraintName: "FK_scm5rx2kuhgkhdfvskyo924cy", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-121") {
+		addForeignKeyConstraint(baseColumnNames: "feature_relationship_feature_relationship_properties_id", baseTableName: "feature_relationship_feature_property", constraintName: "FK_ebgnfbogf1lwdxd8jc17511o7", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_relationship", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-122") {
+		addForeignKeyConstraint(baseColumnNames: "feature_relationship_feature_relationship_publications_id", baseTableName: "feature_relationship_publication", constraintName: "FK_bdd324e5jb0lpuhs7biy2kacm", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature_relationship", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-123") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "feature_relationship_publication", constraintName: "FK_4j4u29xis9bhr65slfaimgjye", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-124") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "feature_synonym", constraintName: "FK_nf9qbuay984ixqd2k1425rnyo", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-125") {
+		addForeignKeyConstraint(baseColumnNames: "feature_synonyms_id", baseTableName: "feature_synonym", constraintName: "FK_gsol4u8wrfwbkh1qrx18i3u6", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-126") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "feature_synonym", constraintName: "FK_82wsc3bv9i01t9851xv4xekis", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-127") {
+		addForeignKeyConstraint(baseColumnNames: "synonym_id", baseTableName: "feature_synonym", constraintName: "FK_ll4cqdh994s6x8n7vku1q7iwd", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "synonym", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-128") {
+		addForeignKeyConstraint(baseColumnNames: "cvterm_id", baseTableName: "featurecvterm", constraintName: "FK_cuwo3ernssd0t0wjceb7lmm11", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-129") {
+		addForeignKeyConstraint(baseColumnNames: "feature_id", baseTableName: "featurecvterm", constraintName: "FK_iy7bbt67s7jaemiajsrqalv5o", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "feature", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-130") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "featurecvterm", constraintName: "FK_q3wop7ii25dgiofnp2l3yj9v0", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-131") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "featurecvterm_dbxref", constraintName: "FK_r9xhefcekikp1od79ectkb22b", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-132") {
+		addForeignKeyConstraint(baseColumnNames: "featurecvterm_featurecvtermdbxrefs_id", baseTableName: "featurecvterm_dbxref", constraintName: "FK_pniehpb3pk1rqe95ejk0od6vg", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "featurecvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-133") {
+		addForeignKeyConstraint(baseColumnNames: "featurecvterm_featurecvterm_publications_id", baseTableName: "featurecvterm_publication", constraintName: "FK_3a9j1mryggb0bcaguvkhw8hjm", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "featurecvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-134") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "featurecvterm_publication", constraintName: "FK_g6l9cr99p5dhb0kvs9y0tjwnv", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-135") {
+		addForeignKeyConstraint(baseColumnNames: "role_id", baseTableName: "grails_user_roles", constraintName: "FK_4mxkyj2itw9wyvcn6d8d4mta2", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "role", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-136") {
+		addForeignKeyConstraint(baseColumnNames: "user_id", baseTableName: "grails_user_roles", constraintName: "FK_jsuq1rc9mb07tg4kubnqn8yw6", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-137") {
+		addForeignKeyConstraint(baseColumnNames: "organism_organism_properties_id", baseTableName: "organism_organism_property", constraintName: "FK_qyxdgqthtlgixvtdkkhc8g3pu", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-138") {
+		addForeignKeyConstraint(baseColumnNames: "organism_property_id", baseTableName: "organism_organism_property", constraintName: "FK_f1e1d91q04mqaij1ep3s36ujl", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-139") {
+		addForeignKeyConstraint(baseColumnNames: "organism_property_id", baseTableName: "organism_property_organism_property", constraintName: "FK_8jxbx51qysqlm07orah5xg45y", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-140") {
+		addForeignKeyConstraint(baseColumnNames: "organism_property_organism_properties_id", baseTableName: "organism_property_organism_property", constraintName: "FK_3rtkicqpr3ca8dwivye3yajaa", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-141") {
+		addForeignKeyConstraint(baseColumnNames: "organism_property_organismdbxrefs_id", baseTableName: "organism_property_organismdbxref", constraintName: "FK_kaayriabr4k4b3aomk46dmc77", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism_property", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-142") {
+		addForeignKeyConstraint(baseColumnNames: "organismdbxref_id", baseTableName: "organism_property_organismdbxref", constraintName: "FK_jbw15sttun6yrcrxchi0lwtam", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organismdbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-143") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "organismdbxref", constraintName: "FK_s3vk7onqrk0n4c86xnvqmm3ho", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-144") {
+		addForeignKeyConstraint(baseColumnNames: "organism_id", baseTableName: "organismdbxref", constraintName: "FK_l1jfi0wpnyooutd820p5gskr", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-145") {
+		addForeignKeyConstraint(baseColumnNames: "group_id", baseTableName: "permission", constraintName: "FK_hycx5el5itt1lqidt532shkpj", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "user_group", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-146") {
+		addForeignKeyConstraint(baseColumnNames: "organism_id", baseTableName: "permission", constraintName: "FK_4nvoxx3htem6jseb4rmu0aqfp", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-147") {
+		addForeignKeyConstraint(baseColumnNames: "user_id", baseTableName: "permission", constraintName: "FK_6p3mx8al2w4f7ltqiwf1j88fm", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-148") {
+		addForeignKeyConstraint(baseColumnNames: "assay_id", baseTableName: "phenotype", constraintName: "FK_cwgh6naf9gackae2ei11v6p41", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-149") {
+		addForeignKeyConstraint(baseColumnNames: "attribute_id", baseTableName: "phenotype", constraintName: "FK_phmfgylejydjqyrvo3imc97go", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-150") {
+		addForeignKeyConstraint(baseColumnNames: "cvalue_id", baseTableName: "phenotype", constraintName: "FK_jh0fc3orduigl8s7ymentbtrs", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-151") {
+		addForeignKeyConstraint(baseColumnNames: "observable_id", baseTableName: "phenotype", constraintName: "FK_gb4wy9qesx6vnekxekm18k9xa", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-152") {
+		addForeignKeyConstraint(baseColumnNames: "cvterm_id", baseTableName: "phenotype_cvterm", constraintName: "FK_9e2v7goj5w6nds5jo0x1va1nm", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-153") {
+		addForeignKeyConstraint(baseColumnNames: "phenotype_phenotypecvterms_id", baseTableName: "phenotype_cvterm", constraintName: "FK_aicsmj1kn20ikm14292g9r2j9", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "phenotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-154") {
+		addForeignKeyConstraint(baseColumnNames: "environment_id", baseTableName: "phenotype_description", constraintName: "FK_t52r166gd8710vffy3aompe7d", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "environment", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-155") {
+		addForeignKeyConstraint(baseColumnNames: "genotype_id", baseTableName: "phenotype_description", constraintName: "FK_bf1bstadamyw0gsarkb933l5b", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "genotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-156") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "phenotype_description", constraintName: "FK_1kkkx1uxs6li0r72qhvke6o77", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-157") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "phenotype_description", constraintName: "FK_8pbyj05khavdl5a648c7pmcil", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-158") {
+		addForeignKeyConstraint(baseColumnNames: "environment_id", baseTableName: "phenotype_statement", constraintName: "FK_q6jvhi3l7ty0m9tpbn09d8pxj", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "environment", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-159") {
+		addForeignKeyConstraint(baseColumnNames: "genotype_id", baseTableName: "phenotype_statement", constraintName: "FK_8rbhsxxdf669tyed8jrr747hv", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "genotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-160") {
+		addForeignKeyConstraint(baseColumnNames: "phenotype_id", baseTableName: "phenotype_statement", constraintName: "FK_gskh1e7b6qa2du48ayu49lr3s", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "phenotype", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-161") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "phenotype_statement", constraintName: "FK_hffc43aavltp8t5dtwactux5f", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-162") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "phenotype_statement", constraintName: "FK_tk1pgifvuhurefn0y3myfyyt4", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-163") {
+		addForeignKeyConstraint(baseColumnNames: "organism_id", baseTableName: "preference", constraintName: "FK_42b0lk4rcfjcagw84jugd1sgj", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-164") {
+		addForeignKeyConstraint(baseColumnNames: "sequence_id", baseTableName: "preference", constraintName: "FK_i94ksmdxi88hcqnuycebgkdvs", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "sequence", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-165") {
+		addForeignKeyConstraint(baseColumnNames: "user_id", baseTableName: "preference", constraintName: "FK_ro87rogww8hoobbwya2nn16xk", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-166") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "publication", constraintName: "FK_h3g8f3q2krcnwmq2nasbanlay", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-167") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "publication_author", constraintName: "FK_9eou8yof43krmrsdvfhfuisln", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-168") {
+		addForeignKeyConstraint(baseColumnNames: "object_publication_id", baseTableName: "publication_relationship", constraintName: "FK_97qgrmdull1avkfqpfq1mc1wt", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-169") {
+		addForeignKeyConstraint(baseColumnNames: "subject_publication_id", baseTableName: "publication_relationship", constraintName: "FK_euu6xx78omdvver8lij1ys2oq", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-170") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "publication_relationship", constraintName: "FK_q6hf14oiq9pomkjrhtndonmeh", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-171") {
+		addForeignKeyConstraint(baseColumnNames: "dbxref_id", baseTableName: "publicationdbxref", constraintName: "FK_oh81hma8qx88fhvcmfugx836b", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "dbxref", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-172") {
+		addForeignKeyConstraint(baseColumnNames: "publication_id", baseTableName: "publicationdbxref", constraintName: "FK_pgoyqd75q47r6ycwowcppbhk6", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "publication", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-173") {
+		addForeignKeyConstraint(baseColumnNames: "role_id", baseTableName: "role_permissions", constraintName: "FK_d4atqq8ege1sij0316vh2mxfu", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "role", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-174") {
+		addForeignKeyConstraint(baseColumnNames: "organism_id", baseTableName: "sequence", constraintName: "FK_rux0954nxr4lwvj2qgyjibua7", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "organism", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-175") {
+		addForeignKeyConstraint(baseColumnNames: "sequence_id", baseTableName: "sequence_chunk", constraintName: "FK_4tsu0cp2dh2avbxifp1h1c9vd", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "sequence", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-176") {
+		addForeignKeyConstraint(baseColumnNames: "type_id", baseTableName: "synonym", constraintName: "FK_4ylco1irefvydmsnedglqqdfu", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "cvterm", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-177") {
+		addForeignKeyConstraint(baseColumnNames: "user_group_id", baseTableName: "user_group_users", constraintName: "FK_9jib0g899h0gy3dypo7datfm9", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "user_group", referencesUniqueColumn: "false")
+	}
+
+	changeSet(author: "nathandunn (generated)", id: "1445456090131-178") {
+		addForeignKeyConstraint(baseColumnNames: "user_id", baseTableName: "user_group_users", constraintName: "FK_jppito7humh6e3v5mjjtutd7h", deferrable: "false", initiallyDeferred: "false", referencedColumnNames: "id", referencedTableName: "grails_user", referencesUniqueColumn: "false")
+	}
+}
diff --git a/grails-app/migrations/changelog-2_0_1.groovy b/grails-app/migrations/changelog-2_0_1.groovy
new file mode 100644
index 0000000..1b1b640
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_1.groovy
@@ -0,0 +1,117 @@
+databaseChangeLog = {
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-1") {
+        modifyDataType(columnName: "is_transcript", newDataType: "boolean", tableName: "custom_domain_mapping")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-2") {
+        modifyDataType(columnName: "export_source_genomic_sequence", newDataType: "boolean", tableName: "data_adapter")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-3") {
+        modifyDataType(columnName: "description", newDataType: "text", tableName: "feature")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-4") {
+        modifyDataType(columnName: "is_analysis", newDataType: "boolean", tableName: "feature")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-5") {
+        modifyDataType(columnName: "is_obsolete", newDataType: "boolean", tableName: "feature")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-6") {
+        modifyDataType(columnName: "name", newDataType: "text", tableName: "feature")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-7") {
+        modifyDataType(columnName: "current", newDataType: "boolean", tableName: "feature_event")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-8") {
+        modifyDataType(columnName: "name", newDataType: "text", tableName: "feature_event")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-9") {
+        modifyDataType(columnName: "is_fmax_partial", newDataType: "boolean", tableName: "feature_location")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-10") {
+        modifyDataType(columnName: "is_fmin_partial", newDataType: "boolean", tableName: "feature_location")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-11") {
+        modifyDataType(columnName: "is_current", newDataType: "boolean", tableName: "feature_synonym")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-12") {
+        modifyDataType(columnName: "is_internal", newDataType: "boolean", tableName: "feature_synonym")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-13") {
+        modifyDataType(columnName: "is_not", newDataType: "boolean", tableName: "featurecvterm")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-14") {
+        modifyDataType(columnName: "public_mode", newDataType: "boolean", tableName: "organism")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-15") {
+        modifyDataType(columnName: "valid", newDataType: "boolean", tableName: "organism")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-16") {
+        modifyDataType(columnName: "current_organism", newDataType: "boolean", tableName: "preference")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-17") {
+        modifyDataType(columnName: "native_track_list", newDataType: "boolean", tableName: "preference")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-18") {
+        modifyDataType(columnName: "active", newDataType: "boolean", tableName: "proxy")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-19") {
+        modifyDataType(columnName: "is_obsolete", newDataType: "boolean", tableName: "publication")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-20") {
+        modifyDataType(columnName: "editor", newDataType: "boolean", tableName: "publication_author")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-21") {
+        modifyDataType(columnName: "is_current", newDataType: "boolean", tableName: "publicationdbxref")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-22") {
+        modifyDataType(columnName: "remove_temp_directory", newDataType: "boolean", tableName: "search_tool")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-23") {
+        modifyDataType(columnName: "public_group", newDataType: "boolean", tableName: "user_group")
+    }
+
+    changeSet(author: "nathandunn (generated)", id: "1445460972540-27") {
+        preConditions(onFail: 'MARK_RAN', onError: "HALT") {
+            and {
+//                tableExists(tableName: "sequence",schemaName:"public")
+//                columnExists(columnName: "seq_chunk_prefix", tableName: "sequence",schemaName:"public")
+                tableExists(tableName: "sequence")
+                columnExists(columnName: "seq_chunk_prefix", tableName: "sequence")
+//                columnExists(columnName: "splice_acceptor", tableName: "sequence")
+//                columnExists(columnName: "splice_donor", tableName: "sequence")
+//                columnExists(columnName: "translation_table_location", tableName: "sequence")
+//                columnExists(columnName: "ref_seq_file", tableName: "sequence")
+//                columnExists(columnName: "sequence_directory", tableName: "sequence")
+            }
+        }
+        dropColumn(columnName: "ref_seq_file", tableName: "sequence")
+        dropColumn(columnName: "splice_acceptor", tableName: "sequence")
+        dropColumn(columnName: "splice_donor_site", tableName: "sequence")
+        dropColumn(columnName: "translation_table_location", tableName: "sequence")
+        dropColumn(columnName: "seq_chunk_prefix", tableName: "sequence")
+        dropColumn(columnName: "sequence_directory", tableName: "sequence")
+    }
+
+}
diff --git a/grails-app/migrations/changelog-2_0_2.groovy b/grails-app/migrations/changelog-2_0_2.groovy
new file mode 100644
index 0000000..935db7e
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_2.groovy
@@ -0,0 +1,12 @@
+databaseChangeLog = {
+	changeSet(author: "cmdcolin (generated)", id: "1454711582784-1") {
+        preConditions(onFail: 'MARK_RAN', onError: "HALT") {
+            not {
+                indexExists(indexName:"feature_uniqueName")
+            }
+        }
+        createIndex(indexName: "feature_uniqueName", tableName: "feature_event", unique: "false") {
+            column(name: "unique_name")
+        }
+    }
+}
diff --git a/grails-app/migrations/changelog-2_0_3.groovy b/grails-app/migrations/changelog-2_0_3.groovy
new file mode 100644
index 0000000..2ee898d
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_3.groovy
@@ -0,0 +1,24 @@
+databaseChangeLog = {
+
+    changeSet(author: "deepak.unni3", id: "1459788030174-1") {
+        modifyDataType(columnName: "value", newDataType: "text", tableName: "feature_property")
+    }
+
+    changeSet(author: "nathandunn", id: "1459788030174-2") {
+        preConditions(onFail: 'MARK_RAN', onError: "HALT") {
+            not {
+                columnExists(columnName: "client_token", tableName: "preference")
+			}
+        } 
+        addColumn(tableName: "preference"){
+           column(name:"client_token",type:"varchar(255)",value:"GENERATED DEFAULT TOKEN")
+        }
+        addColumn(tableName: "preference"){
+           column(name:"date_created",type:"timestamp")
+        }
+        addColumn(tableName: "preference"){
+           column(name:"last_updated",type:"timestamp")
+        }
+        addNotNullConstraint(tableName: "preference",columnName:"client_token", columnDataType:"varchar(255)")
+    }
+}
diff --git a/grails-app/migrations/changelog-2_0_7.groovy b/grails-app/migrations/changelog-2_0_7.groovy
new file mode 100644
index 0000000..c7b54a3
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_7.groovy
@@ -0,0 +1,7 @@
+databaseChangeLog = {
+
+    changeSet(author: "nathandunn", id: "1459788030175-1") {
+        delete(tableName: "preference")
+    }
+
+}
diff --git a/grails-app/migrations/changelog-2_0_8.groovy b/grails-app/migrations/changelog-2_0_8.groovy
new file mode 100644
index 0000000..f8a6454
--- /dev/null
+++ b/grails-app/migrations/changelog-2_0_8.groovy
@@ -0,0 +1,7 @@
+databaseChangeLog = {
+
+    changeSet(author: "deepak.unni3", id: "1459788030176-1") {
+
+        dropNotNullConstraint(tableName: "sequence", columnName:"seq_chunk_size", columnDataType:"integer")
+    }
+}
\ No newline at end of file
diff --git a/grails-app/migrations/changelog.groovy b/grails-app/migrations/changelog.groovy
new file mode 100644
index 0000000..91625a8
--- /dev/null
+++ b/grails-app/migrations/changelog.groovy
@@ -0,0 +1,7 @@
+databaseChangeLog = {
+    include file: 'changelog-2_0_1.groovy'
+    include file: 'changelog-2_0_2.groovy'
+    include file: 'changelog-2_0_3.groovy'
+    include file: 'changelog-2_0_7.groovy'
+    include file: 'changelog-2_0_8.groovy'
+}
diff --git a/grails-app/realms/org/bbop/apollo/DbRealm.groovy b/grails-app/realms/org/bbop/apollo/DbRealm.groovy
new file mode 100644
index 0000000..5a2b8fa
--- /dev/null
+++ b/grails-app/realms/org/bbop/apollo/DbRealm.groovy
@@ -0,0 +1,132 @@
+package org.bbop.apollo
+
+import org.apache.shiro.authc.AccountException
+import org.apache.shiro.authc.IncorrectCredentialsException
+import org.apache.shiro.authc.UnknownAccountException
+import org.apache.shiro.authc.SimpleAccount
+import org.apache.shiro.authz.permission.WildcardPermission
+
+class DbRealm {
+    static authTokenClass = org.apache.shiro.authc.UsernamePasswordToken
+
+    def credentialMatcher
+    def shiroPermissionResolver
+
+    def authenticate(authToken) {
+        log.info "Attempting to authenticate ${authToken.username} in DB realm..."
+        def username = authToken.username
+
+        // Null username is invalid
+        if (username == null) {
+            throw new AccountException("Null usernames are not allowed by this realm.")
+        }
+
+        // Get the user with the given username. If the user is not
+        // found, then they don't have an account and we throw an
+        // exception.
+        def user = User.findByUsername(username)
+        if (!user) {
+            throw new UnknownAccountException("No account found for user [${username}]")
+        }
+
+        log.info "Found user '${user.username}' in DB"
+
+        // Now check the user's password against the hashed value stored
+        // in the database.
+        def account = new SimpleAccount(username, user.passwordHash, "DbRealm")
+        if (!credentialMatcher.doCredentialsMatch(authToken, account)) {
+            log.info "Invalid password (DB realm)"
+            throw new IncorrectCredentialsException("Invalid password for user '${username}'")
+        }
+
+        return account
+    }
+
+    def hasRole(principal, roleName) {
+        def roles = User.withCriteria {
+            roles {
+                eq("name", roleName)
+            }
+            eq("username", principal)
+        }
+
+        return roles.size() > 0
+    }
+
+    def hasAllRoles(principal, roles) {
+        def r = User.withCriteria {
+            roles {
+                'in'("name", roles)
+            }
+            eq("username", principal)
+        }
+
+        return r.size() == roles.size()
+    }
+
+    def isPermitted(principal, requiredPermission) {
+        // Does the user have the given permission directly associated
+        // with himself?
+        //
+        // First find all the permissions that the user has that match
+        // the required permission's type and project code.
+        def user = User.findByUsername(principal)
+        def permissions = user.permissions
+
+        // Try each of the permissions found and see whether any of
+        // them confer the required permission.
+        def retval = permissions?.find { permString ->
+            // Create a real permission instance from the database
+            // permission.
+            def perm = shiroPermissionResolver.resolvePermission(permString)
+
+            // Now check whether this permission implies the required
+            // one.
+            if (perm.implies(requiredPermission)) {
+                // User has the permission!
+                return true
+            }
+            else {
+                return false
+            }
+        }
+
+        if (retval != null) {
+            // Found a matching permission!
+            return true
+        }
+
+        // If not, does he gain it through a role?
+        //
+        // Get the permissions from the roles that the user does have.
+        def results = User.executeQuery("select distinct p from User as user join user.roles as role join role.permissions as p where user.username = '$principal'")
+
+        // There may be some duplicate entries in the results, but
+        // at this stage it is not worth trying to remove them. Now,
+        // create a real permission from each result and check it
+        // against the required one.
+        retval = results.find { permString ->
+            // Create a real permission instance from the database
+            // permission.
+            def perm = shiroPermissionResolver.resolvePermission(permString)
+
+            // Now check whether this permission implies the required
+            // one.
+            if (perm.implies(requiredPermission)) {
+                // User has the permission!
+                return true
+            }
+            else {
+                return false
+            }
+        }
+
+        if (retval != null) {
+            // Found a matching permission!
+            return true
+        }
+        else {
+            return false
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/AnnotationEditorService.groovy b/grails-app/services/org/bbop/apollo/AnnotationEditorService.groovy
new file mode 100644
index 0000000..23c514d
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/AnnotationEditorService.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+
+ at Transactional
+class AnnotationEditorService {
+
+
+    @NotTransactional
+    String cleanJSONString(String inputString){
+        String outputString = new String(inputString)
+        // remove leading string
+        outputString = outputString.indexOf("\"")==0 ? outputString.substring(1) : outputString
+        outputString = outputString.lastIndexOf("\"")==outputString.length()-1 ? outputString.substring(0,outputString.length()-1) : outputString
+//        outputString = outputString.replaceAll("/\\\\/","")
+        outputString = outputString.replaceAll("\\\\\"","\"")
+        return outputString
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/AnnotatorService.groovy b/grails-app/services/org/bbop/apollo/AnnotatorService.groovy
new file mode 100644
index 0000000..cc8b9c2
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/AnnotatorService.groovy
@@ -0,0 +1,92 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.preference.OrganismDTO
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+ at Transactional
+class AnnotatorService {
+
+    def permissionService
+    def preferenceService
+    def requestHandlingService
+
+    def getAppState(String token) {
+        JSONObject appStateObject = new JSONObject()
+        try {
+            List<Organism> organismList = permissionService.getOrganismsForCurrentUser()
+            UserOrganismPreference userOrganismPreference = UserOrganismPreference.findByUserAndCurrentOrganismAndClientToken(permissionService.currentUser, true,token,[max: 1, sort: "lastUpdated", order: "desc"])
+            log.debug "found organism preference: ${userOrganismPreference} for token ${token}"
+            Long defaultOrganismId = userOrganismPreference ? userOrganismPreference.organism.id : null
+
+            Map<Organism,Boolean> organismBooleanMap  = permissionService.userHasOrganismPermissions(PermissionEnum.ADMINISTRATE)
+            Map<Sequence,Integer> sequenceIntegerMap  = [:]
+            Map<Organism,Integer> annotationCountMap = [:]
+            if(organismList){
+                Sequence.executeQuery("select o,count(s) from Organism o join o.sequences s where o in (:organismList) group by o ",[organismList:organismList]).each(){
+                    sequenceIntegerMap.put(it[0],it[1])
+                }
+                Feature.executeQuery("select o,count(distinct f) from Feature f left join f.parentFeatureRelationships pfr  join f.featureLocations fl join fl.sequence s join s.organism o  where f.childFeatureRelationships is empty and o in (:organismList) and f.class in (:viewableTypes) group by o", [organismList: organismList, viewableTypes: requestHandlingService.viewableAnnotationList]).each {
+                    annotationCountMap.put(it[0],it[1])
+                }
+            }
+
+
+
+            JSONArray organismArray = new JSONArray()
+            for (Organism organism in organismList) {
+                Integer sequenceCount = sequenceIntegerMap.get(organism) ?: 0
+                JSONObject jsonObject = [
+                        id             : organism.id as Long,
+                        commonName     : organism.commonName,
+                        blatdb         : organism.blatdb,
+                        directory      : organism.directory,
+                        annotationCount: annotationCountMap.get(organism) ?: 0,
+                        sequences      : sequenceCount,
+                        genus          : organism.genus,
+                        species        : organism.species,
+                        valid          : organism.valid,
+                        publicMode     : organism.publicMode,
+                        nonDefaultTranslationTable : organism.nonDefaultTranslationTable,
+                        currentOrganism: defaultOrganismId != null ? organism.id == defaultOrganismId : false,
+                        editable       : organismBooleanMap.get(organism) ?: false
+
+                ] as JSONObject
+                organismArray.add(jsonObject)
+            }
+            appStateObject.put("organismList", organismArray)
+            UserOrganismPreferenceDTO currentUserOrganismPreferenceDTO = preferenceService.getCurrentOrganismPreference(permissionService.currentUser,null,token)
+            if(currentUserOrganismPreferenceDTO){
+                OrganismDTO currentOrganism = currentUserOrganismPreferenceDTO?.organism
+                appStateObject.put("currentOrganism", currentOrganism )
+
+
+                if (!currentUserOrganismPreferenceDTO.sequence) {
+                    Organism organism = Organism.findById(currentOrganism.id)
+                    Sequence sequence = Sequence.findByOrganism(organism,[sort:"name",order:"asc",max: 1])
+                    // often the case when creating it
+                    currentUserOrganismPreferenceDTO.sequence = preferenceService.getDTOFromSequence(sequence)
+                }
+                appStateObject.put("currentSequence", currentUserOrganismPreferenceDTO.sequence)
+
+
+                if (currentUserOrganismPreferenceDTO.startbp && currentUserOrganismPreferenceDTO.endbp) {
+                    appStateObject.put("currentStartBp", currentUserOrganismPreferenceDTO.startbp)
+                    appStateObject.put("currentEndBp", currentUserOrganismPreferenceDTO.endbp)
+                }
+            }
+        }
+        catch(PermissionException e) {
+            def error=[error: "Error: "+e]
+            log.error(error.error)
+            return error
+        }
+
+
+
+        return appStateObject
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/CdsService.groovy b/grails-app/services/org/bbop/apollo/CdsService.groovy
new file mode 100644
index 0000000..b2c66f0
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/CdsService.groovy
@@ -0,0 +1,169 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.Strand
+
+ at Transactional
+class CdsService {
+
+    public static final String MANUALLY_SET_TRANSLATION_START = "Manually set translation start";
+    public static final String MANUALLY_SET_TRANSLATION_END = "Manually set translation end";
+
+    def featureRelationshipService
+    def featurePropertyService
+    def transcriptService
+    def featureService
+    def exonService
+    def sequenceService
+    def overlapperService
+    
+    public void setManuallySetTranslationStart(CDS cds, boolean manuallySetTranslationStart) {
+        if (manuallySetTranslationStart && isManuallySetTranslationStart(cds)) {
+            return;
+        }
+        if (!manuallySetTranslationStart && !isManuallySetTranslationStart(cds)) {
+            return;
+        }
+        if (manuallySetTranslationStart) {
+            featurePropertyService.addComment(cds, MANUALLY_SET_TRANSLATION_START)
+        }
+        if (!manuallySetTranslationStart) {
+            featurePropertyService.deleteComment(cds, MANUALLY_SET_TRANSLATION_START)
+        }
+    }
+
+    public boolean isManuallySetTranslationStart(CDS cds) {
+        for (Comment comment : featurePropertyService.getComments(cds)) {
+            if (comment.value.equals(MANUALLY_SET_TRANSLATION_START)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    public boolean isManuallySetTranslationEnd(CDS cds) {
+
+        for (Comment comment : featurePropertyService.getComments(cds)) {
+            if (comment.value.equals(MANUALLY_SET_TRANSLATION_END)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setManuallySetTranslationEnd(CDS cds, boolean manuallySetTranslationEnd) {
+        if (manuallySetTranslationEnd && isManuallySetTranslationEnd(cds)) {
+            return;
+        }
+        if (!manuallySetTranslationEnd && !isManuallySetTranslationEnd(cds)) {
+            return;
+        }
+        if (manuallySetTranslationEnd) {
+            featurePropertyService.addComment(cds, MANUALLY_SET_TRANSLATION_END)
+        }
+        if (!manuallySetTranslationEnd) {
+            featurePropertyService.deleteComment(cds, MANUALLY_SET_TRANSLATION_END)
+        }
+    }
+
+
+    /**
+     * TODO: is this right?  I think it should be CDS , not transcript?
+     * TODO: is this just remove parents and children?
+     * @param cds
+     * @param stopCodonReadThrough
+     * @return
+     */
+    def deleteStopCodonReadThrough(CDS cds, StopCodonReadThrough stopCodonReadThrough) {
+        featureRelationshipService.deleteChildrenForTypes(cds, StopCodonReadThrough.ontologyId)
+        featureRelationshipService.deleteParentForTypes(stopCodonReadThrough, Transcript.ontologyId)
+        stopCodonReadThrough.delete()
+    }
+
+    def deleteStopCodonReadThrough(CDS cds) {
+        StopCodonReadThrough stopCodonReadThrough = (StopCodonReadThrough) featureRelationshipService.getChildForFeature(cds,StopCodonReadThrough.ontologyId)
+        if (stopCodonReadThrough != null) {
+            deleteStopCodonReadThrough(cds, stopCodonReadThrough);
+        }
+
+    }
+
+    def getStopCodonReadThrough(CDS cds){
+        return featureRelationshipService.getChildrenForFeatureAndTypes(cds,StopCodonReadThrough.ontologyId)
+    }
+
+    public StopCodonReadThrough createStopCodonReadThrough(CDS cds) {
+        String uniqueName = cds.getUniqueName() + FeatureStringEnum.STOP_CODON_READHTHROUGH_SUFFIX.value;
+        StopCodonReadThrough stopCodonReadThrough = new StopCodonReadThrough(
+                uniqueName: uniqueName
+                ,name: uniqueName
+                , isAnalysis: cds.isIsAnalysis()
+                , isObsolete: cds.isIsObsolete()
+        ).save(failOnError: true)
+        FeatureLocation featureLocation = new FeatureLocation(
+                sequence: cds.featureLocation.sequence
+                , feature: stopCodonReadThrough
+                ,fmin: cds.featureLocation.fmin
+                ,fmax: cds.featureLocation.fmax
+        ).save(failOnError: true)
+
+        stopCodonReadThrough.addToFeatureLocations(featureLocation)
+        stopCodonReadThrough.featureLocation.setStrand(cds.getStrand());
+
+        stopCodonReadThrough.save(flush: true)
+
+        return stopCodonReadThrough;
+    }
+
+    def setStopCodonReadThrough(CDS cds, StopCodonReadThrough stopCodonReadThrough, boolean replace = true) {
+        if (replace) {
+            featureRelationshipService.setChildForType(cds,stopCodonReadThrough)
+        }
+
+        FeatureRelationship fr = new FeatureRelationship(
+                parentFeature: cds
+                , childFeature: stopCodonReadThrough
+                , rank: 0 // TODO: Do we need to rank the order of any other transcripts?
+        ).save(insert: true,failOnError: true)
+        cds.addToParentFeatureRelationships(fr);
+        stopCodonReadThrough.addToChildFeatureRelationships(fr)
+
+        stopCodonReadThrough.save(failOnError: true)
+        cds.save(flush: true,failOnError: true)
+
+    }
+
+    def hasStopCodonReadThrough(CDS cds) {
+        return getStopCodonReadThrough(cds).size() != 0
+    }
+
+    def getResiduesFromCDS(CDS cds) {
+        // New implementation that infers CDS based on overlapping exons
+        Transcript transcript = transcriptService.getTranscript(cds)
+        List <Exon> exons = transcriptService.getSortedExons(transcript,true)
+        String residues = ""
+        for(Exon exon : exons) {
+            if (!overlapperService.overlaps(exon,cds)) {
+                continue
+            }
+            int fmin = exon.fmin < cds.fmin ? cds.fmin : exon.fmin
+            int fmax = exon.fmax > cds.fmax ? cds.fmax : exon.fmax
+            int localStart
+            int localEnd
+            if (cds.getFeatureLocation().strand == Strand.NEGATIVE.value) {
+                localEnd = featureService.convertSourceCoordinateToLocalCoordinate((Feature) exon, fmin) + 1
+                localStart = featureService.convertSourceCoordinateToLocalCoordinate((Feature) exon, fmax) + 1
+            } 
+            else {
+                localStart = featureService.convertSourceCoordinateToLocalCoordinate((Feature) exon, fmin)
+                localEnd = featureService.convertSourceCoordinateToLocalCoordinate((Feature) exon, fmax)
+            }
+            residues += sequenceService.getResiduesFromFeature((Feature) exon).substring(localStart, localEnd)
+        }
+        return residues
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/ChadoHandlerService.groovy b/grails-app/services/org/bbop/apollo/ChadoHandlerService.groovy
new file mode 100644
index 0000000..a0e11d2
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ChadoHandlerService.groovy
@@ -0,0 +1,1459 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import java.security.MessageDigest
+import java.sql.Timestamp
+
+/**
+ *
+ * Chado Compliance Layers
+ * Level 0: Relational schema - this basically means that the schema is adhered to
+ * Level 1: Ontologies - this means that all features in the feature table are of a type represented in SO and
+ * all feature relationships in feature_relationship table must be SO relationship types
+ * Level 2: Graph - all features relationships between a feature of type X and Y must correspond to relationship of
+ * that type in SO.
+ *
+ * Relevant Chado modules:
+ * Chado General Module
+ * Chado CV Module
+ * Chado Organism Module
+ * Chado Sequence Module
+ * Chado Publication Module
+ *
+ */
+
+ at Transactional
+class ChadoHandlerService {
+
+    def configWrapperService
+    def sequenceService
+    def featureRelationshipService
+    def transcriptService
+    def cdsService
+
+    private static final String SEQUENCE_ONTOLOGY = "sequence"
+    private static final String RELATIONSHIP_ONTOLOGY = "relationship"
+    private static final String FEATURE_PROPERTY = "feature_property"
+    private static final def topLevelFeatureTypes = [Gene.alternateCvTerm, Pseudogene.alternateCvTerm, TransposableElement.alternateCvTerm, RepeatRegion.alternateCvTerm,
+                                                     Insertion.alternateCvTerm, Deletion.alternateCvTerm, Substitution.alternateCvTerm]
+    private static final ontologyDb = ["SO", "GO", "RO"]
+    Map<String, org.gmod.chado.Organism> chadoOrganismsMap = new HashMap<String, org.gmod.chado.Organism>()
+    Map<String, Integer> exportStatisticsMap = new HashMap<String, Integer>();
+    ArrayList<org.bbop.apollo.Feature> processedFeatures = new ArrayList<org.bbop.apollo.Feature>()
+    ArrayList<org.bbop.apollo.Feature> failedFeatures = new ArrayList<org.bbop.apollo.Feature>()
+
+    def writeFeatures(Organism organism, ArrayList<Sequence> sequenceList, ArrayList<Feature> features, boolean exportAllSequences = false) {
+        JSONObject returnObject = new JSONObject()
+        if (!configWrapperService.hasChadoDataSource()) {
+            log.error("Cannot export annotations to Chado as Chado data source has not been configured")
+            returnObject.error = "Cannot export annotations to Chado as Chado data source has not been configured."
+        }
+        else {
+            if (!checkForOntologies()) {
+                log.error "No ontologies loaded into the Chado database"
+                returnObject.error = "No ontologies loaded into the Chado database. Refer to "
+            }
+            else if (organism.genus == null || organism.species == null) {
+                log.error "Apollo Organism must have genus and species defined."
+                returnObject.error = "Apollo Organism must have genus and species defined."
+            }
+            else {
+                returnObject = writeFeaturesToChado(organism, sequenceList, features, exportAllSequences)
+            }
+        }
+
+        return returnObject
+    }
+
+    /**
+     * Writes all features in features array into Chado for the given organism
+     * @param organism
+     * @param features
+     * @return
+     */
+    def writeFeaturesToChado(Organism organism, ArrayList<Sequence> sequenceList, ArrayList<Feature> features, boolean exportAllSequences = false) {
+        /*
+        The exporter assumes that the following ontologies are pre-loaded into the Chado data source:
+        1. Sequence Ontology
+        2. Gene Ontology
+        3. Relations Ontology
+         */
+
+        initializeExportStatistics()
+        long totalTime = System.currentTimeMillis()
+        // Create the organism
+        long startTime = System.currentTimeMillis()
+        createChadoOrganism(organism)
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado Organism for ${organism.commonName}: ${endTime - startTime} ms"
+
+        // Create chado feature for sequence in sequenceList
+        if (sequenceList.size() > 0) {
+            createChadoFeatureForSequences(organism, sequenceList, configWrapperService.getChadoExportFastaForSequence())
+        }
+        else
+        if (exportAllSequences) {
+            sequenceList = Sequence.findAllByOrganism(organism)
+            createChadoFeatureForSequences(organism, sequenceList, configWrapperService.getChadoExportFastaForSequence())
+        }
+
+        def existingChadoAnnotations = org.gmod.chado.Feature.executeQuery(
+                "SELECT DISTINCT f.uniquename FROM org.gmod.chado.Feature f WHERE f.type.name IN :topLevelFeatureTypes",
+                [topLevelFeatureTypes: topLevelFeatureTypes])
+
+        // creating Chado feature for each Apollo feature
+        features.each { apolloFeature ->
+            startTime = System.currentTimeMillis()
+            createChadoFeaturesForAnnotation(organism, apolloFeature)
+            existingChadoAnnotations.remove(apolloFeature.uniqueName)
+            endTime = System.currentTimeMillis()
+            log.debug "Time taken to process annotation ${apolloFeature.name} of type ${apolloFeature.class.canonicalName}: ${endTime - startTime} ms"
+        }
+
+        // delete features that are in Chado but not in Apollo
+        existingChadoAnnotations.each { uniquename ->
+            org.gmod.chado.Feature chadoFeature = getChadoFeature(uniquename)
+            log.debug "Deleting ${uniquename}"
+            deleteChadoFeature(chadoFeature)
+        }
+
+        JSONObject exportStatistics = new JSONObject()
+        exportStatistics = [ "Organism count" : chadoOrganismsMap.size(),
+                             "Sequence count" : exportStatisticsMap.get("sequence_feature_count"),
+                             "Feature count" : exportStatisticsMap.get("feature_count"),
+                             "Featureloc count" : exportStatisticsMap.get("featureloc_count"),
+                             "Time Taken" : (System.currentTimeMillis() - totalTime) / 1000 + " seconds" ]
+
+        return exportStatistics
+    }
+
+    /**
+     * For each Apollo annotation, traverses through the model hierarchy and creates a Chado representation of the annotation.
+     * @param organism
+     * @param topLevelFeature
+     * @return
+     */
+    def createChadoFeaturesForAnnotation(org.bbop.apollo.Organism organism, org.bbop.apollo.Feature topLevelFeature) {
+        /*
+        Top-level features are gene, pseudogene, transposable_element, repeat_region, insertion, deletion, substitution.
+        A top-level feature that is not an instance of type Gene is likely to be a singleton feature.
+         */
+        org.gmod.chado.Feature topLevelChadoFeature = getChadoFeature(topLevelFeature.uniqueName)
+        if (topLevelChadoFeature) {
+            // if a top level annotation already exists we delete it
+            long startTime = System.currentTimeMillis()
+            deleteChadoFeature(topLevelChadoFeature)
+            long endTime = System.currentTimeMillis()
+            log.debug "Time taken to delete existing annotation ${endTime - startTime} ms"
+        }
+
+        createChadoFeature(organism, topLevelFeature)
+        if (topLevelFeature instanceof Gene) {
+            // annotation is a Gene / Pseudogene
+            def transcripts = transcriptService.getTranscripts(topLevelFeature)
+            transcripts.each { transcript ->
+                org.gmod.chado.Feature chadoFeature = createChadoFeature(organism, transcript)
+                transcript.childFeatureRelationships.each { featureRelationship ->
+                    createChadoFeatureRelationship(organism, chadoFeature, featureRelationship)
+                }
+
+                def exons = transcriptService.getSortedExons(transcript,false)
+                /*
+                In GMOD Chado Best Practices, it is noted that exons can be part_of more than one mRNA and that
+                no two distinct exon rows should have exact same featureloc coordinates (this would indicate they are the same exon).
+                TODO: Do we factor this logic into export if two exons from two separate transcripts/isoforms have same fmin and fmax?
+                */
+                exons.each { exon ->
+                        org.gmod.chado.Feature chadoExonFeature = createChadoFeature(organism, exon)
+                        exon.childFeatureRelationships.each { featureRelationship ->
+                            createChadoFeatureRelationship(organism, chadoExonFeature, featureRelationship)
+                        }
+                }
+                if (transcript instanceof MRNA) {
+                    // TODO: Do we create a chado feature for stop_codon_read_through
+                    def cds = transcriptService.getCDS(transcript)
+                    org.gmod.chado.Feature chadoCdsFeature = createChadoCdsFeature(organism, transcript, cds, configWrapperService.getChadoExportFastaForCds())
+                    cds.childFeatureRelationships.each { featureRelationship ->
+                        createChadoFeatureRelationship(organism, chadoCdsFeature, featureRelationship, "part_of")
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Queries the Chado database to find feature that has the given uniquename.
+     * @param uniqueName
+     * @return
+     */
+    def getChadoFeature(String uniqueName) {
+        org.gmod.chado.Feature chadoFeature
+        def featureResult = org.gmod.chado.Feature.findAllByUniquename(uniqueName)
+        if (featureResult.size() == 0) {
+            chadoFeature = null
+        }
+        else if (featureResult.size() == 1) {
+            chadoFeature = featureResult.get(0)
+        }
+        else {
+            log.error "${featureResult} - More than one result found for feature uniquename '${uniqueName}'. Returning null."
+            chadoFeature = null
+        }
+
+        return chadoFeature
+    }
+
+    /**
+     * Deletes a given feature from the Chado database.
+     * @param chadoFeature
+     * @return
+     */
+    def deleteChadoFeature(org.gmod.chado.Feature chadoFeature) {
+        def chadoFeatureLocs = getChadoFeatureloc(chadoFeature)
+        chadoFeatureLocs.each { fl ->
+            fl.delete()
+        }
+
+        def chadoFeatureDbxrefs = getChadoFeatureDbxrefs(chadoFeature)
+        chadoFeatureDbxrefs.each { fd ->
+            if (! ontologyDb.contains(fd.dbxref.db.name)) {
+                fd.dbxref.delete()
+            }
+        }
+
+        def chadoFeatureProperties = getChadoFeatureProps(chadoFeature)
+        chadoFeatureProperties.each { fp ->
+            fp.delete()
+        }
+
+        def chadoChildFeatureRelationships = getChildFeatureRelationships(chadoFeature)
+        chadoChildFeatureRelationships.each { child ->
+            deleteChadoFeature(child.subject)
+        }
+
+        // TODO: featureSynonyms, featurePubs, featureGenotypes, featurePhenotypes
+        chadoFeature.delete(flush: true)
+    }
+
+    /**
+     * Queries the Chado database and returns all featurelocs for a given Chado Feature
+     * @param chadoFeature
+     * @return
+     */
+    def getChadoFeatureloc(org.gmod.chado.Feature chadoFeature) {
+        long startTime = System.currentTimeMillis()
+        def results = org.gmod.chado.Featureloc.executeQuery(
+                        "SELECT DISTINCT fl FROM org.gmod.chado.Featureloc fl WHERE fl.feature.uniquename = :queryUniqueName",
+                        [queryUniqueName: chadoFeature.uniquename]
+        )
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken to query featurelocs for ChadoFeature: ${chadoFeature.uniquename} - ${endTime - startTime} ms"
+        return results
+    }
+
+    /**
+     * Queries the Chado database and returns all FeatureDbxrefs for a given Chado Feature
+     * @param chadoFeature
+     * @return
+     */
+    def getChadoFeatureDbxrefs(org.gmod.chado.Feature chadoFeature) {
+        long startTime = System.currentTimeMillis()
+        def results = org.gmod.chado.FeatureDbxref.executeQuery(
+                        "SELECT DISTINCT fd FROM org.gmod.chado.FeatureDbxref fd JOIN fd.dbxref dbxref JOIN dbxref.db db WHERE fd.feature.uniquename = :queryUniqueName",
+                        [queryUniqueName: chadoFeature.uniquename]
+        )
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken to query featureDbxrefs for ChadoFeature: ${chadoFeature.uniquename} - ${endTime - startTime} ms"
+        return results
+    }
+
+    /**
+     * Queries the Chado database and returns all FeatureProps for a given Chado Feature
+     * @param chadoFeature
+     * @return
+     */
+    def getChadoFeatureProps(org.gmod.chado.Feature chadoFeature) {
+        long startTime = System.currentTimeMillis()
+        def results = org.gmod.chado.Featureprop.executeQuery(
+                        "SELECT DISTINCT fp FROM org.gmod.chado.Featureprop fp JOIN fp.feature f WHERE f.uniquename = :queryUniqueName",
+                        [queryUniqueName: chadoFeature.uniquename]
+        )
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken to query featureprops for ChadoFeature: ${chadoFeature.uniquename} - ${endTime - startTime} ms"
+        return results
+    }
+
+    /**
+     * Queries the Chado database and retuns all child feature relationships for a given Chado Feature
+     * @param chadoFeature
+     * @return
+     */
+    def getChildFeatureRelationships(org.gmod.chado.Feature chadoFeature) {
+        long startTime = System.currentTimeMillis()
+        def results = org.gmod.chado.FeatureRelationship.executeQuery(
+                        "SELECT DISTINCT fr FROM org.gmod.chado.FeatureRelationship fr JOIN fr.subject s JOIN fr.object o WHERE o.uniquename = :queryUniqueName",
+                        [queryUniqueName: chadoFeature.uniquename]
+        )
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken to query feature_relationships for ChadoFeature: ${chadoFeature.uniquename} - ${endTime - startTime} ms"
+        return results
+    }
+
+    /**
+     * Create a Chado feature for a given Apollo feature.
+     * @param Organism
+     * @param feature
+     * @return
+     */
+    def createChadoFeature(org.bbop.apollo.Organism organism, org.bbop.apollo.Feature feature) {
+        long startTime, endTime
+        String type = feature.hasProperty('alternateCvTerm') ? feature.alternateCvTerm : feature.cvTerm
+
+        // feature
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Feature chadoFeature = new org.gmod.chado.Feature(
+                uniquename: feature.uniqueName,
+                name: feature.name,
+                seqlen: feature.sequenceLength,
+                md5checksum: feature.md5checksum,
+                isAnalysis: feature.isAnalysis,
+                isObsolete: feature.isObsolete,
+                timeaccessioned: feature.dateCreated,
+                timelastmodified: feature.lastUpdated,
+                organism: chadoOrganismsMap.get(organism.commonName),
+                type: getChadoCvterm(type, SEQUENCE_ONTOLOGY)
+        ).save()
+        exportStatisticsMap['feature_count'] += 1
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado feature of type ${feature.class.simpleName}: ${endTime - startTime} ms"
+
+        // featureloc
+        createChadoFeatureloc(organism, chadoFeature, feature)
+
+        /*
+         TODO: Cannot treat feature.symbol as a Chado synonym
+         because feature -> synonym link requires a Publication, according to Chado specification
+        */
+        //if (feature.symbol) {
+        //    org.gmod.chado.Synonym chadoSynonym = getChadoSynonym(feature.symbol, "symbol")
+        //
+        //    // Creating a linking relationship between Chado feature and Chado synonym
+        //    org.gmod.chado.FeatureSynonym chadoFeatureSynonym = new org.gmod.chado.FeatureSynonym(
+        //            feature: chadoFeature,
+        //            synonym: chadoSynonym,
+        //            pub: null,
+        //            isCurrent: true,
+        //            isInternal: false
+        //    ).save()
+        //}
+
+        // As an alternative, feature.symbol is treated as Chado featureprop
+        if (feature.symbol) {
+            org.gmod.chado.Featureprop chadoFeatureprop = new org.gmod.chado.Featureprop(
+                    value: feature.symbol,
+                    rank: 0,
+                    feature: chadoFeature,
+                    type: getChadoCvterm("symbol", FEATURE_PROPERTY)
+            ).save()
+        }
+
+        // Feature description treated as Chado featureprop
+        if (feature.description) {
+            org.gmod.chado.Featureprop chadoFeatureProp = new org.gmod.chado.Featureprop(
+                    value: feature.description,
+                    rank: 0,
+                    feature: chadoFeature,
+                    type: getChadoCvterm("description", FEATURE_PROPERTY)
+            ).save()
+        }
+
+        /* TODO: How to determine the primary dbxref?
+         Currently the feature.dbxref_id will remain empty as we do not know which of
+         the dbxref will serve as the primary identifier.
+         Chado specification suggests using feature.dbxref_id to link to primary
+         identifier and to use feature_dbxref table for all additional identifiers.
+        */
+
+        // dbxref
+        if (feature.featureDBXrefs) {
+            createChadoDbxref(chadoFeature, feature.featureDBXrefs)
+        }
+
+        // properties
+        if (feature.featureProperties) {
+            createChadoProperty(chadoFeature, feature.featureProperties)
+        }
+
+        // Feature owner treated as featureprop
+        if (feature.owners) {
+            int rank = 0
+            feature.owners.each { owner ->
+                org.gmod.chado.Featureprop chadoFeatureProp = new org.gmod.chado.Featureprop(
+                        value: owner.username,
+                        rank: rank,
+                        feature: chadoFeature,
+                        type: getChadoCvterm("owner", FEATURE_PROPERTY)
+                ).save()
+                rank++
+            }
+        }
+
+        // publications
+        //if (feature.featurePublications) {
+        //    createChadoFeaturePub(chadoFeature, feature.featurePublications)
+        //}
+
+        // synonyms
+        //if (feature.featureSynonyms) {
+        //    createChadoSynonym(chadoFeature, feature.featureSynonyms)
+        //}
+
+        // genotypes - TODO: When Apollo has Genotype associated with annotations
+        //if (feature.featureGenotypes) {
+        //    createChadoGenotype(chadoFeature, feature.featureGenotypes)
+        //}
+
+        // phenotypes - TODO: When Apollo has Phenotype associated with annotations
+        //if (feature.featurePhenotypes) {
+        //    createChadoPhenotype(chadoFeature, feature.featurePhenotypes)
+        //}
+
+        //feature.featureLocations - TODO: If Apollo has features with multiple Feature Locations
+        //feature.featureCVTerms - TODO: If Apollo has features with multiple CvTerms
+
+        chadoFeature.save(flush: true)
+
+        return chadoFeature
+    }
+
+    /**
+     * Create an instance of Chado featureloc, for a given Chado feature, with location information
+     * from an Apollo feature location.
+     * @param chadoFeature
+     * @param feature
+     * @return chadoFeatureLoc
+     */
+    def createChadoFeatureloc(org.bbop.apollo.Organism organism, org.gmod.chado.Feature chadoFeature, org.bbop.apollo.Feature feature) {
+        /*
+         In Chado, locgroup and rank are used to uniquely identify featureloc for features that
+         have more than one featureloc.
+         In Apollo, we currently do not use locgroup and rank for any purposes and their values
+         are set to 0, the default, as suggested by standard Chado specification.
+         */
+
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Featureloc chadoFeatureLoc = new org.gmod.chado.Featureloc(
+                fmin: feature.featureLocation.fmin,
+                fmax: feature.featureLocation.fmax,
+                isFminPartial: feature.featureLocation.isFminPartial,
+                isFmaxPartial: feature.featureLocation.isFmaxPartial,
+                strand: feature.featureLocation.strand,
+                locgroup: feature.featureLocation.locgroup,
+                rank: feature.featureLocation.rank,
+                feature: chadoFeature,
+                srcfeature: getSrcFeatureForFeature(organism, feature.featureLocation.sequence)
+        ).save(flush: true)
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado featureloc for feature fmin: ${feature.fmin} fmax: ${feature.fmax}: ${endTime - startTime} ms"
+        exportStatisticsMap['featureloc_count'] += 1
+        feature.featureLocation.featureLocationPublications.each { featureLocationPublication ->
+            createChadoFeaturelocPub(chadoFeatureLoc, featureLocationPublication)
+        }
+        return chadoFeatureLoc
+    }
+
+    /**
+     * Create an instance of Chado feature of type 'CDS' for a given Apollo CDS.
+     * @param organism
+     * @param transcript
+     * @param cds
+     * @param storeSequence
+     * @return chadoCdsFeature
+     */
+    def createChadoCdsFeature(org.bbop.apollo.Organism organism, org.bbop.apollo.Transcript transcript, org.bbop.apollo.CDS cds, boolean storeSequence = false) {
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Feature chadoCdsFeature = new org.gmod.chado.Feature(
+                uniquename: cds.uniqueName,
+                name: cds.name,
+                isAnalysis: cds.isAnalysis,
+                isObsolete: cds.isObsolete,
+                timeaccessioned: cds.dateCreated,
+                timelastmodified: cds.lastUpdated,
+                organism: chadoOrganismsMap.get(organism.commonName),
+                type: getChadoCvterm(cds.cvTerm, SEQUENCE_ONTOLOGY)
+        )
+        if (storeSequence) {
+            String sequence = cdsService.getResiduesFromCDS(cds)
+            chadoCdsFeature.residues = sequence
+            chadoCdsFeature.seqlen = sequence.length()
+            chadoCdsFeature.md5checksum = generateMD5checksum(sequence)
+        }
+
+        chadoCdsFeature.save()
+        exportStatisticsMap['feature_count'] += 1
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado feature of CDS ${cds.uniqueName}: ${endTime - startTime} ms"
+        createChadoFeatureloc(organism, chadoCdsFeature, cds)
+        return chadoCdsFeature
+    }
+
+    /**
+     * Create an instance of Chado feature of type 'polypeptide' from a given Apollo CDS and also
+     * store its amino acid sequence.
+     * @param organism
+     * @param transcript
+     * @param cds
+     * @param storeSequence
+     * @return chadoPolyPeptideFeature
+     */
+    def createChadoPolypeptide(org.bbop.apollo.Organism organism, org.bbop.apollo.Transcript transcript, org.bbop.apollo.CDS cds, boolean storeSequence = false) {
+        long startTime, endTime
+        Timestamp timestamp = generateTimeStamp()
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Feature chadoPolypeptideFeature = new org.gmod.chado.Feature(
+                uniquename: generateUniqueName(),
+                name: transcript.name + "-pep",
+                isAnalysis: true,
+                isObsolete: false,
+                timeaccessioned: timestamp,
+                timelastmodified: timestamp,
+                organism: chadoOrganismsMap.get(organism.commonName),
+                type: getChadoCvterm("polypeptide", SEQUENCE_ONTOLOGY)
+        )
+
+        if (storeSequence) {}
+
+        chadoPolypeptideFeature.save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado feature of Polypeptide for Transcript ${transcript.name}: ${endTime - startTime} ms"
+        createChadoFeatureloc(organism, chadoPolypeptideFeature, cds)
+        return chadoPolypeptideFeature
+    }
+
+    /**
+     * Checks if there is a Chado feature representation for a given Apollo Sequence.
+     * @param sequence
+     * @return
+     */
+    def getSrcFeatureForFeature(org.bbop.apollo.Organism organism, org.bbop.apollo.Sequence sequence) {
+        org.gmod.chado.Feature srcFeature
+        long startTime = System.currentTimeMillis()
+        def sequenceFeatureResult = org.gmod.chado.Feature.executeQuery(
+                "SELECT DISTINCT sf FROM org.gmod.chado.Feature sf WHERE sf.name = :querySequenceName AND sf.organism.genus = :queryGenus AND sf.organism.species = :querySpecies AND sf.type.name = :querySequenceType",
+                [querySequenceName: sequence.name, queryGenus: organism.genus, querySpecies: organism.species, querySequenceType: "chromosome"])
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken for querying for sequence ${sequence.name} of organism ${organism.genus} ${organism.species}: ${endTime - startTime} ms"
+
+        if (sequenceFeatureResult.size() == 0) {
+            srcFeature = createChadoFeatureForSequence(organism, sequence, configWrapperService.getChadoExportFastaForSequence())
+        }
+        else if (sequenceFeatureResult.size() == 1) {
+            srcFeature = sequenceFeatureResult.get(0)
+        }
+        else {
+            log.error "${sequenceFeatureResult} - More than one result found for sequence name '${sequence.name}'. Returning null."
+            srcFeature = null
+        }
+        return srcFeature
+    }
+
+    /**
+     * Creates an instance of Chado FeaturelocPub and creates a linking relationship between Chado feature and Chado pub.
+     * @param chadoFeatureloc
+     * @param featureLocationPublication
+     * @return
+     */
+    def createChadoFeaturelocPub(org.gmod.chado.Featureloc chadoFeatureloc, org.bbop.apollo.Publication featureLocationPublication) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(featureLocationPublication)
+        org.gmod.chado.FeaturelocPub chadoFeaturelocPub = new org.gmod.chado.FeaturelocPub(
+                featureloc: chadoFeatureloc,
+                pub: chadoPublication
+        ).save()
+        return chadoFeaturelocPub
+    }
+
+    /**
+     * Creates an instance of Chado feature_relationship for a given Apollo FeatureRelationship.
+     * Default relationship type is 'part_of'.
+     * @param organism
+     * @param feature
+     * @param featureRelationship
+     * @param relationshipType
+     * @return chadoFeatureRelationship
+     */
+    def createChadoFeatureRelationship(org.bbop.apollo.Organism organism, org.gmod.chado.Feature feature, org.bbop.apollo.FeatureRelationship featureRelationship, String relationshipType = "part_of") {
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.FeatureRelationship chadoFeatureRelationship = new org.gmod.chado.FeatureRelationship(
+                subject: feature,
+                object: getChadoFeature(featureRelationship.parentFeature.uniqueName),
+                value: featureRelationship.value,
+                rank: featureRelationship.rank,
+                type: getChadoCvterm(relationshipType, RELATIONSHIP_ONTOLOGY)
+        ).save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado feature_relationship ${feature.class.simpleName} ${relationshipType} ${featureRelationship.parentFeature.class.simpleName}: ${endTime - startTime} ms"
+
+        featureRelationship.featureRelationshipPublications.each { featureRelationshipPublication ->
+            createChadoFeatureRelationshipPublication(chadoFeatureRelationship, featureRelationshipPublication)
+        }
+
+        featureRelationship.featureRelationshipProperties.each { featureRelationshipProperty ->
+            createChadoFeatureRelationshipProperty(chadoFeatureRelationship, featureRelationshipProperty)
+        }
+        return chadoFeatureRelationship
+    }
+
+    /**
+     * Creates a Chado feature_relationship_pub for a Chado feature_relationship based on a given Apollo Publication.
+     * @param chadoFeatureRelationship
+     * @param publication
+     * @return
+     */
+    def createChadoFeatureRelationshipPublication(org.gmod.chado.FeatureRelationship chadoFeatureRelationship, org.bbop.apollo.Publication publication) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(publication)
+        org.gmod.chado.FeatureRelationshipPub chadoFeatureRelationshipPub = new org.gmod.chado.FeatureRelationshipPub(
+                featureRelationship: chadoFeatureRelationship,
+                pub: chadoPublication
+        ).save()
+        return chadoFeatureRelationshipPub
+    }
+
+    /**
+     * Creates a Chado feature_relationshipprop for a Chado feature_relationship based on a given Apollo FeatureProperty
+     * @param chadoFeatureRelationship
+     * @param featureProperty
+     * @return
+     */
+    def createChadoFeatureRelationshipProperty(org.gmod.chado.FeatureRelationship chadoFeatureRelationship, org.bbop.apollo.FeatureProperty featureProperty) {
+        org.gmod.chado.FeatureRelationshipprop chadoFeatureRelationshipprop = new org.gmod.chado.FeatureRelationshipprop(
+                featureRelationship: chadoFeatureRelationship,
+                rank: featureProperty.rank,
+                value: featureProperty.value,
+                type: getChadoCvterm(featureProperty.type.name, FEATURE_PROPERTY)
+        ).save()
+
+        featureProperty.featurePropertyPublications.each { featurePropertyPublication ->
+            createChadoFeatureRelationshipPropertyPublication(chadoFeatureRelationshipprop, featurePropertyPublication)
+        }
+        return chadoFeatureRelationshipprop
+    }
+
+    /**
+     * Creates a Chado feature_relationshipprop_pub for a Chado feature_relationshipprop based on a given Apollo Publication
+     * @param chadoFeatureRelationshipprop
+     * @param featurePropertyPublication
+     * @return
+     */
+    def createChadoFeatureRelationshipPropertyPublication(org.gmod.chado.FeatureRelationshipprop chadoFeatureRelationshipprop, org.bbop.apollo.Publication featurePropertyPublication) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(featurePropertyPublication)
+        org.gmod.chado.FeatureRelationshippropPub chadoFeatureRelationshippropPub = new org.gmod.chado.FeatureRelationshippropPub(
+                featureRelationshipprop: chadoFeatureRelationshipprop,
+                pub: chadoPublication
+        ).save()
+        return chadoFeatureRelationshippropPub
+    }
+
+    /**
+     * Wrapper for createChadoSynonym() for handling multiple featureSynonyms.
+     * @param chadoFeature
+     * @param featureSynonyms
+     * @return
+     */
+    def createChadoSynonym(org.gmod.chado.Feature chadoFeature, Set<org.bbop.apollo.FeatureSynonym> featureSynonyms) {
+        featureSynonyms.each { featureSynonym ->
+            createChadoSynonym(chadoFeature, featureSynonym)
+        }
+    }
+
+    /**
+     *
+     * @param synonym
+     * @return
+     */
+    def getChadoSynonym(org.bbop.apollo.Synonym synonym) {
+        org.gmod.chado.Synonym chadoSynonym
+        def synonymResult = org.gmod.chado.Synonym.executeQuery(
+                "SELECT DISTINCT s FROM org.gmod.chado.Synonym s JOIN s.type c WHERE s.name = :querySynonym AND c.name = :queryCvterm",
+                [querySynonym: synonym.name, queryCvterm: synonym.type.name])
+        if (synonymResult.size() == 0) {
+            chadoSynonym = createChadoSynonym(synonym.name)
+        }
+        else if (synonymResult.size() == 1) {
+            chadoSynonym = synonymResult.get(0)
+        }
+        else {
+            log.error "${synonymResult} - More than one result found for synonym '${synonym.name}'of type '${synonym.type.name}'. Returning null."
+            chadoSynonym = null
+        }
+        return chadoSynonym
+    }
+
+    /**
+     *
+     * @param synonymName
+     * @param synonymType
+     * @return
+     */
+    def getChadoSynonym(String synonymName, String synonymType) {
+        org.gmod.chado.Synonym chadoSynonym
+        def synonymResult = org.gmod.chado.Synonym.executeQuery(
+                "SELECT DISTINCT s FROM org.gmod.chado.Synonym s JOIN s.type c WHERE s.name = :querySynonym AND c.name = :queryCvterm",
+                [querySynonym: synonymName, queryCvterm: synonymType])
+        if (synonymResult.size() == 0) {
+            chadoSynonym = createChadoSynonym(synonymName)
+        }
+        else if (synonymResult.size() == 1) {
+            chadoSynonym = synonymResult.get(0)
+        }
+        else {
+            log.error "${synonymResult} - More than one result found for synonym '${synonymName}'of type '${synonymType}'. Returning null."
+            chadoSynonym = null
+        }
+        return chadoSynonym
+    }
+
+    /**
+     * Creates an instance of Chado synonym for a given Apollo FeatureSynonym and creates a linking relationship between
+     * Chado feature and Chado synonym via feature_synonym.
+     * @param chadoFeature
+     * @param featureSynonym
+     * @return chadoSynonym
+     */
+    def createChadoSynonym(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.FeatureSynonym featureSynonym) {
+        org.gmod.chado.Synonym chadoSynonym = createChadoSynonym(featureSynonym.synonym)
+
+        //Publications from FeatureSynonym
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(featureSynonym.publication)
+
+        org.gmod.chado.FeatureSynonym chadoFeatureSynonym = new org.gmod.chado.FeatureSynonym(
+                feature: chadoFeature,
+                synonym: chadoSynonym,
+                pub: chadoPublication,
+                isCurrent: featureSynonym.isCurrent,  // default
+                isInternal: featureSynonym.isInternal // default
+        ).save()
+
+        return chadoSynonym
+    }
+
+    /**
+     * Creates an instance of Chado synonym for a given Apollo Synonym
+     * @param synonymName
+     * @param synonymType
+     * @return
+     */
+    def createChadoSynonym(String synonymName) {
+        org.gmod.chado.Synonym chadoSynonym = new org.gmod.chado.Synonym(
+                name: synonymName,
+                synonymSgml: synonymName,
+                type: getChadoCvterm("synonym", FEATURE_PROPERTY)
+        ).save()
+
+        return chadoSynonym
+    }
+
+    /**
+     * Wrapper for createChadoFeaturePub() for handling multiple publications.
+     * @param chadoFeature
+     * @param publications
+     * @return
+     */
+    def createChadoFeaturePub(org.gmod.chado.Feature chadoFeature, Set<org.bbop.apollo.Publication> publications) {
+        publications.each { publication ->
+            createChadoFeaturePub(chadoFeature, publication)
+        }
+    }
+
+    /**
+     * Creates an instance of Chado pub for a given Apollo Publication and creates a linking relationship between
+     * Chado feature and Chado pub via Chado feature_pub.
+     * @param chadoFeature
+     * @param publication
+     */
+    def createChadoFeaturePub(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.Publication publication) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(publication)
+
+        // Linking feature to publication via feature_pub
+        org.gmod.chado.FeaturePub chadoFeaturePub = new org.gmod.chado.FeaturePub(
+                feature: chadoFeature,
+                pub: chadoPublication
+        ).save()
+
+        return chadoPublication
+    }
+
+    /**
+     * Wrapper for createChadoProperty() for handling multiple FeatureProperties
+     * @param chadoFeature
+     * @param featureProperties
+     * @return
+     */
+    def createChadoProperty(org.gmod.chado.Feature chadoFeature, Set<org.bbop.apollo.FeatureProperty> featureProperties) {
+        int commentRank = 0
+        featureProperties.each { featureProperty ->
+            if (featureProperty instanceof org.bbop.apollo.Comment) {
+                createChadoFeaturePropertyForComment(chadoFeature, featureProperty, commentRank)
+                commentRank++;
+            }
+            else {
+                createChadoProperty(chadoFeature, featureProperty)
+            }
+
+        }
+    }
+
+    /**
+     * Creates an instance of Chado featureprop for an Apollo FeatureProperty.
+     * @param chadoFeature
+     * @param featureProperty
+     */
+    def createChadoProperty(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.FeatureProperty featureProperty) {
+        long startTime, endTime
+        String type = "feature_property"
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Featureprop chadoFeatureProp = new org.gmod.chado.Featureprop(
+                value: featureProperty.value,
+                rank: featureProperty.rank,
+                feature: chadoFeature,
+                type: getChadoCvterm(type, FEATURE_PROPERTY)
+        ).save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado featureprop of type '${type}' and value ${featureProperty.value}: ${endTime - startTime} ms"
+
+        featureProperty.featurePropertyPublications.each { featurePropertyPublication ->
+            createChadoPropertyPub(chadoFeatureProp, featurePropertyPublication)
+        }
+        return chadoFeatureProp
+    }
+
+    /**
+     * Creates an instance of Chado featureprop for a given Apollo FeatureProperty of type 'comment'.
+     * @param chadoFeature
+     * @param comment
+     * @param rank
+     * @return
+     */
+    def createChadoFeaturePropertyForComment(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.FeatureProperty comment, int rank) {
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Featureprop chadoFeatureProp = new org.gmod.chado.Featureprop(
+                value: comment.value,
+                rank: rank,
+                feature: chadoFeature,
+                type: getChadoCvterm("comment", FEATURE_PROPERTY)
+        ).save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado featureprop of type 'comment' and value ${comment.value}: ${endTime - startTime} ms"
+        return chadoFeatureProp
+    }
+
+    /**
+     * Creates an instance of Chado pub for a given Apollo publication and creates a linking relationship
+     * beteen Chado featureprop and Chado pub via Chado featureprop_pub.
+     * @param chadoFeatureProp
+     * @param publication
+     */
+    def createChadoPropertyPub(org.gmod.chado.Featureprop chadoFeatureProp, org.bbop.apollo.Publication publication) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(publication)
+        org.gmod.chado.FeaturepropPub featurePropertyPublication = new org.gmod.chado.FeaturepropPub(
+                featureprop: chadoFeatureProp,
+                pub: chadoPublication
+        ).save()
+        return featurePropertyPublication
+    }
+
+    /**
+     * Experimental
+     * @param cvTerm
+     */
+    def createChadoCvterm(org.bbop.apollo.CVTerm cvTerm) {
+        // TODO: Must be careful in creating and setting all properties
+        org.gmod.chado.Cv chadoCv = new org.gmod.chado.Cv(
+                name: cvTerm.cv.name,
+                definition: cvTerm.cv.definition
+        ).save()
+
+        org.gmod.chado.Db chadoDb = new org.gmod.chado.Db(
+                name: cvTerm.dbxref.db.name,
+                description: cvTerm.dbxref.db.description,
+                urlprefix: cvTerm.dbxref.db.urlPrefix,
+                url: cvTerm.dbxref.db.url
+        ).save()
+
+        org.gmod.chado.Dbxref chadoDbxref = new org.gmod.chado.Dbxref(
+                accession: cvTerm.dbxref.accession,
+                description: cvTerm.dbxref.description,
+                version: cvTerm.dbxref.version,
+                db: chadoDb
+        ).save()
+
+        org.gmod.chado.Cvterm chadoCvterm = new org.gmod.chado.Cvterm(
+                name: cvTerm.name,
+                definition: cvTerm.definition,
+                isObsolete: cvTerm.isObsolete,
+                isRelationshiptype: cvTerm.isRelationshipType,
+                cv: chadoCv,
+                dbxref: chadoDbxref,
+        ).save()
+
+        org.gmod.chado.CvtermDbxref chadoCvTermDbxref = new org.gmod.chado.CvtermDbxref(
+                cvTerm: chadoCvterm,
+                dbxref: chadoDbxref,
+                isForDefinition: 0 // TODO: How to set this attribute for new Cvterm from Apollo?
+        ).save()
+
+        org.gmod.chado.CvtermRelationship chadoCvTermRelationship = new org.gmod.chado.CvtermRelationship(
+
+        )
+
+        //cvTerm.childCVTermRelationships
+        //cvTerm.parentCVTermRelationships
+    }
+
+    /**
+     *
+     * @param dbxref
+     * @return
+     */
+    def getChadoDbxref(org.bbop.apollo.DBXref dbxref) {
+        org.gmod.chado.Dbxref chadoDbxref
+        def dbxrefResult
+        if (dbxref.version == null) {
+            dbxrefResult = org.gmod.chado.Dbxref.executeQuery(
+                    "SELECT DISTINCT d FROM org.gmod.chado.Dbxref d JOIN d.db db WHERE d.accession = :queryDbxref AND db.name = :queryDb",
+                    [queryDbxref: dbxref.accession, queryDb: dbxref.db.name])
+        }
+        else {
+            dbxrefResult = org.gmod.chado.Dbxref.executeQuery(
+                    "SELECT DISTINCT d FROM org.gmod.chado.Dbxref d JOIN d.db db WHERE d.accession = :queryDbxref AND d.version = :queryDbxrefVersion AND db.name = :queryDb",
+                    [queryDbxref: dbxref.accession, queryDbxrefVersion: dbxref.version, queryDb: dbxref.db.name])
+        }
+
+        if (dbxrefResult.size() == 0) {
+            chadoDbxref = createChadoDbxref(dbxref)
+        }
+        else if (dbxrefResult.size() == 1) {
+            chadoDbxref = dbxrefResult.get(0)
+        }
+        else {
+            log.error "${dbxrefResult} - More than one result found for dbxref '${dbxref.db.name}:${dbxref.accession}'. Returning null."
+            chadoDbxref = null
+        }
+        return chadoDbxref
+    }
+
+    /**
+     * A wrapper for createChadoDbxref() for handling multiple dbxrefs
+     * @param chadoFeature
+     * @param dbxrefs
+     * @return
+     */
+    def createChadoDbxref(org.gmod.chado.Feature chadoFeature, Set<org.bbop.apollo.DBXref> dbxrefs) {
+        dbxrefs.each { dbxref ->
+            createChadoDbxref(chadoFeature, dbxref)
+        }
+    }
+
+    /**
+     * Creates an instance of Chado Dbxref, creates a linking relationship between Chado Feature and
+     * Chado Dbxref via Chado feature_dbxref and creates Chado dbxrefprop.
+     * @param chadoFeature
+     * @param dbxref
+     * @return
+     */
+    def createChadoDbxref(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.DBXref dbxref) {
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Dbxref chadoDbxref = getChadoDbxref(dbxref)
+        // create feature_dbxref relationship
+        org.gmod.chado.FeatureDbxref chadoFeatureDbxref = new org.gmod.chado.FeatureDbxref(
+                feature: chadoFeature,
+                dbxref: chadoDbxref,
+                isCurrent: true
+        ).save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado dbxref for ${dbxref.db.name}:${dbxref.accession} : ${endTime - startTime} ms"
+        // dbxref properties
+
+        dbxref.dbxrefProperties.each { dbxrefProperty ->
+            createChadoDbxrefProp(chadoDbxref, dbxrefProperty)
+        }
+        return chadoDbxref
+    }
+
+    /**
+     * Creates an instance of Chado db for a given Apollo DB.
+     * @param db
+     * @return
+     */
+    def createChadoDb(org.bbop.apollo.DB db) {
+        long startTime, endTime
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Db chadoDb = new org.gmod.chado.Db(
+                name: db.name,
+                description: db.description,
+                urlprefix: db.urlPrefix,
+                url: db.url
+        ).save()
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado Db for ${db.name} : ${endTime - startTime} ms"
+        return chadoDb
+    }
+
+    /**
+     *
+     * @param dbxref
+     * @return
+     */
+    def createChadoDbxref(org.bbop.apollo.DBXref dbxref) {
+        // create Chado db for dbxref
+        org.gmod.chado.Db db = getChadoDb(dbxref.db)
+
+        // create Chado dbxref
+        org.gmod.chado.Dbxref chadoDbxref = new org.gmod.chado.Dbxref(
+                accession: dbxref.accession,
+                description: dbxref.description,
+                db: db
+        )
+        if (dbxref.version != null) {
+            chadoDbxref.version = Integer.getInteger(dbxref.version)
+        }
+        chadoDbxref.save()
+
+        dbxref.dbxrefProperties.each { dbxrefProperty ->
+            createChadoDbxrefProp(chadoDbxref, dbxrefProperty)
+        }
+
+        return chadoDbxref
+    }
+
+    /**
+     * Create an instance of Chado dbxref_prop for a given instance of Apollo DBXrefProperty.
+     * @param chadoDbxref
+     * @param dbXrefProperty
+     * @return
+     */
+    def createChadoDbxrefProp(org.gmod.chado.Dbxref chadoDbxref, org.bbop.apollo.DBXrefProperty dbXrefProperty) {
+        org.gmod.chado.Dbxrefprop chadoDbxrefProp = new org.gmod.chado.Dbxrefprop(
+                dbxref: chadoDbxref,
+                type: getChadoCvterm(dbXrefProperty.type.name),
+                value: dbXrefProperty.value,
+                rank: dbXrefProperty.rank
+        ).save()
+        return chadoDbxrefProp
+    }
+
+    /**
+     * wrapper for createChadoFeatureCvTerm()
+     * @param chadoFeature
+     * @param featureCVTerms
+     * @return
+     */
+    def createChadoFeatureCvterm(org.gmod.chado.Feature chadoFeature, Set<org.bbop.apollo.FeatureCVTerm> featureCVTerms) {
+        featureCVTerms.each { featureCvterm ->
+            createChadoFeatureCvterm(chadoFeature, featureCvterm)
+        }
+    }
+
+    /**
+     * Creates an instance of Chado feature_cvterm for a given Apollo FeatureCVTerm.
+     * @param chadoFeature
+     * @param featureCvterm
+     * @return
+     */
+    def createChadoFeatureCvterm(org.gmod.chado.Feature chadoFeature, org.bbop.apollo.FeatureCVTerm featureCvterm) {
+        org.gmod.chado.Pub chadoPublication = createChadoPublication(featureCvterm.publication)
+        org.gmod.chado.FeatureCvterm chadoFeatureCvterm = new org.gmod.chado.FeatureCvterm(
+                feature: chadoFeature,
+                cvterm: getChadoCvterm(featureCvterm.cvterm.name),
+                pub: chadoPublication,
+                rank: featureCvterm.rank,
+                isNot: featureCvterm.isNot
+        ).save()
+        return chadoFeatureCvterm
+    }
+
+    /**
+     * Queries the database and returns an instance of Chado pub.
+     * If the query returns no result then creates a Chado pub.
+     * @param publication
+     * @return
+     */
+    def getChadoPublication(org.bbop.apollo.Publication publication) {
+        org.gmod.chado.Pub chadoPublication
+        def pubResult = org.gmod.chado.Pub.executeQuery(
+                "SELECT DISTINCT p FROM org.gmod.chado.Pub p JOIN p.type c WHERE p.id = :queryPublicationId AND c.name = :queryPublicationType",
+                [queryPublicationId: publication.uniqueName, queryPublicationType: publication.type.name])
+
+        if (pubResult.size() == 0) {
+            chadoPublication = createChadoPublication(publication)
+        }
+        else if (pubResult.size() == 1) {
+            chadoPublication = pubResult.get(0)
+        }
+        else {
+            log.error "${pubResult} - More than one result found for publication '${publication.uniqueName}' of type '${publication.type.name}'. Returning null."
+            chadoPublication = null
+        }
+        return chadoPublication
+    }
+
+    /**
+     * Creates an instance of Chado pub for a given Apollo Publication.
+     * @param publication
+     * @return
+     */
+    def createChadoPublication(org.bbop.apollo.Publication publication) {
+        org.gmod.chado.Pub chadoPublication
+        chadoPublication = new org.gmod.chado.Pub(
+                uniquename: publication.uniqueName,
+                title: publication.title,
+                volumetitle: publication.volumeTitle,
+                volume: publication.volume,
+                seriesName: publication.seriesName,
+                issue: publication.issue,
+                pyear: publication.publicationYear,
+                pages: publication.pages,
+                miniref: publication.miniReference,
+                isObsolete: publication.isObsolete,
+                publisher: publication.publisher,
+                pubplace: publication.publicationPlace,
+                type: getChadoCvterm(publication.type.name)
+        ).save()
+
+        // TODO - Chado pubauthor
+        publication.publicationAuthors.each { publicationAuthor ->
+            org.gmod.chado.Pubauthor chadoPubAuthor = new org.gmod.chado.Pubauthor(
+                    pub: chadoPublication,
+                    rank: publicationAuthor.rank,
+                    editor: publicationAuthor.editor,
+                    givennames: publicationAuthor.givenNames,
+                    surname: publicationAuthor.surname,
+                    suffix: publicationAuthor.suffix
+            ).save()
+        }
+
+        /* Cannot set Chado pubprop as Apollo Publication does not have Publication Properties. */
+
+        // Chado pub_dbxref
+        publication.publicationDBXrefs.each { publicationDbxref ->
+            // Chado dbxref
+            org.gmod.chado.Dbxref chadoDbxref = getChadoDbxref(publicationDbxref.dbxref)
+
+            // Chado pub_dbxref
+            org.gmod.chado.PubDbxref chadoPubDbxref = new org.gmod.chado.PubDbxref(
+                    isCurrent: publicationDbxref.isCurrent,
+                    pub: chadoPublication,
+                    dbxref: chadoDbxref
+            ).save()
+        }
+
+        // Chado pub_relationship
+        publication.childPublicationRelationships.each { publicationRelationship ->
+            org.gmod.chado.PubRelationship chadoPubRelationship = new org.gmod.chado.PubRelationship(
+                    subject: chadoPublication,
+                    cvterm: getChadoCvterm(publicationRelationship.type.name)
+            )
+            org.gmod.chado.Pub objectPublication = getChadoPublication(publicationRelationship.objectPublication)
+            chadoPubRelationship.object = objectPublication
+            chadoPubRelationship.save()
+        }
+        return chadoPublication
+    }
+
+    /**
+     * A wrapper for createChadoFeatureForSequence() for handling multiple sequences
+     * @param sequences
+     * @return
+     */
+    def createChadoFeatureForSequences(Organism organism, Collection<Sequence> sequences, boolean storeSequence = false) {
+        org.gmod.chado.Feature chadoFeature
+        sequences.each { sequence ->
+            chadoFeature = createChadoFeatureForSequence(organism, sequence, storeSequence)
+        }
+        chadoFeature.save(flush: true)
+    }
+
+    /**
+     * Creates a Feature instance for an Apollo Sequence instance.
+     * Optionally, also stores the residues (default is true).
+     * @param organism
+     * @param sequence
+     * @param storeSequence
+     * @return chadoFeature
+     */
+    def createChadoFeatureForSequence(Organism organism, org.bbop.apollo.Sequence sequence, boolean storeSequence = false) {
+        long startTime, endTime
+        Timestamp timeStamp = generateTimeStamp()
+        startTime = System.currentTimeMillis()
+        org.gmod.chado.Feature chadoFeature = getChadoFeatureForSequence(organism, sequence)
+
+        if (chadoFeature == null) {
+            chadoFeature = new org.gmod.chado.Feature(
+                    uniquename: generateUniqueName(),
+                    name: sequence.name,
+                    seqlen: sequence.end - sequence.start,
+                    isAnalysis: false,
+                    isObsolete: false,
+                    timelastmodified: timeStamp,
+                    timeaccessioned: timeStamp,
+                    organism: chadoOrganismsMap.get(organism.commonName),
+                    type: getChadoCvterm("chromosome", SEQUENCE_ONTOLOGY)
+            )
+        }
+
+        if (storeSequence && chadoFeature.residues == null) {
+            String residues = sequenceService.getRawResiduesFromSequence(sequence, sequence.start, sequence.end)
+            chadoFeature.residues = residues
+            chadoFeature.md5checksum = generateMD5checksum(residues)
+        }
+
+        chadoFeature.save()
+        exportStatisticsMap['sequence_feature_count'] += 1
+        endTime = System.currentTimeMillis()
+        log.debug "Time taken to create Chado Feature for sequence ${sequence.name}: ${endTime - startTime} ms"
+        return chadoFeature
+    }
+
+    def getChadoFeatureForSequence(Organism organism, org.bbop.apollo.Sequence sequence) {
+        org.gmod.chado.Feature chadoFeature
+        def sequenceResults = org.gmod.chado.Feature.executeQuery(
+                "SELECT DISTINCT s FROM org.gmod.chado.Feature s WHERE s.name = :querySequenceName AND s.organism.genus = :queryGenus AND s.organism.species = :querySpecies AND s.type.name = :querySequenceType",
+                [querySequenceName: sequence.name, queryGenus: organism.genus, querySpecies: organism.species, querySequenceType: "chromosome"])
+        if (sequenceResults.size() == 0) {
+            chadoFeature = null
+        }
+        else if (sequenceResults.size() == 1) {
+            chadoFeature = sequenceResults.get(0)
+        }
+        else {
+            log.error "${sequenceResults} - More than one result found for sequence name ${sequence.name}. Returning null."
+            chadoFeature = null
+        }
+        return chadoFeature
+    }
+
+    /**
+     * Queries the Chado database and returns a Chado cvterm corresponding to a given CvTerm name.
+     * @param term
+     * @return
+     */
+    def getChadoCvterm(String cvTermString) {
+        org.gmod.chado.Cvterm chadoCvterm
+        long startTime = System.currentTimeMillis()
+        def cvTermList = org.gmod.chado.Cvterm.executeQuery(
+                "SELECT DISTINCT t FROM org.gmod.chado.Cvterm t WHERE t.name = :queryCvTerm",
+                [queryCvTerm: cvTermString])
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken for querying ChadoCvTerm ${cvTermString}: ${endTime - startTime} ms"
+
+        if (cvTermList.size() == 0) {
+            chadoCvterm = null
+        }
+        else if (cvTermList.size() == 1) {
+            chadoCvterm = cvTermList.get(0)
+        }
+        else {
+            log.error "${cvTermList} - More than one result found for cvterm: ${cvTermString}. Returning null."
+            chadoCvterm = null
+        }
+
+        return chadoCvterm
+    }
+
+    /**
+     * Queries the Chado database and returns a Chado cvterm corresponding to a given CvTerm name and CV name.
+     * @param term
+     * @param cvString
+     * @return
+     */
+    def getChadoCvterm(String cvTermString, String cvString) {
+        org.gmod.chado.Cvterm chadoCvterm
+        long startTime = System.currentTimeMillis()
+        def cvTermList = org.gmod.chado.Cvterm.executeQuery(
+                "SELECT DISTINCT t FROM org.gmod.chado.Cvterm t JOIN t.cv cv WHERE cv.name = :queryCvString AND t.name = :queryCvTerm",
+                [queryCvString: cvString, queryCvTerm: cvTermString])
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken for querying for ChadoCvTerm ${cvTermString} of CV ${cvString}: ${endTime - startTime} ms"
+
+        if (cvTermList.size() == 0) {
+            chadoCvterm = null
+        }
+        else if (cvTermList.size() == 1) {
+            chadoCvterm = cvTermList.get(0)
+
+        }
+        else {
+            log.error "${cvTermList} - More than one result found for cvterm: ${cvTermString} of cv: ${cvString}. Returning null."
+            chadoCvterm = null
+        }
+
+        return chadoCvterm
+    }
+
+    /**
+     * Queries the Chado database and returns a Chado db corresponding to a given db
+     * @param dbName
+     * @return
+     */
+    def getChadoDb(org.bbop.apollo.DB db) {
+        org.gmod.chado.Db chadoDb
+        long startTime = System.currentTimeMillis()
+        def dbResult = org.gmod.chado.Db.findAllByName(db.name)
+        long endTime = System.currentTimeMillis()
+        log.debug "Time taken for querying for db ${db.name}: ${endTime - startTime} ms"
+
+        if (dbResult.size() == 0) {
+            chadoDb = createChadoDb(db)
+        }
+        else if (dbResult.size() == 1) {
+            chadoDb = dbResult.get(0)
+        }
+        else {
+            log.error "${dbResult} - More than one result found for db name '${db.name}'. Returning null."
+            chadoDb = null
+        }
+
+        return chadoDb
+    }
+
+    /**
+     * Creates an instance of Chado Organism for a given Apollo Organism.
+     * @param organism
+     * @return
+     */
+    def createChadoOrganism(org.bbop.apollo.Organism organism) {
+        org.gmod.chado.Organism chadoOrganism = getChadoOrganism(organism)
+        if (chadoOrganism == null) {
+            chadoOrganism = new org.gmod.chado.Organism(
+                    abbreviation: organism.abbreviation,
+                    genus: organism.genus,
+                    species: organism.species,
+                    commonName: organism.commonName,
+                    comment: organism.comment
+            ).save()
+
+            organism.organismDBXrefs.each { organismDbxref ->
+                createChadoOrganismDbxref(chadoOrganism, organismDbxref)
+            }
+
+            organism.organismProperties.each { organismProperty ->
+                createChadoOrganismProperty(chadoOrganism, organismProperty)
+            }
+
+            chadoOrganism.save(flush: true)
+        }
+        chadoOrganismsMap.put(organism.commonName, chadoOrganism)
+        return chadoOrganism
+    }
+
+    /**
+     * Queries the Chado database for an Organism that has the same genus and species name as that of given Apollo Organism.
+     * @param organism
+     * @return
+     */
+    def getChadoOrganism(org.bbop.apollo.Organism organism) {
+        org.gmod.chado.Organism chadoOrganism
+        def organismResults = org.gmod.chado.Organism.executeQuery(
+                "SELECT DISTINCT o FROM org.gmod.chado.Organism o WHERE o.genus = :queryGenus AND o.species = :querySpecies",
+                [queryGenus: organism.genus, querySpecies: organism.species])
+
+        if (organismResults.size() == 0) {
+            chadoOrganism = null
+        }
+        else if (organismResults.size() == 1) {
+            chadoOrganism = organismResults.get(0)
+        }
+        else {
+            log.error "${organismResults} - more than one result found for organism '${organism.genus} ${organism.species}'. Returning null."
+            chadoOrganism = null
+        }
+
+        return chadoOrganism
+    }
+
+    /**
+     * Creates an instance of Chado organism_dbxref from a given Apollo OrganismDBXref
+     */
+    def createChadoOrganismDbxref(org.gmod.chado.Organism chadoOrganism, org.bbop.apollo.OrganismDBXref organismDbxref) {
+        org.gmod.chado.Dbxref chadoDbxref = getChadoDbxref(organismDbxref.dbxref)
+        org.gmod.chado.OrganismDbxref chadoOrganismDbxref = new org.gmod.chado.OrganismDbxref(
+                organism: chadoOrganism,
+                dbxref: chadoDbxref
+        ).save()
+        return chadoOrganismDbxref
+    }
+
+    /**
+     *
+     * @param chadoOrganism
+     * @param organismProperty
+     */
+    def createChadoOrganismProperty(org.gmod.chado.Organism chadoOrganism, org.bbop.apollo.OrganismProperty organismProperty) {}
+
+    /**
+     * Generates and returns an instance of java.SQL.Timestamp for the current time, without timezone,
+     * which is required by Chado for Feature attributes such as timelastmodified and timelastaccessioned.
+     * @return Timestamp
+     */
+    def generateTimeStamp() {
+        return new Timestamp(System.currentTimeMillis())
+    }
+
+    /**
+     * Generates and returns a unique name using UUID.
+     * @return String
+     */
+    def generateUniqueName() {
+        return UUID.randomUUID().toString()
+    }
+
+    /**
+     * Generates an MD5 checksum for a given string
+     * @param s
+     * @return
+     */
+    def generateMD5checksum(String s){
+        return MessageDigest.getInstance("MD5").digest(s.bytes).encodeHex().toString()
+    }
+
+    /**
+     * Checks the database for existing Chado features in the feature table.
+     * @return
+     */
+    def checkForExistingFeatures() {
+        int chadoFeatureCount = org.gmod.chado.Feature.count
+        org.gmod.chado.Feature.all.size()
+        if (chadoFeatureCount > 0) {
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Checks the database to make sure that the cvterm table is not empty.
+     * @return
+     */
+    def checkForOntologies() {
+        int cvTermCount = org.gmod.chado.Cvterm.count
+        if (cvTermCount > 0) {
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Initialize map for gathering export statistics.
+     * @return
+     */
+    def initializeExportStatistics() {
+        exportStatisticsMap['feature_count'] = 0
+        exportStatisticsMap['featureloc_count'] = 0
+        exportStatisticsMap['sequence_feature_count'] = 0
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ConfigWrapperService.groovy b/grails-app/services/org/bbop/apollo/ConfigWrapperService.groovy
new file mode 100644
index 0000000..abf66ec
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ConfigWrapperService.groovy
@@ -0,0 +1,129 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.Overlapper
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.TranslationTable
+
+/**
+ * TODO:  move all of this stuff to a database
+ */
+ at Transactional
+class ConfigWrapperService {
+
+    def grailsApplication
+
+    Boolean useCDS() {
+        return grailsApplication.config.apollo.use_cds_for_new_transcripts
+    }
+
+    TranslationTable getTranslationTable() {
+        return SequenceTranslationHandler.getTranslationTableForGeneticCode(getTranslationCode())
+    }
+
+    String getTranslationCode(){
+        return grailsApplication.config.apollo.get_translation_code.toString()
+    }
+
+    Boolean hasDbxrefs(){
+        return grailsApplication.config.apollo.feature_has_dbxrefs
+    }
+    Boolean hasAttributes(){
+        return grailsApplication.config.apollo.feature_has_attributes
+    }
+
+    Boolean hasPubmedIds(){
+        return grailsApplication.config.apollo.feature_has_pubmed_ids
+    }
+    Boolean hasGoIds(){
+        return grailsApplication.config.apollo.feature_has_go_ids
+    }
+    Boolean hasComments(){
+        return grailsApplication.config.apollo.feature_has_comments
+    }
+    Boolean hasStatus(){
+        return grailsApplication.config.apollo.feature_has_status
+    }
+
+    List<String> getSpliceDonorSites(){
+        return grailsApplication.config.apollo.splice_donor_sites
+    }
+
+    List<String> getSpliceAcceptorSites(){
+        return grailsApplication.config.apollo.splice_acceptor_sites
+    }
+
+    int getDefaultMinimumIntronSize() {
+        return grailsApplication.config.apollo.default_minimum_intron_size
+    }
+
+    def getSequenceSearchTools() {
+        return grailsApplication.config.apollo.sequence_search_tools
+    }
+
+    def getDataAdapterTools() {
+        return grailsApplication.config.apollo.data_adapters
+    }
+
+    def exportSubFeatureAttrs() {
+        return grailsApplication.config.apollo.export_subfeature_attrs
+    }
+
+    def getCommonDataDirectory() {
+        return grailsApplication.config.apollo.common_data_directory
+    }
+
+    def hasChadoDataSource() {
+        if (grailsApplication.config.dataSource_chado) {
+            return true
+        }
+        return false
+    }
+
+    def isPostgresChadoDataSource() {
+        if (hasChadoDataSource()) {
+            if (grailsApplication.config.dataSource_chado.url.contains('jdbc:postgresql')) {
+                return true
+            }
+        }
+        return false
+    }
+
+    def getChadoExportFastaForSequence() {
+        return grailsApplication.config.apollo.chado_export_fasta_for_sequence
+    }
+
+    def getChadoExportFastaForCds() {
+        return grailsApplication.config.apollo.chado_export_fasta_for_cds
+    }
+
+    def getAuthentications() {
+        grailsApplication.config.apollo.authentications
+    }
+
+    def getPingUrl() {
+        Boolean phoneHome =  grailsApplication.config.apollo.phone.phoneHome
+        if(phoneHome){
+            String urlString = grailsApplication.config.apollo.phone.url
+            urlString += grailsApplication.config.apollo.phone.bucketPrefix
+            urlString += grailsApplication.metadata['app.version']
+            urlString += "/"
+            urlString += grailsApplication.config.apollo.phone.fileName
+            urlString = urlString.toLowerCase()
+            return urlString
+        }
+        return null
+    }
+
+    Boolean getPhoneHome() {
+        return grailsApplication.config.apollo.phone.phoneHome
+    }
+
+    def getExtraTabs(){
+        return grailsApplication.config.apollo.extraTabs
+    }
+
+    boolean getOnlyOwnersDelete(){
+        return grailsApplication.config.apollo.only_owners_delete
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ConfigurableFeatureService.groovy b/grails-app/services/org/bbop/apollo/ConfigurableFeatureService.groovy
new file mode 100644
index 0000000..c58555f
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ConfigurableFeatureService.groovy
@@ -0,0 +1,61 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+
+ at Transactional
+class ConfigurableFeatureService {
+
+    /**
+     * TODO: integrate with cvTermTranscriptList
+     * @return
+     */
+    List<String> getTranscriptCvTerms() {
+        List<String> returnList = new ArrayList<>()
+        CustomTranscript.all.each {
+            returnList.add(it.getCvTerm())
+            if(it.getAlternateCvTerm()){
+                returnList.add(it.getAlternateCvTerm())
+            }
+        }
+        return returnList
+    }
+
+    /**
+     * TODO: integrate into FS::generateFeatureForType
+     * TODO: integrate into FS::convertJSONToOntologyId
+     * @param cvTerm
+     * @return
+     */
+    String getOntologyIdForCvTerm(String cvTerm){
+        CustomDomainMapping customDomainMapping = CustomDomainMapping.findByCvTerm(cvTerm)
+        if(customDomainMapping) return customDomainMapping.ontologyId
+        return CustomDomainMapping.findByAlternateCvTerm(cvTerm)?.ontologyId
+    }
+
+    /**
+     * TODO: FS::generateFeatureForType
+     * TODO: FS::getCvTermFromFeature
+     * @param cvTerm
+     * @return
+     */
+    Feature generateFeatureForOntologyId(String ontologyId){
+        CustomDomainMapping customDomainMapping = CustomDomainMapping.findByOntologyId(ontologyId)
+
+        if(customDomainMapping.isTranscript){
+            CustomTranscript genericTranscript = new CustomTranscript()
+            genericTranscript.setCvTerm(customDomainMapping.cvTerm)
+            genericTranscript.setAlternateCvTerm(customDomainMapping.alternateCvTerm)
+            genericTranscript.setOntologyId(customDomainMapping.ontologyId)
+            // set anything else?
+            return genericTranscript
+        }
+        else{
+            CustomFeature genericFeature = new CustomFeature()
+            genericFeature.setCvTerm(customDomainMapping.cvTerm)
+            genericFeature.setAlternateCvTerm(customDomainMapping.alternateCvTerm)
+            genericFeature.setOntologyId(customDomainMapping.ontologyId)
+            // set anything else?
+            return genericFeature
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/CvTermService.groovy b/grails-app/services/org/bbop/apollo/CvTermService.groovy
new file mode 100644
index 0000000..fac1c4b
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/CvTermService.groovy
@@ -0,0 +1,54 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+ at Deprecated
+ at Transactional
+class CvTermService {
+
+    CVTerm getPartOf(){
+        CVTerm.findByName(CvTermStringEnum.PART_OF.value)
+    }
+
+    CVTerm getTerm(String term){
+        CVTerm.findByName(term)
+    }
+
+    CVTerm getTerm(FeatureStringEnum featureStringEnum){
+        CVTerm.findByName(featureStringEnum.value)
+    }
+
+    CVTerm getTranscript(){
+        CVTerm.findByName(FeatureStringEnum.TRANSCRIPT.value)
+    }
+
+    CVTerm convertJSONToCVTerm(JSONObject jsonCVTerm){
+        CV cv = CV.findOrSaveByName(jsonCVTerm.getJSONObject(FeatureStringEnum.CV.value).getString(FeatureStringEnum.NAME.value))
+        CVTerm cvTerm = CVTerm.findOrSaveByNameAndCv(jsonCVTerm.getString(FeatureStringEnum.NAME.value),cv)
+        return cvTerm
+    }
+
+    JSONObject convertCVTermToJSON(CVTerm cvTerm){
+        JSONObject jsonCVTerm = new JSONObject();
+        JSONObject jsonCV = new JSONObject();
+        jsonCVTerm.put(FeatureStringEnum.CV.value, jsonCV);
+        jsonCV.put(FeatureStringEnum.NAME.value, cvTerm.getCv().getName());
+        jsonCVTerm.put(FeatureStringEnum.NAME.value, cvTerm.getName());
+        return jsonCVTerm;
+    }
+
+    /**
+     * TODO: replace with a proper subclass
+     * @return
+     */
+    Collection<CVTerm> getFrameshifts() {
+        List<CVTerm> cvTermList = new ArrayList<>()
+        cvTermList.add(getTerm(FeatureStringEnum.MINUS1FRAMESHIFT))
+        cvTermList.add(getTerm(FeatureStringEnum.MINUS2FRAMESHIFT))
+        cvTermList.add(getTerm(FeatureStringEnum.PLUS1FRAMESHIFT))
+        cvTermList.add(getTerm(FeatureStringEnum.PLUS2FRAMESHIFT))
+        return cvTermList
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/DomainMarshallerService.groovy b/grails-app/services/org/bbop/apollo/DomainMarshallerService.groovy
new file mode 100644
index 0000000..b55e3d0
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/DomainMarshallerService.groovy
@@ -0,0 +1,88 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.preference.OrganismDTO
+import org.bbop.apollo.preference.SequenceDTO
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+
+ at Transactional
+class DomainMarshallerService {
+
+    def registerObjects() {
+
+        JSON.registerObjectMarshaller(User) {
+            def returnArray = [:]
+            returnArray['userId'] = it.id
+            returnArray['username'] = it.username
+            returnArray['firstName'] = it.firstName
+            returnArray['lastName'] = it.lastName
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(Organism) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['commonName'] = it.commonName
+            returnArray['genus'] = it?.genus
+            returnArray['species'] = it?.species
+            returnArray['directory'] = it.directory
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(OrganismDTO) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['commonName'] = it.commonName
+            returnArray['directory'] = it.directory
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(Sequence) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['name'] = it.name
+            returnArray['length'] = it?.length
+            returnArray['start'] = it?.start
+            returnArray['end'] = it.end
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(SequenceDTO) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['name'] = it.name
+            returnArray['organism'] = it.organism
+            returnArray['length'] = it?.length
+            returnArray['start'] = it?.start
+            returnArray['end'] = it.end
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(UserOrganismPreference) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['organism'] = it.organism
+            returnArray['currentOrganism'] = it.currentOrganism
+            returnArray['nativeTrackList'] = it?.nativeTrackList
+            returnArray['sequence'] = it?.sequence
+            returnArray['startbp'] = it?.startbp
+            returnArray['endbp'] = it?.endbp
+            return returnArray
+        }
+
+        JSON.registerObjectMarshaller(UserOrganismPreferenceDTO) {
+            def returnArray = [:]
+            returnArray['id'] = it.id
+            returnArray['organism'] = it.organism
+            returnArray['currentOrganism'] = it.currentOrganism
+            returnArray['nativeTrackList'] = it?.nativeTrackList
+            returnArray['sequence'] = it?.sequence
+            returnArray['startbp'] = it?.startbp
+            returnArray['endbp'] = it?.endbp
+            returnArray['clientToken'] = it?.clientToken
+            returnArray['user'] = it?.user
+            return returnArray
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ExonService.groovy b/grails-app/services/org/bbop/apollo/ExonService.groovy
new file mode 100644
index 0000000..9632980
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ExonService.groovy
@@ -0,0 +1,481 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import grails.transaction.Transactional
+import grails.transaction.NotTransactional
+
+//import grails.compiler.GrailsCompileStatic
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.Strand
+
+//@GrailsCompileStatic
+ at Transactional(readOnly = true)
+class ExonService {
+
+//    CvTermService cvTermService
+    def transcriptService
+    def featureService
+    def featureRelationshipService
+    def sequenceService
+    def overlapperService
+    def nameService
+
+    /** Retrieve the transcript that this exon is associated with.  Uses the configuration to
+     * determine which parent is a transcript.  The transcript object is generated on the fly.  Returns
+     * <code>null</code> if this exon is not associated with any transcript.
+     *
+     * @return Transcript that this Exon is associated with
+     */
+    public Transcript getTranscript(Exon exon) {
+
+        // this could be for any transcript, though
+        return (Transcript) featureRelationshipService.getParentForFeature(exon,transcriptService.ontologyIds as String[])
+    }
+
+    /**
+     * Merge exon1 and exon2.  The "newly" created exon retains exon1's ID.
+     *
+     * @param exon1 - Exon to be merged to
+     * @param exon2 - Exon to be merged with
+     * @throws AnnotationException - If exons don't belong to the same transcript or are in separate strands
+     */
+    @Transactional
+    public void mergeExons(Exon exon1, Exon exon2) throws AnnotationException {
+//        // both exons must be part of the same transcript
+//        if (!getTranscript(exon1).equals(getTranscript(exon2))) {
+//            throw new AnnotationEditorException("mergeExons(): Exons must have same parent transcript", exon1, exon2);
+//        }
+        // both exons must be in the same strand
+        Transcript transcript = getTranscript(exon1);
+        if (!exon1?.featureLocation?.getStrand()?.equals(exon2?.featureLocation?.getStrand())) {
+            throw new AnnotationException("mergeExons(): Exons must be in the same strand ${exon1} ${exon2}");
+        }
+        if (exon1.getFmin() > exon2.getFmin()) {
+            setFmin(exon1, exon2.getFmin())
+//            exon1.setFmin(exon2.getFmin());
+        }
+        if (exon1.getFmax() < exon2.getFmax()) {
+            setFmax(exon1, exon2.fmax)
+//            exon1.setFmax(exon2.getFmax());
+        }
+        // need to delete exon2 from transcript
+        if (getTranscript(exon2) != null) {
+            deleteExon(getTranscript(exon2), exon2);
+        }
+        
+//        setLongestORF(getTranscript(exon1));
+        featureService.removeExonOverlapsAndAdjacencies(transcript);
+
+//        Date date = new Date();
+//        exon1.setTimeLastModified(date);
+//        transcript.setTimeLastModified(date);
+
+        // TODO: event fire
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+    }
+
+
+    /**
+     * Delete an exon from a transcript.  If there are no exons left on the transcript, the transcript
+     * is deleted from the parent gene.
+     *
+     * @param transcript - Transcript to have the exon deleted from
+     * @param exon - Exon to be deleted from the transcript
+     */
+    @Transactional
+    public void deleteExon(Transcript transcript, Exon exon) {
+        featureRelationshipService.removeFeatureRelationship(transcript,exon)
+
+
+        // an empty transcript should be removed from gene,  TODO??
+//        if (transcript.getNumberOfExons() == 0) {
+//            if (transcript.getGene() != null) {
+//                deleteTranscript(transcript.getGene(), transcript);
+//            }
+//            else {
+//                deleteFeature(transcript);
+//            }
+//        }
+//        else {
+//            setLongestORF(transcript);
+//        }
+        // update transcript boundaries if necessary
+        if (exon.getFmin().equals(transcript.getFmin())) {
+            int fmin = Integer.MAX_VALUE;
+            for (Exon e : transcriptService.getExons(transcript)) {
+                if (e.getFmin() < fmin) {
+                    fmin = e.getFmin();
+                }
+            }
+            transcriptService.setFmin(transcript,fmin);
+        }
+        if (exon.getFmax().equals(transcript.getFmax())) {
+            int fmax = Integer.MIN_VALUE;
+            for (Exon e : transcriptService.getExons(transcript)) {
+                if (e.getFmax() > fmax) {
+                    fmax = e.getFmax();
+                }
+            }
+            transcriptService.setFmax(transcript,fmax);
+        }
+        // update gene boundaries if necessary
+        transcriptService.updateGeneBoundaries(transcript);
+
+//        FeatureLocation.deleteAll(exon.featureLocations)
+        exon.save(flush: true)
+        exon.featureLocations.clear()
+        exon.parentFeatureRelationships?.clear()
+        exon.childFeatureRelationships?.clear()
+        exon.featureProperties?.clear()
+        List<FeatureRelationship> parentFeatures = FeatureRelationship.findAllByChildFeature(exon)
+        def childFeatures = FeatureRelationship.findAllByParentFeature(exon)
+        if(parentFeatures){
+            parentFeatures.each { FeatureRelationship it ->
+                FeatureRelationship.executeUpdate("delete from FeatureRelationship fr where fr.id = :frid",[frid:it.id])
+            }
+        }
+
+//        FeatureProperty.executeUpdate("delete from FeatureProperty fp where fp.feature.id = :exonId",[exonId:exon.id])
+//        Exon.executeUpdate("delete from Exon e where e.id = :exonId",[exonId:exon.id])
+        exon.delete(flush: true)
+//        Exon.deleteAll(exon)
+        transcript.save(flush: true)
+
+
+    }
+
+
+    @Transactional
+    public void setFmin(Exon exon, Integer fmin) {
+        exon.getFeatureLocation().setFmin(fmin);
+        Transcript transcript = getTranscript(exon)
+        if (transcript != null && fmin < transcript.getFmin()) {
+            transcriptService.setFmin(transcript, fmin);
+        }
+    }
+
+    @Transactional
+    public void setFmax(Exon exon, Integer fmax) {
+        exon.getFeatureLocation().setFmax(fmax);
+        Transcript transcript = getTranscript(exon)
+        if (transcript != null && fmax > transcript.getFmax()) {
+            transcriptService.setFmax(transcript, fmax)
+        }
+    }
+
+
+    @Transactional
+    public Exon makeIntron(Exon exon, int genomicPosition, int minimumIntronSize) {
+        String sequence = sequenceService.getResiduesFromFeature(exon)
+        int exonPosition = featureService.convertSourceCoordinateToLocalCoordinate(exon,genomicPosition);
+//        // find donor coordinate
+        String donorSite = null;
+        int donorCoordinate = -1;
+        for(String donor : SequenceTranslationHandler.spliceDonorSites){
+            int coordinate = sequence.substring(0, exonPosition - minimumIntronSize).lastIndexOf(donor);
+            if (coordinate > donorCoordinate) {
+                donorCoordinate = coordinate;
+                donorSite = donor;
+            }
+        }
+//        // find acceptor coordinate
+        String acceptorSite = null;
+        int acceptorCoordinate = -1;
+        for (String acceptor : SequenceTranslationHandler.getSpliceAcceptorSites()) {
+            int coordinate = sequence.substring(exonPosition + minimumIntronSize, sequence.length()).indexOf(acceptor);
+            if (acceptorCoordinate == -1 || coordinate < acceptorCoordinate) {
+                acceptorCoordinate = coordinate;
+                acceptorSite = acceptor;
+            }
+        }
+//        // no donor/acceptor found
+        if (donorCoordinate == -1 || acceptorCoordinate == -1 || (acceptorCoordinate - donorCoordinate) == 1) {
+            //return splitExon(exon, genomicPosition - 1, genomicPosition + 1, splitExonUniqueName);
+            return null;
+        }
+        acceptorCoordinate += exonPosition + minimumIntronSize;
+        FeatureLocation exonFeatureLocation = exon.featureLocation
+        if (exonFeatureLocation.getStrand().equals(-1)) {
+            int tmp = acceptorCoordinate;
+            acceptorCoordinate = donorCoordinate + 1 - donorSite.length();
+            donorCoordinate = tmp + 1;
+        } else {
+            acceptorCoordinate += acceptorSite.length();
+        }
+        Exon splitExon = splitExon(exon, featureService.convertLocalCoordinateToSourceCoordinate(exon,donorCoordinate) , featureService.convertLocalCoordinateToSourceCoordinate(exon,acceptorCoordinate));
+
+        exon.save()
+        splitExon.save()
+
+        return splitExon
+    }
+//
+
+
+    /**
+     * Set exon boundaries.
+     *
+     * @param exon - Exon to be modified
+     * @param fmin - New fmin to be set
+     * @param fmax - New fmax to be set
+     */
+    @Transactional
+    public void setExonBoundaries(Exon exon, int fmin, int fmax) {
+
+        Transcript transcript = getTranscript(exon)
+//        Transcript transcript = exon.getTranscript();
+        exon.getFeatureLocation().fmin = fmin;
+        exon.getFeatureLocation().fmax = fmax;
+        featureService.removeExonOverlapsAndAdjacencies(transcript);
+
+        featureService.updateGeneBoundaries(transcriptService.getGene(transcript));
+    }
+
+    @Transactional
+    def setToDownstreamDonor(Exon exon) {
+        Transcript transcript = getTranscript(exon)
+        Gene gene = transcriptService.getGene(transcript)
+
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+
+        Integer nextExonFmin = null;
+        Integer nextExonFmax = null;
+        for (ListIterator<Exon> iter = exons.listIterator(); iter.hasNext(); ) {
+            Exon e = iter.next();
+            if (e.getUniqueName().equals(exon.getUniqueName())) {
+                if (iter.hasNext()) {
+                    Exon e2 = iter.next();
+                    nextExonFmin = e2.getFeatureLocation().getFmin();
+                    nextExonFmax = e2.getFeatureLocation().getFmax();
+                    break;
+                }
+            }
+        }
+        FeatureLocation exonFeatureLocation = FeatureLocation.findByFeature(exon)
+        int coordinate = exonFeatureLocation.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exonFeatureLocation.fmin) + 2 : featureService.convertSourceCoordinateToLocalCoordinate(gene,exonFeatureLocation.fmax) + 1;
+        String residues = sequenceService.getResiduesFromFeature(gene)
+        while (coordinate < residues.length()) {
+            int c = featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate);
+            if (nextExonFmin != null && (c >= nextExonFmin && c <= nextExonFmax + 1)) {
+                throw new AnnotationException("Cannot set to downstream donor - will overlap next exon");
+            }
+            String seq = residues.substring(coordinate, coordinate + 2);
+
+            if (SequenceTranslationHandler.getSpliceDonorSites().contains(seq)) {
+                if (exon.getFeatureLocation().getStrand() == -1) {
+                    setExonBoundaries(exon,featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate)+1,exon.getFeatureLocation().getFmax())
+                } else {
+                    setExonBoundaries(exon,exon.getFeatureLocation().getFmin(),featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate))
+                }
+                return;
+            }
+            ++coordinate;
+        }
+    }
+
+    @Transactional
+    def setToUpstreamDonor(Exon exon) {
+        Transcript transcript = getTranscript(exon)
+        Gene gene = transcriptService.getGene(transcript)
+
+        FeatureLocation exonFeatureLocation = FeatureLocation.findByFeature(exon)
+        int coordinate = exonFeatureLocation.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exonFeatureLocation.fmin)  : featureService.convertSourceCoordinateToLocalCoordinate(gene,exonFeatureLocation.fmax) - 1;
+        int exonStart = exonFeatureLocation.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmax()) - 1 : featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmin());
+        String residues = sequenceService.getResiduesFromFeature(gene)
+        while (coordinate > 0 ) {
+
+            if (coordinate <= exonStart) {
+                throw new AnnotationException("Cannot set to upstream donor - will remove exon");
+            }
+            String seq = residues.substring(coordinate, coordinate + 2);
+
+//            log.debug "seq ${seq} in ${SequenceTranslationHandler.spliceDonorSites}"
+            if (SequenceTranslationHandler.getSpliceDonorSites().contains(seq)) {
+                if (exon.getStrand() == -1) {
+                    setExonBoundaries(exon, featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate) + 1, exon.getFmax());
+                } else {
+                    setExonBoundaries(exon, exon.getFmin(), featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate));
+                }
+                return;
+            }
+            --coordinate;
+        }
+    }
+
+    @Transactional
+    def setToUpstreamAcceptor(Exon exon) {
+        Transcript transcript = getTranscript(exon);
+        Gene gene = transcriptService.getGene(transcript);
+
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        Integer prevExonFmin = null;
+        Integer prevExonFmax = null;
+        for (ListIterator<Exon> iter = exons.listIterator(); iter.hasNext(); ) {
+            Exon e = iter.next();
+            if (e.getUniqueName().equals(exon.getUniqueName())) {
+                if (iter.hasPrevious()) {
+                    iter.previous();
+                    if (iter.hasPrevious()) {
+                        Exon e2 = iter.previous();
+                        prevExonFmin = e2.getFmin();
+                        prevExonFmax = e2.getFmax();
+                    }
+                }
+                break;
+            }
+        }
+        int coordinate = exon.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmax() + 2) : featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmin() - 3);
+        String residues = sequenceService.getResiduesFromFeature(gene)
+        while (coordinate >= 0) {
+            int c = featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate);
+            if (prevExonFmin != null && (c >= prevExonFmin && c <= prevExonFmax - 2)) {
+                throw new AnnotationException("Cannot set to upstream acceptor - will overlap previous exon");
+            }
+            String seq = residues.substring(coordinate, coordinate + 2);
+            if (SequenceTranslationHandler.getSpliceAcceptorSites().contains(seq)) {
+                if (exon.getStrand() == -1) {
+                    setExonBoundaries(exon, exon.getFmin(), featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate) - 1);
+                } else {
+                    setExonBoundaries(exon, featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate) + 2, exon.getFmax());
+                }
+                return;
+            }
+            --coordinate;
+        }
+
+    }
+
+    @Transactional
+    def setToDownstreamAcceptor(Exon exon) {
+        log.debug "setting downstream acceptor: ${exon}"
+        Transcript transcript = getTranscript(exon);
+        Gene gene = transcriptService.getGene(transcript);
+        int coordinate = exon.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmax()) : featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmin());
+        int exonEnd = exon.getStrand() == -1 ? featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmin()) : featureService.convertSourceCoordinateToLocalCoordinate(gene,exon.getFmax()) - 1;
+        String residues = sequenceService.getResiduesFromFeature(gene);
+        while (coordinate < residues.length()) {
+            if (coordinate >= exonEnd) {
+                throw new AnnotationException("Cannot set to downstream acceptor - will remove exon");
+            }
+            String seq = residues.substring(coordinate, coordinate + 2);
+            if (SequenceTranslationHandler.getSpliceAcceptorSites().contains(seq)) {
+                if (exon.getStrand() == -1) {
+                    setExonBoundaries(exon, exon.getFmin(), featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate) - 1);
+                } else {
+                    setExonBoundaries(exon, featureService.convertLocalCoordinateToSourceCoordinate(gene,coordinate) + 2, exon.getFmax());
+                }
+                return;
+            }
+            ++coordinate;
+        }
+
+    }
+
+    @Transactional
+    Exon splitExon(Exon exon, int newLeftMax, int newRightMin) {
+        Exon leftExon = exon;
+        FeatureLocation leftFeatureLocation = leftExon.getFeatureLocation()
+
+        String uniqueName = nameService.generateUniqueName(exon)
+        Exon rightExon = new Exon(
+                uniqueName: uniqueName
+                ,name: uniqueName
+                ,isAnalysis: leftExon.isAnalysis
+                ,isObsolete: leftExon.isObsolete
+        ).save(insert:true)
+
+        
+        FeatureLocation rightFeatureLocation = new FeatureLocation(
+                feature: rightExon
+                ,fmin: leftFeatureLocation.fmin
+                ,isFminPartial: leftFeatureLocation.isFminPartial
+                ,fmax: leftFeatureLocation.fmax
+                ,isFmaxPartial: leftFeatureLocation.isFmaxPartial
+                ,strand: leftFeatureLocation.strand
+                ,phase: leftFeatureLocation.phase
+                ,residueInfo: leftFeatureLocation.residueInfo
+                ,locgroup: leftFeatureLocation.locgroup
+                ,rank: leftFeatureLocation.rank
+                ,sequence: leftFeatureLocation.sequence
+        ).save(insert:true)
+        rightExon.addToFeatureLocations(rightFeatureLocation)
+
+        leftFeatureLocation.fmax = newLeftMax
+        rightFeatureLocation.fmin = newRightMin
+        
+        leftFeatureLocation.save()
+        rightFeatureLocation.save()
+
+        Transcript transcript = getTranscript(leftExon)
+        transcriptService.addExon(transcript,rightExon)
+
+        transcript.save()
+        rightExon.save()
+
+        return rightExon
+
+    }
+
+    String getCodingSequenceInPhase(Exon exon, boolean removePartialCodons) {
+        Transcript transcript = getTranscript(exon)
+        CDS cds = transcriptService.getCDS(transcript)
+        if (cds == null || !overlapperService.overlaps(exon, cds, true)) {
+            return ""
+        }
+
+        String residues = sequenceService.getGenomicResiduesFromSequenceWithAlterations(
+                exon.featureLocation.sequence
+                ,exon.fmin < cds.fmin ? cds.fmin : exon.fmin
+                ,exon.fmax > cds.fmax ? cds.fmax : exon.fmax
+                ,Strand.getStrandForValue(exon.featureLocation.strand)
+        )
+
+        ArrayList <Exon> exons = transcriptService.getSortedExons(transcript,false)
+        if (exon.strand == Strand.NEGATIVE.value) {
+            Collections.reverse(exons)
+        }
+
+        int phase = getPhaseForExon(exon)
+        if (removePartialCodons) {
+            residues = residues.substring(phase)
+            residues = residues.substring(0, residues.length() - (residues.length() % 3))
+        }
+        return residues
+    }
+
+    int getPhaseForExon(Exon exon) {
+        int phase = 0
+        Transcript transcript = getTranscript(exon)
+        CDS cds = transcriptService.getCDS(transcript)
+        if (cds == null || !overlapperService.overlaps(exon, cds, true)) {
+            return phase
+        }
+
+        ArrayList <Exon> exons = transcriptService.getSortedExons(transcript,false)
+        if (exon.strand == Strand.NEGATIVE.value) {
+            Collections.reverse(exons)
+        }
+
+        int length = getLengthOfPreviousExons(exons, exon, cds)
+        phase = length % 3 == 0 ? 0 : 3 - (length % 3)
+        return phase
+    }
+
+    int getLengthOfPreviousExons(ArrayList<Exon> exons, Exon exon, CDS cds) {
+        int length = 0
+        for (Exon e in exons) {
+            if (e.equals(exon)) {
+                break
+            }
+            if (!overlapperService.overlaps(e, cds, true)) {
+                continue
+            }
+            int fmin = e.fmin < cds.fmin ? cds.fmin : e.fmin
+            int fmax = e.fmax > cds.fmax ? cds.fmax : e.fmax
+            length += fmin < fmax ? fmax - fmin : fmin - fmax
+        }
+        return length
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FastaHandlerService.groovy b/grails-app/services/org/bbop/apollo/FastaHandlerService.groovy
new file mode 100644
index 0000000..58ca70c
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FastaHandlerService.groovy
@@ -0,0 +1,264 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.sequence.Strand
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import java.io.*;
+import java.util.*;
+import java.util.zip.GZIPOutputStream;
+
+
+public class FastaHandlerService {
+
+    private File file;
+    private PrintWriter out;
+    private Mode mode;
+    public final static int NUM_RESIDUES_PER_LINE = 60;
+
+    def sequenceService
+    def transcriptService
+    def featurePropertyService
+
+    public enum Mode {
+        READ,
+        WRITE
+    }
+    
+    public enum Format {
+        TEXT,
+        GZIP
+    }
+    
+
+    public void close() {
+        if (mode == Mode.READ) {
+            //TODO
+        }
+        else if (mode == Mode.WRITE) { 
+            out.close();
+        }
+    }
+    
+    public void writeFeatures(Collection<Feature> features, String seqType, Set<String> metaDataToExport, String path, Mode mode, Format format,String region = null ) throws IOException {
+        this.mode = mode
+        file = new File(path)
+        file.createNewFile()
+        if(mode == Mode.READ) {
+            if (!file.canRead()) {
+                throw new IOException("Cannot read FASTA file: " + file.getAbsolutePath())
+            }
+        }
+        if(mode == Mode.WRITE) {
+            if(!file.canWrite()) {
+                throw new IOException("Cannot write FASTA to file: " + file.getAbsolutePath())
+            }
+            switch(format) {
+                case Format.TEXT:
+                    out = new PrintWriter(new BufferedWriter(new FileWriter(file)))
+                    break
+                case Format.GZIP:
+                    out = new PrintWriter(new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(file))))
+                    break
+            }
+            
+        }
+        writeFeatures(features.iterator(), seqType, metaDataToExport,region);
+        out.flush()
+        out.close()
+    }
+    
+    public void writeFeatures(Iterator<? extends Feature> iterator, String seqType, Set<String> metaDataToExport,String region) throws IOException {
+        if (mode != Mode.WRITE) {
+            throw new IOException("Cannot write to file in READ mode");
+        }
+        while (iterator.hasNext()) {
+            Feature feature = iterator.next();
+            if(feature.class.name in [Gene.class.name, Pseudogene.class.name]) {
+                def transcriptList = transcriptService.getTranscripts(feature)
+                for (Transcript transcript in transcriptList) {
+                    writeFeature(transcript, seqType, metaDataToExport);
+                }
+            }
+            else {
+                writeFeature(feature, seqType, metaDataToExport)
+            }
+        }
+    }
+    
+    public void writeFeature(Feature feature, String seqType, Set<String> metaDataToExport) {
+        String seq = sequenceService.getSequenceForFeature(feature, seqType, 0)
+        int featureLength = seq.length()
+        if (featureLength == 0) {
+            // no sequence returned by getSequenceForFeature()
+            log.debug " export for ${seqType.toUpperCase()} resulted in a sequence length 0 for ${feature.uniqueName} of type ${feature.class.canonicalName}"
+            return
+        }
+
+        String strand
+
+        if (feature.getStrand() == Strand.POSITIVE.getValue()) {
+            strand = Strand.POSITIVE.getDisplay()
+        } else if (feature.getStrand() == Strand.NEGATIVE.getValue()) {
+            strand = Strand.NEGATIVE.getDisplay()
+        } else {
+            strand = "."
+        }
+        //int featureLength = sequenceService.getResiduesFromFeature(feature).length()
+        String defline = String.format(">%s (%s) %d residues [%s:%d-%d %s strand] [%s]", feature.getUniqueName(), feature.cvTerm, featureLength, feature.getFeatureLocation().getSequence().name, feature.fmin + 1, feature.fmax, strand, seqType);
+        if (!metaDataToExport.isEmpty()) {
+            boolean first = true;
+            if (metaDataToExport.contains("name") && feature.getName() != null) {
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "name=" + feature.getName();
+            }
+            if (metaDataToExport.contains("symbol") && feature.getSymbol() != null) {
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "symbol=" + feature.symbol
+            }
+            if (metaDataToExport.contains("description") && feature.getDescription() != null) {
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "description=" + feature.description
+            }
+            if (metaDataToExport.contains("status") && feature.getStatus() != null) {
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "status=" + feature.status.value
+            }
+            if (metaDataToExport.contains("dbxrefs")) {
+                Iterator<DBXref> dbxrefIter = feature.featureDBXrefs.iterator();
+                if (dbxrefIter.hasNext()) {
+                    if (!first) {
+                        defline += ";";
+                    }
+                    else {
+                        defline += " ";
+                    }
+                    first = false;
+                    StringBuilder dbxrefs = new StringBuilder();
+                    DBXref dbxref = dbxrefIter.next();
+                    dbxrefs.append(dbxref.getDb().getName() + ":" + dbxref.getAccession());
+                    while (dbxrefIter.hasNext()) {
+                        dbxrefs.append(",");
+                        dbxref = dbxrefIter.next();
+                        dbxrefs.append(dbxref.getDb().getName() + ":" + dbxref.getAccession());
+                    }
+                    defline += "dbxrefs=" + dbxrefs.toString();
+                }
+            }
+            if (metaDataToExport.contains("attributes")) {
+                Iterator<FeatureProperty> propertyIter = feature.featureProperties.iterator();
+                if (propertyIter.hasNext()) {
+                    Map<String, StringBuilder> properties = new HashMap<String, StringBuilder>();
+                    while (propertyIter.hasNext()){
+                        FeatureProperty prop = propertyIter.next();
+                        StringBuilder props = properties.get(prop.getTag());
+                        if (props == null) {
+                            props = new StringBuilder();
+                            properties.put(prop.getTag(), props);
+                        }
+                        else {
+                            props.append(",");
+                        }
+                        props.append(prop.getValue());
+                    }
+//                    while (propertyIter.hasNext());
+                    if (!first) {
+                        defline += ";";
+                    }
+                    else {
+                        defline += " ";
+                    }
+                    first = false;
+                    for (Map.Entry<String, StringBuilder> iter : properties.entrySet()) {
+                        defline += iter.getKey() + "=" + iter.getValue().toString();
+                    }
+                }
+            }
+            // TODO: set comment
+            if (metaDataToExport.contains(FeatureStringEnum.COMMENTS.value)) {
+                Iterator<Comment> commentIter = featurePropertyService.getComments(feature).iterator()
+                if (commentIter.hasNext()) {
+                    StringBuilder comments = new StringBuilder();
+                    comments.append(commentIter.next().value);
+                    while (commentIter.hasNext()) {
+                        comments.append(",");
+                        comments.append(commentIter.next().value);
+                    }
+                    if (!first) {
+                        defline += ";";
+                    }
+                    else {
+                        defline += " ";
+                    }
+                    first = false;
+                    defline += FeatureStringEnum.COMMENTS.value+"=" + comments.toString();
+                }
+            }
+            // TODO: set owner
+            if (metaDataToExport.contains(FeatureStringEnum.OWNER.value) && feature.getOwner() != null) {
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += FeatureStringEnum.OWNER.value+"=" + feature.getOwner().getOwner();
+            }
+            if (metaDataToExport.contains("date_creation") && feature.dateCreated != null) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(feature.dateCreated)
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "date_last_modified=" + String.format("%d-%02d-%d", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
+            }
+            if (metaDataToExport.contains("date_last_modified") && feature.lastUpdated != null) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(feature.lastUpdated);
+                if (!first) {
+                    defline += ";";
+                }
+                else {
+                    defline += " ";
+                }
+                first = false;
+                defline += "date_last_modified=" + String.format("%d-%02d-%d", calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
+            }
+        }
+        out.println(defline);
+//        String seq = sequenceService.getResiduesFromFeature(feature)
+        // TODO: use splitter
+        for (int i = 0; i < seq.length(); i += NUM_RESIDUES_PER_LINE) {
+            int endIdx = i + NUM_RESIDUES_PER_LINE;
+            out.println(seq.substring(i, endIdx > seq.length() ? seq.length() : endIdx));
+        }
+    }
+    
+}
diff --git a/grails-app/services/org/bbop/apollo/FeatureEventService.groovy b/grails-app/services/org/bbop/apollo/FeatureEventService.groovy
new file mode 100644
index 0000000..96a628d
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FeatureEventService.groovy
@@ -0,0 +1,910 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.event.AnnotationEvent
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.history.FeatureOperation
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+
+import java.text.DateFormat
+
+/**
+ */
+ at Transactional
+class FeatureEventService {
+
+    def permissionService
+    def transcriptService
+    def featureService
+    def requestHandlingService
+
+    /**
+     *
+     * @param featureOperation
+     * @param geneName
+     * @param transcriptUniqueName
+     * @param commandObject
+     * @param jsonObject
+     * @param user
+     * @return
+     */
+    FeatureEvent addNewFeatureEvent(FeatureOperation featureOperation, String geneName, String transcriptUniqueName, JSONObject commandObject, JSONObject jsonObject, User user) {
+        addNewFeatureEventWithUser(featureOperation, geneName, transcriptUniqueName, commandObject, jsonObject, user)
+    }
+
+    FeatureEvent addNewFeatureEventWithUser(FeatureOperation featureOperation, String name, String uniqueName, JSONObject commandObject, JSONObject jsonObject, User user) {
+        JSONArray newFeatureArray = new JSONArray()
+        newFeatureArray.add(jsonObject)
+        return addNewFeatureEvent(featureOperation, name, uniqueName, commandObject, new JSONArray(), newFeatureArray, user)
+
+    }
+
+    /**
+     * Convention is that 1 is the parent and is returned first in the array.
+     * Because we are tracking the split in the actual object blocks, the newJSONArray is also split
+     * @param name1
+     * @param uniqueName1
+     * @param name2
+     * @param uniqueName2
+     * @param commandObject
+     * @param oldFeatureObject
+     * @param newFeatureArray
+     * @param user
+     * @return
+     */
+    @Timed
+    List<FeatureEvent> addSplitFeatureEvent(String name1, String uniqueName1, String name2, String uniqueName2
+                                            , JSONObject commandObject, JSONObject oldFeatureObject
+                                            , JSONArray newFeatureArray
+                                            , User user) {
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = extractFeatureEventGroup(uniqueName1)
+        featureEventMap.putAll(extractFeatureEventGroup(uniqueName2))
+        List<FeatureEvent> featureEventList = new ArrayList<>()
+        JSONArray oldFeatureArray = new JSONArray()
+        oldFeatureArray.add(oldFeatureObject)
+
+        List<FeatureEvent> lastFeatureEventList = findCurrentFeatureEvent(uniqueName1, featureEventMap)
+        if (lastFeatureEventList.size() != 1) {
+            throw new AnnotationException("Not one current feature event being split for: " + uniqueName1)
+        }
+        if (!lastFeatureEventList) {
+            throw new AnnotationException("Can not find original feature event to split for " + uniqueName1)
+        }
+        FeatureEvent lastFeatureEvent = lastFeatureEventList[0]
+        lastFeatureEvent.current = false;
+        lastFeatureEvent.save(flush: true)
+        deleteFutureHistoryEvents(lastFeatureEvent)
+
+        Date addDate = new Date()
+
+        JSONArray newFeatureArray1 = new JSONArray()
+        JSONArray newFeatureArray2 = new JSONArray()
+
+        newFeatureArray1.add(newFeatureArray.getJSONObject(0))
+        newFeatureArray2.add(newFeatureArray.getJSONObject(1))
+
+        FeatureEvent featureEvent1 = new FeatureEvent(
+                editor: user
+                , name: name1
+                , uniqueName: uniqueName1
+                , operation: FeatureOperation.SPLIT_TRANSCRIPT
+                , current: true
+                , originalJsonCommand: commandObject.toString()
+                , newFeaturesJsonArray: newFeatureArray1.toString()
+                , oldFeaturesJsonArray: oldFeatureArray.toString()
+                , dateCreated: addDate
+                , lastUpdated: addDate
+        ).save()
+
+        FeatureEvent featureEvent2 = new FeatureEvent(
+                editor: user
+                , name: name2
+                , uniqueName: uniqueName2
+                , operation: FeatureOperation.SPLIT_TRANSCRIPT
+                , current: true
+                , originalJsonCommand: commandObject.toString()
+                , newFeaturesJsonArray: newFeatureArray2.toString()
+                , oldFeaturesJsonArray: oldFeatureArray.toString()
+                , dateCreated: addDate
+                , lastUpdated: addDate
+        ).save()
+
+        lastFeatureEvent.childId = featureEvent1.id
+        lastFeatureEvent.childSplitId = featureEvent2.id
+        featureEvent2.parentId = lastFeatureEvent.id
+        featureEvent1.parentId = lastFeatureEvent.id
+
+        featureEvent1.save()
+        featureEvent2.save()
+        lastFeatureEvent.save()
+
+        featureEventList.add(featureEvent1)
+        featureEventList.add(featureEvent2)
+
+
+        return featureEventList
+    }
+
+    /**
+     * Convention is that 1 becomes the child and is returned.
+     * Because we are tracking the merge in the actual object blocks, the newJSONArray is also split
+     * @param geneName1
+     * @param uniqueName1
+     * @param geneName2
+     * @param uniqueName2
+     * @param commandObject
+     * @param oldFeatureArray
+     * @param newFeatureObject
+     * @param user
+     * @return
+     */
+    @Timed
+    List<FeatureEvent> addMergeFeatureEvent(String geneName1, String uniqueName1, String geneName2, String uniqueName2, JSONObject commandObject, JSONArray oldFeatureArray, JSONObject newFeatureObject,
+                                            User user) {
+        List<FeatureEvent> featureEventList = new ArrayList<>()
+        Map<String, Map<Long, FeatureEvent>> featureEventMap1 = extractFeatureEventGroup(uniqueName1)
+        Map<String, Map<Long, FeatureEvent>> featureEventMap2 = extractFeatureEventGroup(uniqueName2)
+
+        List<FeatureEvent> lastFeatureEventLeftList = findCurrentFeatureEvent(uniqueName1, featureEventMap1)
+        if (!lastFeatureEventLeftList) {
+            throw new AnnotationException("Can not find original feature event to split for " + uniqueName1)
+        }
+        List<FeatureEvent> lastFeatureEventRightList = findCurrentFeatureEvent(uniqueName2, featureEventMap2)
+        if (!lastFeatureEventRightList) {
+            throw new AnnotationException("Can not find original feature event to split for " + uniqueName2)
+        }
+
+
+        FeatureEvent lastFeatureEventLeft = lastFeatureEventLeftList[0]
+        FeatureEvent lastFeatureEventRight = lastFeatureEventRightList[0]
+        lastFeatureEventLeft.current = false;
+        lastFeatureEventRight.current = false;
+        lastFeatureEventLeft.save()
+        lastFeatureEventRight.save()
+//        featureEventMap1 = featureEventMap1.putAll(featureEventMap2)
+        deleteFutureHistoryEvents(lastFeatureEventLeft)
+        deleteFutureHistoryEvents(lastFeatureEventRight)
+
+        Date addDate = new Date()
+
+        JSONArray newFeatureArray1 = new JSONArray()
+
+        newFeatureArray1.add(newFeatureObject)
+
+        FeatureEvent featureEvent1 = new FeatureEvent(
+                editor: user
+                , name: geneName1
+                , uniqueName: uniqueName1
+                , operation: FeatureOperation.MERGE_TRANSCRIPTS
+                , current: true
+                , originalJsonCommand: commandObject.toString()
+                , newFeaturesJsonArray: newFeatureArray1.toString()
+                , oldFeaturesJsonArray: oldFeatureArray.toString()
+                , dateCreated: addDate
+                , lastUpdated: addDate
+        ).save()
+
+
+        lastFeatureEventLeft.childId = featureEvent1.id
+        lastFeatureEventRight.childId = featureEvent1.id
+        featureEvent1.parentId = lastFeatureEventLeft.id
+        featureEvent1.parentMergeId = lastFeatureEventRight.id
+
+        featureEvent1.save()
+        lastFeatureEventLeft.save()
+        lastFeatureEventRight.save(flush: true)
+
+        featureEventList.add(featureEvent1)
+
+
+        return featureEventList
+    }
+
+    /**
+     * For non-split , non-merge operations
+     */
+    @Timed
+    def addNewFeatureEvent(FeatureOperation featureOperation, String name, String uniqueName, JSONObject inputCommand, JSONArray oldFeatureArray, JSONArray newFeatureArray, User user) {
+
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = extractFeatureEventGroup(uniqueName)
+
+
+        List<FeatureEvent> lastFeatureEventList = findCurrentFeatureEvent(uniqueName, featureEventMap)
+        FeatureEvent lastFeatureEvent = null
+        lastFeatureEventList?.each { a ->
+            if (a.uniqueName == uniqueName) {
+                lastFeatureEvent = a
+            }
+        }
+        if (lastFeatureEvent) {
+            lastFeatureEvent.current = false;
+            lastFeatureEvent.save(flush: true)
+            deleteFutureHistoryEvents(lastFeatureEvent)
+        }
+
+        FeatureEvent featureEvent = new FeatureEvent(
+                editor: user
+                , name: name
+                , uniqueName: uniqueName
+                , operation: featureOperation.name()
+                , current: true
+                , parentId: lastFeatureEvent?.id
+                , originalJsonCommand: inputCommand.toString()
+                , newFeaturesJsonArray: newFeatureArray.toString()
+                , oldFeaturesJsonArray: oldFeatureArray.toString()
+                , dateCreated: new Date()
+                , lastUpdated: new Date()
+        ).save(flush: true)
+
+        // set the children here properly
+        if (lastFeatureEvent) {
+            lastFeatureEvent.childId = featureEvent.id
+            lastFeatureEvent.save(flush: true)
+        }
+
+        return featureEvent
+    }
+
+    Map<String, Map<Long, FeatureEvent>> extractFeatureEventGroup(String uniqueName, Map<String, Map<Long, FeatureEvent>> featureEventMap = new HashMap<>()) {
+        def featureEvents = FeatureEvent.findAllByUniqueName(uniqueName)
+        Map<Long, FeatureEvent> longFeatureEventMap = new HashMap<>()
+        Set<Long> idsToCollect = new HashSet<>()
+        featureEvents.each {
+            longFeatureEventMap.put(it.id, it)
+            idsToCollect.add(it.childId)
+            idsToCollect.add(it.childSplitId)
+            idsToCollect.add(it.parentId)
+            idsToCollect.add(it.parentMergeId)
+            idsToCollect = idsToCollect - longFeatureEventMap.keySet()
+        }
+        idsToCollect.removeAll(Collections.singleton(null));
+
+        featureEventMap.put(uniqueName, longFeatureEventMap)
+
+        if (idsToCollect) {
+
+            Collection<String> uniqueNames = FeatureEvent.withCriteria {
+                and {
+                    'in'("id", idsToCollect)
+                    not {
+                        'in'("uniqueName", featureEventMap.keySet())
+                    }
+                }
+            }.uniqueName.unique()
+
+            uniqueNames.each {
+                featureEventMap.putAll(extractFeatureEventGroup(it, featureEventMap))
+            }
+        }
+
+
+        return featureEventMap
+    }
+
+    def setNotPreviousFutureHistoryEvents(FeatureEvent featureEvent) {
+        List<List<FeatureEvent>> featureEventList = findPreviousFeatureEvents(featureEvent)
+        featureEventList.each { array ->
+            array.each {
+                if (it.current) {
+                    it.current = false
+                    it.save(flush: true)
+                }
+            }
+        }
+    }
+
+    def setNotCurrentFutureHistoryEvents(FeatureEvent featureEvent) {
+        List<List<FeatureEvent>> featureEventList = findFutureFeatureEvents(featureEvent)
+        featureEventList.each { array ->
+            array.each {
+                if (it.current) {
+                    it.current = false
+                    it.save(flush: true)
+                }
+            }
+        }
+    }
+
+    def deleteFutureHistoryEvents(FeatureEvent featureEvent) {
+        List<List<FeatureEvent>> featureEventList = findFutureFeatureEvents(featureEvent)
+        int count = 0
+        featureEventList.each { it.each { it.delete(); ++count } }
+        return count
+    }
+
+    FeatureEvent findFeatureEventFromMap(Long parentId, Map<String, Map<Long, FeatureEvent>> featureEventMap) {
+        for (Map<Long, FeatureEvent> map in featureEventMap.values()) {
+            if (map.containsKey(parentId)) {
+                return map.get(parentId)
+            }
+        }
+        return null
+    }
+
+    /**
+     * Should flatten the tree and add indexes going all the way up on each one.
+     *
+     * @param featureEvent
+     * @param featureEventMap
+     * @return
+     */
+    @Timed
+    List<List<FeatureEvent>> findPreviousFeatureEvents(FeatureEvent featureEvent) {
+        Map<Integer, Set<FeatureEvent>> map = new TreeMap<Integer, Set<FeatureEvent>>()
+        buildMap(featureEvent, map, 0, true, false)
+        removeSelfFromMap(featureEvent, map)
+        // find and remove self
+        List<List<FeatureEvent>> featureEventList = generateArrayFromTree(map)
+
+        def uniqueFeatureEventList = makeUniqueFeatureEvents(featureEventList)
+        return uniqueFeatureEventList
+    }
+
+    List<List<FeatureEvent>> makeUniqueFeatureEvents(List<List<FeatureEvent>> featureEventList) {
+        def uniqueFeatureEventList = featureEventList.unique(false) { a, b ->
+            for (aIndex in a) {
+                for (bIndex in b) {
+                    if (aIndex.id == bIndex.id) {
+                        return 0
+                    }
+                }
+            }
+            return 1
+        }
+        return uniqueFeatureEventList
+    }
+
+    def removeSelfFromMap(FeatureEvent featureEvent, TreeMap<Integer, Set<FeatureEvent>> map) {
+        def iter = map.keySet().iterator()
+        def keysToRemove = []
+        while (iter.hasNext()) {
+            def it = iter.next()
+            if (map.get(it).contains(featureEvent)) {
+                keysToRemove << it
+            }
+        }
+
+        keysToRemove.each {
+            map.remove(it)
+        }
+    }
+/**
+ * Ordered from 0 is first N is last (most recent)
+ * @param featureEvent
+ * @return
+ */
+    @Timed
+    List<List<FeatureEvent>> findFutureFeatureEvents(FeatureEvent featureEvent) {
+        Map<Integer, Set<FeatureEvent>> map = new TreeMap<Integer, Set<FeatureEvent>>()
+        buildMap(featureEvent, map, 0, false, true)
+        removeSelfFromMap(featureEvent, map)
+        // find and remove self
+        List<List<FeatureEvent>> featureEventList = generateArrayFromTree(map)
+
+        def uniqueFeatureEventList = makeUniqueFeatureEvents(featureEventList)
+        return uniqueFeatureEventList
+    }
+
+
+    @Timed
+    def addNewFeatureEvent(FeatureOperation featureOperation, String name, String uniqueName, JSONObject inputCommand, JSONObject oldJsonObject, JSONObject newJsonObject, User user) {
+        JSONArray newFeatureArray = new JSONArray()
+        newFeatureArray.add(newJsonObject)
+        JSONArray oldFeatureArray = new JSONArray()
+        oldFeatureArray.add(oldJsonObject)
+
+        return addNewFeatureEvent(featureOperation, name, uniqueName, inputCommand, oldFeatureArray, newFeatureArray, user)
+    }
+
+    /**
+     * Evaluates if history can be set to a certain position.
+     * Should mirror setTransactionForFeature
+     * @param uniqueName
+     * @param count
+     * @return Error message
+     */
+    Boolean evaluateSetTransactionForFeature(String uniqueName, int newIndex) throws AnnotationException{
+        try {
+            log.debug "setting previous transaction for feature ${uniqueName} -> ${newIndex}"
+            log.debug "unique values: ${FeatureEvent.countByUniqueName(uniqueName)} -> ${newIndex}"
+
+            // find the current index of the current feature
+            Integer currentIndex = getCurrentFeatureEventIndex(uniqueName)
+            log.debug "deepest current index ${currentIndex}"
+
+            List<FeatureEvent> currentFeatureEventArray = findCurrentFeatureEvent(uniqueName)
+            log.debug "current feature event array ${currentFeatureEventArray as JSON}"
+            FeatureEvent currentFeatureEvent = currentFeatureEventArray.find() { it.uniqueName == uniqueName }
+            currentFeatureEvent = currentFeatureEvent ?: currentFeatureEventArray.first()
+
+            log.debug "current feature event ${currentFeatureEvent as JSON}"
+            // if the current index is GREATER, then find the future indexes and set appropriately
+            if (newIndex > currentIndex) {
+                List<List<FeatureEvent>> futureFeatureEvents = findFutureFeatureEvents(currentFeatureEvent)
+                currentFeatureEventArray = futureFeatureEvents.get(newIndex - currentIndex - 1)
+                // subtract one for the index offset
+            }
+            // if the current index is LESS, then find the previous indexes and set appropriately
+            else if (newIndex < currentIndex) {
+                List<List<FeatureEvent>> previousFeatureEvents = findPreviousFeatureEvents(currentFeatureEvent)
+    //            currentFeatureEventArray = previousFeatureEvents.get(newIndex)
+                if (newIndex >= previousFeatureEvents.size()) {
+                    throw new AnnotationException("Can not undo this operation due to a split or a merge.  Try to undo or redo using a different genomic feature.")
+                }
+            }
+            return true
+        } catch (e) {
+            // just pass it through
+            if(e instanceof AnnotationException){
+                throw e
+            }
+            else{
+                throw new AnnotationException("Can not set history for this operation.  Try to undo or redo using a different genomic feature. ${e.message}")
+            }
+        }
+    }
+
+    /**
+     * CurrentIndex of 0 is the oldest.  Highest number is the most recent
+     * This returns an array.  We could have any number of splits going forward, so we have to return an array here.
+     * @param uniqueName
+     * @param newIndex
+     * @return
+     */
+    List<FeatureEvent> setTransactionForFeature(String uniqueName, int newIndex) {
+
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = extractFeatureEventGroup(uniqueName)
+
+        log.debug "setting previous transaction for feature ${uniqueName} -> ${newIndex}"
+        log.debug "unique values: ${FeatureEvent.countByUniqueName(uniqueName)} -> ${newIndex}"
+
+        // find the current index of the current feature
+        Integer currentIndex = getCurrentFeatureEventIndex(uniqueName)
+        log.debug "deepest current index ${currentIndex}"
+
+        List<FeatureEvent> currentFeatureEventArray = findCurrentFeatureEvent(uniqueName)
+        log.debug "current feature event array ${currentFeatureEventArray}"
+        FeatureEvent currentFeatureEvent = currentFeatureEventArray.find() { it.uniqueName == uniqueName }
+        currentFeatureEvent = currentFeatureEvent ?: currentFeatureEventArray.first()
+
+        log.debug "current feature event ${currentFeatureEvent}"
+        // if the current index is GREATER, then find the future indexes and set appropriately
+        if (newIndex > currentIndex) {
+            List<List<FeatureEvent>> futureFeatureEvents = findFutureFeatureEvents(currentFeatureEvent)
+            currentFeatureEventArray = futureFeatureEvents.get(newIndex - currentIndex - 1)
+            // subtract one for the index offset
+            currentFeatureEventArray.each {
+                it.current = true
+                it.save()
+            }
+        }
+        // if the current index is LESS, then find the previous indexes and set appropriately
+        else if (newIndex < currentIndex) {
+            List<List<FeatureEvent>> previousFeatureEvents = findPreviousFeatureEvents(currentFeatureEvent)
+            currentFeatureEventArray = previousFeatureEvents.get(newIndex)
+            currentFeatureEventArray.each {
+                it.current = true
+                it.save()
+            }
+        } else {
+            log.warn("Setting history to same place ${currentIndex} -> ${newIndex}")
+            return findCurrentFeatureEvent(uniqueName, featureEventMap)
+        }
+
+        currentFeatureEvent = currentFeatureEventArray.find() { it.uniqueName == uniqueName }
+        currentFeatureEvent = currentFeatureEvent ?: currentFeatureEventArray.first()
+
+        setNotPreviousFutureHistoryEvents(currentFeatureEvent)
+        setNotCurrentFutureHistoryEvents(currentFeatureEvent)
+
+        currentFeatureEvent.save(flush: true)
+
+        return findCurrentFeatureEvent(uniqueName, featureEventMap)
+    }
+
+    def setHistoryState(JSONObject inputObject, int count) {
+
+        String uniqueName = inputObject.getString(FeatureStringEnum.UNIQUENAME.value)
+        log.debug "undo count ${count}"
+        if (count < 0) {
+            log.warn("Can not undo any further")
+            return
+        }
+
+        List<List<FeatureEvent>> history = getHistory(uniqueName)
+        int total = history.size()
+        if (total == 0) {
+            Set<String> uniqueNames = extractFeatureEventGroup(uniqueName).keySet()
+            uniqueNames.remove(uniqueName)
+            for (int i = 0; total == 0 && i < uniqueNames.size(); i++) {
+                String name = uniqueNames[i]
+                total = getHistory(name)?.size()
+                uniqueName = total > 0 ? name : uniqueNames
+            }
+        }
+        if (count >= total) {
+            log.warn("Can not redo any further")
+            return
+        }
+
+        def newUniqueNames = history[count].collect() {
+            it.uniqueName
+        }
+
+        Sequence sequence = Feature.findByUniqueNameInList(newUniqueNames).featureLocation.sequence
+        log.debug "sequence: ${sequence}"
+
+
+
+        assert evaluateSetTransactionForFeature(uniqueName, count)
+        deleteCurrentState(inputObject, newUniqueNames, sequence)
+        List<FeatureEvent> featureEventArray = setTransactionForFeature(uniqueName, count)
+
+        def transcriptsToCheckForIsoformOverlap = []
+        featureEventArray.each { featureEvent ->
+            JSONArray jsonArray = (JSONArray) JSON.parse(featureEvent.newFeaturesJsonArray)
+            JSONObject originalCommandObject = (JSONObject) JSON.parse(featureEvent.originalJsonCommand)
+            log.debug "array to add size: ${jsonArray.size()} "
+            for (int i = 0; i < jsonArray.size(); i++) {
+                JSONObject jsonFeature = jsonArray.getJSONObject(i)
+
+                JSONObject addCommandObject = new JSONObject()
+                JSONArray featuresToAddArray = new JSONArray()
+                featuresToAddArray.add(jsonFeature)
+                addCommandObject.put(FeatureStringEnum.FEATURES.value, featuresToAddArray)
+
+                // we have to explicitly set the track (if we have features ... which we should)
+                if (!addCommandObject.containsKey(FeatureStringEnum.TRACK.value) && featuresToAddArray.size() > 0) {
+                    addCommandObject.put(FeatureStringEnum.TRACK.value, featuresToAddArray.getJSONObject(0).getString(FeatureStringEnum.SEQUENCE.value))
+                }
+
+                addCommandObject = permissionService.copyRequestValues(inputObject, addCommandObject)
+
+                addCommandObject.put(FeatureStringEnum.SUPPRESS_HISTORY.value, true)
+
+
+                if (featureService.isJsonTranscript(jsonFeature)) {
+                    // set the original gene name
+                    addCommandObject.put(FeatureStringEnum.SUPPRESS_EVENTS.value, true)
+                    for (int k = 0; k < featuresToAddArray.size(); k++) {
+                        JSONObject featureObject = featuresToAddArray.getJSONObject(k)
+                        featureObject.put(FeatureStringEnum.GENE_NAME.value, featureEvent.name)
+                    }
+                    log.debug "original command object = ${originalCommandObject as JSON}"
+                    log.debug "final command object = ${addCommandObject as JSON}"
+                    requestHandlingService.addTranscript(addCommandObject)
+                    transcriptsToCheckForIsoformOverlap.add(jsonFeature.getString("uniquename"))
+
+                } else {
+                    addCommandObject.put(FeatureStringEnum.SUPPRESS_EVENTS.value, false)
+                    requestHandlingService.addFeature(addCommandObject)
+                }
+
+            }
+        }
+
+        // after all the transcripts from the feature event has been added, applying isoform overlap rule
+        Set transcriptsToUpdate = new HashSet()
+        transcriptsToCheckForIsoformOverlap.each {
+            transcriptsToUpdate.add(it)
+            transcriptsToUpdate.addAll(featureService.handleDynamicIsoformOverlap(Transcript.findByUniqueName(it)).uniqueName)
+        }
+
+        // firing update annotation event
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = requestHandlingService.createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                Transcript transcript = Transcript.findByUniqueName(it)
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                requestHandlingService.fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        return featureEventArray
+
+    }
+
+
+    def deleteCurrentState(JSONObject inputObject, List<String> newUniqueNames, Sequence sequence) {
+        for (uniqueName in newUniqueNames) {
+            deleteCurrentState(inputObject, uniqueName, sequence)
+        }
+    }
+
+
+    def deleteCurrentState(JSONObject inputObject, String uniqueName, Sequence sequence) {
+
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = extractFeatureEventGroup(uniqueName)
+
+        // need to get uniqueNames for EACH current featureEvent
+        def currentFeatureEvents = findCurrentFeatureEvent(uniqueName, featureEventMap)
+        for (FeatureEvent deleteFeatureEvent in currentFeatureEvents) {
+            JSONObject deleteCommandObject = new JSONObject()
+            JSONArray featuresArray = new JSONArray()
+            log.debug "delete feature event uniqueNamee: ${deleteFeatureEvent.uniqueName}"
+            JSONObject featureToDelete = new JSONObject()
+            featureToDelete.put(FeatureStringEnum.UNIQUENAME.value, deleteFeatureEvent.uniqueName)
+            featuresArray.add(featureToDelete)
+
+            log.debug "inputObject ${inputObject as JSON}"
+            log.debug "deleteCommandObject ${deleteCommandObject as JSON}"
+
+            if (!deleteCommandObject.containsKey(FeatureStringEnum.TRACK.value)) {
+                deleteCommandObject.put(FeatureStringEnum.TRACK.value, sequence.name)
+            }
+            deleteCommandObject.put(FeatureStringEnum.SUPPRESS_HISTORY, true)
+            log.debug "final deleteCommandObject ${deleteCommandObject as JSON}"
+
+            deleteCommandObject.put(FeatureStringEnum.FEATURES.value, featuresArray)
+            deleteCommandObject = permissionService.copyRequestValues(inputObject, deleteCommandObject)
+
+            log.debug " final delete JSON ${deleteCommandObject as JSON}"
+            // suppress any events that are not part of the new state
+            requestHandlingService.deleteFeature(deleteCommandObject)
+            log.debug "deletion success . .  "
+        }
+
+    }
+
+    /**
+     * Count back from most recent
+     * @param inputObject
+     * @param count
+     * @param confirm
+     * @return
+     */
+    def redo(JSONObject inputObject, int countForward) {
+        log.info "redoing ${countForward}"
+        if (countForward == 0) {
+            log.warn "Redo to the same state"
+            return
+        }
+        String uniqueName = inputObject.get(FeatureStringEnum.UNIQUENAME.value)
+        int currentIndex = getCurrentFeatureEventIndex(uniqueName)
+//        Set<String> uniqueNames = extractFeatureEventGroup(uniqueName).keySet()
+//        assert uniqueNames.remove(uniqueName)
+//        uniqueNames.each {
+//            currentIndex = Math.max(getCurrentFeatureEventIndex(it), currentIndex)
+//        }
+        int count = currentIndex + countForward
+        log.info "current Index ${currentIndex}"
+        log.info "${count} = ${currentIndex}-${countForward}"
+        setHistoryState(inputObject, count)
+    }
+
+    /**
+     * We count backwards in order to get the correct count.
+     * @param uniqueName
+     * @return
+     */
+    int getCurrentFeatureEventIndex(String uniqueName, Map<String, Map<Long, FeatureEvent>> featureEventMap = null) {
+
+        List<FeatureEvent> currentFeatureEventList = FeatureEvent.findAllByUniqueNameAndCurrent(uniqueName, true, [sort: "dateCreated", order: "asc"])
+        featureEventMap = extractFeatureEventGroup(uniqueName)
+        if (!currentFeatureEventList) {
+            featureEventMap.keySet().each {
+                if (!currentFeatureEventList && uniqueName != it) {
+                    currentFeatureEventList = FeatureEvent.findAllByUniqueNameAndCurrent(it, true, [sort: "dateCreated", order: "asc"])
+                }
+            }
+        }
+
+        if (currentFeatureEventList.size() != 1) {
+            throw new AnnotationException("Feature event list is the wrong size ${currentFeatureEventList?.size()}")
+        }
+        FeatureEvent currentFeatureEvent = currentFeatureEventList.iterator().next()
+        def history = getHistory(uniqueName)
+        Integer deepestIndex = history.size() - 1
+        def previousEvents = findPreviousFeatureEvents(currentFeatureEvent)
+        def futureEvents = findFutureFeatureEvents(currentFeatureEvent)
+        def offset = deepestIndex - futureEvents.size() - previousEvents.size()
+        def returnIndex = previousEvents.size() + offset
+        return returnIndex
+//        Integer deepestIndex = getDeepestIndex(-1, currentFeatureEvent, featureEventMap)
+    }
+
+    int getDeepestIndex(int index, FeatureEvent currentFeatureEvent, Map<String, Map<Long, FeatureEvent>> featureEventMap) {
+        ++index
+        FeatureEvent featureEvent1 = null
+        FeatureEvent featureEvent2 = null
+        if (currentFeatureEvent.parentId) {
+            featureEvent1 = findFeatureEventFromMap(currentFeatureEvent.parentId, featureEventMap)
+        }
+        if (currentFeatureEvent.parentMergeId) {
+            featureEvent2 = findFeatureEventFromMap(currentFeatureEvent.parentMergeId, featureEventMap)
+        }
+        Integer p1Id = featureEvent1 ? getDeepestIndex(index, featureEvent1, featureEventMap) : index
+        Integer p2Id = featureEvent2 ? getDeepestIndex(index, featureEvent2, featureEventMap) : index
+        return Math.max(p1Id, p2Id)
+    }
+
+    def undo(JSONObject inputObject, int countBackwards) {
+        log.info "undoing ${countBackwards}"
+        if (countBackwards == 0) {
+            log.warn "Undo to the same state"
+            return
+        }
+
+        String uniqueName = inputObject.get(FeatureStringEnum.UNIQUENAME.value)
+        int currentIndex = getCurrentFeatureEventIndex(uniqueName)
+        int count = currentIndex - countBackwards
+        log.debug "${count} = ${currentIndex}-${countBackwards}"
+        setHistoryState(inputObject, count)
+    }
+
+    /**
+     * We find the current one for the uniqueName and its index
+     * We then count forward by the same amount and return that.
+     * @param uniqueName
+     * @return
+     */
+    @Timed
+    List<FeatureEvent> findCurrentFeatureEvent(String uniqueName, Map<String, Map<Long, FeatureEvent>> featureEventMap = null) {
+        featureEventMap = featureEventMap ?: extractFeatureEventGroup(uniqueName)
+
+        List<FeatureEvent> currentFeatureEvents = []
+        featureEventMap.values().each {
+            it.values().each() { featureEvent ->
+                if (featureEvent.current) {
+                    currentFeatureEvents.add(featureEvent)
+                }
+            }
+        }
+
+        return currentFeatureEvents
+
+    }
+
+    /**
+     * This is the root uniqueName
+     * Should returned sorted most recent at 0, latest at end
+     *
+     * If splits occur we need to show them
+     * @param uniqueName
+     * @return
+     */
+    List<List<FeatureEvent>> getHistory(String uniqueName, Map<String, Map<Long, FeatureEvent>> featureEventMap = null) {
+        featureEventMap = featureEventMap ?: extractFeatureEventGroup(uniqueName)
+
+
+        List<FeatureEvent> currentFeatureEvents = findCurrentFeatureEvent(uniqueName, featureEventMap)
+        // if we revert a split or do a merge
+        if (!currentFeatureEvents) {
+            return new ArrayList<List<FeatureEvent>>()
+        }
+
+
+        TreeMap<Integer, Set<FeatureEvent>> unindexedMap = new TreeMap<>()
+        buildMap(currentFeatureEvents[0], unindexedMap, 0, true, true)
+
+        List<List<FeatureEvent>> featureEvents = generateArrayFromTree(unindexedMap)
+
+        // if we have a split, it will pick up the same values twice
+        // so we need to filter those out
+        // sort by what is on top of what
+        // if we have a merge, where one of the merges has history, but the other doesn't
+        def uniqueFeatureEvents = featureEvents.unique(false) { a, b ->
+            for (aIndex in a) {
+                for (bIndex in b) {
+                    if (aIndex.id == bIndex.id) {
+                        return 0
+                    }
+                }
+            }
+            return 1
+        }
+        return uniqueFeatureEvents
+    }
+
+    List<List<FeatureEvent>> generateArrayFromTree(TreeMap<Integer, Set<FeatureEvent>> unindexedMap) {
+        List<List<FeatureEvent>> featureEvents = new ArrayList<List<FeatureEvent>>()
+        for (int i = 0; i < unindexedMap.size(); i++) {
+            featureEvents.add(new ArrayList<FeatureEvent>())
+        }
+
+        if (unindexedMap) {
+            Integer offset = unindexedMap.keySet().first()
+            for (index in unindexedMap.keySet()) {
+                featureEvents.set(index - offset, unindexedMap.get(index) as List<FeatureEvent>)
+            }
+        }
+        return featureEvents
+    }
+
+    def buildMap(FeatureEvent featureEvent, TreeMap<Integer, Set<FeatureEvent>> unindexedMap, int index, Boolean includePrevious, Boolean includeFuture) {
+
+        if (!featureEvent) return
+
+        def featureEventSet = unindexedMap.get(index)
+        if (!featureEventSet) {
+            featureEventSet = new HashSet<FeatureEvent>()
+        } else if (featureEventSet.contains(featureEvent)) {
+            return
+        }
+        featureEventSet.add(featureEvent)
+        unindexedMap.put(index, featureEventSet)
+
+        if (featureEvent.parentId && includePrevious) {
+            FeatureEvent parentFeatureEvent = FeatureEvent.findById(featureEvent.parentId)
+            buildMap(parentFeatureEvent, unindexedMap, index - 1, includePrevious, includeFuture)
+
+            if (featureEvent.parentMergeId) {
+                FeatureEvent mergeFeatureEvent = FeatureEvent.findById(featureEvent.parentMergeId)
+                buildMap(mergeFeatureEvent, unindexedMap, index - 1, includePrevious, includeFuture)
+            }
+        }
+        if (featureEvent.childId && includeFuture) {
+            FeatureEvent childFeatureEvent = FeatureEvent.findById(featureEvent.childId)
+            buildMap(childFeatureEvent, unindexedMap, index + 1, includePrevious, includeFuture)
+
+            if (featureEvent.childSplitId) {
+                FeatureEvent splitFeatureEvent = FeatureEvent.findById(featureEvent.childSplitId)
+                buildMap(splitFeatureEvent, unindexedMap, index + 1, includePrevious, includeFuture)
+            }
+        }
+    }
+
+    JSONObject generateHistory(JSONObject historyContainer, JSONArray featuresArray) {
+        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
+        for (int i = 0; i < featuresArray.size(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+
+            List<FeatureEvent> currentFeatureEventList = findCurrentFeatureEvent(uniqueName)
+            def thisFeatureEvent = currentFeatureEventList.find() { it.uniqueName == uniqueName }
+            thisFeatureEvent = thisFeatureEvent ?: currentFeatureEventList.first()
+            def futureEvents = findFutureFeatureEvents(thisFeatureEvent)
+            def previousEvents = findPreviousFeatureEvents(thisFeatureEvent)
+
+
+            JSONArray history = new JSONArray();
+            jsonFeature.put(FeatureStringEnum.HISTORY.value, history);
+            List<List<FeatureEvent>> transactionList = new ArrayList<>()
+            transactionList.addAll(previousEvents)
+            transactionList.add(currentFeatureEventList)
+            transactionList.addAll(futureEvents)
+
+
+            for (int j = 0; j < transactionList.size(); ++j) {
+                // not sure if this is correct, or if I should just add both?
+                List<FeatureEvent> transactionSet = transactionList[j]
+                FeatureEvent transaction = transactionSet.find() { it.uniqueName == uniqueName }
+                transaction = transaction ?: transactionSet.first()
+                // prefer a predecessor that is not "ADD_TRANSCRIPT"?
+
+                JSONObject historyItem = new JSONObject();
+                historyItem.put(AbstractApolloController.REST_OPERATION, transaction.operation.name());
+                historyItem.put(FeatureStringEnum.EDITOR.value, transaction.getEditor()?.username);
+                historyItem.put(FeatureStringEnum.DATE.value, dateFormat.format(transaction.dateCreated));
+                if (transaction.current) {
+                    historyItem.put(FeatureStringEnum.CURRENT.value, true);
+                } else {
+                    historyItem.put(FeatureStringEnum.CURRENT.value, false);
+                }
+                JSONArray historyFeatures = new JSONArray();
+                historyItem.put(FeatureStringEnum.FEATURES.value, historyFeatures);
+
+                if (transaction.newFeaturesJsonArray) {
+                    JSONArray newFeaturesJsonArray = (JSONArray) JSON.parse(transaction.newFeaturesJsonArray)
+                    for (int featureIndex = 0; featureIndex < newFeaturesJsonArray.size(); featureIndex++) {
+                        JSONObject featureJsonObject = newFeaturesJsonArray.getJSONObject(featureIndex)
+                        historyFeatures.put(featureJsonObject);
+                    }
+                    history.put(historyItem);
+                }
+            }
+            historyContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(jsonFeature);
+        }
+        return historyContainer
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FeaturePropertyService.groovy b/grails-app/services/org/bbop/apollo/FeaturePropertyService.groovy
new file mode 100644
index 0000000..700b3ed
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FeaturePropertyService.groovy
@@ -0,0 +1,114 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import grails.transaction.NotTransactional
+
+class FeaturePropertyService {
+
+
+    public static List<String> nonReservedClasses = ["Comment","Owner","Description","Symbol","Status","ReadthroughStopCodon"]
+
+
+/** Get comments for this feature.
+ *
+ * @return Comments for this feature
+ */
+    @Transactional
+    public Collection<Comment> getComments(Feature feature) {
+        List<Comment> comments = new ArrayList<Comment>();
+
+        // TODO: move out of loop and into own service method
+        for (FeatureProperty fp : feature.getFeatureProperties()) {
+            if (Comment.ontologyId == fp.ontologyId) {
+                comments.add((Comment) fp);
+            }
+        }
+
+        Collections.sort(comments, new Comparator<Comment>() {
+            public int compare(Comment comment1, Comment comment2) {
+                if (comment1.getType().equals(comment2.getType())) {
+                    return new Integer(comment1.getRank()).compareTo(comment2.getRank());
+                }
+                return new Integer(comment1.hashCode()).compareTo(comment2.hashCode());
+            }
+        });
+        return comments;
+    }
+
+    @Transactional
+    def addComment(Feature feature, Comment comment) {
+        addProperty(feature, comment)
+    }
+
+    @Transactional
+    def addComment(Feature feature, String commentString) {
+        Comment comment = new Comment(
+                feature: feature
+                ,value: commentString
+        ).save()
+        feature.addToFeatureProperties(comment)
+        feature.save()
+    }
+
+    @Transactional
+    boolean deleteComment(Feature feature, String commentString) {
+        Comment comment = Comment.findByFeatureAndValue(feature, commentString)
+        if (comment) {
+            feature.removeFromFeatureProperties(comment)
+            Comment.deleteAll(comment)
+            return true
+        }
+        return false
+    }
+
+    @Transactional
+    def setFeatureProperty(Feature feature,String type,String tag,String value){
+
+        for(FeatureProperty featureProperty in feature.featureProperties){
+            if(featureProperty.type.name == type){
+                featureProperty.tag = tag
+                featureProperty.value = value
+                featureProperty.save(flush: true)
+                return true
+            }
+        }
+
+        feature.addToFeatureProperties()
+
+        return false
+
+    }
+
+    @Transactional
+    def addProperty(Feature feature, FeatureProperty property) {
+        int rank = 0;
+        log.debug "value of FP to add: ${property.value} ${property.tag}"
+        for (FeatureProperty fp : feature.getFeatureProperties()) {
+            if (fp.getType().equals(property.getType())) {
+                if (fp.getRank() > rank) {
+                    rank = fp.getRank();
+                }
+            }
+        }
+        property.setRank(rank + 1);
+        boolean ok = feature.addToFeatureProperties(property);
+
+    }
+
+    @Transactional
+    public boolean deleteProperty(Feature feature, FeatureProperty property) {
+        for (FeatureProperty fp : feature.getFeatureProperties()) {
+            if (fp.getType().equals(property.getType()) && fp.getValue().equals(property.getValue())) {
+                feature.getFeatureProperties().remove(fp);
+                return true;
+            }
+        }
+    }
+
+    @Transactional
+    def getNonReservedProperties(Feature feature) {
+        return FeatureProperty.findAllByFeature(feature).findAll(){
+            return !nonReservedClasses.contains(it.cvTerm)
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FeatureRelationshipService.groovy b/grails-app/services/org/bbop/apollo/FeatureRelationshipService.groovy
new file mode 100644
index 0000000..6eb855f
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FeatureRelationshipService.groovy
@@ -0,0 +1,226 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+
+ at Transactional(readOnly = true)
+class FeatureRelationshipService {
+
+    List<Feature> getChildrenForFeatureAndTypes(Feature feature, String... ontologyIds) {
+        def list = new ArrayList<Feature>()
+        if (feature?.parentFeatureRelationships != null) {
+            feature.parentFeatureRelationships.each { it ->
+                if (ontologyIds.size() == 0 || (it && ontologyIds.contains(it.childFeature.ontologyId))) {
+                    list.push(it.childFeature)
+                }
+            }
+        }
+
+        return list
+    }
+
+
+    Feature getChildForFeature(Feature feature, String ontologyId) {
+        List<Feature> featureList = getChildrenForFeatureAndTypes(feature, ontologyId)
+
+        if (featureList.size() == 0) {
+            return null
+        }
+
+        if (featureList.size() > 1) {
+            log.error "More than one child feature relationships found for ${feature} and ID ${ontologyId}"
+            return null
+        }
+
+        return featureList.get(0)
+    }
+
+    Feature getParentForFeature(Feature feature, String... ontologyId) {
+
+        List<Feature> featureList = getParentsForFeature(feature, ontologyId)
+
+        if (featureList.size() == 0) {
+            return null
+        }
+
+        if (featureList.size() > 1) {
+            log.error "More than one feature relationships parent found for ${feature} and ID ${ontologyId}"
+            return null
+        }
+
+        return featureList.get(0)
+    }
+
+    List<Feature> getParentsForFeature(Feature feature, String... ontologyIds) {
+        def list = new ArrayList<Feature>()
+        if (feature?.childFeatureRelationships != null) {
+            feature.childFeatureRelationships.each { it ->
+                if (ontologyIds.size() == 0 || (it && ontologyIds.contains(it.parentFeature.ontologyId))) {
+                    list.push(it.parentFeature)
+                }
+            }
+        }
+
+        return list
+    }
+
+    @Transactional
+    def deleteRelationships(Feature feature, String parentOntologyId, String childOntologyId) {
+        deleteChildrenForTypes(feature, childOntologyId)
+        deleteParentForTypes(feature, parentOntologyId)
+    }
+
+    @Transactional
+    def setChildForType(Feature parentFeature, Feature childFeature) {
+        List<FeatureRelationship> results = FeatureRelationship.findAllByParentFeature(parentFeature).findAll() {
+            it.childFeature.ontologyId == childFeature.ontologyId
+        }
+
+
+        if (results.size() == 1) {
+            results.get(0).childFeature = childFeature
+            return true
+        } else {
+            if (results.size() == 0) {
+                log.info "No feature relationships exist for parent ${parentFeature} and child ${childFeature}"
+            }
+            if (results.size() > 1) {
+                log.warn "${results.size()} feature relationships exist for parent ${parentFeature} and child ${childFeature}"
+            }
+            return false
+        }
+
+    }
+
+    @Transactional
+    def deleteChildrenForTypes(Feature feature, String... ontologyIds) {
+        def criteria = FeatureRelationship.createCriteria()
+
+        def featureRelationships = criteria {
+            eq("parentFeature", feature)
+        }.findAll() {
+            ontologyIds.length == 0 || it.childFeature.ontologyId in ontologyIds
+        }
+
+        int numRelationships = featureRelationships.size()
+        for (int i = 0; i < numRelationships; i++) {
+            FeatureRelationship featureRelationship = featureRelationships.get(i)
+            removeFeatureRelationship(featureRelationship.parentFeature, featureRelationship.childFeature)
+        }
+    }
+
+    @Transactional
+    def deleteParentForTypes(Feature feature, String... ontologyIds) {
+        // delete transcript -> non canonical 3' splice site child relationship
+        def criteria = FeatureRelationship.createCriteria()
+
+        criteria {
+            eq("childFeature", feature)
+        }.findAll() {
+            it.parentFeature.ontologyId in ontologyIds
+        }.each {
+            feature.removeFromChildFeatureRelationships(it)
+        }
+
+    }
+
+    // based on Transcript.setCDS
+    @Transactional
+    def addChildFeature(Feature parent, Feature child, boolean replace = true) {
+
+        // replace if of the same type
+        if (replace) {
+            boolean found = false
+            def criteria = FeatureRelationship.createCriteria()
+            criteria {
+                eq("parentFeature", parent)
+            }
+            .findAll() {
+                it.childFeature.ontologyId == child.ontologyId
+            }
+            .each {
+                found = true
+                it.childFeature = child
+                it.save()
+                return
+            }
+
+            if (found) {
+                return
+            }
+
+        }
+
+
+
+        FeatureRelationship fr = new FeatureRelationship(
+                parentFeature: parent
+                , childFeature: child
+        ).save(flush: true);
+        parent.addToParentFeatureRelationships(fr)
+        child.addToChildFeatureRelationships(fr)
+        child.save(flush: true)
+        parent.save(flush: true)
+    }
+
+    @Transactional
+    public void removeFeatureRelationship(Feature parentFeature, Feature childFeature) {
+
+        FeatureRelationship featureRelationship = FeatureRelationship.findByParentFeatureAndChildFeature(parentFeature, childFeature)
+        if (featureRelationship) {
+            parentFeature.parentFeatureRelationships?.remove(featureRelationship)
+            childFeature.childFeatureRelationships?.remove(featureRelationship)
+            parentFeature.save(flush: true)
+            childFeature.save(flush: true)
+        }
+    }
+
+    List<Frameshift> getFeaturePropertyForTypes(Transcript transcript, List<String> strings) {
+        return (List<Frameshift>) FeatureProperty.findAllByFeaturesInListAndOntologyIdsInList([transcript], strings)
+    }
+
+    List<Feature> getChildren(Feature feature) {
+        def exonRelations = feature.parentFeatureRelationships.findAll()
+        return exonRelations.collect { it ->
+            it.childFeature
+        }
+    }
+
+    /**
+     * Iterate to all of the children, and delete the child and thereby the feature relationship automatically.
+     *
+     *
+     * @param feature
+     * @return
+     */
+    @Transactional
+    def deleteFeatureAndChildren(Feature feature) {
+
+        // if grandchildren then delete those
+        for (FeatureRelationship featureRelationship in feature.parentFeatureRelationships) {
+            if (featureRelationship.childFeature?.parentFeatureRelationships) {
+                deleteFeatureAndChildren(featureRelationship.childFeature)
+            }
+        }
+
+        // create a list of relationships to remove (assume we have no grandchildren here)
+        List<FeatureRelationship> relationshipsToRemove = []
+        for (FeatureRelationship featureRelationship in feature.parentFeatureRelationships) {
+            relationshipsToRemove << featureRelationship
+        }
+
+        // actually delete those
+        relationshipsToRemove.each {
+            it.childFeature.delete()
+            feature.removeFromParentFeatureRelationships(it)
+            it.delete()
+        }
+
+        // last, delete self or save updated relationships
+        if (!feature.parentFeatureRelationships && !feature.childFeatureRelationships) {
+            feature.delete(flush: true)
+        } else {
+            feature.save(flush: true)
+        }
+
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FeatureService.groovy b/grails-app/services/org/bbop/apollo/FeatureService.groovy
new file mode 100644
index 0000000..8bf42ac
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FeatureService.groovy
@@ -0,0 +1,2977 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.alteration.SequenceAlterationInContext
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.Strand
+import org.bbop.apollo.sequence.TranslationTable
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+import org.hibernate.FlushMode
+
+ at Transactional(readOnly = true)
+class FeatureService {
+
+
+    def nameService
+    def configWrapperService
+    def featureService
+    def transcriptService
+    def exonService
+    def cdsService
+    def nonCanonicalSplitSiteService
+    def featureRelationshipService
+    def featurePropertyService
+    def sequenceService
+    def permissionService
+    def overlapperService
+    def organismService
+    def sessionFactory
+
+    public static final def rnaFeatureTypes = [MRNA.alternateCvTerm,MiRNA.alternateCvTerm,NcRNA.alternateCvTerm, RRNA.alternateCvTerm, SnRNA.alternateCvTerm, SnoRNA.alternateCvTerm, TRNA.alternateCvTerm, Transcript.alternateCvTerm]
+    public static final def singletonFeatureTypes = [RepeatRegion.alternateCvTerm, TransposableElement.alternateCvTerm]
+    @Timed
+    @Transactional
+    FeatureLocation convertJSONToFeatureLocation(JSONObject jsonLocation, Sequence sequence, int defaultStrand = Strand.POSITIVE.value) throws JSONException {
+        FeatureLocation gsolLocation = new FeatureLocation();
+        if (jsonLocation.has(FeatureStringEnum.ID.value)) {
+            gsolLocation.setId(jsonLocation.getLong(FeatureStringEnum.ID.value));
+        }
+        gsolLocation.setFmin(jsonLocation.getInt(FeatureStringEnum.FMIN.value));
+        gsolLocation.setFmax(jsonLocation.getInt(FeatureStringEnum.FMAX.value));
+        if (jsonLocation.getInt(FeatureStringEnum.STRAND.value) == Strand.POSITIVE.value || jsonLocation.getInt(FeatureStringEnum.STRAND.value) == Strand.NEGATIVE.value) {
+            gsolLocation.setStrand(jsonLocation.getInt(FeatureStringEnum.STRAND.value));
+        }
+        else {
+            gsolLocation.setStrand(defaultStrand)
+        }
+        gsolLocation.setSequence(sequence)
+        return gsolLocation;
+    }
+
+    /** Get features that overlap a given location.
+     *
+     * @param location - FeatureLocation that the features overlap
+     * @param compareStrands - Whether to compare strands in overlap
+     * @return Collection of Feature objects that overlap the FeatureLocation
+     */
+    Collection<Transcript> getOverlappingTranscripts(FeatureLocation location, boolean compareStrands = true) {
+        List<Transcript> transcriptList = new ArrayList<>()
+        List<Transcript> overlappingFeaturesList = getOverlappingFeatures(location, compareStrands)
+
+        for (Feature eachFeature : overlappingFeaturesList) {
+            Feature feature = Feature.get(eachFeature.id)
+            if (feature instanceof Transcript) {
+                transcriptList.add((Transcript) feature)
+            }
+        }
+
+        return transcriptList
+    }
+
+    /** Get features that overlap a given location.
+     *
+     * @param location - FeatureLocation that the features overlap
+     * @param compareStrands - Whether to compare strands in overlap
+     * @return Collection of Feature objects that overlap the FeatureLocation
+     */
+    Collection<Feature> getOverlappingFeatures(FeatureLocation location, boolean compareStrands = true) {
+        if (compareStrands) {
+            //Feature.executeQuery("select distinct f from Feature f join f.featureLocations fl where fl.sequence = :sequence and fl.strand = :strand and ((fl.fmin <= :fmin and fl.fmax > :fmin) or (fl.fmin <= :fmax and fl.fmax >= :fmax ))",[fmin:location.fmin,fmax:location.fmax,strand:location.strand,sequence:location.sequence])
+            Feature.executeQuery("select distinct f from Feature f join f.featureLocations fl where fl.sequence = :sequence and fl.strand = :strand and ((fl.fmin <= :fmin and fl.fmax > :fmin) or (fl.fmin <= :fmax and fl.fmax >= :fmax) or (fl.fmin >= :fmin and fl.fmax <= :fmax))", [fmin: location.fmin, fmax: location.fmax, strand: location.strand, sequence: location.sequence])
+        } else {
+            //Feature.executeQuery("select distinct f from Feature f join f.featureLocations fl where fl.sequence = :sequence and ((fl.fmin <= :fmin and fl.fmax > :fmin) or (fl.fmin <= :fmax and fl.fmax >= :fmax ))",[fmin:location.fmin,fmax:location.fmax,sequence:location.sequence])
+            Feature.executeQuery("select distinct f from Feature f join f.featureLocations fl where fl.sequence = :sequence and ((fl.fmin <= :fmin and fl.fmax > :fmin) or (fl.fmin <= :fmax and fl.fmax >= :fmax) or (fl.fmin >= :fmin and fl.fmax <= :fmax))", [fmin: location.fmin, fmax: location.fmax, sequence: location.sequence])
+        }
+    }
+
+    def getOverlappingSequenceAlterations(Sequence sequence, int fmin, int fmax) {
+        Feature.executeQuery(
+                "SELECT DISTINCT sa FROM SequenceAlteration sa JOIN sa.featureLocations fl WHERE fl.sequence = :sequence AND ((fl.fmin <= :fmin AND fl.fmax > :fmin) OR (fl.fmin <= :fmax AND fl.fmax >= :fmax) OR (fl.fmin >= :fmin AND fl.fmax <= :fmax))",
+                [fmin: fmin, fmax: fmax, sequence: sequence]
+        )
+    }
+
+    @Transactional
+    void updateNewGsolFeatureAttributes(Feature gsolFeature, Sequence sequence = null) {
+
+        gsolFeature.setIsAnalysis(false);
+        gsolFeature.setIsObsolete(false);
+
+        if (sequence) {
+            gsolFeature.getFeatureLocations().iterator().next().sequence = sequence;
+        }
+
+        // TODO: this may be a mistake, is different than the original code
+        // you are iterating through all of the children in order to set the SourceFeature and analysis
+        // for (FeatureRelationship fr : gsolFeature.getChildFeatureRelationships()) {
+        for (FeatureRelationship fr : gsolFeature.getParentFeatureRelationships()) {
+            updateNewGsolFeatureAttributes(fr.getChildFeature(), sequence);
+        }
+    }
+
+    @Transactional
+    def setOwner(Feature feature, User owner) {
+        if (owner && feature) {
+            log.debug "setting owner for feature ${feature} to ${owner}"
+            feature.addToOwners(owner)
+        } else {
+            log.warn "user ${owner} or feature ${feature} is null so not setting"
+        }
+    }
+
+    /**
+     * From Gene.addTranscript
+     * @return
+     */
+
+    @Timed
+    @Transactional
+    def generateTranscript(JSONObject jsonTranscript, Sequence sequence, boolean suppressHistory, boolean useCDS = configWrapperService.useCDS(), boolean useName = false) {
+        Gene gene = jsonTranscript.has(FeatureStringEnum.PARENT_ID.value) ? (Gene) Feature.findByUniqueName(jsonTranscript.getString(FeatureStringEnum.PARENT_ID.value)) : null;
+        Transcript transcript = null
+        boolean readThroughStopCodon = false
+
+        User owner = permissionService.getCurrentUser(jsonTranscript)
+        // if the gene is set, then don't process, just set the transcript for the found gene
+        if (gene) {
+            // Scenario I - if 'parent_id' attribute is given then find the gene
+            transcript = (Transcript) convertJSONToFeature(jsonTranscript, sequence);
+            if (transcript.getFmin() < 0 || transcript.getFmax() < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates")
+            }
+
+            setOwner(transcript, owner);
+
+            CDS cds = transcriptService.getCDS(transcript)
+            if (cds) {
+                readThroughStopCodon = cdsService.getStopCodonReadThrough(cds) ? true : false
+            }
+
+            if (!useCDS || cds == null) {
+                calculateCDS(transcript, readThroughStopCodon)
+            }
+            else {
+                // if there are any sequence alterations that overlaps this transcript then
+                // recalculate the CDS to account for these changes
+                def sequenceAlterations = getSequenceAlterationsForFeature(transcript)
+                if (sequenceAlterations.size() > 0) {
+                    calculateCDS(transcript)
+                }
+            }
+
+            addTranscriptToGene(gene, transcript);
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript);
+            if (!suppressHistory) {
+                transcript.name = nameService.generateUniqueName(transcript)
+            }
+            // setting back the original name for transcript
+            if (useName && jsonTranscript.has(FeatureStringEnum.NAME.value)) {
+                transcript.name = jsonTranscript.get(FeatureStringEnum.NAME.value)
+            }
+        } else {
+            // Scenario II - find an overlapping isoform and if present, add current transcript to its gene
+            FeatureLocation featureLocation = convertJSONToFeatureLocation(jsonTranscript.getJSONObject(FeatureStringEnum.LOCATION.value), sequence)
+            Collection<Feature> overlappingFeatures = getOverlappingFeatures(featureLocation).findAll() {
+                it = Feature.get(it.id)
+                it instanceof Gene
+            }
+
+            log.debug "overlapping features: ${overlappingFeatures.size()}"
+            for (Feature eachFeature : overlappingFeatures) {
+                // get the proper object instead of its proxy, due to lazy loading
+                Feature feature = Feature.get(eachFeature.id)
+                log.debug "evaluating overlap of feature ${feature.name} of class ${feature.class.name}"
+
+                if (!gene && feature instanceof Gene && !(feature instanceof Pseudogene)) {
+                    Gene tmpGene = (Gene) feature;
+                    log.debug "found an overlapping gene ${tmpGene}"
+                    // removing name from transcript JSON since its naming will be based off of the overlapping gene
+                    Transcript tmpTranscript
+                    if (jsonTranscript.has(FeatureStringEnum.NAME.value)) {
+                        String originalName = jsonTranscript.get(FeatureStringEnum.NAME.value)
+                        jsonTranscript.remove(FeatureStringEnum.NAME.value)
+                        tmpTranscript = (Transcript) convertJSONToFeature(jsonTranscript, sequence);
+                        jsonTranscript.put(FeatureStringEnum.NAME.value, originalName)
+                        updateNewGsolFeatureAttributes(tmpTranscript)
+                    }
+                    else {
+                        tmpTranscript = (Transcript) convertJSONToFeature(jsonTranscript, sequence);
+                        updateNewGsolFeatureAttributes(tmpTranscript)
+                    }
+
+                    if (tmpTranscript.getFmin() < 0 || tmpTranscript.getFmax() < 0) {
+                        throw new AnnotationException("Feature cannot have negative coordinates");
+                    }
+
+                    //this one is working, but was marked as needing improvement
+                    setOwner(tmpTranscript, owner);
+
+                    CDS cds = transcriptService.getCDS(tmpTranscript)
+                    if (cds) {
+                        readThroughStopCodon = cdsService.getStopCodonReadThrough(cds) ? true : false
+                    }
+
+                    if (!useCDS || cds == null) {
+                        calculateCDS(tmpTranscript, readThroughStopCodon)
+                    }
+                    else {
+                        // if there are any sequence alterations that overlaps this transcript then
+                        // recalculate the CDS to account for these changes
+                        def sequenceAlterations = getSequenceAlterationsForFeature(tmpTranscript)
+                        if (sequenceAlterations.size() > 0) {
+                            calculateCDS(tmpTranscript)
+                        }
+                    }
+
+                    if (!suppressHistory) {
+                        tmpTranscript.name = nameService.generateUniqueName(tmpTranscript, tmpGene.name)
+                    }
+
+                    // setting back the original name for transcript
+                    if (useName && jsonTranscript.has(FeatureStringEnum.NAME.value)) {
+                        tmpTranscript.name = jsonTranscript.get(FeatureStringEnum.NAME.value)
+                    }
+
+                    if (tmpTranscript && tmpGene && overlapperService.overlaps(tmpTranscript, tmpGene)) {
+                        log.debug "There is an overlap, adding to an existing gene"
+                        transcript = tmpTranscript;
+                        gene = tmpGene;
+                        addTranscriptToGene(gene, transcript)
+                        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript);
+                        transcript.save()
+
+                        if (jsonTranscript.has(FeatureStringEnum.PARENT.value)) {
+                            // use metadata of incoming transcript's gene
+                            JSONObject jsonGene = jsonTranscript.getJSONObject(FeatureStringEnum.PARENT.value)
+                            if (jsonGene.has(FeatureStringEnum.DBXREFS.value)) {
+                                // parse dbxrefs
+                                JSONArray dbxrefs = jsonGene.getJSONArray(FeatureStringEnum.DBXREFS.value)
+                                for (JSONObject dbxref : dbxrefs) {
+                                    String dbString = dbxref.get(FeatureStringEnum.DB.value).name
+                                    String accessionString = dbxref.get(FeatureStringEnum.ACCESSION.value)
+                                    // TODO: needs improvement
+                                    boolean exists = false
+                                    tmpGene.featureDBXrefs.each {
+                                        if (it.db.name == dbString && it.accession == accessionString) {
+                                            exists = true
+                                        }
+                                    }
+                                    if (!exists) {
+                                        addNonPrimaryDbxrefs(tmpGene, dbString, accessionString)
+                                    }
+                                }
+                            }
+                            tmpGene.save()
+
+                            if (jsonGene.has(FeatureStringEnum.PROPERTIES.value)) {
+                                // parse properties
+                                JSONArray featureProperties = jsonGene.getJSONArray(FeatureStringEnum.PROPERTIES.value)
+                                for (JSONObject featureProperty : featureProperties) {
+                                    String tagString = featureProperty.get(FeatureStringEnum.TYPE.value).name
+                                    String valueString = featureProperty.get(FeatureStringEnum.VALUE.value)
+                                    // TODO: needs improvement
+                                    boolean exists = false
+                                    tmpGene.featureProperties.each {
+                                        if (it instanceof Comment) {
+                                            exists = true
+                                        }
+                                        else if (it.tag == tagString && it.value == valueString) {
+                                            exists = true
+                                        }
+                                    }
+                                    if (!exists) {
+                                        if (tagString == FeatureStringEnum.COMMENT.value) {
+                                            // if FeatureProperty is a comment
+                                            featurePropertyService.addComment(tmpGene, valueString)
+                                        }
+                                        else {
+                                            addNonReservedProperties(tmpGene, tagString, valueString)
+                                        }
+                                    }
+                                }
+                            }
+                            tmpGene.save()
+                        }
+                        gene.save(insert: false, flush: true)
+                        break;
+                    } else {
+                        featureRelationshipService.deleteFeatureAndChildren(tmpTranscript)
+                        log.debug "There is no overlap, we are going to return a NULL gene and a NULL transcript "
+                    }
+                } else {
+                    log.error "Feature is not an instance of a gene or is a pseudogene"
+                }
+            }
+        }
+        if (gene == null) {
+            log.debug "gene is null"
+            // Scenario III - create a de-novo gene
+            JSONObject jsonGene = new JSONObject();
+            if (jsonTranscript.has(FeatureStringEnum.PARENT.value)) {
+                // Scenario IIIa - use the 'parent' attribute, if provided, from transcript JSON
+                jsonGene = JSON.parse(jsonTranscript.getString(FeatureStringEnum.PARENT.value)) as JSONObject
+                jsonGene.put(FeatureStringEnum.CHILDREN.value, new JSONArray().put(jsonTranscript))
+            }
+            else {
+                // Scenario IIIb - use the current mRNA's featurelocation for gene
+                jsonGene.put(FeatureStringEnum.CHILDREN.value, new JSONArray().put(jsonTranscript));
+                jsonGene.put(FeatureStringEnum.LOCATION.value, jsonTranscript.getJSONObject(FeatureStringEnum.LOCATION.value));
+                String cvTermString = FeatureStringEnum.GENE.value
+                jsonGene.put(FeatureStringEnum.TYPE.value, convertCVTermToJSON(FeatureStringEnum.CV.value, cvTermString));
+            }
+
+            String geneName = null
+            if (jsonGene.has(FeatureStringEnum.NAME.value)) {
+                geneName = jsonGene.get(FeatureStringEnum.NAME.value)
+            }
+            else if (jsonTranscript.has(FeatureStringEnum.NAME.value)) {
+                geneName = jsonTranscript.getString(FeatureStringEnum.NAME.value)
+            }
+            else {
+//                geneName = nameService.makeUniqueFeatureName(sequence.organism, sequence.name, new LetterPaddingStrategy(), false)
+                geneName = nameService.makeUniqueGeneName(sequence.organism, sequence.name, false)
+            }
+            if (!suppressHistory) {
+//                geneName = nameService.makeUniqueFeatureName(sequence.organism, geneName, new LetterPaddingStrategy(), true)
+                geneName = nameService.makeUniqueGeneName(sequence.organism, geneName, true)
+            }
+            // set back to the original gene name
+            if (jsonTranscript.has(FeatureStringEnum.GENE_NAME.value)) {
+                geneName = jsonTranscript.getString(FeatureStringEnum.GENE_NAME.value)
+            }
+            jsonGene.put(FeatureStringEnum.NAME.value, geneName)
+
+            gene = (Gene) convertJSONToFeature(jsonGene, sequence);
+            updateNewGsolFeatureAttributes(gene, sequence);
+
+            if (gene.getFmin() < 0 || gene.getFmax() < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates");
+            }
+            transcript = transcriptService.getTranscripts(gene).iterator().next();
+            CDS cds = transcriptService.getCDS(transcript)
+            if (cds) {
+                readThroughStopCodon = cdsService.getStopCodonReadThrough(cds) ? true : false
+            }
+
+            if (!useCDS || cds == null) {
+                calculateCDS(transcript, readThroughStopCodon)
+            }
+            else {
+                // if there are any sequence alterations that overlaps this transcript then
+                // recalculate the CDS to account for these changes
+                def sequenceAlterations = getSequenceAlterationsForFeature(transcript)
+                if (sequenceAlterations.size() > 0) {
+                    calculateCDS(transcript)
+                }
+            }
+            removeExonOverlapsAndAdjacenciesForFeature(gene)
+            if (!suppressHistory) {
+                transcript.name = nameService.generateUniqueName(transcript)
+            }
+
+            // set back the original transcript name
+            if (useName && jsonTranscript.has(FeatureStringEnum.NAME.value)) {
+                transcript.name = jsonTranscript.getString(FeatureStringEnum.NAME.value)
+            }
+
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript);
+            gene.save(insert: true)
+            transcript.save(flush: true)
+
+            // doesn't work well for testing
+            setOwner(gene, owner);
+            setOwner(transcript, owner);
+        }
+        return transcript;
+    }
+
+// TODO: this is kind of a hack for now
+    JSONObject convertCVTermToJSON(String cv, String cvTerm) {
+        JSONObject jsonCVTerm = new JSONObject();
+        JSONObject jsonCV = new JSONObject();
+        jsonCVTerm.put(FeatureStringEnum.CV.value, jsonCV);
+        jsonCV.put(FeatureStringEnum.NAME.value, cv);
+        jsonCVTerm.put(FeatureStringEnum.NAME.value, cvTerm);
+        return jsonCVTerm;
+    }
+
+    /**
+     * TODO: Should be the same result as the older method, need to check:
+     *
+     *         if (transcript.getGene() != null) {return transcript.getGene();}return transcript;
+     * @param feature
+     * @return
+     */
+    @Timed
+    Feature getTopLevelFeature(Feature feature) {
+        Collection<Feature> parents = feature?.childFeatureRelationships*.parentFeature
+        if (parents) {
+            return getTopLevelFeature(parents.iterator().next());
+        } else {
+            return feature;
+        }
+    }
+
+
+    @Timed
+    @Transactional
+    def removeExonOverlapsAndAdjacenciesForFeature(Feature feature) {
+        if (feature instanceof Gene) {
+            for (Transcript transcript : transcriptService.getTranscripts((Gene) feature)) {
+                removeExonOverlapsAndAdjacencies(transcript);
+            }
+        } else if (feature instanceof Transcript) {
+            removeExonOverlapsAndAdjacencies((Transcript) feature);
+        }
+    }
+
+    @Transactional
+    def addTranscriptToGene(Gene gene, Transcript transcript) {
+        removeExonOverlapsAndAdjacencies(transcript);
+        // no feature location, set location to transcript's
+        if (gene.getFeatureLocation() == null) {
+            FeatureLocation transcriptFeatureLocation = transcript.getFeatureLocation()
+            FeatureLocation featureLocation = new FeatureLocation()
+            featureLocation.properties = transcriptFeatureLocation.properties
+            featureLocation.id = null
+            featureLocation.save()
+            gene.addToFeatureLocations(featureLocation);
+        } else {
+            // if the transcript's bounds are beyond the gene's bounds, need to adjust the gene's bounds
+            if (transcript.getFeatureLocation().getFmin() < gene.getFeatureLocation().getFmin()) {
+                gene.getFeatureLocation().setFmin(transcript.getFeatureLocation().getFmin());
+            }
+            if (transcript.getFeatureLocation().getFmax() > gene.getFeatureLocation().getFmax()) {
+                gene.getFeatureLocation().setFmax(transcript.getFeatureLocation().getFmax());
+            }
+        }
+
+        // add transcript
+        FeatureRelationship featureRelationship = new FeatureRelationship(
+                parentFeature: gene
+                , childFeature: transcript
+        ).save(failOnError: true)
+        gene.addToParentFeatureRelationships(featureRelationship)
+        transcript.addToChildFeatureRelationships(featureRelationship)
+
+
+        updateGeneBoundaries(gene);
+
+//        getSession().indexFeature(transcript);
+
+        // event fire
+//        TODO: determine event model?
+//        fireAnnotationChangeEvent(transcript, gene, AnnotationChangeEvent.Operation.ADD);
+    }
+
+    /**
+     * TODO:  this is an N^2  search of overlapping exons
+     * @param transcript
+     * @return
+     */
+    @Transactional
+    def removeExonOverlapsAndAdjacencies(Transcript transcript) throws AnnotationException {
+        List<Exon> sortedExons = transcriptService.getSortedExons(transcript,false)
+        if (!sortedExons || sortedExons?.size() <= 1) {
+            return;
+        }
+        Collections.sort(sortedExons, new FeaturePositionComparator<Exon>(false))
+        int inc = 1;
+        for (int i = 0; i < sortedExons.size() - 1; i += inc) {
+            inc = 1;
+            Exon leftExon = sortedExons.get(i);
+            for (int j = i + 1; j < sortedExons.size(); ++j) {
+                Exon rightExon = sortedExons.get(j);
+                if (overlapperService.overlaps(leftExon, rightExon) || isAdjacentTo(leftExon.getFeatureLocation(), rightExon.getFeatureLocation())) {
+                    try {
+                        exonService.mergeExons(leftExon, rightExon);
+                        sortedExons = transcriptService.getSortedExons(transcript,false)
+                        // we have to reload the sortedExons again and start over
+                        ++inc;
+                    } catch (AnnotationException e) {
+                        // we should probably just re-throw this
+                        log.error(e)
+                        throw e
+                    }
+                }
+            }
+        }
+    }
+
+/** Checks whether this AbstractSimpleLocationBioFeature is adjacent to the FeatureLocation.
+ *
+ * @param location - FeatureLocation to check adjacency against
+ * @return true if there is adjacency
+ */
+    boolean isAdjacentTo(FeatureLocation leftLocation, FeatureLocation location) {
+        return isAdjacentTo(leftLocation, location, true);
+    }
+
+    boolean isAdjacentTo(FeatureLocation leftFeatureLocation, FeatureLocation rightFeatureLocation, boolean compareStrands) {
+        if (leftFeatureLocation.sequence != rightFeatureLocation.sequence) {
+            return false;
+        }
+        int thisFmin = leftFeatureLocation.getFmin();
+        int thisFmax = leftFeatureLocation.getFmax();
+        int thisStrand = leftFeatureLocation.getStrand();
+        int otherFmin = rightFeatureLocation.getFmin();
+        int otherFmax = rightFeatureLocation.getFmax();
+        int otherStrand = rightFeatureLocation.getStrand();
+        boolean strandsOverlap = compareStrands ? thisStrand == otherStrand : true;
+        if (strandsOverlap &&
+                (thisFmax == otherFmin ||
+                        thisFmin == otherFmax)) {
+            return true;
+        }
+        return false;
+    }
+
+
+    @Transactional
+    def calculateCDS(Transcript transcript) {
+        // NOTE: isPseudogene call seemed redundant with isProtenCoding
+        CDS cds = transcriptService.getCDS(transcript)
+        calculateCDS(transcript, cdsService.hasStopCodonReadThrough(cds))
+//        if (transcriptService.isProteinCoding(transcript) && (transcriptService.getGene(transcript) == null)) {
+////            calculateCDS(editor, transcript, transcript.getCDS() != null ? transcript.getCDS().getStopCodonReadThrough() != null : false);
+////            calculateCDS(transcript, transcript.getCDS() != null ? transcript.getCDS().getStopCodonReadThrough() != null : false);
+//            calculateCDS(transcript, transcriptService.getCDS(transcript) != null ? transcriptService.getStopCodonReadThrough(transcript) != null : false);
+//        }
+    }
+
+    @Timed
+    @Transactional
+    def calculateCDS(Transcript transcript, boolean readThroughStopCodon) {
+        CDS cds = transcriptService.getCDS(transcript);
+        log.info "calculateCDS"
+        if (cds == null) {
+            setLongestORF(transcript, readThroughStopCodon);
+            return;
+        }
+        boolean manuallySetStart = cdsService.isManuallySetTranslationStart(cds);
+        boolean manuallySetEnd = cdsService.isManuallySetTranslationEnd(cds);
+        if (manuallySetStart && manuallySetEnd) {
+            return;
+        }
+        if (!manuallySetStart && !manuallySetEnd) {
+            setLongestORF(transcript, readThroughStopCodon);
+        } else if (manuallySetStart) {
+            setTranslationStart(transcript, cds.getFeatureLocation().getStrand().equals(-1) ? cds.getFmax() - 1 : cds.getFmin(), true, readThroughStopCodon);
+        } else {
+            setTranslationEnd(transcript, cds.getFeatureLocation().getStrand().equals(-1) ? cds.getFmin() : cds.getFmax() - 1, true);
+        }
+    }
+
+/**
+ * Calculate the longest ORF for a transcript.  If a valid start codon is not found, allow for partial CDS start/end.
+ * Calls setLongestORF(Transcript, TranslationTable, boolean) with the translation table and whether partial
+ * ORF calculation extensions are allowed from the configuration associated with this editor.
+ *
+ * @param transcript - Transcript to set the longest ORF to
+ */
+    @Transactional
+    void setLongestORF(Transcript transcript) {
+        log.debug "setLongestORF(transcript) ${transcript}"
+        setLongestORF(transcript, false);
+    }
+
+/**
+ * Set the translation start in the transcript.  Sets the translation start in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationStart - Coordinate of the start of translation
+ */
+    @Transactional
+    void setTranslationStart(Transcript transcript, int translationStart) {
+        log.debug "setTranslationStart"
+        setTranslationStart(transcript, translationStart, false);
+    }
+
+/**
+ * Set the translation start in the transcript.  Sets the translation start in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationStart - Coordinate of the start of translation
+ * @param setTranslationEnd - if set to true, will search for the nearest in frame stop codon
+ */
+    @Transactional
+    void setTranslationStart(Transcript transcript, int translationStart, boolean setTranslationEnd) {
+        log.debug "setTranslationStart(transcript,translationStart,translationEnd)"
+        setTranslationStart(transcript, translationStart, setTranslationEnd, false);
+    }
+
+/**
+ * Set the translation start in the transcript.  Sets the translation start in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationStart - Coordinate of the start of translation
+ * @param setTranslationEnd - if set to true, will search for the nearest in frame stop codon
+ * @param readThroughStopCodon - if set to true, will read through the first stop codon to the next
+ */
+    @Transactional
+    void setTranslationStart(Transcript transcript, int translationStart, boolean setTranslationEnd, boolean readThroughStopCodon) {
+        log.debug "setTranslationStart(transcript,translationStart,translationEnd,readThroughStopCodon)"
+        setTranslationStart(transcript, translationStart, setTranslationEnd, setTranslationEnd ? organismService.getTranslationTable(transcript.featureLocation.sequence.organism) : null, readThroughStopCodon);
+    }
+
+    /** Convert local coordinate to source feature coordinate.
+     *
+     * @param localCoordinate - Coordinate to convert to source coordinate
+     * @return Source feature coordinate, -1 if local coordinate is longer than feature's length or negative
+     */
+    int convertLocalCoordinateToSourceCoordinate(Feature feature, int localCoordinate) {
+        log.debug "convertLocalCoordinateToSourceCoordinate"
+
+        if (localCoordinate < 0 || localCoordinate > feature.getLength()) {
+            return -1;
+        }
+        if (feature.getFeatureLocation().getStrand() == -1) {
+            return feature.getFeatureLocation().getFmax() - localCoordinate - 1;
+        } else {
+            return feature.getFeatureLocation().getFmin() + localCoordinate;
+        }
+    }
+
+    int convertLocalCoordinateToSourceCoordinateForTranscript(Transcript transcript, int localCoordinate) {
+        // Method converts localCoordinate to sourceCoordinate in reference to the Transcript
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        int sourceCoordinate = -1;
+        if (exons.size() == 0) {
+            return convertLocalCoordinateToSourceCoordinate(transcript, localCoordinate);
+        }
+        int currentLength = 0;
+        int currentCoordinate = localCoordinate;
+        for (Exon exon : exons) {
+            int exonLength = exon.getLength();
+            if (currentLength + exonLength >= localCoordinate) {
+                if (transcript.getFeatureLocation().getStrand() == Strand.NEGATIVE.value) {
+                    sourceCoordinate = exon.getFeatureLocation().getFmax() - currentCoordinate - 1;
+                } else {
+                    sourceCoordinate = exon.getFeatureLocation().getFmin() + currentCoordinate;
+                }
+                break;
+            }
+            currentLength += exonLength;
+            currentCoordinate -= exonLength;
+        }
+        return sourceCoordinate;
+    }
+
+    int convertLocalCoordinateToSourceCoordinateForCDS(CDS cds, int localCoordinate) {
+        // Method converts localCoordinate to sourceCoordinate in reference to the CDS
+        Transcript transcript = transcriptService.getTranscript(cds)
+        if (!transcript) {
+            return convertLocalCoordinateToSourceCoordinate(cds, localCoordinate);
+        }
+        int offset = 0;
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        if (exons.size() == 0) {
+            log.debug "FS::convertLocalCoordinateToSourceCoordinateForCDS() - No exons for given transcript"
+            return convertLocalCoordinateToSourceCoordinate(cds, localCoordinate)
+        }
+        if (transcript.strand == Strand.NEGATIVE.value) {
+            exons.reverse()
+        }
+        for (Exon exon : exons) {
+            if (!overlapperService.overlaps(cds, exon)) {
+                offset += exon.getLength();
+                continue;
+            } else if (overlapperService.overlaps(cds, exon)) {
+                if (exon.fmin >= cds.fmin && exon.fmax <= cds.fmax) {
+                    // exon falls within the boundaries of the CDS
+                    continue
+                } else {
+                    // exon doesn't overlap completely with the CDS
+                    if (exon.fmin < cds.fmin && exon.strand == Strand.POSITIVE.value) {
+                        offset += cds.fmin - exon.fmin
+                    } else if (exon.fmax > cds.fmax && exon.strand == Strand.NEGATIVE.value) {
+                        offset += exon.fmax - cds.fmax
+                    }
+                }
+            }
+
+            if (exon.getFeatureLocation().getStrand() == Strand.NEGATIVE.value) {
+                offset += exon.getFeatureLocation().getFmax() - exon.getFeatureLocation().getFmax();
+            } else {
+                offset += exon.getFeatureLocation().getFmin() - exon.getFeatureLocation().getFmin();
+            }
+            break;
+        }
+        return convertLocalCoordinateToSourceCoordinateForTranscript(transcript, localCoordinate + offset);
+    }
+
+/**
+ * Set the translation start in the transcript.  Sets the translation start in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationStart - Coordinate of the start of translation
+ * @param setTranslationEnd - if set to true, will search for the nearest in frame stop codon
+ * @param translationTable - Translation table that defines the codon translation
+ * @param readThroughStopCodon - if set to true, will read through the first stop codon to the next
+ */
+    @Transactional
+    void setTranslationStart(Transcript transcript, int translationStart, boolean setTranslationEnd, TranslationTable translationTable, boolean readThroughStopCodon) {
+        CDS cds = transcriptService.getCDS(transcript);
+        if (cds == null) {
+            cds = transcriptService.createCDS(transcript);
+            featureRelationshipService.addChildFeature(transcript, cds)
+//            transcript.setCDS(cds);
+        }
+        FeatureLocation transcriptFeatureLocation = FeatureLocation.findByFeature(transcript)
+        if (transcriptFeatureLocation.strand == Strand.NEGATIVE.value) {
+            setFmax(cds, translationStart + 1);
+        } else {
+            setFmin(cds, translationStart);
+        }
+        cdsService.setManuallySetTranslationStart(cds, true);
+//        cds.deleteStopCodonReadThrough();
+        cdsService.deleteStopCodonReadThrough(cds);
+//        featureRelationshipService.deleteRelationships()
+
+        if (setTranslationEnd && translationTable != null) {
+            String mrna = getResiduesWithAlterationsAndFrameshifts(transcript);
+            if (mrna == null || mrna.equals("null")) {
+                return;
+            }
+            int stopCodonCount = 0;
+            for (int i = convertSourceCoordinateToLocalCoordinateForTranscript(transcript, translationStart); i < transcript.getLength(); i += 3) {
+                if (i < 0 || i + 3 > mrna.length()) {
+                    break;
+                }
+                String codon = mrna.substring(i, i + 3);
+
+                if (translationTable.getStopCodons().contains(codon)) {
+                    if (readThroughStopCodon && ++stopCodonCount < 2) {
+//                        StopCodonReadThrough stopCodonReadThrough = cdsService.getStopCodonReadThrough(cds);
+                        StopCodonReadThrough stopCodonReadThrough = (StopCodonReadThrough) featureRelationshipService.getChildForFeature(cds, StopCodonReadThrough.ontologyId)
+                        if (stopCodonReadThrough == null) {
+                            stopCodonReadThrough = cdsService.createStopCodonReadThrough(cds);
+                            cdsService.setStopCodonReadThrough(cds, stopCodonReadThrough)
+//                            cds.setStopCodonReadThrough(stopCodonReadThrough);
+                            if (cds.strand == Strand.NEGATIVE.value) {
+                                stopCodonReadThrough.featureLocation.setFmin(convertModifiedLocalCoordinateToSourceCoordinate(transcript, i + 2));
+                                stopCodonReadThrough.featureLocation.setFmax(convertModifiedLocalCoordinateToSourceCoordinate(transcript, i) + 1);
+                            } else {
+                                stopCodonReadThrough.featureLocation.setFmin(convertModifiedLocalCoordinateToSourceCoordinate(transcript, i));
+                                stopCodonReadThrough.featureLocation.setFmax(convertModifiedLocalCoordinateToSourceCoordinate(transcript, i + 2) + 1);
+                            }
+                        }
+                        continue;
+                    }
+                    if (transcript.strand == Strand.NEGATIVE.value) {
+                        cds.featureLocation.setFmin(convertLocalCoordinateToSourceCoordinateForTranscript(transcript, i + 2));
+                    } else {
+                        cds.featureLocation.setFmax(convertLocalCoordinateToSourceCoordinateForTranscript(transcript, i + 3));
+                    }
+                    return;
+                }
+            }
+            if (transcript.strand == Strand.NEGATIVE.value) {
+                cds.featureLocation.setFmin(transcript.getFmin());
+                cds.featureLocation.setIsFminPartial(true);
+            } else {
+                cds.featureLocation.setFmax(transcript.getFmax());
+                cds.featureLocation.setIsFmaxPartial(true);
+            }
+        }
+
+        Date date = new Date();
+        cds.setLastUpdated(date);
+        transcript.setLastUpdated(date);
+
+        // event fire
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+    }
+
+/** Set the translation end in the transcript.  Sets the translation end in the underlying CDS feature.
+ *  Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationEnd - Coordinate of the end of translation
+ */
+/*
+public void setTranslationEnd(Transcript transcript, int translationEnd) {
+    CDS cds = transcript.getCDS();
+    if (cds == null) {
+        cds = createCDS(transcript);
+        transcript.setCDS(cds);
+    }
+    if (transcript.getStrand() == -1) {
+        cds.setFmin(translationEnd + 1);
+    }
+    else {
+        cds.setFmax(translationEnd);
+    }
+    setManuallySetTranslationEnd(cds, true);
+    cds.deleteStopCodonReadThrough();
+
+    // event fire
+    fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+}
+*/
+
+/**
+ * Set the translation end in the transcript.  Sets the translation end in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation end in
+ * @param translationEnd - Coordinate of the end of translation
+ */
+    @Transactional
+    void setTranslationEnd(Transcript transcript, int translationEnd) {
+        setTranslationEnd(transcript, translationEnd, false);
+    }
+
+/**
+ * Set the translation end in the transcript.  Sets the translation end in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation end in
+ * @param translationEnd - Coordinate of the end of translation
+ * @param setTranslationStart - if set to true, will search for the nearest in frame start
+ */
+    @Transactional
+    void setTranslationEnd(Transcript transcript, int translationEnd, boolean setTranslationStart) {
+        setTranslationEnd(transcript, translationEnd, setTranslationStart,
+                setTranslationStart ? organismService.getTranslationTable(transcript.featureLocation.sequence.organism) : null
+        );
+    }
+
+/**
+ * Set the translation end in the transcript.  Sets the translation end in the underlying CDS feature.
+ * Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation end in
+ * @param translationEnd - Coordinate of the end of translation
+ * @param setTranslationStart - if set to true, will search for the nearest in frame start codon
+ * @param translationTable - Translation table that defines the codon translation
+ */
+    @Transactional
+    void setTranslationEnd(Transcript transcript, int translationEnd, boolean setTranslationStart, TranslationTable translationTable) {
+        CDS cds = transcriptService.getCDS(transcript);
+        if (cds == null) {
+            cds = transcriptService.createCDS(transcript);
+            transcriptService.setCDS(transcript, cds);
+        }
+        if (transcript.strand == Strand.NEGATIVE.value) {
+            cds.featureLocation.setFmin(translationEnd);
+        } else {
+            cds.featureLocation.setFmax(translationEnd + 1);
+        }
+        cdsService.setManuallySetTranslationEnd(cds, true);
+        cdsService.deleteStopCodonReadThrough(cds);
+        if (setTranslationStart && translationTable != null) {
+            String mrna = getResiduesWithAlterationsAndFrameshifts(transcript);
+            if (mrna == null || mrna.equals("null")) {
+                return;
+            }
+            for (int i = convertSourceCoordinateToLocalCoordinateForTranscript(transcript, translationEnd) - 3; i >= 0; i -= 3) {
+                if (i - 3 < 0) {
+                    break;
+                }
+                String codon = mrna.substring(i, i + 3);
+                if (translationTable.getStartCodons().contains(codon)) {
+                    if (transcript.strand == Strand.NEGATIVE.value) {
+                        cds.featureLocation.setFmax(convertLocalCoordinateToSourceCoordinateForTranscript(transcript, i + 3));
+                    } else {
+                        cds.featureLocation.setFmin(convertLocalCoordinateToSourceCoordinateForTranscript(transcript, i + 2));
+                    }
+                    return;
+                }
+            }
+            if (transcript.strand == Strand.NEGATIVE.value) {
+                cds.featureLocation.setFmin(transcript.getFmin());
+                cds.featureLocation.setIsFminPartial(true);
+            } else {
+                cds.featureLocation.setFmax(transcript.getFmax());
+                cds.featureLocation.setIsFmaxPartial(true);
+            }
+        }
+
+        Date date = new Date();
+        cds.setLastUpdated(date);
+        transcript.setLastUpdated(date);
+
+        // event fire TODO: ??
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+    }
+
+    @Transactional
+    void setTranslationFmin(Transcript transcript, int translationFmin) {
+        CDS cds = transcriptService.getCDS(transcript);
+        if (cds == null) {
+            cds = transcriptService.createCDS(transcript);
+            transcriptService.setCDS(transcript, cds);
+        }
+        setFmin(cds, translationFmin);
+        // event fire
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+    }
+
+    @Transactional
+    void setTranslationFmax(Transcript transcript, int translationFmax) {
+        CDS cds = transcriptService.getCDS(transcript);
+        if (cds == null) {
+            cds = transcriptService.createCDS(transcript);
+            transcriptService.setCDS(transcript, cds);
+        }
+        setFmax(cds, translationFmax);
+
+        // event fire
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+    }
+
+/**
+ * Set the translation start and end in the transcript.  Sets the translation start and end in the underlying CDS
+ * feature.  Instantiates the CDS object for the transcript if it doesn't already exist.
+ *
+ * @param transcript - Transcript to set the translation start in
+ * @param translationStart - Coordinate of the start of translation
+ * @param translationEnd - Coordinate of the end of translation
+ * @param manuallySetStart - whether the start was manually set
+ * @param manuallySetEnd - whether the end was manually set
+ */
+    @Transactional
+    void setTranslationEnds(Transcript transcript, int translationStart, int translationEnd, boolean manuallySetStart, boolean manuallySetEnd) {
+        setTranslationFmin(transcript, translationStart);
+        setTranslationFmax(transcript, translationEnd);
+        cdsService.setManuallySetTranslationStart(transcriptService.getCDS(transcript), manuallySetStart);
+        cdsService.setManuallySetTranslationEnd(transcriptService.getCDS(transcript), manuallySetEnd);
+
+        Date date = new Date();
+        transcriptService.getCDS(transcript).setLastUpdated(date);
+        transcript.setLastUpdated(date);
+
+        // event fire
+//        fireAnnotationChangeEvent(transcript, transcript.getGene(), AnnotationChangeEvent.Operation.UPDATE);
+
+    }
+
+
+    /** Get the residues for a feature with any alterations and frameshifts.
+     *
+     * @param feature - Feature to retrieve the residues for
+     * @return Residues for the feature with any alterations and frameshifts
+     */
+    String getResiduesWithAlterationsAndFrameshifts(Feature feature) {
+        if (!(feature instanceof CDS)) {
+            return getResiduesWithAlterations(feature, getSequenceAlterationsForFeature(feature))
+        }
+        Transcript transcript = (Transcript) featureRelationshipService.getParentForFeature(feature, Transcript.ontologyId)
+        Collection<SequenceAlteration> alterations = getFrameshiftsAsAlterations(transcript);
+        List<SequenceAlteration> allSequenceAlterationList = getSequenceAlterationsForFeature(feature)
+        alterations.addAll(allSequenceAlterationList);
+        return getResiduesWithAlterations(feature, alterations)
+    }
+
+    /**
+     // TODO: should be a single query here, currently 194 ms
+     * Get all sequenceAlterations associated with a feature.
+     * Basically I want to include all upstream alterations on a sequence for that feature
+     * @param feature
+     * @return
+     */
+    List<SequenceAlteration> getAllSequenceAlterationsForFeature(Feature feature) {
+        List<Sequence> sequence = Sequence.executeQuery("select s from Feature  f join f.featureLocations fl join fl.sequence s where f = :feature ", [feature: feature])
+        SequenceAlteration.executeQuery("select sa from SequenceAlteration sa join sa.featureLocations fl join fl.sequence s where s = :sequence order by fl.fmin asc ", [sequence: sequence])
+    }
+
+    List<SequenceAlteration> getFrameshiftsAsAlterations(Transcript transcript) {
+        List<SequenceAlteration> frameshifts = new ArrayList<SequenceAlteration>();
+        CDS cds = transcriptService.getCDS(transcript);
+        if (cds == null) {
+            return frameshifts;
+        }
+        Sequence sequence = cds.getFeatureLocation().sequence
+        List<Frameshift> frameshiftList = transcriptService.getFrameshifts(transcript)
+        for (Frameshift frameshift : frameshiftList) {
+            if (frameshift.isPlusFrameshift()) {
+                // a plus frameshift skips bases during translation, which can be mapped to a deletion for the
+                // the skipped bases
+//                Deletion deletion = new Deletion(cds.getOrganism(), "Deletion-" + frameshift.getCoordinate(), false,
+//                        false, new Timestamp(new Date().getTime()), cds.getConfiguration());
+
+                FeatureLocation featureLocation = new FeatureLocation(
+                        fmin: frameshift.coordinate
+                        , fmax: frameshift.coordinate + frameshift.frameshiftValue
+                        , strand: cds.featureLocation.strand
+                        , sequence: sequence
+                )
+
+                Deletion deletion = new Deletion(
+                        uniqueName: FeatureStringEnum.DELETION_PREFIX.value + frameshift.coordinate
+                        , isObsolete: false
+                        , isAnalysis: false
+                )
+
+                featureLocation.feature = deletion
+                deletion.addToFeatureLocations(featureLocation)
+
+                frameshifts.add(deletion);
+                featureLocation.save()
+                deletion.save()
+                frameshift.save(flush: true)
+
+//                deletion.setFeatureLocation(frameshift.getCoordinate(),
+//                        frameshift.getCoordinate() + frameshift.getFrameshiftValue(),
+//                        cds.getFeatureLocation().getStrand(), sourceFeature);
+
+
+            } else {
+                // a minus frameshift goes back bases during translation, which can be mapped to an insertion for the
+                // the repeated bases
+                Insertion insertion = new Insertion(
+                        uniqueName: FeatureStringEnum.INSERTION_PREFIX.value + frameshift.coordinate
+                        , isAnalysis: false
+                        , isObsolete: false
+                ).save()
+
+//                Insertion insertion = new Insertion(cds.getOrganism(), "Insertion-" + frameshift.getCoordinate(), false,
+//                        false, new Timestamp(new Date().getTime()), cds.getConfiguration());
+
+                FeatureLocation featureLocation = new FeatureLocation(
+                        fmin: frameshift.coordinate
+                        , fmax: frameshift.coordinate + frameshift.frameshiftValue
+                        , strand: cds.featureLocation.strand
+                        , sequence: sequence
+                ).save()
+
+                insertion.addToFeatureLocations(featureLocation)
+                featureLocation.feature = insertion
+
+//                insertion.setFeatureLocation(frameshift.getCoordinate() + frameshift.getFrameshiftValue(),
+//                        frameshift.getCoordinate() + frameshift.getFrameshiftValue(),
+//                        cds.getFeatureLocation().getStrand(), sourceFeature);
+
+                String alterationResidues = sequenceService.getRawResiduesFromSequence(sequence, frameshift.getCoordinate() + frameshift.getFrameshiftValue(), frameshift.getCoordinate())
+                insertion.alterationResidue = alterationResidues
+                // TODO: correct?
+//                insertion.setResidues(sequence.getResidues().substring(
+//                        frameshift.getCoordinate() + frameshift.getFrameshiftValue(), frameshift.getCoordinate()));
+                frameshifts.add(insertion);
+
+                insertion.save()
+                featureLocation.save()
+                frameshift.save(flush: true)
+            }
+        }
+        return frameshifts;
+    }
+
+/*
+ * Calculate the longest ORF for a transcript.  If a valid start codon is not found, allow for partial CDS start/end.
+ *
+ * @param transcript - Transcript to set the longest ORF to
+ * @param translationTable - Translation table that defines the codon translation
+ * @param allowPartialExtension - Where partial ORFs should be used for possible extension
+ *
+ */
+    @Timed
+    @Transactional
+    void setLongestORF(Transcript transcript, boolean readThroughStopCodon) {
+        Organism organism = transcript.featureLocation.sequence.organism
+        TranslationTable translationTable = organismService.getTranslationTable(organism)
+        String mrna = getResiduesWithAlterationsAndFrameshifts(transcript);
+        if (!mrna) {
+            return;
+        }
+        String longestPeptide = "";
+        int bestStartIndex = -1;
+        int bestStopIndex = -1;
+        int startIndex = -1;
+        int stopIndex = -1;
+        boolean partialStart = false;
+        boolean partialStop = false;
+
+        if (mrna.length() > 3) {
+            for (String startCodon : translationTable.getStartCodons()) {
+                // find the first start codon
+                startIndex = mrna.indexOf(startCodon)
+                while(startIndex >= 0) {
+                    String mrnaSubstring = mrna.substring(startIndex)
+                    String aa = SequenceTranslationHandler.translateSequence(mrnaSubstring, translationTable, true, readThroughStopCodon)
+                    if (aa.length() > longestPeptide.length()) {
+                        longestPeptide = aa
+                        bestStartIndex = startIndex
+                    }
+                    startIndex = mrna.indexOf(startCodon, startIndex + 1)
+                }
+            }
+
+            // Just in case the 5' end is missing, check to see if a longer
+            // translation can be obatained without looking for a start codon
+            startIndex = 0
+            while(startIndex < 3) {
+                String mrnaSubstring = mrna.substring(startIndex)
+                String aa = SequenceTranslationHandler.translateSequence(mrnaSubstring, translationTable, true, readThroughStopCodon)
+                if (aa.length() > longestPeptide.length()) {
+                    partialStart = true
+                    longestPeptide = aa
+                    bestStartIndex = startIndex
+                }
+                startIndex++
+            }
+        }
+
+        // check for partial stop
+        if (!longestPeptide.substring(longestPeptide.length() - 1).equals(TranslationTable.STOP)) {
+            partialStop = true
+            bestStopIndex = -1
+        }
+        else {
+            stopIndex = bestStartIndex + (longestPeptide.length() * 3)
+            partialStop = false
+            bestStopIndex = stopIndex
+        }
+
+        log.debug "bestStartIndex: ${bestStartIndex} bestStopIndex: ${bestStopIndex}; partialStart: ${partialStart} partialStop: ${partialStop}"
+
+        if (transcript instanceof MRNA) {
+            CDS cds = transcriptService.getCDS(transcript)
+            if (cds == null) {
+                cds = transcriptService.createCDS(transcript);
+                transcriptService.setCDS(transcript, cds);
+            }
+
+            int fmin = convertModifiedLocalCoordinateToSourceCoordinate(transcript, bestStartIndex)
+
+            if (bestStopIndex >= 0) {
+                log.debug "bestStopIndex >= 0"
+                int fmax = convertModifiedLocalCoordinateToSourceCoordinate(transcript, bestStopIndex)
+                if (cds.strand == Strand.NEGATIVE.value) {
+                    int tmp = fmin
+                    fmin = fmax + 1
+                    fmax = tmp + 1
+                }
+                setFmin(cds, fmin)
+                setFmax(cds, fmax)
+            }
+            else {
+                log.debug "bestStopIndex < 0"
+                int fmax = transcript.strand == Strand.NEGATIVE.value ? transcript.fmin : transcript.fmax
+                if (cds.strand == Strand.NEGATIVE.value) {
+                    int tmp = fmin
+                    fmin = fmax
+                    fmax = tmp + 1
+                }
+                setFmin(cds, fmin)
+                setFmax(cds, fmax)
+            }
+
+            if (cds.featureLocation.strand == Strand.NEGATIVE.value) {
+                cds.featureLocation.setIsFminPartial(partialStop)
+                cds.featureLocation.setIsFmaxPartial(partialStart)
+            }
+            else {
+                cds.featureLocation.setIsFminPartial(partialStart)
+                cds.featureLocation.setIsFmaxPartial(partialStop)
+            }
+
+            log.debug "Final CDS fmin: ${cds.fmin} fmax: ${cds.fmax}"
+
+            if (readThroughStopCodon) {
+                cdsService.deleteStopCodonReadThrough(cds)
+                String aa = SequenceTranslationHandler.translateSequence(getResiduesWithAlterationsAndFrameshifts(cds), translationTable, true, true);
+                int firstStopIndex = aa.indexOf(TranslationTable.STOP);
+                if (firstStopIndex < aa.length() - 1) {
+                    StopCodonReadThrough stopCodonReadThrough = cdsService.createStopCodonReadThrough(cds);
+                    cdsService.setStopCodonReadThrough(cds, stopCodonReadThrough);
+                    int offset = transcript.getStrand() == -1 ? -2 : 0;
+                    setFmin(stopCodonReadThrough, convertModifiedLocalCoordinateToSourceCoordinate(cds, firstStopIndex * 3) + offset);
+                    setFmax(stopCodonReadThrough, convertModifiedLocalCoordinateToSourceCoordinate(cds, firstStopIndex * 3) + 3 + offset);
+                }
+            } else {
+                cdsService.deleteStopCodonReadThrough(cds);
+            }
+            cdsService.setManuallySetTranslationStart(cds, false);
+            cdsService.setManuallySetTranslationEnd(cds, false);
+        }
+    }
+
+
+    @Timed
+    @Transactional
+    Feature convertJSONToFeature(JSONObject jsonFeature, Sequence sequence) {
+        Feature gsolFeature
+        try {
+            JSONObject type = jsonFeature.getJSONObject(FeatureStringEnum.TYPE.value);
+            String ontologyId = convertJSONToOntologyId(type)
+            if (!ontologyId) {
+                log.warn "Feature type not set for ${type}"
+                return null
+            }
+
+            gsolFeature = generateFeatureForType(ontologyId)
+            if (jsonFeature.has(FeatureStringEnum.ID.value)) {
+                gsolFeature.setId(jsonFeature.getLong(FeatureStringEnum.ID.value));
+            }
+
+            if (jsonFeature.has(FeatureStringEnum.UNIQUENAME.value)) {
+                gsolFeature.setUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value));
+            } else {
+                gsolFeature.setUniqueName(nameService.generateUniqueName());
+            }
+            if (jsonFeature.has(FeatureStringEnum.NAME.value)) {
+                gsolFeature.setName(jsonFeature.getString(FeatureStringEnum.NAME.value));
+            }
+            else {
+                // since name attribute cannot be null, using the feature's own uniqueName
+                gsolFeature.name = gsolFeature.uniqueName
+            }
+            if (jsonFeature.has(FeatureStringEnum.SYMBOL.value)) {
+                gsolFeature.setSymbol(jsonFeature.getString(FeatureStringEnum.SYMBOL.value));
+            }
+            if (jsonFeature.has(FeatureStringEnum.DESCRIPTION.value)) {
+                gsolFeature.setDescription(jsonFeature.getString(FeatureStringEnum.DESCRIPTION.value));
+            }
+            if (gsolFeature instanceof Deletion) {
+                int deletionLength = jsonFeature.location.fmax - jsonFeature.location.fmin
+                gsolFeature.deletionLength = deletionLength
+            }
+
+            gsolFeature.save(failOnError: true)
+
+
+            if (jsonFeature.has(FeatureStringEnum.LOCATION.value)) {
+                JSONObject jsonLocation = jsonFeature.getJSONObject(FeatureStringEnum.LOCATION.value);
+                FeatureLocation featureLocation
+                if (singletonFeatureTypes.contains(type.getString(FeatureStringEnum.NAME.value))) {
+                    featureLocation = convertJSONToFeatureLocation(jsonLocation, sequence, Strand.NONE.value)
+                } else {
+                    featureLocation = convertJSONToFeatureLocation(jsonLocation, sequence)
+                }
+                featureLocation.sequence = sequence
+                featureLocation.feature = gsolFeature
+                featureLocation.save()
+                gsolFeature.addToFeatureLocations(featureLocation);
+            }
+
+            if (gsolFeature instanceof Deletion) {
+                sequenceService.setResiduesForFeatureFromLocation((Deletion) gsolFeature)
+            } else if (jsonFeature.has(FeatureStringEnum.RESIDUES.value) && gsolFeature instanceof SequenceAlteration) {
+                sequenceService.setResiduesForFeature(gsolFeature, jsonFeature.getString(FeatureStringEnum.RESIDUES.value))
+            }
+
+            if (jsonFeature.has(FeatureStringEnum.CHILDREN.value)) {
+                JSONArray children = jsonFeature.getJSONArray(FeatureStringEnum.CHILDREN.value);
+                log.debug "jsonFeature ${jsonFeature} has ${children?.size()} children"
+                for (int i = 0; i < children.length(); ++i) {
+                    JSONObject childObject = children.getJSONObject(i)
+                    Feature child = convertJSONToFeature(childObject, sequence);
+                    // if it retuns null, we ignore it
+                    if (child) {
+                        child.save(failOnError: true)
+                        FeatureRelationship fr = new FeatureRelationship();
+                        fr.setParentFeature(gsolFeature);
+                        fr.setChildFeature(child);
+                        fr.save(failOnError: true)
+                        gsolFeature.addToParentFeatureRelationships(fr);
+                        child.addToChildFeatureRelationships(fr);
+                        child.save()
+                    }
+                    gsolFeature.save()
+                }
+            }
+            if (jsonFeature.has(FeatureStringEnum.TIMEACCESSION.value)) {
+                gsolFeature.setDateCreated(new Date(jsonFeature.getInt(FeatureStringEnum.TIMEACCESSION.value)));
+            } else {
+                gsolFeature.setDateCreated(new Date());
+            }
+            if (jsonFeature.has(FeatureStringEnum.TIMELASTMODIFIED.value)) {
+                gsolFeature.setLastUpdated(new Date(jsonFeature.getInt(FeatureStringEnum.TIMELASTMODIFIED.value)));
+            } else {
+                gsolFeature.setLastUpdated(new Date());
+            }
+            if (jsonFeature.has(FeatureStringEnum.PROPERTIES.value)) {
+                JSONArray properties = jsonFeature.getJSONArray(FeatureStringEnum.PROPERTIES.value);
+                for (int i = 0; i < properties.length(); ++i) {
+                    JSONObject property = properties.getJSONObject(i);
+                    JSONObject propertyType = property.getJSONObject(FeatureStringEnum.TYPE.value);
+                    String propertyName = ""
+                    if (property.has(FeatureStringEnum.NAME.value)) {
+                        propertyName = property.get(FeatureStringEnum.NAME.value)
+                    }
+                    else {
+                        propertyName = propertyType.get(FeatureStringEnum.NAME.value)
+                    }
+                    String propertyValue = property.get(FeatureStringEnum.VALUE.value)
+
+                    FeatureProperty gsolProperty = null;
+                    if (propertyName == FeatureStringEnum.STATUS.value) {
+                        // property of type 'Status'
+                        AvailableStatus availableStatus = AvailableStatus.findByValue(propertyValue)
+                        if (availableStatus) {
+                            Status status = new Status(
+                                    value: availableStatus.value,
+                                    feature: gsolFeature
+                            ).save(failOnError: true)
+                            gsolFeature.status = status
+                            gsolFeature.save()
+                        }
+                        else {
+                            log.warn "Ignoring status ${propertyValue} as its not defined."
+                        }
+                    }
+                    else {
+                        if (propertyName == FeatureStringEnum.COMMENT.value) {
+                            // property of type 'Comment'
+                            gsolProperty = new Comment();
+                        } else {
+                            gsolProperty = new FeatureProperty();
+                        }
+
+                        if (propertyType.has(FeatureStringEnum.NAME.value)) {
+                            CV cv = CV.findByName(propertyType.getJSONObject(FeatureStringEnum.CV.value).getString(FeatureStringEnum.NAME.value))
+                            CVTerm cvTerm = CVTerm.findByNameAndCv(propertyType.getString(FeatureStringEnum.NAME.value), cv)
+                            gsolProperty.setType(cvTerm);
+                        } else {
+                            log.warn "No proper type for the CV is set ${propertyType as JSON}"
+                        }
+                        gsolProperty.setTag(propertyName)
+                        gsolProperty.setValue(propertyValue)
+                        gsolProperty.setFeature(gsolFeature);
+
+                        int rank = 0;
+                        for (FeatureProperty fp : gsolFeature.getFeatureProperties()) {
+                            if (fp.getType().equals(gsolProperty.getType())) {
+                                if (fp.getRank() > rank) {
+                                    rank = fp.getRank();
+                                }
+                            }
+                        }
+                        gsolProperty.setRank(rank + 1);
+                        gsolProperty.save()
+                        gsolFeature.addToFeatureProperties(gsolProperty);
+                    }
+                }
+            }
+            if (jsonFeature.has(FeatureStringEnum.DBXREFS.value)) {
+                JSONArray dbxrefs = jsonFeature.getJSONArray(FeatureStringEnum.DBXREFS.value);
+                for (int i = 0; i < dbxrefs.length(); ++i) {
+                    JSONObject dbxref = dbxrefs.getJSONObject(i);
+                    JSONObject db = dbxref.getJSONObject(FeatureStringEnum.DB.value);
+
+
+                    DB newDB = DB.findOrSaveByName(db.getString(FeatureStringEnum.NAME.value))
+                    DBXref newDBXref = DBXref.findOrSaveByDbAndAccession(
+                            newDB,
+                            dbxref.getString(FeatureStringEnum.ACCESSION.value)
+                    ).save()
+                    gsolFeature.addToFeatureDBXrefs(newDBXref)
+                    gsolFeature.save()
+                }
+            }
+        }
+        catch (JSONException e) {
+            log.error("Exception creating Feature from JSON ${jsonFeature}", e)
+            return null;
+        }
+        return gsolFeature;
+    }
+
+
+    String getCvTermFromFeature(Feature feature) {
+        String cvTerm = feature.hasProperty(FeatureStringEnum.ALTERNATECVTERM.value) ? feature.getProperty(FeatureStringEnum.ALTERNATECVTERM.value) : feature.cvTerm
+        return cvTerm
+    }
+
+    boolean isJsonTranscript(JSONObject jsonObject) {
+        JSONObject typeObject = jsonObject.getJSONObject(FeatureStringEnum.TYPE.value)
+        String typeString = typeObject.getString(FeatureStringEnum.NAME.value)
+        if (typeString == MRNA.cvTerm || typeString == MRNA.alternateCvTerm) {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    // TODO: (perform on client side, slightly ugly)
+    Feature generateFeatureForType(String ontologyId) {
+        switch (ontologyId) {
+            case MRNA.ontologyId: return new MRNA()
+            case MiRNA.ontologyId: return new MiRNA()
+            case NcRNA.ontologyId: return new NcRNA()
+            case SnoRNA.ontologyId: return new SnoRNA()
+            case SnRNA.ontologyId: return new SnRNA()
+            case RRNA.ontologyId: return new RRNA()
+            case TRNA.ontologyId: return new TRNA()
+            case Exon.ontologyId: return new Exon()
+            case CDS.ontologyId: return new CDS()
+            case Intron.ontologyId: return new Intron()
+            case Gene.ontologyId: return new Gene()
+            case Pseudogene.ontologyId: return new Pseudogene()
+            case Transcript.ontologyId: return new Transcript()
+            case TransposableElement.ontologyId: return new TransposableElement()
+            case RepeatRegion.ontologyId: return new RepeatRegion()
+            case Insertion.ontologyId: return new Insertion()
+            case Deletion.ontologyId: return new Deletion()
+            case Substitution.ontologyId: return new Substitution()
+            case NonCanonicalFivePrimeSpliceSite.ontologyId: return new NonCanonicalFivePrimeSpliceSite()
+            case NonCanonicalThreePrimeSpliceSite.ontologyId: return new NonCanonicalThreePrimeSpliceSite()
+            case StopCodonReadThrough.ontologyId: return new StopCodonReadThrough()
+            default:
+                log.error("No feature type exists for ${ontologyId}")
+                return null
+        }
+    }
+
+
+    String convertJSONToOntologyId(JSONObject jsonCVTerm) {
+        String cvString = jsonCVTerm.getJSONObject(FeatureStringEnum.CV.value).getString(FeatureStringEnum.NAME.value)
+        String cvTermString = jsonCVTerm.getString(FeatureStringEnum.NAME.value)
+
+        if (cvString.equalsIgnoreCase(FeatureStringEnum.CV.value) || cvString.equalsIgnoreCase(FeatureStringEnum.SEQUENCE.value)) {
+            switch (cvTermString.toUpperCase()) {
+                case MRNA.cvTerm.toUpperCase(): return MRNA.ontologyId
+                case MiRNA.cvTerm.toUpperCase(): return MiRNA.ontologyId
+                case NcRNA.cvTerm.toUpperCase(): return NcRNA.ontologyId
+                case SnoRNA.cvTerm.toUpperCase(): return SnoRNA.ontologyId
+                case SnRNA.cvTerm.toUpperCase(): return SnRNA.ontologyId
+                case RRNA.cvTerm.toUpperCase(): return RRNA.ontologyId
+                case TRNA.cvTerm.toUpperCase(): return TRNA.ontologyId
+                case Transcript.cvTerm.toUpperCase(): return Transcript.ontologyId
+                case Gene.cvTerm.toUpperCase(): return Gene.ontologyId
+                case Exon.cvTerm.toUpperCase(): return Exon.ontologyId
+                case CDS.cvTerm.toUpperCase(): return CDS.ontologyId
+                case Intron.cvTerm.toUpperCase(): return Intron.ontologyId
+                case Pseudogene.cvTerm.toUpperCase(): return Pseudogene.ontologyId
+                case TransposableElement.alternateCvTerm.toUpperCase():
+                case TransposableElement.cvTerm.toUpperCase(): return TransposableElement.ontologyId
+                case RepeatRegion.alternateCvTerm.toUpperCase():
+                case RepeatRegion.cvTerm.toUpperCase(): return RepeatRegion.ontologyId
+                case Insertion.cvTerm.toUpperCase(): return Insertion.ontologyId
+                case Deletion.cvTerm.toUpperCase(): return Deletion.ontologyId
+                case Substitution.cvTerm.toUpperCase(): return Substitution.ontologyId
+                case StopCodonReadThrough.cvTerm.toUpperCase(): return StopCodonReadThrough.ontologyId
+                case NonCanonicalFivePrimeSpliceSite.cvTerm.toUpperCase(): return NonCanonicalFivePrimeSpliceSite.ontologyId
+                case NonCanonicalThreePrimeSpliceSite.cvTerm.toUpperCase(): return NonCanonicalThreePrimeSpliceSite.ontologyId
+                case NonCanonicalFivePrimeSpliceSite.alternateCvTerm.toUpperCase(): return NonCanonicalFivePrimeSpliceSite.ontologyId
+                case NonCanonicalThreePrimeSpliceSite.alternateCvTerm.toUpperCase(): return NonCanonicalThreePrimeSpliceSite.ontologyId
+                default:
+                    log.error("CV Term not known ${cvTermString} for CV ${FeatureStringEnum.SEQUENCE}")
+                    return null
+            }
+        } else {
+            log.error("CV not known ${cvString}")
+        }
+
+        return null
+
+    }
+
+    @Transactional
+    void updateGeneBoundaries(Gene gene) {
+        log.debug "updateGeneBoundaries"
+        if (gene == null) {
+            return;
+        }
+        int geneFmax = Integer.MIN_VALUE;
+        int geneFmin = Integer.MAX_VALUE;
+        for (Transcript t : transcriptService.getTranscripts(gene)) {
+            if (t.getFmin() < geneFmin) {
+                geneFmin = t.getFmin();
+            }
+            if (t.getFmax() > geneFmax) {
+                geneFmax = t.getFmax();
+            }
+        }
+        gene.featureLocation.setFmin(geneFmin);
+        gene.featureLocation.setFmax(geneFmax);
+        gene.setLastUpdated(new Date());
+    }
+
+    @Transactional
+    def setFmin(Feature feature, int fmin) {
+        feature.getFeatureLocation().setFmin(fmin);
+    }
+
+    @Transactional
+    def setFmax(Feature feature, int fmax) {
+        feature.getFeatureLocation().setFmax(fmax);
+    }
+
+    /** Convert source feature coordinate to local coordinate.
+     *
+     * @param sourceCoordinate - Coordinate to convert to local coordinate
+     * @return Local coordinate, -1 if source coordinate is <= fmin or >= fmax
+     */
+    int convertSourceCoordinateToLocalCoordinate(Feature feature, int sourceCoordinate) {
+        return convertSourceCoordinateToLocalCoordinate(feature.featureLocation.fmin, feature.featureLocation.fmax, Strand.getStrandForValue(feature.featureLocation.strand), sourceCoordinate)
+    }
+
+    int convertSourceCoordinateToLocalCoordinate(int fmin, int fmax, Strand strand, int sourceCoordinate) {
+        if (sourceCoordinate < fmin || sourceCoordinate > fmax) {
+            return -1;
+        }
+        if (strand == Strand.NEGATIVE) {
+            return fmax - 1 - sourceCoordinate;
+        } else {
+            return sourceCoordinate - fmin;
+        }
+    }
+
+    int convertSourceCoordinateToLocalCoordinateForTranscript(Transcript transcript, int sourceCoordinate) {
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        int localCoordinate = -1
+        int currentCoordinate = 0
+        for (Exon exon : exons) {
+            if (exon.fmin <= sourceCoordinate && exon.fmax >= sourceCoordinate) {
+                //sourceCoordinate falls within the exon
+                if (exon.strand == Strand.NEGATIVE.value) {
+                    localCoordinate = currentCoordinate + (exon.fmax - sourceCoordinate) - 1;
+                } else {
+                    localCoordinate = currentCoordinate + (sourceCoordinate - exon.fmin);
+                }
+            }
+            currentCoordinate += exon.getLength();
+        }
+        return localCoordinate
+    }
+
+
+    int convertSourceCoordinateToLocalCoordinateForCDS(Feature feature, int sourceCoordinate) {
+        def exons = []
+        CDS cds
+        if (feature instanceof Transcript) {
+            exons = transcriptService.getSortedExons(feature, true)
+            cds = transcriptService.getCDS(feature)
+        }
+        else if (feature instanceof CDS) {
+            Transcript transcript = transcriptService.getTranscript(feature)
+            exons = transcriptService.getSortedExons(transcript, true)
+            cds = feature
+        }
+
+        int localCoordinate = 0
+
+        if (!(cds.fmin <= sourceCoordinate && cds.fmax >= sourceCoordinate)) {
+            return -1
+        }
+        int x = 0
+        int y = 0
+        if (feature.strand == Strand.POSITIVE.value) {
+            for (Exon exon : exons) {
+                if (overlapperService.overlaps(exon, cds, true) && exon.fmin >= cds.fmin && exon.fmax <= cds.fmax) {
+                    // complete overlap
+                    x = exon.fmin
+                    y = exon.fmax
+                } else if (overlapperService.overlaps(exon, cds, true)) {
+                    // partial overlap
+                    if (exon.fmin < cds.fmin && exon.fmax < cds.fmax) {
+                        x = cds.fmin
+                        y = exon.fmax
+                    } else {
+                        //exon.fmin > cds.fmin && exon.fmax > cds.fmax
+                        x = exon.fmin
+                        y = cds.fmax
+                    }
+                } else {
+                    // no overlap
+                    continue
+                }
+
+                if (x <= sourceCoordinate && y >= sourceCoordinate) {
+                    localCoordinate += sourceCoordinate - x
+                    return localCoordinate
+                } else {
+                    localCoordinate += y - x
+                }
+            }
+        } else {
+            for (Exon exon : exons) {
+                if (overlapperService.overlaps(exon, cds, true) && exon.fmin >= cds.fmin && exon.fmax <= cds.fmax) {
+                    // complete overlap
+                    x = exon.fmax
+                    y = exon.fmin
+                } else if (overlapperService.overlaps(exon, cds, true)) {
+                    // partial overlap
+                    //x = cds.fmax
+                    //y = exon.fmin
+                    if (exon.fmin <= cds.fmin && exon.fmax <= cds.fmax) {
+                        x = exon.fmax
+                        y = cds.fmin
+                    } else {
+                        //exon.fmin > cds.fmin && exon.fmax > cds.fmax
+                        x = cds.fmax
+                        y = exon.fmin
+                    }
+                } else {
+                    // no overlap
+                    continue
+                }
+                if (y <= sourceCoordinate && x >= sourceCoordinate) {
+                    localCoordinate += (x - sourceCoordinate) - 1
+                    return localCoordinate
+                } else {
+                    localCoordinate += (x - y)
+                }
+            }
+        }
+    }
+
+
+    void removeFeatureRelationship(Transcript transcript, Feature feature) {
+
+        FeatureRelationship featureRelationship = FeatureRelationship.findByParentFeatureAndChildFeature(transcript, feature)
+        if (featureRelationship) {
+            FeatureRelationship.deleteAll()
+        }
+    }
+
+    /**
+     * @param gsolFeature
+     * @param includeSequence
+     * @return
+     */
+    @Timed
+    JSONObject convertFeatureToJSONLite(Feature gsolFeature, boolean includeSequence = false, int depth) {
+        JSONObject jsonFeature = new JSONObject();
+        if (gsolFeature.id) {
+            jsonFeature.put(FeatureStringEnum.ID.value, gsolFeature.id);
+        }
+        jsonFeature.put(FeatureStringEnum.TYPE.value, generateJSONFeatureStringForType(gsolFeature.ontologyId));
+        jsonFeature.put(FeatureStringEnum.UNIQUENAME.value, gsolFeature.getUniqueName());
+        if (gsolFeature.getName() != null) {
+            jsonFeature.put(FeatureStringEnum.NAME.value, gsolFeature.getName());
+        }
+        if (gsolFeature.symbol) {
+            jsonFeature.put(FeatureStringEnum.SYMBOL.value, gsolFeature.symbol);
+        }
+        if (gsolFeature.description) {
+            jsonFeature.put(FeatureStringEnum.DESCRIPTION.value, gsolFeature.description);
+        }
+        long start = System.currentTimeMillis();
+        if (depth <= 1) {
+            String finalOwnerString
+            if (gsolFeature.owners) {
+                String ownerString = ""
+                for (owner in gsolFeature.owners) {
+                    ownerString += gsolFeature.owner.username + " "
+                }
+                finalOwnerString = ownerString?.trim()
+            } else if (gsolFeature.owner) {
+                finalOwnerString = gsolFeature?.owner?.username
+            } else {
+                finalOwnerString = "None"
+            }
+            jsonFeature.put(FeatureStringEnum.OWNER.value.toLowerCase(), finalOwnerString);
+        }
+
+        if (gsolFeature.featureLocation) {
+            jsonFeature.put(FeatureStringEnum.SEQUENCE.value, gsolFeature.featureLocation.sequence.name);
+            jsonFeature.put(FeatureStringEnum.LOCATION.value, convertFeatureLocationToJSON(gsolFeature.featureLocation));
+        }
+
+
+        if (depth <= 1) {
+            List<Feature> childFeatures = featureRelationshipService.getChildrenForFeatureAndTypes(gsolFeature)
+            if (childFeatures) {
+                JSONArray children = new JSONArray();
+                jsonFeature.put(FeatureStringEnum.CHILDREN.value, children);
+                for (Feature f : childFeatures) {
+                    Feature childFeature = f
+                    children.put(convertFeatureToJSONLite(childFeature, includeSequence, depth + 1));
+                }
+            }
+        }
+
+
+
+        jsonFeature.put(FeatureStringEnum.DATE_LAST_MODIFIED.value, gsolFeature.lastUpdated.time);
+        jsonFeature.put(FeatureStringEnum.DATE_CREATION.value, gsolFeature.dateCreated.time);
+        return jsonFeature;
+    }
+
+    String generateOwnerString(Feature feature){
+        if(feature.owner){
+          return feature.owner.username
+        }
+        if (feature.owners) {
+            String ownerString = ""
+            for (owner in feature.owners) {
+                ownerString += owner.username + " "
+            }
+            return ownerString
+        }
+        return "None"
+    }
+
+    /**
+     * @param gsolFeature
+     * @param includeSequence
+     * @return
+     */
+    @Timed
+    JSONObject convertFeatureToJSON(Feature gsolFeature, boolean includeSequence = false) {
+        JSONObject jsonFeature = new JSONObject();
+        if (gsolFeature.id) {
+            jsonFeature.put(FeatureStringEnum.ID.value, gsolFeature.id);
+        }
+        jsonFeature.put(FeatureStringEnum.TYPE.value, generateJSONFeatureStringForType(gsolFeature.ontologyId));
+        jsonFeature.put(FeatureStringEnum.UNIQUENAME.value, gsolFeature.getUniqueName());
+        if (gsolFeature.getName() != null) {
+            jsonFeature.put(FeatureStringEnum.NAME.value, gsolFeature.getName());
+        }
+        if (gsolFeature.symbol) {
+            jsonFeature.put(FeatureStringEnum.SYMBOL.value, gsolFeature.symbol);
+        }
+        if (gsolFeature.description) {
+            jsonFeature.put(FeatureStringEnum.DESCRIPTION.value, gsolFeature.description);
+        }
+
+        long start = System.currentTimeMillis();
+        String finalOwnerString = generateOwnerString(gsolFeature)
+        jsonFeature.put(FeatureStringEnum.OWNER.value.toLowerCase(), finalOwnerString);
+
+        long durationInMilliseconds = System.currentTimeMillis() - start;
+
+        start = System.currentTimeMillis();
+        if (gsolFeature.featureLocation) {
+            Sequence sequence = gsolFeature.featureLocation.sequence
+            jsonFeature.put(FeatureStringEnum.SEQUENCE.value, sequence.name);
+        }
+
+        durationInMilliseconds = System.currentTimeMillis() - start;
+
+
+        start = System.currentTimeMillis();
+
+        // TODO: move this to a configurable place or in another method to process afterwards
+        //            List<String> errorList = new ArrayList<>()
+        //            errorList.addAll(new Cds3Filter().filterFeature(gsolFeature))
+        //            errorList.addAll(new StopCodonFilter().filterFeature(gsolFeature))
+        //            JSONArray notesArray = new JSONArray()
+        //            for (String error : errorList) {
+        //                notesArray.put(error)
+        //            }
+        //            jsonFeature.put(FeatureStringEnum.NOTES.value, notesArray)
+        //            durationInMilliseconds = System.currentTimeMillis()-start;
+        //log.debug "notes ${durationInMilliseconds}"
+
+
+        start = System.currentTimeMillis();
+        // get children
+        List<Feature> childFeatures = featureRelationshipService.getChildrenForFeatureAndTypes(gsolFeature)
+
+
+        durationInMilliseconds = System.currentTimeMillis() - start;
+        if (childFeatures) {
+            JSONArray children = new JSONArray();
+            jsonFeature.put(FeatureStringEnum.CHILDREN.value, children);
+            for (Feature f : childFeatures) {
+                Feature childFeature = f
+                children.put(convertFeatureToJSON(childFeature, includeSequence));
+            }
+        }
+
+
+
+
+        start = System.currentTimeMillis()
+        // get parents
+        List<Feature> parentFeatures = featureRelationshipService.getParentsForFeature(gsolFeature)
+
+        durationInMilliseconds = System.currentTimeMillis() - start;
+        //log.debug "parents ${durationInMilliseconds}"
+        if (parentFeatures?.size() == 1) {
+            Feature parent = parentFeatures.iterator().next();
+            jsonFeature.put(FeatureStringEnum.PARENT_ID.value, parent.getUniqueName());
+            jsonFeature.put(FeatureStringEnum.PARENT_NAME.value, parent.getName());
+            jsonFeature.put(FeatureStringEnum.PARENT_TYPE.value, generateJSONFeatureStringForType(parent.ontologyId));
+        }
+
+
+        start = System.currentTimeMillis()
+
+        Collection<FeatureLocation> featureLocations = gsolFeature.getFeatureLocations();
+        if (featureLocations) {
+            FeatureLocation gsolFeatureLocation = featureLocations.iterator().next();
+            if (gsolFeatureLocation != null) {
+                jsonFeature.put(FeatureStringEnum.LOCATION.value, convertFeatureLocationToJSON(gsolFeatureLocation));
+            }
+        }
+
+        durationInMilliseconds = System.currentTimeMillis() - start;
+        //log.debug "featloc ${durationInMilliseconds}"
+
+
+        if (gsolFeature instanceof SequenceAlteration) {
+            SequenceAlteration sequenceAlteration = (SequenceAlteration) gsolFeature
+            if (sequenceAlteration.alterationResidue) {
+                jsonFeature.put(FeatureStringEnum.RESIDUES.value, sequenceAlteration.alterationResidue);
+            }
+        } else if (includeSequence) {
+            String residues = sequenceService.getResiduesFromFeature(gsolFeature)
+            if (residues) {
+                jsonFeature.put(FeatureStringEnum.RESIDUES.value, residues);
+            }
+        }
+
+        //e.g. properties: [{value: "demo", type: {name: "owner", cv: {name: "feature_property"}}}]
+        Collection<FeatureProperty> gsolFeatureProperties = gsolFeature.getFeatureProperties();
+
+        JSONArray properties = new JSONArray();
+        jsonFeature.put(FeatureStringEnum.PROPERTIES.value, properties);
+        if (gsolFeatureProperties) {
+            for (FeatureProperty property : gsolFeatureProperties) {
+                JSONObject jsonProperty = new JSONObject();
+                JSONObject jsonPropertyType = new JSONObject()
+                if (property instanceof Comment) {
+                    JSONObject jsonPropertyTypeCv = new JSONObject()
+                    jsonPropertyTypeCv.put(FeatureStringEnum.NAME.value, FeatureStringEnum.FEATURE_PROPERTY.value)
+                    jsonPropertyType.put(FeatureStringEnum.CV.value, jsonPropertyTypeCv)
+
+                    jsonProperty.put(FeatureStringEnum.TYPE.value, jsonPropertyType);
+                    jsonProperty.put(FeatureStringEnum.NAME.value, FeatureStringEnum.COMMENT.value);
+                    jsonProperty.put(FeatureStringEnum.VALUE.value, property.getValue());
+                    properties.put(jsonProperty);
+                    continue
+                }
+                if (property.tag == "justification") {
+                    JSONObject jsonPropertyTypeCv = new JSONObject()
+                    jsonPropertyTypeCv.put(FeatureStringEnum.NAME.value, FeatureStringEnum.FEATURE_PROPERTY.value)
+                    jsonPropertyType.put(FeatureStringEnum.CV.value, jsonPropertyTypeCv)
+
+                    jsonProperty.put(FeatureStringEnum.TYPE.value, jsonPropertyType);
+                    jsonProperty.put(FeatureStringEnum.NAME.value, "justification");
+                    jsonProperty.put(FeatureStringEnum.VALUE.value, property.getValue());
+                    properties.put(jsonProperty);
+                    continue
+                }
+                jsonPropertyType.put(FeatureStringEnum.NAME.value, property.type)
+                JSONObject jsonPropertyTypeCv = new JSONObject()
+                jsonPropertyTypeCv.put(FeatureStringEnum.NAME.value, FeatureStringEnum.FEATURE_PROPERTY.value)
+                jsonPropertyType.put(FeatureStringEnum.CV.value, jsonPropertyTypeCv)
+
+                jsonProperty.put(FeatureStringEnum.TYPE.value, jsonPropertyType);
+                jsonProperty.put(FeatureStringEnum.NAME.value, property.getTag());
+                jsonProperty.put(FeatureStringEnum.VALUE.value, property.getValue());
+                properties.put(jsonProperty);
+            }
+        }
+//        JSONObject ownerProperty = JSON.parse("{value: ${finalOwnerString}, type: {name: 'owner', cv: {name: 'feature_property'}}}") as JSONObject
+//        properties.put(ownerProperty)
+
+
+        Collection<DBXref> gsolFeatureDbxrefs = gsolFeature.getFeatureDBXrefs();
+        if (gsolFeatureDbxrefs) {
+            JSONArray dbxrefs = new JSONArray();
+            jsonFeature.put(FeatureStringEnum.DBXREFS.value, dbxrefs);
+            for (DBXref gsolDbxref : gsolFeatureDbxrefs) {
+                JSONObject dbxref = new JSONObject();
+                dbxref.put(FeatureStringEnum.ACCESSION.value, gsolDbxref.getAccession());
+                dbxref.put(FeatureStringEnum.DB.value, new JSONObject().put(FeatureStringEnum.NAME.value, gsolDbxref.getDb().getName()));
+                dbxrefs.put(dbxref);
+            }
+        }
+        jsonFeature.put(FeatureStringEnum.DATE_LAST_MODIFIED.value, gsolFeature.lastUpdated.time);
+        jsonFeature.put(FeatureStringEnum.DATE_CREATION.value, gsolFeature.dateCreated.time);
+        return jsonFeature;
+    }
+
+    JSONObject generateJSONFeatureStringForType(String ontologyId) {
+        if (ontologyId == null) return null;
+        JSONObject jsonObject = new JSONObject();
+        def feature = generateFeatureForType(ontologyId)
+        String cvTerm = feature.hasProperty(FeatureStringEnum.ALTERNATECVTERM.value) ? feature.getProperty(FeatureStringEnum.ALTERNATECVTERM.value) : feature.cvTerm
+
+        jsonObject.put(FeatureStringEnum.NAME.value, cvTerm)
+
+        JSONObject cvObject = new JSONObject()
+        cvObject.put(FeatureStringEnum.NAME.value, FeatureStringEnum.SEQUENCE.value)
+        jsonObject.put(FeatureStringEnum.CV.value, cvObject)
+
+        return jsonObject
+    }
+
+    @Timed
+    JSONObject convertFeatureLocationToJSON(FeatureLocation gsolFeatureLocation) throws JSONException {
+        JSONObject jsonFeatureLocation = new JSONObject();
+        if (gsolFeatureLocation.id) {
+            jsonFeatureLocation.put(FeatureStringEnum.ID.value, gsolFeatureLocation.id);
+        }
+        jsonFeatureLocation.put(FeatureStringEnum.FMIN.value, gsolFeatureLocation.getFmin());
+        jsonFeatureLocation.put(FeatureStringEnum.FMAX.value, gsolFeatureLocation.getFmax());
+        if (gsolFeatureLocation.isIsFminPartial()) {
+            jsonFeatureLocation.put(FeatureStringEnum.IS_FMIN_PARTIAL.value, true);
+        }
+        if (gsolFeatureLocation.isIsFmaxPartial()) {
+            jsonFeatureLocation.put(FeatureStringEnum.IS_FMAX_PARTIAL.value, true);
+        }
+        jsonFeatureLocation.put(FeatureStringEnum.STRAND.value, gsolFeatureLocation.getStrand());
+        return jsonFeatureLocation;
+    }
+
+    @Transactional
+    Boolean deleteFeature(Feature feature, HashMap<String, List<Feature>> modifiedFeaturesUniqueNames = new ArrayList<>()) {
+
+        if (feature instanceof Exon) {
+            Exon exon = (Exon) feature;
+            Transcript transcript = (Transcript) Transcript.findByUniqueName(exonService.getTranscript(exon).getUniqueName());
+
+            if (!(transcriptService.getGene(transcript) instanceof Pseudogene) && transcriptService.isProteinCoding(transcript)) {
+                CDS cds = transcriptService.getCDS(transcript);
+                if (cdsService.isManuallySetTranslationStart(cds)) {
+                    int cdsStart = cds.getStrand() == -1 ? cds.getFmax() : cds.getFmin();
+                    if (cdsStart >= exon.getFmin() && cdsStart <= exon.getFmax()) {
+                        cdsService.setManuallySetTranslationStart(cds, false);
+                    }
+                }
+            }
+
+            exonService.deleteExon(transcript, exon)
+            List<Feature> deletedFeatures = modifiedFeaturesUniqueNames.get(transcript.getUniqueName());
+            if (deletedFeatures == null) {
+                deletedFeatures = new ArrayList<Feature>();
+                modifiedFeaturesUniqueNames.put(transcript.getUniqueName(), deletedFeatures);
+            }
+            deletedFeatures.add(exon);
+            return transcriptService.getExons(transcript)?.size() > 0;
+        } else {
+            List<Feature> deletedFeatures = modifiedFeaturesUniqueNames.get(feature.getUniqueName());
+            if (deletedFeatures == null) {
+                deletedFeatures = new ArrayList<Feature>();
+                modifiedFeaturesUniqueNames.put(feature.getUniqueName(), deletedFeatures);
+            }
+            deletedFeatures.add(feature);
+            return false;
+        }
+    }
+
+
+    @Transactional
+    def flipStrand(Feature feature) {
+
+        for (FeatureLocation featureLocation in feature.featureLocations) {
+            featureLocation.strand = featureLocation.strand == Strand.POSITIVE.value ? Strand.NEGATIVE.value : Strand.POSITIVE.value
+            featureLocation.save()
+        }
+
+        for (Feature childFeature : feature?.parentFeatureRelationships?.childFeature) {
+            flipStrand(childFeature)
+        }
+
+        feature.save()
+        return feature
+
+    }
+
+    boolean typeHasChildren(Feature feature) {
+        def f = Feature.get(feature.id)
+        boolean hasChildren = !(f instanceof Exon) && !(f instanceof CDS) && !(f instanceof SpliceSite)
+        return hasChildren
+    }
+
+    /**
+     * If genes is empty, create a new gene.
+     * Else, merge
+     * @param genes
+     */
+    @Transactional
+    private Gene mergeGenes(Set<Gene> genes) {
+        // TODO: implement
+        Gene newGene = null
+
+        if (!genes) {
+            log.error "No genes to merge, returning null"
+        }
+
+        for (Gene gene in genes) {
+            if (!newGene) {
+                newGene = gene
+            } else {
+                // merging code goes here
+
+            }
+        }
+
+
+        return newGene
+    }
+
+    /**
+     * Remove old gene / transcript from the transcript
+     * Delete gene if no overlapping.
+     * @param transcript
+     * @param gene
+     */
+    @Transactional
+    private void setGeneTranscript(Transcript transcript, Gene gene) {
+        Gene oldGene = transcriptService.getGene(transcript)
+        if (gene.uniqueName == oldGene.uniqueName) {
+            log.info "Same gene do not need to set"
+            return
+        }
+
+        transcriptService.deleteTranscript(oldGene, transcript)
+        addTranscriptToGene(gene, transcript)
+
+        // if this is empty then delete the gene
+        if (!featureRelationshipService.getChildren(oldGene)) {
+            deleteFeature(oldGene)
+        }
+    }
+
+    /**
+     * From https://github.com/GMOD/Apollo/issues/73
+     * Need to add another call after other calculations are done to verify that we verify that we have not left our current isoform siblings or that we have just joined some and we should merge genes (always taking the one on the left).
+     1 - using OrfOverlapper, find other isoforms
+     2 - for each isoform, confirm that they belong to the same gene (if not, we merge genes)
+     3 - confirm that no other non-overlapping isoforms have the same gene (if not, we create a new gene)
+     * @param transcript
+     */
+    @Transactional
+    def handleIsoformOverlap(Transcript transcript) {
+        Gene originalGene = transcriptService.getGene(transcript)
+
+        // TODO: should go left to right, may need to sort
+        List<Transcript> originalTranscripts = transcriptService.getTranscripts(originalGene)?.sort() { a, b ->
+            a.featureLocation.fmin <=> b.featureLocation.fmin
+        }
+        List<Transcript> newTranscripts = getOverlappingTranscripts(transcript.featureLocation)?.sort() { a, b ->
+            a.featureLocation.fmin <=> b.featureLocation.fmin
+        };
+
+        List<Transcript> leftBehindTranscripts = originalTranscripts - newTranscripts
+
+        Set<Gene> newGenesToMerge = new HashSet<>()
+        for (Transcript newTranscript in newTranscripts) {
+            newGenesToMerge.add(transcriptService.getGene(newTranscript))
+        }
+        Gene newGene = newGenesToMerge ? mergeGenes(newGenesToMerge) : new Gene(
+                name: transcript.name
+                , uniqueName: nameService.generateUniqueName()
+        ).save(flush: true, insert: true)
+
+        for (Transcript newTranscript in newTranscripts) {
+            setGeneTranscript(newTranscript, newGene)
+        }
+
+
+        Set<Gene> usedGenes = new HashSet<>()
+        while (leftBehindTranscripts.size() > 0) {
+            Transcript originalOverlappingTranscript = leftBehindTranscripts.pop()
+            Gene originalOverlappingGene = transcriptService.getGene(originalOverlappingTranscript)
+            List<Transcript> overlappingTranscripts = getOverlappingTranscripts(originalOverlappingTranscript.featureLocation)
+            overlappingTranscripts = overlappingTranscripts - usedGenes
+            overlappingTranscripts.each { it ->
+                setGeneTranscript(it, originalOverlappingGene)
+            }
+            leftBehindTranscripts = leftBehindTranscripts - overlappingTranscripts
+        }
+    }
+
+    @Transactional
+    def handleDynamicIsoformOverlap(Transcript transcript) {
+        // Get all transcripts that overlap transcript and verify if they have the proper parent gene assigned
+        List<Transcript> allOverlappingTranscripts = getTranscriptsWithOverlappingOrf(transcript)
+        List<Transcript> allTranscriptsForCurrentGene = transcriptService.getTranscripts(transcriptService.getGene(transcript))
+        List<Transcript> allTranscripts = (allOverlappingTranscripts + allTranscriptsForCurrentGene).unique()
+        List<Transcript> allSortedTranscripts
+        // force null / 0 strand to be positive
+        // when getting the up-most strand, make sure to put matching transcript strands BEFORE unmatching strands
+        if (transcript.strand != Strand.NEGATIVE.value) {
+            allSortedTranscripts = allTranscripts?.sort() { a, b ->
+                a.strand <=> b.strand ?: a.featureLocation.fmin <=> b.featureLocation.fmin ?: a.name <=> b.name
+            }
+        } else {
+            allSortedTranscripts = allTranscripts?.sort() { a, b ->
+                b.strand <=> a.strand ?: b.featureLocation.fmax <=> a.featureLocation.fmax ?: a.name <=> b.name
+            }
+        }
+        // In a normal scenario, all sorted transcripts should have the same parent indicating no changes to be made.
+        // If there are transcripts that do overlap but do not have the same parent gene then these transcripts should
+        // be merged to the 5' most transcript's gene.
+        // If there are transcripts that do not overlap but have the same parent gene then these transcripts should be
+        // given a new, de-novo gene.
+        log.debug "allSortedTranscripts:${allSortedTranscripts.name}"
+        Transcript fivePrimeTranscript = allSortedTranscripts.get(0)
+        Gene fivePrimeGene = transcriptService.getGene(fivePrimeTranscript)
+        log.debug "5' Transcript: ${fivePrimeTranscript.name}"
+        log.debug "5' Gene: ${fivePrimeGene.name}"
+        allSortedTranscripts.remove(0)
+        ArrayList<Transcript> transcriptsToAssociate = new ArrayList<Transcript>()
+        ArrayList<Gene> genesToMerge = new ArrayList<Gene>()
+        ArrayList<Transcript> transcriptsToDissociate = new ArrayList<Transcript>()
+        ArrayList<Transcript> transcriptsToUpdate = new ArrayList<Transcript>()
+
+        for (Transcript eachTranscript : allSortedTranscripts) {
+            if (eachTranscript && fivePrimeGene && overlapperService.overlaps(eachTranscript, fivePrimeGene)) {
+                if (transcriptService.getGene(eachTranscript).uniqueName != fivePrimeGene.uniqueName) {
+                    transcriptsToAssociate.add(eachTranscript)
+                    genesToMerge.add(transcriptService.getGene(eachTranscript))
+                }
+            } else {
+                if (transcriptService.getGene(eachTranscript).uniqueName == fivePrimeGene.uniqueName) {
+                    transcriptsToDissociate.add(eachTranscript)
+                }
+            }
+        }
+
+        log.debug "Transcripts to Associate: ${transcriptsToAssociate}"
+        log.debug "Transcripts to Dissociate: ${transcriptsToDissociate}"
+        transcriptsToUpdate.addAll(transcriptsToAssociate)
+        transcriptsToUpdate.addAll(transcriptsToDissociate)
+
+        if (transcriptsToAssociate) {
+            Gene mergedGene = mergeGeneEntities(fivePrimeGene, genesToMerge.unique())
+            for (Transcript eachTranscript in transcriptsToAssociate) {
+                Gene eachTranscriptParent = transcriptService.getGene(eachTranscript)
+                featureRelationshipService.removeFeatureRelationship(eachTranscriptParent, eachTranscript)
+                addTranscriptToGene(mergedGene, eachTranscript)
+                eachTranscript.name = nameService.generateUniqueName(eachTranscript, mergedGene.name)
+                eachTranscript.save()
+                if (eachTranscriptParent.parentFeatureRelationships.size() == 0) {
+                    ArrayList<FeatureProperty> featureProperties = eachTranscriptParent.featureProperties
+                    for (FeatureProperty fp : featureProperties) {
+                        featurePropertyService.deleteProperty(eachTranscriptParent, fp)
+                    }
+                    //eachTranscriptParent.delete()
+                    // replace a direct delete with the standard method
+                    Feature topLevelFeature = featureService.getTopLevelFeature(eachTranscriptParent)
+                    featureRelationshipService.deleteFeatureAndChildren(topLevelFeature)
+                }
+            }
+        }
+
+        if (transcriptsToDissociate) {
+            Transcript firstTranscript = null
+            for (Transcript eachTranscript in transcriptsToDissociate) {
+                if (firstTranscript == null) {
+                    firstTranscript = eachTranscript
+                    Gene newGene = new Gene(
+                            uniqueName: nameService.generateUniqueName(),
+                            name: nameService.generateUniqueName(fivePrimeGene)
+                    )
+
+                    firstTranscript.owners.each {
+                        newGene.addToOwners(it)
+                    }
+                    newGene.save(flush: true)
+
+                    FeatureLocation newGeneFeatureLocation = new FeatureLocation(
+                            feature: newGene,
+                            fmin: firstTranscript.fmin,
+                            fmax: firstTranscript.fmax,
+                            strand: firstTranscript.strand,
+                            sequence: firstTranscript.featureLocation.sequence,
+                            residueInfo: firstTranscript.featureLocation.residueInfo,
+                            locgroup: firstTranscript.featureLocation.locgroup,
+                            rank: firstTranscript.featureLocation.rank
+                    ).save(flush: true)
+                    newGene.addToFeatureLocations(newGeneFeatureLocation)
+                    featureRelationshipService.removeFeatureRelationship(transcriptService.getGene(firstTranscript), firstTranscript)
+                    addTranscriptToGene(newGene, firstTranscript)
+                    firstTranscript.name = nameService.generateUniqueName(firstTranscript, newGene.name)
+                    firstTranscript.save(flush: true)
+                    continue
+                }
+                if (eachTranscript && firstTranscript && overlapperService.overlaps(eachTranscript, firstTranscript)) {
+                    featureRelationshipService.removeFeatureRelationship(transcriptService.getGene(eachTranscript), eachTranscript)
+                    addTranscriptToGene(transcriptService.getGene(firstTranscript), eachTranscript)
+                    firstTranscript.name = nameService.generateUniqueName(firstTranscript, transcriptService.getGene(firstTranscript).name)
+                    firstTranscript.save(flush: true)
+                } else {
+                    throw new AnnotationException("Left behind transcript that doesn't overlap with any other transcripts")
+                }
+            }
+        }
+        return transcriptsToUpdate
+    }
+
+    def getTranscriptsWithOverlappingOrf(Transcript transcript) {
+        ArrayList<Transcript> overlappingTranscripts = getOverlappingTranscripts(transcript.featureLocation)
+        overlappingTranscripts.remove(transcript) // removing itself
+        ArrayList<Transcript> transcriptsWithOverlappingOrf = new ArrayList<Transcript>()
+        for (Transcript eachTranscript in overlappingTranscripts) {
+            if (eachTranscript && transcript && overlapperService.overlaps(eachTranscript, transcript)) {
+                transcriptsWithOverlappingOrf.add(eachTranscript)
+            }
+        }
+        return transcriptsWithOverlappingOrf
+    }
+
+    @Transactional
+    Gene mergeGeneEntities(Gene mainGene, ArrayList<Gene> genes) {
+        def fminList = genes.featureLocation.fmin
+        def fmaxList = genes.featureLocation.fmax
+        fminList.add(mainGene.fmin)
+        fmaxList.add(mainGene.fmax)
+
+        FeatureLocation newFeatureLocation = mainGene.featureLocation
+        newFeatureLocation.fmin = fminList.min()
+        newFeatureLocation.fmax = fmaxList.max()
+        newFeatureLocation.save(flush: true)
+        for (Gene gene in genes) {
+            gene.featureDBXrefs.each { mainGene.addToFeatureDBXrefs(it) }
+            gene.featureGenotypes.each { mainGene.addToFeatureGenotypes(it) }
+            gene.featurePhenotypes.each { mainGene.addToFeaturePhenotypes(it) }
+            gene.featurePublications.each { mainGene.addToFeaturePublications(it) }
+            gene.featureProperties.each { mainGene.addToFeatureProperties(it) }
+            gene.featureSynonyms.each { mainGene.addToFeatureSynonyms(it) }
+            gene.owners.each { mainGene.addToOwners(it) }
+            gene.synonyms.each { mainGene.addToSynonyms(it) }
+        }
+
+        mainGene.save(flush: true)
+        return mainGene
+    }
+
+    private class SequenceAlterationInContextPositionComparator<SequenceAlterationInContext> implements Comparator<SequenceAlterationInContext> {
+        @Override
+        int compare(SequenceAlterationInContext obj1, SequenceAlterationInContext obj2) {
+            return obj1.fmin - obj2.fmin
+        }
+    }
+
+    def sortSequenceAlterationInContext(List<SequenceAlterationInContext> sequenceAlterationInContextList) {
+        Collections.sort(sequenceAlterationInContextList, new SequenceAlterationInContextPositionComparator<SequenceAlterationInContext>())
+        return sequenceAlterationInContextList
+    }
+
+    def sequenceAlterationInContextOverlapper(Feature feature, SequenceAlterationInContext sequenceAlteration) {
+        def exonList = []
+        if (feature instanceof Transcript) {
+            exonList = transcriptService.getSortedExons(feature, true)
+        }
+        else if (feature instanceof CDS) {
+            Transcript transcript = transcriptService.getTranscript(feature)
+            exonList = transcriptService.getSortedExons(transcript, true)
+        }
+
+        for (Exon exon : exonList) {
+            int fmin = exon.fmin
+            int fmax = exon.fmax
+            if ((sequenceAlteration.fmin >= fmin && sequenceAlteration.fmin <= fmax) || (sequenceAlteration.fmin + sequenceAlteration.alterationResidue.length() >= fmin && sequenceAlteration.fmax + sequenceAlteration.alterationResidue.length() <= fmax)) {
+                // alteration overlaps with exon
+                return true
+            }
+        }
+        return false
+    }
+
+    String getResiduesWithAlterations(Feature feature, Collection<SequenceAlteration> sequenceAlterations = new ArrayList<>()) {
+        String residueString = null
+        List<SequenceAlterationInContext> sequenceAlterationInContextList = new ArrayList<>()
+        if (feature instanceof Transcript) {
+            residueString = transcriptService.getResiduesFromTranscript((Transcript) feature)
+            // sequence from exons, with UTRs too
+            sequenceAlterationInContextList = getSequenceAlterationsInContext(feature, sequenceAlterations)
+        } else if (feature instanceof CDS) {
+            residueString = cdsService.getResiduesFromCDS((CDS) feature)
+            // sequence from exons without UTRs
+            sequenceAlterationInContextList = getSequenceAlterationsInContext(transcriptService.getTranscript(feature), sequenceAlterations)
+        } else {
+            // sequence from feature, as is
+            residueString = sequenceService.getResiduesFromFeature(feature)
+            sequenceAlterationInContextList = getSequenceAlterationsInContext(feature, sequenceAlterations)
+        }
+        if (sequenceAlterations.size() == 0 || sequenceAlterationInContextList.size() == 0) {
+            return residueString
+        }
+
+        StringBuilder residues = new StringBuilder(residueString);
+        List<SequenceAlterationInContext> orderedSequenceAlterationInContextList = new ArrayList<>(sequenceAlterationInContextList)
+        Collections.sort(orderedSequenceAlterationInContextList, new SequenceAlterationInContextPositionComparator<SequenceAlterationInContext>());
+        if (!feature.strand.equals(orderedSequenceAlterationInContextList.get(0).strand)) {
+            Collections.reverse(orderedSequenceAlterationInContextList);
+        }
+
+        int currentOffset = 0
+        for (SequenceAlterationInContext sequenceAlteration : orderedSequenceAlterationInContextList) {
+            int localCoordinate
+            if (feature instanceof Transcript) {
+                localCoordinate = convertSourceCoordinateToLocalCoordinateForTranscript((Transcript) feature, sequenceAlteration.fmin);
+
+            } else if (feature instanceof CDS) {
+                if (!((sequenceAlteration.fmin >= feature.fmin && sequenceAlteration.fmin <= feature.fmax) || (sequenceAlteration.fmax >= feature.fmin && sequenceAlteration.fmax <= feature.fmin))) {
+                    // check to verify if alteration is part of the CDS
+                    continue
+                }
+                localCoordinate = convertSourceCoordinateToLocalCoordinateForCDS(transcriptService.getTranscript(feature), sequenceAlteration.fmin)
+            } else {
+                localCoordinate = convertSourceCoordinateToLocalCoordinate(feature, sequenceAlteration.fmin);
+            }
+
+            String sequenceAlterationResidues = sequenceAlteration.alterationResidue
+            if (feature.getFeatureLocation().getStrand() == -1) {
+                sequenceAlterationResidues = SequenceTranslationHandler.reverseComplementSequence(sequenceAlterationResidues);
+            }
+            // Insertions
+            if (sequenceAlteration.instanceOf == Insertion.canonicalName) {
+                if (feature.getFeatureLocation().getStrand() == -1) {
+                    ++localCoordinate;
+                }
+                residues.insert(localCoordinate + currentOffset, sequenceAlterationResidues);
+                currentOffset += sequenceAlterationResidues.length();
+            }
+            // Deletions
+            else if (sequenceAlteration.instanceOf == Deletion.canonicalName) {
+                if (feature.getFeatureLocation().getStrand() == -1) {
+                    residues.delete(localCoordinate + currentOffset - sequenceAlteration.alterationResidue.length() + 1,
+                            localCoordinate + currentOffset + 1);
+                } else {
+                    residues.delete(localCoordinate + currentOffset,
+                            localCoordinate + currentOffset + sequenceAlteration.alterationResidue.length());
+                }
+                currentOffset -= sequenceAlterationResidues.length();
+            }
+            // Substitions
+            else if (sequenceAlteration.instanceOf == Substitution.canonicalName) {
+                int start = feature.getStrand() == -1 ? localCoordinate - (sequenceAlteration.alterationResidue.length() - 1) : localCoordinate;
+                residues.replace(start + currentOffset,
+                        start + currentOffset + sequenceAlteration.alterationResidue.length(),
+                        sequenceAlterationResidues);
+            }
+        }
+
+        return residues.toString();
+    }
+
+    List<SequenceAlteration> getSequenceAlterationsForFeature(Feature feature) {
+        int fmin = feature.fmin
+        int fmax = feature.fmax
+        Sequence sequence = feature.featureLocation.sequence
+        sessionFactory.currentSession.flushMode = FlushMode.MANUAL
+
+        List<SequenceAlteration> sequenceAlterations = SequenceAlteration.executeQuery("select distinct sa from SequenceAlteration sa join sa.featureLocations fl where ((fl.fmin >= :fmin and fl.fmin <= :fmax) or (fl.fmax >= :fmin and fl.fmax <= :fmax)) and fl.sequence = :seqId", [fmin: fmin, fmax: fmax, seqId: sequence])
+        sessionFactory.currentSession.flushMode = FlushMode.AUTO
+
+        return sequenceAlterations
+    }
+
+
+    List<SequenceAlterationInContext> getSequenceAlterationsInContext(Feature feature, Collection<SequenceAlteration> sequenceAlterations) {
+        List<SequenceAlterationInContext> sequenceAlterationInContextList = new ArrayList<>()
+        if (!(feature instanceof CDS) && !(feature instanceof Transcript)) {
+            // for features that are not instance of CDS or Transcript (ex. Single exons)
+            int featureFmin = feature.fmin
+            int featureFmax = feature.fmax
+            for (SequenceAlteration eachSequenceAlteration : sequenceAlterations) {
+                int alterationFmin = eachSequenceAlteration.fmin
+                int alterationFmax = eachSequenceAlteration.fmax
+                SequenceAlterationInContext sa = new SequenceAlterationInContext()
+                if ((alterationFmin >= featureFmin && alterationFmax <= featureFmax) && (alterationFmax >= featureFmin && alterationFmax <= featureFmax)) {
+                    // alteration is within the generic feature
+                    sa.fmin = alterationFmin
+                    sa.fmax = alterationFmax
+                    if (eachSequenceAlteration instanceof Insertion) {
+                        sa.instanceOf = Insertion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Deletion) {
+                        sa.instanceOf = Deletion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Substitution) {
+                        sa.instanceOf = Substitution.canonicalName
+                    }
+                    sa.type = 'within'
+                    sa.strand = eachSequenceAlteration.strand
+                    sa.name = eachSequenceAlteration.name + '-inContext'
+                    sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                    sa.offset = eachSequenceAlteration.offset
+                    sa.alterationResidue = eachSequenceAlteration.alterationResidue
+                    sequenceAlterationInContextList.add(sa)
+                } else if ((alterationFmin >= featureFmin && alterationFmin <= featureFmax) && (alterationFmax >= featureFmin && alterationFmax >= featureFmax)) {
+                    // alteration starts in exon but ends in an intron
+                    int difference = alterationFmax - featureFmax
+                    sa.fmin = alterationFmin
+                    sa.fmax = Math.min(featureFmax, alterationFmax)
+                    if (eachSequenceAlteration instanceof Insertion) {
+                        sa.instanceOf = Insertion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Deletion) {
+                        sa.instanceOf = Deletion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Substitution) {
+                        sa.instanceOf = Substitution.canonicalName
+                    }
+                    sa.type = 'exon-to-intron'
+                    sa.strand = eachSequenceAlteration.strand
+                    sa.name = eachSequenceAlteration.name + '-inContext'
+                    sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                    sa.offset = eachSequenceAlteration.offset - difference
+                    sa.alterationResidue = eachSequenceAlteration.alterationResidue.substring(0, eachSequenceAlteration.alterationResidue.length() - difference)
+                    sequenceAlterationInContextList.add(sa)
+                } else if ((alterationFmin <= featureFmin && alterationFmin <= featureFmax) && (alterationFmax >= featureFmin && alterationFmax <= featureFmax)) {
+                    // alteration starts within intron but ends in an exon
+                    int difference = featureFmin - alterationFmin
+                    sa.fmin = Math.max(featureFmin, alterationFmin)
+                    sa.fmax = alterationFmax
+                    if (eachSequenceAlteration instanceof Insertion) {
+                        sa.instanceOf = Insertion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Deletion) {
+                        sa.instanceOf = Deletion.canonicalName
+                    } else if (eachSequenceAlteration instanceof Substitution) {
+                        sa.instanceOf = Substitution.canonicalName
+                    }
+                    sa.type = 'intron-to-exon'
+                    sa.strand = eachSequenceAlteration.strand
+                    sa.name = eachSequenceAlteration.name + '-inContext'
+                    sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                    sa.offset = eachSequenceAlteration.offset - difference
+                    sa.alterationResidue = eachSequenceAlteration.alterationResidue.substring(difference, eachSequenceAlteration.alterationResidue.length())
+                    sequenceAlterationInContextList.add(sa)
+                }
+            }
+        } else {
+            List<Exon> exonList = feature instanceof CDS ? transcriptService.getSortedExons(transcriptService.getTranscript(feature),true) : transcriptService.getSortedExons( (Transcript) feature, true)
+            for (Exon exon : exonList) {
+                int exonFmin = exon.fmin
+                int exonFmax = exon.fmax
+
+                for (SequenceAlteration eachSequenceAlteration : sequenceAlterations) {
+                    int alterationFmin = eachSequenceAlteration.fmin
+                    int alterationFmax = eachSequenceAlteration.fmax
+                    SequenceAlterationInContext sa = new SequenceAlterationInContext()
+                    if ((alterationFmin >= exonFmin && alterationFmin <= exonFmax) && (alterationFmax >= exonFmin && alterationFmax <= exonFmax)) {
+                        // alteration is within exon
+                        sa.fmin = alterationFmin
+                        sa.fmax = alterationFmax
+                        if (eachSequenceAlteration instanceof Insertion) {
+                            sa.instanceOf = Insertion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Deletion) {
+                            sa.instanceOf = Deletion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Substitution) {
+                            sa.instanceOf = Substitution.canonicalName
+                        }
+                        sa.type = 'within'
+                        sa.strand = eachSequenceAlteration.strand
+                        sa.name = eachSequenceAlteration.name + '-inContext'
+                        sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                        sa.offset = eachSequenceAlteration.offset
+                        sa.alterationResidue = eachSequenceAlteration.alterationResidue
+                        sequenceAlterationInContextList.add(sa)
+                    } else if ((alterationFmin >= exonFmin && alterationFmin <= exonFmax) && (alterationFmax >= exonFmin && alterationFmax >= exonFmax)) {
+                        // alteration starts in exon but ends in an intron
+                        int difference = alterationFmax - exonFmax
+                        sa.fmin = alterationFmin
+                        sa.fmax = Math.min(exonFmax, alterationFmax)
+                        if (eachSequenceAlteration instanceof Insertion) {
+                            sa.instanceOf = Insertion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Deletion) {
+                            sa.instanceOf = Deletion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Substitution) {
+                            sa.instanceOf = Substitution.canonicalName
+                        }
+                        sa.type = 'exon-to-intron'
+                        sa.strand = eachSequenceAlteration.strand
+                        sa.name = eachSequenceAlteration.name + '-inContext'
+                        sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                        sa.offset = eachSequenceAlteration.offset - difference
+                        sa.alterationResidue = eachSequenceAlteration.alterationResidue.substring(0, eachSequenceAlteration.alterationResidue.length() - difference)
+                        sequenceAlterationInContextList.add(sa)
+                    } else if ((alterationFmin <= exonFmin && alterationFmin <= exonFmax) && (alterationFmax >= exonFmin && alterationFmax <= exonFmax)) {
+                        // alteration starts within intron but ends in an exon
+                        int difference = exonFmin - alterationFmin
+                        sa.fmin = Math.max(exonFmin, alterationFmin)
+                        sa.fmax = alterationFmax
+                        if (eachSequenceAlteration instanceof Insertion) {
+                            sa.instanceOf = Insertion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Deletion) {
+                            sa.instanceOf = Deletion.canonicalName
+                        } else if (eachSequenceAlteration instanceof Substitution) {
+                            sa.instanceOf = Substitution.canonicalName
+                        }
+                        sa.type = 'intron-to-exon'
+                        sa.strand = eachSequenceAlteration.strand
+                        sa.name = eachSequenceAlteration.name + '-inContext'
+                        sa.originalAlterationUniqueName = eachSequenceAlteration.uniqueName
+                        sa.offset = eachSequenceAlteration.offset - difference
+                        sa.alterationResidue = eachSequenceAlteration.alterationResidue.substring(difference, eachSequenceAlteration.alterationResidue.length())
+                        sequenceAlterationInContextList.add(sa)
+                    }
+                }
+            }
+        }
+        return sequenceAlterationInContextList
+    }
+
+    int convertModifiedLocalCoordinateToSourceCoordinate(Feature feature, int localCoordinate) {
+        Transcript transcript = (Transcript) featureRelationshipService.getParentForFeature(feature, Transcript.ontologyId)
+        List<SequenceAlterationInContext> alterations = new ArrayList<>()
+        if (feature instanceof CDS) {
+            List<SequenceAlteration> frameshiftsAsAlterations = getFrameshiftsAsAlterations(transcript)
+            if (frameshiftsAsAlterations.size() > 0) {
+                for (SequenceAlteration frameshifts : frameshiftsAsAlterations) {
+                    SequenceAlterationInContext sa = new SequenceAlterationInContext()
+                    sa.fmin = frameshifts.fmin
+                    sa.fmax = frameshifts.fmax
+                    sa.alterationResidue = frameshifts.alterationResidue
+                    sa.type = 'frameshifts-as-alteration'
+                    sa.instanceOf = Frameshift.canonicalName
+                    sa.originalAlterationUniqueName = frameshifts.uniqueName
+                    sa.name = frameshifts.uniqueName + '-frameshifts-inContext'
+                    alterations.add(sa)
+                }
+            }
+        }
+
+        alterations.addAll(getSequenceAlterationsInContext(feature, getAllSequenceAlterationsForFeature(feature)))
+        if (alterations.size() == 0) {
+            if (feature instanceof CDS) {
+                // if feature is CDS then calling convertLocalCoordinateToSourceCoordinateForCDS
+                return convertLocalCoordinateToSourceCoordinateForCDS((CDS) feature, localCoordinate);
+            } else if (feature instanceof Transcript) {
+                // if feature is Transcript then calling convertLocalCoordinateToSourceCoordinateForTranscript
+                return convertLocalCoordinateToSourceCoordinateForTranscript((Transcript) feature, localCoordinate);
+            } else {
+                // calling convertLocalCoordinateToSourceCoordinate
+                return convertLocalCoordinateToSourceCoordinate(feature, localCoordinate);
+            }
+        }
+
+        Collections.sort(alterations, new SequenceAlterationInContextPositionComparator<SequenceAlterationInContext>());
+        if (feature.getFeatureLocation().getStrand() == -1) {
+            Collections.reverse(alterations);
+        }
+
+        int insertionOffset = 0
+        int deletionOffset = 0
+        for (SequenceAlterationInContext alteration : alterations) {
+            int alterationResidueLength = alteration.alterationResidue.length()
+            if (!sequenceAlterationInContextOverlapper(feature, alteration)) {
+                // sequenceAlterationInContextOverlapper method verifies if the alteration is within any of the given exons of the transcript
+                continue;
+            }
+            int coordinateInContext = -1
+            if (feature instanceof CDS) {
+                // if feature is CDS then calling convertSourceCoordinateToLocalCoordinateForCDS
+                coordinateInContext = convertSourceCoordinateToLocalCoordinateForCDS(feature, alteration.fmin)
+            } else if (feature instanceof Transcript) {
+                // if feature is Transcript then calling convertSourceCoordinateToLocalCoordinateForTranscript
+                coordinateInContext = convertSourceCoordinateToLocalCoordinateForTranscript( (Transcript) feature, alteration.fmin)
+            } else {
+                // calling convertSourceCoordinateToLocalCoordinate
+                coordinateInContext = convertSourceCoordinateToLocalCoordinate(feature, alteration.fmin)
+            }
+
+            if (feature.strand == Strand.NEGATIVE.value) {
+                if (coordinateInContext <= localCoordinate && alteration.instanceOf == Deletion.canonicalName) {
+                    deletionOffset += alterationResidueLength
+                }
+                if ((coordinateInContext - alterationResidueLength) - 1 <= localCoordinate && alteration.instanceOf == Insertion.canonicalName) {
+                    insertionOffset += alterationResidueLength
+                }
+                if ((localCoordinate - coordinateInContext) - 1 < alterationResidueLength && (localCoordinate - coordinateInContext) >= 0 && alteration.instanceOf == Insertion.canonicalName) {
+                    insertionOffset -= (alterationResidueLength - (localCoordinate - coordinateInContext - 1))
+
+                }
+
+            } else {
+                if (coordinateInContext < localCoordinate && alteration.instanceOf == Deletion.canonicalName) {
+                    deletionOffset += alterationResidueLength
+                }
+                if ((coordinateInContext + alterationResidueLength) <= localCoordinate && alteration.instanceOf == Insertion.canonicalName) {
+                    insertionOffset += alterationResidueLength
+                }
+                if ((localCoordinate - coordinateInContext) < alterationResidueLength && (localCoordinate - coordinateInContext) >= 0 && alteration.instanceOf == Insertion.canonicalName) {
+                    insertionOffset += localCoordinate - coordinateInContext
+                }
+            }
+        }
+        localCoordinate = localCoordinate - insertionOffset
+        localCoordinate = localCoordinate + deletionOffset
+
+        if (feature instanceof CDS) {
+            // if feature is CDS then calling convertLocalCoordinateToSourceCoordinateForCDS
+            return convertLocalCoordinateToSourceCoordinateForCDS((CDS) feature, localCoordinate)
+        } else if (feature instanceof Transcript) {
+            // if feature is Transcript then calling convertLocalCoordinateToSourceCoordinateForTranscript
+            return convertLocalCoordinateToSourceCoordinateForTranscript((Transcript) feature, localCoordinate)
+        } else {
+            // calling convertLocalCoordinateToSourceCoordinate for all other feature types
+            return convertLocalCoordinateToSourceCoordinate(feature, localCoordinate)
+        }
+    }
+
+    /* convert an input local coordinate to a local coordinate that incorporates sequence alterations */
+
+    int convertSourceToModifiedLocalCoordinate(Feature feature, Integer localCoordinate, List<SequenceAlteration> alterations = new ArrayList<>()) {
+        log.debug "convertSourceToModifiedLocalCoordinate"
+
+        if (alterations.size() == 0) {
+            log.debug "No alterations returning ${localCoordinate}"
+            return localCoordinate
+        }
+
+
+        Collections.sort(alterations, new FeaturePositionComparator<SequenceAlteration>());
+        if (feature.getFeatureLocation().getStrand() == -1) {
+            Collections.reverse(alterations);
+        }
+
+        int deletionOffset = 0
+        int insertionOffset = 0
+
+        for (SequenceAlteration alteration : alterations) {
+            int alterationResidueLength = alteration.alterationResidue.length()
+            int coordinateInContext = convertSourceCoordinateToLocalCoordinate(feature, alteration.fmin);
+
+            //getAllSequenceAlterationsForFeature returns alterations over entire scaffold?!
+            if (alteration.fmin <= feature.fmin || alteration.fmax > feature.fmax) {
+                continue
+            }
+
+            if (feature.strand == Strand.NEGATIVE.value) {
+                coordinateInContext = feature.featureLocation.calculateLength() - coordinateInContext
+                log.debug "Checking negative insertion ${coordinateInContext} ${localCoordinate} ${(coordinateInContext - alterationResidueLength) - 1}"
+                if (coordinateInContext <= localCoordinate && alteration instanceof Deletion) {
+                    log.debug "Processing negative deletion"
+                    deletionOffset += alterationResidueLength
+                }
+                if ((coordinateInContext - alterationResidueLength) - 1 <= localCoordinate && alteration instanceof Insertion) {
+                    log.debug "Processing negative insertion ${coordinateInContext} ${localCoordinate} ${(coordinateInContext - alterationResidueLength) - 1}"
+                    insertionOffset += alterationResidueLength
+                }
+                if ((localCoordinate - coordinateInContext) - 1 < alterationResidueLength && (localCoordinate - coordinateInContext) >= 0 && alteration instanceof Insertion) {
+                    log.debug "Processing negative insertion pt 2"
+                    insertionOffset -= (alterationResidueLength - (localCoordinate - coordinateInContext - 1))
+
+                }
+
+            } else {
+                if (coordinateInContext < localCoordinate && alteration instanceof Deletion) {
+                    log.debug "Processing positive deletion"
+                    deletionOffset += alterationResidueLength
+                }
+                if ((coordinateInContext + alterationResidueLength) <= localCoordinate && alteration instanceof Insertion) {
+                    log.debug "Processing positive insertion"
+                    insertionOffset += alterationResidueLength
+                }
+                if ((localCoordinate - coordinateInContext) < alterationResidueLength && (localCoordinate - coordinateInContext) >= 0 && alteration instanceof Insertion) {
+                    log.debug "Processing positive insertion pt 2"
+                    insertionOffset += localCoordinate - coordinateInContext
+                }
+            }
+
+        }
+
+        log.debug "Returning ${localCoordinate - deletionOffset + insertionOffset}"
+        return localCoordinate - deletionOffset + insertionOffset
+
+    }
+
+
+    def changeAnnotationType(JSONObject inputObject, Feature feature, Sequence sequence, User user, String type) {
+        String uniqueName = feature.uniqueName
+        String originalType = feature.alternateCvTerm ? feature.alternateCvTerm : feature.cvTerm
+        JSONObject currentFeatureJsonObject = convertFeatureToJSON(feature)
+        Feature newFeature = null
+
+        String topLevelFeatureType = null
+        if (type == Transcript.alternateCvTerm) {
+            topLevelFeatureType = Pseudogene.alternateCvTerm
+        } else if (singletonFeatureTypes.contains(type)) {
+            topLevelFeatureType = type
+        } else {
+            topLevelFeatureType = Gene.alternateCvTerm
+        }
+
+        Gene parentGene = null
+        String parentGeneSymbol = null
+        String parentGeneDescription = null
+        Set<DBXref> parentGeneDbxrefs = null
+        Set<FeatureProperty> parentGeneFeatureProperties = null
+        List<Transcript> transcriptList = []
+
+        if (feature instanceof Transcript) {
+            parentGene = transcriptService.getGene((Transcript) feature)
+            parentGeneSymbol = parentGene.symbol
+            parentGeneDescription = parentGene.description
+            parentGeneDbxrefs = parentGene.featureDBXrefs
+            parentGeneFeatureProperties = parentGene.featureProperties
+            transcriptList = transcriptService.getTranscripts(parentGene)
+        }
+
+        log.debug "Parent gene Dbxrefs: ${parentGeneDbxrefs}"
+        log.debug "Parent gene Feature Properties: ${parentGeneFeatureProperties}"
+
+        if (currentFeatureJsonObject.has(FeatureStringEnum.PARENT_TYPE.value)) {
+            currentFeatureJsonObject.get(FeatureStringEnum.PARENT_TYPE.value).name = topLevelFeatureType
+        }
+        currentFeatureJsonObject.get(FeatureStringEnum.TYPE.value).name = type
+        currentFeatureJsonObject.put(FeatureStringEnum.USERNAME.value, currentFeatureJsonObject.get(FeatureStringEnum.OWNER.value.toLowerCase()))
+        currentFeatureJsonObject.remove(FeatureStringEnum.PARENT_ID.value)
+        currentFeatureJsonObject.remove(FeatureStringEnum.ID.value)
+        currentFeatureJsonObject.remove(FeatureStringEnum.OWNER.value.toLowerCase())
+        currentFeatureJsonObject.remove(FeatureStringEnum.DATE_CREATION.value)
+        currentFeatureJsonObject.remove(FeatureStringEnum.DATE_LAST_MODIFIED.value)
+        if (currentFeatureJsonObject.has(FeatureStringEnum.CHILDREN.value)) {
+            for (JSONObject childFeature : currentFeatureJsonObject.get(FeatureStringEnum.CHILDREN.value)) {
+                childFeature.remove(FeatureStringEnum.ID.value)
+                childFeature.remove(FeatureStringEnum.OWNER.value.toLowerCase())
+                childFeature.remove(FeatureStringEnum.DATE_CREATION.value)
+                childFeature.remove(FeatureStringEnum.DATE_LAST_MODIFIED.value)
+                childFeature.get(FeatureStringEnum.PARENT_TYPE.value).name = type
+            }
+        }
+
+
+        if (!singletonFeatureTypes.contains(originalType) && rnaFeatureTypes.contains(type)) {
+            // *RNA to *RNA
+            if (transcriptList.size() == 1) {
+                featureRelationshipService.deleteFeatureAndChildren(parentGene)
+            } else {
+                featureRelationshipService.removeFeatureRelationship(parentGene, feature)
+                featureRelationshipService.deleteFeatureAndChildren(feature)
+            }
+
+            log.debug "Converting ${originalType} to ${type}"
+            Transcript transcript = null
+            if (type == MRNA.alternateCvTerm) {
+                // *RNA to mRNA
+                transcript = generateTranscript(currentFeatureJsonObject, sequence, true)
+                setLongestORF(transcript)
+            } else {
+                // *RNA to *RNA
+                transcript = addFeature(currentFeatureJsonObject, sequence, user, true)
+                setLongestORF(transcript)
+            }
+
+            Gene newGene = transcriptService.getGene(transcript)
+            newGene.symbol = parentGeneSymbol
+            newGene.description = parentGeneDescription
+
+            parentGeneDbxrefs.each { it ->
+                DBXref dbxref = new DBXref(
+                        db: it.db,
+                        accession: it.accession,
+                        version: it.version,
+                        description: it.description
+                ).save()
+                newGene.addToFeatureDBXrefs(dbxref)
+            }
+
+            parentGeneFeatureProperties.each { it ->
+                if (it instanceof Comment) {
+                    featurePropertyService.addComment(newGene, it.value)
+                } else {
+                    FeatureProperty fp = new FeatureProperty(
+                            type: it.type,
+                            value: it.value,
+                            rank: it.rank,
+                            tag: it.tag,
+                            feature: newGene
+                    ).save()
+                    newGene.addToFeatureProperties(fp)
+                }
+            }
+            newGene.save(flush: true)
+            newFeature = transcript
+        } else if (!singletonFeatureTypes.contains(originalType) && singletonFeatureTypes.contains(type)) {
+            // *RNA to singleton
+            if (transcriptList.size() == 1) {
+                featureRelationshipService.deleteFeatureAndChildren(parentGene)
+            } else {
+                featureRelationshipService.removeFeatureRelationship(parentGene, feature)
+                featureRelationshipService.deleteFeatureAndChildren(feature)
+            }
+            currentFeatureJsonObject.put(FeatureStringEnum.UNIQUENAME.value, uniqueName)
+            currentFeatureJsonObject.remove(FeatureStringEnum.CHILDREN.value)
+            currentFeatureJsonObject.remove(FeatureStringEnum.PARENT_TYPE.value)
+            currentFeatureJsonObject.remove(FeatureStringEnum.PARENT_ID.value)
+            currentFeatureJsonObject.get(FeatureStringEnum.LOCATION.value).strand = 0
+            Feature singleton = addFeature(currentFeatureJsonObject, sequence, user, true)
+            newFeature = singleton
+        } else if (singletonFeatureTypes.contains(originalType) && singletonFeatureTypes.contains(type)) {
+            // singleton to singleton
+            currentFeatureJsonObject.put(FeatureStringEnum.UNIQUENAME.value, uniqueName)
+            featureRelationshipService.deleteFeatureAndChildren(feature)
+            Feature singleton = addFeature(currentFeatureJsonObject, sequence, user, true)
+            newFeature = singleton
+        } else {
+            log.error "Not enough information available to change ${uniqueName} from ${originalType} -> ${type}."
+        }
+
+        // TODO: synonyms, featureSynonyms, featureGenotypes, featurePhenotypes
+
+        return newFeature
+    }
+
+    def addFeature(JSONObject jsonFeature, Sequence sequence, User user, boolean suppressHistory, boolean useName = false) {
+        Feature returnFeature = null
+
+        if (rnaFeatureTypes.contains(jsonFeature.get(FeatureStringEnum.TYPE.value).name)) {
+            Gene gene = jsonFeature.has(FeatureStringEnum.PARENT_ID.value) ? (Gene) Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.PARENT_ID.value)) : null
+            Transcript transcript = null
+
+            if (gene) {
+                // Scenario I - if 'parent_id' attribute is given then find the gene
+                transcript = (Transcript) convertJSONToFeature(jsonFeature, sequence)
+                if (transcript.fmin < 0 || transcript.fmax < 0) {
+                    throw new AnnotationException("Feature cannot have negative coordinates")
+                }
+
+                setOwner(transcript, user)
+
+                addTranscriptToGene(gene, transcript)
+                if (!suppressHistory) {
+                    String name = nameService.generateUniqueName(transcript)
+                    transcript.name = name
+                }
+
+                // set the original name for feature
+                if (useName && jsonFeature.has(FeatureStringEnum.NAME.value)) {
+                    transcript.name = jsonFeature.get(FeatureStringEnum.NAME.value)
+                }
+
+            } else {
+                // Scenario II - find and overlapping isoform and if present, add current transcript to its gene.
+                // Disabling Scenario II since there is no appropriate overlapper to determine overlaps between non-coding transcripts.
+            }
+
+            if (gene == null) {
+                log.debug "gene is still NULL"
+                // Scenario III - create a de-novo gene
+                JSONObject jsonGene = new JSONObject()
+                if (jsonFeature.has(FeatureStringEnum.PARENT.value)) {
+                    // Scenario IIIa - use the 'parent' attribute, if provided, from feature JSON
+                    jsonGene = JSON.parse(jsonFeature.getString(FeatureStringEnum.PARENT.value)) as JSONObject
+                    jsonGene.put(FeatureStringEnum.CHILDREN.value, new JSONArray().put(jsonFeature))
+                }
+                else {
+                    // Scenario IIIb - use the current mRNA's featurelocation for gene
+                    jsonGene.put(FeatureStringEnum.CHILDREN.value, new JSONArray().put(jsonFeature))
+                    jsonGene.put(FeatureStringEnum.LOCATION.value, jsonFeature.getJSONObject(FeatureStringEnum.LOCATION.value))
+                    String cvTermString = jsonFeature.get(FeatureStringEnum.TYPE.value).name == Transcript.alternateCvTerm ? Pseudogene.alternateCvTerm : Gene.alternateCvTerm
+                    jsonGene.put(FeatureStringEnum.TYPE.value, convertCVTermToJSON(FeatureStringEnum.CV.value, cvTermString))
+                }
+
+                String geneName = null
+                if (jsonGene.has(FeatureStringEnum.NAME.value)) {
+                    geneName = jsonGene.getString(FeatureStringEnum.NAME.value)
+                    log.debug "jsonGene already has 'name': ${geneName}"
+                }
+                else if (jsonFeature.has(FeatureStringEnum.PARENT_NAME.value)) {
+                    String principalName = jsonFeature.getString(FeatureStringEnum.PARENT_NAME.value)
+                    geneName = nameService.makeUniqueGeneName(sequence.organism, principalName, false)
+                    log.debug "jsonFeature has 'parent_name' attribute; using ${principalName} to generate ${geneName}"
+                }
+                else
+                if (jsonFeature.has(FeatureStringEnum.NAME.value)) {
+                    geneName = jsonFeature.getString(FeatureStringEnum.NAME.value)
+                    log.debug "jsonGene already has 'name': ${geneName}"
+                }
+                else {
+                    geneName = nameService.makeUniqueGeneName(sequence.organism, sequence.name, false)
+                    log.debug "Making a new unique gene name: ${geneName}"
+                }
+
+                if (!suppressHistory) {
+                    geneName = nameService.makeUniqueGeneName(sequence.organism, geneName, true)
+                }
+
+                // set back to the original gene name
+                if (jsonFeature.has(FeatureStringEnum.GENE_NAME.value)) {
+                    geneName = jsonFeature.getString(FeatureStringEnum.GENE_NAME.value)
+                }
+                jsonGene.put(FeatureStringEnum.NAME.value, geneName)
+
+                gene = (Gene) convertJSONToFeature(jsonGene, sequence)
+                updateNewGsolFeatureAttributes(gene, sequence)
+
+                if (gene.fmin < 0 || gene.fmax < 0) {
+                    throw new AnnotationException("Feature cannot have negative coordinates")
+                }
+
+                transcript = transcriptService.getTranscripts(gene).first()
+                removeExonOverlapsAndAdjacenciesForFeature(gene)
+                if (!suppressHistory) {
+                    String name = nameService.generateUniqueName(transcript, geneName)
+                    transcript.name = name
+                }
+
+                // setting back the original name for the feature
+                if (useName && jsonFeature.has(FeatureStringEnum.NAME.value)) {
+                    transcript.name = jsonFeature.get(FeatureStringEnum.NAME.value)
+                }
+
+                gene.save(insert: true)
+                transcript.save(flush: true)
+
+                setOwner(gene, user);
+                setOwner(transcript, user);
+            }
+
+            removeExonOverlapsAndAdjacencies(transcript)
+            CDS cds = transcriptService.getCDS(transcript)
+            if (cds != null) {
+                featureRelationshipService.deleteChildrenForTypes(transcript, CDS.ontologyId)
+                if (cds.parentFeatureRelationships) featureRelationshipService.deleteChildrenForTypes(cds, StopCodonReadThrough.ontologyId)
+                cds.delete()
+            }
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+            returnFeature = transcript
+        } else {
+            if (!jsonFeature.containsKey(FeatureStringEnum.NAME.value) && jsonFeature.containsKey(FeatureStringEnum.CHILDREN.value)) {
+                JSONArray childArray = jsonFeature.getJSONArray(FeatureStringEnum.CHILDREN.value)
+                if (childArray?.size() == 1 && childArray.getJSONObject(0).containsKey(FeatureStringEnum.NAME.value)) {
+                    jsonFeature.put(FeatureStringEnum.NAME.value, childArray.getJSONObject(0).getString(FeatureStringEnum.NAME.value))
+                }
+            }
+            Feature feature = convertJSONToFeature(jsonFeature, sequence)
+            if (!suppressHistory) {
+                String name = nameService.generateUniqueName(feature, feature.name)
+                feature.name = name
+            }
+            updateNewGsolFeatureAttributes(feature, sequence)
+
+            // setting back the original name for feature
+            if (useName && jsonFeature.has(FeatureStringEnum.NAME.value)) {
+                feature.name = jsonFeature.get(FeatureStringEnum.NAME.value)
+            }
+
+            setOwner(feature, user);
+            feature.save(insert: true, flush: true)
+            if (jsonFeature.get(FeatureStringEnum.TYPE.value).name == Gene.alternateCvTerm ||
+                    jsonFeature.get(FeatureStringEnum.TYPE.value).name == Pseudogene.alternateCvTerm) {
+                Transcript transcript = transcriptService.getTranscripts(feature).iterator().next()
+                setOwner(transcript, user);
+                removeExonOverlapsAndAdjacencies(transcript)
+                CDS cds = transcriptService.getCDS(transcript)
+                if (cds != null) {
+                    featureRelationshipService.deleteChildrenForTypes(transcript, CDS.ontologyId)
+                    if (cds.parentFeatureRelationships) featureRelationshipService.deleteChildrenForTypes(cds, StopCodonReadThrough.ontologyId)
+                    cds.delete()
+                }
+                nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+                transcript.save(flush: true)
+                returnFeature = transcript
+            } else {
+                returnFeature = feature
+            }
+        }
+
+        return returnFeature
+    }
+
+    def addNonPrimaryDbxrefs(Feature feature, String dbString, String accessionString) {
+        DB db = DB.findByName(dbString)
+        if (!db) {
+            db = new DB(name: dbString).save()
+        }
+        DBXref dbxref = DBXref.findOrSaveByAccessionAndDb(accessionString, db)
+        dbxref.save(flush: true)
+        feature.addToFeatureDBXrefs(dbxref)
+        feature.save()
+    }
+
+    def addNonReservedProperties(Feature feature, String tagString, String valueString) {
+        FeatureProperty featureProperty = new FeatureProperty(
+                feature: feature,
+                value: valueString,
+                tag: tagString
+        ).save()
+        featurePropertyService.addProperty(feature, featureProperty)
+        feature.save()
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FeatureTypeService.groovy b/grails-app/services/org/bbop/apollo/FeatureTypeService.groovy
new file mode 100644
index 0000000..22c1155
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FeatureTypeService.groovy
@@ -0,0 +1,34 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+ at Transactional
+class FeatureTypeService {
+
+    def createFeatureTypeForFeature(Class clazz,String display) {
+
+        FeatureType featureType = new FeatureType(
+                name: clazz.cvTerm
+                ,display: display
+                , type: "sequence"
+                , ontologyId: clazz.ontologyId
+        ).save(insert: true, flush: true)
+        return featureType
+    }
+
+    def stubDefaultFeatureTypes(){
+        createFeatureTypeForFeature(Gene.class,Gene.cvTerm)
+        createFeatureTypeForFeature(Pseudogene.class,Pseudogene.cvTerm)
+        createFeatureTypeForFeature(Transcript.class,Transcript.cvTerm)
+        createFeatureTypeForFeature(MRNA.class,MRNA.alternateCvTerm)
+        createFeatureTypeForFeature(SnRNA.class,SnRNA.alternateCvTerm)
+        createFeatureTypeForFeature(SnoRNA.class,SnoRNA.alternateCvTerm)
+        createFeatureTypeForFeature(MiRNA.class,MiRNA.alternateCvTerm)
+        createFeatureTypeForFeature(TRNA.class,TRNA.alternateCvTerm)
+        createFeatureTypeForFeature(NcRNA.class,NcRNA.alternateCvTerm)
+        createFeatureTypeForFeature(RRNA.class,RRNA.alternateCvTerm)
+        createFeatureTypeForFeature(RepeatRegion.class,RepeatRegion.alternateCvTerm)
+        createFeatureTypeForFeature(TransposableElement.class,TransposableElement.alternateCvTerm)
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/FileService.groovy b/grails-app/services/org/bbop/apollo/FileService.groovy
new file mode 100644
index 0000000..c0ede51
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/FileService.groovy
@@ -0,0 +1,210 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.apache.commons.compress.archivers.ArchiveInputStream
+import org.apache.commons.compress.archivers.ArchiveStreamFactory
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
+import org.apache.commons.io.IOUtils
+import org.springframework.web.multipart.commons.CommonsMultipartFile
+
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
+import java.nio.file.FileSystemException
+
+ at Transactional
+class FileService {
+
+    /**
+     * Decompress an archive to a folder specified by directoryName in the given path
+     * @param archiveFile
+     * @param path
+     * @param directoryName
+     * @param tempDir
+     * @return
+     */
+    def decompress(File archiveFile, String path, String directoryName = null, boolean tempDir = false) {
+        // decompressing
+        if (archiveFile.name.contains(".zip")) {
+            decompressZipArchive(archiveFile, path, directoryName, tempDir)
+        }
+        else if (archiveFile.name.contains(".tar.gz") || archiveFile.name.contains(".tgz")) {
+            decompressTarArchive(archiveFile, path, directoryName, tempDir)
+        }
+        else {
+            throw new IOException("Cannot detect format (either *.zip, *.tar.gz or *.tgz) for file: ${archiveFile.name}")
+        }
+    }
+
+    /**
+     * Decompress a zip archive to a folder specified by directoryName in the given path
+     * @param zipFile
+     * @param path
+     * @param directoryName
+     * @param tempDir
+     * @return
+     */
+    def decompressZipArchive(File zipFile, String path, String directoryName = null, boolean tempDir = false) {
+        String archiveRootDirectoryName
+        boolean atArchiveRoot = true
+        String initialLocation = tempDir ? path + File.separator + "temp" : path
+        log.debug "initial location: ${initialLocation}"
+        final InputStream is = new FileInputStream(zipFile);
+        ArchiveInputStream ais = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.ZIP, is);
+        ZipArchiveEntry entry = null
+
+        while ((entry = (ZipArchiveEntry) ais.getNextEntry()) != null) {
+            if (atArchiveRoot) {
+                archiveRootDirectoryName = entry.getName()
+                atArchiveRoot = false
+            }
+
+            validateFileName(entry.getName(), archiveRootDirectoryName)
+            if (entry.isDirectory()) {
+                File dir = new File(initialLocation, entry.getName());
+                if (!dir.exists()) {
+                    dir.mkdirs();
+                }
+                continue;
+            }
+
+            File outputFile = new File(initialLocation, entry.getName());
+
+            if (outputFile.isDirectory()) {
+                continue;
+            }
+
+            if (outputFile.exists()) {
+                continue;
+            }
+
+            OutputStream os = new FileOutputStream(outputFile);
+            IOUtils.copy(ais, os);
+            os.close();
+        }
+
+        ais.close()
+        is.close()
+
+        if (tempDir) {
+            // move files from temp directory to folder supplied via directoryName
+            String unpackedArchiveLocation = initialLocation + File.separator + archiveRootDirectoryName
+            String finalLocation = path + File.separator + directoryName
+            File finalLocationFile = new File(finalLocation)
+            if (finalLocationFile.mkdir()) log.debug "${finalLocation} directory created"
+            try {
+                Files.move(new File(unpackedArchiveLocation).toPath(), finalLocationFile.toPath(), StandardCopyOption.ATOMIC_MOVE)
+                log.debug "files moved from ${unpackedArchiveLocation} to ${finalLocation}"
+            } catch (FileSystemException fse) {
+                log.error fse.message
+            }
+
+            // delete temp folder
+            new File(initialLocation).deleteDir()
+        }
+    }
+
+    /**
+     * Decompress a tar.gz archive to a folder specified by directoryName in the given path
+     * @param tarFile
+     * @param path
+     * @param directoryName
+     * @param tempDir
+     * @return
+     */
+    def decompressTarArchive(File tarFile, String path, String directoryName = null, boolean tempDir = false) {
+        boolean atArchiveRoot = true
+        String archiveRootDirectoryName
+        String initialLocation = tempDir ? path + File.separator + "temp" : path
+        log.debug "initial location: ${initialLocation}"
+        TarArchiveInputStream tais = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(tarFile)))
+        TarArchiveEntry entry = null
+
+        while ((entry = (TarArchiveEntry) tais.getNextEntry()) != null) {
+            if (atArchiveRoot) {
+                archiveRootDirectoryName = entry.getName()
+                atArchiveRoot = false
+            }
+
+            validateFileName(entry.getName(), archiveRootDirectoryName)
+            if (entry.isDirectory()) {
+                File dir = new File(initialLocation, entry.getName())
+                if (!dir.exists()) {
+                    dir.mkdirs()
+                }
+                continue;
+            }
+
+            File outputFile = new File(initialLocation, entry.getName())
+
+            if (outputFile.isDirectory()) {
+                continue;
+            }
+
+            if (outputFile.exists()) {
+                continue;
+            }
+
+            FileOutputStream fos = new FileOutputStream(outputFile);
+            IOUtils.copy(tais, fos);
+            fos.close();
+        }
+
+        if (tempDir) {
+            // move files from temp directory to folder supplied via directoryName
+            String unpackedArchiveLocation = initialLocation + File.separator + archiveRootDirectoryName
+            String finalLocation = path + File.separator + directoryName
+            File finalLocationFile = new File(finalLocation)
+            if (finalLocationFile.mkdir()) log.debug "${finalLocation} directory created"
+            try {
+                Files.move(new File(unpackedArchiveLocation).toPath(), finalLocationFile.toPath(), StandardCopyOption.ATOMIC_MOVE)
+                log.debug "files moved from ${unpackedArchiveLocation} to ${finalLocation}"
+            } catch (FileSystemException fse) {
+                log.error fse.message
+            }
+
+            // delete temp folder
+            new File(initialLocation).deleteDir()
+        }
+    }
+
+
+    def store(CommonsMultipartFile file, String path, String directoryName = null, boolean tempDir = false) {
+        File pathFile = new File(path)
+        if (!pathFile.exists()) {
+            pathFile.mkdirs()
+        }
+        String destinationFileName = directoryName ?
+                path + File.separator + directoryName + File.separator + file.getOriginalFilename() :
+                path + File.separator + file.getOriginalFilename()
+
+        File destinationFile = new File(destinationFileName)
+        try {
+            log.debug "transferring track file to ${destinationFileName}"
+            file.transferTo(destinationFile)
+        } catch (Exception e) {
+            log.error e.message
+        }
+    }
+
+    /**
+     * Validate that a given file falls within its intended output directory
+     * @param fileName
+     * @param intendedOutputDirectory
+     * @return
+     * @throws IOException
+     */
+    def validateFileName(String fileName, String intendedOutputDirectory) throws IOException {
+        File file = new File(fileName)
+        String canonicalPath = file.getCanonicalPath()
+        File intendedOutputDirectoryFile = new File(intendedOutputDirectory)
+        String canonicalIntendedOutputDirectoryPath = intendedOutputDirectoryFile.getCanonicalPath()
+        if (canonicalPath.startsWith(canonicalIntendedOutputDirectoryPath)) {
+            return canonicalPath
+        } else {
+            throw new IOException("File is outside extraction target directory.")
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/Gff3HandlerService.groovy b/grails-app/services/org/bbop/apollo/Gff3HandlerService.groovy
new file mode 100644
index 0000000..b685c57
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/Gff3HandlerService.groovy
@@ -0,0 +1,439 @@
+package org.bbop.apollo
+
+import org.apache.commons.lang.WordUtils
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.Strand
+import org.grails.plugins.metrics.groovy.Timed
+import org.springframework.format.datetime.DateFormatter
+import java.text.SimpleDateFormat
+
+
+
+public class Gff3HandlerService {
+
+    def sequenceService
+    def featureRelationshipService
+    def transcriptService
+    def exonService
+    def configWrapperService
+    def requestHandlingService 
+    def featureService
+    def overlapperService
+    def featurePropertyService
+
+    SimpleDateFormat gff3DateFormat = new SimpleDateFormat("YYYY-MM-dd")
+
+    static final def unusedStandardAttributes = ["Alias", "Target", "Gap", "Derives_from", "Ontology_term", "Is_circular"];
+    @Timed
+    public void writeFeaturesToText(String path, Collection<? extends Feature> features, String source, Boolean exportSequence = false, Collection<Sequence> sequences = null) throws IOException {
+        WriteObject writeObject = new WriteObject()
+
+        writeObject.mode = Mode.WRITE
+        writeObject.file = new File(path)
+        writeObject.format = Format.TEXT
+
+        // TODO: use specified metadata?
+        writeObject.attributesToExport.add(FeatureStringEnum.NAME.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.SYMBOL.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.SYNONYMS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.DESCRIPTION.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.STATUS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.DBXREFS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.OWNER.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.ATTRIBUTES.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.PUBMEDIDS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.GOIDS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.COMMENTS.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.DATE_CREATION.value);
+        writeObject.attributesToExport.add(FeatureStringEnum.DATE_LAST_MODIFIED.value);
+
+        if (!writeObject.file.canWrite()) {
+            throw new IOException("Cannot write GFF3 to: " + writeObject.file.getAbsolutePath());
+        }
+
+        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(writeObject.file, true)));
+        writeObject.out = out
+        out.println("##gff-version 3")
+        writeFeatures(writeObject, features, source)
+        if(exportSequence) {
+            writeFastaForReferenceSequences(writeObject, sequences)
+            writeFastaForSequenceAlterations(writeObject, features)
+        }
+        out.flush()
+        out.close()
+    }
+
+
+    @Timed
+    public void writeFeatures(WriteObject writeObject, Collection<? extends Feature> features, String source) throws IOException {
+        Map<Sequence, Collection<Feature>> featuresBySource = new HashMap<Sequence, Collection<Feature>>();
+        for (Feature feature : features) {
+            Sequence sourceFeature = feature.featureLocation.sequence
+            Collection<Feature> featureList = featuresBySource.get(sourceFeature);
+            if (!featureList) {
+                featureList = new ArrayList<Feature>();
+                featuresBySource.put(sourceFeature, featureList);
+            }
+            featureList.add(feature);
+        }
+        featuresBySource.sort{ it.key }
+        for (Map.Entry<Sequence, Collection<Feature>> entry : featuresBySource.entrySet()) {
+            writeGroupDirectives(writeObject, entry.getKey());
+            for (Feature feature : entry.getValue()) {
+                writeFeature(writeObject, feature, source);
+                writeFeatureGroupEnd(writeObject.out);
+            }
+        }
+    }
+
+    @Timed
+    public void writeFeatures(WriteObject writeObject, Iterator<? extends Feature> iterator, String source, boolean needDirectives) throws IOException {
+        while (iterator.hasNext()) {
+            Feature feature = iterator.next();
+            if (needDirectives) {
+                writeGroupDirectives(writeObject, feature.featureLocation.sequence)
+                needDirectives = false;
+            }
+            writeFeature(writeObject, feature, source);
+            writeFeatureGroupEnd(writeObject.out);
+        }
+    }
+
+    static private void writeGroupDirectives(WriteObject writeObject, Sequence sourceFeature) {
+        if (sourceFeature.featureLocations?.size() == 0) return;
+        writeObject.out.println(String.format("##sequence-region %s %d %d", sourceFeature.name, sourceFeature.start + 1, sourceFeature.end));
+    }
+
+    static private void writeFeatureGroupEnd(PrintWriter out) {
+        out.println("###");
+    }
+
+    static private void writeEmptyFastaDirective(PrintWriter out) {
+        out.println("##FASTA");
+    }
+
+    private void writeFeature(WriteObject writeObject, Feature feature, String source) {
+        for (GFF3Entry entry : convertToEntry(writeObject, feature, source)) {
+            writeObject.out.println(entry.toString());
+        }
+    }
+
+    public void writeFasta(WriteObject writeObject, Collection<? extends Feature> features) {
+        writeEmptyFastaDirective(writeObject.out);
+        for (Feature feature : features) {
+            writeFasta(writeObject.out, feature, false);
+        }
+    }
+
+    public void writeFasta(PrintWriter out, Feature feature) {
+        writeFasta(out, feature, true);
+    }
+
+    public void writeFasta(PrintWriter out, Feature feature, boolean writeFastaDirective) {
+        writeFasta(out, feature, writeFastaDirective, true);
+    }
+
+    public void writeFasta(PrintWriter out, Feature feature, boolean writeFastaDirective, boolean useLocation) {
+        int lineLength = 60;
+        if (writeFastaDirective) {
+            writeEmptyFastaDirective(out);
+        }
+        String residues = null;
+        if (useLocation) {
+            residues = sequenceService.getResidueFromFeatureLocation(feature.featureLocation)
+        } else {
+            residues = sequenceService.getResiduesFromFeature(feature)
+        }
+        if (residues != null) {
+            out.println(">" + feature.getUniqueName());
+            int idx = 0;
+            while (idx < residues.length()) {
+                out.println(residues.substring(idx, Math.min(idx + lineLength, residues.length())));
+                idx += lineLength;
+            }
+        }
+    }
+    
+    public void writeFastaForReferenceSequences(WriteObject writeObject, Collection<Sequence> sequences) {
+        for (Sequence sequence : sequences) {
+            writeFastaForReferenceSequence(writeObject, sequence)
+        }
+    }
+    
+    public void writeFastaForReferenceSequence(WriteObject writeObject, Sequence sequence) {
+        int lineLength = 60;
+        String residues = null
+        writeEmptyFastaDirective(writeObject.out);
+        residues = sequenceService.getRawResiduesFromSequence(sequence, 0, sequence.length)
+        if (residues != null) {
+            writeObject.out.println(">" + sequence.name);
+            int idx = 0;
+            while(idx < residues.length()) {
+                writeObject.out.println(residues.substring(idx, Math.min(idx + lineLength, residues.length())))
+                idx += lineLength
+            }
+        }
+    }
+    
+    public void writeFastaForSequenceAlterations(WriteObject writeObject, Collection<? extends Feature> features) {
+        for (Feature feature : features) {
+            if (feature instanceof SequenceAlteration) {
+                writeFastaForSequenceAlteration(writeObject, feature)
+            }
+        }
+    }
+    
+    public void writeFastaForSequenceAlteration(WriteObject writeObject, SequenceAlteration sequenceAlteration) {
+        int lineLength = 60;
+        String residues = null
+        residues = sequenceAlteration.getAlterationResidue()
+        if(residues != null) {
+            writeObject.out.println(">" + sequenceAlteration.name)
+            int idx = 0;
+            while(idx < residues.length()) {
+                writeObject.out.println(residues.substring(idx, Math.min(idx + lineLength, residues.length())))
+                idx += lineLength
+            }
+        }
+    }
+    
+    private Collection<GFF3Entry> convertToEntry(WriteObject writeObject, Feature feature, String source) {
+        List<GFF3Entry> gffEntries = new ArrayList<GFF3Entry>();
+        convertToEntry(writeObject, feature, source, gffEntries);
+        return gffEntries;
+    }
+
+    @Timed
+    private void convertToEntry(WriteObject writeObject, Feature feature, String source, Collection<GFF3Entry> gffEntries) {
+
+        //log.debug "converting feature to ${feature.name} entry of # of entries ${gffEntries.size()}"
+
+        String seqId = feature.featureLocation.sequence.name
+        String type = featureService.getCvTermFromFeature(feature);
+        int start = feature.getFmin() + 1;
+        int end = feature.getFmax().equals(feature.getFmin()) ? feature.getFmax() + 1 : feature.getFmax();
+        String score = ".";
+        String strand;
+        if (feature.getStrand() == Strand.POSITIVE.getValue()) {
+            strand = Strand.POSITIVE.getDisplay()
+        } else if (feature.getStrand() == Strand.NEGATIVE.getValue()) {
+            strand = Strand.NEGATIVE.getDisplay()
+        } else {
+            strand = "."
+        }
+        String phase = ".";
+        GFF3Entry entry = new GFF3Entry(seqId, source, type, start, end, score, strand, phase);
+        entry.setAttributes(extractAttributes(writeObject, feature));
+        gffEntries.add(entry);
+        if(featureService.typeHasChildren(feature)){
+            for (Feature child : featureRelationshipService.getChildren(feature)) {
+                if (child instanceof CDS) {
+                    convertToEntry(writeObject, (CDS) child, source, gffEntries);
+                } else {
+                    convertToEntry(writeObject, child, source, gffEntries);
+                }
+            }
+        }
+    }
+
+    @Timed
+    private void convertToEntry(WriteObject writeObject, CDS cds, String source, Collection<GFF3Entry> gffEntries) {
+        //log.debug "converting CDS to ${cds.name} entry of # of entries ${gffEntries.size()}"
+
+        String seqId = cds.featureLocation.sequence.name
+        String type = cds.cvTerm
+        String score = ".";
+        String strand;
+        if (cds.getStrand() == 1) {
+            strand = "+";
+        } else if (cds.getStrand() == -1) {
+            strand = "-";
+        } else {
+            strand = ".";
+        }
+        Transcript transcript = transcriptService.getParentTranscriptForFeature(cds)
+
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        int length = 0;
+        for (Exon exon : exons) {
+            if (!overlapperService.overlaps(exon, cds)) {
+                continue;
+            }
+            int fmin = exon.getFmin() < cds.getFmin() ? cds.getFmin() : exon.getFmin();
+            int fmax = exon.getFmax() > cds.getFmax() ? cds.getFmax() : exon.getFmax();
+            String phase;
+            if (length % 3 == 0) {
+                phase = "0";
+            } else if (length % 3 == 1) {
+                phase = "2";
+            } else {
+                phase = "1";
+            }
+            length += fmax - fmin;
+            GFF3Entry entry = new GFF3Entry(seqId, source, type, fmin + 1, fmax, score, strand, phase);
+            entry.setAttributes(extractAttributes(writeObject, cds));
+            gffEntries.add(entry);
+        }
+        for (Feature child : featureRelationshipService.getChildren(cds)) {
+            convertToEntry(writeObject, child, source, gffEntries);
+        }
+    }
+
+    @Timed
+    private Map<String, String> extractAttributes(WriteObject writeObject, Feature feature) {
+        Map<String, String> attributes = new HashMap<String, String>();
+        attributes.put(FeatureStringEnum.EXPORT_ID.value, encodeString(feature.getUniqueName()));
+        if (feature.getName() != null && !isBlank(feature.getName()) && writeObject.attributesToExport.contains(FeatureStringEnum.NAME.value)) {
+            attributes.put(FeatureStringEnum.EXPORT_NAME.value, encodeString(feature.getName()));
+        }
+        if (!(feature.class.name in requestHandlingService.viewableAnnotationList+requestHandlingService.viewableAlterations)) {
+            def parent= featureRelationshipService.getParentForFeature(feature)
+            attributes.put(FeatureStringEnum.EXPORT_PARENT.value, encodeString(parent.uniqueName));
+        }
+        if(configWrapperService.exportSubFeatureAttrs() || feature.class.name in requestHandlingService.viewableAnnotationList+requestHandlingService.viewableAnnotationTranscriptList+requestHandlingService.viewableAlterations) {
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.SYNONYMS.value)) {
+                Iterator<Synonym> synonymIter = feature.synonyms.iterator();
+                if (synonymIter.hasNext()) {
+                    StringBuilder synonyms = new StringBuilder();
+                    synonyms.append(synonymIter.next().getName());
+                    while (synonymIter.hasNext()) {
+                        synonyms.append(",");
+                        synonyms.append(encodeString(synonymIter.next().getName()));
+                    }
+                    attributes.put(FeatureStringEnum.EXPORT_ALIAS.value, synonyms.toString());
+                }
+            }
+
+
+            //TODO: Target
+            //TODO: Gap
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.COMMENTS.value)) {
+                Iterator<Comment> commentIter = featurePropertyService.getComments(feature).iterator()
+                if (commentIter.hasNext()) {
+                    StringBuilder comments = new StringBuilder();
+                    comments.append(encodeString(commentIter.next().value));
+                    while (commentIter.hasNext()) {
+                        comments.append(",");
+                        comments.append(encodeString(commentIter.next().value));
+                    }
+                    attributes.put(FeatureStringEnum.EXPORT_NOTE.value, comments.toString());
+                }
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.DBXREFS.value)) {
+                Iterator<DBXref> dbxrefIter = feature.featureDBXrefs.iterator();
+                if (dbxrefIter.hasNext()) {
+                    StringBuilder dbxrefs = new StringBuilder();
+                    DBXref dbxref = dbxrefIter.next();
+                    dbxrefs.append(encodeString(dbxref.getDb().getName() + ":" + dbxref.getAccession()));
+                    while (dbxrefIter.hasNext()) {
+                        dbxrefs.append(",");
+                        dbxref = dbxrefIter.next();
+                        dbxrefs.append(encodeString(dbxref.getDb().getName()) + ":" + encodeString(dbxref.getAccession()));
+                    }
+                    attributes.put(FeatureStringEnum.EXPORT_DBXREF.value, dbxrefs.toString());
+                }
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.DESCRIPTION.value) && feature.getDescription() != null && !isBlank(feature.getDescription())) {
+
+                attributes.put(FeatureStringEnum.DESCRIPTION.value, encodeString(feature.getDescription()));
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.STATUS.value) && feature.getStatus() != null) {
+                attributes.put(FeatureStringEnum.STATUS.value, encodeString(feature.getStatus().value));
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.SYMBOL.value) && feature.getSymbol() != null && !isBlank(feature.getSymbol())) {
+                attributes.put(FeatureStringEnum.SYMBOL.value, encodeString(feature.getSymbol()));
+            }
+            //TODO: Ontology_term
+            //TODO: Is_circular
+            Iterator<FeatureProperty> propertyIter = feature.featureProperties.iterator();
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.ATTRIBUTES.value)) {
+                if (propertyIter.hasNext()) {
+                    Map<String, StringBuilder> properties = new HashMap<String, StringBuilder>();
+                    while (propertyIter.hasNext()) {
+                        FeatureProperty prop = propertyIter.next();
+                        if (prop instanceof Comment) {
+                            // ignoring 'comment' as they are already processed earlier
+                            continue
+                        }
+                        StringBuilder props = properties.get(prop.getTag());
+                        if (props == null) {
+                            if (prop.getTag() == null) {
+                                // tag is null for generic properties
+                                continue
+                            }
+                            props = new StringBuilder();
+                            properties.put(prop.getTag(), props);
+                        } else {
+                            props.append(",");
+                        }
+                        props.append(encodeString(prop.getValue()));
+                    }
+                    for (Map.Entry<String, StringBuilder> iter : properties.entrySet()) {
+                        if (iter.getKey() in unusedStandardAttributes) {
+                            attributes.put(encodeString(WordUtils.capitalizeFully(iter.getKey())), iter.getValue().toString());
+                        }
+                        else {
+                            attributes.put(encodeString(WordUtils.uncapitalize(iter.getKey())), iter.getValue().toString());
+                        }
+                    }
+                }
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.OWNER.value) && feature.getOwner()) {
+                attributes.put(FeatureStringEnum.OWNER.value.toLowerCase(), encodeString(feature.getOwner().username));
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.DATE_CREATION.value)) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(feature.dateCreated);
+                attributes.put(FeatureStringEnum.DATE_CREATION.value, encodeString(formatDate(calendar.time)));
+            }
+            if (writeObject.attributesToExport.contains(FeatureStringEnum.DATE_LAST_MODIFIED.value)) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(feature.lastUpdated);
+                attributes.put(FeatureStringEnum.DATE_LAST_MODIFIED.value, encodeString(formatDate(calendar.time)));
+            }
+
+
+            if(feature.class.name in [Insertion.class.name,Substitution.class.name]) {
+                attributes.put(FeatureStringEnum.RESIDUES.value, feature.alterationResidue)
+            }
+        }
+        return attributes;
+    }
+
+    String formatDate(Date date){
+        return gff3DateFormat.format(date)
+    }
+
+    static private String encodeString(String str) {
+        return str ? str.replaceAll(",", "%2C").replaceAll("=", "%3D").replaceAll(";", "%3B").replaceAll("\t", "%09") : ""
+    }
+
+
+    public enum Mode {
+        READ,
+        WRITE
+    }
+
+    public enum Format {
+        TEXT,
+        GZIP
+    }
+    
+    private boolean isBlank(String attributeValue) {
+        if (attributeValue == "") {
+            return true
+        }
+        else {
+            return false
+        }
+    }
+
+    private class WriteObject {
+        File file;
+        PrintWriter out;
+        Mode mode;
+        Set<String> attributesToExport = new HashSet<>();
+        Format format;
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/JbrowseService.groovy b/grails-app/services/org/bbop/apollo/JbrowseService.groovy
new file mode 100644
index 0000000..cb2620f
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/JbrowseService.groovy
@@ -0,0 +1,41 @@
+package org.bbop.apollo
+
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+
+ at Transactional
+class JbrowseService {
+
+    @NotTransactional
+    Boolean hasOverlappingDirectory(String path1, String path2) {
+        String[] paths1 = path1.split("/")
+        String[] paths2 = path2.split("/")
+
+        log.debug "Comparing ${paths1[paths1.length-1]} to ${paths2[0]}"
+        if(paths1[paths1.length-1]==paths2[0]){
+            return true
+        }
+        return false
+    }
+
+    @NotTransactional
+    String fixOverlappingPath(String path1, String path2) {
+        String[] paths1 = path1.split("/")
+        String[] paths2 = path2.split("/")
+        List<String> finalPaths = new ArrayList<>()
+        if(paths1[paths1.length-1]==paths2[0]){
+            // add all but the last one
+            for(p in paths1){
+                if(p != paths2[0]){
+                    finalPaths.add(p)
+                }
+            }
+        }
+        else{
+            finalPaths.addAll(paths2)
+        }
+        finalPaths.addAll(paths2)
+        return finalPaths.join("/")
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/NameService.groovy b/grails-app/services/org/bbop/apollo/NameService.groovy
new file mode 100644
index 0000000..f8ea513
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/NameService.groovy
@@ -0,0 +1,156 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+
+ at Transactional(readOnly = true)
+class NameService {
+
+    def transcriptService
+    def letterPaddingStrategy = new LetterPaddingStrategy()
+    def leftPaddingStrategy = new LeftPaddingStrategy()
+
+
+    // TODO: replace with more reasonable naming schemas
+    String generateUniqueName() {
+        UUID.randomUUID().toString()
+    }
+
+    String generateUniqueName(Feature thisFeature,String principalName = null ) {
+        Organism organism = thisFeature.featureLocation.sequence.organism
+        if(thisFeature.name) {
+            if (thisFeature instanceof Transcript) {
+                log.debug "instance of transcript"
+                if(!principalName){
+                    Gene gene = transcriptService.getGene((Transcript) thisFeature)
+                    log.debug "transcript has gene ${gene}"
+                    if(!gene){
+                        gene = transcriptService.getPseudogene((Transcript) thisFeature)
+                        log.debug "transcript has pseudogene ${gene}"
+                    }
+                    principalName = gene.name
+                }
+                return makeUniqueTranscriptName(organism,principalName.trim()+"-")
+            } else
+            if (thisFeature instanceof Gene) {
+                log.debug "instance of Gene"
+                if(!principalName){
+                    principalName = ((Gene) thisFeature).name
+                }
+                if(Gene.countByName(principalName.trim())==0){
+                    return principalName
+                }
+                  return makeUniqueGeneName(organism,principalName.trim())
+            }
+            if (thisFeature instanceof Exon || thisFeature instanceof NonCanonicalFivePrimeSpliceSite || thisFeature instanceof NonCanonicalThreePrimeSpliceSite || thisFeature instanceof CDS) {
+                log.debug "instance of Exon"
+                return generateUniqueName()
+            }
+            else{
+                if(!principalName){
+                    principalName = thisFeature.name
+                }
+                return makeUniqueFeatureName(organism,principalName.trim(),new LetterPaddingStrategy())
+            }
+        }
+        else{
+            generateUniqueName()
+        }
+    }
+
+
+    boolean isUniqueGene(Organism organism,String name){
+//        if(Gene.countByName(name)==0) {
+//            return true
+//        }
+//        List results = (Gene.executeQuery("select count(f) from Gene f join f.featureLocations fl join fl.sequence s where s.organism = :org and f.name = :name ",[org:organism,name:name]))
+        Integer numberResults = Gene.findAllByName(name).findAll(){
+            it.featureLocation.sequence.organism == organism
+        }.size()
+        return 0 == numberResults
+    }
+
+    boolean isUnique(Organism organism,String name){
+//        if(Feature.countByName(name)==0) {
+//            return true
+//        }
+//        List results = (Feature.executeQuery("select count(f) from Feature f join f.featureLocations fl join fl.sequence s where s.organism = :org and f.name = :name ",[org:organism,name:name]))
+        Integer numberResults = Feature.findAllByName(name).findAll(){
+            it.featureLocation.sequence.organism == organism
+        }.size()
+        return 0 == numberResults
+    }
+
+    String makeUniqueTranscriptName(Organism organism,String principalName){
+        String name
+
+        name = principalName + leftPaddingStrategy.pad(0)
+        if(Transcript.countByName(name)==0){
+            return name
+        }
+
+//        List results = (Feature.executeQuery("select f.name from Transcript f join f.featureLocations fl join fl.sequence s where s.organism = :org and f.name like :name ",[org:organism,name:principalName+'%']))
+        // See https://github.com/GMOD/Apollo/issues/1276
+        // only does sort over found results
+        List<String> results= Feature.findAllByNameLike(principalName+"%").findAll(){
+            it.featureLocation.sequence.organism == organism
+        }.name
+
+        name = principalName + leftPaddingStrategy.pad(results.size())
+        int count = results.size()
+        while(results.contains(name)){
+            name = principalName + leftPaddingStrategy.pad(count)
+            ++count
+        }
+        return name
+    }
+
+    String makeUniqueGeneName(Organism organism,String principalName,boolean useOriginal=false){
+
+        if(useOriginal && isUniqueGene(organism,principalName)){
+            return principalName
+        }
+
+        if(isUniqueGene(organism,principalName)){
+            return principalName
+        }
+
+        String name = principalName + letterPaddingStrategy.pad(0)
+
+//        List results = (Gene.executeQuery("select f.name from Gene f join f.featureLocations fl join fl.sequence s where s.organism = :org and f.name like :name ",[org:organism,name:principalName+'%']))
+        List<String> results= Gene.findAllByNameLike(principalName+"%").findAll(){
+            it.featureLocation.sequence.organism == organism
+        }.name
+        int count = results.size()
+        while(results.contains(name)){
+            name = principalName + letterPaddingStrategy.pad(count)
+            ++count
+        }
+        return name
+
+//        name = principalName + letterPaddingStrategy.pad(i++)
+//        while(!isUnique(organism,name)){
+//            name = principalName + letterPaddingStrategy.pad(i++)
+//        }
+//        return name
+    }
+
+    String makeUniqueFeatureName(Organism organism,String principalName,PaddingStrategy paddingStrategy,boolean useOriginal=false){
+        String name
+        int i = 0
+
+        if(useOriginal && isUnique(organism,principalName)){
+            return principalName
+        }
+
+        if(isUnique(organism,principalName)){
+            return principalName
+        }
+
+        name = principalName + paddingStrategy.pad(i++)
+        while(!isUnique(organism,name)){
+            name = principalName + paddingStrategy.pad(i++)
+        }
+        return name
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/NonCanonicalSplitSiteService.groovy b/grails-app/services/org/bbop/apollo/NonCanonicalSplitSiteService.groovy
new file mode 100644
index 0000000..b337b39
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/NonCanonicalSplitSiteService.groovy
@@ -0,0 +1,281 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.Strand
+import org.grails.plugins.metrics.groovy.Timed
+
+//@GrailsCompileStatic
+ at Transactional
+class NonCanonicalSplitSiteService {
+
+    def featureRelationshipService
+    def exonService
+    def transcriptService
+    def featureService
+    def sequenceService
+
+    /** Delete an non canonical 5' splice site.  Deletes both the transcript -> non canonical 5' splice site and
+     *  non canonical 5' splice site -> transcript relationships.
+     *
+     * @param nonCanonicalFivePrimeSpliceSite - NonCanonicalFivePrimeSpliceSite to be deleted
+     */
+    public void deleteNonCanonicalFivePrimeSpliceSite(Transcript transcript, NonCanonicalFivePrimeSpliceSite nonCanonicalFivePrimeSpliceSite) {
+
+        featureRelationshipService.deleteChildrenForTypes(transcript,NonCanonicalFivePrimeSpliceSite.ontologyId)
+        featureRelationshipService.deleteParentForTypes(nonCanonicalFivePrimeSpliceSite,Transcript.ontologyId)
+        nonCanonicalFivePrimeSpliceSite.delete(flush: true)
+    }
+
+    public void deleteNonCanonicalThreePrimeSpliceSite(Transcript transcript, NonCanonicalThreePrimeSpliceSite nonCanonicalThreePrimeSpliceSite) {
+        featureRelationshipService.deleteChildrenForTypes(transcript,NonCanonicalThreePrimeSpliceSite.ontologyId)
+        featureRelationshipService.deleteParentForTypes(nonCanonicalThreePrimeSpliceSite,Transcript.ontologyId)
+        nonCanonicalThreePrimeSpliceSite.delete(flush: true )
+    }
+
+    /** Delete all non canonical 5' splice site.  Deletes all transcript -> non canonical 5' splice sites and
+     *  non canonical 5' splice sites -> transcript relationships.
+     *
+     */
+    public void deleteAllNonCanonicalFivePrimeSpliceSites(Transcript transcript) {
+        for (NonCanonicalFivePrimeSpliceSite spliceSite : getNonCanonicalFivePrimeSpliceSites(transcript)) {
+            deleteNonCanonicalFivePrimeSpliceSite(transcript,spliceSite);
+        }
+    }
+
+    /** Retrieve all the non canonical 5' splice sites associated with this transcript.  Uses the configuration to determine
+     *  which children are non canonical 5' splice sites.  Non canonical 5' splice site objects are generated on the fly.
+     *  The collection will be empty if there are no non canonical 5' splice sites associated with the transcript.
+     *
+     * @return Collection of non canonical 5' splice sites associated with this transcript
+     */
+    public Collection<NonCanonicalFivePrimeSpliceSite> getNonCanonicalFivePrimeSpliceSites(Transcript transcript) {
+        return (Collection<NonCanonicalFivePrimeSpliceSite>) featureRelationshipService.getChildrenForFeatureAndTypes(transcript,NonCanonicalFivePrimeSpliceSite.ontologyId)
+    }
+
+    /** Retrieve all the non canonical 3' splice sites associated with this transcript.  Uses the configuration to determine
+     *  which children are non canonical 3' splice sites.  Non canonical 3' splice site objects are generated on the fly.
+     *  The collection will be empty if there are no non canonical 3' splice sites associated with the transcript.
+     *
+     * @return Collection of non canonical 3' splice sites associated with this transcript
+     */
+    public Collection<NonCanonicalThreePrimeSpliceSite> getNonCanonicalThreePrimeSpliceSites(Transcript transcript) {
+//        return (Collection<NonCanonicalThreePrimeSpliceSite>) featureRelationshipService.getChildrenForFeatureAndTypes(transcript,FeatureStringEnum.NONCANONICALTHREEPRIMESPLICESITE)
+        return (Collection<NonCanonicalThreePrimeSpliceSite>) featureRelationshipService.getChildrenForFeatureAndTypes(transcript,NonCanonicalThreePrimeSpliceSite.ontologyId)
+    }
+
+    /** Delete all non canonical 3' splice site.  Deletes all transcript -> non canonical 3' splice sites and
+     *  non canonical 3' splice sites -> transcript relationships.
+     *
+     */
+    public void deleteAllNonCanonicalThreePrimeSpliceSites(Transcript transcript) {
+        for (NonCanonicalThreePrimeSpliceSite spliceSite : getNonCanonicalThreePrimeSpliceSites(transcript)) {
+//            featureRelationshipService.deleteRelationships(transcript,NonCanonicalThreePrimeSpliceSite.ontologyId,Transcript.ontologyId)
+            deleteNonCanonicalThreePrimeSpliceSite(transcript,spliceSite)
+        }
+    }
+
+    @Timed
+    public void findNonCanonicalAcceptorDonorSpliceSites(Transcript transcript) {
+
+        transcript.attach()
+
+        deleteAllNonCanonicalFivePrimeSpliceSites(transcript)
+        deleteAllNonCanonicalThreePrimeSpliceSites(transcript)
+
+        List<Exon> exons = transcriptService.getSortedExons(transcript,true)
+        int fmin=transcript.getFeatureLocation().fmin
+        int fmax=transcript.getFeatureLocation().fmax
+        Sequence sequence=transcript.getFeatureLocation().sequence
+        Strand strand=transcript.getFeatureLocation().strand==-1?Strand.NEGATIVE:Strand.POSITIVE
+
+        String residues = sequenceService.getGenomicResiduesFromSequenceWithAlterations(sequence,fmin,fmax,strand);
+        if(transcript.getStrand()==-1)residues=residues.reverse()
+
+        List<SequenceAlteration> sequenceAlterationList = new ArrayList<>()
+        sequenceAlterationList.addAll(featureService.getAllSequenceAlterationsForFeature(transcript))
+
+        for (Exon exon : exons) {
+            int fivePrimeSpliceSitePosition = -1;
+            int threePrimeSpliceSitePosition = -1;
+            boolean validFivePrimeSplice = false;
+            boolean validThreePrimeSplice = false;
+            for (String donor : SequenceTranslationHandler.getSpliceDonorSites()){
+                for (String acceptor : SequenceTranslationHandler.getSpliceAcceptorSites()){
+                    int local11=exon.fmin-donor.length()-transcript.fmin
+                    int local22=exon.fmin-transcript.fmin
+                    int local33=exon.fmax-transcript.fmin
+                    int local44=exon.fmax+donor.length()-transcript.fmin
+
+                    int local1=featureService.convertSourceToModifiedLocalCoordinate(transcript,local11,sequenceAlterationList)
+                    int local2=featureService.convertSourceToModifiedLocalCoordinate(transcript,local22,sequenceAlterationList)
+                    int local3=featureService.convertSourceToModifiedLocalCoordinate(transcript,local33,sequenceAlterationList)
+                    int local4=featureService.convertSourceToModifiedLocalCoordinate(transcript,local44,sequenceAlterationList)
+
+
+                    if (exon.featureLocation.getStrand() == -1) {
+                        int tmp1=local1
+                        int tmp2=local2
+                        local1=local3
+                        local2=local4
+                        local3=tmp1
+                        local4=tmp2
+                    }
+                    if(local1>=0&&local2 < residues.length()) {
+                        String acceptorSpliceSiteSequence = residues.substring(local1,local2)
+                        acceptorSpliceSiteSequence=transcript.getStrand()==-1?acceptorSpliceSiteSequence.reverse():acceptorSpliceSiteSequence
+                        log.debug "acceptor ${local1} ${local2} ${acceptorSpliceSiteSequence} ${acceptor}"
+                        if(acceptorSpliceSiteSequence==acceptor)
+                            validThreePrimeSplice=true
+                        else
+                            threePrimeSpliceSitePosition = exon.getStrand() == -1 ? local1 : local2;
+                    }
+
+                    if(local3>=0&&local4<residues.length()) {
+                        String donorSpliceSiteSequence = residues.substring(local3,local4)
+                        donorSpliceSiteSequence=transcript.getStrand()==-1?donorSpliceSiteSequence.reverse():donorSpliceSiteSequence
+                        log.debug "donor ${local3} ${local4} ${donorSpliceSiteSequence} ${donor}"
+                        if(donorSpliceSiteSequence==donor)
+                            validFivePrimeSplice=true
+                        else
+                            fivePrimeSpliceSitePosition = exon.getStrand() == -1 ? local3 : local4;
+                    }
+                }
+            }
+            if (!validFivePrimeSplice && fivePrimeSpliceSitePosition != -1) {
+                def loc=fivePrimeSpliceSitePosition+transcript.fmin
+                log.debug "adding a noncanonical five prime splice site at ${fivePrimeSpliceSitePosition} ${loc}"
+                addNonCanonicalFivePrimeSpliceSite(transcript,createNonCanonicalFivePrimeSpliceSite(transcript, loc));
+            }
+            if (!validThreePrimeSplice && threePrimeSpliceSitePosition != -1) {
+                def loc=threePrimeSpliceSitePosition+transcript.fmin
+                log.debug "adding a noncanonical three prime splice site at ${threePrimeSpliceSitePosition} ${loc}"
+                addNonCanonicalThreePrimeSpliceSite(transcript,createNonCanonicalThreePrimeSpliceSite(transcript, loc));
+            }
+        }
+
+        for (NonCanonicalFivePrimeSpliceSite spliceSite : getNonCanonicalFivePrimeSpliceSites(transcript)) {
+            if (spliceSite.getDateCreated() == null) {
+                spliceSite.setDateCreated(new Date());
+            }
+            spliceSite.setLastUpdated(new Date());
+        }
+        for (NonCanonicalThreePrimeSpliceSite spliceSite : getNonCanonicalThreePrimeSpliceSites(transcript)) {
+            if (spliceSite.getDateCreated() == null) {
+                spliceSite.setDateCreated(new Date());
+            }
+            spliceSite.setLastUpdated(new Date());
+        }
+    }
+
+    /** Add a non canonical 5' splice site.  Sets the splice site's transcript to this transcript object.
+     *
+     * @param nonCanonicalFivePrimeSpliceSite - Non canonical 5' splice site to be added
+     */
+    public void addNonCanonicalFivePrimeSpliceSite(Transcript transcript,NonCanonicalFivePrimeSpliceSite nonCanonicalFivePrimeSpliceSite) {
+//        CVTerm partOfCvterm = cvTermService.partOf
+
+        // add non canonical 5' splice site
+        FeatureRelationship fr = new FeatureRelationship(
+//                type: cvTermService.partOf
+                parentFeature: transcript
+                , childFeature: nonCanonicalFivePrimeSpliceSite
+                ,rank:0 // TODO: Do we need to rank the order of any other transcripts?
+        ).save();
+        transcript.addToParentFeatureRelationships(fr);
+        nonCanonicalFivePrimeSpliceSite.addToChildFeatureRelationships(fr);
+    }
+
+    /** Add a non canonical 3' splice site.  Sets the splice site's transcript to this transcript object.
+     *
+     * @param nonCanonicalThreePrimeSpliceSite - Non canonical 3' splice site to be added
+     */
+    public void addNonCanonicalThreePrimeSpliceSite(Transcript transcript,NonCanonicalThreePrimeSpliceSite nonCanonicalThreePrimeSpliceSite) {
+
+        // add non canonical 3' splice site
+        FeatureRelationship fr = new FeatureRelationship(
+//                type: cvTermService.partOf
+                parentFeature: transcript
+                , childFeature: nonCanonicalThreePrimeSpliceSite
+                ,rank:0 // TODO: Do we need to rank the order of any other transcripts?
+        ).save();
+        transcript.addToParentFeatureRelationships(fr);
+        nonCanonicalThreePrimeSpliceSite.addToChildFeatureRelationships(fr);
+    }
+
+    private NonCanonicalFivePrimeSpliceSite createNonCanonicalFivePrimeSpliceSite(Transcript transcript, int position) {
+        String uniqueName = transcript.getUniqueName() + "-non_canonical_five_prime_splice_site-" + position;
+        NonCanonicalFivePrimeSpliceSite spliceSite = new NonCanonicalFivePrimeSpliceSite(
+                uniqueName: uniqueName
+                ,isAnalysis: transcript.isAnalysis
+                ,isObsolete: transcript.isObsolete
+                ,name: uniqueName
+                ).save()
+        spliceSite.addToFeatureLocations(new FeatureLocation(
+                strand: transcript.strand
+                ,sequence: transcript.featureLocation.sequence
+                ,fmin: position
+                ,fmax: position
+                ,feature: spliceSite
+        ).save());
+        return spliceSite;
+    }
+
+
+    private NonCanonicalThreePrimeSpliceSite createNonCanonicalThreePrimeSpliceSite(Transcript transcript, int position) {
+        String uniqueName = transcript.getUniqueName() + "-non_canonical_three_prime_splice_site-" + position;
+        NonCanonicalThreePrimeSpliceSite spliceSite = new NonCanonicalThreePrimeSpliceSite(
+                uniqueName: uniqueName
+                ,name: uniqueName
+                ,isAnalysis: transcript.isAnalysis
+                ,isObsolete: transcript.isObsolete
+//                ,timeAccessioned: new Date()
+        ).save()
+        spliceSite.addToFeatureLocations(new FeatureLocation(
+                strand: transcript.strand
+                ,sequence: transcript.featureLocation.sequence
+                ,fmin: position
+                ,fmax: position
+                ,feature: spliceSite
+        ).save());
+//        spliceSite.setFeatureLocation(new FeatureLocation());
+//        spliceSite.featureLocation.setStrand(transcript.getStrand());
+//        spliceSite.getFeatureLocation().setSourceFeature(transcript.getFeatureLocation().getSourceFeature());
+//        spliceSite.featureLocation.setFmin(position);
+//        spliceSite.featureLocation.setFmax(position);
+//        spliceSite.setLastUpdated(new Date());
+        return spliceSite;
+    }
+
+    private static FlankingRegion createFlankingRegion(Sequence sequence, int fmin, int fmax,Strand strand) {
+//        FlankingRegion flankingRegion = new FlankingRegion();
+//        flankingRegion.setIsAnalysis(false)
+//        flankingRegion.setIsObsolete(false)
+//        flankingRegion.setName(nameService.generateUniqueName())
+//        flankingRegion.setUniqueName(flankingRegion.name)
+//        flankingRegion.save()
+
+//        flankingRegion.addToFeatureLocations(new FeatureLocation(
+//                strand: feature.strand
+//                ,sequence: feature.featureLocation.sequence
+//                ,fmin: fmin
+//                ,fmax: fmax
+//                ,feature: flankingRegion
+//        ).save());
+
+//        flankingRegion.add(new FeatureLocation());
+//        flankingRegion.getFeatureLocation().setSourceFeature(feature.getFeatureLocation().getSourceFeature());
+//        flankingRegion.featureLocation.setStrand(feature.getStrand());
+//        flankingRegion.featureLocation.setFmin(fmin);
+//        flankingRegion.featureLocation.setFmax(fmax);
+        FlankingRegion flankingRegion = new FlankingRegion(
+                sequence: sequence
+                ,fmin: fmin
+                ,fmax: fmax
+                ,strand: strand
+        )
+
+
+        return flankingRegion;
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/OrganismService.groovy b/grails-app/services/org/bbop/apollo/OrganismService.groovy
new file mode 100644
index 0000000..8cd8e2d
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/OrganismService.groovy
@@ -0,0 +1,86 @@
+package org.bbop.apollo
+
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.TranslationTable
+
+import java.nio.file.FileSystemException
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipInputStream
+
+ at Transactional
+class OrganismService {
+
+    def featureService
+    def configWrapperService
+
+    int TRANSACTION_SIZE = 30
+
+    @NotTransactional
+    deleteAllFeaturesForOrganism(Organism organism) {
+
+        def featurePairs = Feature.executeQuery("select f.id,f.uniqueName from Feature f join f.featureLocations fl join fl.sequence s join s.organism o where o=:organism", [organism: organism])
+        // maximum transaction size  30
+        log.debug "feature sublists created ${featurePairs.size()}"
+        def featureSubLists = featurePairs.collate(TRANSACTION_SIZE)
+        if (!featureSubLists) {
+            log.warn("Nothing to delete for ${organism?.commonName}")
+            return
+        }
+        log.debug "sublists size ${featureSubLists.size()}"
+        int count = 0
+        long startTime = System.currentTimeMillis()
+        long endTime
+        double totalTime
+        featureSubLists.each { featureList ->
+            if (featureList) {
+                def ids = featureList.collect() {
+                    it[0]
+                }
+                log.info"ids ${ids.size()}"
+                def uniqueNames = featureList.collect() {
+                    it[1]
+                }
+                log.debug "uniqueNames ${uniqueNames.size()}"
+                Feature.withNewTransaction{
+                    def features = Feature.findAllByIdInList(ids)
+                    features.each { f ->
+                        f.delete()
+                    }
+                    def featureEvents = FeatureEvent.findAllByUniqueNameInList(uniqueNames)
+                    featureEvents.each { fe ->
+                        fe.delete()
+                    }
+                    organism.save(flush: true)
+                    count += featureList.size()
+                    log.info "${count} / ${featurePairs.size()}  =  ${100 * count / featurePairs.size()}% "
+                }
+                log.info "deleted ${featurePairs.size()}"
+            }
+            endTime = System.currentTimeMillis()
+            totalTime = (endTime - startTime) / 1000.0f
+            startTime = System.currentTimeMillis()
+            double rate = featureList.size() / totalTime
+            log.info "Deleted ${rate} features / sec"
+        }
+        return featurePairs.size()
+    }
+
+
+    TranslationTable getTranslationTable(Organism organism) {
+        if(organism?.nonDefaultTranslationTable){
+            log.debug "overriding default translation table for ${organism.commonName} with ${organism.nonDefaultTranslationTable}"
+            return SequenceTranslationHandler.getTranslationTableForGeneticCode(organism.nonDefaultTranslationTable)
+        }
+        // just use the default
+        else{
+            log.debug "using the default translation table"
+            return  configWrapperService.getTranslationTable()
+        }
+
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/OverlapperService.groovy b/grails-app/services/org/bbop/apollo/OverlapperService.groovy
new file mode 100644
index 0000000..f49586c
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/OverlapperService.groovy
@@ -0,0 +1,186 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.Overlapper
+import org.bbop.apollo.sequence.Strand
+
+ at Transactional(readOnly = true)
+class OverlapperService implements Overlapper{
+
+
+    def transcriptService
+    def exonService 
+    def configWrapperService 
+
+    @Override
+    boolean overlaps(Transcript transcript, Gene gene) {
+        return overlapsOrf(transcript,gene)
+    }
+
+    @Override
+    boolean overlaps(Transcript transcript1, Transcript transcript2) {
+        return overlapsOrf(transcript1,transcript2)
+    }
+
+
+    boolean overlapsOrf(Transcript transcript, Gene gene) {
+        long start = System.currentTimeMillis();
+        for (Transcript geneTranscript : transcriptService.getTranscripts(gene)) {
+            if (transcript.uniqueName == geneTranscript.uniqueName) {
+                // if transcript and geneTranscript are the same then don't test for overlap
+                // to avoid false positive
+                continue
+            }
+            if (overlapsOrf(transcript, geneTranscript)) {
+                log.debug "@Duration for cdsOverlap: ${System.currentTimeMillis() - start}"
+                return true;
+            }
+        }
+        log.debug "@Duration for cdsOverlap: ${System.currentTimeMillis() - start}"
+        return false;
+    }
+
+    boolean overlapsOrf(Transcript transcript1, Transcript transcript2) {
+//        log.debug("overlapsOrf(Transcript transcript1, Transcript transcript2) ")
+        if ((transcriptService.isProteinCoding(transcript1) && transcriptService.isProteinCoding(transcript2))
+                && ((transcriptService.getGene(transcript1) == null || transcriptService.getGene(transcript2) == null) || (!(transcriptService.getGene(transcript1) instanceof Pseudogene) && !(transcriptService.getGene(transcript2) instanceof Pseudogene)))) {
+
+            CDS cds = transcriptService.getCDS(transcript1);
+
+            CDS cds2 = transcriptService.getCDS(transcript2)
+            if (cds2 && cds && overlaps(cds,cds2) &&  (overlaps(cds2,cds)))  {
+                List<Exon> exons1 = transcriptService.getSortedExons(transcript1,true);
+                List<Exon> exons2 = transcriptService.getSortedExons(transcript2,true);
+                return cdsOverlap(exons1, exons2, true);
+            }
+        }
+        return false
+    }
+
+    private class CDSEntity {
+        // POGO for handling CDS of individual exons
+        int fmin;
+        int fmax;
+        int length;
+        int phase;
+        String name;
+        String uniqueName;
+        Sequence sequence;
+        int strand;
+    }
+    
+    boolean overlaps(CDSEntity cds1, CDSEntity cds2) {
+        //overlaps() method for POGO CDSEntity
+        return overlaps(cds1.fmin, cds1.fmax, cds2.fmin, cds2.fmax)
+    }
+    
+    private ArrayList<CDSEntity> getCDSEntities(CDS cds, List<Exon> exons) {
+        ArrayList<CDSEntity> cdsEntities = new ArrayList<CDSEntity>();
+        HashMap<String,String> exonFrame = new HashMap<String,String>();
+        for (Exon exon : exons) {
+            if (!overlaps(exon,cds)) {
+                continue
+            }
+            int fmin = exon.fmin < cds.fmin ? cds.fmin : exon.fmin
+            int fmax = exon.fmax > cds.fmax ? cds.fmax : exon.fmax
+            int cdsEntityLength = fmax - fmin
+            
+            CDSEntity cdsEntity = new CDSEntity();
+            cdsEntity.fmin = fmin;
+            cdsEntity.fmax = fmax;
+            cdsEntity.length = cdsEntityLength;
+            cdsEntity.name = cds.name;
+            cdsEntity.uniqueName = cds.uniqueName + "-cds-entity"
+            cdsEntity.sequence = cds.featureLocation.sequence
+            cdsEntity.strand = cds.strand
+            cdsEntities.add(cdsEntity);
+        }
+        return cdsEntities;
+    }
+    
+    private boolean cdsOverlap(List<Exon> exons1, List<Exon> exons2, boolean checkStrand) {
+        // implementation for determining isoforms based on CDS overlaps
+        if(exons1.size()==0 || exons2.size()==0){
+            return false
+        }
+        CDS cds1 = transcriptService.getCDS( exonService.getTranscript(exons1.get(0)) )
+        CDS cds2 = transcriptService.getCDS( exonService.getTranscript(exons2.get(0)) )
+        ArrayList<CDSEntity> cdsEntitiesForTranscript1 = getCDSEntities(cds1, exons1)
+        ArrayList<CDSEntity> cdsEntitiesForTranscript2 = getCDSEntities(cds2, exons2)
+        int cds1Length = 0
+        int cds1Overhang = 0
+        int cds1AbsFrame = 0
+        int cds1RelFrame = 0
+        int cds1Frame = 0
+        int cds2Length = 0
+        int cds2Overhang = 0
+        int cds2AbsFrame = 0
+        int cds2RelFrame = 0
+        int cds2Frame = 0
+
+        for (int i = 0; i < cdsEntitiesForTranscript1.size(); i++) {
+            CDSEntity c1 = cdsEntitiesForTranscript1.get(i)
+            cds1Overhang = cds1Length % 3
+            cds1RelFrame = (3 - cds1Overhang) % 3
+            cds1AbsFrame = c1.strand == Strand.POSITIVE.value ? c1.fmin % 3 : (c1.fmax - 1) % 3
+            cds1Frame = c1.strand == Strand.POSITIVE.value ? (cds1AbsFrame + cds1RelFrame) % 3 : (3 + cds1AbsFrame - cds1RelFrame) % 3
+
+            for (int j = 0; j < cdsEntitiesForTranscript2.size(); j++) {
+                CDSEntity c2 = cdsEntitiesForTranscript2.get(j)
+                cds2Overhang = cds2Length % 3
+                cds2RelFrame = (3 - cds2Overhang) % 3
+                cds2AbsFrame = c2.strand == Strand.POSITIVE.value ? c2.fmin % 3 : (c2.fmax - 1) % 3
+                cds2Frame = c2.strand == Strand.POSITIVE.value ? (cds2AbsFrame + cds2RelFrame) % 3 : (3 + cds2AbsFrame - cds2RelFrame) % 3
+
+                log.debug "Comparing CDSEntity ${c1.fmin}-${c1.fmax} to ${c2.fmin}-${c2.fmax}"
+                log.debug "CDS1 vs. CDS2 frame: ${cds1Frame} vs. ${cds2Frame}"
+                if (overlaps(c1,c2)) {
+                    if (checkStrand) {
+                        if ((c1.strand == c2.strand) && (cds1Frame == cds2Frame)) {
+                            log.debug "Conditions met"
+                            return true
+                        }
+                    }
+                    else {
+                        return true
+                    }
+                }
+                cds2Length += c2.length
+            }
+            cds1Length += c1.length
+        }
+        return false
+    }
+    
+    boolean overlaps(Feature leftFeature, Feature rightFeature, boolean compareStrands = true) {
+        //log.debug("overlaps(Feature leftFeature, Feature rightFeature, boolean compareStrands)")
+        return overlaps(leftFeature.featureLocation, rightFeature.featureLocation, compareStrands)
+    }
+
+    boolean overlaps(FeatureLocation leftFeatureLocation, FeatureLocation rightFeatureLocation, boolean compareStrands = true) {
+        //log.debug("overlaps(FeatureLocation leftFeatureLocation, FeatureLocation rightFeatureLocation, boolean compareStrands)")
+        if (leftFeatureLocation.sequence != rightFeatureLocation.sequence) {
+            return false;
+        }
+        int thisFmin = leftFeatureLocation.getFmin();
+        int thisFmax = leftFeatureLocation.getFmax();
+        int thisStrand = leftFeatureLocation.getStrand();
+        int otherFmin = rightFeatureLocation.getFmin();
+        int otherFmax = rightFeatureLocation.getFmax();
+        int otherStrand = rightFeatureLocation.getStrand();
+        boolean strandsOverlap = compareStrands ? thisStrand == otherStrand : true;
+        if (strandsOverlap &&
+                overlaps(thisFmin,thisFmax,otherFmin,otherFmax)
+                ) {
+            return true;
+        }
+        return false;
+    }
+
+    boolean overlaps(int leftFmin, int leftFmax,int rightFmin,int rightFmax) {
+        return (leftFmin <= rightFmin && leftFmax > rightFmin ||
+                        leftFmin >= rightFmin && leftFmin < rightFmax)
+    }
+
+
+}
diff --git a/grails-app/services/org/bbop/apollo/PermissionService.groovy b/grails-app/services/org/bbop/apollo/PermissionService.groovy
new file mode 100644
index 0000000..e2ab19b
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/PermissionService.groovy
@@ -0,0 +1,669 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.session.Session
+import org.apache.shiro.subject.Subject
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap
+
+import javax.servlet.http.HttpServletRequest
+
+ at Transactional
+class PermissionService {
+
+    def preferenceService
+    def configWrapperService
+    def grailsApplication
+
+
+    def remoteUserAuthenticatorService
+    def usernamePasswordAuthenticatorService
+
+    boolean isUserAdmin(User user) {
+        if (user != null) {
+            for (Role role in user.roles) {
+                if (role.name == UserService.ADMIN) {
+                    return true
+                }
+            }
+        }
+
+        return false
+    }
+
+    boolean isAdmin() {
+        String currentUserName = SecurityUtils.subject.principal
+        if (currentUserName) {
+            User researcher = User.findByUsername(currentUserName)
+            if (isUserAdmin(researcher)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    List<Organism> getOrganisms(User user) {
+        if (isUserAdmin(user)) {
+            return Organism.listOrderByCommonName()
+        }
+        Set<Organism> organismList = new HashSet<>()
+        for (UserOrganismPermission userPermission in UserOrganismPermission.findAllByUser(user)) {
+            if (userPermission.permissions) {
+                organismList.add(userPermission.organism)
+            }
+        }
+        for (UserGroup userGroup in user?.userGroups) {
+            organismList.addAll(getOrganismsForGroup(userGroup))
+        }
+        List<Organism> returnOrganismList = []
+        for (Organism organism in organismList.sort() { a, b -> a.commonName <=> b.commonName }) {
+            returnOrganismList.add(organism)
+        }
+
+        return returnOrganismList
+    }
+
+    List<Organism> getOrganismsForGroup(UserGroup group) {
+        if (isAdmin()) {
+            return Organism.listOrderByCommonName()
+        }
+        List<Organism> organismList = new ArrayList<>()
+        for (GroupOrganismPermission groupPermission in GroupOrganismPermission.findAllByGroup(group)) {
+            // minimally, you should have at least one permission
+            if (groupPermission.permissions) {
+                organismList.add(groupPermission.organism)
+            }
+        }
+        return organismList
+    }
+
+
+    static Collection<PermissionEnum> mergeOrganismPermissions(Collection<PermissionEnum> permissionsA, Collection<PermissionEnum> permissionsB) {
+        Set<PermissionEnum> permissionEnums = new HashSet<>()
+        permissionEnums.addAll(permissionsA)
+
+        for (PermissionEnum permissionEnum in permissionsB) {
+            permissionEnums.add(permissionEnum)
+        }
+
+        return permissionEnums
+    }
+
+
+    List<PermissionEnum> getOrganismPermissionsForUser(Organism organism, User user) {
+        Set<PermissionEnum> permissions = new HashSet<>()
+        if (isUserAdmin(user)) {
+            permissions.addAll(PermissionEnum.ADMINISTRATE as List)
+        }
+
+        List<UserOrganismPermission> userPermissionList = UserOrganismPermission.findAllByOrganismAndUser(organism, user)
+        for (UserOrganismPermission userPermission in userPermissionList) {
+            JSONArray jsonArray = JSON.parse(userPermission.permissions) as JSONArray
+            for (int i = 0; i < jsonArray.size(); i++) {
+                String permission = jsonArray.getString(i)
+                PermissionEnum permissionEnum = PermissionEnum.getValueForString(permission)
+                permissions.add(permissionEnum)
+            }
+        }
+        if (user != null) {
+            for (UserGroup group in user.userGroups) {
+                permissions = mergeOrganismPermissions(permissions, getOrganismPermissionsForUserGroup(organism, group))
+            }
+        } else {
+            permissions.add(PermissionEnum.NONE)
+        }
+
+
+        return permissions as List
+
+    }
+
+    List<PermissionEnum> getOrganismPermissionsForUserGroup(Organism organism, UserGroup userGroup) {
+        Set<PermissionEnum> permissions = new HashSet<>()
+
+        List<GroupOrganismPermission> groupPermissionList = GroupOrganismPermission.findAllByOrganismAndGroup(organism, userGroup)
+        for (GroupOrganismPermission groupPermission in groupPermissionList) {
+            JSONArray jsonArray = JSON.parse(groupPermission.permissions) as JSONArray
+            for (int i = 0; i < jsonArray.size(); i++) {
+                String permission = jsonArray.getString(i)
+                PermissionEnum permissionEnum = PermissionEnum.getValueForString(permission)
+                permissions.add(permissionEnum)
+            }
+        }
+        return permissions as List
+    }
+
+
+    void setOrganismPermissionsForUser(List<PermissionEnum> permissions, Organism organism, User user, String token) {
+
+        UserOrganismPermission userOrganismPermission = UserOrganismPermission.findByOrganismAndUser(organism, user)
+        if (!userOrganismPermission) {
+            new UserOrganismPermission(
+                    organism: organism
+                    , permissions: generatePermissionString(permissions)
+                    , user: user
+                    , token: token
+            ).save(insert: true)
+        } else {
+            userOrganismPermission.permissions = generatePermissionString(permissions)
+            userOrganismPermission.save()
+        }
+
+    }
+
+    void setOrganismPermissionsForUserGroup(List<PermissionEnum> permissions, Organism organism, UserGroup group, String token) {
+
+        GroupOrganismPermission groupOrganismPermission = GroupOrganismPermission.findByOrganismAndGroup(organism, group)
+        if (!groupOrganismPermission) {
+            new GroupOrganismPermission(
+                    organism: organism
+                    , permissions: generatePermissionString(permissions)
+                    , group: group
+                    , token: token
+            ).save(insert: true)
+        } else {
+            groupOrganismPermission.permissions = generatePermissionString(permissions)
+            groupOrganismPermission.save()
+        }
+    }
+
+    private static String generatePermissionString(List<PermissionEnum> permissionEnums) {
+        JSONArray jsonArray = new JSONArray()
+        for (PermissionEnum permissionEnum in permissionEnums) {
+            jsonArray.add(permissionEnum.name())
+        }
+        return jsonArray.toString()
+    }
+
+    /**
+     * Get all of the highest organism permissions for a user
+     * @param user
+     * @return
+     */
+    Map<String, Integer> getPermissionsForUser(User user) {
+        Map<String, Integer> returnMap = new HashMap<>()
+        returnMap.put(user.username, 0)
+        Organism.all.each { organism ->
+            List<PermissionEnum> permissionEnums = getOrganismPermissionsForUser(organism, user)
+            int highestValue = findHighestEnumValue(permissionEnums)
+            if (highestValue > returnMap.get(user.username)) {
+                returnMap.put(user.username, highestValue)
+            }
+        }
+
+        return returnMap
+    }
+
+
+    PermissionEnum findHighestEnum(List<PermissionEnum> permissionEnums) {
+        PermissionEnum highestValue = PermissionEnum.NONE
+        permissionEnums.each { it ->
+            highestValue = it.rank > highestValue.rank ? it : highestValue
+        }
+
+        return highestValue
+    }
+
+    int findHighestEnumValue(List<PermissionEnum> permissionEnums) {
+        int highestValue = -1
+        permissionEnums.each { it ->
+            highestValue = it.value > highestValue ? it.value : highestValue
+        }
+
+        return highestValue
+    }
+
+    JSONObject copyValue(FeatureStringEnum featureStringEnum, JSONObject fromJSON, JSONObject toJSON) {
+        if (fromJSON.containsKey(featureStringEnum.value)) {
+            toJSON.put(featureStringEnum.value, fromJSON.getString(featureStringEnum.value))
+        } else {
+            log.info "No ${featureStringEnum.value} to copy from ${fromJSON}"
+        }
+        return toJSON
+    }
+
+    /**
+     * Copies values relevant to request
+     * @param fromJSON
+     * @param toJSON
+     * @return
+     */
+    JSONObject copyRequestValues(JSONObject fromJSON, JSONObject toJSON) {
+        copyValue(FeatureStringEnum.USERNAME, fromJSON, toJSON)
+        copyValue(FeatureStringEnum.CLIENT_TOKEN, fromJSON, toJSON)
+        copyValue(FeatureStringEnum.ORGANISM, fromJSON, toJSON)
+        return toJSON
+    }
+
+    def getOrganismsForCurrentUser(JSONObject jsonObject) {
+        User thisUser = getCurrentUser(jsonObject)
+        if (thisUser) {
+            return getOrganisms(thisUser) as List<Organism>
+        }
+        return []
+    }
+
+    static String getSequenceNameFromInput(JSONObject inputObject) {
+        String trackName = null
+        if (inputObject.has(FeatureStringEnum.SEQUENCE.value)) {
+            trackName = inputObject.sequence
+        }
+        if (inputObject.has(FeatureStringEnum.TRACK.value)) {
+            trackName = inputObject.track
+        }
+        return trackName
+    }
+
+    // get current user from session or input object
+    User getCurrentUser(JSONObject inputObject = new JSONObject()) {
+        String username = null
+        if (inputObject?.has(FeatureStringEnum.USERNAME.value)) {
+            username = inputObject.getString(FeatureStringEnum.USERNAME.value)
+        }
+        if (!username) {
+            username = SecurityUtils.subject.principal
+        }
+        if (!username) {
+            return null
+        }
+
+        User user = User.findByUsername(username)
+        return user
+
+    }
+
+
+    Organism getOrganismFromInput(JSONObject inputObject) {
+
+        if (inputObject.has(FeatureStringEnum.ORGANISM.value)) {
+            String organismString = inputObject.getString(FeatureStringEnum.ORGANISM.value)
+            return preferenceService.getOrganismForTokenInDB(organismString)
+        }
+        return null
+    }
+
+    /**
+     * This method finds the proper username with their proper organism for the current organism when including the track name.
+     *
+     * @param inputObject
+     * @param requiredPermissionEnum
+     * @return
+     */
+    Sequence checkPermissions(JSONObject inputObject, PermissionEnum requiredPermissionEnum) {
+        Organism organism
+        String sequenceName = getSequenceNameFromInput(inputObject)
+
+        User user = getCurrentUser(inputObject)
+        organism = getOrganismFromInput(inputObject)
+
+        if (!organism) {
+            String clientToken = inputObject.getString(FeatureStringEnum.CLIENT_TOKEN.value)
+            UserOrganismPreferenceDTO preferenceDTO = preferenceService.getCurrentOrganismPreference(user, sequenceName, clientToken)
+            log.debug "Permission service found DTO: ${preferenceDTO as JSON}"
+            if (preferenceDTO) {
+                organism = Organism.findById(preferenceDTO.organism.id)
+            }
+        }
+
+        Sequence sequence
+        if (!sequenceName) {
+            sequence = UserOrganismPreference.findByClientTokenAndOrganism(sequenceName, organism, [max: 1, sort: "lastUpdated", order: "desc"])?.sequence
+        } else {
+            sequence = Sequence.findByNameAndOrganism(sequenceName, organism)
+            if (!sequence) {
+                throw new AnnotationException("No sequence found for name '${sequenceName}' and organism '${organism?.commonName}'")
+            }
+        }
+
+        if (!sequence && organism) {
+            sequence = Sequence.findByOrganism(organism, [max: 1, sort: "end", order: "desc"])
+        }
+
+        List<PermissionEnum> permissionEnums = getOrganismPermissionsForUser(organism, user)
+        PermissionEnum highestValue = isUserAdmin(user) ? PermissionEnum.ADMINISTRATE : findHighestEnum(permissionEnums)
+
+        if (highestValue.rank < requiredPermissionEnum.rank) {
+            log.debug "highest value ${highestValue}"
+            log.debug "required permission ${requiredPermissionEnum}"
+            log.debug "highest value display ${highestValue.display}"
+            log.debug "permission display ${requiredPermissionEnum.display}"
+            throw new AnnotationException("You have insufficient permissions [${highestValue.display} < ${requiredPermissionEnum.display}] to perform this operation")
+        }
+        return sequence
+    }
+
+    Boolean checkPermissions(PermissionEnum requiredPermissionEnum) {
+        try {
+            Session session = SecurityUtils.subject.getSession(false)
+            if (session) {
+                Map<String, Integer> permissions = (Map<String, Integer>) session.getAttribute(FeatureStringEnum.PERMISSIONS.getValue())
+                // permissions not always on session if they come through a web-service, see #1759
+                if(!permissions){
+                    User user = User.findByUsername(SecurityUtils.subject.principal.toString())
+                    permissions = getPermissionsForUser(user)
+                }
+                if (permissions) {
+                    Integer permission = permissions.get(SecurityUtils.subject.principal)
+                    PermissionEnum sessionPermissionsEnum = isAdmin() ? PermissionEnum.ADMINISTRATE : PermissionEnum.getValueForOldInteger(permission)
+
+                    if (sessionPermissionsEnum == null) {
+                        log.warn "No permissions found in session"
+                        return false
+                    }
+
+                    if (sessionPermissionsEnum.rank < requiredPermissionEnum.rank) {
+                        log.warn "Permission required ${requiredPermissionEnum.display} vs found ${sessionPermissionsEnum.display}"
+                        return false
+                    }
+                    return true
+                } else {
+                    log.debug "No permissions found on session"
+                }
+            } else {
+                log.debug "No session found"
+            }
+
+        } catch (e) {
+            log.error "Error checking permissions from session ${e}"
+            e.printStackTrace()
+            return false
+        }
+        return false
+
+    }
+
+    PermissionEnum checkPermissions(JSONObject jsonObject, Organism organism, PermissionEnum requiredPermissionEnum) {
+
+        //def session = RequestContextHolder.currentRequestAttributes().getSession()
+        String username = jsonObject.getString(FeatureStringEnum.USERNAME.value)
+
+
+        User user = User.findByUsername(username)
+
+        List<PermissionEnum> permissionEnums = getOrganismPermissionsForUser(organism, user)
+        PermissionEnum highestValue = isUserAdmin(user) ? PermissionEnum.ADMINISTRATE : findHighestEnum(permissionEnums)
+
+        if (highestValue.rank < requiredPermissionEnum.rank) {
+            //return false
+            throw new AnnotationException("You have insufficient permissions [${highestValue.display} < ${requiredPermissionEnum.display}] to perform this operation")
+        }
+
+        return highestValue
+    }
+
+    /**
+     * This method validates after logged in, so it *should* not need a special authenticator.
+     * @param jsonObject
+     * @return
+     */
+    JSONObject validateSessionForJsonObject(JSONObject jsonObject) {
+        // not sure if permissions with translate through or not
+        Session session = SecurityUtils.subject.getSession(false)
+        if (!session) {
+            // login with jsonObject tokens
+            log.debug "creating session with found json object ${jsonObject.username}, ${jsonObject.password as String}"
+            if (!jsonObject.username) {
+                log.error "Username not supplied so can not authenticate."
+                jsonObject.error_message = "Username not supplied so can not authenticate."
+                return jsonObject
+            }
+            def authToken = new UsernamePasswordToken(jsonObject.username, jsonObject.password as String)
+            try {
+                Subject subject = SecurityUtils.getSubject()
+                subject.getSession(true)
+//                session = subject.getSession(true)
+
+                subject.login(authToken)
+                if (!subject.authenticated) {
+                    log.warn "Failed to authenticate user ${jsonObject.username}"
+                    jsonObject.error_message = "Failed to authenticate user ${jsonObject.username}"
+                    return jsonObject
+                }
+            } catch (Exception ae) {
+                log.error("Problem authenticating: " + ae.fillInStackTrace())
+                jsonObject.error_message = "Problem authenticating: " + ae.fillInStackTrace()
+                return jsonObject
+            }
+        } else if (!jsonObject.username && SecurityUtils?.subject?.principal) {
+            jsonObject.username = SecurityUtils?.subject?.principal
+        } else if (!jsonObject.username && session.attributeKeys.contains(FeatureStringEnum.USERNAME.value)) {
+            jsonObject.username = session.getAttribute(FeatureStringEnum.USERNAME.value)
+        }
+        return jsonObject
+    }
+
+    /**
+     * If a user exists and is a admin (not just for organism), then check, otherwise a regular user is still a valid user.
+     * @param jsonObject
+     * @param permissionEnum
+     * @return
+     */
+    Boolean hasGlobalPermissions(JSONObject jsonObject, PermissionEnum permissionEnum) {
+        jsonObject = validateSessionForJsonObject(jsonObject)
+        User user = User.findByUsername(jsonObject.username)
+        if (!user) {
+            log.error("User ${jsonObject.username} does not exist in the database.")
+            return false
+        }
+        if (jsonObject.error_message) {
+            log.error("Error with user permissions ${user.username}:  ${jsonObject.error_message}")
+            return false
+        }
+
+        // if the rank required is less than administrator than ask if they are an administrator
+        if (PermissionEnum.ADMINISTRATE.rank < permissionEnum.rank) {
+            return isUserAdmin(user)
+        }
+        return true
+    }
+
+    Boolean hasPermissions(JSONObject jsonObject, PermissionEnum permissionEnum) {
+        if (!hasGlobalPermissions(jsonObject, permissionEnum)) {
+            log.info("User lacks permissions ${permissionEnum.display}")
+            return false
+        }
+        String clientToken = jsonObject.getString(FeatureStringEnum.CLIENT_TOKEN.value)
+
+        Organism organism = getOrganismFromInput(jsonObject)
+
+        organism = organism ?: preferenceService.getCurrentOrganismPreferenceInDB(clientToken)?.organism
+        // don't set the preferences if it is coming off a script
+        if (clientToken != FeatureStringEnum.IGNORE.value) {
+            preferenceService.setCurrentOrganism(getCurrentUser(), organism, clientToken)
+        }
+
+        return checkPermissions(jsonObject, organism, permissionEnum)
+
+    }
+
+
+    Boolean hasAnyPermissions(User user) {
+
+        Map<String, Integer> permissions = getPermissionsForUser(user)
+        if (!permissions) {
+            return false
+        }
+
+        for (Integer value : permissions.values()) {
+            if (value > PermissionEnum.NONE.value) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    PermissionEnum findHighestOrganismPermissionForCurrentUser(Organism organism) {
+        findHighestOrganismPermissionForUser(organism, currentUser)
+    }
+
+    PermissionEnum findHighestOrganismPermissionForUser(Organism organism, User user) {
+        List<PermissionEnum> permissionEnums = getOrganismPermissionsForUser(organism, user)
+
+        PermissionEnum highestEnum = PermissionEnum.NONE
+        for (PermissionEnum permissionEnum : permissionEnums) {
+            if (permissionEnum.rank > highestEnum.rank) {
+                highestEnum = permissionEnum
+            }
+        }
+        return highestEnum
+    }
+
+    Map<Organism, Boolean> userHasOrganismPermissions(PermissionEnum permissionEnum) {
+        Map<Organism, Boolean> organismUserMap = [:]
+        UserOrganismPermission.findAllByUser(currentUser).each { permission ->
+            PermissionEnum highestPermssion = findHighestOrganismPermissionForCurrentUser(permission.organism)
+            organismUserMap.put(permission.organism, highestPermssion?.rank >= permissionEnum?.rank)
+        }
+
+        return organismUserMap
+    }
+
+    Boolean userHasOrganismPermission(Organism organism, PermissionEnum permissionEnum) {
+        return findHighestOrganismPermissionForCurrentUser(organism).rank >= permissionEnum.rank
+    }
+
+    def authenticateWithToken(UsernamePasswordToken usernamePasswordToken = null, HttpServletRequest request) {
+
+        def authentications = configWrapperService.authentications
+
+        for (auth in authentications) {
+            if (auth.active) {
+                log.info "Authenticating with ${auth.className}"
+                def authenticationService
+                if ("remoteUserAuthenticatorService" == auth.className) {
+                    authenticationService = remoteUserAuthenticatorService
+                    if (auth?.params?.containsKey("default_group")) {
+                        authenticationService.setDefaultGroup(auth.params.get("default_group"))
+                    }
+                } else if ("usernamePasswordAuthenticatorService" == auth.className) {
+                    authenticationService = usernamePasswordAuthenticatorService
+                } else {
+                    log.error("No authentication service for ${auth.className}")
+                    // better to return false if mis-configured
+                    return false
+                }
+
+                if (authenticationService.requiresToken()) {
+                    def req = handleInput(request, request.parameterMap)
+                    def authToken = usernamePasswordToken ?: null
+                    if (!authToken && req.username) {
+                        authToken = new UsernamePasswordToken(req.username as String, req.password as String)
+                    }
+                    if (authenticationService.authenticate(authToken, request)) {
+                        log.info "Authenticated user ${authToken.username} using ${auth.name}"
+                        return true
+                    }
+                } else {
+                    if (authenticationService.authenticate(request)) {
+                        log.info "Authenticated user ${auth.name}"
+                        return true
+                    }
+                }
+            }
+        }
+        log.warn "Failed to authenticate user"
+        return false
+    }
+
+    /**
+     * Verifies that "userId" matches userName for the secured session user
+     * @param jsonObject
+     * @return
+     */
+    Boolean sameUser(JSONObject jsonObject, HttpServletRequest request) {
+        // not sure if permissions with translate through or not
+        Session session = SecurityUtils.subject.getSession(false)
+        if (!session) {
+            // login with jsonObject tokens
+            log.debug "creating session with found json object ${jsonObject.username}, ${jsonObject.password as String}"
+            UsernamePasswordToken authToken = new UsernamePasswordToken(jsonObject.username, jsonObject.password as String)
+            authenticateWithToken(authToken, request)
+        } else if (!jsonObject.username && SecurityUtils?.subject?.principal) {
+            jsonObject.username = SecurityUtils?.subject?.principal
+        } else if (!jsonObject.username && session.attributeKeys.contains(FeatureStringEnum.USERNAME.value)) {
+            jsonObject.username = session.getAttribute(FeatureStringEnum.USERNAME.value)
+        }
+        if (jsonObject.username) {
+            User user = User.findByUsername(jsonObject.username)
+            return user?.id == jsonObject.userId
+        }
+        return false
+    }
+
+    @NotTransactional
+    def getInsufficientPermissionMessage(PermissionEnum permissionEnum) {
+        if (permissionEnum == PermissionEnum.ADMINISTRATE) {
+            return "Must have permissions ${PermissionEnum.ADMINISTRATE.display}."
+        } else {
+            return "Must have permissions ${permissionEnum.display} or better."
+        }
+    }
+
+    /**
+     * we prefer the param over the dataObject one I guess
+     * @param params
+     * @param dataObject
+     * @return
+     */
+    @NotTransactional
+    String handleToken(GrailsParameterMap params, JSONObject dataObject) {
+        // replace the dataObject either way
+        if (params.containsKey(FeatureStringEnum.CLIENT_TOKEN.value)) {
+            dataObject.put(FeatureStringEnum.CLIENT_TOKEN.value, params.get(FeatureStringEnum.CLIENT_TOKEN.value))
+        }
+        // if the dataObject doesn't contain nor does the param, then we create it
+        if (!dataObject.containsKey(FeatureStringEnum.CLIENT_TOKEN.value)) {
+            // client should generate token, not server
+//            dataObject.put(FeatureStringEnum.CLIENT_TOKEN.value,ClientTokenGenerator.generateRandomString())
+            dataObject.put(FeatureStringEnum.CLIENT_TOKEN.value, FeatureStringEnum.IGNORE.value)
+        }
+        String clientToken = dataObject.get(FeatureStringEnum.CLIENT_TOKEN.value)
+        if (!dataObject.containsKey(FeatureStringEnum.ORGANISM.value)) {
+            log.debug("dataObject does not contain organism (may not be needed)")
+        }
+        return clientToken
+    }
+
+    @NotTransactional
+    JSONObject handleInput(HttpServletRequest request, GrailsParameterMap params) {
+        JSONObject payloadJson = new JSONObject()
+        if (request.JSON) {
+            payloadJson = request.JSON as JSONObject
+        }
+        if (!payloadJson || payloadJson.size() == 0) {
+            if (params.data) {
+                payloadJson = JSON.parse(params.data.toString()) as JSONObject
+            } else {
+                payloadJson = params as JSONObject
+            }
+        }
+        handleToken(params, payloadJson)
+        return payloadJson
+    }
+
+    JSONObject handleInput(HttpServletRequest request, Map params) {
+        JSONObject payloadJson = new JSONObject()
+        if (request.JSON) {
+            payloadJson = request.JSON as JSONObject
+        }
+        else {
+            params.keySet().each { key ->
+                // TODO: what about this?
+                payloadJson.put(key, params.get(key)[0])
+
+            }
+        }
+        return payloadJson
+    }
+
+
+}
diff --git a/grails-app/services/org/bbop/apollo/PhoneHomeService.groovy b/grails-app/services/org/bbop/apollo/PhoneHomeService.groovy
new file mode 100644
index 0000000..efdc6c1
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/PhoneHomeService.groovy
@@ -0,0 +1,71 @@
+package org.bbop.apollo
+
+import grails.async.Promise
+import static grails.async.Promises.*
+import grails.transaction.Transactional
+import groovy.json.JsonSlurper
+
+ at Transactional
+class PhoneHomeService {
+
+    def configWrapperService
+    def grailsApplication
+
+    def pingServerAsync(String message = null ,Map<String,String> argMap = [:]) {
+        try {
+            Promise p = task {
+                // Long running task
+                pingServer(org.bbop.apollo.PhoneHomeEnum.START.value)
+            }
+            p.onError { Throwable err ->
+                log.error "An error occured while phoning home ${err.message}"
+            }
+            p.onComplete { result ->
+                log.info "Phone home completed"
+            }
+            def result = p.get()
+        } catch (e) {
+            log.warn("Failed to phone home: "+e)
+        }
+    }
+
+    /**
+     * Only process args if there is a message
+     * @param message
+     * @param args
+     * @return
+     */
+    def pingServer(String message = null ,Map<String,String> argMap = [:]) {
+        if(!configWrapperService.phoneHome) {
+            println("Not phoning home")
+            return
+        }
+
+        try {
+            String apiString = configWrapperService.pingUrl
+            ServerData.withTransaction{
+                if(ServerData.count>1){
+                    ServerData.deleteAll(ServerData.all)
+                }
+                if(ServerData.count==0){
+                    new ServerData().save(flush: true,insert:true)
+                }
+                apiString += "?${PhoneHomeEnum.SERVER.value}="+ServerData.first().name
+                apiString += "&${PhoneHomeEnum.ENVIRONMENT.value}="+grails.util.Environment.current.name
+                if(message){
+                    apiString += "&${PhoneHomeEnum.MESSAGE.value}=${message}"
+
+                    for(k in argMap){
+                        apiString += "&${k.key}=${k.value}"
+                    }
+                }
+            }
+            log.debug("Phoning home to ${apiString}")
+            URL apiUrl = new URL(apiString)
+            def responseJson = new JsonSlurper().parse(apiUrl)
+            return responseJson
+        } catch (e) {
+            log.warn("Not phoning home due to error: "+e.toString())
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/PreferenceService.groovy b/grails-app/services/org/bbop/apollo/PreferenceService.groovy
new file mode 100644
index 0000000..7d485af
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/PreferenceService.groovy
@@ -0,0 +1,756 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.session.Session
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.preference.OrganismDTO
+import org.bbop.apollo.preference.SequenceDTO
+import org.bbop.apollo.preference.UserDTO
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+ at Transactional
+class PreferenceService {
+
+    def permissionService
+
+    final Integer PREFERENCE_SAVE_DELAY_SECONDS = 5  // saves every 30 seconds
+    // enqueue to store save
+    private Map<UserOrganismPreferenceDTO, Date> saveSequenceLocationMap = new HashMap<>()
+    // set of client locations
+    private Set<String> currentlySavingLocation = new HashSet<>()
+    private Date lastSaveEvaluation = new Date()
+
+
+    Organism getSessionOrganism(String clientToken) {
+        JSONObject preferenceObject = getSessionPreferenceObject(clientToken)
+        if (preferenceObject) {
+            def organismId = preferenceObject.organism.id as Long
+            return Organism.get(organismId) ?: Organism.findById(organismId)
+        }
+        return null
+    }
+
+
+    UserOrganismPreferenceDTO getSessionPreference(String clientToken) {
+        JSONObject preferenceObject = getSessionPreferenceObject(clientToken)
+        return preferenceObject ? getDTOPreferenceFromObject(preferenceObject) : null
+    }
+
+    UserOrganismPreferenceDTO getDTOPreferenceFromObject(JSONObject userOrganismPreferenceObject) {
+        OrganismDTO organismDTO = getDTOFromOrganismFromObject(userOrganismPreferenceObject.getJSONObject(FeatureStringEnum.ORGANISM.value))
+        SequenceDTO sequenceDTO = getDTOSequenceFromObject(userOrganismPreferenceObject.getJSONObject(FeatureStringEnum.SEQUENCE.value))
+        UserDTO userDTO = getDTOUserFromObject(userOrganismPreferenceObject.getJSONObject("user"))
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = new UserOrganismPreferenceDTO(
+                organism: organismDTO
+                , sequence: sequenceDTO
+                , id: userOrganismPreferenceObject.id
+                , user: userDTO
+                , currentOrganism: userOrganismPreferenceObject.currentOrganism
+                , nativeTrackList: userOrganismPreferenceObject.nativeTrackList
+                , startbp: userOrganismPreferenceObject.startbp
+                , endbp: userOrganismPreferenceObject.endbp
+                , clientToken: userOrganismPreferenceObject.clientToken
+        )
+        return userOrganismPreferenceDTO
+    }
+
+    UserDTO getDTOUserFromObject(JSONObject user) {
+        UserDTO userDTO = new UserDTO(
+                id: user.id
+                , username: user.username
+        )
+        return userDTO
+    }
+
+    SequenceDTO getDTOSequenceFromObject(JSONObject sequence) {
+        if (!sequence) return null
+        OrganismDTO organismDTO = getDTOFromOrganismFromObject(sequence.getJSONObject(FeatureStringEnum.ORGANISM.value))
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                id: sequence.id
+                , organism: organismDTO
+                , name: sequence.name
+                , start: sequence.start
+                , end: sequence.end
+                , length: sequence.length
+        )
+        return sequenceDTO
+    }
+
+    OrganismDTO getDTOFromOrganismFromObject(JSONObject organism) {
+        OrganismDTO organismDTO = new OrganismDTO(
+                id: organism.id
+                , commonName: organism.commonName
+                , directory: organism.directory
+        )
+        return organismDTO
+    }
+
+
+    JSONObject getSessionPreferenceObject(String clientToken) {
+        try {
+            Session session = SecurityUtils.subject.getSession(false)
+            if (session) {
+//                printKeys(session)
+                String preferenceString = session.getAttribute(FeatureStringEnum.PREFERENCE.getValue() + "::" + clientToken)?.toString()
+                if (!preferenceString) return null
+                return JSON.parse(preferenceString) as JSONObject
+            } else {
+                log.debug "No session found"
+            }
+        } catch (e) {
+            log.debug "faild to get the gession preference objec5 ${e}"
+        }
+        return null
+    }
+
+    UserOrganismPreferenceDTO setSessionPreference(String clientToken, UserOrganismPreferenceDTO userOrganismPreferenceDTO) {
+        Session session = SecurityUtils.subject.getSession(false)
+        if (session) {
+            // should be client_token , JSONObject
+            String preferenceString = (userOrganismPreferenceDTO as JSON).toString()
+            session.setAttribute(FeatureStringEnum.PREFERENCE.getValue() + "::" + clientToken, preferenceString)
+        } else {
+            log.warn "No session found"
+        }
+        return userOrganismPreferenceDTO
+    }
+
+
+    Organism getCurrentOrganismForCurrentUser(String clientToken) {
+        log.debug "PS: getCurrentOrganismForCurrentUser ${clientToken}"
+        Organism organism = getSessionOrganism(clientToken)
+        log.debug "found organism in session ${organism} so returning"
+        if (organism) return organism
+        if (permissionService.currentUser == null) {
+            log.warn "No user present, so using the client token"
+            organism = getOrganismForTokenInDB(clientToken)
+            return organism
+        } else {
+            UserOrganismPreferenceDTO userOrganismPreference = getCurrentOrganismPreference(permissionService.currentUser, null, clientToken)
+            OrganismDTO organismDTO = setSessionPreference(clientToken, userOrganismPreference)?.organism
+            return Organism.findById(organismDTO.id)
+        }
+    }
+
+    Organism getOrganismForTokenInDB(String token) {
+        log.debug "token for org ${token}"
+        if (token.isLong()) {
+            log.debug "is long "
+            return Organism.findById(Long.parseLong(token))
+        } else {
+            log.debug "is NOT long "
+            return Organism.findByCommonNameIlike(token)
+        }
+    }
+
+    Organism getOrganismForToken(String token) {
+        Organism organism = getSessionOrganism(token)
+        if (organism) {
+            return organism
+        } else {
+            return getOrganismForTokenInDB(token)
+        }
+    }
+
+
+    UserOrganismPreferenceDTO setCurrentOrganism(User user, Organism organism, String clientToken) {
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = getCurrentOrganismPreference(user, null, clientToken)
+        if (userOrganismPreferenceDTO.organism.id == organism.id) {
+            log.info "Same organism so not changing preference"
+            return userOrganismPreferenceDTO
+        }
+        evaluateSaves(true, clientToken)
+        // we have to go to the database to see if there was a prior sequence
+        // we look for the organism association with the current active token, first
+        UserOrganismPreference userOrganismPreference = UserOrganismPreference.findByUserAndOrganismAndCurrentOrganismAndClientToken(user, organism, true, clientToken, [sort: "lastUpdated", order: "desc", max: 1])
+        // we look for the organism association with the current inactive token, first
+        userOrganismPreference = userOrganismPreference ?: UserOrganismPreference.findByUserAndOrganismAndClientToken(user, organism, clientToken, [sort: "lastUpdated", order: "desc", max: 1])
+        if (!userOrganismPreference) {
+
+            // We need to create a new preference
+            // We look for the organism association with any current organism
+            UserOrganismPreference existingPreference = UserOrganismPreference.findByUserAndOrganismAndCurrentOrganism(user, organism, true, [sort: "lastUpdated", order: "desc", max: 1])
+            // We look for the organism association with any non-current organism
+            existingPreference = existingPreference ?: UserOrganismPreference.findByUserAndOrganism(user, organism, [sort: "lastUpdated", order: "desc", max: 1])
+
+            // if the existing preference exists, then use those values
+            if (existingPreference) {
+                userOrganismPreference = new UserOrganismPreference(
+                        user: user
+                        , organism: organism
+                        , clientToken: clientToken
+                        , sequence: existingPreference.sequence
+                        , currentOrganism: true
+                        , startbp: existingPreference.startbp
+                        , endbp: existingPreference.endbp
+                )
+            }
+
+            // else use random ones
+            else {
+                // then create one
+                Sequence sequence = Sequence.findAllByOrganism(organism, [max: 1, sort: "end", order: "desc"]).first()
+                userOrganismPreference = new UserOrganismPreference(
+                        user: user
+                        , organism: organism
+                        , clientToken: clientToken
+                        , sequence: sequence
+                        , currentOrganism: true
+                        , startbp: 0
+                        , endbp: sequence.end
+                )
+            }
+
+        } else {
+            userOrganismPreference.currentOrganism = true
+        }
+        userOrganismPreference.save(flush: true)
+
+        userOrganismPreferenceDTO = getDTOFromPreference(userOrganismPreference)
+
+        setSessionPreference(clientToken, userOrganismPreferenceDTO)
+        setCurrentOrganismInDB(userOrganismPreferenceDTO)
+        return userOrganismPreferenceDTO
+    }
+
+    /**
+     * We can save the preference locally.
+     *
+     * In the database we set this organism and sequence to true.
+     *
+     * 1. If there is none for this organism + sequence + client_token
+     *    Then we generate it
+     * 2. If there is one for this organism + sequence + client_token
+     *    We retrieve it and update the start / stop
+     *
+     * Else . . .
+     *
+     * @param userOrganismPreferenceDTO
+     * @return
+     */
+    UserOrganismPreference setCurrentOrganismInDB(UserOrganismPreferenceDTO userOrganismPreferenceDTO) {
+//        UserOrganismPreference setCurrentOrganismInDB(User user, Organism organism, String clientToken) {
+        UserOrganismPreference organismPreference = userOrganismPreferenceDTO.id ? UserOrganismPreference.findById(userOrganismPreferenceDTO.id) : null
+
+        // 2. update it
+        if (organismPreference
+                && userOrganismPreferenceDTO.clientToken == organismPreference.clientToken
+                && userOrganismPreferenceDTO.sequence.name == organismPreference.sequence.name
+                && userOrganismPreferenceDTO.organism.id == organismPreference.organism.id
+        ) {
+            organismPreference.startbp = userOrganismPreferenceDTO.startbp
+            organismPreference.endbp = userOrganismPreferenceDTO.endbp
+            organismPreference.save()
+        }
+        // 1. create a new one
+        else {
+            organismPreference = generateNewPreferenceFromDTO(userOrganismPreferenceDTO)
+        }
+
+        // if the preference already exists
+        def userOrganismPreferences = UserOrganismPreference.findAllByUserAndOrganismAndClientToken(organismPreference.user, organismPreference.organism, organismPreference.clientToken, [sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences found: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences))
+        }
+        int affected = setOtherCurrentOrganismsFalseInDB(organismPreference, organismPreference.user, organismPreference.clientToken)
+        log.debug "others set to false: ${affected}"
+        return organismPreference
+    }
+
+    protected static
+    def setOtherCurrentOrganismsFalseInDB(UserOrganismPreference userOrganismPreference) {
+        return setOtherCurrentOrganismsFalseInDB(userOrganismPreference, userOrganismPreference.user, userOrganismPreference.clientToken)
+    }
+
+    protected static
+    def setOtherCurrentOrganismsFalseInDB(UserOrganismPreference userOrganismPreference, User user, String clientToken) {
+        int affected = UserOrganismPreference.executeUpdate(
+                "update UserOrganismPreference  pref set pref.currentOrganism = false " +
+                        "where pref.id != :prefId and pref.user = :user and pref.clientToken = :clientToken",
+                [prefId: userOrganismPreference.id, user: user, clientToken: clientToken])
+
+        Date now = new Date()
+        Date newDate = new Date(now.getTime() - 8 * 1000 * 60 * 60)
+        // no current organisms if they are 8 hours hold
+        if (userOrganismPreference.sequence) {
+            affected += UserOrganismPreference.executeUpdate(
+                    "update UserOrganismPreference  pref set pref.currentOrganism = false " +
+                            "where pref.id != :prefId and pref.user = :user and pref.organism = :organism and pref.sequence = :sequence and pref.lastUpdated < :lastUpdated ",
+                    [prefId: userOrganismPreference.id, user: user, organism: userOrganismPreference.organism, sequence: userOrganismPreference.sequence, lastUpdated: newDate])
+        }
+
+        // if the preference is not set correctly, we have to make sure we add it correctly
+        affected += UserOrganismPreference.executeUpdate(
+                "update UserOrganismPreference  pref set pref.currentOrganism = true " +
+                        "where pref.id = :prefId and pref.user = :user and pref.clientToken = :clientToken and pref.currentOrganism = false ",
+                [prefId: userOrganismPreference.id, user: user, clientToken: clientToken])
+
+        return affected
+    }
+
+    UserOrganismPreferenceDTO setCurrentSequence(User user, Sequence sequence, String clientToken) {
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = getCurrentOrganismPreference(user, sequence.name, clientToken) ?: null
+        if (userOrganismPreferenceDTO.sequence.id == sequence.id) {
+            log.info "Same sequence so not changing preference"
+            return userOrganismPreferenceDTO
+        }
+
+        evaluateSaves(true, clientToken)
+        UserOrganismPreference userOrganismPreference = UserOrganismPreference.findByUserAndSequenceAndCurrentOrganismAndClientToken(user, sequence, true, clientToken, [sort: "lastUpdated", order: "desc", max: 1])
+        userOrganismPreference = userOrganismPreference ?: UserOrganismPreference.findByUserAndSequenceAndClientToken(user, sequence, clientToken, [sort: "lastUpdated", order: "desc", max: 1])
+        userOrganismPreference = userOrganismPreference ?: UserOrganismPreference.findByUserAndSequenceAndCurrentOrganism(user, sequence, true, [sort: "lastUpdated", order: "desc", max: 1])
+        userOrganismPreference = userOrganismPreference ?: UserOrganismPreference.findByUserAndSequence(user, sequence, [sort: "lastUpdated", order: "desc", max: 1])
+
+        if (!userOrganismPreference) {
+            // then create one
+            userOrganismPreference = new UserOrganismPreference(
+                    user: user
+                    , organism: sequence.organism
+                    , clientToken: clientToken
+                    , sequence: sequence
+                    , currentOrganism: true
+                    , startbp: 0
+                    , endbp: sequence.end
+            )
+        } else {
+            userOrganismPreference.currentOrganism = true
+        }
+        userOrganismPreference.save(flush: true)
+
+        userOrganismPreferenceDTO = getDTOFromPreference(userOrganismPreference)
+
+        setSessionPreference(clientToken, userOrganismPreferenceDTO)
+        setCurrentSequenceInDB(user, sequence, clientToken)
+        return userOrganismPreferenceDTO
+    }
+
+    private
+    static UserOrganismPreference getMostRecentPreference(List<UserOrganismPreference> userOrganismPreferences) {
+        if (userOrganismPreferences) {
+            return userOrganismPreferences.sort() { a, b ->
+                b.lastUpdated <=> a.lastUpdated
+            }.first()
+        }
+        return null
+    }
+
+    UserOrganismPreference setCurrentSequenceInDB(User user, Sequence sequence, String clientToken) {
+        Organism organism = sequence.organism
+        def userOrganismPreferences = UserOrganismPreference.findAllByUserAndOrganismAndClientTokenAndSequence(user, organism, clientToken, sequence, [sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences for sequence and organism: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences), user, clientToken)
+        }
+        UserOrganismPreference userOrganismPreference = getMostRecentPreference(userOrganismPreferences)
+
+        if (!userOrganismPreference) {
+            userOrganismPreference = new UserOrganismPreference(
+                    user: user
+                    , organism: organism
+                    , currentOrganism: true
+                    , sequence: sequence
+                    , clientToken: clientToken
+            ).save(flush: true, insert: true)
+        } else if (!userOrganismPreference.currentOrganism) {
+            userOrganismPreference.currentOrganism = true
+            userOrganismPreference.sequence = sequence
+            userOrganismPreference.save(flush: true, insert: false)
+        }
+        setOtherCurrentOrganismsFalseInDB(userOrganismPreference, user, clientToken)
+        return userOrganismPreference
+    }
+
+    UserOrganismPreferenceDTO setCurrentSequenceLocation(String sequenceName, Integer startBp, Integer endBp, String clientToken) {
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = getCurrentOrganismPreference(permissionService.currentUser, sequenceName, clientToken)
+        if (userOrganismPreferenceDTO.sequence.name != sequenceName || userOrganismPreferenceDTO.sequence?.organism?.id != userOrganismPreferenceDTO.organism.id) {
+            Organism organism = Organism.findById(userOrganismPreferenceDTO.organism.id)
+            Sequence sequence = Sequence.findByNameAndOrganism(sequenceName, organism)
+            userOrganismPreferenceDTO.sequence = getDTOFromSequence(sequence)
+        }
+        userOrganismPreferenceDTO.startbp = startBp
+        userOrganismPreferenceDTO.endbp = endBp
+        userOrganismPreferenceDTO.currentOrganism = true
+        setSessionPreference(clientToken, userOrganismPreferenceDTO)
+
+        scheduleSaveSequenceLocationInDB(userOrganismPreferenceDTO)
+
+        return userOrganismPreferenceDTO
+    }
+
+    def evaluateSaves(boolean forceSaves = false, String onlySaveToken = null) {
+        log.debug "Evaluating saves: ${forceSaves}"
+        long timeDiff = (new Date()).getTime() - lastSaveEvaluation.getTime()
+        if (!forceSaves && timeDiff / 1000.0 < PREFERENCE_SAVE_DELAY_SECONDS) {
+            log.debug "Not saving yet timeDif: ${timeDiff}"
+            return
+        }
+        log.debug "Saving with time diff: ${timeDiff}"
+        lastSaveEvaluation = new Date()
+        def sequenceMapSet = saveSequenceLocationMap.entrySet()
+        def sequenceMapSetIterator = sequenceMapSet.iterator()
+
+        try {
+            while (sequenceMapSetIterator.hasNext()) {
+                Map.Entry<UserOrganismPreferenceDTO, Date> userOrganismPreferenceDTOEntry = sequenceMapSetIterator.next()
+                log.debug "DTO: ${userOrganismPreferenceDTOEntry.key as JSON}"
+                log.debug "value date : ${saveSequenceLocationMap.get(userOrganismPreferenceDTOEntry.key)}"
+                log.debug "value date 2 : ${userOrganismPreferenceDTOEntry.value}"
+                if (onlySaveToken && onlySaveToken == userOrganismPreferenceDTOEntry.key.clientToken) {
+                    evaluateSave(userOrganismPreferenceDTOEntry.value, userOrganismPreferenceDTOEntry.key, forceSaves)
+                } else {
+                    evaluateSave(userOrganismPreferenceDTOEntry.value, userOrganismPreferenceDTOEntry.key, forceSaves)
+                }
+            }
+        } catch (e) {
+            log.warn("Problem saving preference: " + e)
+        }
+
+    }
+
+    def evaluateSave(Date date, UserOrganismPreferenceDTO preferenceDTO, Boolean forceSave) {
+        try {
+            currentlySavingLocation.add(preferenceDTO.clientToken)
+            Date now = new Date()
+            log.debug "trying to save it ${preferenceDTO.clientToken}"
+            def timeDiff = (now.getTime() - date.getTime()) / 1000
+            if (forceSave || timeDiff > PREFERENCE_SAVE_DELAY_SECONDS) {
+                log.debug "saving ${preferenceDTO.clientToken} location to the database time: ${timeDiff}"
+                setCurrentSequenceLocationInDB(preferenceDTO)
+            } else {
+                log.debug "not saving ${preferenceDTO.clientToken} location to the database time: ${timeDiff}"
+            }
+        } catch (e) {
+            log.error "Problem saving ${e} for ${preferenceDTO as JSON}"
+        } finally {
+            currentlySavingLocation.remove(preferenceDTO.clientToken)
+        }
+    }
+
+    def scheduleDbSave(Date date, UserOrganismPreferenceDTO preferenceDTO) {
+        // these should replace, though
+        if (!currentlySavingLocation.contains(preferenceDTO.clientToken)) {
+            // saves with the latest one now
+            currentlySavingLocation.add(preferenceDTO.clientToken)
+        }
+        saveSequenceLocationMap.put(preferenceDTO, date)
+    }
+
+    def scheduleSaveSequenceLocationInDB(UserOrganismPreferenceDTO userOrganismPreferenceDTO) {
+        Date date = saveSequenceLocationMap.get(userOrganismPreferenceDTO)
+        log.debug "Scheduling save date: ${date}"
+        if (!date) {
+            log.debug "No last save found so saving ${userOrganismPreferenceDTO.clientToken} location to the database."
+            setCurrentSequenceLocationInDB(userOrganismPreferenceDTO)
+        } else {
+            log.debug "Evaluate save for date: ${date}"
+            scheduleDbSave(date, userOrganismPreferenceDTO)
+        }
+        saveSequenceLocationMap.put(userOrganismPreferenceDTO, new Date())
+        saveOutstandingLocationPreferences(userOrganismPreferenceDTO.clientToken)
+    }
+
+    def saveOutstandingLocationPreferences(String ignoreToken = "") {
+        saveSequenceLocationMap.each {
+            if (it.key.clientToken != ignoreToken) {
+                scheduleDbSave(it.value, it.key)
+            }
+        }
+    }
+
+    UserOrganismPreference generateNewPreferenceFromDTO(UserOrganismPreferenceDTO userOrganismPreferenceDTO) {
+        Organism organism = Organism.findById(userOrganismPreferenceDTO.organism.id)
+        Sequence sequence = Sequence.findByNameAndOrganism(userOrganismPreferenceDTO.sequence.name, organism)
+        User user = User.findByUsername(userOrganismPreferenceDTO.user.username)
+        UserOrganismPreference userOrganismPreference = new UserOrganismPreference(
+                organism: organism
+                , user: user
+                , sequence: sequence
+                , currentOrganism: userOrganismPreferenceDTO.currentOrganism
+                , nativeTrackList: userOrganismPreferenceDTO.nativeTrackList
+                , startbp: userOrganismPreferenceDTO.startbp
+                , endbp: userOrganismPreferenceDTO.endbp
+                , clientToken: userOrganismPreferenceDTO.clientToken
+        ).save(insert: true)
+        return userOrganismPreference
+    }
+
+    SequenceDTO getDTOFromSequence(Sequence sequence) {
+//        if(!sequence) return null
+        OrganismDTO organismDTO = getDTOFromOrganism(sequence.organism)
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                id: sequence.id
+                , organism: organismDTO
+                , name: sequence.name
+                , start: sequence.start
+                , end: sequence.end
+                , length: sequence.length
+        )
+        return sequenceDTO
+    }
+
+    OrganismDTO getDTOFromOrganism(Organism organism) {
+        OrganismDTO organismDTO = new OrganismDTO(
+                id: organism.id
+                , commonName: organism.commonName
+                , directory: organism.directory
+        )
+        return organismDTO
+    }
+
+    UserDTO getDTOFromUser(User user) {
+        UserDTO userDTO = new UserDTO(
+                id: user.id
+                , username: user.username
+        )
+        return userDTO
+    }
+
+    UserOrganismPreferenceDTO getDTOFromPreference(UserOrganismPreference userOrganismPreference) {
+        Sequence sequence = userOrganismPreference.sequence
+        SequenceDTO sequenceDTO = getDTOFromSequence(sequence)
+        OrganismDTO organismDTO = getDTOFromOrganism(userOrganismPreference.organism)
+        UserDTO userDTO = getDTOFromUser(userOrganismPreference.user)
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = new UserOrganismPreferenceDTO(
+                organism: organismDTO
+                , sequence: sequenceDTO
+                , id: userOrganismPreference.id
+                , user: userDTO
+                , currentOrganism: userOrganismPreference.currentOrganism
+                , nativeTrackList: userOrganismPreference.nativeTrackList
+                , startbp: userOrganismPreference.startbp
+                , endbp: userOrganismPreference.endbp
+                , clientToken: userOrganismPreference.clientToken
+        )
+        return userOrganismPreferenceDTO
+    }
+
+
+    UserOrganismPreference setCurrentSequenceLocationInDB(UserOrganismPreferenceDTO preferenceDTO) {
+        log.debug "Saving location in DB: ${preferenceDTO as JSON}"
+        saveSequenceLocationMap.remove(preferenceDTO)
+
+        preferenceDTO = getSessionPreference(preferenceDTO.clientToken) ?: preferenceDTO
+
+
+        User user = User.findByUsername(preferenceDTO.user.username)
+        String sequenceName = preferenceDTO.sequence.name
+        String clientToken = preferenceDTO.clientToken
+        Integer startBp = preferenceDTO.startbp
+        Integer endBp = preferenceDTO.endbp
+
+        OrganismDTO currentOrganism = getCurrentOrganismPreference(user, sequenceName, clientToken)?.organism
+        if (!currentOrganism) {
+            throw new AnnotationException("Organism preference is not set for user")
+        }
+        Organism organism = Organism.findById(currentOrganism.id)
+        Sequence sequence = Sequence.findByNameAndOrganism(sequenceName, organism)
+        if (!sequence) {
+            throw new AnnotationException("Sequence name is invalid ${sequenceName} and organism ${organism as JSON}")
+        }
+
+        def userOrganismPreferences = UserOrganismPreference.createCriteria().list {
+            createAlias('sequence', 'sequence', org.hibernate.criterion.CriteriaSpecification.LEFT_JOIN)
+            and {
+                eq("user", user)
+                eq("clientToken", clientToken)
+                eq("sequence.name", sequenceName)
+                eq("organism", organism)
+            }
+        }
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences found: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences), user, clientToken)
+        }
+        UserOrganismPreference userOrganismPreference = getMostRecentPreference(userOrganismPreferences)
+        if (!userOrganismPreference) {
+            log.debug "Creating a new PReference"
+            userOrganismPreference = new UserOrganismPreference(
+                    user: user
+                    , currentOrganism: true
+                    , sequence: sequence
+                    , organism: organism
+                    , clientToken: clientToken
+            ).save(insert: true)
+        }
+        log.debug "init save ${userOrganismPreference as JSON}"
+
+        log.debug "version ${userOrganismPreference.version} for ${userOrganismPreference.organism.commonName} ${userOrganismPreference.currentOrganism}"
+
+        userOrganismPreference.refresh()
+
+        log.debug "refresh save ${userOrganismPreference as JSON}"
+
+        userOrganismPreference.clientToken = clientToken
+        userOrganismPreference.currentOrganism = true
+        userOrganismPreference.sequence = sequence
+
+        // use the current value if we aren't setting it
+        if (userOrganismPreference.startbp) {
+            userOrganismPreference.startbp = startBp ?: userOrganismPreference.startbp
+        } else {
+            userOrganismPreference.startbp = startBp ?: 0
+        }
+
+        if (userOrganismPreference.endbp) {
+            userOrganismPreference.endbp = endBp ?: userOrganismPreference.endbp
+        } else {
+            userOrganismPreference.endbp = endBp ?: sequence.end
+        }
+
+        userOrganismPreference.save(flush: true, insert: false)
+        log.debug "final save ${userOrganismPreference as JSON}"
+    }
+
+    UserOrganismPreferenceDTO getCurrentOrganismPreference(User user, String sequenceName, String clientToken) {
+
+        UserOrganismPreferenceDTO preference = getSessionPreference(clientToken)
+        preference = preference ?: getSavingPreferences(user, sequenceName, clientToken)
+        log.debug "found in-memory preference: ${preference ? preference as JSON : null}"
+        return preference ?: getDTOFromPreference(getCurrentOrganismPreferenceInDB(user, sequenceName, clientToken))
+    }
+
+    def getSavingPreferences(User user, String sequenceName, String clientToken) {
+        return saveSequenceLocationMap.keySet().find() {
+            it.clientToken == clientToken && it.user.username == user.username && it.sequence.name == sequenceName
+        }
+    }
+
+    UserOrganismPreference getCurrentOrganismPreferenceInDB(User user, String sequenceName, String clientToken) {
+        if (!user && !clientToken) {
+            log.warn("No organism preference if no user ${user} or client token ${clientToken}")
+            return null
+        }
+
+        // 1 - if a user exists, look up their client token and if they have a current organism.
+        def userOrganismPreferences = UserOrganismPreference.findAllByUserAndCurrentOrganismAndClientToken(user, true, clientToken, [sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences found: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences), user, clientToken)
+        }
+        UserOrganismPreference userOrganismPreference = getMostRecentPreference(userOrganismPreferences)
+        if (userOrganismPreference) {
+            return userOrganismPreference
+        }
+
+        // 2 - if there is not a current organism for that token, then grab the first non-current one (unlikely) and make it current
+        userOrganismPreferences = UserOrganismPreference.findAllByUserAndCurrentOrganismAndClientToken(user, false, clientToken, [sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences found: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences), user, clientToken)
+        }
+        userOrganismPreference = getMostRecentPreference(userOrganismPreferences)
+        if (userOrganismPreference) {
+            setOtherCurrentOrganismsFalseInDB(userOrganismPreference, user, clientToken)
+            userOrganismPreference.currentOrganism = true
+            userOrganismPreference.save(flush: true, insert: false)
+            return userOrganismPreference
+        }
+
+        //3 - if none at all exist, we should ignore the client token and look it up by the user (missing), saving it for the current client token
+        // we create a new one off of that, but for this client token
+        userOrganismPreferences = UserOrganismPreference.findAllByUserAndCurrentOrganism(user, true, [sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreferences.size() > 1) {
+            log.warn("Multiple preferences found: " + userOrganismPreferences.size())
+            setOtherCurrentOrganismsFalseInDB(getMostRecentPreference(userOrganismPreferences), user, clientToken)
+        }
+        userOrganismPreference = getMostRecentPreference(userOrganismPreferences)
+
+        // just grab an adjacent one for that user, that is not current
+        userOrganismPreference = userOrganismPreference ?: UserOrganismPreference.findByUserAndCurrentOrganism(user, false, [max: 1, sort: "lastUpdated", order: "desc"])
+        if (userOrganismPreference) {
+            Organism organism = userOrganismPreference.organism
+            Sequence sequence = sequenceName ? Sequence.findByNameAndOrganism(sequenceName, organism) : userOrganismPreference.sequence
+            UserOrganismPreference newPreference = new UserOrganismPreference(
+                    user: user
+                    , organism: organism
+                    , currentOrganism: true
+                    , sequence: sequence
+                    , startbp: userOrganismPreference.startbp
+                    , endbp: userOrganismPreference.endbp
+                    , clientToken: clientToken
+            ).save(insert: true, flush: true)
+            return newPreference
+        }
+
+        // 4 - if none at all exist, then we create one
+        if (!userOrganismPreference) {
+            // find a random organism based on sequence
+            Sequence sequence = sequenceName ? Sequence.findByName(sequenceName) : null
+            Set<Organism> organisms = permissionService.getOrganisms(user)
+            Organism organism = null
+            if (sequence) {
+                organism = sequence.organism
+            }
+            if (!organism && organisms) {
+                organism = organisms.first()
+            }
+            if (!organism && permissionService.isAdmin()) {
+                organism = Organism.first()
+            }
+            if (!organism) {
+                throw new PermissionException("User does not have permission for any organisms.")
+            }
+
+//            sequence = sequence ?: Sequence.findByOrganism(organism, [sort: "end", order: "desc", max: 1])
+            sequence = sequence ?: Sequence.findAllByOrganism(organism, [sort: "end", order: "desc", max: 1]).first()
+            UserOrganismPreference newUserOrganismPreference = new UserOrganismPreference(
+                    user: user
+                    , organism: organism
+                    , currentOrganism: true
+                    , sequence: sequence
+                    , clientToken: clientToken
+            )
+            if (sequence) {
+                newUserOrganismPreference.startbp = sequence.start
+                newUserOrganismPreference.endbp = sequence.end
+            }
+            newUserOrganismPreference.save(insert: true, flush: true)
+            return newUserOrganismPreference
+        }
+
+        return userOrganismPreference
+    }
+
+    UserOrganismPreference getCurrentOrganismPreferenceInDB(String token) {
+        getCurrentOrganismPreferenceInDB(permissionService.currentUser, null, token)
+    }
+
+    /**
+     * 1. Find all preferences a month old or more
+     * 2. For each client token shared by a user and more than one organism
+     * 3. Delete the older set of client tokens for each organisms / user combination
+     */
+    def removeStalePreferences() {
+
+        try {
+            log.info "Removing stale preferences"
+            Date lastMonth = new Date().minus(0)
+            int removalCount = 0
+
+            // get user client tokens
+            Map<User, Map<Organism, UserOrganismPreference>> userClientTokens = [:]
+            UserOrganismPreference.findAllByLastUpdatedLessThan(lastMonth, [sort: "lastUpdated", order: "desc"]).each {
+                Map<Organism, UserOrganismPreference> organismPreferenceMap = userClientTokens.containsKey(it.user) ? userClientTokens.get((it.user)) : [:]
+                if (organismPreferenceMap.containsKey(it.organism)) {
+                    UserOrganismPreference preference = organismPreferenceMap.get(it.organism)
+                    // since we are sorting from the newest to the oldest, so just delete the older one
+                    preference.delete()
+                    ++removalCount
+                    organismPreferenceMap.remove(it.organism)
+                } else {
+                    organismPreferenceMap.put(it.organism, it)
+                }
+                userClientTokens.put(it.user, organismPreferenceMap)
+            }
+
+            log.info "Removed ${removalCount} stale preferences"
+        } catch (e) {
+            log.error("Error removing preferences ${e}")
+        }
+
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ProjectionService.groovy b/grails-app/services/org/bbop/apollo/ProjectionService.groovy
new file mode 100644
index 0000000..7e8613c
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ProjectionService.groovy
@@ -0,0 +1,35 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.projection.ReferenceTrack
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+/**
+ * The goal of this track is to create a ReferenceTrack and
+ * very quickly
+ */
+ at Transactional(readOnly = true)
+class ProjectionService {
+
+
+    def createReferenceTrack(JSONObject inputObject) {
+        ReferenceTrack referenceTrack = new ReferenceTrack()
+
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Integer buffer = 0
+        Integer paddingConstant = 20
+        for(int i = 0 ; i < features.size() ;i++){
+            JSONObject feature = features.getJSONObject(i)
+            JSONObject featureLocation = feature.getJSONObject(FeatureStringEnum.LOCATION.value);
+            Integer fmin = featureLocation.getInt(FeatureStringEnum.FMIN.value)
+            Integer fmax = featureLocation.getInt(FeatureStringEnum.FMAX.value)
+            
+        }
+
+
+
+
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ProxyService.groovy b/grails-app/services/org/bbop/apollo/ProxyService.groovy
new file mode 100644
index 0000000..f1c7d11
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ProxyService.groovy
@@ -0,0 +1,68 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+
+ at Transactional(readOnly = true)
+class ProxyService {
+
+//    private final static List<String> defaultProxies = ["http://golr.berkeleybop.org/"]
+    def grailsApplication
+
+    /**
+     * Looks through all proxies to return valid proxies
+     *
+     * @return
+     */
+    Proxy findProxyForUrl(String referenceUrl){
+        Proxy proxy = Proxy.findByReferenceUrlAndActive(referenceUrl,true)
+        if(!proxy){
+            def proxyList = Proxy.findAllByReferenceUrl(referenceUrl,[sort:"fallbackOrder",order:"asc"])
+            if(proxyList){
+                return proxyList.first()
+            }
+        }
+        return proxy
+    }
+
+    @Transactional
+    def initProxies(){
+        def proxies = grailsApplication.config.apollo.proxies
+
+        for(proxyConfig in proxies){
+            def proxy = Proxy.findByReferenceUrlAndTargetUrl(proxyConfig.referenceUrl,proxyConfig.targetUrl)
+
+            if(!proxy){
+                proxy = new Proxy(
+                        referenceUrl: proxyConfig.referenceUrl
+                        , targetUrl: proxyConfig.targetUrl
+                        ,active: proxyConfig.active
+                        ,fallbackOrder: proxyConfig.fallbackOrder
+                ).save(failOnError: false,insert: true)
+            }
+            else
+            if (proxyConfig.replace) {
+                active: proxyConfig.active
+                fallbackOrder: proxyConfig.fallbackOrder
+                proxy.save(failOnError: false,insert: false)
+            }
+        }
+    }
+
+    /**
+     *
+     * @param urlProxy
+     * @return
+     */
+    @Transactional
+    Proxy findDefaultProxy(String urlProxy) {
+        if(defaultProxies.contains(urlProxy)){
+            Proxy proxy = new Proxy(
+                    referenceUrl: urlProxy
+                    ,targetUrl: urlProxy
+                    ,active: true
+            ).save(failOnError: true)
+            return proxy
+        }
+        return null
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/ReportService.groovy b/grails-app/services/org/bbop/apollo/ReportService.groovy
new file mode 100644
index 0000000..dcd35ab
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/ReportService.groovy
@@ -0,0 +1,222 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.report.AnnotatorSummary
+import org.bbop.apollo.report.OrganismPermissionSummary
+import org.bbop.apollo.report.OrganismSummary
+import org.bbop.apollo.report.SequenceSummary
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.parser.JSONParser
+
+ at Transactional
+class ReportService {
+
+    def permissionService
+
+    def generateAllFeatureSummary() {
+        OrganismSummary thisFeatureSummaryInstance = new OrganismSummary()
+        thisFeatureSummaryInstance.geneCount = Gene.count
+
+
+        Map<String, Integer> transcriptMap = new TreeMap<>()
+        Transcript.executeQuery("select distinct g from Transcript g ").each {
+            String className = it.class.canonicalName.substring("org.bbop.apollo.".size())
+            Integer count = transcriptMap.get(className) ?: 0
+            transcriptMap.put(className, ++count)
+        }
+        thisFeatureSummaryInstance.transcriptTypeCount = transcriptMap
+        if (transcriptMap) {
+            thisFeatureSummaryInstance.transcriptCount = transcriptMap.values()?.sum()
+        } else {
+            thisFeatureSummaryInstance.transcriptCount = 0
+        }
+
+        thisFeatureSummaryInstance.transcriptCount = Transcript.count
+        thisFeatureSummaryInstance.transposableElementCount = TransposableElement.count
+        thisFeatureSummaryInstance.repeatRegionCount = RepeatRegion.count
+        thisFeatureSummaryInstance.exonCount = Exon.count
+        thisFeatureSummaryInstance.sequenceCount = Sequence.count
+
+
+
+        return thisFeatureSummaryInstance
+    }
+
+    def generateOrganismSummary(Organism organism) {
+        OrganismSummary thisFeatureSummaryInstance = new OrganismSummary()
+        thisFeatureSummaryInstance.geneCount = (int) Gene.executeQuery("select count(distinct g) from Gene g join g.featureLocations fl join fl.sequence s join s.organism o where o = :organism", [organism: organism]).iterator().next()
+
+        thisFeatureSummaryInstance.annotators = User.executeQuery("select distinct own from Feature g join g.featureLocations fl join fl.sequence s join s.organism o join g.owners own where o = :organism", [organism: organism])
+
+
+        Map<String, Integer> transcriptMap = new TreeMap<>()
+        Transcript.executeQuery("select distinct g from Transcript g join g.featureLocations fl join fl.sequence s join s.organism o where o = :organism", [organism: organism]).each {
+            String className = it.class.canonicalName.substring("org.bbop.apollo.".size())
+            Integer count = transcriptMap.get(className) ?: 0
+            transcriptMap.put(className, ++count)
+        }
+        thisFeatureSummaryInstance.transcriptTypeCount = transcriptMap
+        if (transcriptMap) {
+            thisFeatureSummaryInstance.transcriptCount = transcriptMap.values()?.sum()
+        } else {
+            thisFeatureSummaryInstance.transcriptCount = 0
+        }
+        thisFeatureSummaryInstance.sequenceCount = Sequence.countByOrganism(organism)
+        thisFeatureSummaryInstance.organismId = organism.id
+
+        thisFeatureSummaryInstance.transposableElementCount = (int) TransposableElement.executeQuery("select count(distinct g) from TransposableElement g join g.featureLocations fl join fl.sequence s join s.organism o where o = :organism", [organism: organism]).iterator().next()
+        thisFeatureSummaryInstance.repeatRegionCount = (int) RepeatRegion.executeQuery("select count(distinct g) from RepeatRegion g join g.featureLocations fl join fl.sequence s join s.organism o where o = :organism", [organism: organism]).iterator().next()
+        thisFeatureSummaryInstance.exonCount = (int) Exon.executeQuery("select count(distinct g) from Exon g join g.featureLocations fl join fl.sequence s join s.organism o where o = :organism", [organism: organism]).iterator().next()
+        return thisFeatureSummaryInstance
+
+    }
+
+
+    def generateSequenceSummary(Sequence sequence) {
+        SequenceSummary sequenceSummary = new SequenceSummary()
+        sequenceSummary.sequence = sequence
+        sequenceSummary.geneCount = (int) Gene.executeQuery("select count(g) from Gene g join g.featureLocations fl join fl.sequence s where s = :sequence ", [sequence: sequence]).iterator().next()
+        sequenceSummary.transposableElementCount = (int) TransposableElement.executeQuery("select count(g) from TransposableElement  g join g.featureLocations fl join fl.sequence s where s = :sequence ", [sequence: sequence]).iterator().next()
+        sequenceSummary.repeatRegionCount = (int) RepeatRegion.executeQuery("select count(g) from RepeatRegion  g join g.featureLocations fl join fl.sequence s where s = :sequence ", [sequence: sequence]).iterator().next()
+        sequenceSummary.exonCount = (int) Exon.executeQuery("select count(g) from Exon g join g.featureLocations fl join fl.sequence s where s = :sequence ", [sequence: sequence]).iterator().next()
+        sequenceSummary.annotators = User.executeQuery("select distinct annotator from Feature g join g.featureLocations fl join fl.sequence s join g.owners annotator where s = :sequence ", [sequence: sequence])
+
+
+        Map<String, Integer> transcriptMap = new TreeMap<>()
+        Transcript.executeQuery("select distinct g from Transcript g join g.featureLocations fl join fl.sequence s  where s = :sequence", [sequence: sequence]).each {
+            String className = it.class.canonicalName.substring("org.bbop.apollo.".size())
+            Integer count = transcriptMap.get(className) ?: 0
+            transcriptMap.put(className, ++count)
+        }
+        sequenceSummary.transcriptTypeCount = transcriptMap
+        if (transcriptMap) {
+            sequenceSummary.transcriptCount = transcriptMap.values()?.sum()
+        } else {
+            sequenceSummary.transcriptCount = 0
+        }
+
+
+        return sequenceSummary
+    }
+
+    def copyProperties(source, target) {
+        source.properties.each { key, value ->
+            if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) {
+                try {
+                    target[key] = value
+                }
+                catch (ReadOnlyPropertyException rpo) {
+                }
+            }
+        }
+    }
+
+    AnnotatorSummary generateAnnotatorSummary(User owner, boolean includePermissions = false) {
+        AnnotatorSummary annotatorSummary = new AnnotatorSummary()
+        annotatorSummary.annotator = owner
+        annotatorSummary.geneCount = (int) Gene.executeQuery("select count(distinct g) from Gene g join g.owners owner where owner = :owner ", [owner: owner]).iterator().next()
+        annotatorSummary.transposableElementCount = (int) TransposableElement.executeQuery("select count(distinct g) from TransposableElement g join g.owners owner where owner = :owner", [owner: owner]).iterator().next()
+        annotatorSummary.repeatRegionCount = (int) TransposableElement.executeQuery("select count(distinct g) from RepeatRegion g join g.owners owner where owner = :owner", [owner: owner]).iterator().next()
+        annotatorSummary.exonCount = (int) TransposableElement.executeQuery("select count(distinct g) from Exon g join g.childFeatureRelationships child join child.parentFeature.owners owner where owner = :owner", [owner: owner]).iterator().next()
+
+
+        Map<String, Integer> transcriptMap = new TreeMap<>()
+        Transcript.executeQuery("select distinct g from Transcript g join g.owners owner where owner = :owner ", [owner: owner]).each {
+            String className = it.class.canonicalName.substring("org.bbop.apollo.".size())
+            Integer count = transcriptMap.get(className) ?: 0
+            transcriptMap.put(className, ++count)
+        }
+        annotatorSummary.transcriptTypeCount = transcriptMap
+        if (transcriptMap) {
+            annotatorSummary.transcriptCount = transcriptMap.values()?.sum()
+        } else {
+            annotatorSummary.transcriptCount = 0
+        }
+
+        // TODO: add groups as well
+        if (includePermissions && !permissionService.isUserAdmin(owner)) {
+
+            List<OrganismPermissionSummary> userOrganismPermissionList = new ArrayList<>()
+            if (permissionService.isUserAdmin(owner)) {
+                Organism.listOrderByCommonName().each {
+                    OrganismPermissionSummary organismPermissionSummary = new OrganismPermissionSummary()
+                    UserOrganismPermission userOrganismPermission = new UserOrganismPermission()
+                    userOrganismPermission.permissions = [PermissionEnum.ADMINISTRATE, PermissionEnum.EXPORT, PermissionEnum.READ, PermissionEnum.WRITE]
+                    userOrganismPermission.organism = it
+                    organismPermissionSummary.userOrganismPermission = userOrganismPermission
+                    copyProperties(generateOrganismSummary(it), organismPermissionSummary)
+                    userOrganismPermissionList.add(organismPermissionSummary)
+                }
+            } else {
+                UserOrganismPermission.findAllByUser(owner).each {
+                    OrganismPermissionSummary organismPermissionSummary = new OrganismPermissionSummary()
+                    organismPermissionSummary.userOrganismPermission = it
+                    copyProperties(generateOrganismSummary(it.organism), organismPermissionSummary)
+
+                    userOrganismPermissionList.add(organismPermissionSummary)
+                }
+            }
+
+            owner.userGroups.each { group ->
+                for (GroupOrganismPermission groupPermission in GroupOrganismPermission.findAllByGroup(group)) {
+                    // minimally, you should have at least one permission
+                    if (groupPermission.permissions) {
+                        OrganismPermissionSummary organismPermissionSummary = new OrganismPermissionSummary()
+                        UserOrganismPermission userOrganismPermission = new UserOrganismPermission()
+                        userOrganismPermission.permissions = groupPermission.permissions
+                        userOrganismPermission.organism = groupPermission.organism
+                        organismPermissionSummary.userOrganismPermission = userOrganismPermission
+                        copyProperties(generateOrganismSummary(groupPermission.organism), organismPermissionSummary)
+                        userOrganismPermissionList.add(organismPermissionSummary)
+                    }
+                }
+
+            }
+
+            annotatorSummary.userOrganismPermissionList = mergePermissions(userOrganismPermissionList)
+        }
+
+
+
+        return annotatorSummary
+    }
+
+    List<OrganismPermissionSummary> mergePermissions(ArrayList<OrganismPermissionSummary> organismPermissionSummaries) {
+        Map<String,OrganismPermissionSummary> map = new TreeMap<>()
+
+        organismPermissionSummaries.each {
+            String organismName = it.userOrganismPermission.organism.commonName
+            OrganismPermissionSummary organismPermissionSummary = null
+            if(map.containsKey(organismName)){
+                organismPermissionSummary = map.get(organismName)
+                String finalPermissions = mergePermissionStrings(organismPermissionSummary.userOrganismPermission.permissions,it.userOrganismPermission.permissions)
+                organismPermissionSummary.userOrganismPermission.permissions = finalPermissions
+            }
+            else{
+                organismPermissionSummary = it
+            }
+            map.put(organismName,organismPermissionSummary)
+        }
+
+        return map.values() as List
+    }
+
+    String mergePermissionStrings(String s1, String s2) {
+        Set<String> permissions = new TreeSet<>()
+        def array1 = JSON.parse(s1) as JSONArray
+        def array2 = JSON.parse(s2) as JSONArray
+        for(int i = 0 ; i < array1.size() ; i++){
+            permissions.add(array1.getString(i))
+        }
+        for(int i = 0 ; i < array2.size() ; i++){
+            permissions.add(array2.getString(i))
+        }
+        JSONArray returnArray = new JSONArray()
+        permissions.each {
+            returnArray.add(it)
+        }
+        return returnArray.toString()
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/RequestHandlingService.groovy b/grails-app/services/org/bbop/apollo/RequestHandlingService.groovy
new file mode 100644
index 0000000..45b70f6
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/RequestHandlingService.groovy
@@ -0,0 +1,2360 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.bbop.apollo.event.AnnotationEvent
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import org.bbop.apollo.history.FeatureOperation
+
+//import grails.compiler.GrailsCompileStatic
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.grails.plugins.metrics.groovy.Timed
+import org.hibernate.FetchMode
+
+/**
+ * This class is responsible for handling JSON requests from the AnnotationEditorController and routing
+ * to the proper service classes.
+ *
+ * It's goal is to replace a a lot of the layers in AnnotationEditorController
+ *
+ * Furthermore, this handles requests for websocket, which come in via a different mechanism than the controller
+ */
+ at Transactional
+class RequestHandlingService {
+
+    public static String REST_SEQUENCE_ALTERNATION_EVENT = "sequenceAlterationEvent"
+
+    def featureService
+    def featureRelationshipService
+    def transcriptService
+    def cdsService
+    def exonService
+    def nonCanonicalSplitSiteService
+    def configWrapperService
+    def nameService
+    def permissionService
+    def preferenceService
+    def featurePropertyService
+    def featureEventService
+    def brokerMessagingTemplate
+
+
+    public static final List<String> viewableAnnotationFeatureList = [
+            RepeatRegion.class.name,
+            TransposableElement.class.name
+    ]
+    public static final List<String> viewableAnnotationTranscriptParentList = [
+            Gene.class.name,
+            Pseudogene.class.name
+    ]
+
+    public static final List<String> viewableAnnotationTranscriptList = [
+            Transcript.class.name,
+            MRNA.class.name,
+            TRNA.class.name,
+            SnRNA.class.name,
+            SnoRNA.class.name,
+            NcRNA.class.name,
+            RRNA.class.name,
+            MiRNA.class.name,
+    ]
+
+    public static final List<String> viewableAlterations = [
+            Deletion.class.name,
+            Insertion.class.name,
+            Substitution.class.name
+    ]
+
+    public static
+    final List<String> viewableAnnotationList = viewableAnnotationFeatureList + viewableAnnotationTranscriptParentList
+    public static
+    final List<String> viewableAnnotationTypesList = viewableAnnotationFeatureList + viewableAnnotationTranscriptList + viewableAnnotationTranscriptParentList
+
+    private String underscoreToCamelCase(String underscore) {
+        if (!underscore || underscore.isAllWhitespace()) {
+            return ''
+        }
+        return underscore.replaceAll(/_\w/) { it[1].toUpperCase() }
+    }
+
+
+    JSONObject setSymbol(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Sequence sequence = null
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            String symbolString = jsonFeature.getString(FeatureStringEnum.SYMBOL.value)
+            sequence = sequence ?: feature.getFeatureLocation().getSequence()
+            permissionService.checkPermissions(inputObject, sequence.organism, PermissionEnum.WRITE)
+
+            feature.symbol = symbolString
+            feature.save(flush: true, failOnError: true)
+
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+        }
+
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return new JSONObject()
+    }
+
+    JSONObject setDescription(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = null
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            String descriptionString = jsonFeature.getString(FeatureStringEnum.DESCRIPTION.value)
+            sequence = sequence ?: feature.getFeatureLocation().getSequence()
+            permissionService.checkPermissions(inputObject, sequence.organism, PermissionEnum.WRITE)
+
+
+            feature.description = descriptionString
+            feature.save(flush: true, failOnError: true)
+
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature))
+        }
+
+        return updateFeatureContainer
+    }
+
+    private JSONObject wrapFeature(JSONObject jsonObject, Feature feature) {
+
+        // only pass in transcript
+        if (feature instanceof Gene) {
+            feature.parentFeatureRelationships.childFeature.each { childFeature ->
+                jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(childFeature))
+            }
+        } else {
+            jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature))
+        }
+
+        return jsonObject
+    }
+
+    def deleteNonPrimaryDbxrefs(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            JSONArray dbXrefJSONArray = jsonFeature.getJSONArray(FeatureStringEnum.DBXREFS.value)
+
+            for (int j = 0; j < dbXrefJSONArray.size(); j++) {
+                JSONObject dbXfrefJsonObject = dbXrefJSONArray.getJSONObject(j)
+                String dbString = dbXfrefJsonObject.getString(FeatureStringEnum.DB.value)
+                String accessionString = dbXfrefJsonObject.getString(FeatureStringEnum.ACCESSION.value)
+                DB db = DB.findByName(dbString)
+                if (db) {
+                    DBXref dbXref = DBXref.findByAccessionAndDb(accessionString, db)
+                    if (dbXref) {
+                        feature.removeFromFeatureDBXrefs(dbXref)
+                        feature.save(failOnError: true)
+                    }
+                }
+            }
+
+            feature.save(flush: true, failOnError: true)
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+        }
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+
+    }
+
+    /**
+     * @return
+     */
+    def updateNonPrimaryDbxrefs(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            log.debug "feature: ${jsonFeature.getJSONArray(FeatureStringEnum.OLD_DBXREFS.value)}"
+            JSONObject oldDbXrefJSONObject = jsonFeature.getJSONArray(FeatureStringEnum.OLD_DBXREFS.value).getJSONObject(0)
+            JSONObject newDbXrefJSONObject = jsonFeature.getJSONArray(FeatureStringEnum.NEW_DBXREFS.value).getJSONObject(0)
+
+            String dbString = oldDbXrefJSONObject.getString(FeatureStringEnum.DB.value)
+            log.debug "dbString: ${dbString}"
+            String accessionString = oldDbXrefJSONObject.getString(FeatureStringEnum.ACCESSION.value)
+            log.debug "accessionString : ${accessionString}"
+            DB db = DB.findByName(dbString)
+            if (!db) {
+                db = new DB(name: dbString).save()
+            }
+            DBXref oldDbXref = DBXref.findByAccessionAndDb(accessionString, db)
+
+            if (!oldDbXref) {
+                log.error("could not find original dbxref: " + oldDbXrefJSONObject)
+            }
+
+            oldDbXref.db = DB.findOrSaveByName(newDbXrefJSONObject.getString(FeatureStringEnum.DB.value))
+            oldDbXref.accession = newDbXrefJSONObject.getString(FeatureStringEnum.ACCESSION.value)
+
+            oldDbXref.save(flush: true, failOnError: true)
+
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+        }
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+
+
+    }
+
+    /**
+     * For each feature add the list of comments
+     * @param inputObject
+     */
+    def addComments(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            JSONArray commentsArray = jsonFeature.getJSONArray(FeatureStringEnum.COMMENTS.value)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+
+            for (int commentIndex = 0; commentIndex < commentsArray.size(); commentIndex++) {
+                String commentString = commentsArray.getString(commentIndex);
+                Comment comment = new Comment(value: commentString, feature: feature).save()
+                featurePropertyService.addComment(feature, comment)
+            }
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+
+        }
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+    }
+
+    def deleteComments(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            JSONArray commentsArray = jsonFeature.getJSONArray(FeatureStringEnum.COMMENTS.value)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+
+            for (int commentIndex = 0; commentIndex < commentsArray.size(); commentIndex++) {
+                String commentString = commentsArray.getString(commentIndex);
+                featurePropertyService.deleteComment(feature, commentString)
+            }
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+
+        }
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+    }
+
+    def updateComments(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            JSONArray oldComments = jsonFeature.getJSONArray(FeatureStringEnum.OLD_COMMENTS.value)
+            JSONArray newComments = jsonFeature.getJSONArray(FeatureStringEnum.NEW_COMMENTS.value)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+
+            for (int commentIndex = 0; commentIndex < oldComments.size(); commentIndex++) {
+                String oldCommentString = oldComments.getString(commentIndex)
+                String newCommentString = newComments.getString(commentIndex)
+
+                Comment comment = Comment.findByFeatureAndValue(feature, oldCommentString)
+                comment.value = newCommentString
+                comment.save()
+            }
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+
+        }
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+        return updateFeatureContainer
+    }
+
+    def setStatus(JSONObject inputObject) {
+        log.debug "status being set ${inputObject as JSON}"
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            String statusString = jsonFeature.getString(FeatureStringEnum.STATUS.value)
+            AvailableStatus availableStatus = AvailableStatus.findByValue(statusString)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            if (availableStatus) {
+                Status status = new Status(
+                        value: availableStatus.value
+                        , feature: feature
+                ).save()
+                feature.status = status
+                feature.save()
+            }
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+
+        }
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+        return updateFeatureContainer
+    }
+
+    /**
+     * being invoked the RHS
+     */
+    def deleteStatus(JSONObject inputObject) {
+        log.debug "deleteStatus ${inputObject as JSON}"
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            String statusString = jsonFeature.getString(FeatureStringEnum.STATUS.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            feature.status = null
+            feature.save()
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+
+        }
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+        return updateFeatureContainer
+    }
+
+
+    def getComments(JSONObject inputObject) {
+        log.debug "getComments"
+        JSONObject featureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.READ)
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            JSONArray commentsArray = new JSONArray()
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+
+            for (Comment comment in featurePropertyService.getComments(feature)) {
+                String commentString = comment.value
+                commentsArray.put(commentString)
+            }
+            jsonFeature.put(FeatureStringEnum.COMMENTS.value, commentsArray)
+        }
+        return featureContainer
+
+    }
+
+    def addNonPrimaryDbxrefs(JSONObject inputObject) {
+        log.debug "addNonPrimaryDbxrefs"
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            log.debug "feature: ${jsonFeature.getJSONArray(FeatureStringEnum.DBXREFS.value)}"
+            JSONArray dbXrefJSONArray = jsonFeature.getJSONArray(FeatureStringEnum.DBXREFS.value)
+
+            for (int j = 0; j < dbXrefJSONArray.size(); j++) {
+                JSONObject dbXfrefJsonObject = dbXrefJSONArray.getJSONObject(j)
+                log.debug "innerArray ${j}: ${dbXfrefJsonObject}"
+                String dbString = dbXfrefJsonObject.getString(FeatureStringEnum.DB.value)
+                log.debug "dbString: ${dbString}"
+                String accessionString = dbXfrefJsonObject.getString(FeatureStringEnum.ACCESSION.value)
+                log.debug "accessionString : ${accessionString}"
+                featureService.addNonPrimaryDbxrefs(feature, dbString, accessionString)
+            }
+            feature.save(flush: true, failOnError: true)
+
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+        }
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.ADD
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+
+
+    }
+
+    JSONObject setName(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            if (!sequence) sequence = feature.getFeatureLocation().getSequence()
+            feature.name = jsonFeature.get(FeatureStringEnum.NAME.value)
+
+
+            feature.save(flush: true, failOnError: true)
+
+            updateFeatureContainer = wrapFeature(updateFeatureContainer, feature)
+        }
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: updateFeatureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return updateFeatureContainer
+    }
+
+    @Timed
+    @Transactional(readOnly = true)
+    JSONObject getFeatures(JSONObject inputObject) {
+        long start = System.currentTimeMillis()
+
+        String sequenceName = permissionService.getSequenceNameFromInput(inputObject)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.READ)
+        if (sequenceName != sequence.name) {
+            sequence = Sequence.findByNameAndOrganism(sequenceName, sequence.organism)
+            preferenceService.setCurrentSequence(permissionService.getCurrentUser(inputObject), sequence, inputObject.getString(FeatureStringEnum.CLIENT_TOKEN.value))
+        }
+        log.debug "getFeatures for organism -> ${sequence.organism.commonName} and ${sequence.name}"
+
+        def features = Feature.createCriteria().listDistinct {
+            featureLocations {
+                eq('sequence', sequence)
+            }
+            fetchMode 'owners', FetchMode.JOIN
+            fetchMode 'featureLocations', FetchMode.JOIN
+            fetchMode 'featureLocations.sequence', FetchMode.JOIN
+            fetchMode 'featureProperties', FetchMode.JOIN
+            fetchMode 'featureDBXrefs', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships', FetchMode.JOIN
+            fetchMode 'childFeatureRelationships', FetchMode.JOIN
+            fetchMode 'childFeatureRelationships.parentFeature', FetchMode.JOIN
+            fetchMode 'childFeatureRelationships.parentFeature.featureLocations', FetchMode.JOIN
+            fetchMode 'childFeatureRelationships.parentFeature.featureLocations.sequence', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.parentFeature', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.parentFeature.featureLocations', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.parentFeature.featureLocations.sequence', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.parentFeatureRelationships', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.childFeatureRelationships', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.featureLocations', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.featureLocations.sequence', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.featureProperties', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.featureDBXrefs', FetchMode.JOIN
+            fetchMode 'parentFeatureRelationships.childFeature.owners', FetchMode.JOIN
+            'in'('class', viewableAnnotationTranscriptList + viewableAnnotationFeatureList)
+        }
+
+
+        JSONArray jsonFeatures = new JSONArray()
+        features.each { feature ->
+            JSONObject jsonObject = featureService.convertFeatureToJSON(feature, false)
+            jsonFeatures.put(jsonObject)
+        }
+
+        inputObject.put(AnnotationEditorController.REST_FEATURES, jsonFeatures)
+        log.debug "getFeatures ${System.currentTimeMillis() - start}ms"
+        return inputObject
+
+    }
+
+    /**
+     * First feature is transcript, and the rest must be exons to add
+     * @param inputObject
+     * @return
+     */
+    @Timed
+    JSONObject addExon(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        String uniqueName = features.getJSONObject(0).getString(FeatureStringEnum.UNIQUENAME.value)
+        Transcript transcript = Transcript.findByUniqueName(uniqueName)
+        JSONObject oldJsonObject = featureService.convertFeatureToJSON(transcript)
+
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 1; i < features.length(); ++i) {
+            JSONObject jsonExon = features.getJSONObject(i);
+            // could be that this is null
+            Exon gsolExon = (Exon) featureService.convertJSONToFeature(jsonExon, sequence)
+
+            featureService.updateNewGsolFeatureAttributes(gsolExon, sequence);
+
+            if (gsolExon.getFmin() < 0 || gsolExon.getFmax() < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates")
+            }
+
+            transcriptService.addExon(transcript, gsolExon, false)
+
+            gsolExon.save()
+        }
+        featureService.removeExonOverlapsAndAdjacencies(transcript)
+        transcriptService.updateGeneBoundaries(transcript)
+        featureService.calculateCDS(transcript)
+
+        transcript.save(flush: true)
+        transcript.attach()
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+        transcript.save(flush: true)
+
+        // TODO: one of these two versions . . .
+        JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript, false)
+        JSONObject returnObject = createJSONFeatureContainer(newJsonObject)
+
+        Gene gene = transcriptService.getGene(transcript)
+
+        featureEventService.addNewFeatureEvent(FeatureOperation.ADD_EXON, gene.name, transcript.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: returnObject
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return returnObject
+
+    }
+
+    @Timed
+    JSONObject addTranscript(JSONObject inputObject) throws Exception {
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject returnObject = createJSONFeatureContainer()
+
+        log.info "addTranscript ${inputObject?.size()}"
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        log.debug "sequence: ${sequence}"
+        log.debug "organism: ${sequence.organism}"
+        log.info "number of features: ${featuresArray?.size()}"
+
+        boolean useName = false
+        boolean useCDS = configWrapperService.useCDS()
+        boolean suppressHistory = false
+        boolean suppressEvents = false
+
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_HISTORY.value)) {
+            suppressHistory = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_HISTORY.value)
+        }
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_EVENTS.value)) {
+            suppressEvents = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_EVENTS.value)
+        }
+
+        List<Transcript> transcriptList = new ArrayList<>()
+        def transcriptJSONList = []
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonTranscript = featuresArray.getJSONObject(i)
+            jsonTranscript = permissionService.copyRequestValues(inputObject, jsonTranscript)
+            if (jsonTranscript.has(FeatureStringEnum.USE_CDS.value)) {
+                useCDS = jsonTranscript.getBoolean(FeatureStringEnum.USE_CDS.value)
+            }
+            useName = jsonTranscript.has(FeatureStringEnum.USE_NAME.value) ? jsonTranscript.getBoolean(FeatureStringEnum.USE_NAME.value) : false
+            Transcript transcript = featureService.generateTranscript(jsonTranscript, sequence, suppressHistory, useCDS, useName)
+
+            // should automatically write to history
+            transcript.save(flush: true)
+            transcriptList.add(transcript)
+
+            Gene gene = transcriptService.getGene(transcript)
+            inputObject.put(FeatureStringEnum.NAME.value, gene.name)
+
+            if (!suppressHistory) {
+                def json = featureService.convertFeatureToJSON(transcript)
+                featureEventService.addNewFeatureEventWithUser(FeatureOperation.ADD_TRANSCRIPT, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, json, permissionService.getCurrentUser(inputObject))
+                transcriptJSONList += json
+            }
+        }
+
+
+        returnObject.put(FeatureStringEnum.FEATURES.value, transcriptJSONList as JSONArray)
+
+        if (!suppressEvents) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: returnObject
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.ADD
+            )
+
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return returnObject
+    }
+
+    /**
+     * Transcript is the first object
+     * @param inputObject
+     */
+    @Timed
+    JSONObject setTranslationStart(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject transcriptJSONObject = features.getJSONObject(0);
+
+        Transcript transcript = Transcript.findByUniqueName(transcriptJSONObject.getString(FeatureStringEnum.UNIQUENAME.value))
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        boolean setStart = transcriptJSONObject.has(FeatureStringEnum.LOCATION.value);
+        if (!setStart) {
+            CDS cds = transcriptService.getCDS(transcript)
+            cdsService.setManuallySetTranslationStart(cds, false)
+            featureService.calculateCDS(transcript)
+        } else {
+            JSONObject jsonCDSLocation = transcriptJSONObject.getJSONObject(FeatureStringEnum.LOCATION.value);
+            featureService.setTranslationStart(transcript, jsonCDSLocation.getInt(FeatureStringEnum.FMIN.value), true)
+        }
+
+        transcript.save()
+
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        Gene gene = transcriptService.getGene(transcript)
+        JSONObject newJSONObject = featureService.convertFeatureToJSON(transcript, false)
+        featureEventService.addNewFeatureEvent(setStart ? FeatureOperation.SET_TRANSLATION_START : FeatureOperation.UNSET_TRANSLATION_START, gene.name, transcript.uniqueName, inputObject, transcriptJSONObject, newJSONObject, permissionService.getCurrentUser(inputObject))
+        JSONObject featureContainer = createJSONFeatureContainer(newJSONObject);
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return featureContainer
+    }
+
+    /**
+     * Transcript is the first object
+     * @param inputObject
+     */
+    @Timed
+    JSONObject setTranslationEnd(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject transcriptJSONObject = features.getJSONObject(0);
+
+        Transcript transcript = Transcript.findByUniqueName(transcriptJSONObject.getString(FeatureStringEnum.UNIQUENAME.value))
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        boolean setEnd = transcriptJSONObject.has(FeatureStringEnum.LOCATION.value);
+        if (!setEnd) {
+            CDS cds = transcriptService.getCDS(transcript)
+            cdsService.setManuallySetTranslationEnd(cds, false)
+            featureService.calculateCDS(transcript)
+        } else {
+            JSONObject jsonCDSLocation = transcriptJSONObject.getJSONObject(FeatureStringEnum.LOCATION.value);
+            //featureService.setTranslationEnd(transcript, jsonCDSLocation.getInt(FeatureStringEnum.FMAX.value), true)
+            //TODO: Should translationStart be allowed to be set automatically?
+            featureService.setTranslationEnd(transcript, jsonCDSLocation.getInt(FeatureStringEnum.FMAX.value))
+        }
+        transcript.save()
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject newJSONObject = featureService.convertFeatureToJSON(transcript, false)
+        featureEventService.addNewFeatureEvent(setEnd ? FeatureOperation.SET_TRANSLATION_END : FeatureOperation.UNSET_TRANSLATION_END, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, transcriptJSONObject, newJSONObject, permissionService.getCurrentUser(inputObject))
+        JSONObject featureContainer = createJSONFeatureContainer(newJSONObject);
+
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return featureContainer
+    }
+
+    @Timed
+    def setReadthroughStopCodon(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject transcriptJSONObject = features.getJSONObject(0)
+
+        Transcript transcript = Transcript.findByUniqueName(transcriptJSONObject.getString(FeatureStringEnum.UNIQUENAME.value))
+        JSONObject oldJsonObject = featureService.convertFeatureToJSON(transcript, false)
+
+        boolean readThroughStopCodon = transcriptJSONObject.getBoolean(FeatureStringEnum.READTHROUGH_STOP_CODON.value)
+        featureService.calculateCDS(transcript, readThroughStopCodon);
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        transcript.save(flush: true)
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject featureContainer = createJSONFeatureContainer(featureService.convertFeatureToJSON(transcript, false))
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+        JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript, false)
+        featureEventService.addNewFeatureEvent(readThroughStopCodon ? FeatureOperation.SET_READTHROUGH_STOP_CODON : FeatureOperation.UNSET_READTHROUGH_STOP_CODON, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+        JSONObject returnObject = createJSONFeatureContainer(newJsonObject);
+
+        return returnObject
+    }
+
+    @Timed
+    def setAcceptor(JSONObject inputObject, boolean upstreamDonor) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONObject featureContainer = createJSONFeatureContainer()
+        JSONArray transcriptArray = new JSONArray()
+        featureContainer.put(FeatureStringEnum.FEATURES.value, transcriptArray)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject oldJsonObject = features.getJSONObject(i)
+            String uniqueName = oldJsonObject.getString(FeatureStringEnum.UNIQUENAME.value)
+            Exon exon = Exon.findByUniqueName(uniqueName)
+            Transcript transcript = exonService.getTranscript(exon)
+
+            if (upstreamDonor) {
+                exonService.setToUpstreamAcceptor(exon)
+            } else {
+                exonService.setToDownstreamAcceptor(exon)
+            }
+
+            featureService.calculateCDS(transcript)
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+            transcript.save()
+            def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+            if (transcriptsToUpdate.size() > 0) {
+                JSONObject updateFeatureContainer = createJSONFeatureContainer()
+                transcriptsToUpdate.each {
+                    updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+                }
+                if (sequence) {
+                    AnnotationEvent annotationEvent = new AnnotationEvent(
+                            features: updateFeatureContainer,
+                            sequence: sequence,
+                            operation: AnnotationEvent.Operation.UPDATE
+                    )
+                    fireAnnotationEvent(annotationEvent)
+                }
+            }
+
+            JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript)
+            transcriptArray.add(newJsonObject)
+            featureEventService.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+        }
+
+
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return featureContainer
+    }
+
+
+    @Timed
+    def setDonor(JSONObject inputObject, boolean upstreamDonor) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONObject featureContainer = createJSONFeatureContainer();
+        JSONArray transcriptArray = new JSONArray()
+        featureContainer.put(FeatureStringEnum.FEATURES.value, transcriptArray)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject oldJsonObject = features.getJSONObject(i)
+            String uniqueName = oldJsonObject.getString(FeatureStringEnum.UNIQUENAME.value);
+            Exon exon = Exon.findByUniqueName(uniqueName)
+            Transcript transcript = exonService.getTranscript(exon)
+            if (upstreamDonor) {
+                exonService.setToUpstreamDonor(exon)
+            } else {
+                exonService.setToDownstreamDonor(exon)
+            }
+
+
+            featureService.calculateCDS(transcript)
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+            transcript.save()
+            def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+            if (transcriptsToUpdate.size() > 0) {
+                JSONObject updateFeatureContainer = createJSONFeatureContainer()
+                transcriptsToUpdate.each {
+                    updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+                }
+                if (sequence) {
+                    AnnotationEvent annotationEvent = new AnnotationEvent(
+                            features: updateFeatureContainer,
+                            sequence: sequence,
+                            operation: AnnotationEvent.Operation.UPDATE
+                    )
+                    fireAnnotationEvent(annotationEvent)
+                }
+            }
+
+            JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript)
+            transcriptArray.add(newJsonObject)
+            featureEventService.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+        }
+
+
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return featureContainer
+    }
+
+    @Timed
+    JSONObject setLongestOrf(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject transcriptJSONObject = features.getJSONObject(0);
+
+        Transcript transcript = Transcript.findByUniqueName(transcriptJSONObject.getString(FeatureStringEnum.UNIQUENAME.value))
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        featureService.setLongestORF(transcript, false)
+
+        transcript.save(flush: true, insert: false)
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject featureContainer = createJSONFeatureContainer(featureService.convertFeatureToJSON(transcript, false));
+
+        if (sequence) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.UPDATE
+            )
+            fireAnnotationEvent(annotationEvent)
+        }
+
+        return featureContainer
+    }
+
+    /**
+     * TODO: test in interface
+     * @param inputObject
+     * @return
+     */
+    @Timed
+    JSONObject setExonBoundaries(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONObject returnObject = createJSONFeatureContainer()
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject locationCommand = features.getJSONObject(i);
+            if (!locationCommand.has(FeatureStringEnum.LOCATION.value)) {
+                continue;
+            }
+            JSONObject jsonLocation = locationCommand.getJSONObject(FeatureStringEnum.LOCATION.value);
+            int fmin = jsonLocation.getInt(FeatureStringEnum.FMIN.value);
+            int fmax = jsonLocation.getInt(FeatureStringEnum.FMAX.value);
+            if (fmin < 0 || fmax < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates");
+            }
+            Exon exon = Exon.findByUniqueName(locationCommand.getString(FeatureStringEnum.UNIQUENAME.value))
+            Transcript transcript = exonService.getTranscript(exon)
+            JSONObject oldTranscriptJsonObject = featureService.convertFeatureToJSON(transcript)
+
+
+            FeatureLocation transcriptFeatureLocation = FeatureLocation.findByFeature(transcript)
+            FeatureLocation exonFeatureLocation = FeatureLocation.findByFeature(exon)
+            if (transcriptFeatureLocation.fmin == exonFeatureLocation.fmin) {
+                transcriptFeatureLocation.fmin = fmin
+            }
+            if (transcriptFeatureLocation.fmax == exonFeatureLocation.fmax) {
+                transcriptFeatureLocation.fmax = fmax
+            }
+
+
+            exonFeatureLocation.fmin = fmin
+            exonFeatureLocation.fmax = fmax
+            featureService.removeExonOverlapsAndAdjacencies(transcript)
+            transcriptService.updateGeneBoundaries(transcript)
+
+            exon.save()
+
+            featureService.calculateCDS(transcript)
+            nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+
+            transcript.save()
+            def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+            if (transcriptsToUpdate.size() > 0) {
+                JSONObject updateFeatureContainer = createJSONFeatureContainer()
+                transcriptsToUpdate.each {
+                    updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+                }
+                if (sequence) {
+                    AnnotationEvent annotationEvent = new AnnotationEvent(
+                            features: updateFeatureContainer,
+                            sequence: sequence,
+                            operation: AnnotationEvent.Operation.UPDATE
+                    )
+                    fireAnnotationEvent(annotationEvent)
+                }
+            }
+
+            JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript, false)
+            returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(newJsonObject);
+            featureEventService.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldTranscriptJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+        }
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: returnObject
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+
+        return returnObject
+    }
+
+    @Timed
+    JSONObject setBoundaries(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONObject returnObject = createJSONFeatureContainerFromFeatures()
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject oldJsonFeature = features.getJSONObject(i);
+            if (!oldJsonFeature.has(FeatureStringEnum.LOCATION.value)) {
+                continue;
+            }
+            JSONObject jsonLocation = oldJsonFeature.getJSONObject(FeatureStringEnum.LOCATION.value);
+            int fmin = jsonLocation.getInt(FeatureStringEnum.FMIN.value);
+            int fmax = jsonLocation.getInt(FeatureStringEnum.FMAX.value);
+            if (fmin < 0 || fmax < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates");
+            }
+            Feature feature = Feature.findByUniqueName(oldJsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+//            editor.setBoundaries(feature, fmin, fmax);
+            FeatureLocation featureLocation = FeatureLocation.findByFeature(feature)
+
+            featureLocation.fmin = fmin
+            featureLocation.fmax = fmax
+            feature.save()
+
+            JSONObject newJsonFeature = featureService.convertFeatureToJSON(feature, false)
+            returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(newJsonFeature);
+            featureEventService.addNewFeatureEvent(FeatureOperation.SET_BOUNDARIES, feature.name, feature.uniqueName, inputObject, oldJsonFeature, newJsonFeature, permissionService.getCurrentUser(inputObject))
+        }
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: returnObject
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return returnObject
+    }
+
+    def fireAnnotationEvent(AnnotationEvent... annotationEvents) {
+        for (AnnotationEvent annotationEvent in annotationEvents) {
+            handleChangeEvent(annotationEvent)
+        }
+    }
+
+    void sendAnnotationEvent(String returnString, Sequence sequence) {
+        if (returnString.startsWith("[")) {
+            returnString = returnString.substring(1, returnString.length() - 1)
+        }
+        try {
+            brokerMessagingTemplate.convertAndSend "/topic/AnnotationNotification/" + sequence.organismId + "/" + sequence.id, returnString
+        } catch (e) {
+            log.error("problem sending message: ${e}")
+        }
+    }
+
+    void handleChangeEvent(AnnotationEvent event) {
+
+        if (!event) {
+            return;
+        }
+        JSONArray operations = new JSONArray();
+        JSONObject features = event.getFeatures();
+        try {
+            features.put(AnnotationEditorController.REST_OPERATION, event.getOperation().name());
+            features.put(REST_SEQUENCE_ALTERNATION_EVENT, event.isSequenceAlterationEvent());
+            if (event.username) {
+                features.put(FeatureStringEnum.USERNAME.value, event.username);
+            }
+            operations.put(features);
+        }
+        catch (JSONException e) {
+            log.error("error handling change event ${event}: ${e}")
+        }
+
+        sendAnnotationEvent(operations.toString(), event.sequence);
+
+    }
+
+    @Timed
+    private JSONObject createJSONFeatureContainerFromFeatures(Feature... features) throws JSONException {
+        def jsonObjects = new ArrayList()
+        for (Feature feature in features) {
+            JSONObject featureObject = featureService.convertFeatureToJSON(feature, false)
+            jsonObjects.add(featureObject)
+        }
+        return createJSONFeatureContainer(jsonObjects as JSONObject[])
+    }
+
+    @Timed
+    JSONObject createJSONFeatureContainer(JSONObject... features) throws JSONException {
+        JSONObject jsonFeatureContainer = new JSONObject();
+        JSONArray jsonFeatures = new JSONArray();
+        jsonFeatureContainer.put(FeatureStringEnum.FEATURES.value, jsonFeatures);
+        for (JSONObject feature : features) {
+            jsonFeatures.put(feature);
+        }
+        return jsonFeatureContainer;
+    }
+
+    @Timed
+    JSONObject deleteSequenceAlteration(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONObject deleteFeatureContainer = createJSONFeatureContainer();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            SequenceAlteration sequenceAlteration = SequenceAlteration.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            FeatureLocation sequenceAlterationFeatureLocation = sequenceAlteration.getFeatureLocation()
+            deleteFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(sequenceAlteration, true));
+            FeatureLocation.deleteAll(sequenceAlteration.featureLocations)
+            sequenceAlteration.delete(flush: true)
+
+            for (Feature feature : featureService.getOverlappingFeatures(sequenceAlterationFeatureLocation, false)) {
+                if (feature instanceof Gene) {
+                    for (Transcript transcript : transcriptService.getTranscripts((Gene) feature)) {
+                        CDS cds = transcriptService.getCDS(transcript)
+                        featureService.setLongestORF(transcript, cdsService.hasStopCodonReadThrough(cds))
+                        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+                        updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript, true));
+                    }
+                    feature.save(flush: true)
+                }
+            }
+        }
+
+        AnnotationEvent deleteAnnotationEvent = new AnnotationEvent(
+                features: deleteFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.DELETE
+                , sequenceAlterationEvent: true
+        )
+        AnnotationEvent updateAnnotationEvent = new AnnotationEvent(
+                features: updateFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+        fireAnnotationEvent(deleteAnnotationEvent)
+        fireAnnotationEvent(updateAnnotationEvent)
+
+        return createJSONFeatureContainer()
+    }
+
+//    { "track": "GroupUn4157", "features": [ { "location": { "fmin": 1284, "fmax": 1284, "strand": 1 }, "type": {"name": "insertion", "cv": { "name":"sequence" } }, "residues": "ATATATA" } ], "operation": "add_sequence_alteration" }
+    @Timed
+    def addSequenceAlteration(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONObject addFeatureContainer = createJSONFeatureContainer();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        User activeUser = permissionService.getCurrentUser(inputObject)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            int fmin = jsonFeature.get(FeatureStringEnum.LOCATION.value).fmin
+            int fmax = jsonFeature.get(FeatureStringEnum.LOCATION.value).fmax
+            if (featureService.getOverlappingSequenceAlterations(sequence, fmin, fmax)) {
+                // found an overlapping sequence alteration
+                throw new AnnotationException("Cannot create an overlapping sequence alteration");
+            }
+            SequenceAlteration sequenceAlteration = (SequenceAlteration) featureService.convertJSONToFeature(jsonFeature, sequence)
+            if (activeUser) {
+                featureService.setOwner(sequenceAlteration, activeUser)
+            } else {
+                log.error("Unable to find valid user to set on transcript!")
+            }
+            sequenceAlteration.save()
+
+            featureService.updateNewGsolFeatureAttributes(sequenceAlteration, sequence)
+
+            if (sequenceAlteration.getFmin() < 0 || sequenceAlteration.getFmax() < 0) {
+                throw new AnnotationException("Feature cannot have negative coordinates");
+            }
+
+            sequenceAlteration.save(flush: true)
+
+            // TODO: Should we make it compulsory for the request object to have comments
+            // TODO: If so, then we should change all of our integration tests for sequence alterations to have comments
+            if (jsonFeature.has(FeatureStringEnum.NON_RESERVED_PROPERTIES.value)) {
+                JSONArray properties = jsonFeature.getJSONArray(FeatureStringEnum.NON_RESERVED_PROPERTIES.value);
+                for (int j = 0; j < properties.length(); ++j) {
+                    JSONObject property = properties.getJSONObject(i);
+                    String tag = property.getString(FeatureStringEnum.TAG.value)
+                    String value = property.getString(FeatureStringEnum.VALUE.value)
+                    FeatureProperty featureProperty = new FeatureProperty(
+                            feature: sequenceAlteration,
+                            value: value,
+                            tag: tag
+                    ).save()
+                    featurePropertyService.addProperty(sequenceAlteration, featureProperty)
+                    sequenceAlteration.save(flush: true)
+                }
+            }
+
+
+            for (Feature feature : featureService.getOverlappingFeatures(sequenceAlteration.getFeatureLocation(), false)) {
+                if (feature instanceof Gene) {
+                    for (Transcript transcript : transcriptService.getTranscripts((Gene) feature)) {
+                        CDS cds = transcriptService.getCDS(transcript)
+                        featureService.setLongestORF(transcript, cdsService.hasStopCodonReadThrough(cds))
+                        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+                        updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript, false));
+                    }
+                }
+            }
+            addFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(sequenceAlteration, true));
+        }
+
+        AnnotationEvent addAnnotationEvent = new AnnotationEvent(
+                features: addFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.ADD
+                , sequenceAlterationEvent: true
+        )
+        AnnotationEvent updateAnnotationEvent = new AnnotationEvent(
+                features: updateFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+        fireAnnotationEvent(addAnnotationEvent)
+        fireAnnotationEvent(updateAnnotationEvent)
+
+        return addFeatureContainer
+
+    }
+
+    def addNonReservedProperties(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            JSONArray properties = jsonFeature.getJSONArray(FeatureStringEnum.NON_RESERVED_PROPERTIES.value);
+            for (int j = 0; j < properties.length(); ++j) {
+                JSONObject property = properties.getJSONObject(j);
+                String tag = property.getString(FeatureStringEnum.TAG.value)
+                String value = property.getString(FeatureStringEnum.VALUE.value)
+                featureService.addNonReservedProperties(feature, tag, value)
+            }
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature));
+        }
+
+//        AnnotationEvent annotationEvent = new AnnotationEvent(
+//                features: updateFeatureContainer
+//                , sequence:sequence
+//                , operation: AnnotationEvent.Operation.ADD
+//                , sequenceAlterationEvent: false
+//        )
+//
+//        fireAnnotationEvent(annotationEvent)
+
+        return updateFeatureContainer
+    }
+
+    def deleteNonReservedProperties(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            JSONArray properties = jsonFeature.getJSONArray(FeatureStringEnum.NON_RESERVED_PROPERTIES.value);
+            for (int j = 0; j < properties.length(); ++j) {
+                JSONObject property = properties.getJSONObject(j);
+                String tagString = property.getString(FeatureStringEnum.TAG.value)
+                String valueString = property.getString(FeatureStringEnum.VALUE.value)
+                log.debug "tagString ${tagString}"
+                log.debug "valueString ${valueString}"
+                // a NonReservedProperty will always have a tag
+                FeatureProperty featureProperty = FeatureProperty.findByTagAndValueAndFeature(tagString, valueString, feature)
+                if (featureProperty) {
+                    log.info "Removing feature property"
+                    feature.removeFromFeatureProperties(featureProperty)
+                    feature.save()
+                    featureProperty.delete(flush: true)
+                } else {
+                    log.error "Could not find feature property to delete ${property as JSON}"
+                }
+            }
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature));
+        }
+
+        return updateFeatureContainer
+    }
+
+    def updateNonReservedProperties(JSONObject inputObject) {
+        JSONObject updateFeatureContainer = createJSONFeatureContainer();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            JSONArray oldProperties = jsonFeature.getJSONArray(FeatureStringEnum.OLD_NON_RESERVED_PROPERTIES.value);
+            JSONArray newProperties = jsonFeature.getJSONArray(FeatureStringEnum.NEW_NON_RESERVED_PROPERTIES.value);
+            for (int j = 0; j < oldProperties.length(); ++j) {
+                JSONObject oldProperty = oldProperties.getJSONObject(j);
+                JSONObject newProperty = newProperties.getJSONObject(j);
+                String oldTag = oldProperty.getString(FeatureStringEnum.TAG.value)
+                String oldValue = oldProperty.getString(FeatureStringEnum.VALUE.value)
+                String newTag = newProperty.getString(FeatureStringEnum.TAG.value)
+                String newValue = newProperty.getString(FeatureStringEnum.VALUE.value)
+
+                FeatureProperty featureProperty = FeatureProperty.findByTagAndValueAndFeature(oldTag, oldValue, feature)
+                if (feature) {
+                    featureProperty.tag = newTag
+                    featureProperty.value = newValue
+                    featureProperty.save()
+                } else {
+                    log.error("No feature property found for tag ${oldTag} and value ${oldValue} for feature ${feature}")
+                }
+            }
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature));
+
+        }
+//        fireDataStoreChange(updateFeatureContainer, track, Operation.UPDATE);
+    }
+
+    def lockFeature(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject featureContainer = createJSONFeatureContainer();
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            if (FeatureProperty.findByFeatureAndValue(feature, FeatureStringEnum.LOCKED.value)) {
+                log.error("Feature ${feature.name} already locked")
+            } else {
+                FeatureProperty featureProperty = new FeatureProperty(
+                        value: FeatureStringEnum.LOCKED.value
+                        , feature: feature
+                ).save()
+                feature.addToFeatureProperties(featureProperty)
+                feature.save()
+                featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature, false));
+            }
+        }
+
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    def unlockFeature(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject featureContainer = createJSONFeatureContainer();
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+            FeatureProperty featureProperty = FeatureProperty.findByFeatureAndValue(feature, FeatureStringEnum.LOCKED.value)
+            if (featureProperty) {
+                feature.removeFromFeatureProperties(featureProperty)
+                feature.save()
+                FeatureProperty.deleteAll(featureProperty)
+                featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature));
+            } else {
+                log.error("Feature ${feature.name} was not locked.  Doing nothing.")
+            }
+        }
+
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def flipStrand(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        JSONObject featureContainer = createJSONFeatureContainer()
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i)
+            Feature feature = Feature.findByUniqueName(jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value))
+
+            if (feature instanceof Transcript) {
+                feature = transcriptService.flipTranscriptStrand((Transcript) feature)
+                featureService.setLongestORF((Transcript) feature)
+                nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites((Transcript) feature)
+                def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(feature)
+                if (transcriptService.getGene((Transcript) feature)) {
+                    featureEventService.addNewFeatureEventWithUser(FeatureOperation.FLIP_STRAND, transcriptService.getGene((Transcript) feature).name, feature.uniqueName, inputObject, featureService.convertFeatureToJSON((Transcript) feature), permissionService.getCurrentUser(inputObject))
+                    if (transcriptsToUpdate.size()) {
+                        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+                        transcriptsToUpdate.each {
+                            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+                        }
+                        if (sequence) {
+                            AnnotationEvent annotationEvent = new AnnotationEvent(
+                                    features: updateFeatureContainer,
+                                    sequence: sequence,
+                                    operation: AnnotationEvent.Operation.UPDATE
+                            )
+                            fireAnnotationEvent(annotationEvent)
+                        }
+                    }
+                }
+                else{
+                    log.error("Transcript failed to produce gene with moving to opposite strand: "+feature.name)
+                }
+            } else {
+                feature = featureService.flipStrand(feature)
+                featureEventService.addNewFeatureEventWithUser(FeatureOperation.FLIP_STRAND, feature.name, feature.uniqueName, inputObject, featureService.convertFeatureToJSON(feature), permissionService.getCurrentUser(inputObject))
+            }
+            featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature, false));
+        }
+
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def mergeExons(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Exon exon1 = (Exon) Exon.findByUniqueName(features.getJSONObject(0).getString(FeatureStringEnum.UNIQUENAME.value));
+        Exon exon2 = (Exon) Exon.findByUniqueName(features.getJSONObject(1).getString(FeatureStringEnum.UNIQUENAME.value));
+        Transcript transcript1 = exonService.getTranscript(exon1)
+        JSONObject oldJsonObject = featureService.convertFeatureToJSON(transcript1)
+        exonService.mergeExons(exon1, exon2)
+        featureService.calculateCDS(transcript1);
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript1);
+        // rename?
+
+        transcript1.save(flush: true)
+        exon1.save(flush: true)
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript1)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript1)
+        JSONObject featureContainer = createJSONFeatureContainer(newJsonObject)
+
+        featureEventService.addNewFeatureEvent(FeatureOperation.MERGE_EXONS, transcriptService.getGene(transcript1).name, transcript1.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def splitExon(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject jsonExon = features.getJSONObject(0)
+        Exon exon = (Exon) Exon.findByUniqueName(jsonExon.getString(FeatureStringEnum.UNIQUENAME.value));
+        JSONObject exonLocation = jsonExon.getJSONObject(FeatureStringEnum.LOCATION.value);
+        Transcript transcript = exonService.getTranscript(exon)
+        JSONObject oldJsonObject = featureService.convertFeatureToJSON(transcript)
+
+
+        Exon splitExon = exonService.splitExon(exon, exonLocation.getInt(FeatureStringEnum.FMAX.value), exonLocation.getInt(FeatureStringEnum.FMIN.value))
+        featureService.updateNewGsolFeatureAttributes(splitExon, sequence)
+        featureService.calculateCDS(transcript)
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+
+        exon.save()
+        transcript.save(flush: true)
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript)
+        JSONObject featureContainer = createJSONFeatureContainer(newJsonObject);
+
+        featureEventService.addNewFeatureEvent(FeatureOperation.SPLIT_EXON, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldJsonObject, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    /**
+     * First object is Transcript.
+     * Subsequence objects are exons
+     * @param inputObject
+     */
+    @Timed
+    def deleteExon(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject jsonTranscript = features.getJSONObject(0)
+
+        Transcript transcript = Transcript.findByUniqueName(jsonTranscript.getString(FeatureStringEnum.UNIQUENAME.value));
+        for (int i = 1; i < features.length(); ++i) {
+            JSONObject jsonExon = features.getJSONObject(i)
+            Exon exon = Exon.findByUniqueName(jsonExon.getString(FeatureStringEnum.UNIQUENAME.value));
+            checkOwnersDelete(exon, inputObject)
+
+            exonService.deleteExon(transcript, exon);
+        }
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        Feature topLevelFeature = featureService.getTopLevelFeature(transcript)
+        JSONObject featureContainer = createJSONFeatureContainer(featureService.convertFeatureToJSON(topLevelFeature))
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.DELETE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def addFeature(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        log.debug "adding sequence with found sequence ${sequence}"
+        User user = permissionService.getCurrentUser(inputObject)
+
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject returnObject = createJSONFeatureContainer()
+
+        boolean useName = false
+        boolean suppressHistory = false
+        boolean suppressEvents = false
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_HISTORY.value)) {
+            suppressHistory = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_HISTORY.value)
+        }
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_EVENTS.value)) {
+            suppressEvents = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_EVENTS.value)
+        }
+
+        for (int i = 0; i < featuresArray.size(); i++) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            useName = jsonFeature.has(FeatureStringEnum.USE_NAME.value) ? jsonFeature.get(FeatureStringEnum.USE_NAME.value) : false
+            if (jsonFeature.get(FeatureStringEnum.TYPE.value).name == Gene.alternateCvTerm ||
+                    jsonFeature.get(FeatureStringEnum.TYPE.value).name == Pseudogene.alternateCvTerm) {
+                // if jsonFeature is of type gene or pseudogene
+                JSONObject jsonGene = JSON.parse(jsonFeature.toString())
+                jsonGene.remove(FeatureStringEnum.CHILDREN.value)
+                if (jsonFeature.containsKey(FeatureStringEnum.CHILDREN.value)) {
+                    for (JSONObject transcriptJsonFeature in jsonFeature.getJSONArray(FeatureStringEnum.CHILDREN.value)) {
+                        // look at its children JSON Array to get the features at the *RNA level
+                        // adding jsonGene to each individual transcript
+                        transcriptJsonFeature.put(FeatureStringEnum.PARENT.value, jsonGene)
+                        Feature newFeature = featureService.addFeature(transcriptJsonFeature, sequence, user, suppressHistory, useName)
+                        JSONObject newFeatureJsonObject = featureService.convertFeatureToJSON(newFeature)
+                        JSONObject jsonObject = newFeatureJsonObject
+
+                        if (!suppressHistory) {
+                            featureEventService.addNewFeatureEvent(FeatureOperation.ADD_FEATURE, newFeature.name, newFeature.uniqueName, inputObject, newFeatureJsonObject, user)
+                        }
+                        returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(jsonObject);
+                    }
+                }
+            } else {
+                // jsonFeature is of type transposable_element or repeat_region
+                Feature newFeature = featureService.addFeature(jsonFeature, sequence, user, suppressHistory, true)
+                JSONObject newFeatureJsonObject = featureService.convertFeatureToJSON(newFeature)
+                JSONObject jsonObject = newFeatureJsonObject
+
+                if (!suppressHistory) {
+                    featureEventService.addNewFeatureEvent(FeatureOperation.ADD_FEATURE, newFeature.name, newFeature.uniqueName, inputObject, newFeatureJsonObject, user)
+                }
+                returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).put(jsonObject);
+            }
+        }
+
+        if (!suppressEvents) {
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: returnObject
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.ADD
+            )
+
+            fireAnnotationEvent(annotationEvent)
+        }
+        return returnObject
+    }
+
+    /**
+     *  From AnnotationEditorService
+     */
+//    { "track": "Group1.3", "features": [ { "uniquename": "179e77b9-9329-4633-9f9e-888e3cf9b76a" } ], "operation": "delete_feature" }:
+    @Timed
+    def deleteFeature(JSONObject inputObject) {
+        log.debug "in delete feature ${inputObject as JSON}"
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        boolean suppressEvents = false
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_EVENTS.value)) {
+            suppressEvents = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_EVENTS.value)
+        }
+        boolean suppressHistory = false
+        if (inputObject.has(FeatureStringEnum.SUPPRESS_HISTORY.value)) {
+            suppressHistory = inputObject.getBoolean(FeatureStringEnum.SUPPRESS_HISTORY.value)
+        }
+
+        JSONObject featureContainer = createJSONFeatureContainer();
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+        Map<String, List<Feature>> modifiedFeaturesUniqueNames = new HashMap<String, List<Feature>>();
+        boolean isUpdateOperation = false
+
+        JSONArray oldJsonObjectsArray = new JSONArray()
+        // we have to hold transcripts if feature is an exon, etc. or a feature itself if not a transcript
+        Map<String, JSONObject> oldFeatureMap = new HashMap<>()
+        log.debug "features to delete: ${featuresArray.size()}"
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            Feature feature
+            String uniqueName
+            if (jsonFeature.has(FeatureStringEnum.UNIQUENAME.value)) {
+                uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+                feature = Feature.findByUniqueName(uniqueName)
+            } else {
+                feature = Feature.findByName(jsonFeature.getString(FeatureStringEnum.NAME.value))
+                uniqueName = feature.uniqueName
+            }
+
+            checkOwnersDelete(feature, inputObject)
+
+            log.debug "feature found to delete ${feature?.name}"
+            if (feature) {
+                if (feature instanceof Exon) {
+                    Transcript transcript = exonService.getTranscript((Exon) feature)
+                    // if its the same transcript, we don't want to overwrite it
+                    if (!oldFeatureMap.containsKey(transcript.uniqueName)) {
+                        oldFeatureMap.put(transcript.uniqueName, featureService.convertFeatureToJSON(transcript))
+                    }
+                } else {
+                    if (!oldFeatureMap.containsKey(feature.uniqueName)) {
+                        oldFeatureMap.put(feature.uniqueName, featureService.convertFeatureToJSON(feature))
+                    }
+                }
+                //oldJsonObjectsArray.add(featureService.convertFeatureToJSON(feature))
+                // is this a bug?
+                isUpdateOperation = featureService.deleteFeature(feature, modifiedFeaturesUniqueNames) || isUpdateOperation;
+                List<Feature> modifiedFeaturesList = modifiedFeaturesUniqueNames.get(uniqueName)
+                if (modifiedFeaturesList == null) {
+                    modifiedFeaturesList = new ArrayList<>()
+                }
+                modifiedFeaturesList.add(feature)
+                modifiedFeaturesUniqueNames.put(uniqueName, modifiedFeaturesList)
+            }
+
+        }
+        for (String key : oldFeatureMap.keySet()) {
+            log.debug "setting keys"
+            oldJsonObjectsArray.add(oldFeatureMap.get(key))
+        }
+
+        for (Map.Entry<String, List<Feature>> entry : modifiedFeaturesUniqueNames.entrySet()) {
+            String uniqueName = entry.getKey();
+            Feature feature = Feature.findByUniqueName(uniqueName);
+            log.debug "updating name for feature ${uniqueName} -> ${feature}"
+            if (feature == null) {
+                log.info("Feature already deleted");
+                continue;
+            }
+            if (!isUpdateOperation) {
+                log.debug "is not update operation "
+                // when the line below is used, the client gives an error saying TypeError: Cannot read property 'fmin' of undefined(…)
+                // featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(new JSONObject().put(FeatureStringEnum.UNIQUENAME.value, uniqueName));
+                featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(feature))
+                if (feature instanceof Transcript) {
+                    Transcript transcript = (Transcript) feature;
+                    Gene gene = transcriptService.getGene(transcript)
+                    if (!gene) {
+                        gene = transcriptService.getPseudogene(transcript)
+                    }
+                    int numberTranscripts = transcriptService.getTranscripts(gene).size()
+                    if (numberTranscripts == 1) {
+                        Feature topLevelFeature = featureService.getTopLevelFeature(gene)
+                        featureRelationshipService.deleteFeatureAndChildren(topLevelFeature)
+
+                        if (!suppressEvents) {
+                            AnnotationEvent annotationEvent = new AnnotationEvent(
+                                    features: featureContainer
+                                    , sequence: sequence
+                                    , operation: AnnotationEvent.Operation.DELETE
+                            )
+
+                            fireAnnotationEvent(annotationEvent)
+                        }
+                    } else {
+                        featureRelationshipService.removeFeatureRelationship(gene, transcript)
+                        featureRelationshipService.deleteFeatureAndChildren(transcript)
+                        featureService.updateGeneBoundaries(gene)
+                        gene.save()
+
+                        if (!suppressEvents) {
+                            AnnotationEvent annotationEvent = new AnnotationEvent(
+                                    features: featureContainer
+                                    , sequence: sequence
+                                    , operation: AnnotationEvent.Operation.UPDATE
+                            )
+                            fireAnnotationEvent(annotationEvent)
+                        }
+                    }
+
+                    // TODO: handle transcript merging ??
+//                    List<String> toBeDeleted = new ArrayList<String>();
+//                    toBeDeleted.add(feature.getUniqueName());
+//                    while (!toBeDeleted.isEmpty()) {
+//                        String id = toBeDeleted.remove(toBeDeleted.size() - 1);
+//                        for (Transaction t : historyStore.getTransactionListForFeature(id)) {
+//                            if (t.getOperation().equals(Transaction.Operation.MERGE_TRANSCRIPTS)) {
+//                                if (editor.getSession().getFeatureByUniqueName(t.getOldFeatures().get(1).getUniqueName()) == null) {
+//                                    toBeDeleted.add(t.getOldFeatures().get(1).getUniqueName());
+//                                }
+//                            }
+//                        }
+//                        historyStore.deleteHistoryForFeature(id);
+//                    }
+
+                } else {
+                    Feature topLevelFeature = featureService.getTopLevelFeature(feature)
+                    featureRelationshipService.deleteFeatureAndChildren(topLevelFeature)
+
+                    if (!suppressEvents) {
+                        AnnotationEvent annotationEvent = new AnnotationEvent(
+                                features: featureContainer
+                                , sequence: sequence
+                                , operation: AnnotationEvent.Operation.DELETE
+                        )
+
+                        fireAnnotationEvent(annotationEvent)
+                    }
+                }
+            } else {
+                String featureName
+                log.debug "IS update operation "
+                FeatureOperation featureOperation
+                if (feature instanceof Transcript) {
+                    Transcript transcript = (Transcript) feature;
+                    featureService.calculateCDS(transcript)
+                    nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+                    transcript.name = transcript.name ?: nameService.generateUniqueName(transcript)
+                    Gene gene = transcriptService.getGene(transcript)
+                    gene.save()
+
+                    def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+                    featureService.updateGeneBoundaries(gene)
+                    if (transcriptsToUpdate.size() > 0) {
+                        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+                        transcriptsToUpdate.each {
+                            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+                        }
+                        if (sequence) {
+                            AnnotationEvent annotationEvent = new AnnotationEvent(
+                                    features: updateFeatureContainer,
+                                    sequence: sequence,
+                                    operation: AnnotationEvent.Operation.UPDATE
+                            )
+                            fireAnnotationEvent(annotationEvent)
+                        }
+                    }
+
+                    featureOperation = FeatureOperation.DELETE_EXON
+                    featureName = gene.name
+                } else {
+                    feature.save()
+                    featureOperation = FeatureOperation.DELETE_FEATURE
+                    featureName = feature.name
+                }
+
+                JSONObject newJsonObject = featureService.convertFeatureToJSON(feature)
+                featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(newJsonObject);
+
+
+                if (!suppressEvents) {
+                    featureEventService.addNewFeatureEvent(featureOperation, featureName, feature.uniqueName, inputObject, new JSONObject().put(FeatureStringEnum.FEATURES.value, oldJsonObjectsArray), newJsonObject, permissionService.getCurrentUser(inputObject))
+                }
+            }
+        }
+
+
+
+        if (!suppressEvents) {
+            AnnotationEvent finalAnnotationEvent = new AnnotationEvent(
+                    features: featureContainer
+                    , sequence: sequence
+            )
+
+            finalAnnotationEvent.operation = isUpdateOperation ? AnnotationEvent.Operation.UPDATE : AnnotationEvent.Operation.DELETE
+            fireAnnotationEvent(finalAnnotationEvent)
+        }
+
+        return createJSONFeatureContainer()
+    }
+
+    def checkOwnersDelete(Feature feature, JSONObject inputObject) {
+        if (configWrapperService.onlyOwnersDelete) {
+            def currentUser = permissionService.getCurrentUser(inputObject)
+            def isAdmin = permissionService.isUserAdmin(currentUser)
+            def owners = findOwners(feature)
+            if (!isAdmin && !(currentUser in owners)) {
+                throw new AnnotationException("Only feature owner or admin may delete, change type, or revert annotation to an earlier state")
+            }
+        }
+    }
+
+    private findOwners(Feature feature) {
+        if (!feature) return null
+        if (feature.owners) {
+            return feature.owners
+        }
+        return findOwners(featureRelationshipService.getParentForFeature(feature))
+    }
+
+    @Timed
+    def makeIntron(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject jsonExon = featuresArray.getJSONObject(0)
+        Exon exon = Exon.findByUniqueName(jsonExon.getString(FeatureStringEnum.UNIQUENAME.value))
+        Transcript transcript = exonService.getTranscript(exon)
+        JSONObject oldJsonTranscript = featureService.convertFeatureToJSON(transcript)
+        JSONObject exonLocation = jsonExon.getJSONObject(FeatureStringEnum.LOCATION.value)
+
+        Exon splitExon = exonService.makeIntron(
+                exon
+                , exonLocation.getInt(FeatureStringEnum.FMIN.value)
+                , configWrapperService.getDefaultMinimumIntronSize()
+        )
+        if (splitExon == null) {
+            def returnContainer = createJSONFeatureContainer()
+            returnContainer.put(FeatureStringEnum.ERROR_MESSAGE.value, "Unable to find canonical splice sites.")
+            String username = permissionService.getCurrentUser(inputObject)?.username
+            AnnotationEvent annotationEvent = new AnnotationEvent(
+                    features: returnContainer
+                    , sequence: sequence
+                    , operation: AnnotationEvent.Operation.ERROR
+                    , username: username
+            )
+
+            fireAnnotationEvent(annotationEvent)
+            return returnContainer
+        }
+        featureService.updateNewGsolFeatureAttributes(splitExon, sequence)
+        featureService.calculateCDS(transcript)
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript)
+
+        transcript.name = transcript.name ?: nameService.generateUniqueName(transcript)
+
+        transcript.save(failOnError: true)
+        exon.save(failOnError: true)
+        splitExon.save(failOnError: true, flush: true)
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        JSONObject newJsonObject = featureService.convertFeatureToJSON(transcript)
+        JSONObject featureContainer = createJSONFeatureContainer(newJsonObject)
+
+        featureEventService.addNewFeatureEvent(FeatureOperation.SPLIT_EXON, transcriptService.getGene(transcript).name, transcript.uniqueName, inputObject, oldJsonTranscript, newJsonObject, permissionService.getCurrentUser(inputObject))
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def splitTranscript(JSONObject inputObject) {
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        Exon exon1 = Exon.findByUniqueName(featuresArray.getJSONObject(0).getString(FeatureStringEnum.UNIQUENAME.value))
+        Exon exon2 = Exon.findByUniqueName(featuresArray.getJSONObject(1).getString(FeatureStringEnum.UNIQUENAME.value))
+
+        Transcript transcript1 = exonService.getTranscript(exon1)
+        // transcript2 should contain the second part of transcript1 starting from exon2
+        Transcript transcript2 = transcriptService.splitTranscript(transcript1, exon1, exon2)
+
+        featureService.updateNewGsolFeatureAttributes(transcript2, sequence);
+        featureService.calculateCDS(transcript1)
+        featureService.calculateCDS(transcript2)
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript1);
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript2);
+        transcript1.name = transcript1.name ?: nameService.generateUniqueName(transcript1)
+        transcript2.name = transcript2.name ?: nameService.generateUniqueName(transcript2)
+
+        transcript1.owners.each { transcript2.addToOwners(it) }
+
+        Gene gene1 = transcriptService.getGene(transcript1)
+        Gene gene2 = transcriptService.getGene(transcript2)
+
+        // relying on featureService::handleDynamicIsoformOverlap() to assign the proper parent
+        // to transcript2, based on isoform overlap rule
+        ArrayList<Transcript> transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript1)
+        transcriptsToUpdate.addAll(featureService.handleDynamicIsoformOverlap(transcript2))
+
+        // updateContainer for update annotation event
+        JSONObject updateContainer = createJSONFeatureContainer();
+        Gene updatedGene1 = transcriptService.getGene(transcript1)
+        Gene updatedGene2 = transcriptService.getGene(transcript2)
+        for (Transcript t : transcriptService.getTranscripts(updatedGene1)) {
+            updateContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(t));
+        }
+
+        if (updatedGene1.uniqueName != updatedGene2.uniqueName) {
+            for (Transcript t : transcriptService.getTranscripts(updatedGene2)) {
+                updateContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(t));
+            }
+        }
+
+        // returnContainer for return object
+        Feature topLevelExonFeature = featureService.getTopLevelFeature(transcript1)
+        JSONObject returnContainer = createJSONFeatureContainerFromFeatures(topLevelExonFeature)
+
+        // features to add to history
+        JSONObject featureForHistory = createJSONFeatureContainer()
+        featureForHistory.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript1))
+        featureForHistory.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript2))
+
+        // add history for transcript1 and transcript2
+        Boolean suppressHistory = inputObject.has(FeatureStringEnum.SUPPRESS_HISTORY.value) ? inputObject.getBoolean(FeatureStringEnum.SUPPRESS_HISTORY.value) : false
+        if (!suppressHistory) {
+            try {
+                featureEventService.addSplitFeatureEvent(updatedGene1.name, transcript1.uniqueName
+                        , updatedGene2.name, transcript2.uniqueName
+                        , inputObject
+                        , featureService.convertFeatureToJSON(transcript1)
+                        , featureForHistory.getJSONArray(FeatureStringEnum.FEATURES.value)
+                        , permissionService.getCurrentUser(inputObject)
+                )
+            } catch (e) {
+                log.error "There was an error adding history ${e}"
+            }
+        }
+
+        // firing annotation update event
+        AnnotationEvent updateAnnotationEvent = new AnnotationEvent(
+                features: updateContainer,
+                sequence: sequence,
+                operation: AnnotationEvent.Operation.UPDATE
+        )
+        fireAnnotationEvent(updateAnnotationEvent)
+
+        return returnContainer
+    }
+
+    @Timed
+    def mergeTranscripts(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject jsonTranscript1 = featuresArray.getJSONObject(0)
+        JSONObject jsonTranscript2 = featuresArray.getJSONObject(1)
+        Transcript transcript1 = Transcript.findByUniqueName(jsonTranscript1.getString(FeatureStringEnum.UNIQUENAME.value))
+        Transcript transcript2 = Transcript.findByUniqueName(jsonTranscript2.getString(FeatureStringEnum.UNIQUENAME.value))
+
+        // cannot merge transcripts from different strands
+        if (!transcript1.getStrand().equals(transcript2.getStrand())) {
+            throw new AnnotationException("You cannot merge transcripts on opposite strands");
+        }
+
+        List<Transcript> sortedTranscripts = [transcript1, transcript2].sort { a, b ->
+            a.fmin <=> b.fmin ?: a.fmax <=> b.fmax ?: a.name <=> b.name
+        }
+        if (transcript1.strand == Strand.NEGATIVE.value) {
+            sortedTranscripts.reverse(true)
+        }
+        transcript1 = sortedTranscripts.get(0)
+        transcript2 = sortedTranscripts.get(1)
+        Gene gene1 = transcriptService.getGene(transcript1)
+        Gene gene2 = transcriptService.getGene(transcript2)
+        String gene1Name = gene1.name
+        String gene2Name = gene2.name
+        String transcript1UniqueName = transcript1.uniqueName
+        String transcript2UniqueName = transcript2.uniqueName
+
+        JSONObject transcript2JSONObject = featureService.convertFeatureToJSON(transcript2)
+
+        // calculate longest ORF, to reset any changes made to the CDS, before a merge
+        featureService.setLongestORF(transcript1);
+        featureService.setLongestORF(transcript2);
+        // merging transcripts
+        transcriptService.mergeTranscripts(transcript1, transcript2)
+        featureService.calculateCDS(transcript1)
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(transcript1)
+
+        // calling handleDynamicIsoformOverlap() to account for all overlapping transcripts to the merged transcript
+        def transcriptsToUpdate = featureService.handleDynamicIsoformOverlap(transcript1)
+        if (transcriptsToUpdate.size() > 0) {
+            JSONObject updateFeatureContainer = createJSONFeatureContainer()
+            transcriptsToUpdate.each {
+                updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(it))
+            }
+            if (sequence) {
+                AnnotationEvent annotationEvent = new AnnotationEvent(
+                        features: updateFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.UPDATE
+                )
+                fireAnnotationEvent(annotationEvent)
+            }
+        }
+
+        Gene mergedTranscriptGene = transcriptService.getGene(transcript1)
+        transcript1.name = transcript1.name ?: nameService.generateUniqueName(transcript1)
+
+        JSONObject returnObject = createJSONFeatureContainerFromFeatures(featureService.getTopLevelFeature(transcript1))
+
+        // update feature container for update annotation event for transcripts of gene1
+        JSONObject updateFeatureContainer = createJSONFeatureContainer()
+        gene1.refresh()
+        for (Transcript transcript : transcriptService.getTranscripts(gene1)) {
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript))
+        }
+        for (Transcript transcript : transcriptService.getTranscripts(mergedTranscriptGene)) {
+            updateFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript))
+        }
+
+        // delete feature container for delete annotation event
+        JSONObject deleteFeatureContainer = createJSONFeatureContainer()
+        deleteFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(transcript2JSONObject)
+
+        // TODO: history tracking
+        JSONObject featureForHistory = createJSONFeatureContainer()
+        featureForHistory.getJSONArray(FeatureStringEnum.FEATURES.value).put(featureService.convertFeatureToJSON(transcript1))
+
+        Boolean suppressHistory = inputObject.has(FeatureStringEnum.SUPPRESS_HISTORY.value) ? inputObject.getBoolean(FeatureStringEnum.SUPPRESS_HISTORY.value) : false
+        if (!suppressHistory) {
+            JSONArray oldJsonArray = new JSONArray()
+            oldJsonArray.add(jsonTranscript1)
+            oldJsonArray.add(jsonTranscript2)
+            try {
+                log.debug "trying to add history"
+                featureEventService.addMergeFeatureEvent(gene1Name, transcript1UniqueName
+                        , gene2Name, transcript2UniqueName
+                        , inputObject, oldJsonArray
+                        , featureForHistory.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0)
+                        , permissionService.getCurrentUser(inputObject)
+                )
+                log.debug "ADDED history"
+            } catch (e) {
+                log.error " There was a problem adding history for this merge event ${e}"
+            }
+        }
+
+        AnnotationEvent deleteAnnotationEvent = new AnnotationEvent(
+                features: deleteFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.DELETE
+        )
+
+        AnnotationEvent updateAnnotationEvent = new AnnotationEvent(
+                features: updateFeatureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.UPDATE
+        )
+
+        // firing update and delete annotation event
+        fireAnnotationEvent(updateAnnotationEvent, deleteAnnotationEvent)
+
+        return returnObject
+    }
+
+    @Timed
+    def duplicateTranscript(JSONObject inputObject) {
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+
+        Transcript transcript = Transcript.findByUniqueName(inputObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).getString(FeatureStringEnum.UNIQUENAME.value))
+
+        Transcript duplicateTranscript = transcriptService.duplicateTranscript(transcript)
+        duplicateTranscript.save()
+        Feature topFeature = featureService.getTopLevelFeature(transcript)
+        topFeature.save()
+        JSONObject featureContainer = createJSONFeatureContainer(featureService.convertFeatureToJSON(topFeature))
+
+        AnnotationEvent annotationEvent = new AnnotationEvent(
+                features: featureContainer
+                , sequence: sequence
+                , operation: AnnotationEvent.Operation.ADD
+        )
+
+        fireAnnotationEvent(annotationEvent)
+
+        return featureContainer
+    }
+
+    @Timed
+    def undo(JSONObject inputObject) {
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        permissionService.getCurrentUser(inputObject)
+
+        for (int i = 0; i < featuresArray.size(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            int count = inputObject.containsKey(FeatureStringEnum.COUNT.value) ? inputObject.getInt(FeatureStringEnum.COUNT.value) : false
+            jsonFeature = permissionService.copyRequestValues(inputObject, jsonFeature)
+            featureEventService.undo(jsonFeature, count)
+        }
+        return new JSONObject()
+    }
+
+    @Timed
+    def redo(JSONObject inputObject) {
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        permissionService.getCurrentUser(inputObject)
+
+        for (int i = 0; i < featuresArray.size(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i);
+            int count = inputObject.containsKey(FeatureStringEnum.COUNT.value) ? inputObject.getInt(FeatureStringEnum.COUNT.value) : false
+            jsonFeature = permissionService.copyRequestValues(inputObject, jsonFeature)
+            featureEventService.redo(jsonFeature, count)
+        }
+        return new JSONObject()
+    }
+
+    def changeAnnotationType(JSONObject inputObject) {
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        Sequence sequence = permissionService.checkPermissions(inputObject, PermissionEnum.WRITE)
+        User user = permissionService.getCurrentUser(inputObject)
+        JSONObject featureContainer = createJSONFeatureContainer()
+
+        def singletonFeatureTypes = [RepeatRegion.alternateCvTerm, TransposableElement.alternateCvTerm]
+        def rnaFeatureTypes = [MRNA.alternateCvTerm, MiRNA.alternateCvTerm, NcRNA.alternateCvTerm, RRNA.alternateCvTerm, SnRNA.alternateCvTerm, SnoRNA.alternateCvTerm, TRNA.alternateCvTerm, Transcript.alternateCvTerm]
+
+        for (int i = 0; i < features.length(); i++) {
+            String type = features.get(i).type
+            String uniqueName = features.get(i).uniquename
+            Feature feature = Feature.findByUniqueName(uniqueName)
+            checkOwnersDelete(feature, inputObject)
+            FeatureEvent currentFeatureEvent = featureEventService.findCurrentFeatureEvent(feature.uniqueName).get(0)
+            JSONObject currentFeatureJsonObject = featureService.convertFeatureToJSON(feature)
+            JSONObject originalFeatureJsonObject = JSON.parse(currentFeatureEvent.newFeaturesJsonArray) as JSONObject
+            String originalType = feature.alternateCvTerm ? feature.alternateCvTerm : feature.cvTerm
+
+            if (originalType == type) {
+                log.warn "Cannot change ${uniqueName} from ${originalType} -> ${type}. Nothing to do."
+            } else if (originalType in singletonFeatureTypes && type in rnaFeatureTypes) {
+                log.error "Not enough information available to change ${uniqueName} from ${originalType} -> ${type}."
+            } else {
+                log.info "Changing ${uniqueName} from ${originalType} to ${type}"
+                Feature newFeature = featureService.changeAnnotationType(inputObject, feature, sequence, user, type)
+                JSONObject newFeatureJsonObject = featureService.convertFeatureToJSON(newFeature)
+                log.debug "New feature json object: ${newFeatureJsonObject.toString()}"
+                JSONArray oldFeatureJsonArray = new JSONArray()
+                JSONArray newFeatureJsonArray = new JSONArray()
+                oldFeatureJsonArray.add(originalFeatureJsonObject)
+                newFeatureJsonArray.add(newFeatureJsonObject)
+                featureEventService.addNewFeatureEvent(FeatureOperation.CHANGE_ANNOTATION_TYPE, feature.name,
+                        uniqueName, inputObject, oldFeatureJsonArray, newFeatureJsonArray, user)
+
+                JSONObject deleteFeatureContainer = createJSONFeatureContainer()
+                deleteFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(currentFeatureJsonObject)
+                AnnotationEvent deleteAnnotationEvent = new AnnotationEvent(
+                        features: deleteFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.DELETE
+                )
+                fireAnnotationEvent(deleteAnnotationEvent)
+
+                JSONObject addFeatureContainer = createJSONFeatureContainer()
+                addFeatureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(newFeatureJsonObject)
+                AnnotationEvent addAnnotationEvent = new AnnotationEvent(
+                        features: addFeatureContainer,
+                        sequence: sequence,
+                        operation: AnnotationEvent.Operation.ADD
+                )
+                fireAnnotationEvent(addAnnotationEvent)
+                featureContainer.getJSONArray(FeatureStringEnum.FEATURES.value).put(newFeatureJsonObject)
+            }
+        }
+
+        return featureContainer
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/SequenceSearchService.groovy b/grails-app/services/org/bbop/apollo/SequenceSearchService.groovy
new file mode 100644
index 0000000..b842781
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/SequenceSearchService.groovy
@@ -0,0 +1,81 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import groovy.json.JsonBuilder
+import org.bbop.apollo.sequence.search.blast.TabDelimittedAlignment
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+
+ at Transactional
+class SequenceSearchService {
+
+    def configWrapperService
+
+    def searchSequence(JSONObject input, String database) {
+
+
+        try {
+            String ret=input.get('search').get('key')
+            JSONObject searchUtils=configWrapperService.getSequenceSearchTools().get(ret)
+            searchUtils.put("database",database)
+
+            // dynamically allocate a search_class
+            def searcher=this.class.classLoader.loadClass( searchUtils.get('search_class'))?.newInstance()
+
+            // pass configuration
+            searcher.parseConfiguration(searchUtils)
+
+            Collection<TabDelimittedAlignment> results = searcher.search('searchid',
+                    input.get('search').residues,
+                    input.get('search').database_id)
+
+            JsonBuilder json = new JsonBuilder ()
+            json.matches results, { TabDelimittedAlignment result ->
+                "identity" result.percentId
+                "significance" result.eValue
+                "subject"({
+                    "location" ({
+                        "fmin" result.subjectStart
+                        "fmax" result.subjectEnd
+                        "strand" result.subjectStrand
+                    })
+                    "feature" ({
+                        "uniquename" result.subjectId
+                        "type"({
+                            "name" "region"
+                            "cv" ({
+                                "name" "sequence"
+                            })
+                        })
+                    })
+                })
+                "query"({
+                    "location" ({
+                        "fmin" result.queryStart
+                        "fmax" result.queryEnd
+                        "strand" result.queryStrand
+                    })
+                    "feature" ({
+                        "uniquename" result.queryId
+                        "type" ({
+                            "name" "region"
+                            "cv"({
+                                "name" "sequence"
+                            })
+                        })
+                    })
+                })
+
+                rawscore result.bitscore
+            }
+            return json.toString()
+        }
+        catch(Exception e) {
+            def obj=new JSONObject()
+            obj.put("error",e.getMessage())
+            return obj.toString()
+        }
+    }
+
+
+}
diff --git a/grails-app/services/org/bbop/apollo/SequenceService.groovy b/grails-app/services/org/bbop/apollo/SequenceService.groovy
new file mode 100644
index 0000000..179e461
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/SequenceService.groovy
@@ -0,0 +1,595 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import htsjdk.samtools.reference.FastaSequenceFile
+import htsjdk.samtools.reference.FastaSequenceIndex
+import htsjdk.samtools.reference.FastaSequenceIndexCreator
+import htsjdk.samtools.reference.IndexedFastaSequenceFile
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import grails.transaction.Transactional
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.StandardTranslationTable
+import org.bbop.apollo.sequence.Strand
+import org.bbop.apollo.alteration.SequenceAlterationInContext
+import org.bbop.apollo.sequence.TranslationTable
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import groovy.json.JsonSlurper
+import org.hibernate.criterion.CriteriaSpecification
+import org.hibernate.sql.JoinType
+
+import java.util.zip.CRC32
+
+ at Transactional
+class SequenceService {
+    
+    def configWrapperService
+    def grailsApplication
+    def featureService
+    def transcriptService
+    def requestHandlingService
+    def exonService
+    def cdsService
+    def gff3HandlerService
+    def overlapperService
+    def organismService
+    def trackService
+
+
+
+    List<FeatureLocation> getFeatureLocations(Sequence sequence){
+        FeatureLocation.findAllBySequence(sequence)
+    }
+
+    /**
+     * Get residues from sequence . . . could be multiple locations
+     * @param feature
+     * @return
+     */
+    String getResiduesFromFeature(Feature feature) {
+        String returnResidues = ""
+        def orderedFeatureLocations = feature.featureLocations.sort { it.fmin }
+        for(FeatureLocation featureLocation in orderedFeatureLocations) {
+            String residues = getResidueFromFeatureLocation(featureLocation)
+            if(featureLocation.strand == Strand.NEGATIVE.value) {
+                returnResidues += SequenceTranslationHandler.reverseComplementSequence(residues)
+            }
+            else returnResidues += residues
+        }
+
+
+        return returnResidues
+    }
+
+    String getResidueFromFeatureLocation(FeatureLocation featureLocation) {
+        return getRawResiduesFromSequence(featureLocation.sequence,featureLocation.fmin,featureLocation.fmax)
+    }
+
+
+    String getGenomicResiduesFromSequenceWithAlterations(FlankingRegion flankingRegion) {
+        return getGenomicResiduesFromSequenceWithAlterations(flankingRegion.sequence,flankingRegion.fmin,flankingRegion.fmax,flankingRegion.strand)
+    }
+
+    /**
+     * Just meant for non-transcript genomic sequence
+     * @param sequence
+     * @param fmin
+     * @param fmax
+     * @param strand
+     * @return
+     */
+    String getGenomicResiduesFromSequenceWithAlterations(Sequence sequence, int fmin, int fmax,Strand strand) {
+        String residueString = getRawResiduesFromSequence(sequence,fmin,fmax)
+        if(strand==Strand.NEGATIVE){
+            residueString = SequenceTranslationHandler.reverseComplementSequence(residueString)
+        }
+        
+        StringBuilder residues = new StringBuilder(residueString);
+        List<SequenceAlteration> sequenceAlterationList = SequenceAlteration.withCriteria {
+            createAlias('featureLocations', 'fl', JoinType.INNER_JOIN)
+            createAlias('fl.sequence', 's', JoinType.INNER_JOIN)
+            and {
+                or {
+                    and {
+                        ge("fl.fmin", fmin)
+                        le("fl.fmin", fmax)
+                    }
+                    and {
+                        ge("fl.fmax", fmin)
+                        le("fl.fmax", fmax)
+                    }
+                }
+                eq("s.id",sequence.id)
+            }
+        }.unique()
+        log.debug "sequence alterations found ${sequenceAlterationList.size()}"
+        List<SequenceAlterationInContext> sequenceAlterationsInContextList = new ArrayList<SequenceAlterationInContext>()
+        for (SequenceAlteration sequenceAlteration : sequenceAlterationList) {
+            int alterationFmin = sequenceAlteration.fmin
+            int alterationFmax = sequenceAlteration.fmax
+            SequenceAlterationInContext sa = new SequenceAlterationInContext()
+            if ((alterationFmin >= fmin && alterationFmax <= fmax) && (alterationFmax >= fmin && alterationFmax <= fmax)) {
+                // alteration is within the generic feature
+                sa.fmin = alterationFmin
+                sa.fmax = alterationFmax
+                if (sequenceAlteration instanceof Insertion) {
+                    sa.instanceOf = Insertion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Deletion) {
+                    sa.instanceOf = Deletion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Substitution) {
+                    sa.instanceOf = Substitution.canonicalName
+                }
+                sa.type = 'within'
+                sa.strand = sequenceAlteration.strand
+                sa.name = sequenceAlteration.name + '-inContext'
+                sa.originalAlterationUniqueName = sequenceAlteration.uniqueName
+                sa.offset = sequenceAlteration.offset
+                sa.alterationResidue = sequenceAlteration.alterationResidue
+                sequenceAlterationsInContextList.add(sa)
+            }
+            else if ((alterationFmin >= fmin && alterationFmin <= fmax) && (alterationFmax >= fmin && alterationFmax >= fmax)) {
+                // alteration starts in exon but ends in an intron
+                int difference = alterationFmax - fmax
+                sa.fmin = alterationFmin
+                sa.fmax = Math.min(fmax,alterationFmax)
+                if (sequenceAlteration instanceof Insertion) {
+                    sa.instanceOf = Insertion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Deletion) {
+                    sa.instanceOf = Deletion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Substitution) {
+                    sa.instanceOf = Substitution.canonicalName
+                }
+                sa.type = 'exon-to-intron'
+                sa.strand = sequenceAlteration.strand
+                sa.name = sequenceAlteration.name + '-inContext'
+                sa.originalAlterationUniqueName = sequenceAlteration.uniqueName
+                sa.offset = sequenceAlteration.offset - difference
+                sa.alterationResidue = sequenceAlteration.alterationResidue.substring(0, sequenceAlteration.alterationResidue.length() - difference)
+                sequenceAlterationsInContextList.add(sa)
+            }
+            else if ((alterationFmin <= fmin && alterationFmin <= fmax) && (alterationFmax >= fmin && alterationFmax <= fmax)) {
+                // alteration starts within intron but ends in an exon
+                int difference = fmin - alterationFmin
+                sa.fmin = Math.max(fmin, alterationFmin)
+                sa.fmax = alterationFmax
+                if (sequenceAlteration instanceof Insertion) {
+                    sa.instanceOf = Insertion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Deletion) {
+                    sa.instanceOf = Deletion.canonicalName
+                }
+                else if (sequenceAlteration instanceof Substitution) {
+                    sa.instanceOf = Substitution.canonicalName
+                }
+                sa.type = 'intron-to-exon'
+                sa.strand = sequenceAlteration.strand
+                sa.name = sequenceAlteration.name + '-inContext'
+                sa.originalAlterationUniqueName = sequenceAlteration.uniqueName
+                sa.offset = sequenceAlteration.offset - difference
+                sa.alterationResidue = sequenceAlteration.alterationResidue.substring(difference, sequenceAlteration.alterationResidue.length())
+                sequenceAlterationsInContextList.add(sa)
+            }
+        }
+
+        ArrayList<SequenceAlterationInContext> orderedSequenceAlterationInContextList = featureService.sortSequenceAlterationInContext(sequenceAlterationsInContextList)
+        if (sequenceAlterationsInContextList.size() != 0) {
+            if (!strand.equals(orderedSequenceAlterationInContextList.get(0).strand)) {
+                Collections.reverse(orderedSequenceAlterationInContextList);
+            }
+        }
+
+        int currentOffset = 0;
+        for (SequenceAlterationInContext sequenceAlteration in orderedSequenceAlterationInContextList) {
+            int localCoordinate = featureService.convertSourceCoordinateToLocalCoordinate(fmin,fmax,strand, sequenceAlteration.fmin);
+            String sequenceAlterationResidues = sequenceAlteration.alterationResidue
+            int alterationLength = sequenceAlteration.alterationResidue.length()
+            if (strand == Strand.NEGATIVE) {
+                sequenceAlterationResidues = SequenceTranslationHandler.reverseComplementSequence(sequenceAlterationResidues);
+            }
+            // Insertions
+            if (sequenceAlteration.instanceOf == Insertion.canonicalName) {
+                if (strand==Strand.NEGATIVE) {
+                    ++localCoordinate;
+                }
+                residues.insert(localCoordinate + currentOffset, sequenceAlterationResidues);
+                currentOffset += alterationLength;
+            }
+            // Deletions
+            else if (sequenceAlteration.instanceOf == Deletion.canonicalName) {
+                if (strand == Strand.NEGATIVE) {
+                    residues.delete(localCoordinate + currentOffset - alterationLength + 1,
+                            localCoordinate + currentOffset + 1);
+                } else {
+                    residues.delete(localCoordinate + currentOffset,
+                            localCoordinate + currentOffset + alterationLength);
+                }
+                currentOffset -= alterationLength;
+            }
+            // Substitions
+            else if (sequenceAlteration.instanceOf == Substitution.canonicalName) {
+                int start = strand == Strand.NEGATIVE ? localCoordinate - (alterationLength - 1) : localCoordinate;
+                residues.replace(start + currentOffset,
+                        start + currentOffset + alterationLength,
+                        sequenceAlterationResidues);
+            }
+        }
+
+        return residues.toString()
+    }
+
+    String getRawResiduesFromSequence(Sequence sequence, int fmin, int fmax) {
+        if(sequence.organism.genomeFasta) {
+            getRawResiduesFromSequenceFasta(sequence, fmin, fmax)
+        }
+        else {
+            getRawResiduesFromSequenceChunks(sequence, fmin, fmax)
+        }
+    }
+
+    String getRawResiduesFromSequenceFasta(Sequence sequence, int fmin, int fmax) {
+        String sequenceString
+        File genomeFastaFile = new File(sequence.organism.genomeFastaFileName)
+        File genomeFastaIndexFile = new File(sequence.organism.genomeFastaIndexFileName)
+        IndexedFastaSequenceFile indexedFastaSequenceFile = new IndexedFastaSequenceFile(genomeFastaFile, new FastaSequenceIndex(genomeFastaIndexFile))
+        // using fmin + 1 since getSubsequenceAt uses 1-based start and ends
+        sequenceString = indexedFastaSequenceFile.getSubsequenceAt(sequence.name, (long) fmin + 1, (long) fmax).getBaseString()
+        return sequenceString
+    }
+
+    String getRawResiduesFromSequenceChunks(Sequence sequence, int fmin, int fmax) {
+        StringBuilder sequenceString = new StringBuilder()
+
+        int startChunkNumber = fmin / sequence.seqChunkSize;
+        int endChunkNumber = (fmax - 1 ) / sequence.seqChunkSize;
+
+        
+        for(int i = startChunkNumber ; i<= endChunkNumber ; i++){
+            sequenceString.append(loadResidueForSequence(sequence,i))
+        }
+
+        int startPosition = fmin - (startChunkNumber * sequence.seqChunkSize);
+        return sequenceString.substring(startPosition,startPosition + (fmax-fmin))
+    }
+
+    String loadResidueForSequence(Sequence sequence, int chunkNumber) {
+        CRC32 crc = new CRC32();
+        crc.update(sequence.name.getBytes());
+        String hex = String.format("%08x", crc.getValue())
+        String []dirs = splitStringByNumberOfCharacters(hex, 3)
+        String seqDir = String.format("%s/seq/%s/%s/%s", sequence.organism.directory, dirs[0], dirs[1], dirs[2]);
+        String filePath = seqDir+ "/"+ sequence.name + "-" + chunkNumber + ".txt"
+
+        return new File(filePath).getText().toUpperCase()
+    }
+
+    String[] splitStringByNumberOfCharacters(String label, int size) {
+        return label.toList().collate(size)*.join()
+    }
+
+    def loadRefSeqs(Organism organism) {
+        JSONObject referenceTrackObject = getReferenceTrackObject(organism)
+        if (referenceTrackObject.storeClass == "JBrowse/Store/Sequence/IndexedFasta") {
+            loadGenomeFasta(organism, referenceTrackObject)
+        }
+        else {
+            loadRefSeqsJson(organism)
+        }
+    }
+
+    def loadRefSeqsJson(Organism organism) {
+        log.info "loading refseq ${organism.refseqFile}"
+        organism.valid = false ;
+        organism.save(flush: true, failOnError: true,insert:false)
+
+        File refSeqsFile = new File(organism.refseqFile);
+        if(refSeqsFile.exists()) {
+            def refSeqs=refSeqsFile.withReader { r ->
+                new JsonSlurper().parse( r )
+            }
+
+            Sequence.deleteAll(Sequence.findAllByOrganism(organism))
+            refSeqs.each { refSeq ->
+                int length;
+                if(refSeq.length) {
+                    length = refSeq.length
+                }
+                else {
+                    //workaround for jbrowse refSeqs that have no length element
+                    length = refSeq.end-refSeq.start
+                }
+                Sequence sequence = new Sequence(
+                        organism: organism
+                        ,length: length
+                        ,seqChunkSize: refSeq.seqChunkSize
+                        ,start: refSeq.start
+                        ,end: refSeq.end
+                        ,name: refSeq.name
+                ).save(failOnError: true)
+            }
+
+            organism.valid = true
+            organism.save(flush: true,insert:false,failOnError: true)
+
+        }
+    }
+
+    def loadGenomeFasta(Organism organism, JSONObject referenceTrackObject) {
+        organism.valid = false;
+        organism.save(flush: true, failOnError: true, insert: false)
+
+        String genomeFastaFileName = organism.directory + File.separator + referenceTrackObject.urlTemplate
+        String genomeFastaIndexFileName = organism.directory + File.separator + referenceTrackObject.faiUrlTemplate
+        File genomeFastaFile = new File(genomeFastaFileName)
+        if(genomeFastaFile.exists()) {
+            organism.genomeFasta = referenceTrackObject.urlTemplate
+            File genomeFastaIndexFile = new File(genomeFastaIndexFileName)
+            if (genomeFastaIndexFile.exists()) {
+                organism.genomeFastaIndex = referenceTrackObject.faiUrlTemplate
+                FastaSequenceIndex index = new FastaSequenceIndex(genomeFastaIndexFile)
+                // reading the index
+                def iterator = index.iterator()
+                while(iterator.hasNext()) {
+                    def entry = iterator.next()
+                    Sequence sequence = new Sequence(
+                            organism: organism,
+                            length: entry.size,
+                            start: 0,
+                            end: entry.size,
+                            name: entry.contig
+                    ).save(failOnError: true)
+                }
+
+                organism.valid = true
+                organism.save(flush: true, insert: false, failOnError: true)
+            }
+            else {
+                throw  new FileNotFoundException("Genome fasta index ${genomeFastaIndexFile.getCanonicalPath()} does not exist!")
+            }
+        }
+        else {
+            throw new FileNotFoundException("Genome fasta ${genomeFastaFile.getCanonicalPath()} does not exist!")
+        }
+    }
+
+    def getReferenceTrackObject(Organism organism) {
+        JSONObject referenceTrackObject = new JSONObject()
+        File directory = new File(organism.directory)
+        if (directory.exists()) {
+            File trackListFile = new File(organism.trackList)
+            JSONObject trackListJsonObject = JSON.parse(trackListFile.text) as JSONObject
+            referenceTrackObject = trackService.findTrackFromArray(trackListJsonObject.getJSONArray(FeatureStringEnum.TRACKS.value), "DNA")
+        }
+        return referenceTrackObject
+    }
+
+    def setResiduesForFeature(SequenceAlteration sequenceAlteration, String residue) {
+        sequenceAlteration.alterationResidue = residue
+    }
+
+    def setResiduesForFeatureFromLocation(Deletion deletion) {
+        FeatureLocation featureLocation = deletion.featureLocation
+        deletion.alterationResidue = getResidueFromFeatureLocation(featureLocation)
+    }
+    
+    
+    def getSequenceForFeature(Feature gbolFeature, String type, int flank = 0) {
+        // Method returns the sequence for a single feature
+        // Directly called for FASTA Export
+        String featureResidues = null
+        Organism organism = gbolFeature.featureLocation.sequence.organism
+        TranslationTable translationTable = organismService.getTranslationTable(organism)
+
+        if (type.equals(FeatureStringEnum.TYPE_PEPTIDE.value)) {
+            if (gbolFeature instanceof Transcript && transcriptService.isProteinCoding((Transcript) gbolFeature)) {
+                CDS cds = transcriptService.getCDS((Transcript) gbolFeature)
+                Boolean readThroughStop = false
+                if (cdsService.getStopCodonReadThrough(cds).size() > 0) {
+                    readThroughStop = true
+                }
+                String rawSequence = featureService.getResiduesWithAlterationsAndFrameshifts(cds)
+                featureResidues = SequenceTranslationHandler.translateSequence(rawSequence, translationTable, true, readThroughStop)
+                if (featureResidues.charAt(featureResidues.size() - 1) == StandardTranslationTable.STOP.charAt(0)) {
+                    featureResidues = featureResidues.substring(0, featureResidues.size() - 1)
+                }
+                int idx;
+                if ((idx = featureResidues.indexOf(StandardTranslationTable.STOP)) != -1) {
+                    String codon = rawSequence.substring(idx * 3, idx * 3 + 3)
+                    String aa = translationTable.getAlternateTranslationTable().get(codon)
+                    if (aa != null) {
+                        featureResidues = featureResidues.replace(StandardTranslationTable.STOP, aa)
+                    }
+                }
+            } else if (gbolFeature instanceof Exon && transcriptService.isProteinCoding(exonService.getTranscript((Exon) gbolFeature))) {
+                log.debug "Fetching peptide sequence for selected exon: ${gbolFeature}"
+                String rawSequence = exonService.getCodingSequenceInPhase((Exon) gbolFeature, true)
+                Boolean readThroughStop = false
+                if (cdsService.getStopCodonReadThrough(transcriptService.getCDS(exonService.getTranscript((Exon) gbolFeature))).size() > 0) {
+                    readThroughStop = true
+                }
+                featureResidues = SequenceTranslationHandler.translateSequence(rawSequence, translationTable, true, readThroughStop)
+                if (featureResidues.length()>0 && featureResidues.charAt(featureResidues.length() - 1) == StandardTranslationTable.STOP.charAt(0)) {
+                    featureResidues = featureResidues.substring(0, featureResidues.length() - 1)
+                }
+                int idx
+                if ((idx = featureResidues.indexOf(StandardTranslationTable.STOP)) != -1) {
+                    String codon = rawSequence.substring(idx * 3, idx * 3 + 3)
+                    String aa = translationTable.getAlternateTranslationTable().get(codon)
+                    if (aa != null) {
+                        featureResidues = featureResidues.replace(StandardTranslationTable.STOP, aa)
+                    }
+                }
+            } else {
+                featureResidues = ""
+            }
+        } else if (type.equals(FeatureStringEnum.TYPE_CDS.value)) {
+            if (gbolFeature instanceof Transcript && transcriptService.isProteinCoding((Transcript) gbolFeature)) {
+                featureResidues = featureService.getResiduesWithAlterationsAndFrameshifts(transcriptService.getCDS((Transcript) gbolFeature))
+                boolean hasStopCodonReadThrough = false
+                if (cdsService.getStopCodonReadThrough(transcriptService.getCDS((Transcript) gbolFeature)).size() > 0) {
+                    hasStopCodonReadThrough = true
+                }
+                String verifiedResidues = checkForInFrameStopCodon(featureResidues, 0, hasStopCodonReadThrough,translationTable)
+                featureResidues = verifiedResidues
+            } else if (gbolFeature instanceof Exon && transcriptService.isProteinCoding(exonService.getTranscript((Exon) gbolFeature))) {
+                log.debug "Fetching CDS sequence for selected exon: ${gbolFeature}"
+                featureResidues = exonService.getCodingSequenceInPhase((Exon) gbolFeature, false)
+                boolean hasStopCodonReadThrough = false
+                def stopCodonReadThroughList = cdsService.getStopCodonReadThrough(transcriptService.getCDS(exonService.getTranscript((Exon) gbolFeature)))
+                if (stopCodonReadThroughList.size() > 0) {
+                    if (overlapperService.overlaps(stopCodonReadThroughList.get(0), gbolFeature)) {
+                        hasStopCodonReadThrough = true
+                    }
+                }
+                int phase = exonService.getPhaseForExon((Exon) gbolFeature)
+                String verifiedResidues = checkForInFrameStopCodon(featureResidues, phase, hasStopCodonReadThrough,translationTable)
+                featureResidues = verifiedResidues
+            } else {
+                featureResidues = ""
+            }
+
+        } else if (type.equals(FeatureStringEnum.TYPE_CDNA.value)) {
+            if (gbolFeature instanceof Transcript || gbolFeature instanceof Exon) {
+                featureResidues = featureService.getResiduesWithAlterationsAndFrameshifts(gbolFeature)
+            } else {
+                featureResidues = ""
+            }
+        } else if (type.equals(FeatureStringEnum.TYPE_GENOMIC.value)) {
+            int fmin = gbolFeature.getFmin() - flank
+            int fmax = gbolFeature.getFmax() + flank
+
+            if (flank > 0) {
+                if (fmin < 0) {
+                    fmin = 0
+                }
+                if (fmin < gbolFeature.getFeatureLocation().sequence.start) {
+                    fmin = gbolFeature.getFeatureLocation().sequence.start
+                }
+                if (fmax > gbolFeature.getFeatureLocation().sequence.length) {
+                    fmax = gbolFeature.getFeatureLocation().sequence.length
+                }
+                if (fmax > gbolFeature.getFeatureLocation().sequence.end) {
+                    fmax = gbolFeature.getFeatureLocation().sequence.end
+                }
+
+            }
+            featureResidues = getGenomicResiduesFromSequenceWithAlterations(gbolFeature.featureLocation.sequence,fmin,fmax,Strand.getStrandForValue(gbolFeature.strand))
+        }
+        return featureResidues
+    }
+
+    def checkForInFrameStopCodon(String residues, int phase, boolean hasStopCodonReadThrough ,TranslationTable translationTable) {
+        String codon;
+        def stopCodons = translationTable.stopCodons
+        for (int i = phase; i < residues.length(); i += 3) {
+            if (i + 3 >= residues.length()) {
+                break
+            }
+
+            codon = residues.substring(i, i + 3)
+            if (stopCodons.contains(codon)) {
+                if (hasStopCodonReadThrough) {
+                    hasStopCodonReadThrough = false
+                }
+                else {
+                    return residues.substring(0, i + 3)
+                }
+            }
+
+        }
+        return residues
+    }
+
+    def getSequenceForFeatures(JSONObject inputObject, File outputFile=null) {
+        // Method returns a JSONObject 
+        // Suitable for 'get sequence' operation from AEC
+        log.debug "input at getSequenceForFeature: ${inputObject}"
+        JSONArray featuresArray = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        String type = inputObject.getString(FeatureStringEnum.TYPE.value)
+        int flank
+        if (inputObject.has('flank')) {
+            flank = inputObject.getInt("flank")
+            log.debug "flank from request object: ${flank}"
+        } else {
+            flank = 0
+        }
+
+        for (int i = 0; i < featuresArray.length(); ++i) {
+            JSONObject jsonFeature = featuresArray.getJSONObject(i)
+            String uniqueName = jsonFeature.get(FeatureStringEnum.UNIQUENAME.value)
+            Feature gbolFeature = Feature.findByUniqueName(uniqueName)
+            String sequence = getSequenceForFeature(gbolFeature, type, flank)
+
+            JSONObject outFeature = featureService.convertFeatureToJSON(gbolFeature)
+            outFeature.put("residues", sequence)
+            outFeature.put("uniquename", uniqueName)
+            return outFeature
+        }
+    }
+    
+    def getGff3ForFeature(JSONObject inputObject, File outputFile) {
+        List<Feature> featuresToWrite = new ArrayList<>();
+        JSONArray features = inputObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        for (int i = 0; i < features.length(); ++i) {
+            JSONObject jsonFeature = features.getJSONObject(i);
+            String uniqueName = jsonFeature.getString(FeatureStringEnum.UNIQUENAME.value);
+            Feature gbolFeature = Feature.findByUniqueName(uniqueName)
+            gbolFeature = featureService.getTopLevelFeature(gbolFeature)
+            featuresToWrite.add(gbolFeature);
+
+            int fmin = gbolFeature.fmin
+            int fmax = gbolFeature.fmax
+
+            Sequence sequence = gbolFeature.featureLocation.sequence
+
+            // TODO: does strand and alteration length matter here?
+            List<Feature> listOfSequenceAlterations = Feature.executeQuery("select distinct f from Feature f join f.featureLocations fl join fl.sequence s where s = :sequence and f.class in :sequenceTypes and fl.fmin >= :fmin and fl.fmax <= :fmax ", [sequence: sequence, sequenceTypes: requestHandlingService.viewableAlterations,fmin:fmin,fmax:fmax])
+            featuresToWrite += listOfSequenceAlterations
+        }
+        gff3HandlerService.writeFeaturesToText(outputFile.absolutePath, featuresToWrite, grailsApplication.config.apollo.gff3.source as String)
+    }
+
+    String checkCache(String organismString, String sequenceName, String featureName, String type, Map paramMap) {
+        String mapString = paramMap ? (paramMap as JSON).toString() : null
+        return SequenceCache.findByOrganismNameAndSequenceNameAndFeatureNameAndTypeAndParamMap(organismString, sequenceName, featureName, type, mapString)?.response
+    }
+
+    String checkCache(String organismString, String sequenceName, Long fmin, Long fmax,  Map paramMap) {
+        String mapString = paramMap ? (paramMap as JSON).toString() : null
+        return SequenceCache.findByOrganismNameAndSequenceNameAndFminAndFmaxAndParamMap(organismString, sequenceName, fmin, fmax, mapString)?.response
+    }
+
+    @Transactional
+    def cacheRequest(String responseString, String organismString, String sequenceName, String featureName, String type, Map paramMap) {
+        SequenceCache sequenceCache = new SequenceCache(
+                response: responseString
+                , organismName: organismString
+                , sequenceName: sequenceName
+                , featureName: featureName
+                , type: type
+        )
+        if (paramMap) {
+            sequenceCache.paramMap = (paramMap as JSON).toString()
+        }
+        sequenceCache.save()
+    }
+
+    @Transactional
+    def cacheRequest(String responseString, String organismString, String sequenceName, Long fmin, Long fmax, Map paramMap) {
+        SequenceCache sequenceCache = new SequenceCache(
+                response: responseString
+                , organismName: organismString
+                , sequenceName: sequenceName
+                , fmin: fmin
+                , fmax: fmax
+        )
+        if (paramMap) {
+            sequenceCache.paramMap = (paramMap as JSON).toString()
+        }
+        sequenceCache.save()
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/SvgService.groovy b/grails-app/services/org/bbop/apollo/SvgService.groovy
new file mode 100644
index 0000000..73d9a89
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/SvgService.groovy
@@ -0,0 +1,177 @@
+package org.bbop.apollo
+
+import grails.transaction.Transactional
+import org.apache.batik.dom.GenericDOMImplementation
+import org.apache.batik.ext.awt.geom.Polygon2D
+import org.apache.batik.svggen.SVGGraphics2D
+import org.bbop.apollo.track.RenderObject
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.w3c.dom.DOMImplementation
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+
+import java.awt.*
+
+ at Transactional(readOnly = true)
+class SvgService {
+
+    final Integer GLOBAL_HEIGHT = 30
+    final Integer GLOBAL_WIDTH = 400
+
+    def renderSVGFromJSONArray(JSONArray jsonArray) {
+        DOMImplementation domImpl =
+                GenericDOMImplementation.getDOMImplementation()
+
+        // Create an instance of org.w3c.dom.Document.
+        String svgNS = "http://www.w3.org/2000/svg";
+        Document document = domImpl.createDocument(svgNS, "svg", null);
+
+
+        // Create an instance of the SVG Generator.
+        SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
+        int maxIsoForms = getMaxIsoForms(jsonArray)
+        log.debug "max isoformas: ${maxIsoForms}"
+        int canvasHeight = maxIsoForms*GLOBAL_HEIGHT
+        log.debug "canvas height: ${canvasHeight}"
+        svgGenerator.setSVGCanvasSize(new Dimension(500,canvasHeight))
+
+        generateFeatures(svgGenerator, jsonArray)
+
+        StringWriter stringWriter = new StringWriter()
+        svgGenerator.stream(stringWriter, true, false)
+        stringWriter.close()
+        log.debug "should be returning ${stringWriter.toString()}"
+        return stringWriter.toString()
+    }
+
+    int getMaxIsoForms(JSONArray jsonArray) {
+        int maxIsoForms = 1
+        for(def childArray in jsonArray){
+            maxIsoForms = childArray?.children?.size() > maxIsoForms ? childArray?.children?.size() : maxIsoForms
+        }
+        return maxIsoForms
+    }
+
+    def generateFeatures(SVGGraphics2D svgGraphics2D, JSONArray jsonArray) {
+        if (!jsonArray) return
+
+        RenderObject renderObject = new RenderObject(
+               globalFmin:jsonArray.first().fmin,
+               globalFmax:jsonArray.first().fmax,
+        )
+
+        // Renders in the X-axis, multiple feature roots
+        log.debug "# of feature regions: ${jsonArray?.size()}"
+        for (JSONObject jsonObject in jsonArray) {
+            generateFeature(svgGraphics2D, jsonObject, renderObject)
+        }
+    }
+
+    def generateFeature(SVGGraphics2D svgGraphics2D, JSONObject jsonObject, RenderObject renderObject) {
+        // assume type is mRNA for type
+        int globalWidth = renderObject.globalWidth
+        int internalFmin = GLOBAL_WIDTH * ((jsonObject.fmin - renderObject.globalFmin) / globalWidth) ?: 1
+        int internalFmax = GLOBAL_WIDTH * ((jsonObject.fmax - renderObject.globalFmin) / globalWidth)-1
+        int height = GLOBAL_HEIGHT / 2.0
+
+        // this will go away once we start working with introns
+        svgGraphics2D.setStroke(new BasicStroke(2))
+        svgGraphics2D.setPaint(Color.black)
+        svgGraphics2D.drawLine(internalFmin, height, internalFmax, height)
+
+        int stepHeight = 0
+        // render multiple isofrms
+        log.debug "# of isoforms: ${jsonObject.children?.size()}"
+        for(JSONArray children in jsonObject.children){
+            // Renders in the Y-axis
+            renderChildren(svgGraphics2D,children,renderObject,stepHeight)
+            stepHeight += GLOBAL_HEIGHT
+        }
+
+    }
+
+    def drawStrand(SVGGraphics2D svgGraphics2D, JSONObject jsonObject,RenderObject renderObject) {
+        svgGraphics2D.setColor(Color.BLACK)
+        int globalWidth = renderObject.globalWidth
+//        int internalFmin = GLOBAL_WIDTH * ((jsonObject.fmin - renderObject.globalFmin) / globalWidth)
+        int internalFmax = GLOBAL_WIDTH * ((jsonObject.fmax - renderObject.globalFmin) / globalWidth)
+        int height = GLOBAL_HEIGHT / 2.0
+        // draw an arrow to do with strand now
+        if(jsonObject.strand==-1){
+            Polygon2D shape = new Polygon2D()
+            shape.addPoint(2,height)
+            shape.addPoint(12,(int) GLOBAL_HEIGHT * 3 / 4)
+            shape.addPoint(12,(int) 0 + (GLOBAL_HEIGHT / 4) )
+            svgGraphics2D.draw(shape)
+            svgGraphics2D.fill(shape)
+        }
+        else
+        if(jsonObject.strand==1){
+            Polygon2D shape = new Polygon2D()
+            // TODO: not sure why those numbers work, why we need substract 200?
+            shape.addPoint(internalFmax,height)
+            shape.addPoint(internalFmax-10,(int) GLOBAL_HEIGHT * 3 / 4)
+            shape.addPoint(internalFmax-10,(int) 0 + (GLOBAL_HEIGHT / 4) )
+            svgGraphics2D.draw(shape)
+            svgGraphics2D.fill(shape)
+        }
+
+    }
+
+    def renderChildren(SVGGraphics2D svgGraphics2D, JSONArray children, RenderObject renderObject, int stepHeight) {
+        int globalWidth = renderObject.globalWidth
+
+        // sorted by FMIN and the type (CDS to exon)
+        for(JSONObject childObject in children.sort(){ a,b ->
+            if(a.type != b.type ){
+                if(a.type == "exon"){
+                  return -1
+                }
+                else{
+                    return a.type <=> b.type
+                }
+            }
+            return a.fmin <=> b.fmin
+        }){
+            drawStrand(svgGraphics2D,childObject,renderObject)
+            String type = childObject.type
+            Integer phase = childObject.phase  // if a CDS
+            int internalFmin = GLOBAL_WIDTH * ((childObject.fmin - renderObject.globalFmin) / globalWidth) ?: 1
+            int internalFmax = GLOBAL_WIDTH * ((childObject.fmax - renderObject.globalFmin) / globalWidth)
+            int height = GLOBAL_HEIGHT / 2.0
+//            svgGraphics2D.drawRect(internalFmin,stepHeight,(internalFmax-internalFmin),GLOBAL_HEIGHT)
+            if(type=="CDS"){
+                Polygon2D shape = new Polygon2D()
+                shape.addPoint(internalFmin,stepHeight+10)
+                shape.addPoint(internalFmax,stepHeight+10)
+                shape.addPoint(internalFmax,stepHeight+GLOBAL_HEIGHT-10)
+                shape.addPoint(internalFmin,stepHeight+GLOBAL_HEIGHT-10)
+                switch (phase){
+                    case 0:
+                        svgGraphics2D.setColor(Color.RED)
+                        break
+                    case 1:
+                        svgGraphics2D.setColor(Color.BLUE)
+                        break
+                    case 2:
+                        svgGraphics2D.setColor(Color.GREEN)
+                        break
+                }
+                svgGraphics2D.fill(shape)
+            }
+            else
+            if(type=="exon"){
+                Polygon2D shape = new Polygon2D()
+                svgGraphics2D.setColor(Color.BLACK)
+                shape.addPoint(internalFmin,stepHeight+10)
+                shape.addPoint(internalFmax,stepHeight+10)
+                shape.addPoint(internalFmax,stepHeight+GLOBAL_HEIGHT-10)
+                shape.addPoint(internalFmin,stepHeight+GLOBAL_HEIGHT-10)
+                svgGraphics2D.draw(shape)
+                svgGraphics2D.setColor(Color.WHITE)
+                svgGraphics2D.fill(shape)
+            }
+        }
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/TrackMapperService.groovy b/grails-app/services/org/bbop/apollo/TrackMapperService.groovy
new file mode 100644
index 0000000..d27d913
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/TrackMapperService.groovy
@@ -0,0 +1,69 @@
+package org.bbop.apollo
+
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.apache.commons.collections.map.MultiKeyMap
+import org.bbop.apollo.gwt.shared.track.NclistColumnEnum
+import org.bbop.apollo.gwt.shared.track.TrackIndex
+import org.bbop.apollo.sequence.SequenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+ at Transactional(readOnly = true)
+class TrackMapperService {
+
+
+    /**
+     * Format Organism, Track, JSONArray
+     */
+    MultiKeyMap tracks = new MultiKeyMap()
+
+
+    @NotTransactional
+    List<String> getAttributes(SequenceDTO sequenceDTO, Integer index){
+        JSONArray classArray = tracks.get(sequenceDTO.organismCommonName,sequenceDTO.trackName,sequenceDTO.sequenceName)
+        JSONObject classObject =classArray.getJSONObject(index)
+        JSONArray attributesArray = classObject?.getJSONArray("attributes")
+        List<String> returnAttributes = []
+        for(int i = 0 ; attributesArray && i < attributesArray.size() ; i++){
+            returnAttributes << attributesArray.getString(i)
+        }
+        return returnAttributes
+    }
+
+    @NotTransactional
+    def storeTrack(SequenceDTO sequenceDTO, JSONArray jsonArray) {
+        tracks.put(sequenceDTO.organismCommonName,sequenceDTO.trackName,sequenceDTO.sequenceName,jsonArray)
+    }
+
+    @NotTransactional
+    TrackIndex getIndices(SequenceDTO sequenceDTO, Integer index) {
+        List<String> attributes = getAttributes(sequenceDTO,index)
+        TrackIndex trackIndex = new TrackIndex()
+        trackIndex.start = attributes.indexOf(NclistColumnEnum.START.value)+1
+        trackIndex.end = attributes.indexOf(NclistColumnEnum.END.value)+1
+        trackIndex.source = attributes.indexOf(NclistColumnEnum.SOURCE.value)+1
+        trackIndex.chunk = attributes.indexOf(NclistColumnEnum.CHUNK.value)+1
+        trackIndex.id = attributes.indexOf(NclistColumnEnum.ID.value)+1
+        trackIndex.score = attributes.indexOf(NclistColumnEnum.SCORE.value)+1
+        trackIndex.seqId = attributes.indexOf(NclistColumnEnum.SEQ_ID.value)+1
+        trackIndex.strand = attributes.indexOf(NclistColumnEnum.STRAND.value)+1
+        trackIndex.subFeaturesColumn = attributes.indexOf(NclistColumnEnum.SUBFEATURES.value)+1
+        trackIndex.sublistColumn = attributes.indexOf(NclistColumnEnum.SUBLIST.value)+1
+        trackIndex.type = attributes.indexOf(NclistColumnEnum.TYPE.value)+1
+        trackIndex.phase = attributes.indexOf(NclistColumnEnum.PHASE.value)+1
+        trackIndex.name = attributes.indexOf(NclistColumnEnum.NAME.value)+1
+        trackIndex.alias = attributes.indexOf(NclistColumnEnum.ALIAS.value)+1
+        trackIndex.fixCoordinates()
+
+
+        trackIndex.trackName = sequenceDTO.trackName
+        trackIndex.organism = sequenceDTO.organismCommonName
+        trackIndex.classIndex = index
+
+        assert trackIndex.start != 0
+        assert trackIndex.end != 0
+
+        return trackIndex
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/TrackService.groovy b/grails-app/services/org/bbop/apollo/TrackService.groovy
new file mode 100644
index 0000000..1aea3ab
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/TrackService.groovy
@@ -0,0 +1,558 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.NotTransactional
+import grails.transaction.Transactional
+import org.bbop.apollo.gwt.shared.track.TrackIndex
+import org.bbop.apollo.sequence.SequenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONElement
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import java.util.zip.GZIPInputStream
+
+ at Transactional
+class TrackService {
+
+    def preferenceService
+    def trackMapperService
+
+    public static String TRACK_NAME_SPLITTER = "::"
+
+    JSONObject getTrackList(String jbrowseDirectory) {
+        log.debug "got data directory of . . . ? ${jbrowseDirectory}"
+        String absoluteFilePath = jbrowseDirectory + "/trackList.json"
+        File file = new File(absoluteFilePath);
+
+        if (!file.exists()) {
+            log.warn("Could not get for name and path: ${absoluteFilePath}");
+            return null;
+        }
+
+        // add datasets to the configuration
+        JSONObject jsonObject = JSON.parse(file.text) as JSONObject
+        return jsonObject
+    }
+
+    String getTrackDataFile(String jbrowseDirectory, String trackName, String sequence) {
+        JSONObject trackObject = getTrackList(jbrowseDirectory)
+        String urlTemplate = null
+        for (JSONObject track in trackObject.tracks) {
+            if (track.key == trackName) {
+                urlTemplate = track.urlTemplate
+            }
+        }
+
+        return "${urlTemplate.replace("{refseq}", sequence)}"
+    }
+
+    JSONElement retrieveFileObject(String jbrowseDirectory, String trackDataFilePath) {
+
+        if (trackDataFilePath.startsWith("http")) {
+            trackDataFilePath = trackDataFilePath.replace(" ", "%20")
+            if (trackDataFilePath.endsWith(".json")) {
+                return JSON.parse(new URL(trackDataFilePath).text)
+            } else if (trackDataFilePath.endsWith(".jsonz")) {
+                def inputStream = new URL(trackDataFilePath).openStream()
+                GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
+                String outputString = gzipInputStream.readLines().join("\n")
+                return JSON.parse(outputString)
+            } else {
+                log.error("type not understood: " + trackDataFilePath)
+                return null
+            }
+        } else {
+            if (!trackDataFilePath.startsWith("/")) {
+                trackDataFilePath = jbrowseDirectory + "/" + trackDataFilePath
+            }
+            File file = new File(trackDataFilePath)
+            if (!file.exists()) {
+                log.error "File does not exist ${trackDataFilePath}"
+                return null
+            }
+            return JSON.parse(file.text)
+        }
+    }
+
+    JSONObject getTrackData(String trackName, String organism, String sequence) throws FileNotFoundException {
+        String jbrowseDirectory = preferenceService.getOrganismForToken(organism)?.directory
+        String trackDataFilePath = getTrackDataFile(jbrowseDirectory, trackName, sequence)
+        return retrieveFileObject(jbrowseDirectory, trackDataFilePath) as JSONObject
+    }
+
+    @NotTransactional
+    JSONArray getClassesForTrack(String trackName, String organism, String sequence) {
+        JSONObject trackObject = getTrackData(trackName, organism, sequence)
+        return trackObject.getJSONObject("intervals").getJSONArray("classes")
+    }
+
+    def storeTrackData(SequenceDTO sequenceDTO, JSONArray classesForTrack) {
+        trackMapperService.storeTrack(sequenceDTO, classesForTrack)
+    }
+
+    JSONArray getNCList(String trackName, String organismString, String sequence, Long fmin, Long fmax) {
+        assert fmin <= fmax
+
+        // TODO: refactor into a common method
+        JSONArray classesForTrack = getClassesForTrack(trackName, organismString, sequence)
+        Organism organism = preferenceService.getOrganismForToken(organismString)
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                organismCommonName: organism.commonName
+                , trackName: trackName
+                , sequenceName: sequence
+        )
+        this.storeTrackData(sequenceDTO, classesForTrack)
+
+        // 1. get the trackData.json file
+        JSONObject trackObject = getTrackData(trackName, organismString, sequence)
+        JSONArray nclistArray = trackObject.getJSONObject("intervals").getJSONArray("nclist")
+
+        // 1 - extract the appropriate region for fmin / fmax
+        JSONArray filteredList = filterList(nclistArray, fmin, fmax)
+        log.debug "filtered list size ${filteredList.size()} from original ${nclistArray.size()}"
+
+        // if the first featured array has a chunk, then we need to evaluate the chunks instead
+        if (filteredList) {
+            TrackIndex trackIndex = trackMapperService.getIndices(sequenceDTO, filteredList.getJSONArray(0).getInt(0))
+            if (trackIndex.hasChunk()) {
+                List<JSONArray> chunkList = []
+                for (JSONArray chunkArray in filteredList) {
+                    JSONArray chunk = getChunkData(sequenceDTO, chunkArray.getInt(trackIndex.getChunk()))
+                    chunkList.add(filterList(chunk, fmin, fmax))
+                }
+                JSONArray chunkReturnArray = new JSONArray()
+                chunkList.each { ch ->
+                    ch.each {
+                        chunkReturnArray.add(it)
+                    }
+                }
+                return chunkReturnArray
+            }
+        }
+
+        return filteredList
+    }
+
+    /**
+     * reads the file lf-{chunk}.json
+     * @param sequenceDTO
+     * @param chunk
+     * @return
+     */
+    JSONArray getChunkData(SequenceDTO sequenceDTO, int chunk) throws FileNotFoundException {
+        String jbrowseDirectory = preferenceService.getOrganismForToken(sequenceDTO.organismCommonName)?.directory
+
+        String trackName = sequenceDTO.trackName
+        String sequence = sequenceDTO.sequenceName
+
+        String trackDataFilePath = getTrackDataFile(jbrowseDirectory, trackName, sequence)
+
+        trackDataFilePath = trackDataFilePath.replace("trackData.json", "lf-${chunk}.json")
+
+        return retrieveFileObject(jbrowseDirectory, trackDataFilePath) as JSONArray
+    }
+
+    @NotTransactional
+    def convertIndividualNCListToObject(JSONArray featureArray, SequenceDTO sequenceDTO) throws FileNotFoundException {
+
+        if (featureArray.size() > 3) {
+            if (featureArray[0] instanceof Integer) {
+                JSONObject jsonObject = new JSONObject()
+                TrackIndex trackIndex = trackMapperService.getIndices(sequenceDTO, featureArray.getInt(0))
+
+                jsonObject.fmin = featureArray[trackIndex.getStart()]
+                jsonObject.fmax = featureArray[trackIndex.getEnd()]
+                if (trackIndex.source) {
+                    jsonObject.source = featureArray[trackIndex.getSource()]
+                }
+                if (trackIndex.strand) {
+                    jsonObject.strand = featureArray[trackIndex.getStrand()]
+                }
+                if (trackIndex.phase) {
+                    jsonObject.phase = featureArray[trackIndex.phase]
+                }
+                if (trackIndex.type) {
+                    jsonObject.type = featureArray[trackIndex.getType()]
+                }
+                if (trackIndex.score) {
+                    jsonObject.score = featureArray[trackIndex.score]
+                }
+                if (trackIndex.name) {
+                    jsonObject.name = featureArray[trackIndex.name]
+                }
+                if (trackIndex.id) {
+                    jsonObject.id = featureArray[trackIndex.id]
+                }
+                if (trackIndex.seqId) {
+                    jsonObject.seqId = featureArray[trackIndex.seqId]
+                }
+                // sequence source
+//                jsonObject.seqId = featureArray[trackIndex.getSeqId()]
+
+
+                JSONArray childArray = new JSONArray()
+                for (int subIndex = 0; subIndex < featureArray.size(); ++subIndex) {
+                    def subArray = featureArray.get(subIndex)
+                    if (subArray instanceof JSONArray) {
+                        def subArray2 = convertAllNCListToObject(subArray, sequenceDTO)
+                        childArray.addAll(subArray2)
+                    }
+                    if (subArray instanceof JSONObject && subArray.containsKey("Sublist")) {
+                        def subArrays2 = subArray.getJSONArray("Sublist")
+                        childArray.add(convertIndividualNCListToObject(subArrays2, sequenceDTO))
+                    }
+                }
+                if (childArray) {
+                    jsonObject.children = childArray
+                }
+                return jsonObject
+            }
+        }
+        return convertAllNCListToObject(featureArray, sequenceDTO)
+    }
+
+    @NotTransactional
+    JSONArray convertAllNCListToObject(JSONArray fullArray, SequenceDTO sequenceDTO) throws FileNotFoundException {
+        JSONArray returnArray = new JSONArray()
+
+        for (def jsonArray in fullArray) {
+            if (jsonArray instanceof JSONArray) {
+                returnArray.add(convertIndividualNCListToObject(jsonArray, sequenceDTO))
+            }
+        }
+
+        return returnArray
+    }
+
+    @NotTransactional
+    JSONArray filterList(JSONArray inputArray, long fmin, long fmax) {
+        if (fmin < 0 && fmax < 0) return inputArray
+
+        JSONArray jsonArray = new JSONArray()
+
+        for (innerArray in inputArray) {
+            // if there is an overlap
+            if (!(innerArray[2] < fmin || innerArray[1] > fmax)) {
+                // then no
+                jsonArray.add(innerArray)
+            }
+        }
+
+        return jsonArray
+    }
+
+    // TODO: implement with track permissions
+    String getTracks(User user, Organism organism) {
+        String trackList = ""
+        for (UserPermission userPermission in UserPermission.findAllByUserAndOrganism(user, organism)) {
+            trackList += userPermission.trackNames // TODO: add properly
+        }
+        for (UserGroup userGroup in user.userGroups) {
+            trackList += getTracks(userGroup, organism)
+        }
+        return trackList
+    }
+
+    // TODO: implement with track permissions
+    String getTracks(UserGroup group, Organism organism) {
+        String trackList = ""
+        JSONArray jsonArray = new JSONArray()
+        for (GroupPermission groupPermission in GroupPermission.findAllByGroupAndOrganism(group, organism)) {
+            trackList += "||" + groupPermission.trackNames // TODO: add properly
+        }
+        return trackList.trim()
+    }
+
+    // TODO: implement with track permissions
+    String getTrackPermissions(UserGroup userGroup, Organism organism) {
+        JSONArray jsonArray = new JSONArray()
+        for (GroupPermission groupPermission in GroupPermission.findAllByGroupAndOrganism(userGroup, organism)) {
+            jsonArray.add(groupPermission as JSON)
+        }
+        return jsonArray.toString()
+    }
+
+    // TODO: implement with track permissions
+    String getTrackPermissions(User user, Organism organism) {
+        JSONArray jsonArray = new JSONArray()
+        for (UserPermission userPermission in UserPermission.findAllByUserAndOrganism(user, organism)) {
+            jsonArray.add(userPermission as JSON)
+        }
+        String returnString = jsonArray.toString()
+        for (UserGroup userGroup in user.userGroups) {
+            returnString += getTrackPermissions(userGroup, organism)
+        }
+        return returnString
+    }
+
+    @NotTransactional
+    static Map<String, Boolean> mergeTrackVisibilityMaps(Map<String, Boolean> mapA, Map<String, Boolean> mapB) {
+        Map<String, Boolean> returnMap = new HashMap<>()
+        mapA.keySet().each { it ->
+            returnMap.put(it, mapA.get(it))
+        }
+
+        mapB.keySet().each { it ->
+            if (returnMap.containsKey(it)) {
+                returnMap.put(it, returnMap.get(it) || mapB.get(it))
+            } else {
+                returnMap.put(it, mapB.get(it))
+            }
+        }
+        return returnMap
+    }
+
+    Map<String, Boolean> getTracksVisibleForOrganismAndGroup(Organism organism, UserGroup userGroup) {
+        Map<String, Boolean> trackVisibilityMap = new HashMap<>()
+
+        List<GroupTrackPermission> groupPermissions = GroupTrackPermission.findAllByOrganismAndGroup(organism, userGroup)
+        for (GroupTrackPermission groupPermission in groupPermissions) {
+            JSONObject jsonObject = JSON.parse(groupPermission.trackVisibilities) as JSONObject
+
+            // this should make it default to true if a true is ever given
+            jsonObject.keySet().each {
+                Boolean visible = trackVisibilityMap.get(it)
+                // if null or false, can over-ride to true
+                if (!visible) {
+                    trackVisibilityMap.put(it, jsonObject.get(it))
+                }
+            }
+        }
+
+        return trackVisibilityMap
+    }
+
+    /**
+     *
+     * * @param trackVisibilityMap  Map of track names and visibility.
+     * @param user
+     * @param organism
+     */
+    void setTracksVisibleForOrganismAndUser(Map<String, Boolean> trackVisibilityMap, Organism organism, User user) {
+        UserTrackPermission userTrackPermission = UserTrackPermission.findByOrganismAndUser(organism, user)
+        String jsonString = convertHashMapToJsonString(trackVisibilityMap)
+        if (!userTrackPermission) {
+            userTrackPermission = new UserTrackPermission(
+                    user: user
+                    , organism: organism
+                    , trackVisibilities: jsonString
+            ).save(insert: true)
+        } else {
+            userTrackPermission.trackVisibilities = jsonString
+            userTrackPermission.save()
+        }
+    }
+
+    /**
+     * *
+     * * @param trackVisibilityMap  Map of track names and visibility.
+     * @param group
+     * @param organism
+     */
+    void setTracksVisibleForOrganismAndGroup(Map<String, Boolean> trackVisibilityMap, Organism organism, UserGroup group) {
+
+        GroupTrackPermission groupTrackPermission = GroupTrackPermission.findByOrganismAndGroup(organism, group)
+        String jsonString = convertHashMapToJsonString(trackVisibilityMap)
+        if (!groupTrackPermission) {
+            groupTrackPermission = new GroupTrackPermission(
+                    group: group
+                    , organism: organism
+                    , trackVisibilities: jsonString
+            ).save(insert: true)
+        } else {
+            groupTrackPermission.trackVisibilities = jsonString
+            groupTrackPermission.save()
+        }
+
+
+    }
+
+    Map<String, Boolean> getTracksVisibleForOrganismAndUser(Organism organism, User user) {
+        Map<String, Boolean> trackVisibilityMap = new HashMap<>()
+
+        List<UserTrackPermission> userPermissionList = UserTrackPermission.findAllByOrganismAndUser(organism, user)
+        for (UserTrackPermission userPermission in userPermissionList) {
+            JSONObject jsonObject = JSON.parse(userPermission.trackVisibilities) as JSONObject
+
+            jsonObject.keySet().each {
+                Boolean visible = trackVisibilityMap.get(it)
+                // if null or false, can over-ride to true
+                if (!visible) {
+                    trackVisibilityMap.put(it, jsonObject.get(it))
+                }
+            }
+        }
+
+        for (UserGroup group in user.userGroups) {
+            Map<String, Boolean> specificMap = getTracksVisibleForOrganismAndGroup(organism, group)
+            trackVisibilityMap = mergeTrackVisibilityMaps(specificMap, trackVisibilityMap)
+        }
+
+        return trackVisibilityMap
+    }
+
+
+    private String convertHashMapToJsonString(Map map) {
+        JSONObject jsonObject = new JSONObject()
+        map.keySet().each {
+            jsonObject.put(it, map.get(it))
+        }
+        return jsonObject.toString()
+    }
+
+    String checkCache(String organismString, String trackName, String sequence, String featureName, String type, Map paramMap) {
+        String mapString = paramMap ? (paramMap as JSON).toString() : null
+        return TrackCache.findByOrganismNameAndTrackNameAndSequenceNameAndFeatureNameAndTypeAndParamMap(organismString, trackName, sequence, featureName, type, mapString)?.response
+    }
+
+    String checkCache(String organismString, String trackName, String sequence, Long fmin, Long fmax, String type, Map paramMap) {
+        String mapString = paramMap ? (paramMap as JSON).toString() : null
+        return TrackCache.findByOrganismNameAndTrackNameAndSequenceNameAndFminAndFmaxAndTypeAndParamMap(organismString, trackName, sequence, fmin, fmax, type, mapString)?.response
+    }
+
+    @Transactional
+    def cacheRequest(String responseString, String organismString, String trackName, String sequenceName, String featureName, String type, Map paramMap) {
+        TrackCache trackCache = new TrackCache(
+                response: responseString
+                , organismName: organismString
+                , trackName: trackName
+                , sequenceName: sequenceName
+                , featureName: featureName
+                , type: type
+        )
+        if (paramMap) {
+            trackCache.paramMap = (paramMap as JSON).toString()
+        }
+        trackCache.save()
+    }
+
+    @Transactional
+    def cacheRequest(String responseString, String organismString, String trackName, String sequenceName, Long fmin, Long fmax, String type, Map paramMap) {
+        TrackCache trackCache = new TrackCache(
+                response: responseString
+                , organismName: organismString
+                , trackName: trackName
+                , sequenceName: sequenceName
+                , fmin: fmin
+                , fmax: fmax
+                , type: type
+        )
+        if (paramMap) {
+            trackCache.paramMap = (paramMap as JSON).toString()
+        }
+        trackCache.save()
+    }
+
+    /**
+     *
+     * @param tracksArray
+     * @param trackName
+     * @return
+     */
+    @NotTransactional
+    def findTrackFromArray(JSONArray tracksArray, String trackName) {
+        for (int i = 0; i < tracksArray.size(); i++) {
+            JSONObject obj = tracksArray.getJSONObject(i)
+            if (obj.getString("label") == trackName) return obj
+        }
+
+        return null
+    }
+
+    /**
+     * Removes plugins included in annot.json (which is just WebApollo)
+     * @param pluginsArray
+     */
+    @NotTransactional
+    def removeIncludedPlugins(JSONArray pluginsArray) {
+        def iterator = pluginsArray.iterator()
+        while (iterator.hasNext()) {
+            def plugin = iterator.next()
+            if (plugin instanceof JSONObject) {
+                if (plugin.name == "WebApollo") {
+                    iterator.remove()
+                }
+            } else if (plugin instanceof String) {
+                if (plugin == "WebApollo") {
+                    iterator.remove()
+                }
+            }
+        }
+    }
+
+    @NotTransactional
+    JSONArray flattenArray(JSONArray jsonArray, String... types) {
+
+        List<String> typeList = new ArrayList<>()
+        types.each { typeList.add(it) }
+        JSONArray rootArray = new JSONArray()
+        // here we just clone it
+        for (def obj in jsonArray) {
+            if (obj instanceof JSONObject) {
+                if (typeList.contains(obj.type)) {
+                    for (JSONObject child in getGeneChildren(obj, typeList)) {
+                        rootArray.add(child)
+                    }
+//                rootArray.add(obj)
+                }
+            }
+//            else if (obj instanceof JSONArray) {
+//                rootArray.addAll(flattenArray(obj,types))
+////                for (JSONObject child in obj) {
+////                    rootArray.add(child)
+////                }
+//            }
+        }
+
+        return rootArray
+
+    }
+
+    @NotTransactional
+    JSONArray getGeneChildren(JSONObject jsonObject, List<String> typeList) {
+        JSONArray geneChildren = new JSONArray()
+        boolean hasGeneChild = false
+        Iterator childIterator = jsonObject.children.iterator()
+        while (childIterator.hasNext()) {
+            def child = childIterator.next()
+            if (child instanceof JSONObject) {
+                if (typeList.contains(child.type)) {
+                    hasGeneChild = true
+                    geneChildren.addAll(getGeneChildren(child, typeList))
+                }
+            }
+            // if a subarray instead
+            else if (child instanceof JSONArray) {
+                for (grandChild in child) {
+                    if (typeList.contains(grandChild.type)) {
+                        hasGeneChild = true
+                        geneChildren.addAll(getGeneChildren(grandChild, typeList))
+                    }
+                }
+                geneChildren.add(jsonObject)
+
+                Iterator iter = child.iterator()
+                while (iter.hasNext()) {
+                    def object = iter.next()
+                    if (typeList.contains(object.type)) {
+                        iter.remove()
+                    }
+                }
+            }
+        }
+        if (!hasGeneChild) {
+            geneChildren.add(jsonObject)
+        } else {
+            Iterator iter = jsonObject.children.iterator()
+            while (iter.hasNext()) {
+                def object = iter.next()
+                if (typeList.contains(object.type)) {
+                    iter.remove()
+                }
+            }
+
+        }
+        return geneChildren
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/TranscriptService.groovy b/grails-app/services/org/bbop/apollo/TranscriptService.groovy
new file mode 100644
index 0000000..a69397f
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/TranscriptService.groovy
@@ -0,0 +1,508 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import grails.util.CollectionUtils
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.grails.plugins.metrics.groovy.Timed
+
+
+//@GrailsCompileStatic
+ at Transactional(readOnly = true)
+class TranscriptService {
+
+    List<String> ontologyIds = [Transcript.ontologyId, SnRNA.ontologyId, MRNA.ontologyId, SnoRNA.ontologyId, MiRNA.ontologyId, TRNA.ontologyId, NcRNA.ontologyId, RRNA.ontologyId]
+
+    // services
+    def featureService
+    def featureRelationshipService
+    def exonService
+    def nameService
+    def nonCanonicalSplitSiteService
+    def sequenceService
+    def featureEventService
+
+    /** Retrieve the CDS associated with this transcript.  Uses the configuration to determine
+     *  which child is a CDS.  The CDS object is generated on the fly.  Returns <code>null</code>
+     *  if no CDS is associated.
+     *
+     * @return CDS associated with this transcript
+     */
+    public CDS getCDS(Transcript transcript) {
+        return (CDS) featureRelationshipService.getChildForFeature(transcript, CDS.ontologyId)
+
+    }
+
+    /** Retrieve all the exons associated with this transcript.  Uses the configuration to determine
+     *  which children are exons.  Exon objects are generated on the fly.  The collection
+     *  will be empty if there are no exons associated with the transcript.
+     *
+     * @return Collection of exons associated with this transcript
+     */
+    public Collection<Exon> getExons(Transcript transcript) {
+        return (Collection<Exon>) featureRelationshipService.getChildrenForFeatureAndTypes(transcript, Exon.ontologyId)
+    }
+
+    public Collection<Exon> getSortedExons(Transcript transcript, boolean sortByStrand ) {
+        Collection<Exon> exons = getExons(transcript)
+        List<Exon> sortedExons = new LinkedList<Exon>(exons);
+        Collections.sort(sortedExons, new FeaturePositionComparator<Exon>(sortByStrand))
+        return sortedExons
+    }
+
+    /** Retrieve the gene that this transcript is associated with.  Uses the configuration to
+     * determine which parent is a gene.  The gene object is generated on the fly.  Returns
+     * <code>null</code> if this transcript is not associated with any gene.
+     *
+     * @return Gene that this Transcript is associated with
+     */
+    public Gene getGene(Transcript transcript) {
+        return (Gene) featureRelationshipService.getParentForFeature(transcript, Gene.ontologyId, Pseudogene.ontologyId)
+    }
+
+    public Pseudogene getPseudogene(Transcript transcript) {
+        return (Pseudogene) featureRelationshipService.getParentForFeature(transcript, Pseudogene.ontologyId)
+    }
+
+    public boolean isProteinCoding(Transcript transcript) {
+        return transcript instanceof MRNA
+//        if (getGene(transcript) != null && getGene(transcript) instanceof Pseudogene) {
+//            return false;
+//        }
+//        return true;
+    }
+
+    @Transactional
+    CDS createCDS(Transcript transcript) {
+        String uniqueName = transcript.getUniqueName() + FeatureStringEnum.CDS_SUFFIX.value;
+
+        CDS cds = new CDS(
+                uniqueName: uniqueName
+                , isAnalysis: transcript.isAnalysis
+                , isObsolete: transcript.isObsolete
+                , name: uniqueName
+        ).save(failOnError: true)
+
+        FeatureLocation transcriptFeatureLocation = FeatureLocation.findByFeature(transcript)
+
+        FeatureLocation featureLocation = new FeatureLocation(
+                strand: transcriptFeatureLocation.strand
+                , sequence: transcriptFeatureLocation.sequence
+                , fmin: transcriptFeatureLocation.fmin
+                , fmax: transcriptFeatureLocation.fmax
+                , feature: cds
+        ).save(insert: true, failOnError: true)
+        cds.addToFeatureLocations(featureLocation);
+        cds.save(flush: true, insert: true)
+        return cds;
+    }
+
+    /** Delete a transcript.  Deletes both the gene -> transcript and transcript -> gene
+     *  relationships.
+     *
+     * @param transcript - Transcript to be deleted
+     */
+    @Transactional
+    public void deleteTranscript(Gene gene, Transcript transcript) {
+        featureRelationshipService.removeFeatureRelationship(gene, transcript)
+
+        // update bounds
+        Integer fmin = null;
+        Integer fmax = null;
+        for (Transcript t : getTranscripts(gene)) {
+            if (fmin == null || t.getFeatureLocation().getFmin() < fmin) {
+                fmin = t.getFeatureLocation().getFmin();
+            }
+            if (fmax == null || t.getFeatureLocation().getFmax() > fmax) {
+                fmax = t.getFeatureLocation().getFmax();
+            }
+        }
+        if (fmin != null) {
+            setFmin(transcript, fmin)
+        }
+        if (fmax != null) {
+            setFmax(transcript, fmax)
+        }
+    }
+
+    /** Retrieve all the transcripts associated with this gene.  Uses the configuration to determine
+     *  which children are transcripts.  Transcript objects are generated on the fly.  The collection
+     *  will be empty if there are no transcripts associated with the gene.
+     *
+     * @return Collection of transcripts associated with this gene
+     */
+    public Collection<Transcript> getTranscripts(Gene gene) {
+        return (Collection<Transcript>) featureRelationshipService.getChildrenForFeatureAndTypes(gene, ontologyIds as String[])
+    }
+
+    List<Transcript> getTranscriptsSortedByFeatureLocation(Gene gene, boolean sortByStrand) {
+        return getTranscripts(gene).sort(true, new FeaturePositionComparator<Transcript>(sortByStrand))
+    }
+
+    @Transactional
+    public void setFmin(Transcript transcript, Integer fmin) {
+        transcript.getFeatureLocation().setFmin(fmin);
+        Gene gene = getGene(transcript)
+        if (gene != null && fmin < gene.getFmin()) {
+            featureService.setFmin(gene, fmin)
+        }
+    }
+
+    @Transactional
+    public void setFmax(Transcript transcript, Integer fmax) {
+        transcript.getFeatureLocation().setFmax(fmax);
+        Gene gene = getGene(transcript)
+        if (gene != null && fmax > gene.getFmax()) {
+            featureService.setFmax(gene, fmax);
+        }
+    }
+
+    @Transactional
+    def updateGeneBoundaries(Transcript transcript) {
+        Gene gene = getGene(transcript)
+        if (gene == null) {
+            return;
+        }
+        int geneFmax = Integer.MIN_VALUE;
+        int geneFmin = Integer.MAX_VALUE;
+        for (Transcript t : getTranscripts(gene)) {
+            if (t.getFmin() < geneFmin) {
+                geneFmin = t.getFmin();
+            }
+            if (t.getFmax() > geneFmax) {
+                geneFmax = t.getFmax();
+            }
+        }
+        featureService.setFmin(gene, geneFmin)
+        featureService.setFmax(gene, geneFmax)
+
+        // not sure if we want this if not actually saved
+//        gene.setLastUpdated(new Date());
+    }
+
+    List<String> getFrameShiftOntologyIds() {
+        List<String> intFrameshiftOntologyIds = new ArrayList<>()
+
+        intFrameshiftOntologyIds.add(Plus1Frameshift.ontologyId)
+        intFrameshiftOntologyIds.add(Plus2Frameshift.ontologyId)
+        intFrameshiftOntologyIds.add(Minus1Frameshift.ontologyId)
+        intFrameshiftOntologyIds.add(Minus2Frameshift.ontologyId)
+
+
+        return intFrameshiftOntologyIds
+    }
+
+    List<Frameshift> getFrameshifts(Transcript transcript) {
+        return featureRelationshipService.getFeaturePropertyForTypes(transcript, frameShiftOntologyIds)
+    }
+
+    /** Set the CDS associated with this transcript.  Uses the configuration to determine
+     *  the default term to use for CDS features.
+     *
+     * @param cds - CDS to be set to this transcript
+     */
+    @Transactional
+    public void setCDS(Feature feature, CDS cds, boolean replace = true) {
+        if (replace) {
+            log.debug "replacing CDS on feature"
+            if (featureRelationshipService.setChildForType(feature, cds)) {
+                log.debug "returning "
+                return
+            }
+        }
+
+        FeatureRelationship fr = new FeatureRelationship(
+//                type:partOfCvTerm
+                parentFeature: feature
+                , childFeature: cds
+                , rank: 0
+        ).save(insert: true, failOnError: true)
+
+
+        log.debug "fr: ${fr}"
+        log.debug "feature: ${feature}"
+        log.debug "cds: ${cds}"
+        feature.addToParentFeatureRelationships(fr)
+        cds.addToChildFeatureRelationships(fr)
+
+        cds.save()
+        feature.save(flush: true)
+    }
+
+    @Transactional
+    def addExon(Transcript transcript, Exon exon, Boolean fixTranscript = true) {
+
+        log.debug "exon feature locations ${exon.featureLocation}"
+        log.debug "transcript feature locations ${transcript.featureLocation}"
+        FeatureLocation exonFeatureLocation = exon.featureLocation
+        FeatureLocation transcriptFeatureLocation = transcript.featureLocation
+        if (exonFeatureLocation.fmin < transcriptFeatureLocation.fmin) {
+            transcriptFeatureLocation.setFmin(exonFeatureLocation.fmin);
+        }
+        if (exonFeatureLocation.fmax > transcriptFeatureLocation.fmax) {
+            transcriptFeatureLocation.setFmax(exonFeatureLocation.fmax);
+        }
+        transcript.save()
+
+        // if the transcript's bounds are beyond the gene's bounds, need to adjust the gene's bounds
+        Gene gene = getGene(transcript)
+        if (gene) {
+            FeatureLocation geneFeatureLocation = gene.featureLocation
+            if (transcriptFeatureLocation.fmin < geneFeatureLocation.fmin) {
+                geneFeatureLocation.setFmin(transcriptFeatureLocation.fmin);
+            }
+            if (transcriptFeatureLocation.fmax > geneFeatureLocation.fmax) {
+                geneFeatureLocation.setFmax(transcriptFeatureLocation.fmax);
+            }
+        }
+        gene.save()
+
+        int initialSize = transcript.parentFeatureRelationships?.size() ?: 0
+        log.debug "initial size: ${initialSize}" // 3
+        featureRelationshipService.addChildFeature(transcript, exon, false)
+        int finalSize = transcript.parentFeatureRelationships?.size()
+        log.debug "final size: ${finalSize}" // 4 (+1 exon)
+
+
+        if (fixTranscript) {
+            featureService.removeExonOverlapsAndAdjacencies(transcript)
+            log.debug "post remove exons: ${transcript.parentFeatureRelationships?.size()}" // 6 (+2 splice sites)
+//
+//        // if the exon is removed during a merge, then we will get a null-pointer
+            updateGeneBoundaries(transcript);  // 6, moved transcript fmin, fmax
+            log.debug "post update gene boundaries: ${transcript.parentFeatureRelationships?.size()}"
+        }
+    }
+
+    Transcript getParentTranscriptForFeature(Feature feature) {
+        return (Transcript) featureRelationshipService.getParentForFeature(feature, ontologyIds as String[])
+    }
+
+    @Transactional
+    Transcript splitTranscript(Transcript transcript, Exon leftExon, Exon rightExon) {
+        List<Exon> exons = getSortedExons(transcript,true)
+        Transcript splitTranscript = (Transcript) transcript.getClass().newInstance()
+        splitTranscript.uniqueName = nameService.generateUniqueName()
+        splitTranscript.name = nameService.generateUniqueName(transcript)
+        splitTranscript.save()
+
+        // copying featureLocation of transcript to splitTranscript
+        transcript.featureLocations.each { featureLocation ->
+            FeatureLocation newFeatureLocation = new FeatureLocation(
+                    fmin: featureLocation.fmin
+                    , fmax: featureLocation.fmax
+                    , rank: featureLocation.rank
+                    , sequence: featureLocation.sequence
+                    , strand: featureLocation.strand
+
+                    , feature: splitTranscript
+            ).save()
+            splitTranscript.addToFeatureLocations(newFeatureLocation)
+        }
+        splitTranscript.save(flush: true)
+
+        Gene gene = getGene(transcript)
+        // add transcript2 to a new gene
+        Gene splitTranscriptGene = new Gene(
+                name: nameService.generateUniqueName(gene),
+                uniqueName: nameService.generateUniqueName(),
+        ).save(flush: true)
+
+        transcript.owners.each {
+            splitTranscriptGene.addToOwners(it)
+        }
+
+        FeatureLocation splitTranscriptGeneFeatureLocation = new FeatureLocation(
+                feature: splitTranscriptGene,
+                fmin: splitTranscript.fmin,
+                fmax: splitTranscript.fmax,
+                strand: splitTranscript.strand,
+                sequence: splitTranscript.featureLocation.sequence,
+                residueInfo: splitTranscript.featureLocation.residueInfo,
+                locgroup: splitTranscript.featureLocation.locgroup,
+                rank: splitTranscript.featureLocation.rank
+        ).save(flush: true)
+
+        splitTranscriptGene.addToFeatureLocations(splitTranscriptGeneFeatureLocation)
+        splitTranscript.name = nameService.generateUniqueName(splitTranscript, splitTranscriptGene.name)
+        featureService.addTranscriptToGene(splitTranscriptGene, splitTranscript)
+
+        FeatureLocation transcriptFeatureLocation = transcript.featureLocation
+
+        // changing feature location of transcript to the fmax of the left exon
+        transcriptFeatureLocation.fmax = leftExon.fmax
+        FeatureLocation splitFeatureLocation = splitTranscript.featureLocation
+        log.debug "right1: ${rightExon.featureLocation}"
+
+        // changing feature location of splitTranscript to the fmin of the right exon
+        splitFeatureLocation.fmin = rightExon.featureLocation.fmin
+        log.debug "right2: ${rightExon.featureLocation}"
+        for (Exon exon : exons) {
+            // starting with rightExon and all right flanking exons to splitTranscript
+            FeatureLocation exonFeatureLocation = exon.featureLocation
+            FeatureLocation leftFeatureLocation = leftExon.featureLocation
+            if (exonFeatureLocation.fmin > leftFeatureLocation.getFmin()) {
+                log.debug "right3: ${rightExon.featureLocation}"
+//                featureRelationshipService.removeFeatureRelationship()
+//                exonService.deleteExon(transcript, exon);
+                if (exon.equals(rightExon)) {
+                    featureRelationshipService.removeFeatureRelationship(transcript, rightExon)
+                    log.debug "right4: ${rightExon.featureLocation}"
+                    addExon(splitTranscript, rightExon);
+                } else {
+                    featureRelationshipService.removeFeatureRelationship(transcript, exon)
+                    log.debug "right5: ${rightExon.featureLocation}"
+                    addExon(splitTranscript, exon);
+                }
+            }
+        }
+        transcript.save(flush: true)
+        splitTranscript.save(flush: true)
+
+        return splitTranscript
+    }
+
+    /**
+     * Duplicate a transcript.  Adds it to the parent gene if it is set.
+     *
+     * @param transcript - Transcript to be duplicated
+     */
+    @Transactional
+    public Transcript duplicateTranscript(Transcript transcript) {
+        Transcript duplicate = (Transcript) transcript.generateClone();
+        duplicate.name = transcript.name + "-copy"
+        duplicate.uniqueName = nameService.generateUniqueName(transcript)
+
+        Gene gene = getGene(transcript)
+        if (gene) {
+            featureService.addTranscriptToGene(gene, duplicate)
+            gene.save()
+        }
+        // copy exons
+        for (Exon exon : getExons(transcript)) {
+            Exon duplicateExon = (Exon) exon.generateClone()
+            duplicateExon.name = exon.name + "-copy"
+            duplicateExon.uniqueName = nameService.generateUniqueName(duplicateExon)
+            addExon(duplicate, duplicateExon)
+        }
+        // copy CDS
+        CDS cds = getCDS(transcript)
+        if (cds) {
+            CDS duplicateCDS = (CDS) cds.generateClone()
+            duplicateCDS.name = cds.name + "-copy"
+            duplicateCDS.uniqueName = nameService.generateUniqueName(duplicateCDS)
+            setCDS(duplicate, cds)
+        }
+
+
+        duplicate.save()
+
+        return duplicate
+    }
+
+    @Transactional
+    def mergeTranscripts(Transcript transcript1, Transcript transcript2) {
+        // Merging transcripts basically boils down to moving all exons from one transcript to the other
+
+        // moving all exons from transcript2 to transcript1
+        for (Exon exon : getExons(transcript2)) {
+            featureRelationshipService.removeFeatureRelationship(transcript2, exon)
+            addExon(transcript1, exon)
+        }
+        // we have to do this here to calculate overlaps later
+        featureService.calculateCDS(transcript1)
+        featureService.handleDynamicIsoformOverlap(transcript1)
+        transcript1.save(flush: true)
+
+        Gene gene1 = getGene(transcript1)
+        Gene gene2 = getGene(transcript2)
+        String gene2uniquename = gene2.uniqueName
+
+        if (gene1) {
+            gene1.save(flush: true)
+        }
+
+        boolean flag = false
+
+        // if the parent genes aren't the same
+        // we move transcript2 to the other gene if they overlap any remaining transcripts
+        // , this leads to a merge of the genes
+        if (gene1 && gene2) {
+            if (gene1 != gene2) {
+                log.debug "Gene1 != Gene2; merging genes together"
+                List<Transcript> gene2Transcripts = getTranscripts(gene2)
+                if (gene2Transcripts) {
+                    gene2Transcripts.retainAll(featureService.getTranscriptsWithOverlappingOrf(transcript1))
+
+                    for (Transcript transcript : gene2Transcripts) {
+                        // moving all transcripts of gene2 to gene1, except for transcripts2 which needs to be deleted
+                        // only move if it overlapps.
+                        if (transcript != transcript2) {
+                            deleteTranscript(gene2, transcript)
+                            featureService.addTranscriptToGene(gene1, transcript)
+                        }
+                    }
+                }
+                if (getTranscripts(gene2).size() == 0) {
+                    featureRelationshipService.deleteFeatureAndChildren(gene2)
+                    flag = true
+                }
+            }
+        }
+
+        // Delete the empty transcript from the gene, if gene not already deleted
+        if (!flag) {
+            def childFeatures = featureRelationshipService.getChildren(transcript2)
+            featureRelationshipService.deleteChildrenForTypes(transcript2)
+            Feature.deleteAll(childFeatures)
+            deleteTranscript(gene2, transcript2);
+            featureRelationshipService.deleteFeatureAndChildren(transcript2);
+            if (getTranscripts(gene2).size() == 0) {
+                featureRelationshipService.deleteFeatureAndChildren(gene2)
+            }
+        } else {
+            // if gene for transcript2 doesn't exist then transcript2 is orphan
+            // no outstanding relationships that need to be deleted
+            featureService.deleteFeature(transcript2);
+        }
+        featureService.removeExonOverlapsAndAdjacencies(transcript1);
+    }
+
+    @Transactional
+    Transcript flipTranscriptStrand(Transcript oldTranscript) {
+        Gene oldGene = getGene(oldTranscript)
+        if(oldGene.parentFeatureRelationships.size()==1){
+            oldGene = featureService.flipStrand(oldGene)
+            oldGene.save()
+        }
+        else{
+            oldTranscript = featureService.flipStrand(oldTranscript)
+            oldTranscript.save()
+        }
+        nonCanonicalSplitSiteService.findNonCanonicalAcceptorDonorSpliceSites(oldTranscript)
+        oldTranscript.save()
+
+        return oldTranscript
+    }
+
+    String getResiduesFromTranscript(Transcript transcript) {
+        def exons = getSortedExons(transcript,true)
+        if (!exons) {
+            return null
+        }
+
+        StringBuilder residues = new StringBuilder()
+        for (Exon exon in exons) {
+            residues.append(sequenceService.getResiduesFromFeature(exon))
+        }
+        return residues.size() > 0 ? residues.toString() : null
+    }
+
+    Transcript getTranscript(CDS cds) {
+        return (Transcript) featureRelationshipService.getParentForFeature(cds, ontologyIds as String[])
+    }
+
+}
diff --git a/grails-app/services/org/bbop/apollo/UserService.groovy b/grails-app/services/org/bbop/apollo/UserService.groovy
new file mode 100644
index 0000000..dd8cc9e
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/UserService.groovy
@@ -0,0 +1,115 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.transaction.Transactional
+import org.apache.shiro.crypto.hash.Sha256Hash
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+ at Transactional
+class UserService {
+
+    static String USER = "USER"
+    static String ADMIN = "ADMIN"
+
+    // return admin role or user role
+    Role getHighestRole(User user){
+        for(Role role in user.roles.sort(){ a,b -> b.name<=>a.name }){
+            return role
+        }
+    }
+
+    JSONObject convertUserToJson(User currentUser){
+
+        def userObject = new JSONObject()
+        userObject.userId = currentUser.id
+        userObject.username = currentUser.username
+        userObject.firstName = currentUser.firstName
+        userObject.lastName = currentUser.lastName
+        Role role = getHighestRole(currentUser)
+        userObject.role = role?.name
+
+
+        Map<String, JSONObject> organismMap = new HashMap<>()
+        for (UserOrganismPermission userOrganismPermission in UserOrganismPermission.findAllByUser(currentUser)) {
+            JSONObject organismJSON = new JSONObject()
+            organismJSON.put("organism", userOrganismPermission.organism.commonName)
+            organismJSON.put("permissions", userOrganismPermission.permissions)
+            organismJSON.put("permissionArray", userOrganismPermission.permissionValues)
+            organismJSON.put("userId", userOrganismPermission.userId)
+            organismJSON.put("id", userOrganismPermission.id)
+
+            organismMap.put(userOrganismPermission.organism.commonName, organismJSON)
+        }
+        for (GroupOrganismPermission groupOrganismPermission in GroupOrganismPermission.findAllByGroupInList(currentUser.userGroups as List)) {
+
+            JSONObject organismJSON = organismMap.get(groupOrganismPermission.organism.commonName)
+            if (!organismJSON) {
+                organismJSON = new JSONObject()
+                organismJSON.put("organism", groupOrganismPermission.organism.commonName)
+                organismJSON.put("permissions", groupOrganismPermission.permissions)
+                organismJSON.put("id", groupOrganismPermission.id)
+            } else {
+                String permissions = mergePermissions(organismJSON.getString("permissions"), groupOrganismPermission.permissions)
+                organismJSON.put("permissions", permissions)
+            }
+            organismJSON.put("groupId", groupOrganismPermission.groupId)
+
+            organismMap.put(groupOrganismPermission.organism.commonName, organismJSON)
+        }
+
+        JSONArray organismPermissionsArray = new JSONArray()
+        organismPermissionsArray.addAll(organismMap.values())
+
+        userObject.organismPermissions = organismPermissionsArray
+        userObject.put(FeatureStringEnum.HAS_USERS.value,true)
+    }
+
+    String mergePermissions(String permissions1, String permissions2) {
+        log.debug "permissions1: ${permissions1}"
+        log.debug "permissions2: ${permissions2}"
+        JSONArray permissions1Array = JSON.parse(permissions1) as JSONArray
+        JSONArray permissions2Array = JSON.parse(permissions2) as JSONArray
+
+        Set<String> finalPermissions = new HashSet<>()
+        for(int i =0 ; i < permissions1Array.size() ; i++){
+            finalPermissions.add(permissions1Array.getString(i))
+        }
+        for(int i =0 ; i < permissions2Array.size() ; i++){
+            finalPermissions.add(permissions2Array.getString(i))
+        }
+
+        JSONArray returnArray = new JSONArray()
+        for(String permission in finalPermissions){
+            returnArray.add(permission)
+        }
+
+        String finalPermissionsString = returnArray.toString()
+
+        log.debug "final permission string ${finalPermissionsString}"
+
+        return finalPermissionsString
+    }
+
+    def registerAdmin(JSONObject jsonObj) {
+        registerAdmin(jsonObj.username,jsonObj.password,jsonObj.firstName,jsonObj.lastName)
+    }
+    
+    def registerAdmin(String username,String password,String firstName,String lastName) {
+        if(User.countByUsername(username)>0){
+            log.warn("User exists ${username} and can not be added again.")
+            return ;
+        }
+
+        def adminRole = Role.findByName(ADMIN)
+
+        User user = new User(
+                username: username
+                ,passwordHash: new Sha256Hash(password).toHex()
+                ,firstName: firstName
+                ,lastName: lastName
+        ).save(failOnError: true,flush:true)
+        user.addToRoles(adminRole)
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/authenticator/AuthenticatorService.groovy b/grails-app/services/org/bbop/apollo/authenticator/AuthenticatorService.groovy
new file mode 100644
index 0000000..62e48e0
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/authenticator/AuthenticatorService.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo.authenticator
+
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.session.Session
+
+import javax.servlet.http.HttpServletRequest
+
+/**
+ * Created by nathandunn on 6/30/16.
+ */
+interface AuthenticatorService {
+
+    def authenticate(HttpServletRequest request)
+    def authenticate(UsernamePasswordToken usernamePasswordToken,HttpServletRequest request)
+    Boolean requiresToken()
+}
\ No newline at end of file
diff --git a/grails-app/services/org/bbop/apollo/authenticator/RemoteUserAuthenticatorService.groovy b/grails-app/services/org/bbop/apollo/authenticator/RemoteUserAuthenticatorService.groovy
new file mode 100644
index 0000000..6e3070b
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/authenticator/RemoteUserAuthenticatorService.groovy
@@ -0,0 +1,125 @@
+package org.bbop.apollo.authenticator
+
+import grails.transaction.Transactional
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.AuthenticationException
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.crypto.hash.Sha256Hash
+import org.apache.shiro.subject.Subject
+import org.bbop.apollo.Role
+import org.bbop.apollo.User
+import org.bbop.apollo.UserGroup
+import org.bbop.apollo.UserService
+import org.bbop.apollo.gwt.shared.ClientTokenGenerator
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+import javax.servlet.http.HttpServletRequest
+
+ at Transactional
+class RemoteUserAuthenticatorService implements AuthenticatorService {
+
+    String defaultGroup
+
+    static String INTERNAL_PASSWORD = "INTERNAL_PASSWORD"
+
+    def authenticate(HttpServletRequest request) {
+        User user
+        UsernamePasswordToken authToken = new UsernamePasswordToken()
+        String randomPassword = ClientTokenGenerator.generateRandomString()
+        String passwordHash = new Sha256Hash(randomPassword).toHex()
+        Subject subject
+        try {
+            subject = SecurityUtils.getSubject()
+
+            String remoteUser
+            // for testing
+//            if (!request.getHeader(FeatureStringEnum.REMOTE_USER.value)) {
+//                remoteUser = "abcd at 125.com"
+//            } else {
+//            remoteUser = request.getHeader(FeatureStringEnum.REMOTE_USER.value)
+//            }
+
+            remoteUser = request.getHeader(FeatureStringEnum.REMOTE_USER.value)
+            log.warn "Remote user found [${remoteUser}]"
+            if (!remoteUser) {
+                log.warn("No remote user passed in header!")
+                return false
+            }
+            authToken.username = remoteUser
+            user = User.findByUsername(authToken.username)
+            log.warn "User exists ${user} ? "
+            log.warn "for username: ${authToken.username}"
+            if (!user) {
+
+                log.warn "User does not exist so creating new user."
+
+                user = new User(
+                        username: remoteUser,
+                        passwordHash: passwordHash,
+                        firstName: "REMOTE_USER",
+                        lastName: "${remoteUser}",
+                        metadata: randomPassword  // reversible autogenerated password
+                )
+                user.addMetaData(INTERNAL_PASSWORD,randomPassword)
+                user.save(flush: true, failOnError: true, insert: true)
+
+                Role role = Role.findByName(UserService.USER)
+                log.debug "adding role: ${role}"
+                user.addToRoles(role)
+                role.addToUsers(user)
+
+                if (this.defaultGroup) {
+                    log.debug "adding user to default group: ${this.defaultGroup}"
+                    UserGroup userGroup = UserGroup.findByName(this.defaultGroup)
+
+                    userGroup = userGroup ?: new UserGroup(name: this.defaultGroup).save(flush: true)
+
+                    user.addToUserGroups(userGroup)
+                }
+
+                role.save()
+                user.save(flush: true)
+                log.warn "User created ${user}"
+            }
+
+            authToken.password = user.getMetaData(INTERNAL_PASSWORD)
+            subject.login(authToken)
+
+            return true
+        } catch (AuthenticationException ae) {
+            log.error("Problem authenticating: " + ae.fillInStackTrace())
+            // force authentication
+            log.error "Failed to authenticate user ${authToken.username}, resaving password and forcing"
+            user.addMetaData(INTERNAL_PASSWORD,randomPassword)
+            user.passwordHash = passwordHash
+            log.warn("reset password and saving: " + user.getMetaData(INTERNAL_PASSWORD))
+            user.save(flush: true, failOnError: true, insert: false)
+            authToken.password = user.getMetaData(INTERNAL_PASSWORD)
+            log.warn("logging in again")
+            subject.login(authToken)
+            if (subject.authenticated) {
+                log.warn("success!")
+                return true
+            } else {
+                log.warn("fail!")
+                return false
+            }
+        }
+
+    }
+
+    //    @Override
+    def authenticate(UsernamePasswordToken authToken, HttpServletRequest request) {
+        // token is ignored
+        return authenticate(request)
+    }
+
+    def setDefaultGroup(String defaultGroup) {
+        this.defaultGroup = defaultGroup
+    }
+
+    @Override
+    Boolean requiresToken() {
+        return false
+    }
+}
diff --git a/grails-app/services/org/bbop/apollo/authenticator/UsernamePasswordAuthenticatorService.groovy b/grails-app/services/org/bbop/apollo/authenticator/UsernamePasswordAuthenticatorService.groovy
new file mode 100644
index 0000000..10b9d93
--- /dev/null
+++ b/grails-app/services/org/bbop/apollo/authenticator/UsernamePasswordAuthenticatorService.groovy
@@ -0,0 +1,41 @@
+package org.bbop.apollo.authenticator
+
+import grails.transaction.Transactional
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.session.Session
+import org.apache.shiro.subject.Subject
+
+import javax.servlet.http.HttpServletRequest
+
+ at Transactional
+class UsernamePasswordAuthenticatorService implements AuthenticatorService{
+
+    @Override
+    def authenticate(HttpServletRequest request) {
+        log.error("Not implemented without a token")
+        return false
+    }
+
+    def authenticate(UsernamePasswordToken authToken, HttpServletRequest request) {
+        try {
+            Subject subject = SecurityUtils.getSubject();
+            subject.login(authToken)
+            if (!subject.authenticated) {
+                log.error "Failed to authenticate user ${authToken.username}"
+                return false
+            }
+            return true
+        } catch (Exception ae) {
+            log.error("Problem authenticating: " + ae.fillInStackTrace())
+            return false
+        }
+
+    }
+
+
+    @Override
+    Boolean requiresToken() {
+        return true
+    }
+}
diff --git a/grails-app/taglib/org/bbop/apollo/PermissionTagLib.groovy b/grails-app/taglib/org/bbop/apollo/PermissionTagLib.groovy
new file mode 100644
index 0000000..139a4f8
--- /dev/null
+++ b/grails-app/taglib/org/bbop/apollo/PermissionTagLib.groovy
@@ -0,0 +1,29 @@
+package org.bbop.apollo
+
+class PermissionTagLib {
+//    static defaultEncodeAs = [taglib:'html']
+    static defaultEncodeAs = 'raw'
+    //static encodeAsForTags = [tagName: 'raw']
+    static namespace = 'perms'
+    //static encodeAsForTags = [tagName: [taglib:'html'], otherTagName: [taglib:'none']]
+
+    def permissionService
+
+    def isUserAdmin = { attrs, body ->
+        if (permissionService.isUserAdmin(attrs.user)) {
+            out << body()
+        }
+    }
+
+    def isUserNotAdmin = { attrs, body ->
+        if (!permissionService.isUserAdmin(attrs.user)) {
+            out << body()
+        }
+    }
+
+    def admin = { attrs, body ->
+        if (permissionService.admin) {
+            out << body()
+        }
+    }
+}
diff --git a/grails-app/views/annotator/adminPanel.gsp b/grails-app/views/annotator/adminPanel.gsp
new file mode 100644
index 0000000..4877992
--- /dev/null
+++ b/grails-app/views/annotator/adminPanel.gsp
@@ -0,0 +1,18 @@
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<html>
+<head>
+    <meta name="layout" content="embedded">
+</head>
+
+<body>
+
+
+<div class="list-group">
+    <g:each in="${links}" var="link">
+        <g:link class="list-group-item" target="_blank" uri="${link.link}">${link.label}</g:link>
+    </g:each>
+</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/grails-app/views/annotator/componentDiv.gsp b/grails-app/views/annotator/componentDiv.gsp
new file mode 100644
index 0000000..594d88f
--- /dev/null
+++ b/grails-app/views/annotator/componentDiv.gsp
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html ng-app="AnnotatorApplication">
+
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width">
+
+    <title>UI.Layout : holy grail demo</title>
+
+
+    %{--<asset:javascript library="jquery" plugin="jquery"/>--}%
+    <asset:javascript src="annotator/controllers/Annotator.js"/>
+    <asset:stylesheet src="annotator.css"/>
+</head>
+
+<body>
+
+<div ui-layout  >
+    <div class=" html-back" ></div>
+
+    <div class="left-cell" ui-layout="{flow : 'column'}" >
+        <div>
+            <form>
+                <table>
+                    <tr>
+                        <th>A</th>
+                        <th>B</th>
+                        <th>B</th>
+                    </tr>
+                    <tr>
+                    <td>1</td>
+                        <td>2</td>
+                        <td>3</td>
+                    </tr>
+                </table>
+            </form>
+
+        </div>
+        <div class=" js-back" ></div>
+        <div class=" css-back" ></div>
+    </div>
+
+    <div class=" css-back" ></div>
+</div>
+
+<!-- Le javascript -->
+%{--<script type="application/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>--}%
+<script>
+    angular.module('AnnotatorApplication', ['ui.layout']);
+</script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/grails-app/views/annotator/demo.gsp b/grails-app/views/annotator/demo.gsp
new file mode 100644
index 0000000..ed4ca75
--- /dev/null
+++ b/grails-app/views/annotator/demo.gsp
@@ -0,0 +1,40 @@
+<!-- The DOCTYPE declaration above will set the     -->
+<!-- browser's rendering engine into                -->
+<!-- "Standards Mode". Replacing this declaration   -->
+<!-- with a "Quirks Mode" doctype is not supported. -->
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<!DOCTYPE html>
+<html>
+<head>
+
+    <meta name="layout" content="main">
+    %{--<meta name="layout" content="main"/>--}%
+    <title>Annotator</title>
+
+</head>
+
+<body style="background-color: white;">
+
+<div style="padding: 20px;margin: 30px;">
+    <div class="dropdown">
+        <button class="btn btn-sm dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown"
+                aria-expanded="true">
+            Export
+            <span class="caret"></span>
+        </button>
+        <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
+            <li role="presentation"><a role="menuitem" tabindex="-1" href="#">GFF3</a></li>
+            <li role="presentation"><a role="menuitem" tabindex="-1" href="#">FASTA</a></li>
+            <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Chado</a></li>
+        </ul>
+    </div>
+</div>
+
+<br/>
+<br/>
+<br/>
+<br/>
+
+</body>
+</html>
diff --git a/grails-app/views/annotator/detail.gsp b/grails-app/views/annotator/detail.gsp
new file mode 100644
index 0000000..75eaeed
--- /dev/null
+++ b/grails-app/views/annotator/detail.gsp
@@ -0,0 +1,156 @@
+<%@ page import="org.bbop.apollo.gwt.shared.PermissionEnum; org.bbop.apollo.Feature" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="report">
+    <title>Annotators</title>
+
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+<div id="list-track" class="form-group report-header content scaffold-list" role="main">
+    <div class="page-header">
+
+        <h3>Annotation Details: <span class="label label-default"> ${annotatorInstance.firstName} ${annotatorInstance.lastName}
+                (${annotatorInstance.username})</span></h3>
+    </div>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+
+    <h3>Total</h3>
+    <table>
+        <thead>
+        <tr>
+            <th>Top Level Features</th>
+            <th>Genes</th>
+            <th>Transcripts</th>
+            <th>Exons</th>
+            <th>Transposable Elements</th>
+            <th>Repeat Regions</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td>
+                ${annotatorInstance.totalFeatureCount}
+            </td>
+            <td>${annotatorInstance.geneCount}</td>
+            <td>
+                <g:if test="${annotatorInstance.transcriptCount}">
+                    <div class="info-border">
+                        Total
+                        <span class="badge">${annotatorInstance.transcriptCount}</span>
+                    </div>
+                    <div class="info-border">
+                        Protein encoding
+                        <span class="badge"><g:formatNumber
+                                number="${annotatorInstance.proteinCodingTranscriptPercent}" type="percent"/></span>
+                    </div>
+                    <div class="info-border">
+                        Exons / transcript
+                        <span class="badge"><g:formatNumber number="${annotatorInstance.exonsPerTranscript}"
+                                                            type="number"/></span>
+                    </div>
+
+                    <g:each in="${annotatorInstance.transcriptTypeCount}" var="trans">
+                        <div class="info-border">
+                            ${trans.key}
+                            <span class="badge">
+                                ${trans.value}
+                            </span>
+                        </div>
+                    </g:each>
+                </g:if>
+                <g:else>0</g:else>
+            </td>
+            <td>${annotatorInstance.exonCount}</td>
+            <td>${annotatorInstance.transposableElementCount}</td>
+            <td>${annotatorInstance.repeatRegionCount}</td>
+        </tr>
+        </tbody>
+    </table>
+
+    <h3>Organism Breakdown</h3>
+    <table>
+        <thead>
+        <th>Organism</th>
+        <th>Permissions</th>
+        <th>Top Level Features</th>
+        <th>Genes</th>
+        <th>Exons</th>
+        <th>Transcripts</th>
+        <th>Transposable Elements</th>
+        <th>Repeat Regions</th>
+        </thead>
+        <tbody>
+        <g:each in="${annotatorInstance.userOrganismPermissionList}" var="userOrganismPermission">
+            <tr>
+                <td>
+                    <g:link action="report" id="${userOrganismPermission.userOrganismPermission.organism.id}"
+                            controller="sequence">
+                        ${userOrganismPermission.userOrganismPermission.organism.commonName}
+                    </g:link>
+                </td>
+                <td>
+                    <g:if test="${userOrganismPermission.userOrganismPermission.permissions.toLowerCase().contains(org.bbop.apollo.gwt.shared.PermissionEnum.ADMINISTRATE.display)}">
+                        <i class="glyphicon glyphicon-cog" title="Administrate privileges"></i>
+                    </g:if>
+                    <g:if test="${userOrganismPermission.userOrganismPermission.permissions.toLowerCase().contains(org.bbop.apollo.gwt.shared.PermissionEnum.WRITE.display)}">
+                        <i class="glyphicon glyphicon-edit" title="Edit privileges"></i>
+                    </g:if>
+                    <g:if test="${userOrganismPermission.userOrganismPermission.permissions.toLowerCase().contains(org.bbop.apollo.gwt.shared.PermissionEnum.EXPORT.display)}">
+                        <i class="glyphicon glyphicon-download-alt" title="Export privileges"></i>
+                    </g:if>
+                    <g:if test="${userOrganismPermission.userOrganismPermission.permissions.toLowerCase().contains(org.bbop.apollo.gwt.shared.PermissionEnum.READ.display)}">
+                        <i class="glyphicon glyphicon-search" title="Read privileges"></i>
+                    </g:if>
+                </td>
+                <td>
+                    ${userOrganismPermission.totalFeatureCount}
+                </td>
+                <td>${userOrganismPermission.geneCount}</td>
+                <td>
+                    <g:if test="${userOrganismPermission.transcriptCount}">
+                        <button>
+                            Total
+                            <span class="badge">${userOrganismPermission.transcriptCount}</span>
+                        </button>
+                        <button>
+                            Protein encoding
+                            <span class="badge"><g:formatNumber
+                                    number="${userOrganismPermission.proteinCodingTranscriptPercent}"
+                                    type="percent"/></span>
+                        </button>
+                        <button>
+                            Exons / transcript
+                            <span class="badge"><g:formatNumber number="${userOrganismPermission.exonsPerTranscript}"
+                                                                type="number"/></span>
+                        </button>
+
+                        <g:each in="${userOrganismPermission.transcriptTypeCount}" var="trans">
+                            <button>
+                                ${trans.key}
+                                <span class="badge">
+                                    ${trans.value}
+                                </span>
+                            </button>
+                        </g:each>
+                    </g:if>
+                    <g:else>0</g:else>
+                </td>
+                <td>${userOrganismPermission.exonCount}</td>
+                <td>${userOrganismPermission.transposableElementCount}</td>
+                <td>${userOrganismPermission.repeatRegionCount}</td>
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/annotator/index.gsp b/grails-app/views/annotator/index.gsp
new file mode 100644
index 0000000..5746303
--- /dev/null
+++ b/grails-app/views/annotator/index.gsp
@@ -0,0 +1,54 @@
+<!-- The DOCTYPE declaration above will set the     -->
+<!-- browser's rendering engine into                -->
+<!-- "Standards Mode". Replacing this declaration   -->
+<!-- with a "Quirks Mode" doctype is not supported. -->
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<!DOCTYPE html>
+<html>
+<head>
+
+    <meta name="layout" content="annotator2">
+    %{--<meta name="layout" content="main"/>--}%
+    <title>Annotator</title>
+
+    <asset:javascript src="spring-websocket"/>
+
+    <script type="text/javascript" language="javascript" src="annotator.nocache.js"></script>
+    <script>
+        %{--rootUrl: '${applicationContext.servletContext.getContextPath()}'--}%
+        var Options = {
+            showFrame: '${params.showFrame  && params.showFrame == 'true' ? 'true' : 'false' }'
+            ,userId: '${userKey}'
+            ,clientToken:'${clientToken}'
+//            ,top: "10"
+//            ,topUnit: "PCT" // PX, EM, PC, PT, IN, CM
+//            ,height: "80"
+//            ,heightUnit: "PCT" // PX, EM, PC, PT, IN, CM
+        };
+    </script>
+</head>
+
+<body style="background-color: white;">
+
+%{--<div style="top: 10%">--}%
+    %{--<h3>Custom Header</h3>--}%
+%{--</div>--}%
+
+%{--<div style="position: absolute;bottom: 0; height: 10%;">--}%
+    %{--<h3>Custom Footer</h3>--}%
+%{--</div>--}%
+
+<!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
+<noscript>
+    <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
+        Your web browser must have JavaScript enabled
+        in order for this application to display correctly.
+    </div>
+</noscript>
+
+%{--Version <g:meta name="app.version"/>--}%
+%{--Built with Grails <g:meta name="app.grails.version"/>--}%
+
+</body>
+</html>
diff --git a/grails-app/views/annotator/notAuthorized.gsp b/grails-app/views/annotator/notAuthorized.gsp
new file mode 100644
index 0000000..f4de87f
--- /dev/null
+++ b/grails-app/views/annotator/notAuthorized.gsp
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <title>Not Authorized</title>
+</head>
+<body>
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+    </ul>
+</div>
+<div id="list-cannedComment" class="content scaffold-list" role="main">
+    <h1>Not Authorized</h1>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/annotator/report.gsp b/grails-app/views/annotator/report.gsp
new file mode 100644
index 0000000..03e1fd2
--- /dev/null
+++ b/grails-app/views/annotator/report.gsp
@@ -0,0 +1,117 @@
+<%@ page import="org.bbop.apollo.gwt.shared.PermissionEnum; org.bbop.apollo.Feature" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="report">
+    <title>Annotators</title>
+
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+<div id="list-track" class="form-group report-header content scaffold-list" role="main">
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+            <g:sortableColumn property="username" title="Username"/>
+            <g:sortableColumn property="firstName" title="First Name"/>
+            <g:sortableColumn property="lastName" title="Last Name"/>
+            <th>Top Level Features</th>
+            <th>Genes</th>
+            <th>Transcripts</th>
+            <th>Exons</th>
+            <th>TE</th>
+            <th>RR</th>
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${annotatorInstanceList}" status="i" var="annotatorInstance">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+                <td>
+                    <a style="margin: 2px;padding: 2px;" href='<g:createLink action="detail" controller="annotator"
+                                                                             id="${annotatorInstance.annotator.id}">${annotatorInstance.username}</g:createLink>'
+                       class="btn btn-default">
+                        ${annotatorInstance.username}
+                    </a>
+                    <br/>
+                    <perms:isUserAdmin user="${annotatorInstance.annotator}">
+                        <span class="label label-default">Admin</span>
+                    </perms:isUserAdmin>
+                    <perms:isUserNotAdmin user="${annotatorInstance.annotator}">
+                        <g:if test="${annotatorInstance.userOrganismPermissionList}">
+                            <g:each in="${annotatorInstance.userOrganismPermissionList}" var="permission">
+                                <g:if test="${permission.userOrganismPermission.permissionValues}">
+                                    <span>
+                                        ${permission.userOrganismPermission.organism.commonName}
+                                        <g:each in="${permission.userOrganismPermission.permissionValues}" var="pValue">
+                                            <span class="badge">
+                                                ${pValue}
+                                            </span>
+                                        </g:each>
+                                    </span>
+                                    <br/>
+                                </g:if>
+                            </g:each>
+                        </g:if>
+                    </perms:isUserNotAdmin>
+                </td>
+                <td style="text-align: left;">
+                    ${annotatorInstance.firstName}
+                </td>
+                <td>
+                    ${annotatorInstance.lastName}
+                </td>
+                <td>
+                    ${annotatorInstance.totalFeatureCount}
+                </td>
+                <td>${annotatorInstance.geneCount}</td>
+                <td>
+                    <g:if test="${annotatorInstance.transcriptCount}">
+                        <div class="info-border">
+                            Total
+                            <span class="badge">${annotatorInstance.transcriptCount}</span>
+                        </div>
+
+                        <div class="info-border">
+                            Protein encoding
+                            <span class="badge"><g:formatNumber
+                                    number="${annotatorInstance.proteinCodingTranscriptPercent}" type="percent"/></span>
+                        </div>
+
+                        <div class="info-border">
+                            Exons / transcript
+                            <span class="badge"><g:formatNumber number="${annotatorInstance.exonsPerTranscript}"
+                                                                type="number"/></span>
+                        </div>
+
+                        <g:each in="${annotatorInstance.transcriptTypeCount}" var="trans">
+                            <div class="info-border">
+                                ${trans.key}
+                                <span class="badge">
+                                    ${trans.value}
+                                </span>
+                            </div>
+                        </g:each>
+                    </g:if>
+                    <g:else>0</g:else>
+                </td>
+                <td>${annotatorInstance.exonCount}</td>
+                <td>${annotatorInstance.transposableElementCount}</td>
+                <td>${annotatorInstance.repeatRegionCount}</td>
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${annotatorInstanceCount ?: 0}"/>
+    </div>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/annotator/splitter.gsp b/grails-app/views/annotator/splitter.gsp
new file mode 100644
index 0000000..a586c3a
--- /dev/null
+++ b/grails-app/views/annotator/splitter.gsp
@@ -0,0 +1,52 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: ndunn
+  Date: 12/3/14
+  Time: 4:42 PM
+--%>
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<html ng-app="AnnotatorApplication">
+<head>
+    %{--<meta name="layout" content="main"/>--}%
+    <title>Annotator</title>
+
+    <asset:javascript src="annotator/controllers/Annotator.js"/>
+    <asset:stylesheet src="annotator.css"/>
+</head>
+
+<body>
+
+<div class="wrap">
+    <div class="resizable resizable1" ng-controller="AnnotatorController">
+        <div>
+            ASDFASDFADSFADFSDF ASDF ASDF ASDF
+        </div>
+        %{--<button class="btn btn-default btn-lg" ng-click="pingSelf()">Ping</button>--}%
+        %{--<tabset>--}%
+        %{--<tab heading="Static title">Static content</tab>--}%
+        %{--<tab ng-repeat="tab in tabs" heading="{{tab.title}}">--}%
+        %{--<div>--}%
+        %{--<ui-layout options="{flow : 'row'}">--}%
+        %{--<div>A {{tab.title}}</div>--}%
+
+        %{--<div>B {{tab.content}}</div>--}%
+        %{--</ui-layout>--}%
+        %{--</div>--}%
+        %{--</tab>--}%
+        %{--<tab select="alertMe()">--}%
+        %{--<tab-heading>--}%
+        %{--<i class="glyphicon glyphicon-bell"></i> Alert!--}%
+        %{--</tab-heading>--}%
+        %{--I've got an HTML heading, and a select callback. Pretty cool!--}%
+        %{--</tab>--}%
+        %{--</tabset>--}%
+
+    </div>
+
+    <div class="resizable resizable2"></div>
+</div>
+
+</body>
+
+</html>
\ No newline at end of file
diff --git a/grails-app/views/annotator/version.gsp b/grails-app/views/annotator/version.gsp
new file mode 100644
index 0000000..68fa51d
--- /dev/null
+++ b/grails-app/views/annotator/version.gsp
@@ -0,0 +1,49 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: ndunn
+  Date: 4/13/15
+  Time: 2:18 PM
+--%>
+
+<%@ page import="grails.util.Metadata" contentType="text/html;charset=UTF-8" %>
+<html>
+<head>
+    <meta name="layout" content="oldlook">
+    <title>Apollo Version</title>
+</head>
+
+<body>
+
+<nav class="navbar navbar-custom" role="navigation">
+    <div class="navbar-header">
+        <div class="container-fluid">
+            <a class="navbar-brand" href="http://genomearchitect.org/" target="_blank">
+                <g:img style="padding: 0;margin: -18px; height:53px;" id="logo" file="ApolloLogo_100x36.png"/>
+            </a>
+        </div>
+    </div>
+
+</nav>
+
+<div id="list-track" class="content scaffold-list" role="main">
+    <h3>Apollo Genome Annotator</h3>
+    <ul>
+        <li>Version:
+            %{--${grails.util.Metadata.current[attrs[app.version]]}--}%
+            <g:if test="${grails.util.Metadata.current['app.version'].contains('SNAPSHOT')}">
+               <a href='https://github.com/GMOD/Apollo/releases'><g:meta name="app.version"/></a>
+            </g:if>
+            <g:else>
+                <a href='https://github.com/GMOD/Apollo/releases/tag/<g:meta name="app.version"/>'><g:meta name="app.version"/></a>
+            </g:else>
+        </li>
+        <li>Grails version: <g:meta name="app.grails.version"/></li>
+        <li>Groovy version: ${GroovySystem.getVersion()}</li>
+        <li>JVM version: ${System.getProperty('java.version')}</li>
+        <li>Servlet Container Version: ${application.getServerInfo()}</li>
+    </ul>
+
+</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/grails-app/views/annotator/workingDiv.gsp b/grails-app/views/annotator/workingDiv.gsp
new file mode 100644
index 0000000..fa4bf9d
--- /dev/null
+++ b/grails-app/views/annotator/workingDiv.gsp
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html ng-app="x">
+
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width">
+
+    <title>UI.Layout : holy grail demo</title>
+
+    <link rel="stylesheet" href="https://rawgithub.com/angular-ui/ui-layout/v0.0.0/ui-layout.css">
+    %{--<link rel="stylesheet" href="style.css">--}%
+    <style>
+    /* Styles go here */
+
+
+    .html-back{
+        background : #eee url("http://placehold.it/400x300/eee/666&text=HTML") no-repeat center;
+
+    }
+    .css-back{
+        background : #eee url("http://placehold.it/400x300/eee/666&text=CSS") no-repeat center;
+
+    }
+    .js-back{
+        background : #eee url("http://placehold.it/400x300/eee/666&text=JS") no-repeat center;
+    }
+
+    </style>
+</head>
+
+<body>
+
+<div ui-layout  >
+    <div class=" html-back" ></div>
+
+    <div ui-layout="{flow : 'column'}" >
+        <div class=" html-back" ></div>
+        <div class=" js-back" ></div>
+        <div class=" css-back" ></div>
+    </div>
+
+    <div class=" css-back" ></div>
+</div>
+
+<!-- Le javascript -->
+<script type="application/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>
+<script type="application/javascript" src="https://rawgithub.com/angular-ui/ui-layout/v0.0.0/ui-layout.min.js"></script>
+<script>
+    angular.module('x', ['ui.layout']);
+</script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/grails-app/views/auth/login.gsp b/grails-app/views/auth/login.gsp
new file mode 100644
index 0000000..70a66e1
--- /dev/null
+++ b/grails-app/views/auth/login.gsp
@@ -0,0 +1,68 @@
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <meta name="layout" content="login"/>
+    <title>Login</title>
+</head>
+
+<div>
+    <g:if test="${flash.message}">
+        <div class="message">${flash.message}</div>
+    </g:if>
+    <g:form action="signIn">
+        <input type="hidden" name="targetUri" value="${targetUri}" class="col-md-4"/>
+        <div class="col-md-5 col-lg-offset-1" style="margin-top: 10px;">
+            %{--<input type="username" class="form-control" placeholder="Username (email)" required autofocus>--}%
+            %{--<input type="password" class="form-control" placeholder="Password" required>--}%
+            %{--<button class="btn btn-lg btn-primary btn-block" type="submit">--}%
+            %{--Sign in</button>--}%
+            %{--<label class="checkbox pull-left">--}%
+            %{--<input type="checkbox" value="${rememberMe}">--}%
+            %{--Remember me--}%
+            %{--</label>--}%
+            %{--<table>--}%
+            %{--<tbody>--}%
+            %{--<tr>--}%
+            %{--<td>Username:</td>--}%
+            %{--<td>--}%
+            <input name="username" value="${username}" type="username" class="form-control col-md-4"
+                   style="margin:10px;"
+                   placeholder="Username (email)"
+                   required autofocus/>
+            %{--</td>--}%
+            %{--</tr>--}%
+            %{--<tr>--}%
+            %{--<td>Password:</td>--}%
+            %{--<td><input type="password" name="password" value="" /></td>--}%
+            <input type="password" name="password" value="" class="form-control" placeholder="Password" required
+                   style="margin:10px;"/>
+            %{--</tr>--}%
+            %{--<tr>--}%
+            %{--<td>Remember me?:</td>--}%
+            %{--<td><g:checkBox name="rememberMe" value="${rememberMe}" /></td>--}%
+            %{--<div class="col-md-5 row" style="margin-bottom: 10px;">--}%
+                <input class="col-md-4 col-md-offset-1 btn btn-lg btn-primary" type="submit"
+                       value="Login">
+
+                <label class="checkbox col-md-4" style="margin:10px;">
+                    <g:checkBox name="rememberMe" value="${rememberMe}"/>
+                    <input class="checkbox" type="checkbox" name="rememberMe" value="${rememberMe}"/>
+                    Remember me
+                </label>
+            %{--</div>--}%
+
+            %{--<div class="col-md-5 row">--}%
+            %{--</div>--}%
+            %{--</tr>--}%
+            %{--<tr>--}%
+            %{--<td />--}%
+            %{--<input type="submit" value="Sign in"/>--}%
+            %{--<button class="btn btn-lg btn-primary btn-block" type="submit" value="Login">--}%
+            %{--<td><input type="submit" value="Sign in" /></td>--}%
+            %{--</tr>--}%
+            %{--</tbody>--}%
+            %{--</table>--}%
+        </div>
+    </g:form>
+</body>
+</html>
diff --git a/grails-app/views/auth/unauthorized.gsp b/grails-app/views/auth/unauthorized.gsp
new file mode 100644
index 0000000..efe911e
--- /dev/null
+++ b/grails-app/views/auth/unauthorized.gsp
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <meta name="layout" content="main"/>
+    <title>
+        Unauthorized
+    </title>
+    <meta name="unauthorized-page"/>
+
+
+</head>
+
+<body>
+<g:render template="../layouts/reportHeader"/>
+<div class="jumbotron" style="padding-left: 30px;">
+    <h1>
+        Unauthorized
+    </h1>
+    Unauthorized.  You do not have permission to access this page.
+</div>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/grails-app/views/availableStatus/_form.gsp b/grails-app/views/availableStatus/_form.gsp
new file mode 100644
index 0000000..9054ca1
--- /dev/null
+++ b/grails-app/views/availableStatus/_form.gsp
@@ -0,0 +1,38 @@
+<%@ page import="org.bbop.apollo.AvailableStatus" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: availableStatusInstance, field: 'value', 'error')} required">
+	<label for="value">
+		<g:message code="availableStatus.value.label" default="Value" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="value" required="" value="${availableStatusInstance?.value}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: availableStatusInstance, field: 'featureTypes', 'error')} ">
+	<label for="featureTypes">
+		<g:message code="availableStatus.featureTypes.label" default="Feature Types" />
+
+	</label>
+	<g:select name="featureTypes" from="${org.bbop.apollo.FeatureType.list()}"
+			  multiple="multiple"
+			  optionKey="id" size="10"
+			  optionValue="display"
+			  value="${availableStatusInstance?.featureTypes*.id}" class="many-to-many"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: availableStatusInstance, field: 'organisms', 'error')} ">
+	<label for="organisms">
+		<g:message code="availableStatus.organisms.label" default="Organisms" />
+
+	</label>
+	<g:select name="organisms" from="${org.bbop.apollo.Organism.list()}"
+			  multiple="multiple"
+			  optionKey="id" size="10"
+			  optionValue="commonName"
+			  value="${organismFilters?.organism?.id}" class="many-to-many"/>
+
+</div>
diff --git a/grails-app/views/availableStatus/create.gsp b/grails-app/views/availableStatus/create.gsp
new file mode 100644
index 0000000..a9afb6b
--- /dev/null
+++ b/grails-app/views/availableStatus/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'availableStatus.label', default: 'AvailableStatus')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-availableStatus" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-availableStatus" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${availableStatusInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${availableStatusInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:availableStatusInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/availableStatus/edit.gsp b/grails-app/views/availableStatus/edit.gsp
new file mode 100644
index 0000000..c990d6f
--- /dev/null
+++ b/grails-app/views/availableStatus/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.AvailableStatus" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'availableStatus.label', default: 'AvailableStatus')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-availableStatus" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-availableStatus" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${availableStatusInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${availableStatusInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:availableStatusInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${availableStatusInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/availableStatus/index.gsp b/grails-app/views/availableStatus/index.gsp
new file mode 100644
index 0000000..fa46918
--- /dev/null
+++ b/grails-app/views/availableStatus/index.gsp
@@ -0,0 +1,58 @@
+
+<%@ page import="org.bbop.apollo.AvailableStatus" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'availableStatus.label', default: 'AvailableStatus')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-availableStatus" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-availableStatus" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+					
+						<g:sortableColumn property="value" title="${message(code: 'availableStatus.value.label', default: 'Value')}" />
+						<th>Feature Types</th>
+						<th>Organisms</th>
+
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${availableStatusInstanceList}" status="i" var="availableStatusInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+					
+						<td><g:link action="show" id="${availableStatusInstance.id}">${fieldValue(bean: availableStatusInstance, field: "value")}</g:link></td>
+
+						<td>
+							<g:each in="${availableStatusInstance.featureTypes.sort() { a,b -> a.display <=> b.display }}" var="featureType">
+								${featureType.type}:${featureType.name}
+							</g:each>
+						</td>
+						<td>
+							<g:each in="${organismFilters.get(availableStatusInstance)}" var="filter">
+								<g:link controller="organism" id="${filter.organism.id}">${filter.organism.commonName}</g:link>
+							</g:each>
+						</td>
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${availableStatusInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/availableStatus/show.gsp b/grails-app/views/availableStatus/show.gsp
new file mode 100644
index 0000000..78696d6
--- /dev/null
+++ b/grails-app/views/availableStatus/show.gsp
@@ -0,0 +1,79 @@
+<%@ page import="org.bbop.apollo.AvailableStatus" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'availableStatus.label', default: 'AvailableStatus')}"/>
+    <title><g:message code="default.show.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#show-availableStatus" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                      default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]"/></g:link></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="show-availableStatus" class="content scaffold-show" role="main">
+    <h1><g:message code="default.show.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <ol class="property-list availableStatus">
+
+        <g:if test="${availableStatusInstance?.value}">
+            <li class="fieldcontain">
+                <span id="value-label" class="property-label"><g:message code="availableStatus.value.label"
+                                                                         default="Value"/></span>
+
+                <span class="property-value" aria-labelledby="value-label"><g:fieldValue
+                        bean="${availableStatusInstance}" field="value"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${availableStatusInstance?.featureTypes}">
+            <li class="fieldcontain">
+                <span id="featureTypes-label" class="property-label"><g:message code="availableStatus.featureTypes.label"
+                                                                                default="Feature Types"/></span>
+
+                <g:each in="${availableStatusInstance.featureTypes}" var="f">
+                    <span class="property-value" aria-labelledby="featureTypes-label"><g:link controller="featureType"
+                                                                                              action="show"
+                                                                                              id="${f.id}">${f?.name}</g:link></span>
+                </g:each>
+
+            </li>
+        </g:if>
+
+        <g:if test="${organismFilters}">
+            <li class="fieldcontain">
+                <span id="organisms-label" class="property-label"><g:message code="availableStatus.organisms.label"
+                                                                             default="Organisms"/></span>
+
+                <g:each in="${organismFilters}" var="f">
+                    <span class="property-value" aria-labelledby="organisms-label"><g:link controller="organism"
+                                                                                           action="show"
+                                                                                           id="${f.id}">${f?.organism.commonName}</g:link></span>
+                </g:each>
+            </li>
+        </g:if>
+    </ol>
+    <g:form url="[resource: availableStatusInstance, action: 'delete']" method="DELETE">
+        <fieldset class="buttons">
+            <g:link class="edit" action="edit" resource="${availableStatusInstance}"><g:message
+                    code="default.button.edit.label" default="Edit"/></g:link>
+            <g:actionSubmit class="delete" action="delete"
+                            value="${message(code: 'default.button.delete.label', default: 'Delete')}"
+                            onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');"/>
+        </fieldset>
+    </g:form>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/cannedComment/_form.gsp b/grails-app/views/cannedComment/_form.gsp
new file mode 100644
index 0000000..9c38b48
--- /dev/null
+++ b/grails-app/views/cannedComment/_form.gsp
@@ -0,0 +1,47 @@
+<%@ page import="org.bbop.apollo.CannedComment" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: cannedCommentInstance, field: 'comment', 'error')} required">
+	<label for="comment">
+		<g:message code="cannedComment.comment.label" default="Comment" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="comment" required="" value="${cannedCommentInstance?.comment}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedCommentInstance, field: 'metadata', 'error')} ">
+	<label for="metadata">
+		<g:message code="cannedComment.metadata.label" default="Metadata" />
+		
+	</label>
+	<g:textField name="metadata" value="${cannedCommentInstance?.metadata}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedCommentInstance, field: 'featureTypes', 'error')} ">
+	<label for="featureTypes">
+		<g:message code="cannedComment.featureTypes.label" default="Feature Types" />
+		
+	</label>
+	<g:select name="featureTypes" from="${org.bbop.apollo.FeatureType.list()}"
+              multiple="multiple"
+              optionKey="id" size="10"
+              optionValue="display"
+              value="${cannedCommentInstance?.featureTypes*.id}" class="many-to-many"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedCommentInstance, field: 'organisms', 'error')} ">
+	<label for="organisms">
+		<g:message code="cannedComment.organisms.label" default="Organisms" />
+
+	</label>
+	<g:select name="organisms" from="${org.bbop.apollo.Organism.list()}"
+			  multiple="multiple"
+			  optionKey="id" size="10"
+			  optionValue="commonName"
+			  value="${organismFilters?.organism?.id}" class="many-to-many"/>
+
+</div>
diff --git a/grails-app/views/cannedComment/create.gsp b/grails-app/views/cannedComment/create.gsp
new file mode 100644
index 0000000..d2af587
--- /dev/null
+++ b/grails-app/views/cannedComment/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedComment.label', default: 'CannedComment')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-cannedComment" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-cannedComment" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedCommentInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedCommentInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedCommentInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedComment/edit.gsp b/grails-app/views/cannedComment/edit.gsp
new file mode 100644
index 0000000..2cc6254
--- /dev/null
+++ b/grails-app/views/cannedComment/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.CannedComment" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedComment.label', default: 'CannedComment')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-cannedComment" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-cannedComment" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedCommentInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedCommentInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedCommentInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${cannedCommentInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedComment/index.gsp b/grails-app/views/cannedComment/index.gsp
new file mode 100644
index 0000000..453ce3d
--- /dev/null
+++ b/grails-app/views/cannedComment/index.gsp
@@ -0,0 +1,64 @@
+
+<%@ page import="org.bbop.apollo.CannedComment" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedComment.label', default: 'CannedComment')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-cannedComment" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-cannedComment" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+					
+						<g:sortableColumn property="comment" title="${message(code: 'cannedComment.comment.label', default: 'Comment')}" />
+
+						%{--<g:sortableColumn property="metadata" title="${message(code: 'cannedComment.featureTypes.label', default: 'Feature Types')}" />--}%
+						<th>Feature Types</th>
+						<th>Organisms</th>
+						<g:sortableColumn property="metadata" title="${message(code: 'cannedComment.metadata.label', default: 'Metadata')}" />
+					
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${cannedCommentInstanceList}" status="i" var="cannedCommentInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+					
+						<td><g:link action="show" id="${cannedCommentInstance.id}">${fieldValue(bean: cannedCommentInstance, field: "comment")}</g:link></td>
+
+						<td>
+							<g:each in="${cannedCommentInstance.featureTypes.sort() { a,b -> a.display <=> b.display }}" var="featureType">
+								${featureType.type}:${featureType.name}
+							</g:each>
+						</td>
+						<td>
+							<g:each in="${organismFilters.get(cannedCommentInstance)}" var="filter">
+								<g:link controller="organism" id="${filter.organism.id}">${filter.organism.commonName}</g:link>
+							</g:each>
+						</td>
+
+						<td>${fieldValue(bean: cannedCommentInstance, field: "metadata")}</td>
+					
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${cannedCommentInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedComment/show.gsp b/grails-app/views/cannedComment/show.gsp
new file mode 100644
index 0000000..8bf58a8
--- /dev/null
+++ b/grails-app/views/cannedComment/show.gsp
@@ -0,0 +1,91 @@
+<%@ page import="org.bbop.apollo.CannedComment" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'cannedComment.label', default: 'CannedComment')}"/>
+    <title><g:message code="default.show.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#show-cannedComment" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                    default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]"/></g:link></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="show-cannedComment" class="content scaffold-show" role="main">
+    <h1><g:message code="default.show.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <ol class="property-list cannedComment">
+
+        <g:if test="${cannedCommentInstance?.comment}">
+            <li class="fieldcontain">
+                <span id="comment-label" class="property-label"><g:message code="cannedComment.comment.label"
+                                                                           default="Comment"/></span>
+
+                <span class="property-value" aria-labelledby="comment-label"><g:fieldValue
+                        bean="${cannedCommentInstance}" field="comment"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedCommentInstance?.metadata}">
+            <li class="fieldcontain">
+                <span id="metadata-label" class="property-label"><g:message code="cannedComment.metadata.label"
+                                                                            default="Metadata"/></span>
+
+                <span class="property-value" aria-labelledby="metadata-label"><g:fieldValue
+                        bean="${cannedCommentInstance}" field="metadata"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedCommentInstance?.featureTypes}">
+            <li class="fieldcontain">
+                <span id="featureTypes-label" class="property-label"><g:message code="cannedComment.featureTypes.label"
+                                                                                default="Feature Types"/></span>
+
+                <g:each in="${cannedCommentInstance.featureTypes}" var="f">
+                    <span class="property-value" aria-labelledby="featureTypes-label"><g:link controller="featureType"
+                                                                                              action="show"
+                                                                                              id="${f.id}">${f?.name}</g:link></span>
+                </g:each>
+
+            </li>
+        </g:if>
+
+        <g:if test="${organismFilters}">
+            <li class="fieldcontain">
+                <span id="organisms-label" class="property-label"><g:message code="cannedComment.organisms.label"
+                                                                             default="Organisms"/></span>
+
+                <g:each in="${organismFilters}" var="f">
+                    <span class="property-value" aria-labelledby="organisms-label"><g:link controller="organism"
+                                                                                           action="show"
+                                                                                           id="${f.id}">${f?.organism.commonName}</g:link></span>
+                </g:each>
+            </li>
+        </g:if>
+
+    </ol>
+    <g:form url="[resource: cannedCommentInstance, action: 'delete']" method="DELETE">
+        <fieldset class="buttons">
+            <g:link class="edit" action="edit" resource="${cannedCommentInstance}"><g:message
+                    code="default.button.edit.label" default="Edit"/></g:link>
+            <g:actionSubmit class="delete" action="delete"
+                            value="${message(code: 'default.button.delete.label', default: 'Delete')}"
+                            onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');"/>
+        </fieldset>
+    </g:form>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/cannedKey/_form.gsp b/grails-app/views/cannedKey/_form.gsp
new file mode 100644
index 0000000..64ab0c5
--- /dev/null
+++ b/grails-app/views/cannedKey/_form.gsp
@@ -0,0 +1,45 @@
+<%@ page import="org.bbop.apollo.CannedKey" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: cannedKeyInstance, field: 'label', 'error')} required">
+    <label for="label">
+        <g:message code="cannedKey.label.label" default="Label"/>
+        <span class="required-indicator">*</span>
+    </label>
+    <g:textField name="label" required="" value="${cannedKeyInstance?.label}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedKeyInstance, field: 'metadata', 'error')} ">
+    <label for="metadata">
+        <g:message code="cannedKey.metadata.label" default="Metadata"/>
+
+    </label>
+    <g:textField name="metadata" value="${cannedKeyInstance?.metadata}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedKeyInstance, field: 'featureTypes', 'error')} ">
+    <label for="featureTypes">
+        <g:message code="cannedKey.featureTypes.label" default="Feature Types"/>
+
+    </label>
+    <g:select name="featureTypes" from="${org.bbop.apollo.FeatureType.list()}" multiple="multiple" optionKey="id"
+              size="10" value="${cannedKeyInstance?.featureTypes*.id}" class="many-to-many" optionValue="display"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedKeyInstance, field: 'organisms', 'error')} ">
+    <label for="organisms">
+        <g:message code="cannedKey.organisms.label" default="Organisms" />
+
+    </label>
+    <g:select name="organisms" from="${org.bbop.apollo.Organism.list()}"
+              multiple="multiple"
+              optionKey="id" size="10"
+              optionValue="commonName"
+              value="${organismFilters?.organism?.id}" class="many-to-many"/>
+
+</div>
+
diff --git a/grails-app/views/cannedKey/create.gsp b/grails-app/views/cannedKey/create.gsp
new file mode 100644
index 0000000..d281415
--- /dev/null
+++ b/grails-app/views/cannedKey/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedKey.label', default: 'CannedKey')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-cannedKey" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-cannedKey" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedKeyInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedKeyInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedKeyInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedKey/edit.gsp b/grails-app/views/cannedKey/edit.gsp
new file mode 100644
index 0000000..b67e029
--- /dev/null
+++ b/grails-app/views/cannedKey/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.CannedKey" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedKey.label', default: 'CannedKey')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-cannedKey" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-cannedKey" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedKeyInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedKeyInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedKeyInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${cannedKeyInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedKey/index.gsp b/grails-app/views/cannedKey/index.gsp
new file mode 100644
index 0000000..4db6a87
--- /dev/null
+++ b/grails-app/views/cannedKey/index.gsp
@@ -0,0 +1,63 @@
+
+<%@ page import="org.bbop.apollo.CannedKey" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedKey.label', default: 'CannedKey')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-cannedKey" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-cannedKey" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+					
+						<g:sortableColumn property="label" title="${message(code: 'cannedKey.label.label', default: 'Label')}" />
+						<th>Feature Types</th>
+						<th>Organisms</th>
+
+						<g:sortableColumn property="metadata" title="${message(code: 'cannedKey.metadata.label', default: 'Metadata')}" />
+					
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${cannedKeyInstanceList}" status="i" var="cannedKeyInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+					
+						<td><g:link action="show" id="${cannedKeyInstance.id}">${fieldValue(bean: cannedKeyInstance, field: "label")}</g:link></td>
+
+						<td>
+							<g:each in="${cannedKeyInstance.featureTypes.sort() { a,b -> a.display <=> b.display }}" var="featureType">
+								${featureType.type}:${featureType.name}
+							</g:each>
+						</td>
+						<td>
+							<g:each in="${organismFilters.get(cannedKeyInstance)}" var="filter">
+								<g:link controller="organism" id="${filter.organism.id}">${filter.organism.commonName}</g:link>
+							</g:each>
+						</td>
+
+						<td>${fieldValue(bean: cannedKeyInstance, field: "metadata")}</td>
+					
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${cannedKeyInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedKey/show.gsp b/grails-app/views/cannedKey/show.gsp
new file mode 100644
index 0000000..236ebd4
--- /dev/null
+++ b/grails-app/views/cannedKey/show.gsp
@@ -0,0 +1,102 @@
+<%@ page import="org.bbop.apollo.CannedKey" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'cannedKey.label', default: 'CannedKey')}"/>
+    <title><g:message code="default.show.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#show-cannedKey" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]"/></g:link></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="show-cannedKey" class="content scaffold-show" role="main">
+    <h1><g:message code="default.show.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <ol class="property-list cannedKey">
+
+        <g:if test="${cannedKeyInstance?.label}">
+            <li class="fieldcontain">
+                <span id="label-label" class="property-label"><g:message code="cannedKey.label.label"
+                                                                         default="Label"/></span>
+
+                <span class="property-value" aria-labelledby="label-label"><g:fieldValue bean="${cannedKeyInstance}"
+                                                                                         field="label"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedKeyInstance?.metadata}">
+            <li class="fieldcontain">
+                <span id="metadata-label" class="property-label"><g:message code="cannedKey.metadata.label"
+                                                                            default="Metadata"/></span>
+
+                <span class="property-value" aria-labelledby="metadata-label"><g:fieldValue bean="${cannedKeyInstance}"
+                                                                                            field="metadata"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedKeyInstance?.featureTypes}">
+            <li class="fieldcontain">
+                <span id="featureTypes-label" class="property-label"><g:message code="cannedKey.featureTypes.label"
+                                                                                default="Feature Types"/></span>
+
+                <g:each in="${cannedKeyInstance.featureTypes}" var="f">
+                    <span class="property-value" aria-labelledby="featureTypes-label"><g:link controller="featureType"
+                                                                                              action="show"
+                                                                                              id="${f.id}">${f?.display}</g:link></span>
+                </g:each>
+
+            </li>
+        </g:if>
+
+        <g:if test="${organismFilters}">
+            <li class="fieldcontain">
+                <span id="organisms-label" class="property-label"><g:message code="cannedKey.organisms.label"
+                                                                             default="Organisms"/></span>
+
+                <g:each in="${organismFilters}" var="f">
+                    <span class="property-value" aria-labelledby="organisms-label"><g:link controller="organism"
+                                                                                           action="show"
+                                                                                           id="${f.id}">${f?.organism.commonName}</g:link></span>
+                </g:each>
+            </li>
+        </g:if>
+
+    %{--<g:if test="${cannedKeyInstance?.values}">--}%
+    %{--<li class="fieldcontain">--}%
+    %{--<span id="values-label" class="property-label"><g:message code="cannedKey.values.label" default="Values" /></span>--}%
+    %{----}%
+    %{--<g:each in="${cannedKeyInstance.values}" var="v">--}%
+    %{--<span class="property-value" aria-labelledby="values-label"><g:link controller="cannedValue" action="show" id="${v.id}">${v?.label}</g:link></span>--}%
+    %{--</g:each>--}%
+    %{----}%
+    %{--</li>--}%
+    %{--</g:if>--}%
+
+    </ol>
+    <g:form url="[resource: cannedKeyInstance, action: 'delete']" method="DELETE">
+        <fieldset class="buttons">
+            <g:link class="edit" action="edit" resource="${cannedKeyInstance}"><g:message
+                    code="default.button.edit.label" default="Edit"/></g:link>
+            <g:actionSubmit class="delete" action="delete"
+                            value="${message(code: 'default.button.delete.label', default: 'Delete')}"
+                            onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');"/>
+        </fieldset>
+    </g:form>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/cannedValue/_form.gsp b/grails-app/views/cannedValue/_form.gsp
new file mode 100644
index 0000000..20b249c
--- /dev/null
+++ b/grails-app/views/cannedValue/_form.gsp
@@ -0,0 +1,43 @@
+<%@ page import="org.bbop.apollo.CannedValue" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: cannedValueInstance, field: 'label', 'error')} required">
+	<label for="label">
+		<g:message code="cannedValue.label.label" default="Label" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="label" required="" value="${cannedValueInstance?.label}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedValueInstance, field: 'metadata', 'error')} ">
+	<label for="metadata">
+		<g:message code="cannedValue.metadata.label" default="Metadata" />
+		
+	</label>
+	<g:textField name="metadata" value="${cannedValueInstance?.metadata}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedValueInstance, field: 'featureTypes', 'error')} ">
+	<label for="featureTypes">
+		<g:message code="cannedValue.featureTypes.label" default="Feature Types" />
+		
+	</label>
+	<g:select name="featureTypes" from="${org.bbop.apollo.FeatureType.list()}" multiple="multiple" optionKey="id" size="10" value="${cannedValueInstance?.featureTypes*.id}" class="many-to-many" optionValue="display"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: cannedValueInstance, field: 'organisms', 'error')} ">
+	<label for="organisms">
+		<g:message code="cannedValue.organisms.label" default="Organisms" />
+
+	</label>
+	<g:select name="organisms" from="${org.bbop.apollo.Organism.list()}"
+			  multiple="multiple"
+			  optionKey="id" size="10"
+			  optionValue="commonName"
+			  value="${organismFilters?.organism?.id}" class="many-to-many"/>
+
+</div>
diff --git a/grails-app/views/cannedValue/create.gsp b/grails-app/views/cannedValue/create.gsp
new file mode 100644
index 0000000..ba3311d
--- /dev/null
+++ b/grails-app/views/cannedValue/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedValue.label', default: 'CannedValue')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-cannedValue" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-cannedValue" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedValueInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedValueInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedValueInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedValue/edit.gsp b/grails-app/views/cannedValue/edit.gsp
new file mode 100644
index 0000000..053bf44
--- /dev/null
+++ b/grails-app/views/cannedValue/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.CannedValue" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'cannedValue.label', default: 'CannedValue')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-cannedValue" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-cannedValue" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${cannedValueInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${cannedValueInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:cannedValueInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${cannedValueInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/cannedValue/index.gsp b/grails-app/views/cannedValue/index.gsp
new file mode 100644
index 0000000..2c8feef
--- /dev/null
+++ b/grails-app/views/cannedValue/index.gsp
@@ -0,0 +1,70 @@
+<%@ page import="org.bbop.apollo.CannedValue" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'cannedValue.label', default: 'CannedValue')}"/>
+    <title><g:message code="default.list.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#list-cannedValue" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                  default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="list-cannedValue" class="content scaffold-list" role="main">
+    <h1><g:message code="default.list.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+
+            <g:sortableColumn property="label" title="${message(code: 'cannedValue.label.label', default: 'Label')}"/>
+
+            <th>Feature Types</th>
+            <th>Organisms</th>
+            <g:sortableColumn property="metadata"
+                              title="${message(code: 'cannedValue.metadata.label', default: 'Metadata')}"/>
+
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${cannedValueInstanceList}" status="i" var="cannedValueInstance">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+
+                <td><g:link action="show"
+                            id="${cannedValueInstance.id}">${fieldValue(bean: cannedValueInstance, field: "label")}</g:link></td>
+                <td>
+                    <g:each in="${cannedValueInstance.featureTypes.sort() { a, b -> a.display <=> b.display }}"
+                            var="featureType">
+                        ${featureType.type}:${featureType.name}
+                    </g:each>
+                </td>
+                <td>
+                    <g:each in="${organismFilters.get(cannedValueInstance)}" var="filter">
+                        <g:link controller="organism" id="${filter.organism.id}">${filter.organism.commonName}</g:link>
+                    </g:each>
+                </td>
+
+                <td>${fieldValue(bean: cannedValueInstance, field: "metadata")}</td>
+
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${cannedValueInstanceCount ?: 0}"/>
+    </div>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/cannedValue/show.gsp b/grails-app/views/cannedValue/show.gsp
new file mode 100644
index 0000000..7bcf582
--- /dev/null
+++ b/grails-app/views/cannedValue/show.gsp
@@ -0,0 +1,88 @@
+<%@ page import="org.bbop.apollo.CannedValue" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'cannedValue.label', default: 'CannedValue')}"/>
+    <title><g:message code="default.show.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#show-cannedValue" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                  default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]"/></g:link></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="show-cannedValue" class="content scaffold-show" role="main">
+    <h1><g:message code="default.show.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <ol class="property-list cannedValue">
+
+        <g:if test="${cannedValueInstance?.label}">
+            <li class="fieldcontain">
+                <span id="label-label" class="property-label"><g:message code="cannedValue.label.label"
+                                                                         default="Label"/></span>
+
+                <span class="property-value" aria-labelledby="label-label"><g:fieldValue bean="${cannedValueInstance}"
+                                                                                         field="label"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedValueInstance?.metadata}">
+            <li class="fieldcontain">
+                <span id="metadata-label" class="property-label"><g:message code="cannedValue.metadata.label"
+                                                                            default="Metadata"/></span>
+
+                <span class="property-value" aria-labelledby="metadata-label"><g:fieldValue
+                        bean="${cannedValueInstance}" field="metadata"/></span>
+
+            </li>
+        </g:if>
+
+        <g:if test="${cannedValueInstance?.featureTypes}">
+            <li class="fieldcontain">
+                <span id="featureTypes-label" class="property-label"><g:message code="cannedValue.featureTypes.label"
+                                                                                default="Feature Types"/></span>
+
+                <g:each in="${cannedValueInstance.featureTypes}" var="f">
+                    <span class="property-value" aria-labelledby="featureTypes-label"><g:link controller="featureType"
+                                                                                              action="show"
+                                                                                              id="${f.id}">${f?.name}</g:link></span>
+                </g:each>
+
+            </li>
+        </g:if>
+
+        <g:if test="${organismFilters}">
+            <li class="fieldcontain">
+                <span id="organisms-label" class="property-label"><g:message code="cannedValue.organisms.label" default="Organisms" /></span>
+
+                <g:each in="${organismFilters}" var="f">
+                    <span class="property-value" aria-labelledby="organisms-label"><g:link controller="organism" action="show" id="${f.id}">${f?.organism.commonName}</g:link></span>
+                </g:each>
+            </li>
+        </g:if>
+
+    </ol>
+    <g:form url="[resource: cannedValueInstance, action: 'delete']" method="DELETE">
+        <fieldset class="buttons">
+            <g:link class="edit" action="edit" resource="${cannedValueInstance}"><g:message
+                    code="default.button.edit.label" default="Edit"/></g:link>
+            <g:actionSubmit class="delete" action="delete"
+                            value="${message(code: 'default.button.delete.label', default: 'Delete')}"
+                            onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');"/>
+        </fieldset>
+    </g:form>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/error.gsp b/grails-app/views/error.gsp
new file mode 100644
index 0000000..95e11ed
--- /dev/null
+++ b/grails-app/views/error.gsp
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title><g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else></title>
+		<meta name="layout" content="main">
+		<g:if env="development"><asset:stylesheet src="errors.css"/></g:if>
+	</head>
+	<body>
+		<g:if env="development">
+			<g:renderException exception="${exception}" />
+		</g:if>
+		<g:else>
+			<ul class="errors">
+				<li>An error has occurred</li>
+			</ul>
+		</g:else>
+	</body>
+</html>
diff --git a/grails-app/views/featureEvent/_form.gsp b/grails-app/views/featureEvent/_form.gsp
new file mode 100644
index 0000000..d38a843
--- /dev/null
+++ b/grails-app/views/featureEvent/_form.gsp
@@ -0,0 +1,112 @@
+<%@ page import="org.bbop.apollo.FeatureEvent" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'editor', 'error')} ">
+	<label for="editor">
+		<g:message code="featureEvent.editor.label" default="Editor" />
+		
+	</label>
+	<g:select id="editor" name="editor.id" from="${org.bbop.apollo.User.list()}" optionKey="id" value="${featureEventInstance?.editor?.id}" class="many-to-one" noSelection="['null': '']"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'originalJsonCommand', 'error')} ">
+	<label for="originalJsonCommand">
+		<g:message code="featureEvent.originalJsonCommand.label" default="Original Json Command" />
+		
+	</label>
+	<g:textField name="originalJsonCommand" value="${featureEventInstance?.originalJsonCommand}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'newFeaturesJsonArray', 'error')} ">
+	<label for="newFeaturesJsonArray">
+		<g:message code="featureEvent.newFeaturesJsonArray.label" default="New Features Json Array" />
+		
+	</label>
+	<g:textField name="newFeaturesJsonArray" value="${featureEventInstance?.newFeaturesJsonArray}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'oldFeaturesJsonArray', 'error')} ">
+	<label for="oldFeaturesJsonArray">
+		<g:message code="featureEvent.oldFeaturesJsonArray.label" default="Old Features Json Array" />
+		
+	</label>
+	<g:textField name="oldFeaturesJsonArray" value="${featureEventInstance?.oldFeaturesJsonArray}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'name', 'error')} required">
+	<label for="name">
+		<g:message code="featureEvent.name.label" default="Name" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="name" required="" value="${featureEventInstance?.name}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'uniqueName', 'error')} required">
+	<label for="uniqueName">
+		<g:message code="featureEvent.uniqueName.label" default="Unique Name" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="uniqueName" required="" value="${featureEventInstance?.uniqueName}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'parentId', 'error')} ">
+	<label for="parentId">
+		<g:message code="featureEvent.parentId.label" default="Parent Id" />
+		
+	</label>
+	<g:field name="parentId" type="number" value="${featureEventInstance.parentId}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'parentMergeId', 'error')} ">
+	<label for="parentMergeId">
+		<g:message code="featureEvent.parentMergeId.label" default="Parent Merge Id" />
+		
+	</label>
+	<g:field name="parentMergeId" type="number" value="${featureEventInstance.parentMergeId}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'childId', 'error')} ">
+	<label for="childId">
+		<g:message code="featureEvent.childId.label" default="Child Id" />
+		
+	</label>
+	<g:field name="childId" type="number" value="${featureEventInstance.childId}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'childSplitId', 'error')} ">
+	<label for="childSplitId">
+		<g:message code="featureEvent.childSplitId.label" default="Child Split Id" />
+		
+	</label>
+	<g:field name="childSplitId" type="number" value="${featureEventInstance.childSplitId}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'current', 'error')} ">
+	<label for="current">
+		<g:message code="featureEvent.current.label" default="Current" />
+		
+	</label>
+	<g:checkBox name="current" value="${featureEventInstance?.current}" />
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureEventInstance, field: 'operation', 'error')} required">
+	<label for="operation">
+		<g:message code="featureEvent.operation.label" default="Operation" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:select name="operation" from="${org.bbop.apollo.history.FeatureOperation?.values()}" keys="${org.bbop.apollo.history.FeatureOperation.values()*.name()}" required="" value="${featureEventInstance?.operation?.name()}" />
+
+</div>
+
diff --git a/grails-app/views/featureEvent/create.gsp b/grails-app/views/featureEvent/create.gsp
new file mode 100644
index 0000000..eca4241
--- /dev/null
+++ b/grails-app/views/featureEvent/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureEvent.label', default: 'FeatureEvent')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-featureEvent" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-featureEvent" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${featureEventInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${featureEventInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:featureEventInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureEvent/edit.gsp b/grails-app/views/featureEvent/edit.gsp
new file mode 100644
index 0000000..710ba9f
--- /dev/null
+++ b/grails-app/views/featureEvent/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.FeatureEvent" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureEvent.label', default: 'FeatureEvent')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-featureEvent" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-featureEvent" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${featureEventInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${featureEventInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:featureEventInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${featureEventInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureEvent/index.gsp b/grails-app/views/featureEvent/index.gsp
new file mode 100644
index 0000000..c57650f
--- /dev/null
+++ b/grails-app/views/featureEvent/index.gsp
@@ -0,0 +1,66 @@
+
+<%@ page import="org.bbop.apollo.FeatureEvent" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureEvent.label', default: 'FeatureEvent')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-featureEvent" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-featureEvent" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+					
+						<th><g:message code="featureEvent.editor.label" default="Editor" /></th>
+					
+						<g:sortableColumn property="originalJsonCommand" title="${message(code: 'featureEvent.originalJsonCommand.label', default: 'Original Json Command')}" />
+					
+						<g:sortableColumn property="newFeaturesJsonArray" title="${message(code: 'featureEvent.newFeaturesJsonArray.label', default: 'New Features Json Array')}" />
+					
+						<g:sortableColumn property="oldFeaturesJsonArray" title="${message(code: 'featureEvent.oldFeaturesJsonArray.label', default: 'Old Features Json Array')}" />
+					
+						<g:sortableColumn property="name" title="${message(code: 'featureEvent.name.label', default: 'Name')}" />
+					
+						<g:sortableColumn property="uniqueName" title="${message(code: 'featureEvent.uniqueName.label', default: 'Unique Name')}" />
+					
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${featureEventInstanceList}" status="i" var="featureEventInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+					
+						<td><g:link action="show" id="${featureEventInstance.id}">${fieldValue(bean: featureEventInstance, field: "editor")}</g:link></td>
+					
+						<td>${fieldValue(bean: featureEventInstance, field: "originalJsonCommand")}</td>
+					
+						<td>${fieldValue(bean: featureEventInstance, field: "newFeaturesJsonArray")}</td>
+					
+						<td>${fieldValue(bean: featureEventInstance, field: "oldFeaturesJsonArray")}</td>
+					
+						<td>${fieldValue(bean: featureEventInstance, field: "name")}</td>
+					
+						<td>${fieldValue(bean: featureEventInstance, field: "uniqueName")}</td>
+					
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${featureEventInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureEvent/report.gsp b/grails-app/views/featureEvent/report.gsp
new file mode 100644
index 0000000..950ab57
--- /dev/null
+++ b/grails-app/views/featureEvent/report.gsp
@@ -0,0 +1,166 @@
+<%@ page import="org.bbop.apollo.RequestHandlingService; org.bbop.apollo.Organism; org.bbop.apollo.User; org.bbop.apollo.Feature" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <title>Recent Changes</title>
+
+    <script>
+        function doSearch(){
+            document.getElementById("customform").submit();
+        }
+    </script>
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+
+<g:form id="customform" name="myForm" url="[action: 'report', controller: 'featureEvent']">
+    <div class="container-fluid" style="margin: 10px 0px 0px 20px; padding-bottom: 20px;">
+        <g:if test="${flash.message}">
+            <div class="message row col-sm-12" role="status">${flash.message}</div>
+        </g:if>
+        <div class="row col-sm-12">
+            <div class="col-sm-2 form-group">
+                %{--<label for="ownerName">Owner:</label>--}%
+                <g:select name='ownerName' value="${ownerName}"
+                          noSelection="${[null: 'Select User ...']}"
+                          from='${User.listOrderByUsername()}'
+                          optionKey="username" optionValue="username" onchange="doSearch();"/>
+            </div>
+
+            <div class="col-sm-2  form-group">
+                %{--<label for="featureType">Feature type:</label>--}%
+                <g:select name='featureType' value="${featureType}"
+                          noSelection="${[null: 'Select Feature Type...']}"
+                          from='${featureTypes}' onchange="doSearch();"
+                />
+            </div>
+
+            <div class="col-sm-2  form-group">
+                <g:select name='organismName' value="${organismName}"
+                          noSelection="${[null: 'Select Organism ...']}"
+                          from='${Organism.listOrderByCommonName()}'
+                          optionKey="commonName" optionValue="commonName" onchange="doSearch();"/>
+            </div>
+            <div class="col-sm-2  form-group">
+                <g:textField class="form-control input-sm" name="sequenceName" maxlength="50" value="${sequenceName}" placeholder="Sequence Name" />
+            </div>
+        </div>
+        <div class="row col-sm-12">
+            <div class="col-sm-2">
+                <strong>Last Updated</strong>
+            </div>
+            <div class="col-sm-4  form-group">
+                After:
+                <g:datePicker name="afterDate" value="${afterDate}" precision="day" relativeYears="[-20..20]"/>
+            </div>
+            <div class="col-sm-4  form-group">
+                Before:
+                <g:datePicker name="beforeDate" value="${beforeDate}" precision="day" relativeYears="[-20..20]"/>
+            </div>
+        </div>
+        <div class="row col-sm-12">
+            <div class="col-sm-2">
+                <strong>Date Created</strong>
+            </div>
+            <div class="col-sm-4  form-group">
+                After:
+                <g:datePicker name="dateCreatedAfterDate" value="${dateCreatedAfterDate}" precision="day" relativeYears="[-20..20]"/>
+            </div>
+            <div class="col-sm-4  form-group">
+                Before:
+                <g:datePicker name="dateCreatedBeforeDate" value="${dateCreatedBeforeDate}" precision="day" relativeYears="[-20..20]"/>
+            </div>
+        </div>
+
+        <div class="row col-sm-12">
+            <div class="form-group col-sm-4">
+                <button class="col-sm-3 btn btn-primary" type="submit">
+                    <span class="glyphicon glyphicon-search" aria-hidden="true"></span> Search
+                </button>
+            </div>
+        </div>
+        <g:hiddenField name="sort" value="${params.sort}"/>
+        <g:hiddenField name="order" value="${params.order}"/>
+    </div>
+</g:form>
+
+
+<div id="list-feature" class="content scaffold-list" role="main">
+
+    <table>
+        <thead>
+        <tr>
+            <g:sortableColumn property="lastUpdated" title="Last update" params="${filters}"/>
+            <g:sortableColumn property="dateCreated" title="Created" params="${filters}"/>
+            <g:sortableColumn property="organism" title="Organism" params="${filters}"/>
+            <g:sortableColumn property="sequencename" title="Sequence name" params="${filters}"/>
+            <g:sortableColumn property="name" title="Name" params="${filters}"/>
+            <g:sortableColumn property="owners" title="Owner" params="${filters}"/>
+            <g:sortableColumn property="cvTerm" title="Feature type" params="${filters}"/>
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${features}" status="i" var="feature">
+            <g:set var="sequence" value="${feature.featureLocation.sequence}"/>
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+                <td>
+                    <g:formatDate format="dd-MMM-yy HH:mm (E z)" date="${feature.lastUpdated}"/>
+                    (v ${feature.version})
+                </td>
+                <td>
+                    <g:formatDate format="dd-MMM-yy HH:mm (E z)" date="${feature.dateCreated}"/>
+                </td>
+                <td>
+                    <g:link target="_blank" controller="annotator" action="loadLink"
+                            params="[organism: sequence.organism.id]">
+                        ${sequence.organism.commonName}
+                    </g:link>
+                </td>
+                <td>
+                %{--${feature.featureLocation.sequence.name}--}%
+                    <g:set var="sequence" value="${feature.featureLocation.sequence}"/>
+                    <g:link target="_blank" controller="annotator" action="loadLink"
+                            params="[loc: sequence.name + ':' + sequence.start + '..' + sequence.end, organism: sequence.organism.id]">
+                        ${sequence.name}</g:link>
+                    <g:link target="_blank" controller="sequence" action="report" id="${sequence.organism.id}">
+                        <div class="glyphicon glyphicon-list-alt">
+                        </div>
+                    </g:link>
+                </td>
+                <td>
+                    <g:link target="_blank" controller="annotator" action="loadLink"
+                            params="[loc: feature.featureLocation.sequence.name + ':' + feature.featureLocation.fmin + '..' + feature.featureLocation.fmax, organism: feature.featureLocation.sequence.organism.id]">
+                        ${feature.name}
+                    </g:link>
+                </td>
+
+                <td>
+                    <g:link target="_blank" controller="annotator" action="detail" id="${feature.owner?.id}">
+                        ${feature.owner?.username}
+                    </g:link>
+                </td>
+                <td>
+                    ${feature.cvTerm}
+                </td>
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${featureCount ?: 0}" params="${params}"/>
+    </div>
+    <div class="col-sm-4">
+        <div class="btn btn-info">
+            Results <div class="badge badge-important">
+            <g:formatNumber number="${featureCount}" type="number"/>
+        </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/featureEvent/show.gsp b/grails-app/views/featureEvent/show.gsp
new file mode 100644
index 0000000..27a1ab0
--- /dev/null
+++ b/grails-app/views/featureEvent/show.gsp
@@ -0,0 +1,161 @@
+
+<%@ page import="org.bbop.apollo.FeatureEvent" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureEvent.label', default: 'FeatureEvent')}" />
+		<title><g:message code="default.show.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#show-featureEvent" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="show-featureEvent" class="content scaffold-show" role="main">
+			<h1><g:message code="default.show.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<ol class="property-list featureEvent">
+			
+				<g:if test="${featureEventInstance?.editor}">
+				<li class="fieldcontain">
+					<span id="editor-label" class="property-label"><g:message code="featureEvent.editor.label" default="Editor" /></span>
+					
+						<span class="property-value" aria-labelledby="editor-label"><g:link controller="user" action="show" id="${featureEventInstance?.editor?.id}">${featureEventInstance?.editor?.encodeAsHTML()}</g:link></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.originalJsonCommand}">
+				<li class="fieldcontain">
+					<span id="originalJsonCommand-label" class="property-label"><g:message code="featureEvent.originalJsonCommand.label" default="Original Json Command" /></span>
+					
+						<span class="property-value" aria-labelledby="originalJsonCommand-label"><g:fieldValue bean="${featureEventInstance}" field="originalJsonCommand"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.newFeaturesJsonArray}">
+				<li class="fieldcontain">
+					<span id="newFeaturesJsonArray-label" class="property-label"><g:message code="featureEvent.newFeaturesJsonArray.label" default="New Features Json Array" /></span>
+					
+						<span class="property-value" aria-labelledby="newFeaturesJsonArray-label"><g:fieldValue bean="${featureEventInstance}" field="newFeaturesJsonArray"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.oldFeaturesJsonArray}">
+				<li class="fieldcontain">
+					<span id="oldFeaturesJsonArray-label" class="property-label"><g:message code="featureEvent.oldFeaturesJsonArray.label" default="Old Features Json Array" /></span>
+					
+						<span class="property-value" aria-labelledby="oldFeaturesJsonArray-label"><g:fieldValue bean="${featureEventInstance}" field="oldFeaturesJsonArray"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.name}">
+				<li class="fieldcontain">
+					<span id="name-label" class="property-label"><g:message code="featureEvent.name.label" default="Name" /></span>
+					
+						<span class="property-value" aria-labelledby="name-label"><g:fieldValue bean="${featureEventInstance}" field="name"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.uniqueName}">
+				<li class="fieldcontain">
+					<span id="uniqueName-label" class="property-label"><g:message code="featureEvent.uniqueName.label" default="Unique Name" /></span>
+					
+						<span class="property-value" aria-labelledby="uniqueName-label"><g:fieldValue bean="${featureEventInstance}" field="uniqueName"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.parentId}">
+				<li class="fieldcontain">
+					<span id="parentId-label" class="property-label"><g:message code="featureEvent.parentId.label" default="Parent Id" /></span>
+					
+						<span class="property-value" aria-labelledby="parentId-label"><g:fieldValue bean="${featureEventInstance}" field="parentId"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.parentMergeId}">
+				<li class="fieldcontain">
+					<span id="parentMergeId-label" class="property-label"><g:message code="featureEvent.parentMergeId.label" default="Parent Merge Id" /></span>
+					
+						<span class="property-value" aria-labelledby="parentMergeId-label"><g:fieldValue bean="${featureEventInstance}" field="parentMergeId"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.childId}">
+				<li class="fieldcontain">
+					<span id="childId-label" class="property-label"><g:message code="featureEvent.childId.label" default="Child Id" /></span>
+					
+						<span class="property-value" aria-labelledby="childId-label"><g:fieldValue bean="${featureEventInstance}" field="childId"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.childSplitId}">
+				<li class="fieldcontain">
+					<span id="childSplitId-label" class="property-label"><g:message code="featureEvent.childSplitId.label" default="Child Split Id" /></span>
+					
+						<span class="property-value" aria-labelledby="childSplitId-label"><g:fieldValue bean="${featureEventInstance}" field="childSplitId"/></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.current}">
+				<li class="fieldcontain">
+					<span id="current-label" class="property-label"><g:message code="featureEvent.current.label" default="Current" /></span>
+					
+						<span class="property-value" aria-labelledby="current-label"><g:formatBoolean boolean="${featureEventInstance?.current}" /></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.dateCreated}">
+				<li class="fieldcontain">
+					<span id="dateCreated-label" class="property-label"><g:message code="featureEvent.dateCreated.label" default="Date Created" /></span>
+					
+						<span class="property-value" aria-labelledby="dateCreated-label"><g:formatDate date="${featureEventInstance?.dateCreated}" /></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.lastUpdated}">
+				<li class="fieldcontain">
+					<span id="lastUpdated-label" class="property-label"><g:message code="featureEvent.lastUpdated.label" default="Last Updated" /></span>
+					
+						<span class="property-value" aria-labelledby="lastUpdated-label"><g:formatDate date="${featureEventInstance?.lastUpdated}" /></span>
+					
+				</li>
+				</g:if>
+			
+				<g:if test="${featureEventInstance?.operation}">
+				<li class="fieldcontain">
+					<span id="operation-label" class="property-label"><g:message code="featureEvent.operation.label" default="Operation" /></span>
+					
+						<span class="property-value" aria-labelledby="operation-label"><g:fieldValue bean="${featureEventInstance}" field="operation"/></span>
+					
+				</li>
+				</g:if>
+			
+			</ol>
+			<g:form url="[resource:featureEventInstance, action:'delete']" method="DELETE">
+				<fieldset class="buttons">
+					<g:link class="edit" action="edit" resource="${featureEventInstance}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
+					<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureType/_form.gsp b/grails-app/views/featureType/_form.gsp
new file mode 100644
index 0000000..f3b7cf5
--- /dev/null
+++ b/grails-app/views/featureType/_form.gsp
@@ -0,0 +1,40 @@
+<%@ page import="org.bbop.apollo.FeatureType" %>
+
+
+
+<div class="fieldcontain ${hasErrors(bean: featureTypeInstance, field: 'name', 'error')} required">
+	<label for="name">
+		<g:message code="featureType.name.label" default="Name" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="name" required="" value="${featureTypeInstance?.name}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureTypeInstance, field: 'display', 'error')} required">
+	<label for="display">
+		<g:message code="featureType.display.label" default="Display" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="display" required="" value="${featureTypeInstance?.display}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureTypeInstance, field: 'ontologyId', 'error')} required">
+	<label for="ontologyId">
+		<g:message code="featureType.ontologyId.label" default="Ontology Id" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="ontologyId" required="" value="${featureTypeInstance?.ontologyId}"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: featureTypeInstance, field: 'type', 'error')} required">
+	<label for="type">
+		<g:message code="featureType.type.label" default="Type" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:textField name="type" required="" value="${featureTypeInstance?.type}"/>
+
+</div>
+
diff --git a/grails-app/views/featureType/create.gsp b/grails-app/views/featureType/create.gsp
new file mode 100644
index 0000000..c160352
--- /dev/null
+++ b/grails-app/views/featureType/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureType.label', default: 'FeatureType')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-featureType" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-featureType" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${featureTypeInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${featureTypeInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:featureTypeInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureType/edit.gsp b/grails-app/views/featureType/edit.gsp
new file mode 100644
index 0000000..915d7cc
--- /dev/null
+++ b/grails-app/views/featureType/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.FeatureType" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureType.label', default: 'FeatureType')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-featureType" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-featureType" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${featureTypeInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${featureTypeInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:featureTypeInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${featureTypeInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureType/index.gsp b/grails-app/views/featureType/index.gsp
new file mode 100644
index 0000000..de476cd
--- /dev/null
+++ b/grails-app/views/featureType/index.gsp
@@ -0,0 +1,59 @@
+
+<%@ page import="org.bbop.apollo.FeatureType" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'featureType.label', default: 'FeatureType')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-featureType" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-featureType" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+					
+						<g:sortableColumn property="name" title="${message(code: 'featureType.name.label', default: 'Name')}" />
+
+						<g:sortableColumn property="display" title="${message(code: 'featureType.display.label', default: 'Display')}" />
+
+						<g:sortableColumn property="ontologyId" title="${message(code: 'featureType.ontologyId.label', default: 'Ontology Id')}" />
+
+						<g:sortableColumn property="type" title="${message(code: 'featureType.type.label', default: 'Type')}" />
+						<th>Filter</th>
+					
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${featureTypeInstanceList}" status="i" var="featureTypeInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+					
+						<td><g:link action="show" id="${featureTypeInstance.id}">${fieldValue(bean: featureTypeInstance, field: "display")}</g:link></td>
+
+						<td>${fieldValue(bean: featureTypeInstance, field: "display")}</td>
+						<td>${fieldValue(bean: featureTypeInstance, field: "ontologyId")}</td>
+						<td>${fieldValue(bean: featureTypeInstance, field: "type")}</td>
+
+						<td>${fieldValue(bean: featureTypeInstance, field: "type")}:${featureTypeInstance.name}</td>
+					
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${featureTypeInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/featureType/show.gsp b/grails-app/views/featureType/show.gsp
new file mode 100644
index 0000000..8dadf79
--- /dev/null
+++ b/grails-app/views/featureType/show.gsp
@@ -0,0 +1,72 @@
+<%@ page import="org.bbop.apollo.FeatureType" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'featureType.label', default: 'FeatureType')}"/>
+    <title><g:message code="default.show.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#show-featureType" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                                  default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]"/></g:link></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="show-featureType" class="content scaffold-show" role="main">
+    <h1><g:message code="default.show.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <ol class="property-list featureType">
+
+        <li class="fieldcontain">
+            <span id="name-label" class="property-label"><g:message code="featureType.name.label"
+                                                                    default="Name"/></span>
+
+            <span class="property-value" aria-labelledby="name-label"><g:fieldValue bean="${featureTypeInstance}"
+                                                                                    field="name"/></span>
+
+        </li>
+
+        <li class="fieldcontain">
+            <span id="display-label" class="property-label"><g:message code="featureType.display.label"
+                                                                       default="Display"/></span>
+            <span class="property-value" aria-labelledby="display-label"><g:fieldValue bean="${featureTypeInstance}"
+                                                                                       field="display"/></span>
+        </li>
+
+        <li class="fieldcontain">
+            <span id="ontologyId-label" class="property-label"><g:message code="featureType.ontologyId.label"
+                                                                          default="Ontology Id"/></span>
+            <span class="property-value" aria-labelledby="ontologyId-label"><g:fieldValue bean="${featureTypeInstance}"
+                                                                                          field="ontologyId"/></span>
+        </li>
+
+        <li class="fieldcontain">
+            <span id="type-label" class="property-label"><g:message code="featureType.type.label"
+                                                                    default="Type"/></span>
+            <span class="property-value" aria-labelledby="type-label"><g:fieldValue bean="${featureTypeInstance}"
+                                                                                    field="type"/></span>
+        </li>
+
+    </ol>
+    <g:form url="[resource: featureTypeInstance, action: 'delete']" method="DELETE">
+        <fieldset class="buttons">
+            <g:link class="edit" action="edit" resource="${featureTypeInstance}"><g:message
+                    code="default.button.edit.label" default="Edit"/></g:link>
+            <g:actionSubmit class="delete" action="delete"
+                            value="${message(code: 'default.button.delete.label', default: 'Delete')}"
+                            onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');"/>
+        </fieldset>
+    </g:form>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/google_analytics.gsp b/grails-app/views/google_analytics.gsp
new file mode 100644
index 0000000..559fdbf
--- /dev/null
+++ b/grails-app/views/google_analytics.gsp
@@ -0,0 +1,26 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: nathandunn
+  Date: 11/17/16
+  Time: 4:05 PM
+--%>
+
+<g:each var="google_analytics_id" in="${grailsApplication.config.apollo.google_analytics}">
+    <script>
+        (function (i, s, o, g, r, a, m) {
+            i['GoogleAnalyticsObject'] = r;
+            i[r] = i[r] || function () {
+                        (i[r].q = i[r].q || []).push(arguments)
+                    }, i[r].l = 1 * new Date();
+            a = s.createElement(o),
+                    m = s.getElementsByTagName(o)[0];
+            a.async = 1;
+            a.src = g;
+            m.parentNode.insertBefore(a, m)
+        })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
+
+        ga('create', '${google_analytics_id}', 'auto');
+        ga('send', 'pageview');
+
+    </script>
+</g:each>
diff --git a/grails-app/views/home/metrics.gsp b/grails-app/views/home/metrics.gsp
new file mode 100644
index 0000000..d82716b
--- /dev/null
+++ b/grails-app/views/home/metrics.gsp
@@ -0,0 +1,66 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: nathandunn
+  Date: 7/20/15
+  Time: 6:55 AM
+--%>
+
+<head>
+    <meta name="layout" content="report">
+    <title>Performance Metrics</title>
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+<div class="page-header" style="margin-left: 20px;">
+    <div class="header"><h4>Total Hits <span class="label label-info">${countTotal}</span></h4></div>
+
+    <div class="header"><h4>Overall Mean (s/count) <span class="label label-info"><g:formatNumber number="${meanTotal}"
+                                                                                        maxFractionDigits="6"/>
+</span></h4></div>
+        <div class="header"><h4>Total time (s)<span class="label label-info"><g:formatNumber
+                number="${totalTime}" maxFractionDigits="6"/>
+        </span></h4></div>
+        <g:link action="downloadReport"><i class="glyphicon glyphicon-download-alt glyphicon-th-large"></i></g:link>
+    </div>
+
+
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+            <th>Class</th>
+            <th>Method</th>
+            <th>total %</th>
+            <th>total (s)</th>
+            <th>count</th>
+            <th>mean (s)</th>
+            <th>max</th>
+            <th>min</th>
+            <th>stdev</th>
+
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${performanceMetricList}" status="i" var="metric">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+                <td>${metric.className}</td>
+                <td>${metric.methodName}</td>
+                <td><g:formatNumber number="${metric.totalPercent}" maxFractionDigits="2" type="percent"/></td>
+                <td><g:formatNumber number="${metric.total}"/></td>
+                <td>${metric.count}</td>
+                <td><g:formatNumber number="${metric.mean}" minFractionDigits="2" maxFractionDigits="2"/></td>
+                <td><g:formatNumber number="${metric.max}" minFractionDigits="2" maxFractionDigits="2"/></td>
+                <td><g:formatNumber number="${metric.min}" minFractionDigits="2" maxFractionDigits="2"/></td>
+                <td><g:formatNumber number="${metric.stddev}" minFractionDigits="2" maxFractionDigits="2"/></td>
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/grails-app/views/home/systemInfo.gsp b/grails-app/views/home/systemInfo.gsp
new file mode 100644
index 0000000..3c275ac
--- /dev/null
+++ b/grails-app/views/home/systemInfo.gsp
@@ -0,0 +1,90 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: nathandunn
+  Date: 7/20/15
+  Time: 6:55 AM
+--%>
+
+<head>
+    <meta name="layout" content="report">
+    <title>System Info</title>
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+
+
+<div class="row col-md-offset-1">
+
+    <h3>Metrics</h3>
+    <ul class="list-unstyled">
+        <li>
+            <g:link action="threads" controller="metrics">
+                Threads
+            </g:link>
+        </li>
+        <li>
+            <g:link action="metrics" controller="metrics" params="[pretty: true]">
+                Timings
+            </g:link>
+        </li>
+    </ul>
+
+    <div class="row col-md-5">
+        <h3>Runtime Info</h3>
+        <table class="table">
+            <tr>
+
+            </tr>
+            <g:each in="${runtimeMapInstance}" var="data">
+                <tr>
+                    <td>
+                        ${data.key}
+                    </td>
+                    <td>
+                        ${data.value}
+                    </td>
+                </tr>
+            </g:each>
+        </table>
+    </div>
+
+    <div class="row col-md-10">
+        <h3>Java Info</h3>
+        <table class="table">
+            <g:each in="${javaMapInstance.sort{it.key}}" var="data">
+                <g:if test="${!data.key.toLowerCase().contains("password")}">
+                    <tr>
+                        <td>
+                            ${data.key}
+                        </td>
+                        <td>
+                            ${data.value}
+                        </td>
+                    </tr>
+                </g:if>
+            </g:each>
+        </table>
+    </div>
+
+    <div class="row col-md-10">
+        <h3>Servlet Info</h3>
+        <table>
+            <g:each in="${servletMapInstance}" var="data">
+                <tr>
+                    <td>
+                        ${data.key}
+                    </td>
+                    <td>
+                        ${data.value}
+                    </td>
+                </tr>
+            </g:each>
+        </table>
+    </div>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/index.gsp b/grails-app/views/index.gsp
new file mode 100644
index 0000000..cf4c0b4
--- /dev/null
+++ b/grails-app/views/index.gsp
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main"/>
+		<title>Welcome to Grails</title>
+		<style type="text/css" media="screen">
+			#status {
+				background-color: #eee;
+				border: .2em solid #fff;
+				margin: 2em 2em 1em;
+				padding: 1em;
+				width: 12em;
+				float: left;
+				-moz-box-shadow: 0px 0px 1.25em #ccc;
+				-webkit-box-shadow: 0px 0px 1.25em #ccc;
+				box-shadow: 0px 0px 1.25em #ccc;
+				-moz-border-radius: 0.6em;
+				-webkit-border-radius: 0.6em;
+				border-radius: 0.6em;
+			}
+
+			.ie6 #status {
+				display: inline; /* float double margin fix http://www.positioniseverything.net/explorer/doubled-margin.html */
+			}
+
+			#status ul {
+				font-size: 0.9em;
+				list-style-type: none;
+				margin-bottom: 0.6em;
+				padding: 0;
+			}
+
+			#status li {
+				line-height: 1.3;
+			}
+
+			#status h1 {
+				text-transform: uppercase;
+				font-size: 1.1em;
+				margin: 0 0 0.3em;
+			}
+
+			#page-body {
+				margin: 2em 1em 1.25em 18em;
+			}
+
+			h2 {
+				margin-top: 1em;
+				margin-bottom: 0.3em;
+				font-size: 1em;
+			}
+
+			p {
+				line-height: 1.5;
+				margin: 0.25em 0;
+			}
+
+			#controller-list ul {
+				list-style-position: inside;
+			}
+
+			#controller-list li {
+				line-height: 1.3;
+				list-style-position: inside;
+				margin: 0.25em 0;
+			}
+
+			@media screen and (max-width: 480px) {
+				#status {
+					display: none;
+				}
+
+				#page-body {
+					margin: 0 1em 1em;
+				}
+
+				#page-body h1 {
+					margin-top: 0;
+				}
+			}
+		</style>
+	</head>
+	<body>
+		<a href="#page-body" class="skip"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div id="status" role="complementary">
+			<h1>Application Status</h1>
+			<ul>
+				<li>App version: <g:meta name="app.version"/></li>
+				<li>Grails version: <g:meta name="app.grails.version"/></li>
+				<li>Groovy version: ${GroovySystem.getVersion()}</li>
+				<li>JVM version: ${System.getProperty('java.version')}</li>
+				<li>Reloading active: ${grails.util.Environment.reloadingAgentEnabled}</li>
+				<li>Controllers: ${grailsApplication.controllerClasses.size()}</li>
+				<li>Domains: ${grailsApplication.domainClasses.size()}</li>
+				<li>Services: ${grailsApplication.serviceClasses.size()}</li>
+				<li>Tag Libraries: ${grailsApplication.tagLibClasses.size()}</li>
+			</ul>
+			<h1>Installed Plugins</h1>
+			<ul>
+				<g:each var="plugin" in="${applicationContext.getBean('pluginManager').allPlugins}">
+					<li>${plugin.name} - ${plugin.version}</li>
+				</g:each>
+			</ul>
+		</div>
+		<div id="page-body" role="main">
+			<h1>Welcome to Grails</h1>
+			<p>Congratulations, you have successfully started your first Grails application! At the moment
+			   this is the default page, feel free to modify it to either redirect to a controller or display whatever
+			   content you may choose. Below is a list of controllers that are currently deployed in this application,
+			   click on each to execute its default action:</p>
+
+			<div id="controller-list" role="navigation">
+				<h2>Available Controllers:</h2>
+				<ul>
+					<g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }">
+						<li class="controller"><g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link></li>
+					</g:each>
+				</ul>
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/jbrowse/chooseOrganismForJbrowse.gsp b/grails-app/views/jbrowse/chooseOrganismForJbrowse.gsp
new file mode 100644
index 0000000..0c271bf
--- /dev/null
+++ b/grails-app/views/jbrowse/chooseOrganismForJbrowse.gsp
@@ -0,0 +1,46 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: ndunn
+  Date: 6/4/15
+  Time: 2:37 PM
+--%>
+
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <title>Choose JBrowse Organism</title>
+</head>
+
+<body>
+
+<div>
+    <ul>
+        <li>
+            <a class="btn btn-default" href="${createLink(uri: '/')}">
+                <g:img src="ApolloLogo_100x36.png"/>
+            </a>
+        </li>
+    </ul>
+</div>
+
+<g:if test="${flash.message}">
+    <div class="message" role="status">${flash.message}</div>
+</g:if>
+
+
+<div style="margin-left: 20px;">
+    <h3>
+        Choose Organism to View
+    </h3>
+
+    <div class="btn-group-vertical" role="group" style="width: 300px;">
+            <g:each in="${organisms}" var="organism">
+                <a href="${createLink(uri:'')}/${organism.id}/jbrowse/index.html" type="button" class="btn btn-default">${organism.commonName}</a>
+            </g:each>
+    </div>
+
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/layouts/_reportHeader.gsp b/grails-app/views/layouts/_reportHeader.gsp
new file mode 100644
index 0000000..faed69e
--- /dev/null
+++ b/grails-app/views/layouts/_reportHeader.gsp
@@ -0,0 +1,47 @@
+<%@ page import="org.codehaus.groovy.grails.web.json.JSONArray" %>
+%{--<nav class="navbar navbar-default">--}%
+    <div class="apollo-header row">
+        <div class="col-lg-10">
+            <g:link uri="/"><asset:image src="ApolloLogo_100x36.png"/></g:link>
+            <perms:admin>
+                <div class="btn-group" role="group">
+                    <button class="btn btn-default dropdown-toggle glyphicon glyphicon-list-alt" data-toggle="dropdown">
+                        <div style="font-family: Arial, Helvetica, sans-serif; font-weight: bold;display: inline;">
+                        Reports
+                            </div>
+                        <span class="caret"></span>
+                    </button>
+                    <ul class="dropdown-menu apollo-dropdown">
+                        <g:each in="${grailsApplication.config.apollo.administrativePanel}" var="report">
+                            <g:if test="${report.type == 'report'}">
+                                <li><g:link uri="${report.link}">${report.label}</g:link></li>
+                            </g:if>
+                        </g:each>
+                    </ul>
+                </div>
+            </perms:admin>
+        </div>
+
+        %{--<div class="col-lg-4 col-lg-offset-2">--}%
+        <div class="pull-lg-right">
+            <shiro:user>
+            %{----}%
+                <h4>
+                    <div class="label label-primary">
+                        <shiro:principal/>
+                    </div>
+
+                    <div class="btn btn-info">
+                        <g:set var="targetUri" value="/${controllerName}/${actionName}"/>
+                        <g:link class="glyphicon glyphicon-log-out" action="logout" controller="login"
+                                params="[targetUri: targetUri]">
+                            %{--Logout--}%
+                        </g:link>
+                    </div>
+                </h4>
+            %{--<a href="Login?operation=logout" type="button" class="fa fa-icon-signout">Signout</a>--}%
+            </shiro:user>
+        </div>
+
+    </div>
+%{--</nav>--}%
diff --git a/grails-app/views/layouts/annotator.gsp b/grails-app/views/layouts/annotator.gsp
new file mode 100644
index 0000000..89ca828
--- /dev/null
+++ b/grails-app/views/layouts/annotator.gsp
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+    <asset:stylesheet src="annotator.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+    <g:layoutHead/>
+</head>
+
+<body>
+
+%{--<div id="apolloLogo" style="padding: 5px;">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+
+%{--<div class="footer" role="contentinfo"></div>--}%
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/layouts/annotator2.gsp b/grails-app/views/layouts/annotator2.gsp
new file mode 100644
index 0000000..7ec19f9
--- /dev/null
+++ b/grails-app/views/layouts/annotator2.gsp
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+<link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+<asset:stylesheet src="annotator.css"/>
+<asset:javascript src="application.js"/>
+<asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+%{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+
+<g:include view="google_analytics.gsp"/>
+
+
+<g:layoutHead/>
+</head>
+
+%{--<div id="apolloLogo" style="padding: 5px;">--}%
+%{--<a href="http://genomearchitect.org">--}%
+%{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+%{--Genome Annotator--}%
+%{--<nav:primary class="nav primary small-menu"/>--}%
+%{--<nav:primary/>--}%
+%{--<ul class="nav nav-pills header1" >--}%
+%{--<li role="presentation" class="">--}%
+%{--<a href="http://genomearchitect.org">--}%
+%{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+%{--</li>--}%
+
+%{--<li role="presentation" class="menu-item">--}%
+%{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+%{--</li>--}%
+%{--<li role="presentation" class="active menu-item">--}%
+%{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+%{--</li>--}%
+%{--<li role="presentation" class=" menu-item">--}%
+%{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+%{--</li>--}%
+%{--<li role="presentation" class=" menu-item">--}%
+%{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+%{--</li>--}%
+%{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+    <g:layoutBody/>
+
+%{--<div class="footer" role="contentinfo"></div>--}%
+
+
+    <div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt"
+                                                                       default="Loading…"/></div>
+    </body>
+    </html>
diff --git a/grails-app/views/layouts/embedded.gsp b/grails-app/views/layouts/embedded.gsp
new file mode 100644
index 0000000..2a79eb1
--- /dev/null
+++ b/grails-app/views/layouts/embedded.gsp
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+    <asset:stylesheet src="application.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+
+    <g:include view="google_analytics.gsp"/>
+
+    <g:layoutHead/>
+</head>
+
+<body>
+
+%{--<div id="apolloLogo" style="width: 100%">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+%{--bootstrap--}%
+<asset:javascript src="restapidoc/restapidoc.js"/>
+
+
+%{--<div class="footer" role="contentinfo"></div>--}%
+
+
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/layouts/login.gsp b/grails-app/views/layouts/login.gsp
new file mode 100644
index 0000000..7f3065e
--- /dev/null
+++ b/grails-app/views/layouts/login.gsp
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+    <asset:stylesheet src="application.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+
+    <g:include view="google_analytics.gsp"/>
+
+    <g:layoutHead/>
+</head>
+
+<body>
+
+<nav class="navbar navbar-default">
+    <div class="apollo-header row">
+        <g:link uri="/"><asset:image src="ApolloLogo_100x36.png"/></g:link>
+        %{--<perms:admin>--}%
+            %{--<div class="btn btn-group">--}%
+                %{--<button class="btn dropdown-toggle glyphicon glyphicon-list-alt " data-toggle="dropdown">--}%
+                    %{--Reports--}%
+                    %{--<span class="caret"></span>--}%
+                %{--</button>--}%
+                %{--<ul class="dropdown-menu apollo-dropdown">--}%
+                    %{--<g:each in="${grailsApplication.config.apollo.administrativePanel}" var="report">--}%
+                        %{--<g:if test="${report.type == 'report'}">--}%
+                            %{--<li><g:link uri="${report.link}">${report.label}</g:link></li>--}%
+                        %{--</g:if>--}%
+                    %{--</g:each>--}%
+                %{--</ul>--}%
+            %{--</div>--}%
+        %{--</perms:admin>--}%
+    </div>
+</nav>
+
+%{--<div id="apolloLogo" style="width: 100%">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+
+<div class="footer" role="contentinfo"></div>
+
+
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/layouts/main.gsp b/grails-app/views/layouts/main.gsp
new file mode 100644
index 0000000..f1eebf5
--- /dev/null
+++ b/grails-app/views/layouts/main.gsp
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+    <asset:stylesheet src="application.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+
+    <g:include view="google_analytics.gsp"/>
+
+    <g:layoutHead/>
+</head>
+
+<body>
+
+%{--<div id="apolloLogo" style="width: 100%">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+%{--bootstrap--}%
+<asset:javascript src="restapidoc/restapidoc.js"/>
+
+
+<div class="footer" role="contentinfo"></div>
+
+
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/layouts/oldlook.gsp b/grails-app/views/layouts/oldlook.gsp
new file mode 100644
index 0000000..d701d0b
--- /dev/null
+++ b/grails-app/views/layouts/oldlook.gsp
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+
+    <asset:stylesheet src="oldlook.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+    <g:layoutHead/>
+</head>
+
+<body>
+
+%{--<div id="apolloLogo" style="width: 100%">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+
+<div class="footer" role="contentinfo"></div>
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/layouts/report.gsp b/grails-app/views/layouts/report.gsp
new file mode 100644
index 0000000..dfd6bad
--- /dev/null
+++ b/grails-app/views/layouts/report.gsp
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title><g:layoutTitle default="Apollo"/></title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <link rel="apple-touch-icon" href="${assetPath(src: 'apple-touch-icon.png')}">
+    <link rel="apple-touch-icon" sizes="114x114" href="${assetPath(src: 'apple-touch-icon-retina.png')}">
+    <asset:stylesheet src="application.css"/>
+    <asset:javascript src="application.js"/>
+    <asset:link rel="shortcut icon" href="webapollo_favicon.ico" type="image/x-icon"/>
+    %{--<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">--}%
+    <g:include view="google_analytics.gsp"/>
+
+    <g:layoutHead/>
+</head>
+
+<body>
+
+%{--<div id="apolloLogo" style="width: 100%">--}%
+    %{--<a href="http://genomearchitect.org">--}%
+    %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--Genome Annotator--}%
+    %{--<nav:primary class="nav primary small-menu"/>--}%
+    %{--<nav:primary/>--}%
+    %{--<ul class="nav nav-pills header1" >--}%
+        %{--<li role="presentation" class="">--}%
+            %{--<a href="http://genomearchitect.org">--}%
+                %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+        %{--</li>--}%
+
+        %{--<li role="presentation" class="menu-item">--}%
+            %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class="active menu-item">--}%
+            %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+        %{--</li>--}%
+        %{--<li role="presentation" class=" menu-item">--}%
+            %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+        %{--</li>--}%
+    %{--</ul>--}%
+
+%{--</div>--}%
+
+
+
+%{--<g:include view="mainMenu"/>--}%
+%{--<nav:secondary/>--}%
+
+<g:layoutBody/>
+
+<div class="footer" role="contentinfo"></div>
+
+
+
+<div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading…"/></div>
+</body>
+</html>
diff --git a/grails-app/views/login/doLogin.gsp b/grails-app/views/login/doLogin.gsp
new file mode 100644
index 0000000..5180153
--- /dev/null
+++ b/grails-app/views/login/doLogin.gsp
@@ -0,0 +1,109 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: ndunn
+  Date: 3/16/15
+  Time: 7:53 PM
+--%>
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+    <title>Login</title>
+    <!--
+<link rel="stylesheet" type="text/css" href="../styles/login.css" />
+-->
+    <!--
+<script src="jslib/jquery-1.7.1.min.js" type="text/javascript"></script>
+<script type="text/javascript" src="jslib/jquery-ui-1.8.9.custom/jquery-ui-1.8.9.custom.min.js"></script>
+-->
+    <script>
+        var context;
+        $(document).ready(function () {
+            var pathname = location.pathname;
+            context = /^\/([^\/]+)\//.exec(pathname)[1];
+//            $("head").append("<link rel='stylesheet' type='text/css' href='/" + context + "/styles/login.css'/>");
+            $("#login_button").click(function () {
+                login();
+            });
+            $("#clear_button").click(function () {
+//                $(".input_field").val("");
+                $("#username").val("");
+                $("#password").val("");
+            });
+            $(".input_field").keypress(function (event) {
+                var code = event.keyCode ? event.keyCode : event.which;
+                if (code == $.ui.keyCode.ENTER) {
+                    login();
+                }
+            });
+//            $("#username").focus();
+        });
+
+        function login() {
+            var username = $("#username").val();
+            if (!username) {
+                alert("Missing username");
+                return;
+            }
+            var password = $("#password").val();
+            var remember_me = $("#remember_me").val();
+            var json = new Object();
+            json.username = username;
+            json.password = password;
+            json.rememberMe = remember_me;
+            $.ajax({
+                type: "post",
+                url: "/" + context + "/Login?operation=login",
+                processData: false,
+                dataType: "json",
+                contentType: "application/json",
+                data: JSON.stringify(json),
+                success: function (data) {
+                    if(data.error){
+                        setMessage(data.error);
+                    }
+                    else{
+                        window.location.reload();
+                    }
+                },
+                error: function (jqXHR, textStatus, errorThrown) {
+                    alert('error: '+jqXHR.responseText);
+                    var error = $.parseJSON(jqXHR.responseText);
+                    setMessage(error.error);
+                }
+            });
+        }
+
+        function setMessage(message) {
+            $("#message").text(message);
+        }
+
+    </script>
+</head>
+
+<body>
+
+<div class="input-group" style="margin-bottom: 5px;margin-top: 5px;">
+    <input class="form-control" type="text" id="username" placeholder="Username" autofocus="autofocus"/>
+</div>
+
+<div class="input-group" style="margin-bottom: 5px">
+    <input class="form-control" type="password" id="password" placeholder="Password"/>
+</div>
+
+
+%{--<div class="button_login">--}%
+<button class="btn btn-primary" id="login_button">Login</button>
+<button class="btn btn-default" id="clear_button" >Clear</button>
+%{--<button class="btn btn-default" id="rememberme_button" >Remember Me</button>--}%
+<div>
+    Remember me
+    <input type="checkbox" autocomplete="off" id="remember_me" checked>
+</div>
+
+%{--</div>--}%
+<div id="message"></div>
+</body>
+</html>
diff --git a/grails-app/views/menu.gsp b/grails-app/views/menu.gsp
new file mode 100644
index 0000000..72073d7
--- /dev/null
+++ b/grails-app/views/menu.gsp
@@ -0,0 +1,43 @@
+<%--
+  Created by IntelliJ IDEA.
+  User: ndunn
+  Date: 12/11/14
+  Time: 11:57 AM
+--%>
+
+<%@ page contentType="text/html;charset=UTF-8" %>
+<html>
+<head>
+  <meta name="layout" content="annotator">
+  %{--<title></title>--}%
+</head>
+<body>
+%{--<div id="apolloLogo" style="width: 100%">--}%
+  %{--<a href="http://genomearchitect.org">--}%
+  %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+  %{--Genome Annotator--}%
+  %{--<nav:primary class="nav primary small-menu"/>--}%
+  %{--<nav:primary/>--}%
+  %{--<ul class="nav nav-pills header1" >--}%
+    %{--<li role="presentation" class="">--}%
+      %{--<a href="http://genomearchitect.org">--}%
+        %{--<asset:image src="ApolloLogo_100x36.png" alt="Web Apollo"/></a>--}%
+    %{--</li>--}%
+
+    %{--<li role="presentation" class="menu-item">--}%
+      %{--<g:link action="list" controller="organism">Organisms</g:link>--}%
+    %{--</li>--}%
+    %{--<li role="presentation" class="active menu-item">--}%
+      %{--<g:link action="index" controller="sequence">Sequences</g:link>--}%
+    %{--</li>--}%
+    %{--<li role="presentation" class=" menu-item">--}%
+      %{--<g:link action="index" controller="annotator">Annotate</g:link>--}%
+    %{--</li>--}%
+    %{--<li role="presentation" class=" menu-item">--}%
+      %{--<g:link action="permissions" controller="user">Permissions</g:link>--}%
+    %{--</li>--}%
+  %{--</ul>--}%
+
+%{--</div>--}%
+</body>
+</html>
\ No newline at end of file
diff --git a/grails-app/views/organism/_summaryEntry.gsp b/grails-app/views/organism/_summaryEntry.gsp
new file mode 100644
index 0000000..167ac46
--- /dev/null
+++ b/grails-app/views/organism/_summaryEntry.gsp
@@ -0,0 +1,49 @@
+<div class="col-md-offset-0 col-md-4">
+    <ul class="list-group">
+        <li class="list-group-item list-group-item-info">
+            <g:link action="report" controller="sequence" id="${summaryData.organismId}">
+                ${organism ? organism.commonName : "ALL"}
+            </g:link>
+        </li>
+        <li class="list-group-item">
+            Sequences <span class="badge">${summaryData.sequenceCount}</span>
+        </li>
+        <li class="list-group-item">
+            Genes <span class="badge">${summaryData.geneCount}</span>
+        </li>
+        <li class="list-group-item">
+            Transcripts <span class="badge">${summaryData.transcriptCount}</span>
+            <g:if test="${summaryData.transcriptTypeCount}">
+                <ul class="list-group">
+                    <g:each in="${summaryData.transcriptTypeCount}" var="transcriptType">
+                        <li class="list-group-item">
+                            ${transcriptType.key}
+                            <span class="badge">
+                                ${transcriptType.value}
+                            </span>
+                        </li>
+                    </g:each>
+                </ul>
+            </g:if>
+        </li>
+        <li class="list-group-item">
+            % Coding Transcripts (Features) <span
+                class="badge">${(summaryData.proteinCodingTranscriptPercent * 100).round(2)} (${(summaryData.proteinCodingFeaturePercent * 100).round(2)})</span>
+        </li>
+
+        <li class="list-group-item">
+            Transposable Elements <span class="badge">${summaryData.transposableElementCount}</span>
+        </li>
+        <li class="list-group-item">
+            Repeat Regions <span class="badge">${summaryData.repeatRegionCount}</span>
+        </li>
+        <li class="list-group-item">
+            Exons (Exons/Transcript)
+            <span class="badge">${summaryData.exonCount} ${summaryData.exonCount ? "(${summaryData.exonsPerTranscript})" : ''}</span>
+        </li>
+        <li class="list-group-item">
+            Annotators
+            <span class="badge">${summaryData.annotators?.size()} </span>
+        </li>
+    </ul>
+</div>
diff --git a/grails-app/views/organism/report.gsp b/grails-app/views/organism/report.gsp
new file mode 100644
index 0000000..1b8b3dc
--- /dev/null
+++ b/grails-app/views/organism/report.gsp
@@ -0,0 +1,38 @@
+<%@ page import="org.bbop.apollo.Feature" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="report">
+    <title>Organism Report</title>
+</head>
+
+<body>
+%{--<a href="#show-feature" class="skip" tabindex="-1"><g:message code="default.link.skip.label"--}%
+%{--default="Skip to content…"/></a>--}%
+
+<g:render template="../layouts/reportHeader"/>
+
+%{--<div class="nav" role="navigation">--}%
+%{--<ul>--}%
+%{--<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>--}%
+
+%{--</ul>--}%
+%{--<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>--}%
+%{--<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>--}%
+%{--<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>--}%
+%{--</div>--}%
+
+<div id="show-feature" class="content scaffold-show col-sm-offset-0 report-header" role="main">
+    <h3>Summary</h3>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <g:render template="summaryEntry" model="[summaryData: organismSummaryInstance]"/>
+    <g:each in="${organismSummaries}" var="organismSummaryInstance">
+        <g:render template="summaryEntry"
+                  model="[organism: organismSummaryInstance.key, summaryData: organismSummaryInstance.value]"/>
+    </g:each>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/proxy/_form.gsp b/grails-app/views/proxy/_form.gsp
new file mode 100644
index 0000000..18aaf4c
--- /dev/null
+++ b/grails-app/views/proxy/_form.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.Proxy" %>
+
+
+<div class="fieldcontain ${hasErrors(bean: proxyInstance, field: 'referenceUrl', 'error')} required">
+	<label for="referenceUrl">
+		<g:message code="proxy.referenceUrl.label" default="Reference Url" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:field type="url" name="referenceUrl" required="" value="${proxyInstance?.referenceUrl}" size="80"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: proxyInstance, field: 'targetUrl', 'error')} required">
+	<label for="targetUrl">
+		<g:message code="proxy.targetUrl.label" default="Target Url" />
+		<span class="required-indicator">*</span>
+	</label>
+	<g:field type="url" name="targetUrl" required="" value="${proxyInstance?.targetUrl}" size="80"/>
+
+</div>
+
+<div class="fieldcontain ${hasErrors(bean: proxyInstance, field: 'active', 'error')} ">
+	<label for="active">
+		<g:message code="proxy.active.label" default="Active" />
+
+	</label>
+	<g:checkBox name="active" value="${proxyInstance?.active}" />
+
+</div>
+
+
+<div class="fieldcontain ${hasErrors(bean: proxyInstance, field: 'fallbackOrder', 'error')} ">
+	<label for="fallbackOrder">
+		<g:message code="proxy.fallbackOrder.label" default="Fallback Order" />
+		
+	</label>
+	<g:field name="fallbackOrder" type="number" value="${proxyInstance.fallbackOrder}"/>
+
+</div>
+
+<br/>
diff --git a/grails-app/views/proxy/create.gsp b/grails-app/views/proxy/create.gsp
new file mode 100644
index 0000000..f4e3477
--- /dev/null
+++ b/grails-app/views/proxy/create.gsp
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'proxy.label', default: 'Proxy')}" />
+		<title><g:message code="default.create.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#create-proxy" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="create-proxy" class="content scaffold-create" role="main">
+			<h1><g:message code="default.create.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${proxyInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${proxyInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:proxyInstance, action:'save']" >
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/proxy/edit.gsp b/grails-app/views/proxy/edit.gsp
new file mode 100644
index 0000000..f05c2fb
--- /dev/null
+++ b/grails-app/views/proxy/edit.gsp
@@ -0,0 +1,41 @@
+<%@ page import="org.bbop.apollo.Proxy" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'proxy.label', default: 'Proxy')}" />
+		<title><g:message code="default.edit.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#edit-proxy" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="edit-proxy" class="content scaffold-edit" role="main">
+			<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<g:hasErrors bean="${proxyInstance}">
+			<ul class="errors" role="alert">
+				<g:eachError bean="${proxyInstance}" var="error">
+				<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
+				</g:eachError>
+			</ul>
+			</g:hasErrors>
+			<g:form url="[resource:proxyInstance, action:'update']" method="PUT" >
+				<g:hiddenField name="version" value="${proxyInstance?.version}" />
+				<fieldset class="form">
+					<g:render template="form"/>
+				</fieldset>
+				<fieldset class="buttons">
+					<g:actionSubmit class="save" action="update" value="${message(code: 'default.button.update.label', default: 'Update')}" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/proxy/index.gsp b/grails-app/views/proxy/index.gsp
new file mode 100644
index 0000000..333f767
--- /dev/null
+++ b/grails-app/views/proxy/index.gsp
@@ -0,0 +1,73 @@
+
+<%@ page import="org.bbop.apollo.Proxy" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'proxy.label', default: 'Proxy')}" />
+		<title><g:message code="default.list.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#list-proxy" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="list-proxy" class="content scaffold-list" role="main">
+			<h1><g:message code="default.list.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+				<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<table>
+			<thead>
+					<tr>
+						<th></th>
+						<g:sortableColumn property="referenceUrl" title="${message(code: 'proxy.referenceUrl.label', default: 'Reference Url')}" />
+
+						<g:sortableColumn property="targetUrl" title="${message(code: 'proxy.targetUrl.label', default: 'Target Url')}" />
+
+						<g:sortableColumn property="active" title="${message(code: 'proxy.active.label', default: 'Active')}" />
+
+						<g:sortableColumn property="fallbackOrder" title="${message(code: 'proxy.fallbackOrder.label', default: 'Fallback Order')}" />
+					
+						<g:sortableColumn property="lastSuccess" title="${message(code: 'proxy.lastSuccess.label', default: 'Last Success')}" />
+					
+						<g:sortableColumn property="lastFail" title="${message(code: 'proxy.lastFail.label', default: 'Last Fail')}" />
+					
+
+					</tr>
+				</thead>
+				<tbody>
+				<g:each in="${proxyInstanceList}" status="i" var="proxyInstance">
+					<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+
+
+						<td>
+							<g:link action="show" id="${proxyInstance.id}">Show</g:link>
+						</td>
+
+						<td>${fieldValue(bean: proxyInstance, field: "referenceUrl")}</td>
+
+						<td>${fieldValue(bean: proxyInstance, field: "targetUrl")}</td>
+
+						<td><g:formatBoolean boolean="${proxyInstance.active}" /></td>
+
+						<td>${fieldValue(bean: proxyInstance, field: "fallbackOrder")}</td>
+					
+						<td><g:formatDate date="${proxyInstance.lastSuccess}" /></td>
+					
+						<td><g:formatDate date="${proxyInstance.lastFail}" /></td>
+					
+
+					</tr>
+				</g:each>
+				</tbody>
+			</table>
+			<div class="pagination">
+				<g:paginate total="${proxyInstanceCount ?: 0}" />
+			</div>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/proxy/show.gsp b/grails-app/views/proxy/show.gsp
new file mode 100644
index 0000000..f5891f8
--- /dev/null
+++ b/grails-app/views/proxy/show.gsp
@@ -0,0 +1,82 @@
+
+<%@ page import="org.bbop.apollo.Proxy" %>
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta name="layout" content="main">
+		<g:set var="entityName" value="${message(code: 'proxy.label', default: 'Proxy')}" />
+		<title><g:message code="default.show.label" args="[entityName]" /></title>
+	</head>
+	<body>
+		<a href="#show-proxy" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
+		<div class="nav" role="navigation">
+			<ul>
+				<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+				<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
+				<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
+			</ul>
+		</div>
+		<div id="show-proxy" class="content scaffold-show" role="main">
+			<h1><g:message code="default.show.label" args="[entityName]" /></h1>
+			<g:if test="${flash.message}">
+			<div class="message" role="status">${flash.message}</div>
+			</g:if>
+			<ol class="property-list proxy">
+
+				<li class="fieldcontain">
+					<span id="referenceUrl-label" class="property-label"><g:message code="proxy.referenceUrl.label" default="Reference Url" /></span>
+
+					<span class="property-value" aria-labelledby="referenceUrl-label"><g:fieldValue bean="${proxyInstance}" field="referenceUrl"/></span>
+
+				</li>
+				<li class="fieldcontain">
+					<span id="targetUrl-label" class="property-label"><g:message code="proxy.targetUrl.label" default="Target Url" /></span>
+
+					<span class="property-value" aria-labelledby="targetUrl-label"><g:fieldValue bean="${proxyInstance}" field="targetUrl"/></span>
+
+				</li>
+
+				<li class="fieldcontain">
+					<span id="active-label" class="property-label"><g:message code="proxy.active.label" default="Active" /></span>
+
+					<span class="property-value" aria-labelledby="active-label"><g:formatBoolean boolean="${proxyInstance?.active}" /></span>
+
+				</li>
+
+				%{--<g:if test="${proxyInstance?.fallbackOrder}">--}%
+				<li class="fieldcontain">
+					<span id="fallbackOrder-label" class="property-label"><g:message code="proxy.fallbackOrder.label" default="Fallback Order" /></span>
+						<span class="property-value" aria-labelledby="fallbackOrder-label"><g:fieldValue bean="${proxyInstance}" field="fallbackOrder"/></span>
+					
+				</li>
+				%{--</g:if>--}%
+			
+				%{--<g:if test="${proxyInstance?.lastSuccess}">--}%
+				%{--<li class="fieldcontain">--}%
+					%{--<span id="lastSuccess-label" class="property-label"><g:message code="proxy.lastSuccess.label" default="Last Success" /></span>--}%
+					%{----}%
+						%{--<span class="property-value" aria-labelledby="lastSuccess-label"><g:formatDate date="${proxyInstance?.lastSuccess}" /></span>--}%
+					%{----}%
+				%{--</li>--}%
+				%{--</g:if>--}%
+			
+				%{--<g:if test="${proxyInstance?.lastFail}">--}%
+				%{--<li class="fieldcontain">--}%
+					%{--<span id="lastFail-label" class="property-label"><g:message code="proxy.lastFail.label" default="Last Fail" /></span>--}%
+					%{----}%
+						%{--<span class="property-value" aria-labelledby="lastFail-label"><g:formatDate date="${proxyInstance?.lastFail}" /></span>--}%
+					%{----}%
+				%{--</li>--}%
+				%{--</g:if>--}%
+			
+
+			</ol>
+			<g:form url="[resource:proxyInstance, action:'delete']" method="DELETE">
+				<fieldset class="buttons">
+					<g:link class="edit" action="edit" resource="${proxyInstance}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
+					<g:actionSubmit class="delete" action="delete" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
+				</fieldset>
+			</g:form>
+		</div>
+	</body>
+</html>
diff --git a/grails-app/views/sequence/index.gsp b/grails-app/views/sequence/index.gsp
new file mode 100644
index 0000000..90b60db
--- /dev/null
+++ b/grails-app/views/sequence/index.gsp
@@ -0,0 +1,194 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="oldlook">
+
+    <g:set var="entityName" value="${message(code: 'sequence.label', default: 'Sequence')}"/>
+    <title><g:message code="default.list.label" args="[entityName]"/></title>
+
+    %{--<link rel="stylesheet" type="text/css" href="/jbrowse/plugins/WebApollo/"/>--}%
+    %{--<link rel="stylesheet" type="text/css" href="css/selectTrack.css"/>--}%
+    <asset:stylesheet src="selectTrack.css"/>
+    %{--<asset:stylesheet src="search_sequence.css"/>--}%
+    %{--<asset:stylesheet src="userPermissions.css"/>--}%
+    %{--<asset:javascript src="vendor/jquery-1.11.1.min.js"/>--}%
+    <asset:javascript src="selectTrack.js"/>
+    %{--<script type="text/javascript" src="https://www.google.com/jsapi"></script>--}%
+    %{--<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5.1/dojo/dojo.xd.js"></script>--}%
+
+
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.all.css"></script>--}%
+    %{--<script type="text/javascript" src="/js/jquery-ui-menubar/jquery-1.8.2.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery-1.8.2.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.core.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.widget.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.position.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.button.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.menu.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.menubar.js"></script>--}%
+    %{--<script src="js/jquery-ui-menubar/jquery.ui.dialog.js"></script>--}%
+
+
+    %{--<link rel="stylesheet" type="text/css" href="css/search_sequence.css"/>--}%
+    %{--<link rel="stylesheet" type="text/css" href="css/userPermissions.css"/>--}%
+    %{--<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/>--}%
+    %{--<link rel="stylesheet" type="text/css" href="css/bootstrap-glyphicons.css"/>--}%
+
+    %{--<link rel="stylesheet" href="/jbrowse/plugins/WebApollo/jslib/jqueryui/themes/base/jquery.ui.all.css"/>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery-1.8.2.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.core.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.widget.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.position.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.button.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.menu.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.menubar.js"></script>--}%
+    %{--<script src="jslib/jquery-ui-menubar/jquery.ui.dialog.js"></script>--}%
+    <script>
+
+        //    function cleanup_logo() {
+        //        $("#logo").parent().css("padding", "0 0 0 0");
+        //    }
+        //    ;
+    </script>
+</head>
+
+<body>
+%{--<a href="#list-track" class="skip" tabindex="-1"><g:message code="default.link.skip.label"--}%
+%{--default="Skip to content…"/></a>--}%
+
+%{--<div class="row">--}%
+
+<nav class="navbar navbar-custom" role="navigation">
+    <div class="container-fluid">
+
+        <div class="navbar-header">
+            <a class="navbar-brand" href="http://genomearchitect.org/" target="_blank">
+                <img style="padding: 0;margin: -18px; height:53px;" id="logo"  src="../images/ApolloLogo_100x36.png">
+            </a>
+        </div>
+
+        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
+            <ul class="nav navbar-nav">
+                %{--<li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>--}%
+                %{--<li><a href="#">Link</a></li>--}%
+                %{--<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown"--}%
+                        %{--aria-expanded="true">--}%
+                    %{--File--}%
+                    %{--<span class="caret"></span>--}%
+                %{--</button>--}%
+                %{--<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">--}%
+                    %{--<li role="presentation"><a role="menuitem" tabindex="-1" href="#">Export</a></li>--}%
+                %{--</ul>--}%
+
+                <li class="dropdown">
+                    <a href="#" class="dropdown-toggle header-header" data-toggle="dropdown" role="button"
+                       aria-expanded="false">File <span class="caret"></span></a>
+                    <ul class="dropdown-menu" role="menu">
+                        <li><a href="#">Export</a></li>
+                    </ul>
+                </li>
+                <li class="dropdown">
+                    <a href="#" class="dropdown-toggle header-header" data-toggle="dropdown" role="button"
+                       aria-expanded="false">View<span class="caret"></span></a>
+                    <ul class="dropdown-menu" role="menu">
+                        <li><a href="#">Organism</a></li>
+                        <li><a href="#">Changes</a></li>
+                    </ul>
+                </li>
+                <li class="dropdown">
+                    <a href="#" class="dropdown-toggle header-header" data-toggle="dropdown" role="button"
+                       aria-expanded="false">Permissions<span class="caret"></span></a>
+                    <ul class="dropdown-menu" role="menu">
+                        <li><a href="#">Users</a></li>
+                        <li><a href="#">Groups</a></li>
+                    </ul>
+                </li>
+            </ul>
+            %{--<form class="navbar-form navbar-left" role="search">--}%
+                %{--<div class="form-group">--}%
+                    %{--<input type="text" class="form-control" placeholder="Search">--}%
+                %{--</div>--}%
+                %{--<button type="submit" class="btn btn-default">Submit</button>--}%
+            %{--</form>--}%
+            <ul class="nav navbar-nav navbar-right">
+                <li><a href="#" class="header-header">Login</a></li>
+            </ul>
+        </div><!-- /.navbar-collapse -->
+
+        %{--<div class="btn-group" role="group">--}%
+            %{--<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu3" data-toggle="dropdown"--}%
+                    %{--aria-expanded="true">--}%
+                %{--Tools--}%
+                %{--<span class="caret"></span>--}%
+            %{--</button>--}%
+            %{--<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu3">--}%
+                %{--<li role="presentation"><a role="menuitem" tabindex="-1" href="#">Search sequence</a></li>--}%
+            %{--</ul>--}%
+        %{--</div>--}%
+
+        %{--<div class="btn-group" role="group">--}%
+            %{--<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu4" data-toggle="dropdown"--}%
+                    %{--aria-expanded="true">--}%
+                %{--Admin--}%
+                %{--<span class="caret"></span>--}%
+            %{--</button>--}%
+            %{--<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu4">--}%
+                %{--<li role="presentation"><a role="menuitem" tabindex="-1" href="#">Users</a></li>--}%
+                %{--<li role="presentation"><a role="menuitem" tabindex="-1" href="#">Groups</a></li>--}%
+            %{--</ul>--}%
+        %{--</div>--}%
+
+    </div>
+</nav>
+
+<div id="list-track" class="content scaffold-list" role="main">
+    <h1><g:message code="default.list.label" args="[entityName]"/></h1>
+    <g:select name="organism" from="${org.bbop.apollo.Organism.list()}"
+              optionValue="commonName"/>
+    <br/>
+    <br/>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+
+            %{--<th><g:message code="track.organism.label" default="Organism" /></th>--}%
+
+            <g:sortableColumn property="name" title="${message(code: 'track.name.label', default: 'Name')}"/>
+            <g:sortableColumn property="organism.name"
+                              title="${message(code: 'track.name.label', default: 'Organism')}"/>
+
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${sequenceInstanceList}" status="i" var="sequenceInstance">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+
+                <td><g:link action="show"
+                            id="${sequenceInstance.id}">${fieldValue(bean: sequenceInstance, field: "name")}</g:link></td>
+                <td>${sequenceInstance.organism.commonName}
+                <g:link uri="">Browse</g:link>
+                </td>
+
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${trackInstanceCount ?: 0}"/>
+    </div>
+</div>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/sequence/permissions.gsp b/grails-app/views/sequence/permissions.gsp
new file mode 100644
index 0000000..56d8eda
--- /dev/null
+++ b/grails-app/views/sequence/permissions.gsp
@@ -0,0 +1,50 @@
+<%@ page import="org.bbop.apollo.User" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <g:set var="entityName" value="${message(code: 'user.label', default: 'User')}"/>
+    <title><g:message code="default.list.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<a href="#list-user" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                           default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="list-user" class="content scaffold-list col-lg-offset-1" role="main">
+    %{--<h1><g:message code="default.list.label" args="[entityName]"/></h1>--}%
+    <h1>Sequence Permissions</h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+
+    <div class="col-lg-6 col-lg-offset-1">
+        <table>
+            <th>Sequence</th>
+            <th>Admin</th>
+            <th>Write</th>
+            <th>Export</th>
+            <th>Read</th>
+
+        <g:include view="sequence/singleUser.gsp"/>
+        <g:include view="sequence/singleUser.gsp"/>
+        <g:include view="sequence/singleUser.gsp"/>
+        <g:include view="sequence/singleUser.gsp"/>
+        <g:include view="sequence/singleUser.gsp"/>
+        </table>
+    </div>
+
+    <div class="pagination">
+        <g:paginate total="${userInstanceCount ?: 0}"/>
+    </div>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/sequence/report.gsp b/grails-app/views/sequence/report.gsp
new file mode 100644
index 0000000..bb7692e
--- /dev/null
+++ b/grails-app/views/sequence/report.gsp
@@ -0,0 +1,109 @@
+<%@ page import="org.bbop.apollo.Feature" %>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="report">
+    <title>${organism.commonName} Sequences</title>
+
+    <script>
+        function changeOrganism() {
+            var name = $("#organism option:selected").val();
+            window.location.href = "${createLink(action: 'report')}/" + name;
+        }
+    </script>
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+<div id="list-track" class="form-group report-header content scaffold-list" role="main">
+    <div class="row form-group">
+        <div class="col-lg-4 lead">${organism.commonName} Sequences</div>
+
+        <g:select id="organism" class="input-lg" name="organism"
+                  from="${org.bbop.apollo.Organism.listOrderByCommonName()}" optionValue="commonName" optionKey="id"
+                  value="${organism.id}"
+                  onchange=" changeOrganism(); "/>
+    </div>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+            <g:sortableColumn property="name" title="Name"/>
+            <g:sortableColumn property="length" title="Length"/>
+            <th>Annotators</th>
+            <th>Top Level Features</th>
+            <th>Genes</th>
+            <th>Transcripts</th>
+            <th>Exons</th>
+            <th>TE</th>
+            <th>RR</th>
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${sequenceInstanceList}" status="i" var="sequenceInstance">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+
+                <td>
+                    %{--<g:link action="show"--}%
+                            %{--id="${sequenceInstance.id}">${sequenceInstance.name}</g:link></td>--}%
+                ${sequenceInstance.name}
+                <td style="text-align: left;">
+                    <g:formatNumber number="${sequenceInstance.length}" type="number"/>
+                </td>
+                <td style="max-width: 10%;display: block;padding-right: 10px;padding-left: 10px;">
+                    <g:each in="${sequenceInstance.annotators}" var="annotator">
+                        <a style="margin: 2px;padding: 2px;" href='<g:createLink action="report" controller="annotator" id="${annotator.id}">${annotator.username}</g:createLink>' class="btn btn-default">
+                        ${annotator.username}
+                        </a>
+                    </g:each>
+                </td>
+                <td>
+                    ${sequenceInstance.totalFeatureCount}
+                </td>
+                <td>${sequenceInstance.geneCount}</td>
+                <td>
+                    <g:if test="${sequenceInstance.transcriptCount}">
+                        <div class="info-border">
+                            Total
+                            <span class="badge">${sequenceInstance.transcriptCount}</span>
+                        </div>
+                        <div class="info-border">
+                            Protein encoding
+                            <span class="badge"><g:formatNumber number="${sequenceInstance.proteinCodingTranscriptPercent}" type="percent"/></span>
+                        </div>
+                        <div class="info-border">
+                            Exons / transcript
+                            <span class="badge"><g:formatNumber number="${sequenceInstance.exonsPerTranscript}" type="number"/></span>
+                        </div>
+
+                        <g:each in="${sequenceInstance.transcriptTypeCount}" var="trans">
+                            <div class="info-border">
+                                ${trans.key}
+                                <span class="badge">
+                                    ${trans.value}
+                                </span>
+                            </div>
+                        </g:each>
+                    </g:if>
+                    <g:else>0</g:else>
+                </td>
+                <td>${sequenceInstance.exonCount} </td>
+                <td>${sequenceInstance.transposableElementCount}</td>
+                <td>${sequenceInstance.repeatRegionCount}</td>
+
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${sequenceInstanceCount ?: 0}" params="[id:organism.id]"/>
+   </div>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/sequence/singleUser.gsp b/grails-app/views/sequence/singleUser.gsp
new file mode 100644
index 0000000..0aad6a7
--- /dev/null
+++ b/grails-app/views/sequence/singleUser.gsp
@@ -0,0 +1,69 @@
+<tr>
+    <td>
+        <g:link action="show" controller="sequence" id="1">Group1.3</g:link>
+    </td>
+    <td class="">
+        <div class="list-group">
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-important">Bob${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-success">Group${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+        </div>
+    </td>
+    <td class="">
+        <div class="list-group">
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-important">Bob${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-success">Group${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+        </div>
+    </td>
+    <td class="">
+        <div class="list-group">
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-important">Bob${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-success">Group${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+        </div>
+    </td>
+    <td class="">
+        <div class="list-group">
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-important">Bob${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+            <g:each in="${(0..Math.random() * 3.0.intValue())}">
+                <g:if test="${Math.random() > 0.2}">
+                %{--<g:link action="permissions" controller="user" class="badge badge-important">Bob${Math.random()}</g:link>--}%
+                    <div class="badge badge-success">Group${Math.random()}</div><br/>
+                </g:if>
+            </g:each>
+        </div>
+    </td>
+</tr>
\ No newline at end of file
diff --git a/grails-app/views/sequence/websocketTest.gsp b/grails-app/views/sequence/websocketTest.gsp
new file mode 100644
index 0000000..d16ff18
--- /dev/null
+++ b/grails-app/views/sequence/websocketTest.gsp
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+
+    <asset:javascript src="jquery"/>
+    <asset:javascript src="spring-websocket"/>
+
+    <script type="text/javascript">
+        $(function () {
+            var socket = new SockJS("${createLink(uri: '/stomp')}");
+            var client = Stomp.over(socket);
+
+            client.connect({}, function () {
+                client.subscribe("/topic/hello", function (message) {
+                    $("#helloDiv").append(JSON.parse(message.body));
+                });
+                client.subscribe("/topic/AnnotationEditorService", function (message) {
+                    $("#helloDiv2").append(JSON.parse(message.body));
+                });
+
+                client.send("/app/AnnotationEditorService", {}, JSON.stringify("openconnectstring"))
+            });
+
+            $("#helloButton").click(function () {
+                client.send("/app/hello", {}, JSON.stringify("world"));
+            });
+
+
+        });
+    </script>
+
+    <g:set var="entityName" value="${message(code: 'sequence.label', default: 'Sequence')}"/>
+    <title><g:message code="default.list.label" args="[entityName]"/></title>
+</head>
+
+<body>
+<button id="helloButton">hello</button>
+
+<div id="helloDiv"></div>
+
+<div id="helloDiv2"></div>
+
+<a href="#list-track" class="skip" tabindex="-1"><g:message code="default.link.skip.label"
+                                                            default="Skip to content…"/></a>
+
+<div class="nav" role="navigation">
+    <ul>
+        <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
+        <li><g:link class="create" action="create"><g:message code="default.new.label"
+                                                              args="[entityName]"/></g:link></li>
+    </ul>
+</div>
+
+<div id="list-track" class="content scaffold-list" role="main">
+    <h1><g:message code="default.list.label" args="[entityName]"/></h1>
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+    <table>
+        <thead>
+        <tr>
+
+            %{--<th><g:message code="track.organism.label" default="Organism" /></th>--}%
+
+            <g:sortableColumn property="name" title="${message(code: 'track.name.label', default: 'Name')}"/>
+            <g:sortableColumn property="organism.name" title="${message(code: 'track.name.label', default: 'Organism')}"/>
+
+        </tr>
+        </thead>
+        <tbody>
+        <g:each in="${sequenceInstanceList}" status="i" var="sequenceInstance">
+            <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
+
+                <td><g:link action="show"
+                            id="${sequenceInstance.id}">${fieldValue(bean: sequenceInstance, field: "name")}</g:link></td>
+                <td>${sequenceInstance.organism.commonName}
+                <g:link uri="">Browse</g:link>
+                </td>
+
+            </tr>
+        </g:each>
+        </tbody>
+    </table>
+
+    <div class="pagination">
+        <g:paginate total="${trackInstanceCount ?: 0}"/>
+    </div>
+</div>
+</body>
+</html>
diff --git a/grails-app/views/webServices/index.gsp b/grails-app/views/webServices/index.gsp
new file mode 100644
index 0000000..5dd5993
--- /dev/null
+++ b/grails-app/views/webServices/index.gsp
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="layout" content="main">
+    <title>Apollo Web Service APIs</title>
+    <asset:javascript src="vendor/jquery-1.11.1.min.js"/>
+    <asset:javascript src="vendor/jquery-ui-1.11.2.custom/jquery-ui.min.js"/>
+    <asset:javascript src="vendor/angular.min.js"/>
+    <asset:javascript src="vendor/angular-strap.min.js"/>
+    <asset:javascript src="vendor/angular-strap.tpl.min.js"/>
+    <asset:javascript src="vendor/ui-bootstrap-custom-0.13.4.js"/>
+    <asset:javascript src="vendor/ui-bootstrap-custom-tpls-0.13.4.js"/>
+    <asset:stylesheet src="ui-bootstrap-custom-0.13.4-csp.css"/>
+
+
+    %{--<asset:javascript src="restapidoc/restapidoc.json"/>--}%
+    <asset:javascript src="WebServicesController.js"/>
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+%{--<jumbotron>--}%
+<div class="page-header" style="margin-left: 30px;">
+    <h3>Web Service API</h3>
+
+
+    %{--<h4>Examples</h4>--}%
+    Here are a
+    <a href="https://github.com/GMOD/Apollo/tree/master/docs/web_services/examples">number of examples web services scripts</a> in different languages including shell, groovy (Java), and perl.
+
+
+    %{--<h4>Notes about connecting to https</h4>--}%
+
+    %{--TODO--}%
+    %{--<p>--}%
+        %{--It is advisable if sending out secure passwords over the global internet that you use https.--}%
+
+        %{--If you use https, you will need to either:--}%
+    %{--</p>--}%
+    %{--<ul>--}%
+        %{--<li>Add the web service to the CA Store <a href="">More info here.</a></li>--}%
+        %{--<li>Have the client okay specific server connections <a href=""></a></li>--}%
+        %{--<li>Use curl <a href=""></a></li>--}%
+    %{--</ul>--}%
+
+    %{--<h4></h4>--}%
+</div>
+%{--The Apollo Web Service API is a JSON-based REST API to interact with the annotations and other services of Web Apollo. Both the request and response JSON objects can contain feature information that are based on the Chado schema. We use the web services API for several scripting examples and also use them in the Web Apollo JBrowse plugin, and this document provides details on the parameters for each API.--}%
+%{--What is the Web Service API?--}%
+%{--For a given Web Apollo server url (e.g., https://localhost:8080/apollo or any other Web Apollo site on the web), the Web Service API allows us to make requests to the various "controllers" of the application and perform operations.--}%
+%{--The controllers that are available for Web Apollo include the AnnotationEditorController, the OrganismController, the IOServiceController for downloads of data, and the UserController for user management.--}%
+%{--Most API requests will take:--}%
+%{--<div class="">--}%
+%{--The proper url (e.g., to get features from the AnnotationEditorController, we can send requests to http://localhost/apollo/annotationEditor/getFeatures)--}%
+%{--username - an authorized user--}%
+%{--password - a password--}%
+%{--organism - (if applicable) the "common name" of the organism for the operation -- will also pull from the "user preferences" if none is specified.--}%
+%{--track/sequence - (if applicable) reference sequence name (shown in sequence panel / genomic browse)--}%
+%{--</div>--}%
+%{--uniquename - (if applicable) the uniquename is a UUID used to guarantee a unique ID--}%
+%{--</jumbotron>--}%
+
+<div id="list-featureEvent" class="content scaffold-list" role="main">
+    <g:if test="${flash.message}">
+        <div class="message" role="status">${flash.message}</div>
+    </g:if>
+
+
+    <div ng-app="WebServicesApp">
+
+        <div class="col-sm-offset-1" ng-controller="WebServicesController as ctrl"
+             data-root-url="${application.contextPath}">
+
+
+            %{--<div class="page-header">--}%
+            <div class="alert alert-warning">
+                <h3 style="display: inline;">BASE URL:
+                    <a href="${request.secure ? 'https' : 'http'}://${request.serverName}${request.serverPort != 80 ? ":" + request.serverPort : ''}${request.contextPath}">
+                        ${request.secure ? 'https' : 'http'}://${request.serverName}${request.serverPort != 80 ? ":" + request.serverPort : ''}${request.contextPath}
+                    </a>
+                </h3>
+            </div>
+            %{--</div>--}%
+
+            %{--<div style="margin-bottom: 10px;">--}%
+            %{--<button ng-click="$scope.accordion.firstGroupOpen = true;">Expand / Collapse All</button>--}%
+            %{--</div>--}%
+            %{--<button ng-click="ctrl.expandAll=false">Collapse all</button>--}%
+
+            %{--TODO: simulate formatting:--}%
+            %{--http://loic911.github.io/restapidoc/demo/sample/demo.html?doc_url=http://loic911.github.io/restapidoc/demo/sample/restapidoc.json#--}%
+            %{--http://loic911.github.io/restapidoc/#@RestApiResponseObject--}%
+
+            <accordion ng-cloak class="col-sm-11">
+                <accordion-group ng-repeat="api in ctrl.apis" is-open="status.open">
+                    <accordion-heading>
+                        {{api.name}}
+                        <span class="small">{{api.description}}</span>
+
+                        <i class="pull-right glyphicon"
+                           ng-class="{'glyphicon-chevron-down': status.open, 'glyphicon-chevron-right': !status.open}"></i>
+                        <span class="pull-right badge badge-default">{{api.methods.length}}</span>
+
+                    </accordion-heading>
+
+                    %{--Methods: {{api.methods.length}}--}%
+                    <accordion>
+                        <accordion-group ng-repeat="method in api.methods" is-open="inner.open">
+
+                            <accordion-heading>
+                                {{method.methodName}}
+                                <span class="small">{{method.description}}</span>
+
+                                <i class="pull-right glyphicon"
+                                   ng-class="{'glyphicon-chevron-down': inner.open, 'glyphicon-chevron-right': !inner.open}"></i>
+                            </accordion-heading>
+
+                            <table class="table table-condensed table-striped table-bordered">
+                                <tbody>
+                                <tr>
+                                    <th style="width:15%;">Path</th>
+                                    <td>{{method.path}}</td>
+                                </tr>
+                                <tr>
+                                    <th style="width:15%;">Description</th>
+                                    <td>{{method.description}}</td>
+                                </tr>
+                                <tr>
+                                    <th style="width:15%;">Method</th>
+                                    <td>{{method.consumes}} {{method.verb}}</td>
+                                </tr>
+                                <tr>
+                                    <th colspan="2">Parameters</th>
+                                </tr>
+                                <tr ng-repeat="param in method.queryparameters">
+                                    <td style="width:15%">
+                                        <code>
+                                            {{param.name}}
+                                        </code>
+                                    </td>
+                                    <td>
+                                        %{--<span ng-show="param.required" class="badge badge-success">Required</span>--}%
+                                        %{--<span ng-hide="param.required" class="badge badge-info">Optional</span>--}%
+                                        <span class="badge badge-important">{{param.type}}</span>
+                                        {{param.description}}
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <th colspan="2">Response</th>
+                                </tr>
+                                <tr>
+                                    <th style="width:15%;">Produces</th>
+                                    <td>{{method.produces}}</td>
+                                </tr>
+                                </tbody>
+                            </table>
+
+                        </accordion-group>
+                    </accordion>
+
+                </accordion-group>
+
+            </accordion>
+        </div>
+    </div>
+</div>
+
+</body>
+</html>
diff --git a/grails-app/views/web_services.gsp b/grails-app/views/web_services.gsp
new file mode 100644
index 0000000..05e1cc4
--- /dev/null
+++ b/grails-app/views/web_services.gsp
@@ -0,0 +1,2176 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/html">
+
+<head>
+    <meta name="layout" content="main">
+    <title>Apollo Web Services API</title>
+    <asset:stylesheet src="web_api_stylesheet"/>
+    %{--<asset:image src="webapollo_favicon.ico"/>--}%
+</head>
+
+<body>
+
+<g:render template="../layouts/reportHeader"/>
+
+<div class="section" id="login">
+
+    <h3>Apollo Web Service API</h3>
+
+    <p>
+        The Apollo web service API is fully JSON based, to easily interact with JavaScript. Both the request and
+        response JSON are feature information are based on the Chado schema.  We have provided numerous
+        <a href="https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/">scripting examples</a> utilizing
+    web services in addition to utilizing them internally.
+    </p>
+</div>
+
+<div class="section" id="operation-summary">
+    <h3>Operations</h3>
+
+    <p>Summary of some of the operations supported by the API.</p>
+
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_organism"><code>add_organism</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#get_sequences_for_organism"><code>get_sequences_for_organism</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_feature"><code>add_feature</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#delete_feature"><code>delete_feature</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#get_features"><code>get_features</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_transcript"><code>add_transcript</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#duplicate_transcript"><code>duplicate_transcript</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#merge_transcripts"><code>merge_transcripts</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#set_translation_start"><code>set_translation_start</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#set_translation_end"><code>set_translation_end</code>
+    </a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#set_longest_orf"><code>set_longest_orf</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_exon"><code>add_exon</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#delete_exon"><code>delete_exon</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#merge_exons"><code>merge_exons</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#split_exon"><code>split_exon</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#split_transcript"><code>split_transcript</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#add_sequence_alteration"><code>add_sequence_alteration</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#delete_sequence_alteration"><code>delete_sequence_alteration</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default'
+       href="#get_sequence_alterations"><code>get_sequence_alterations</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_attribute"><code>add_attribute</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#add_comments"><code>add_comments</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#set_status"><code>set_status</code></a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#user_controller"><code>UserController methods</code>
+    </a>
+    <a style='margin-bottom: 5px;' class='btn btn-default' href="#group_controller"><code>GroupController methods</code>
+    </a>
+
+</div>
+
+<div class="section" id="requests">
+
+    <h3>Requests</h3>
+
+    <p>
+        For a given apollo server url (e.g., <code>https://myawesomewebsite.edu/apollo</code>), we need an appropriate website.
+    All JSON requests need:
+    </p>
+
+    <ul>
+        <li>To be addressed to the proper url (e.g., to get features <code>/annotationEditor/getFeatures</code> ->
+            <code>https://myawesomewebsite.edu/apollo/annotationEditor/getFeatures</code>)</li>
+        <li><code>username</code> in the JSON object</li>
+        <li><code>password</code> in the JSON object</li>
+        <li><code>organism</code> (optional) common name (shown in organism panel) in the JSON object for feature related operations
+        </li>
+        <li><code>track/sequence</code> (optional) reference sequence name (shown in sequence panel / genomic browse) in the JSON object for most feature operations
+        </li>
+    </ul>
+
+    <p>
+        <code>uniquname</code> is a common parameter as well. This is a <a
+            href="https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html">UUID</a>
+        used to guarantee a unique ID across systems and not the name/symbol of any feature..
+    </p>
+
+    <h4>Errors</h4>
+    If an error has occurred, a proper HTTP error code (most likely 400 or 500) and an error message.
+    is returned, in JSON format:
+
+    <div class="code">{
+    "error": "mergeExons(): Exons must be in the same strand"
+    }
+    </div>
+
+    <h4>Additional Notes</h4>
+
+    <p>
+        If you are sending password you care about over the wire (even if not using web services) it is <strong>highly recommended</strong>  that you use https (which adds encryption ssl) instead of http.
+    </p>
+
+
+    %{--<h4>Request</h4>--}%
+
+    %{--<p>--}%
+    %{--The URL for login is:--}%
+
+    %{--<div class="code">http://$server:$port/ApolloWeb/Login</div>--}%
+    %{--where <code>Rserver</code> is the server name and <code>$port</code> is the server port.--}%
+
+    %{--<p>--}%
+    %{--For example:--}%
+
+    %{--<div class="code">curl -b cookies.txt -c cookies.txt -e "http://localhost:8080" \--}%
+    %{---H "Content-Type:application/json" -d "{'username': 'demo', 'password': 'demo'}"--}%
+    %{--"http://localhost:8080/apollo/Login?operation=login"--}%
+    %{--</div>--}%
+    %{--</p>--}%
+
+
+    %{--<p>--}%
+    %{--Login expects two parameters: <code>username</code> and <code>password</code>. There currently isn't--}%
+    %{--any real user authentication implemented, so <code>username</code> and <code>password</code> should be set to--}%
+    %{--<code>foo</code> and <code>bar</code> respectively.--}%
+    %{--</p>--}%
+
+    %{--<h4>Response</h4>--}%
+
+    %{--<p>--}%
+    %{--Login will return a JSON containing the <code>session-id</code> for the user. This is needed if the user's--}%
+    %{--browser does not support cookies (or is turned off), in which case the <code>session-id</code> should be--}%
+    %{--appended to all subsequent requests as <code>jsessionid=session-id</code> as an URL parameter.--}%
+
+    %{--<div class="code">{"session-id":"43FBA5B967595D260A1C0E6B7052C7A1"}--}%
+    %{--</div>--}%
+</div>
+
+<div class="section" id="feature">
+    <h3>Feature Object</h3>
+
+    <p>
+        Most requests and responses will contain an array of <code>feature</code> JSON objects named
+        <code>features</code>.
+    The <code>feature</code> object is based on the Chado <code>feature</code>, <code>featureloc</code>,
+        <code>cv</code>,
+    and <code>cvterm</code> tables.
+
+    <div class="code">{
+    "residues": "$residues",
+    "type": {
+    "cv": {"name": "$cv_name"},
+    "name": "$cv_term"
+    },
+    "location": {
+    "fmax": $rightmost_intrabase_coordinate_of_feature,
+    "fmin": $leftmost_intrabase_coordinate_of_feature,
+    "strand": $strand
+    },
+    "uniquename": "$feature_unique_name"
+    "children": [ $array_of_child_features ]
+    "properties": [ $array_of_properties ]
+    }
+    </div>
+    where:
+    <ul>
+        <li><code>residues</code> - A sequence of alphabetic characters representing biological residues (nucleic acids,
+        amino acids) [string]
+        </li>
+        <li><code>type.cv.name</code> - The name of the ontology [string]</li>
+        <li><code>type.name</code> - The name for the cvterm [string]</li>
+        <li><code>location.fmax</code> - The rightmost/maximal intrabase boundary in the linear range [integer]</li>
+        <li><code>location.fmin</code> - The leftmost/minimal intrabase boundary in the linear range [integer]</li>
+        <li><code>strand</code> - The orientation/directionality of the location. Should be 0, -1 or +1 [integer]</li>
+        <li><code>uniquename</code> - The unique name for a feature [string]</li>
+        <li><code>children</code> - Array of child feature objects [array]</li>
+        <li><code>properties</code> - Array of properties (including frameshifts for transcripts) [array]</li>
+    </ul>
+    Note that different operations will require different fields to be set (which will be elaborated upon in each
+    operation section).
+</p>
+
+</div>
+
+
+<div class="section" id="operations">
+    <h3>Operation Details</h3>
+
+    <a name="add_organism"><h4>add_organism</h4></a>
+
+    <p>
+        Adds an organism to the database.
+        An example using this script <a
+            href="https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/groovy/add_organism.groovy">add_organism.groovy</a>.
+    </p>
+
+    <p>
+        Request:  <code>/organism/addOrganism</code>
+    </p>
+
+    <div class="code">
+        {
+        "directory": "/opt/apollo/myanimal/jbrowse/data",
+        "username": "bob at admin.gov",
+        "password": "password"
+        "blatdb": "/opt/apollo/myanimal/myanimal.2bit",
+        "genus": "My",
+        "species": "animal",
+        }
+    </div>
+
+    <p>
+        Response Status 200:
+    </p>
+
+    <div class="code">
+        {}
+    </div>
+
+    <a name="get_sequences_for_organism"><h4>get_sequences_for_organism</h4></a>
+
+    <p>
+        Gets all sequence names for a given organism, that has annotations.
+        An example using this operation is <a
+            href="https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/groovy/migrate_annotations.groovy">migrate_annotations.groovy</a>.
+    </p>
+
+    <p>
+        Request: <code>/organism/getSequencesForOrganism</code>
+    </p>
+
+    <div class="code">
+        {
+        "username": "bob at admin.gov",
+        "password": "password",
+        "organism": "organism_common_name"
+        }
+    </div>
+
+    <p>
+        Response Status 200:
+    </p>
+
+    <div class="code">
+        {
+        "username": "bob at admin.gov",
+        "sequences": [ "chr1", "chr4", "chr11" ],
+        "organism": "organism_common_name"
+        }
+    </div>
+
+    <a name="add_feature"><h4>add_feature</h4></a>
+
+    <inp>
+        Add a top level feature. Returns feature just added.
+
+    </inp>
+
+    <p>
+        Request: <code>/annotationEditor/addFeature</code>
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="delete_feature"><h4>delete_feature</h4></a>
+
+    <p>
+        Delete feature(s) from the session. Each feature only requires <code>uniquename</code> to be set. Returns
+    an empty <code>features</code> array.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/deleteFeature</code>
+    </p>
+
+    <div class="code">{
+    "features": [{"uniquename": "gene"}],
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": []}
+    </div>
+
+    <a name="get_features"><h4>get_features</h4></a>
+
+    <p>
+        Get all top level features.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/getFeatures</code>
+    </p>
+
+    <div class="code">
+        { }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="add_transcript"><h4>add_transcript</h4></a>
+
+    <p>
+        Add transcript(s) to a gene. The first element of the <code>features</code> array should be the gene to add
+    the transcript(s) to, with each subsequent feature being a transcript to be added. The gene feature only
+    requires the <code>uniquename</code> field to be set. Returns the gene which the transcript was added to.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/addTranscript</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "gene"},
+    {
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="duplicate_transcript"><h4>duplicate_transcript</h4></a>
+
+    <p>
+        Duplicate a transcript. Only the first transcript in the <code>features</code> array is processed. The transcript
+    feature only requires <code>uniquename</code> to be set. Returns the parent gene of the transcript.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/duplicateTranscript</code>
+    </p>
+
+    <div class="code">{
+    "features": [{"uniquename": "transcript"}]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript-copy"
+    },
+    {
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }
+    ],
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="merge_transcripts"><h4>merge_transcripts</h4></a>
+
+    <p>
+        Merge two transcripts together. Only the transcripts in the first and second positions in the <code>features</code>
+        array
+        are processed. The transcript features only requires <code>uniquename</code> to be set. If the two transcripts
+    belong
+    to different genes, the parent genes are merged as well. Returns the parent gene of the merged transcripts.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/mergeTranscripts</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "transcript1"},
+    {"uniquename": "transcript2"}
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [
+    {
+    "children": [
+    {
+    "location": {
+    "fmax": 2400,
+    "fmin": 2000,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_1"
+    },
+    {
+    "location": {
+    "fmax": 700,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_1"
+    },
+    {
+    "location": {
+    "fmax": 1500,
+    "fmin": 1000,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_2"
+    },
+    {
+    "location": {
+    "fmax": 2700,
+    "fmin": 2500,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_2"
+    }
+    ],
+    "location": {
+    "fmax": 2700,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript1"
+    }
+    ],
+    "location": {
+    "fmax": 2700,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene1"
+    }]}
+    </div>
+
+    <a name="set_translation_start"><h4>set_translation_start</h4></a>
+
+    <p>
+        Set the CDS start and end in a transcript. The transcript feature only needs to have the <code>uniquename</code>
+        field set. The JSON transcript must contain a CDS feature, which will contain the new CDS boundaries. Other
+        children of the transcript will be ignored. Returns the parent gene of the transcript.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/setTranslationStart</code>
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "children": [{
+    "location": {
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "CDS"
+    },
+    "uniquename": "cds"
+    }],
+    "uniquename": "transcript1"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 700,
+    "fmin": 500,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_1"
+    },
+    {
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_1"
+    },
+    {
+    "location": {
+    "fmin": 100,
+    "fmin": 1400,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "CDS"
+    },
+    "uniquename": "transcript1-CDS"
+    },
+    {
+    "location": {
+    "fmax": 1400,
+    "fmin": 1200,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_3"
+    },
+    {
+    "location": {
+    "fmax": 1000,
+    "fmin": 800,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_2"
+    }
+    ],
+    "location": {
+    "fmax": 1400,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript1"
+    }],
+    "location": {
+    "fmax": 1500,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="set_translation_end"><h4>set_translation_end</h4></a>
+
+    <p>
+        Set the CDS end in a transcript. The transcript feature only needs to have the <code>uniquename</code>
+        field set. The JSON transcript must contain a CDS feature, which will contain the new CDS boundaries. Other
+        children of the transcript will be ignored. Returns the parent gene of the transcript.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/setTranslationEnd</code>
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "children": [{
+    "location": {
+    "fmax": 1300,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "CDS"
+    },
+    "uniquename": "cds"
+    }],
+    "uniquename": "transcript1"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 700,
+    "fmin": 500,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_1"
+    },
+    {
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_1"
+    },
+    {
+    "location": {
+    "fmax": 1300,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "CDS"
+    },
+    "uniquename": "transcript1-CDS"
+    },
+    {
+    "location": {
+    "fmax": 1400,
+    "fmin": 1200,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2_3"
+    },
+    {
+    "location": {
+    "fmax": 1000,
+    "fmin": 800,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1_2"
+    }
+    ],
+    "location": {
+    "fmax": 1400,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript1"
+    }],
+    "location": {
+    "fmax": 1500,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+
+    <a name="set_longest_orf"><h4>set_longest_orf</h4></a>
+
+    <p>
+        Calculate the longest ORF for a transcript. The only element in the <code>features</code> array should
+    be the transcript to process. Only the <code>uniquename</code> field needs to be set for the transcript.
+    Returns the transcript's parent gene.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/setLongestOrf</code>
+    </p>
+
+    <div class="code">{
+    "features": [{"uniquename": "transcript"}]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">Response for operation: set_longest_orf
+    {"features": [{
+    "children": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 693,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1"
+    },
+    {
+    "location": {
+    "fmax": 2628,
+    "fmin": 2392,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon3"
+    },
+    {
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "CDS"
+    },
+    "uniquename": "transcript-CDS"
+    },
+    {
+    "location": {
+    "fmax": 2223,
+    "fmin": 849,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2"
+    }
+    ],
+    "location": {
+    "fmax": 2628,
+    "fmin": 638,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 2735,
+    "fmin": 0,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="add_exon"><h4>add_exon</h4></a>
+
+    <p>
+        Add exon(s) to a transcript. The first element of the <code>features</code> array should be the transcript
+    to which the exon(s) will be added to. Each subsequent feature will be an exon. Merges overlapping exons.
+    Returns the parent gene of the transcript.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/addExon</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "transcript"},
+    {
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1"
+    },
+    {
+    "location": {
+    "fmax": 600,
+    "fmin": 400,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2"
+    },
+    {
+    "location": {
+    "fmax": 1000,
+    "fmin": 500,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon3"
+    }
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1"
+    },
+    {
+    "location": {
+    "fmax": 1000,
+    "fmin": 400,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2"
+    }
+    ],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="delete_exon"><h4>delete_exon</h4></a>
+
+    <p>
+        Delete an exon from a transcript. If there are no exons left on the transcript, the transcript
+        is deleted from the parent gene. The first element of the <code>features</code> array should be the transcript
+    to which the exon(s) will be deleted from. Each subsequent feature will be an exon. Returns the parent gene of
+    the transcript.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/deleteExon</code>
+    </p>
+
+    <div class="code">}{
+    "features": [
+    {"uniquename": "transcript"},
+    {"uniquename": "exon1"}
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [{
+    "location": {
+    "fmax": 1000,
+    "fmin": 400,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon2"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="merge_exons"><h4>merge_exons</h4></a>
+
+    <p>
+        Merge exons. The <code>features</code> array should contain two exons. Each exon only requires
+        <code>uniquename</code> to be set. Returns the parent gene.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/mergeExons</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "exon1"},
+    {"uniquename": "exon2"}
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [{
+    "location": {
+    "fmax": 500,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="split_exon"><h4>split_exon</h4></a>
+
+    <p>
+        Splits the exon, creating two exons, the left one ends at at the new fmax and the right one starts at the
+        new fmin. There should only be one element in the <code>features</code> array, the exon to be split.
+    The exon must have <code>uniquename</code>, <code>location.fmin</code>, and <code>location.fmax</code>
+        set. Returns the parent gene.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/splitExon</code>
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "location": {
+    "fmax": 200,
+    "fmin": 300
+    },
+    "uniquename": "exon1"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [{
+    "children": [
+    {
+    "location": {
+    "fmax": 500,
+    "fmin": 300,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1-right"
+    },
+    {
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1-left"
+    }
+    ],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="split_transcript"><h4>split_transcript</h4></a>
+
+    <p>
+        Split a transcript between the two exons. One transcript will contain all exons from the leftmost
+        exon up to leftExon and the other will contain all exons from rightExon to the rightmost exon.
+        The <code>features</code> array should contain two features, the first one for the left exon and
+    the xecond one for the right exon. Exon's only need to have their <code>uniquename</code> fields
+    set. Returns the parent gene.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/splitTranscript</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "exon1-left"},
+    {"uniquename": "exon1-right"}
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "children": [
+    {
+    "children": [{
+    "location": {
+    "fmax": 500,
+    "fmin": 300,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1-right"
+    }],
+    "location": {
+    "fmax": 1000,
+    "fmin": 300,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript-split"
+    },
+    {
+    "children": [{
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "exon"
+    },
+    "uniquename": "exon1-left"
+    }],
+    "location": {
+    "fmax": 200,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "transcript"
+    },
+    "uniquename": "transcript"
+    }
+    ],
+    "location": {
+    "fmax": 1000,
+    "fmin": 100,
+    "strand": 1
+    },
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "gene"
+    },
+    "uniquename": "gene"
+    }]}
+    </div>
+
+    <a name="add_sequence_alteration"><h4>add_sequence_alteration</h4></a>
+
+    <p>
+        Add sequence alteration(s). Each element of the <code>features</code> array should be an alteration feature
+    (e.g. substitution, insertion, deletion). Each alteration needs to have <code>location</code>, <code>type</code>,
+        <code>uniquename</code>, and <code>residues</code> set. Returns the newly added sequence alteration features.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/addSequenceAlteration</code>
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "location": {
+    "fmax": 642,
+    "fmin": 641,
+    "strand": 1
+    },
+    "residues": "T",
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "substitution"
+    },
+    "uniquename": "substitution1"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [{
+    "location": {
+    "fmax": 642,
+    "fmin": 641,
+    "strand": 1
+    },
+    "residues": "T",
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "substitution"
+    },
+    "uniquename": "substitution1"
+    }]}
+    </div>
+
+    <a name="delete_sequence_alteration"><h4>delete_sequence_alteration</h4></a>
+
+    <p>
+        Delete sequence alteration(s). Each feature only requires <code>uniquename</code> to be set. Returns
+    an empty <code>features</code> array.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/deleteSequenceAlteration</code>
+    </p>
+
+    <div class="code">{
+    "features": [
+    {"uniquename": "substitution1"},
+    {"uniquename": "substitution2"}
+    ]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": []}
+    </div>
+
+    <a name="get_sequence_alterations"><h4>get_sequence_alterations</h4></a>
+
+    <p>
+        Get all sequence alterations. Returns an array of alterations.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/getSequenceAlterations</code>
+    </p>
+
+    <div class="code">
+        { }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{"features": [
+    {
+    "location": {
+    "fmax": 701,
+    "fmin": 644,
+    "strand": 1
+    },
+    "residues": "ATT",
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "insertion"
+    },
+    "uniquename": "insertion1"
+    },
+    {
+    "location": {
+    "fmax": 2301,
+    "fmin": 2300,
+    "strand": 1
+    },
+    "residues": "CCC",
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "insertion"
+    },
+    "uniquename": "insertion2"
+    },
+    {
+    "location": {
+    "fmax": 639,
+    "fmin": 638,
+    "strand": 1
+    },
+    "residues": "C",
+    "type": {
+    "cv": {"name": "SO"},
+    "name": "substitution"
+    },
+    "uniquename": "substitution3"
+    }
+    ]}
+    </div>
+
+    %{--<h4>get_residues_with_alterations</h4>--}%
+
+    %{--<p>--}%
+    %{--Get the residues for feature(s) with any alterations. Only <code>uniquename</code> needs to be set for each--}%
+    %{--feature. Returns the requested feature(s), stripped down to only their <code>uniquename</code> and--}%
+    %{--<code>residues</code>.--}%
+    %{--</p>--}%
+
+    %{--<p>--}%
+    %{--Request:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{--}%
+    %{--"features": [{"uniquename": "transcript-CDS"}],--}%
+    %{--"operation": "get_residues_with_alterations"--}%
+    %{--}--}%
+    %{--</div>--}%
+
+    %{--<p>--}%
+    %{--Response:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{"features": [{--}%
+    %{--"residues": "ATGTATCAGTACGGAAGA...",--}%
+    %{--"uniquename": "transcript-CDS"--}%
+    %{--}]}--}%
+    %{--</div>--}%
+
+    %{--<h4>add_frameshift</h4>--}%
+
+    %{--<p>--}%
+    %{--Add a frameshift to the transcript. The transcript must be the first element in the <code>features</code> array and--}%
+    %{--it must contain a <code>properties</code> array, with each element being a frameshift. Returns the transcript's--}%
+    %{--parent gene.--}%
+    %{--</p>--}%
+
+    %{--<p>--}%
+    %{--Request:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{--}%
+    %{--"features": [{--}%
+    %{--"properties": [{--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "plus_1_frameshift"--}%
+    %{--},--}%
+    %{--"value": "100"--}%
+    %{--}],--}%
+    %{--"uniquename": "transcript"--}%
+    %{--}],--}%
+    %{--"operation": "add_frameshift"--}%
+    %{--}--}%
+    %{--</div>--}%
+
+    %{--<p>--}%
+    %{--Response:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{"features": [{--}%
+    %{--"children": [{--}%
+    %{--"children": [--}%
+    %{--{--}%
+    %{--"location": {--}%
+    %{--"fmax": 693,--}%
+    %{--"fmin": 638,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "exon"--}%
+    %{--},--}%
+    %{--"uniquename": "exon1"--}%
+    %{--},--}%
+    %{--{--}%
+    %{--"location": {--}%
+    %{--"fmax": 2628,--}%
+    %{--"fmin": 2392,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "exon"--}%
+    %{--},--}%
+    %{--"uniquename": "exon3"--}%
+    %{--},--}%
+    %{--{--}%
+    %{--"location": {--}%
+    %{--"fmax": 2628,--}%
+    %{--"fmin": 890,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "CDS"--}%
+    %{--},--}%
+    %{--"uniquename": "transcript-CDS"--}%
+    %{--},--}%
+    %{--{--}%
+    %{--"location": {--}%
+    %{--"fmax": 2223,--}%
+    %{--"fmin": 849,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "exon"--}%
+    %{--},--}%
+    %{--"uniquename": "exon2"--}%
+    %{--}--}%
+    %{--],--}%
+    %{--"location": {--}%
+    %{--"fmax": 2628,--}%
+    %{--"fmin": 638,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"properties": [{--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "plus_1_frameshift"--}%
+    %{--},--}%
+    %{--"value": "100"--}%
+    %{--}],--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "transcript"--}%
+    %{--},--}%
+    %{--"uniquename": "transcript"--}%
+    %{--}],--}%
+    %{--"location": {--}%
+    %{--"fmax": 2735,--}%
+    %{--"fmin": 0,--}%
+    %{--"strand": 1--}%
+    %{--},--}%
+    %{--"type": {--}%
+    %{--"cv": {"name": "SO"},--}%
+    %{--"name": "gene"--}%
+    %{--},--}%
+    %{--"uniquename": "gene"--}%
+    %{--}]}--}%
+    %{--</div>--}%
+
+    %{--<h4>get_residues_with_frameshifts</h4>--}%
+
+    %{--<p>--}%
+    %{--Get the residues for feature(s) with any frameshifts. Only applicable to CDS features. Other features will return--}%
+    %{--unmodified residues. Only <code>uniquename</code> needs to be set for each feature. Returns the requested--}%
+    %{--feature(s), stripped down to only their <code>uniquename</code> and <code>residues</code>.--}%
+    %{--</p>--}%
+
+    %{--<p>--}%
+    %{--Request:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{--}%
+    %{--"features": [{"uniquename": "transcript-CDS"}],--}%
+    %{--"operation": "get_residues_with_frameshifts"--}%
+    %{--}--}%
+    %{--</div>--}%
+
+    %{--<p>--}%
+    %{--Response:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{"features": [{--}%
+    %{--"residues": "ATGTATCAGTACGGAAGA...",--}%
+    %{--"uniquename": "transcript-CDS"--}%
+    %{--}]}--}%
+    %{--</div>--}%
+
+    %{--<h4>get_residues_with_alterations_and_frameshifts</h4>--}%
+
+    %{--<p>--}%
+    %{--Get the residues for feature(s) with any alteration and frameshifts. Only <code>uniquename</code> needs to be set--}%
+    %{--for each--}%
+    %{--feature. Returns the requested feature(s), stripped down to only their <code>uniquename</code> and--}%
+    %{--<code>residues</code>.--}%
+    %{--</p>--}%
+
+    %{--<p>--}%
+    %{--Request:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{--}%
+    %{--"features": [{"uniquename": "transcript-CDS"}],--}%
+    %{--"operation": "get_residues_with_alterations_and_frameshifts"--}%
+    %{--}--}%
+    %{--</div>--}%
+
+    %{--<p>--}%
+    %{--Response:--}%
+    %{--</p>--}%
+
+    %{--<div class="code">{"features": [{--}%
+    %{--"residues": "ATGTATCAGTACGGAAGA...",--}%
+    %{--"uniquename": "transcript-CDS"--}%
+    %{--}]}--}%
+    %{--</div>--}%
+
+    <a name="add_attribute"><h4>add_attribute</h4></a>
+
+    <p>
+        Add attributes to a feature
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/addAttribute</code>
+    </p>
+
+    <div class="code">{
+    "features":[{
+    "non_reserved_properties":[{
+    "tag":"attributeName",
+    "value":"attributeValue"
+    }],
+    "uniquename":"bc7a1c02-3503-416f-97f4-70489e6477b0"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{
+    "features":[{
+    "id":207387,
+    "date_creation":1438616294696,
+    "location":{
+    "id":207388,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323
+    },
+    "sequence":"Group1.14",
+    "name":"GB51850-RA",
+    "owner":"user at email.com",
+    "children":[{
+    "id":207389,
+    "date_creation":1438616294686,
+    "location":{
+    "id":207390,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323
+    },
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"gene",
+    "cv":{"name":"sequence"}
+    },
+    "name":"GB51850-RA-00001",
+    "owner":"user at email.com",
+    "children":[{
+    "id":207391,
+    "date_creation":1438616294661,
+    "location":{
+    "id":207392,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323
+    },
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"mRNA",
+    "cv":{"name":"sequence"}
+    },
+    "name":"986735ff-d666-416f-8246-aba7f9c30d66-exon",
+    "owner":"None",
+    "properties":[{
+    "value":"None",
+    "type":{
+    "name":"owner",
+    "cv":{"name":"feature_property"}
+    }
+    }],
+    "uniquename":"986735ff-d666-416f-8246-aba7f9c30d66",
+    "type":{
+    "name":"exon",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294716,
+    "parent_id":"2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    },
+    {
+    "id":207394,
+    "date_creation":1438616294678,
+    "location":{
+    "id":207395,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323},
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"mRNA",
+    "cv":{"name":"sequence"}
+    },
+    "name":"a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981-CDS",
+    "owner":"None",
+    "properties":[{
+    "value":"None",
+    "type":{
+    "name":"owner",
+    "cv":{"name":"feature_property"}
+    }}],
+    "uniquename":"a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981",
+    "type":{
+    "name":"CDS",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294718,
+    "parent_id":"2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    }],
+    "properties":[{
+    "value":"user at email.com",
+    "type":{
+    "name":"owner",
+    "cv":{"name":"feature_property"}
+    }}],
+    "uniquename":"2fe372e5-3ea6-4ef1-83af-747be8473ef3",
+    "type":{
+    "name":"mRNA",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294838,
+    "parent_id":"bc7a1c02-3503-416f-97f4-70489e6477b0"
+    }],
+    "properties":[{
+    "value":"attributeName",
+    "type":{
+    "cv":{"name":"feature_property"}
+    }},
+    {"value":"attributeValue",
+    "type":{
+    "cv":{"name":"feature_property"}
+    }},
+    {
+    "value":"user at email.com",
+    "type":{
+    "name":"owner",
+    "cv":{
+    "name":"feature_property"}
+    }}],
+    "uniquename":"bc7a1c02-3503-416f-97f4-70489e6477b0",
+    "type":{
+    "name":"gene",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294836}
+    ]
+    }
+
+    </div>
+
+    <a name="add_comments"><h4>add_comments</h4></a>
+
+    <p>
+        Add comments to a feature
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/addComments</code>
+    </p>
+
+    <div class="code">{
+    "features":[{
+    "uniquename":"bc7a1c02-3503-416f-97f4-70489e6477b0",
+    "comments":["This is a comment"]
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{
+    "features":[{
+    "id":207389,
+    "date_creation":1438616294686,
+    "location":{
+    "id":207390,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323
+    },
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"gene",
+    "cv":{"name":"sequence"}
+    },
+    "name":"GB51850-RA-00001",
+    "owner":"user at email.com",
+    "children":[{
+    "id":207391,
+    "date_creation":1438616294661,
+    "location":{
+    "id":207392,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323},
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"mRNA",
+    "cv":{"name":"sequence"}
+    },
+    "name":"986735ff-d666-416f-8246-aba7f9c30d66-exon",
+    "owner":"None",
+    "properties":[{
+    "value":"None",
+    "type":{
+    "name":"owner",
+    "cv":{"name":"feature_property"}
+    }}],
+    "uniquename":"986735ff-d666-416f-8246-aba7f9c30d66",
+    "type":{
+    "name":"exon",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294716,
+    "parent_id":"2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    },
+    {"id":207394,
+    "date_creation":1438616294678,
+    "location":{
+    "id":207395,
+    "fmin":97044,
+    "strand":1,
+    "fmax":97323
+    },
+    "sequence":"Group1.14",
+    "parent_type":{
+    "name":"mRNA",
+    "cv":{"name":"sequence"}
+    },
+    "name":"a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981-CDS",
+    "owner":"None",
+    "properties":[{
+    "value":"None",
+    "type":{
+    "name":"owner",
+    "cv":{
+    "name":"feature_property"
+    }}}],
+    "uniquename":"a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981",
+    "type":{
+    "name":"CDS",
+    "cv":{"name":"sequence"}
+    },
+    "date_last_modified":1438616294718,
+    "parent_id":"2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    }],
+    "properties":[{
+    "value":"user at email.com",
+    "type":{
+    "name":"owner",
+    "cv":{
+    "name":"feature_property"
+    }}}],
+    "uniquename":"2fe372e5-3ea6-4ef1-83af-747be8473ef3",
+    "type":{
+    "name":"mRNA",
+    "cv":{
+    "name":"sequence"
+    }},
+    "date_last_modified":1438616294838,
+    "parent_id":"bc7a1c02-3503-416f-97f4-70489e6477b0"
+    }]}
+    }
+    </div>
+
+    <a name="set_status"><h4>set_status</h4></a>
+
+    <p>
+        Set status for a feature to indicate annotation quality or annotation progress.
+    </p>
+
+    <p>
+        Request: <code>/annotationEditor/setStatus</code>
+    </p>
+
+    <div class="code">{
+    "features":[{
+    "status":"Verification Needed",
+    "uniquename":"bc7a1c02-3503-416f-97f4-70489e6477b0"
+    }]
+    }
+    </div>
+
+    <p>
+        Response:
+    </p>
+
+    <div class="code">{
+    "features": [{
+    "id": 207389,
+    "date_creation": 1438616294686,
+    "location": {
+    "id": 207390,
+    "fmin": 97044,
+    "strand": 1,
+    "fmax": 97323
+    },
+    "sequence": "Group1.14",
+    "parent_type": {
+    "name": "gene",
+    "cv": {"name": "sequence"}
+    },
+    "name": "GB51850-RA-00001",
+    "owner": "user at email.com",
+    "children": [{
+    "id": 207391,
+    "date_creation": 1438616294661,
+    "location": {
+    "id": 207392,
+    "fmin": 97044,
+    "strand": 1,
+    "fmax": 97323
+    },
+    "sequence": "Group1.14",
+    "parent_type": {
+    "name": "mRNA",
+    "cv": {"name": "sequence"}
+    },
+    "name": "986735ff-d666-416f-8246-aba7f9c30d66-exon",
+    "owner": "None",
+    "properties": [{
+    "value": "None",
+    "type": {
+    "name": "owner",
+    "cv": {"name": "feature_property"}
+    }}],
+    "uniquename": "986735ff-d666-416f-8246-aba7f9c30d66",
+    "type": {
+    "name": "exon",
+    "cv": {"name": "sequence"}
+    },
+    "date_last_modified": 1438616294716,
+    "parent_id": "2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    },
+    {
+    "id": 207394,
+    "date_creation": 1438616294678,
+    "location": {
+    "id": 207395,
+    "fmin": 97044,
+    "strand": 1,
+    "fmax": 97323
+    },
+    "sequence": "Group1.14",
+    "parent_type": {
+    "name": "mRNA",
+    "cv": {"name": "sequence"}
+    },
+    "name": "a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981-CDS",
+    "owner": "None",
+    "properties": [{
+    "value": "None",
+    "type": {
+    "name": "owner",
+    "cv": {"name": "feature_property"}
+    }}],
+    "uniquename": "a200b8e2-7a1f-44b4-8ec3-2cc7fe4e9981",
+    "type": {
+    "name": "CDS",
+    "cv": {"name": "sequence"}
+    },
+    "date_last_modified": 1438616294718,
+    "parent_id": "2fe372e5-3ea6-4ef1-83af-747be8473ef3"
+    }],
+    "properties": [{
+    "value": "user at email.com",
+    "type": {
+    "name": "owner",
+    "cv": {"name": "feature_property"}
+    }}],
+    "uniquename": "2fe372e5-3ea6-4ef1-83af-747be8473ef3",
+    "type": {
+    "name": "mRNA",
+    "cv": {"name": "sequence"}
+    },
+    "date_last_modified": 1438616294838,
+    "parent_id": "bc7a1c02-3503-416f-97f4-70489e6477b0"
+    }]
+    }
+    </div>
+</div>
+
+<div class="section" id="ioservice">
+    <h3>IO Service</h3>
+
+    <p>
+        All JSON requests need to define:
+        <code>operation</code> field, which defines the operation being
+    </p>
+
+    <ul>
+        <li>
+            <div class="code">'operation' ('read' or 'write')</div>
+        </li>
+        <li>
+            %{--<div class="code">'adapter' ('GFF3','FASTA','Chado')</div>--}%
+            <div class="code">'adapter' ('GFF3','FASTA')</div>
+        </li>
+        <li>
+            <div class="code">'tracks' (an array of tracks / reference sequences, e.g., ["scf111111","scf111112"])</div>
+        </li>
+        <li>
+            <div class="code">'options' (e.g. output=file&format=gzip)</div>
+        </li>
+    </ul>
+
+    <p>
+        requested (read or write) is returned according to the options and the adapter chosen.
+    </p>
+
+    <p>
+        Example:
+    </p>
+
+    <div class="code">
+        curl -e "http://$hostname:$port" --data '{ operation: "write", adapter: "GFF3",
+        tracks: ["scf1117875582023"], options: "output=file&format=gzip",'username': '$username', 'password': '$password','organism':'$organism' }'
+        http://$hostname:$port:8080/apollo/IOService
+    </div>
+
+</div>
+
+<div class="section" id="userservice">
+    <h3>User Web Service</h3>
+
+    <p>
+        User specific operations are restricted to users with administrator permissions.
+    </p>
+
+    <h4>create_user</h4>
+
+    <p>
+        Request: <code>/user/createUser</code>
+
+        <code>
+            {
+            firstName:"Bob",
+            lastName:"Smith",
+            username:"bob at admin.gov",
+            password:"supersecret"
+            }
+        </code>
+    </p>
+
+    <p>
+        Response: <code>{}</code>
+    </p>
+
+    <h4>delete_user</h4>
+
+    <p>
+        Request: <code>/user/deleteUser</code>
+
+        <code>
+            {
+            userToDelete:"bob at admin.gov"
+            }
+        </code>
+
+        Conversely, userId can also be passed in with the database id.
+    </p>
+
+    <p>
+        Response: <code>{}</code>
+    </p>
+
+
+    <a name="user_controller"></a>
+    <h4>Other UserController Operations</h4>
+
+    <p>
+        <a href="https://github.com/GMOD/Apollo/blob/master/grails-app/controllers/org/bbop/apollo/UserController.groovy">UserController</a>
+    </p>
+
+    <ul>
+        <li>addUserToGroup</li>
+        <li>removeUserFromGroup</li>
+        <li>updateUser</li>
+        <li>getOrganismPermissionsForUser</li>
+        <li>updateOrganismPermission</li>
+    </ul>
+
+
+    <a name="group_controller"></a>
+    <h4>Other GroupController Operations</h4>
+
+    <p>
+        <a href="https://github.com/GMOD/Apollo/blob/master/grails-app/controllers/org/bbop/apollo/GroupController.groovy">GroupController</a>
+    </p>
+
+    <ul>
+        <li>getOrganismPermissionsForGroup</li>
+        <li>loadGroups</li>
+        <li>createGroup</li>
+        <li>deleteGroup</li>
+        <li>updateGroup</li>
+        <li>updateOrganismPermission</li>
+    </ul>
+
+</div>
+
+</body>
+
+</html>
diff --git a/grailsw b/grailsw
new file mode 100755
index 0000000..6b74176
--- /dev/null
+++ b/grailsw
@@ -0,0 +1,363 @@
+##############################################################################
+##                                                                          ##
+##  Grails JVM Bootstrap for UN*X                                           ##
+##                                                                          ##
+##############################################################################
+
+PROGNAME=`basename "$0"`
+DIRNAME=`dirname "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that
+MAX_FD="maximum"
+
+warn() {
+    echo "${PROGNAME}: $*"
+}
+
+die() {
+    warn "$*"
+    exit 1
+}
+
+earlyInit() {
+	return
+}
+lateInit() {
+	return
+}
+
+GROOVY_STARTUP=~/.groovy/startup
+if [ -r "$GROOVY_STARTUP" ]; then
+	. "$GROOVY_STARTUP"
+fi
+
+earlyInit
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false;
+darwin=false;
+mingw=false;
+case "`uname`" in
+    CYGWIN*)
+        cygwin=true
+        ;;
+
+    Darwin*)
+        darwin=true
+        ;;
+
+    MINGW*)
+        mingw=true
+        ;;
+esac
+
+# Attempt to set JAVA_HOME if it's not already set
+if [ -z "$JAVA_HOME" ]; then
+
+	# Set JAVA_HOME for Darwin
+	if $darwin; then
+
+		[ -z "$JAVA_HOME" -a -d "/Library/Java/Home" ] &&
+			export JAVA_HOME="/Library/Java/Home"
+
+		[ -z "$JAVA_HOME" -a -d "/System/Library/Frameworks/JavaVM.framework/Home" ] &&
+			export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home"
+
+	fi
+
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+    [ -n "$GRAILS_HOME" ] &&
+        GRAILS_HOME=`cygpath --unix "$GRAILS_HOME"`
+    [ -n "$JAVACMD" ] &&
+        JAVACMD=`cygpath --unix "$JAVACMD"`
+    [ -n "$JAVA_HOME" ] &&
+        JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+    [ -n "$CP" ] &&
+        CP=`cygpath --path --unix "$CP"`
+fi
+
+# quick detection of JDK version without JVM startup overhead
+CLASSFILE_HEADER_FILE="${JAVA_HOME}/include/classfile_constants.h"
+if [ -z "$GRAILS_NO_PERMGEN" -a -f "${CLASSFILE_HEADER_FILE}" ]; then
+  CLASSFILE_VERSION=`cat "${CLASSFILE_HEADER_FILE}" | grep "#define JVM_CLASSFILE_MAJOR_VERSION" | awk '{ print $3 }' 2> /dev/null`
+  if [ 0$CLASSFILE_VERSION -ge 52 ]; then
+    GRAILS_NO_PERMGEN=1
+  fi 
+fi
+
+# Remove possible trailing slash (after possible cygwin correction)
+GRAILS_HOME=`echo $GRAILS_HOME | sed -e 's|/$||g'`
+
+# Locate GRAILS_HOME if not it is not set
+if [ -z "$GRAILS_HOME" -o ! -d "$GRAILS_HOME" ] ; then
+  # resolve links - $0 may be a link to groovy's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+    PRG="$link"
+    else
+    PRG=`dirname "$PRG"`"/$link"
+    fi
+  done
+
+  SAVED="`pwd`"
+  cd "`dirname \"$PRG\"`/.."
+  GRAILS_HOME="`pwd -P`"
+  cd "$SAVED"
+fi
+
+# Warn the user if JAVA_HOME and/or GRAILS_HOME are not set.
+if [ -z "$JAVA_HOME" ] ; then
+    die "JAVA_HOME environment variable is not set"
+elif [ ! -d "$JAVA_HOME" ] ; then
+    die "JAVA_HOME is not a directory: $JAVA_HOME"
+fi
+
+if [ -z "$GRAILS_HOME" ] ; then
+    warn "GRAILS_HOME environment variable is not set"
+fi
+
+if [ ! -d "$GRAILS_HOME" ] ; then
+    die "GRAILS_HOME is not a directory: $GRAILS_HOME"
+fi
+
+# Use default groovy-conf config
+if [ -z "$STARTER_CONF" ]; then
+    STARTER_CONF="$GRAILS_HOME/conf/groovy-starter.conf"
+fi
+STARTER_CLASSPATH="wrapper/grails-wrapper-runtime-2.5.5.jar:wrapper:."
+
+# Allow access to Cocoa classes on OS X
+if $darwin; then
+    STARTER_CLASSPATH="$STARTER_CLASSPATH:/System/Library/Java/Support"
+fi
+
+# Create the final classpath
+# Setting a classpath using the -cp or -classpath option means not to use
+# the global classpath. Groovy behaves then the same as the java
+# interpreter
+if [ -n "$CP" ] ; then
+    CP="$CP"
+elif [ -n "$CLASSPATH" ] ; then
+    CP="$CLASSPATH"
+fi
+
+# Determine the Java command to use to start the JVM
+if [ -z "$JAVACMD" ]; then
+    if [ -n "$JAVA_HOME" ]; then
+        if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+            # IBM's JDK on AIX uses strange locations for the executables
+            JAVACMD="$JAVA_HOME/jre/sh/java"
+        else
+            JAVACMD="$JAVA_HOME/bin/java"
+        fi
+    else
+        JAVACMD="java"
+    fi
+fi
+if [ ! -x "$JAVACMD" ]; then
+    die "JAVA_HOME is not defined correctly; can not execute: $JAVACMD"
+fi
+
+# Increase the maximum file descriptors if we can
+if [ "$cygwin" = "false" ]; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+	if [ "$MAX_FD_LIMIT" != "unlimited" ]; then
+	    if [ $? -eq 0 ]; then
+	        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then
+	            # use the businessSystem max
+	            MAX_FD="$MAX_FD_LIMIT"
+	        fi
+
+	        ulimit -n $MAX_FD
+	        if [ $? -ne 0 ]; then
+	            warn "Could not set maximum file descriptor limit: $MAX_FD"
+	        fi
+	    else
+	        warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT"
+	    fi
+	fi
+fi
+
+# Fix the cygwin agent issue
+AGENT_GRAILS_HOME=$GRAILS_HOME
+if $cygwin ; then
+    [ -n "$GRAILS_HOME" ] &&
+        AGENT_GRAILS_HOME=`cygpath --windows "$GRAILS_HOME"`
+fi
+
+if $mingw ; then
+    # Converts GRAILS_HOME path to Windows syntax
+    [ -n "$GRAILS_HOME" ] &&
+        AGENT_GRAILS_HOME=`cmd //C echo "$GRAILS_HOME"`
+fi
+
+if [ -z "$GRAILS_AGENT_CACHE_DIR" ]; then
+    GRAILS_AGENT_CACHE_DIR=~/.grails/2.5.5/
+fi
+SPRINGLOADED_PARAMS=profile=grails\;cacheDir=$GRAILS_AGENT_CACHE_DIR
+if [ ! -d "$GRAILS_AGENT_CACHE_DIR" ]; then
+    mkdir -p "$GRAILS_AGENT_CACHE_DIR"
+fi
+
+# Process JVM args
+AGENT_STRING="-javaagent:wrapper/springloaded-1.2.5.RELEASE.jar -Xverify:none -Dspringloaded.synchronize=true -Djdk.reflect.allowGetCallerClass=true -Dspringloaded=$SPRINGLOADED_PARAMS"
+CMD_LINE_ARGS=""
+DISABLE_RELOADING=true
+
+while true; do
+  if [ "$1" = "-cp" ] || [ "$1" = "-classpath" ]; then
+    CP=$2
+    shift 2
+    break
+  fi
+
+  if [ "$1" = "-reloading" ]; then
+    AGENT=$AGENT_STRING
+    DISABLE_RELOADING=false
+    shift
+    break
+  fi
+
+  if [ "$1" = "-noreloading" ]; then
+    DISABLE_RELOADING=true
+    shift
+    break
+  fi
+
+  if [ "$1" = "-debug" ]; then
+    JAVA_OPTS="$JAVA_OPTS -Xdebug -Xnoagent -Dgrails.full.stacktrace=true -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
+    shift
+    break
+  fi
+
+  if [ "$1" != -* ]; then
+    break
+  fi
+
+  CMD_LINE_ARGS="$CMD_LINE_ARGS $1"
+  shift
+done
+
+# Enable agent-based reloading for the 'run-app' command.
+if ! $DISABLE_RELOADING; then
+    for a in "$@"; do
+        if [ "$a" = "run-app" ]; then
+            AGENT=$AGENT_STRING
+        fi
+    done
+
+    if [ $# = 0 ]; then
+        AGENT=$AGENT_STRING
+    fi
+fi
+
+ARGUMENTS="$CMD_LINE_ARGS $@"
+
+# Setup Profiler
+useprofiler=false
+if [ "x$PROFILER" != "x" ]; then
+    if [ -r "$PROFILER" ]; then
+        . $PROFILER
+        useprofiler=true
+    else
+        die "Profiler file not found: $PROFILER"
+    fi
+fi
+
+# For Darwin, use classes.jar for TOOLS_JAR
+TOOLS_JAR="$JAVA_HOME/lib/tools.jar"
+if $darwin; then
+    JAVA_OPTS="-Xdock:name=Grails -Xdock:icon=$GRAILS_HOME/media/icons/grails.icns $JAVA_OPTS"
+#   TOOLS_JAR="/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Classes/classes.jar"
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+    GRAILS_HOME=`cygpath --path --mixed "$GRAILS_HOME"`
+    JAVA_HOME=`cygpath --path --mixed "$JAVA_HOME"`
+    STARTER_CONF=`cygpath --path --mixed "$STARTER_CONF"`
+    if [ "x$CP" != "x" ] ; then
+        CP=`cygpath --path --mixed "$CP"`
+    fi
+    TOOLS_JAR=`cygpath --path --mixed "$TOOLS_JAR"`
+    STARTER_CLASSPATH=`cygpath --path --mixed "$STARTER_CLASSPATH"`
+	# We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW; do
+    	ROOTDIRS="$ROOTDIRS$SEP$dir"
+	    SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GROOVY_CYGPATTERN" != "" ] ; then
+    	OURCYGPATTERN="$OURCYGPATTERN|($GROOVY_CYGPATTERN)"
+    fi
+    # Now convert the arguments
+	ARGUMENTS=""
+    for arg in "$@" ; do
+    	CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+    	if [ $CHECK -ne 0 ] ; then
+	    	convArg=`cygpath --path --ignore --mixed -- "$arg"`
+    	else
+			convArg=$arg
+    	fi
+		ARGUMENTS="$ARGUMENTS $convArg"
+    done
+fi
+
+STARTER_MAIN_CLASS=org.grails.wrapper.GrailsWrapper
+
+lateInit
+
+startGrails() {
+  CLASS=$1
+  shift
+  if [ -n "$GRAILS_OPTS" ]
+     then
+ 	GRAILS_OPTS="$GRAILS_OPTS"
+     else
+	GRAILS_OPTS="-server -Xmx768M -Xms64M -Dfile.encoding=UTF-8"
+	if [ "$GRAILS_NO_PERMGEN" != "1" ]; then
+	   GRAILS_OPTS="$GRAILS_OPTS -XX:PermSize=32m -XX:MaxPermSize=256m"
+	fi
+  fi
+  JAVA_OPTS="$GRAILS_OPTS $JAVA_OPTS $AGENT"
+  # Start the Profiler or the JVM
+  if $useprofiler; then
+      runProfiler
+  else
+  	if [ $# -eq 0 ] ; then         # no argument given
+         exec "$JAVACMD" $JAVA_OPTS \
+          -classpath "$STARTER_CLASSPATH" \
+          -Dgrails.home="$GRAILS_HOME" \
+          -Dtools.jar="$TOOLS_JAR" \
+          -Djava.net.preferIPv4Stack=true \
+          $STARTER_MAIN_CLASS \
+          --main $CLASS \
+          --conf "$STARTER_CONF" \
+          --classpath "$CP"
+  	else
+         exec "$JAVACMD" $JAVA_OPTS \
+          -classpath "$STARTER_CLASSPATH" \
+          -Dgrails.home="$GRAILS_HOME" \
+          -Dtools.jar="$TOOLS_JAR" \
+          -Djava.net.preferIPv4Stack=true \
+          $STARTER_MAIN_CLASS \
+          --main $CLASS \
+          --conf "$STARTER_CONF" \
+          --classpath "$CP" \
+          "${ARGUMENTS}"
+  	fi
+  fi
+}
+
+startGrails $STARTER_MAIN_CLASS "$@"
diff --git a/grailsw.bat b/grailsw.bat
new file mode 100644
index 0000000..5ecf769
--- /dev/null
+++ b/grailsw.bat
@@ -0,0 +1,186 @@
+ at if "%DEBUG%" == "" @echo off
+ at rem ##########################################################################
+ at rem                                                                         ##
+ at rem  Grails JVM Bootstrap for Windows                                       ##
+ at rem                                                                         ##
+ at rem ##########################################################################
+
+ at rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set CLASS=org.grails.wrapper.GrailsWrapper
+
+if exist "%USERPROFILE%/.groovy/preinit.bat" call "%USERPROFILE%/.groovy/preinit.bat"
+
+ at rem Determine the command interpreter to execute the "CD" later
+set COMMAND_COM="cmd.exe"
+if exist "%SystemRoot%\system32\cmd.exe" set COMMAND_COM="%SystemRoot%\system32\cmd.exe"
+if exist "%SystemRoot%\command.com" set COMMAND_COM="%SystemRoot%\command.com"
+
+ at rem Use explicit find.exe to prevent cygwin and others find.exe from being used
+set FIND_EXE="find.exe"
+if exist "%SystemRoot%\system32\find.exe" set FIND_EXE="%SystemRoot%\system32\find.exe"
+if exist "%SystemRoot%\command\find.exe" set FIND_EXE="%SystemRoot%\command\find.exe"
+
+:check_JAVA_HOME
+ at rem Make sure we have a valid JAVA_HOME
+if not "%JAVA_HOME%" == "" goto have_JAVA_HOME
+
+echo.
+echo ERROR: Environment variable JAVA_HOME has not been set.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+echo.
+goto end
+
+:have_JAVA_HOME
+ at rem Remove trailing slash from JAVA_HOME if found
+if "%JAVA_HOME:~-1%"=="\" SET JAVA_HOME=%JAVA_HOME:~0,-1%
+
+ at rem Validate JAVA_HOME
+%COMMAND_COM% /C DIR "%JAVA_HOME%" 2>&1 | %FIND_EXE% /I /C "%JAVA_HOME%" >nul
+if not errorlevel 1 goto check_GRAILS_HOME
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+echo.
+goto end
+
+:check_GRAILS_HOME
+ at rem Define GRAILS_HOME if not set
+if "%GRAILS_HOME%" == "" set GRAILS_HOME=%DIRNAME%..
+
+ at rem Remove trailing slash from GRAILS_HOME if found
+if "%GRAILS_HOME:~-1%"=="\" SET GRAILS_HOME=%GRAILS_HOME:~0,-1%
+
+:init
+
+for %%x in ("%USERPROFILE%") do set SHORTHOME=%%~fsx
+if "x%GRAILS_AGENT_CACHE_DIR%" == "x" set GRAILS_AGENT_CACHE_DIR=%SHORTHOME%/.grails/2.5.5/
+set SPRINGLOADED_PARAMS="profile=grails;cacheDir=%GRAILS_AGENT_CACHE_DIR%"
+if not exist "%GRAILS_AGENT_CACHE_DIR%" mkdir "%GRAILS_AGENT_CACHE_DIR%"
+
+if "%GRAILS_NO_PERMGEN%" == "" (
+	type "%JAVA_HOME%\include\classfile_constants.h" 2>nul | findstr /R /C:"#define JVM_CLASSFILE_MAJOR_VERSION 5[23]" >nul
+	if not errorlevel 1 set GRAILS_NO_PERMGEN=1
+)
+
+set AGENT_STRING=-javaagent:wrapper/springloaded-1.2.5.RELEASE.jar -Xverify:none -Dspringloaded.synchronize=true -Djdk.reflect.allowGetCallerClass=true -Dspringloaded=\"%SPRINGLOADED_PARAMS%\"
+set DISABLE_RELOADING=
+if "%GRAILS_OPTS%" == "" (
+	set GRAILS_OPTS=-server -Xmx768M -Xms64M -Dfile.encoding=UTF-8
+	if not "%GRAILS_NO_PERMGEN%" == "1" (
+		set GRAILS_OPTS=-server -Xmx768M -Xms64M -XX:PermSize=32m -XX:MaxPermSize=256m -Dfile.encoding=UTF-8
+	)
+)
+
+ at rem Get command-line arguments, handling Windows variants
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+ at rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set CP=
+set INTERACTIVE=true
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+set CURR_ARG=%~1
+if "%CURR_ARG:~0,2%" == "-D" (
+	set CMD_LINE_ARGS=%CMD_LINE_ARGS% %~1=%~2
+	shift
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "x-cp" (
+	set CP=%~2
+	shift
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "x-debug" (
+	set JAVA_OPTS=%JAVA_OPTS% -Xdebug -Xnoagent -Dgrails.full.stacktrace=true -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "x-classpath" (
+	set CP=%~2
+	shift
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "x-reloading" (
+	set AGENT=%AGENT_STRING%
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "xrun-app" (
+	set AGENT=%AGENT_STRING%
+	set INTERACTIVE=
+	set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
+	shift
+	goto win9xME_args_slurp
+)
+if "x%~1" == "x-noreloading" (
+	set DISABLE_RELOADING=true
+	shift
+	goto win9xME_args_slurp
+)
+set INTERACTIVE=
+set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
+shift
+goto win9xME_args_slurp
+
+:4NT_args
+ at rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+ at rem Setup the command line
+set STARTER_CLASSPATH=wrapper/grails-wrapper-runtime-2.5.5.jar;wrapper;.
+
+if exist "%USERPROFILE%/.groovy/init.bat" call "%USERPROFILE%/.groovy/init.bat"
+
+ at rem Setting a classpath using the -cp or -classpath option means not to use
+ at rem the global classpath. Groovy behaves then the same as the java interpreter
+
+if "x" == "x%CLASSPATH%" goto after_classpath
+set CP=%CP%;%CLASSPATH%
+:after_classpath
+
+if "x%DISABLE_RELOADING%" == "xtrue" (
+	set AGENT=
+) else (
+	if "x%INTERACTIVE%" == "xtrue" (
+		set AGENT=%AGENT_STRING%
+	)
+)
+
+set STARTER_MAIN_CLASS=org.grails.wrapper.GrailsWrapper
+set STARTER_CONF=%GRAILS_HOME%\conf\groovy-starter.conf
+
+set JAVA_EXE=%JAVA_HOME%\bin\java.exe
+set TOOLS_JAR=%JAVA_HOME%\lib\tools.jar
+
+set JAVA_OPTS=%GRAILS_OPTS% %JAVA_OPTS% %AGENT%
+
+set JAVA_OPTS=%JAVA_OPTS% -Dprogram.name="%PROGNAME%"
+set JAVA_OPTS=%JAVA_OPTS% -Dgrails.home="%GRAILS_HOME%"
+set JAVA_OPTS=%JAVA_OPTS% -Dgrails.version=2.5.5
+set JAVA_OPTS=%JAVA_OPTS% -Dbase.dir=.
+set JAVA_OPTS=%JAVA_OPTS% -Dtools.jar="%TOOLS_JAR%"
+set JAVA_OPTS=%JAVA_OPTS% -Dgroovy.starter.conf="%STARTER_CONF%"
+
+if exist "%USERPROFILE%/.groovy/postinit.bat" call "%USERPROFILE%/.groovy/postinit.bat"
+
+ at rem Execute Grails
+CALL "%JAVA_EXE%" %JAVA_OPTS% -classpath "%STARTER_CLASSPATH%" %STARTER_MAIN_CLASS% --main %CLASS% --conf "%STARTER_CONF%" --classpath "%CP%" "%CMD_LINE_ARGS%"
+:end
+ at rem End local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" endlocal
+
+ at rem Optional pause the batch file
+if "%GROOVY_BATCH_PAUSE%" == "on" pause
diff --git a/gwt-sdk/gwt-dev.jar b/gwt-sdk/gwt-dev.jar
new file mode 100644
index 0000000..f2b93ab
Binary files /dev/null and b/gwt-sdk/gwt-dev.jar differ
diff --git a/gwt-sdk/gwt-user.jar b/gwt-sdk/gwt-user.jar
new file mode 100644
index 0000000..fa6de8c
Binary files /dev/null and b/gwt-sdk/gwt-user.jar differ
diff --git a/gwt-sdk/validation-api-1.0.0.GA-sources.jar b/gwt-sdk/validation-api-1.0.0.GA-sources.jar
new file mode 100644
index 0000000..43611a2
Binary files /dev/null and b/gwt-sdk/validation-api-1.0.0.GA-sources.jar differ
diff --git a/gwt-sdk/validation-api-1.0.0.GA.jar b/gwt-sdk/validation-api-1.0.0.GA.jar
new file mode 100644
index 0000000..1ff2dd7
Binary files /dev/null and b/gwt-sdk/validation-api-1.0.0.GA.jar differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..5b87ac4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,10 @@
+<script>
+  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+     (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+	   m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+	    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+  ga('create', 'UA-62921593-1', 'auto');
+    ga('send', 'pageview');
+
+</script>
diff --git a/install_jbrowse.sh b/install_jbrowse.sh
new file mode 100755
index 0000000..608015b
--- /dev/null
+++ b/install_jbrowse.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+done_message () {
+    if [ $? == 0 ]; then
+        echo " done."
+        if [ "x$1" != "x" ]; then
+            echo $1;
+        fi
+    else
+        echo " failed.  See setup.log file for error messages." $2
+    fi
+}
+
+echo > setup.log;
+echo -n "Installing Perl prerequisites ..."
+if [ -f bin/cpanm ]; then 
+    echo  "Prerequisites installed, finished.";
+    exit 0; 
+fi
+
+if ! ( perl -MExtUtils::MakeMaker -e 1 >/dev/null 2>&1); then
+    echo;
+    echo "WARNING: Your Perl installation does not seem to include a complete set of core modules.  Attempting to cope with this, but if installation fails please make sure that at least ExtUtils::MakeMaker is installed.  For most users, the best way to do this is to use your system's package manager: apt, yum, fink, homebrew, or similar.";
+fi;
+( set -x;
+  chmod +x web-app/jbrowse/bin/cpanm
+  cd web-app/jbrowse
+  bin/cpanm -v --notest -l ../../extlib/ --installdeps .< /dev/null;
+  bin/cpanm -v --notest -l ../../extlib/ --installdeps .< /dev/null;
+  set -e;
+  bin/cpanm -v --notest -l ../../extlib/ --installdeps .< /dev/null;
+  cd -
+  cp -r web-app/jbrowse/bin/ bin;
+  chmod +x bin/*.pl;
+  cp -r web-app/jbrowse/src/perl5 src/perl5;
+) >>setup.log 2>&1;
+done_message "" "As a first troubleshooting step, make sure development libraries and header files for GD, Zlib, and libpng are installed and try again.";
diff --git a/lib/gbol/resources/1.0/resources-1.0.jar b/lib/gbol/resources/1.0/resources-1.0.jar
new file mode 100644
index 0000000..bf20e65
Binary files /dev/null and b/lib/gbol/resources/1.0/resources-1.0.jar differ
diff --git a/lib/gwt/gwt-servlet-deps.jar b/lib/gwt/gwt-servlet-deps.jar
new file mode 100644
index 0000000..97770a2
Binary files /dev/null and b/lib/gwt/gwt-servlet-deps.jar differ
diff --git a/lib/gwt/gwt-servlet.jar b/lib/gwt/gwt-servlet.jar
new file mode 100644
index 0000000..8bf8539
Binary files /dev/null and b/lib/gwt/gwt-servlet.jar differ
diff --git a/lib/gwt/gwtbootstrap3-0.9.4.jar b/lib/gwt/gwtbootstrap3-0.9.4.jar
new file mode 100644
index 0000000..24dffbf
Binary files /dev/null and b/lib/gwt/gwtbootstrap3-0.9.4.jar differ
diff --git a/lib/gwt/gwtbootstrap3-extras-0.9.4.jar b/lib/gwt/gwtbootstrap3-extras-0.9.4.jar
new file mode 100644
index 0000000..90b60bd
Binary files /dev/null and b/lib/gwt/gwtbootstrap3-extras-0.9.4.jar differ
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..830283e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "apollo-genome-architect",
+  "version": "2.0.8",
+  "description": "Genome annotation editor with a Java Server backend and a Javascript client that runs in a web browser as a JBrowse plugin. http://genomearchitect.org",
+  "main": "client/apollo/main.js",
+  "directories": {
+    "doc": "docs",
+    "test": "test"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/GMOD/Apollo.git"
+  },
+  "keywords": [
+    "genome-annotation-editor",
+    "jbrowse",
+    "jbrowse-plugin",
+    "biocuration",
+    "apollo"
+  ],
+  "author": "Nathan Dunn, Deepak Unni",
+  "license": "BSD-3-Clause",
+  "bugs": {
+    "url": "https://github.com/GMOD/Apollo/issues"
+  },
+  "homepage": "https://github.com/GMOD/Apollo#readme",
+  "dependencies": {
+    "jbrowse": "github:gmod/jbrowse#17111e6da6677085f1aeed41216fb558b63ed7ad"
+  }
+}
diff --git a/sample-docker-apollo-config.groovy b/sample-docker-apollo-config.groovy
new file mode 100644
index 0000000..94c4552
--- /dev/null
+++ b/sample-docker-apollo-config.groovy
@@ -0,0 +1,3 @@
+
+// Please see https://github.com/GMOD/docker-apollo
+
diff --git a/sample-h2-apollo-config.groovy b/sample-h2-apollo-config.groovy
new file mode 100644
index 0000000..f42869c
--- /dev/null
+++ b/sample-h2-apollo-config.groovy
@@ -0,0 +1,111 @@
+// default username/password on h2 are given here. see docs for adjusting
+dataSource {
+    pooled = true
+    jmxExport = true
+    driverClassName = "org.h2.Driver"
+    username = "sa"
+    password = ""
+}
+// environment specific settings
+environments {
+    development {
+        // sample config to turn on debug logging in development e.g. for apollo run-local
+        log4j.main = {
+            debug "grails.app"
+        }
+        // sample config to edit apollo specific configs in development mode
+        apollo {
+            gff3.source = "testing"
+        }
+        dataSource {
+            // NOTE: this is in memory, so it will be deleted.
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+        }
+    }
+    test {
+        dataSource {
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            url = "jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+        }
+    }
+
+    //note: not necessarily recommended to use h2 in production mode. see precautions
+    production {
+        dataSource {
+            dbCreate = "update"
+            //NOTE: production mode uses file instead of mem database
+            //NOTE: Please specify the appropriate file path, otherwise /tmp/prodDb will be used.
+            url = "jdbc:h2:/opt/apollo/h2/prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE"
+            properties {
+               // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+               jmxEnabled = true
+               initialSize = 5
+               maxActive = 50
+               minIdle = 5
+               maxIdle = 25
+               maxWait = 10000
+               maxAge = 10 * 60000
+               timeBetweenEvictionRunsMillis = 5000
+               minEvictableIdleTimeMillis = 60000
+               validationQuery = "SELECT 1"
+               validationQueryTimeout = 3
+               validationInterval = 15000
+               testOnBorrow = true
+               testWhileIdle = true
+               testOnReturn = false
+               jdbcInterceptors = "ConnectionState"
+               defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+            }
+        }
+    }
+}
+
+// Uncomment to change the default memory configurations
+//grails.project.fork = [
+//        test   : false,
+//        // configure settings for the run-app JVM
+//        run    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the run-war JVM
+//        war    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the Console UI JVM
+//        console: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+//]
+
+// Uncomment to make changes
+//
+//jbrowse {
+//    git {
+//        url= "https://github.com/GMOD/jbrowse"
+//         tag = "9d765aecaee02a41844fed11a241fdb4c35fc9f8"
+////        branch = "master"
+//        alwaysPull = true
+//        alwaysRecheck = true
+//    }
+//    plugins {
+//        NeatHTMLFeatures{
+//            included = true
+//        }
+//        NeatCanvasFeatures{
+//            included = true
+//        }
+//        RegexSequenceSearch{
+//            included = true
+//        }
+//        HideTrackLabels{
+//            included = true
+//        }
+////        MyVariantInfo {
+////            git = 'https://github.com/GMOD/myvariantviewer'
+////            branch = 'master'
+////            alwaysRecheck = "true"
+////            alwaysPull = "true"
+////        }
+////        SashimiPlot {
+////            git = 'https://github.com/cmdcolin/sashimiplot'
+////            branch = 'master'
+////            alwaysPull = "true"
+////        }
+//    }
+//}
+
diff --git a/sample-mysql-apollo-config.groovy b/sample-mysql-apollo-config.groovy
new file mode 100644
index 0000000..4d95ee7
--- /dev/null
+++ b/sample-mysql-apollo-config.groovy
@@ -0,0 +1,110 @@
+
+environments {
+    development {
+        // sample config to turn on debug logging in development e.g. for apollo run-local
+        log4j.main = {
+            debug "grails.app"
+        }
+        // sample config to edit apollo specific configs in development mode
+        apollo {
+            gff3.source = "testing"
+        }
+        dataSource{
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "com.mysql.jdbc.Driver"
+            dialect = org.hibernate.dialect.MySQL5InnoDBDialect
+            url = "jdbc:mysql://localhost/apollo"
+        }
+    }
+    test {
+        dataSource{
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "com.mysql.jdbc.Driver"
+            dialect = org.hibernate.dialect.MySQL5InnoDBDialect
+            url = "jdbc:mysql://localhost/apollo-test"
+        }
+    }
+    production {
+        dataSource{
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "com.mysql.jdbc.Driver"
+            dialect = org.hibernate.dialect.MySQL5InnoDBDialect
+            url = "jdbc:mysql://localhost/apollo-production"
+            properties {
+                // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+                jmxEnabled = true
+                initialSize = 5
+                maxActive = 50
+                minIdle = 5
+                maxIdle = 25
+                maxWait = 10000
+                maxAge = 10 * 60000
+                timeBetweenEvictionRunsMillis = 5000
+                minEvictableIdleTimeMillis = 60000
+                validationQuery = "SELECT 1"
+                validationQueryTimeout = 3
+                validationInterval = 15000
+                testOnBorrow = true
+                testWhileIdle = true
+                testOnReturn = false
+                jdbcInterceptors = "ConnectionState"
+                defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+            }
+        }
+    }
+}
+
+// Uncomment to change the default memory configurations
+//grails.project.fork = [
+//        test   : false,
+//        // configure settings for the run-app JVM
+//        run    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the run-war JVM
+//        war    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the Console UI JVM
+//        console: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+//]
+
+// Uncomment to make changes
+//
+//jbrowse {
+//    git {
+//        url= "https://github.com/GMOD/jbrowse"
+//         tag = "9d765aecaee02a41844fed11a241fdb4c35fc9f8"
+////        branch = "master"
+//        alwaysPull = true
+//        alwaysRecheck = true
+//    }
+//    plugins {
+//        NeatHTMLFeatures{
+//            included = true
+//        }
+//        NeatCanvasFeatures{
+//            included = true
+//        }
+//        RegexSequenceSearch{
+//            included = true
+//        }
+//        HideTrackLabels{
+//            included = true
+//        }
+////        MyVariantInfo {
+////            git = 'https://github.com/GMOD/myvariantviewer'
+////            branch = 'master'
+////            alwaysRecheck = "true"
+////            alwaysPull = "true"
+////        }
+////        SashimiPlot {
+////            git = 'https://github.com/cmdcolin/sashimiplot'
+////            branch = 'master'
+////            alwaysPull = "true"
+////        }
+//    }
+//}
+
diff --git a/sample-postgres-apollo-config.groovy b/sample-postgres-apollo-config.groovy
new file mode 100644
index 0000000..1421eb8
--- /dev/null
+++ b/sample-postgres-apollo-config.groovy
@@ -0,0 +1,158 @@
+environments {
+    development {
+        // sample config to turn on debug logging in development e.g. for apollo run-local
+        log4j.main = {
+            debug "grails.app"
+        }
+        // sample config to edit apollo specific configs in development mode
+        apollo {
+            gff3.source = "testing"
+        }
+        dataSource{
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "org.postgresql.Driver"
+            dialect = org.hibernate.dialect.PostgresPlusDialect
+            url = "jdbc:postgresql://localhost/apollo"
+        }
+//        dataSource_chado{
+//            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+//            username = "<CHANGEME>"
+//            password = "<CHANGEME>"
+//            driverClassName = "org.postgresql.Driver"
+//            dialect = org.hibernate.dialect.PostgresPlusDialect
+//            url = "jdbc:postgresql://localhost/apollo-chado"
+//        }
+    }
+    test {
+        dataSource{
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "org.postgresql.Driver"
+//        dialect = org.hibernate.dialect.PostgresPlusDialect
+            dialect = "org.bbop.apollo.ImprovedPostgresDialect"
+            url = "jdbc:postgresql://localhost/apollo-test"
+        }
+//        dataSource_chado{
+//            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+//            username = "<CHANGEME>"
+//            password = "<CHANGEME>"
+//            driverClassName = "org.postgresql.Driver"
+////        dialect = org.hibernate.dialect.PostgresPlusDialect
+//            dialect = "org.bbop.apollo.ImprovedPostgresDialect"
+//            url = "jdbc:postgresql://localhost/apollo-test-chado"
+//        }
+    }
+    production {
+        dataSource{
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "<CHANGEME>"
+            password = "<CHANGEME>"
+            driverClassName = "org.postgresql.Driver"
+            dialect = org.hibernate.dialect.PostgresPlusDialect
+            url = "jdbc:postgresql://localhost/apollo-production"
+            properties {
+                // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+                jmxEnabled = true
+                initialSize = 5
+                maxActive = 50
+                minIdle = 5
+                maxIdle = 25
+                maxWait = 10000
+                maxAge = 10 * 60000
+                timeBetweenEvictionRunsMillis = 5000
+                minEvictableIdleTimeMillis = 60000
+                validationQuery = "SELECT 1"
+                validationQueryTimeout = 3
+                validationInterval = 15000
+                testOnBorrow = true
+                testWhileIdle = true
+                testOnReturn = false
+                jdbcInterceptors = "ConnectionState"
+                defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+            }
+        }
+//        dataSource_chado{
+//            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+//            username = "<CHANGEME>"
+//            password = "<CHANGEME>"
+//            driverClassName = "org.postgresql.Driver"
+//            dialect = org.hibernate.dialect.PostgresPlusDialect
+//            url = "jdbc:postgresql://localhost/apollo-production-chado"
+//            properties {
+//                // See http://grails.org/doc/latest/guide/conf.html#dataSource for documentation
+//                jmxEnabled = true
+//                initialSize = 5
+//                maxActive = 50
+//                minIdle = 5
+//                maxIdle = 25
+//                maxWait = 10000
+//                maxAge = 10 * 60000
+//                timeBetweenEvictionRunsMillis = 5000
+//                minEvictableIdleTimeMillis = 60000
+//                validationQuery = "SELECT 1"
+//                validationQueryTimeout = 3
+//                validationInterval = 15000
+//                testOnBorrow = true
+//                testWhileIdle = true
+//                testOnReturn = false
+//                jdbcInterceptors = "ConnectionState"
+//                defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
+//            }
+//        }
+    }
+}
+
+// Uncomment to change the default memory configurations
+//grails.project.fork = [
+//        test   : false,
+//        // configure settings for the run-app JVM
+//        run    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the run-war JVM
+//        war    : [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024, forkReserve: false],
+//        // configure settings for the Console UI JVM
+//        console: [maxMemory: 2048, minMemory: 64, debug: false, maxPerm: 1024]
+//]
+
+// Uncomment to make changes
+//
+//jbrowse {
+//    git {
+//        url= "https://github.com/GMOD/jbrowse"
+//         tag = "9d765aecaee02a41844fed11a241fdb4c35fc9f8"
+////        branch = "master"
+//        alwaysPull = true
+//        alwaysRecheck = true
+//
+//	// Warning: We are still testing the performance of NeatFeatures plugins in combination with Apollo.
+//	// We advise caution if enabling these plugins with Apollo until this process is finalized.
+//    }
+//    plugins {
+//        NeatHTMLFeatures{
+//            included = true
+//        }
+//        NeatCanvasFeatures{
+//            included = true
+//        }
+//        RegexSequenceSearch{
+//            included = true
+//        }
+//        HideTrackLabels{
+//            included = true
+//        }
+//        MyVariantInfo {
+//            git = 'https://github.com/GMOD/myvariantviewer'
+//            branch = 'master'
+//            alwaysRecheck = "true"
+//            alwaysPull = "true"
+//        }
+//        SashimiPlot {
+//            git = 'https://github.com/cmdcolin/sashimiplot'
+//            branch = 'master'
+//            alwaysPull = "true"
+//        }
+//    }
+//}
+
diff --git a/scripts/_Events.groovy b/scripts/_Events.groovy
new file mode 100644
index 0000000..4fe36ec
--- /dev/null
+++ b/scripts/_Events.groovy
@@ -0,0 +1,202 @@
+
+// from grails-gwt plugin, maybe be exapnded
+
+//includeTargets << new File("${gwtPluginDir}/scripts/_GwtInternal.groovy")
+
+eventCompileStart = {
+//  println "COMPILE START"
+//  checkGwtHome()
+//  updateClasspath()
+    projectCompiler.srcDirectories << "${basedir}/src/gwt/org/bbop/apollo/gwt/shared"
+}
+
+//eventSetClasspath = { ClassLoader rootLoader ->
+//  updateClasspath(rootLoader)
+//}
+//
+//// Called when the compilation phase completes.
+//eventCompileEnd = {
+//    // Compile the GWT modules. This target is provided by '_GwtInternal'.
+//    checkGwtHome()
+//    if (!usingGwt16) {
+//        compileGwtModules()
+//    }
+//}
+//
+//// Clean up the GWT-generated files on "clean".
+//eventCleanEnd = {
+//    gwtClean()
+//}
+//
+//eventConfigureWarNameEnd = {
+//    // If any of the GWT modules haven't been compiled, force a compilation
+//    // now. This ensures that WAR files are always created with the latest
+//    // compiled JS files.
+//    if (!gwtModulesCompiled) {
+//        gwtForceCompile = true
+//
+//        // Disable draft mode when we create a WAR.
+//        gwtDraftCompile = false
+//        compileGwtModules()
+//    }
+//}
+//
+////
+//// The GWT libs must be copied to the WAR file. In addition, although
+//// we don't do dynamic compilation in production mode, the plugin
+//// groovy class gets compiled with the UnableToCompleteException in
+//// the class file. Thus, we also have to include this particular file
+//// in the system.
+////
+//eventCreateWarStart = { warName, stagingDir ->
+//    if (gwtHome) {
+//      // Extract the UnableToCompleteException file from gwt-dev-*.jar
+//      ant.unjar(dest: "${stagingDir}/WEB-INF/classes") {
+//          patternset(includes: "com/google/gwt/core/ext/UnableToCompleteException.class")
+//          fileset(dir: "${gwtHome}", includes: "gwt-dev-*.jar")
+//      }
+//    } else if (gwtResolvedDependencies) {
+//      def gwtDevJar = gwtResolvedDependencies.find { it.name.contains("gwt-dev")}
+//      // Extract the UnableToCompleteException file from gwt-dev-*.jar
+//      ant.unjar(dest: "${stagingDir}/WEB-INF/classes") {
+//          patternset(includes: "com/google/gwt/core/ext/UnableToCompleteException.class")
+//          path(location: gwtDevJar.absolutePath)
+//      }
+//    }
+//
+//}
+//
+////
+//// Adds the GWT servlet library to the root loader.
+////
+//eventPackageAppEnd = {
+//  if (getBinding().variables.containsKey("gwtHome")) {
+//    def gwtServlet = new File(gwtHome, "gwt-servlet.jar")
+//    if (gwtServlet.exists()) {
+//      rootLoader.addURL(gwtServlet.toURI().toURL())
+//    }
+//  }
+//}
+//
+//eventGwtRunHostedStart = {
+//    compileGwtClasses()
+//}
+//
+//eventGwtCompileStart = {
+//    compileGwtClasses()
+//}
+//
+//void compileGwtClasses(forceCompile = false) {
+//    if (!gwtClassesCompiled && (usingGoogleGin || forceCompile)) {
+//        // Hack to work around an issue in Google Gin:
+//        //
+//        //    http://code.google.com/p/google-gin/issues/detail?id=36
+//        //
+//        ant.mkdir(dir: gwtClassesDir)
+//        gwtJavac( destDir: gwtClassesDir, includes: "**/*.java") {
+//            src(path: 'src/gwt')//current project gwt modules
+//            //include any sources from any included plugins
+//            buildConfig?.gwt?.plugins?.each {pluginName ->
+//              def pluginDir = binding.variables["${pluginName}PluginDir"]
+//              if (pluginDir && new File("${pluginDir}/src/gwt").exists()) {
+//                src(path: "${pluginDir}/src/gwt")
+//              }
+//            }
+//            ant.classpath {
+//
+//                gwtResolvedDependencies.each { File f ->
+//                    pathElement(location: f.absolutePath)
+//                }
+//
+//                fileset(dir: gwtHome) {
+//                    include(name: "gwt-dev*.jar")
+//                    include(name: "gwt-user.jar")
+//                }
+//
+//                if (gwtLibFile.exists()) {
+//                    fileset(dir: gwtLibPath) {
+//                        include(name: "*.jar")
+//                    }
+//                }
+//
+//                if (buildConfig.gwt.use.provided.deps == true) {
+//                    if (grailsSettings.metaClass.hasProperty(grailsSettings, "providedDependencies")) {
+//                        grailsSettings.providedDependencies.each { dep ->
+//                            pathElement(location: dep.absolutePath)
+//                        }
+//                    }
+//                    else {
+//                        ant.echo message: "WARN: You have set gwt.use.provided.deps, " +
+//                                          "but are using a pre-1.2 version of Grails. The setting " +
+//                                          "will be ignored."
+//                    }
+//                }
+//                pathElement(location: grailsSettings.classesDir.path)
+//
+//                // Fix to get this working with Grails 1.3+. We have to
+//                // add the directory where plugin classes are compiled
+//                // to. Pre-1.3, plugin classes were compiled to the same
+//                // directory as the application classes.
+//                if (grailsSettings.metaClass.hasProperty(grailsSettings, "pluginClassesDir")) {
+//                    pathElement(location: grailsSettings.pluginClassesDir.path)
+//                }
+//            }
+//        }
+//        gwtClassesCompiled = true
+//    }
+//}
+//
+//loadGwtTestTypeClass = { ->
+//    def doLoad = { -> classLoader.loadClass('org.codehaus.groovy.grails.plugins.gwt.GwtJUnitGrailsTestType') }
+//    try {
+//      doLoad()
+//    } catch (ClassNotFoundException e) {
+//      includeTargets << grailsScript("_GrailsCompile")
+//      compile()
+//      doLoad()
+//    }
+//  }
+//
+//registerGwtTestTypes = {
+//    // register gwt test types in unit test phase
+//    if (!binding.variables.containsKey("unitTests") || gwtTestTypesRegistered) return
+//    def type = loadGwtTestTypeClass()
+//    unitTests << type.newInstance(gwtTestTypeName, gwtRelativeTestSrcPath)
+//    unitTests << type.newInstance(gwtProdTestTypeName, gwtRelativeTestSrcPath)
+//    gwtTestTypesRegistered = true
+//}
+//
+//eventAllTestsStart = {
+//    registerGwtTestTypes()
+//}
+//
+//eventTestCompileStart = { types ->
+//    // both gwt and normal unit test can refer GWT classes, hence - they must be compiled before compiling test classes
+//    compileGwtClasses(true)
+//    (gwtDependencies + gwtClassesDir).each { classLoader.addURL(it.toURI().toURL()) }
+//
+//    // if we use specific JDK for compiling GWT classes, then we need to compile test/gwt classes as well
+//    // before Grails attempts that
+//    if (types.class == loadGwtTestTypeClass() && gwtJavacCmd) {
+//        def destDir = new File(grailsSettings.testClassesDir, types.relativeSourcePath)
+//        ant.mkdir(dir: destDir)
+//        gwtJavac(destdir: destDir, classpathref: "grails.test.classpath", debug: "yes") {
+//            src(path: new File("${testSourceDir}", types.relativeSourcePath))
+//        }
+//    }
+//
+//}
+//
+//eventPackagePluginsEnd = {
+//    // invoked after installing plugin and compiling its classes
+//    // and from other places in the build process. However, adjusting
+//    // classpaths and registering gwt test types should happen only once
+//
+//    // if GWT dependencies are not discovered, do it now
+//    if (!gwtDependencies) {
+//        eventSetClasspath(classLoader)
+//        classpathSet = false
+//        classpath()
+//    }
+//    registerGwtTestTypes()
+//}
\ No newline at end of file
diff --git a/scripts/chado-schema-with-ontologies.sql.gz b/scripts/chado-schema-with-ontologies.sql.gz
new file mode 100644
index 0000000..b856908
Binary files /dev/null and b/scripts/chado-schema-with-ontologies.sql.gz differ
diff --git a/scripts/clean_organism.sh b/scripts/clean_organism.sh
new file mode 100755
index 0000000..65814e9
--- /dev/null
+++ b/scripts/clean_organism.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+if [ "$#" -ne 2 ]; then
+    echo "DO NOT USE ON PRODUCTION!!!"
+    echo "Use 'delete_annotations_from_organism.groovy' instead"
+    echo "Usage ./clean_organism.sh <common name> <database>"
+	exit ;
+fi
+echo "Processing organism '$1' on database '$2'" 
+
+
+echo "delete from feature_relationship where id in (select fr.id from feature_relationship fr join feature f on f.id = fr.parent_feature_id join feature_location fl on fl.feature_id = f.id join sequence s on fl.sequence_id=s.id join organism o on s.organism_id=o.id where o.common_name = '$1' ) ; " | psql $2
+
+echo "delete from feature_location where id in (select fl.id from feature_location fl join sequence s on fl.sequence_id=s.id join organism o on s.organism_id=o.id where o.common_name = '$1' ) ; " | psql $2
+
+echo "delete from feature_grails_user where feature_owners_id in ( select u.feature_owners_id from feature_grails_user u join feature f on f.id = u.feature_owners_id join feature_location fl on fl.feature_id = f.id join sequence s on fl.sequence_id=s.id join organism o on s.organism_id=o.id where o.common_name = '$1' ) ; " | psql $2
+
+echo "delete from feature_property where id in ( select u.id from feature_property u join feature f on f.id = u.feature_id join feature_location fl on fl.feature_id = f.id join sequence s on fl.sequence_id=s.id join organism o on s.organism_id=o.id where o.common_name = '$1' ) ; " | psql $2
+
+## select with features without feature locations 
+echo "select count(*) from feature f left join feature_location fl on fl.feature_id = f.id join sequence s on fl.sequence_id=s.id join organism o on s.organism_id=o.id where o.common_name = '$1' and fl is null  ; " | psql $2
+
+echo "delete from feature_event fe where not exists (select 'x' from feature f where f.unique_name = f.unique_name )" | psql $2 
+
diff --git a/scripts/copy_client.sh b/scripts/copy_client.sh
new file mode 100755
index 0000000..74bf646
--- /dev/null
+++ b/scripts/copy_client.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+if [ -f web-app/jbrowse/index.html ]; then 
+	rm -rf web-app/jbrowse/plugins/WebApollo
+	cp -r client/apollo web-app/jbrowse/plugins/WebApollo
+    echo "Apollo client installed" ;
+else
+    echo "ERROR!!!!: JBrowse not installed, can not install client." ; 
+fi
+
+
diff --git a/scripts/delete_all_features.sh b/scripts/delete_all_features.sh
new file mode 100755
index 0000000..48b47c5
--- /dev/null
+++ b/scripts/delete_all_features.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+DBARG=apollo
+
+if [ $# -ge 1 ]
+then
+DBARG=$1
+fi
+
+echo "Deleting features from $DBARG"
+
+psql $DBARG -c  "delete from feature_grails_user";
+psql $DBARG -c  "delete from feature_dbxref";
+psql $DBARG -c  "delete from feature_property";
+psql $DBARG -c  "delete from feature_relationship";
+psql $DBARG -c  "delete from feature_location";
+psql $DBARG -c  "delete from feature";
+psql $DBARG -c  "delete from feature_event";
+psql $DBARG -c  "delete from preference";
+
diff --git a/scripts/delete_only_features.sh b/scripts/delete_only_features.sh
new file mode 100755
index 0000000..5f370aa
--- /dev/null
+++ b/scripts/delete_only_features.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+DBARG=apollo
+
+if [ $# -ge 1 ]
+then
+DBARG=$1
+fi
+
+echo "Deleting features from $DBARG"
+
+psql $DBARG -c  "delete from feature_property";
+psql $DBARG -c  "delete from feature_grails_user";
+psql $DBARG -c  "delete from feature_dbxref";
+psql $DBARG -c  "delete from feature_relationship";
+psql $DBARG -c  "delete from feature_location";
+psql $DBARG -c  "delete from feature";
+psql $DBARG -c  "delete from feature_event";
+
diff --git a/scripts/load_chado_schema.sh b/scripts/load_chado_schema.sh
new file mode 100755
index 0000000..56327bf
--- /dev/null
+++ b/scripts/load_chado_schema.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+usage() {
+    echo ""
+    echo "Usage: $0 -u <psql_user> -d <database_name> -h <host> -p <port> -s <chado prebuilt schema> [ -r ]"
+    echo ""
+    echo "Options:"
+    echo "  -u :          PostgreSQL username"
+    echo "  -d :          Name of the database to which the chado schema and ontologies are to be loaded"
+    echo "  -h :          Database host (default: localhost)"
+    echo "  -p :          Port (default: 5432)"
+    echo "  -s :          Chado schema to load (*sql.gz)"
+    echo "  -r :          Flag that triggers pg_dump if database already exists (optional)"
+    echo ""
+    exit
+}
+
+check_config() {
+    PSQL_CREATEDB=$(which createdb)
+    PSQL_DROPDB=$(which dropdb)
+    PSQL_EXEC=$(which psql)
+
+    if ! [ -x $PSQL_EXEC ] ; then
+        echo "You must install PostgreSQL and 'psql' must be accessible in current PATH"
+        exit
+    fi
+
+    if ! [ -x $PSQL_CREATEDB ] ; then
+        echo "'createdb' must be accessible in current PATH"
+        exit
+    fi
+
+    if ! [ -x $PSQL_DROPDB ] ; then
+        echo "'dropdb' must be accessible in current PATH"
+        exit
+    fi
+}
+
+load_chado_schema() {
+    file_type=`echo ${CHADO_SCHEMA##*.}`
+    echo "Loading chado schema ${CHADO_SCHEMA} to database '$2'."
+
+    if [ $file_type == "gz" ]; then
+        gunzip -c $CHADO_SCHEMA | psql -U $1 -d $2 -h $3 -p $4 &> $STDLOG
+    else
+        psql -U $1 -d $2 -h $3 -p $4 < $CHADO_SCHEMA &> $STDLOG
+    fi
+}
+
+dump_database() {
+    OUTPUT=$2"_database_dump_"$TIMESTAMP".sql"
+    echo pg_dump -U $1 -d $2 -h $3 -p $4 -f $OUTPUT -b
+    pg_dump -U $1 -d $2 -h $3 -p $4 -f $OUTPUT -b
+}
+
+# Default
+PORT="5432"
+HOST="localhost"
+DEFAULTDB="template1"
+PGDUMP=0
+TIMESTAMP=`date +%F_%T`
+
+STDLOG="load_chado_schema_${TIMESTAMP}.log"
+
+if [ $# -eq 0 ]; then
+    usage
+    exit
+fi
+
+while getopts "h:p:u:d:s:r" opt; do
+    case "$opt" in
+    h)  HOST=$OPTARG
+        ;;
+    p)  PORT=$OPTARG
+        ;;
+    u)  PG_USER=$OPTARG
+        ;;
+    d)  DB=$OPTARG
+        ;;
+    s)  CHADO_SCHEMA=$OPTARG
+        ;;
+    r)  PGDUMP=1
+        ;;
+    *)  usage
+        ;;
+    ?)  usage
+        ;;
+    esac
+done
+
+if [ -z "${PG_USER}" ] || [ -z "${DB}" ] || [ -z "${CHADO_SCHEMA}" ] ; then
+    usage
+fi
+
+if [ ! -f ${CHADO_SCHEMA} ]; then
+    echo "File ${CHADO_SCHEMA} not found."
+    exit
+fi
+
+check_config
+
+psql -U $PG_USER -h $HOST -p $PORT -d $DEFAULTDB -c "CREATE DATABASE \"$DB\""
+EXIT_STATUS=$?
+
+if [ $EXIT_STATUS -eq 0 ]; then
+    # CREATE DATABASE command was successful (i.e. there was no pre-existing database with the same name)
+    load_chado_schema $PG_USER $DB $HOST $PORT
+
+elif [ $EXIT_STATUS -eq 1 ]; then
+    # CREATE DATABASE command was unsuccessful because there was a pre-existing database with the same name
+    if [ $PGDUMP -eq 1 ]; then
+        # -r option was provided at run-time; will attempt to backup the existing database
+        echo "Database '$DB' already exists. Backing up data via pg_dump."
+        dump_database $PG_USER $DB $HOST $PORT
+        if [ $? -eq 0 ]; then
+            # PG_DUMP was successful
+            echo "pg_dump was successful."
+            echo "Dropping and creating database '$DB'."
+            # DROP DATABASE after a PG_DUMP
+            psql -U $PG_USER -h $HOST -p $PORT -d $DEFAULTDB -c "DROP DATABASE \"$DB\""
+            if [ $? -ne 0 ]; then
+                echo "Cannot drop database '$DB' due to lack of privileges or existing open connections."
+                exit
+            fi
+
+            # CREATE DATABASE
+            psql -U $PG_USER -h $HOST -p $PORT -d $DEFAULTDB -c "CREATE DATABASE \"$DB\""
+            if [ $? -ne 0 ]; then
+                echo "Cannot create database '$DB' due to lack of privileges."
+                exit
+            fi
+
+            # finally, load chado schema
+            load_chado_schema $PG_USER $DB $HOST $PORT
+        fi
+    else
+        # -r option was not provided at run-time; will not try to do anything
+        echo "Database '$DB' already exists. If you would like to do a pg_dump, to backup its contents, run the script again with '-r' flag."
+    fi
+
+else
+    # CREATE DATABASE command was unsuccessful for other reasons
+    echo "Cannot create database '$DB' due to improper connection parameters, lack of privileges or non-existent user '$PG_USER'."
+    exit
+fi
+
+echo "Chado schema loaded successfully to ${DB}. Check $STDLOG for more information."
\ No newline at end of file
diff --git a/scripts/print_all_features.sh b/scripts/print_all_features.sh
new file mode 100755
index 0000000..dc65db8
--- /dev/null
+++ b/scripts/print_all_features.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+DBARG=apollo
+
+if [ $# -ge 1 ]
+then
+DBARG=$1
+fi
+
+echo "Printing features from $DBARG"
+
+echo "Users"
+psql $DBARG -c  "select count(*) from feature_grails_user";
+
+echo "DbXrefs"
+psql $DBARG -c  "select count(*) from feature_dbxref";
+
+echo "Properties"
+psql $DBARG -c  "select count(*) from feature_property";
+
+echo "Relationships"
+psql $DBARG -c  "select count(*) from feature_relationship";
+
+echo "Locations"
+psql $DBARG -c  "select count(*) from feature_location";
+
+echo "Features"
+psql $DBARG -c  "select count(*) from feature";
+
+echo "Feature Events"
+psql $DBARG -c  "select count(*) from feature_event";
+echo "Preferences"
+psql $DBARG -c  "select count(*) from preference";
+
diff --git a/scripts/version-template.jsp b/scripts/version-template.jsp
new file mode 100644
index 0000000..b6e84c8
--- /dev/null
+++ b/scripts/version-template.jsp
@@ -0,0 +1 @@
+<a href='https://github.com/GMOD/Apollo/releases/@GITHUB_VERSION@' target='_blank'>Version: @POM_VERSION@</a>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..2af3b1e
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,19 @@
+/*
+ * This settings file was auto generated by the Gradle buildInit task
+ * by 'nathandunn' at '3/14/16 12:08 PM' with Gradle 2.11
+ *
+ * The settings file is used to specify which projects to include in your build.
+ * In a single project build this file can be empty or even removed.
+ *
+ * Detailed information about configuring a multi-project build in Gradle can be found
+ * in the user guide at https://docs.gradle.org/2.11/userguide/multi_project_builds.html
+ */
+
+/*
+// To declare projects as part of a multi-project build use the 'include' method
+include 'shared'
+include 'api'
+include 'services:webservice'
+*/
+
+rootProject.name = 'nathanApolloMaster'
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/groovy/org/bbop/apollo/ConfigurableFeature.groovy b/src/groovy/org/bbop/apollo/ConfigurableFeature.groovy
new file mode 100644
index 0000000..20b1bea
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/ConfigurableFeature.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+/**
+ * Created by ndunn on 7/15/15.
+ *
+ * Used to customize a feature type
+ * TODO: make sure that this goes into generic feature and generic transcipt or will ahve to over-ride
+ */
+trait ConfigurableFeature {
+
+    abstract void setCvTerm(String cvTerm )
+    abstract void setOntologyId(String ontologyId)
+    abstract void setAlternateCvTerm(String alternateCvTerm)
+    abstract void setClassName(String className)
+
+    abstract String getCvTerm()
+    abstract String getOntologyId()
+    abstract String getAlternateCvTerm()
+    abstract String getClassName()
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/CvTermStringEnum.groovy b/src/groovy/org/bbop/apollo/CvTermStringEnum.groovy
new file mode 100644
index 0000000..c2923a4
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/CvTermStringEnum.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo
+/**
+ * Created by ndunn on 10/28/14.
+ */
+enum CvTermStringEnum {
+     PART_OF("PartOf")
+
+     String value
+
+     public CvTermStringEnum(String value){
+          this.value = value
+     }
+
+     public CvTermStringEnum(){
+          this.value = name().toLowerCase()
+     }
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/DefaultPaddingStrategy.groovy b/src/groovy/org/bbop/apollo/DefaultPaddingStrategy.groovy
new file mode 100644
index 0000000..1431683
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/DefaultPaddingStrategy.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+/**
+ * Created by ndunn on 5/9/15.
+ */
+class DefaultPaddingStrategy implements PaddingStrategy{
+
+    String pad(Integer count){
+        return count.toString()
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/FeaturePositionComparator.groovy b/src/groovy/org/bbop/apollo/FeaturePositionComparator.groovy
new file mode 100644
index 0000000..18990f0
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/FeaturePositionComparator.groovy
@@ -0,0 +1,51 @@
+package org.bbop.apollo
+/**
+ * Created by ndunn on 10/29/14.
+ */
+class FeaturePositionComparator<T extends Feature> implements  Comparator<T>{
+
+    private boolean sortByStrand;
+
+    FeaturePositionComparator() {
+        this(true)
+    }
+
+    FeaturePositionComparator(boolean sortByStrand) {
+        this.sortByStrand = sortByStrand
+    }
+
+    int compare(T feature1, T feature2) {
+
+        if (feature1 == null || feature2 == null) {
+//            log.info("both features null");
+        }
+
+        int retVal = 0;
+        FeatureLocation featureLocation1 = feature1.featureLocation
+        FeatureLocation featureLocation2  = feature2.featureLocation
+        if (featureLocation1.fmin < featureLocation2.fmin) {
+            retVal = -1;
+        }
+        else if (featureLocation1.fmin > featureLocation2.fmin) {
+            retVal = 1;
+        }
+        else if (featureLocation1.fmax < featureLocation2.fmax) {
+            retVal = -1;
+        }
+        else if (featureLocation1.fmax > featureLocation2.fmax) {
+            retVal = 1;
+        }
+        else if (featureLocation1.calculateLength() != featureLocation2.calculateLength()) {
+            retVal = featureLocation1.calculateLength() < featureLocation2.calculateLength() ? -1 : 1;
+        }
+            // overlapping perfectly, use strand to force consistent results
+        else{
+            retVal = featureLocation1.strand - featureLocation2.strand
+        }
+
+        if (sortByStrand && featureLocation1.strand == -1) {
+            retVal *= -1;
+        }
+        return retVal;
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/FlankingRegion.groovy b/src/groovy/org/bbop/apollo/FlankingRegion.groovy
new file mode 100644
index 0000000..310fbef
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/FlankingRegion.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.sequence.Strand
+
+class FlankingRegion {
+
+    Sequence sequence
+    int fmin
+    int fmax
+    Strand strand
+
+
+//    static String cvTerm  = "FlankingRegion"
+//    static String ontologyId = "SO:0000239"
+
+
+}
diff --git a/src/groovy/org/bbop/apollo/FormatUtil.groovy b/src/groovy/org/bbop/apollo/FormatUtil.groovy
new file mode 100644
index 0000000..f9b01e5
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/FormatUtil.groovy
@@ -0,0 +1,78 @@
+package org.bbop.apollo
+
+import grails.util.Pair
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+
+/**
+ * Created by ndunn on 4/14/15.
+ */
+class FormatUtil {
+
+    public static JSONObject convertFromXMLToJSON(Document xmlDocument) throws JSONException {
+        /*
+        NodeList children = xmlDocument.getChildNodes();
+        for (int i = 0; i < children.getLength(); ++i) {
+            Pair<String, JSONObject> childJson = processXMLNode(children.item(i));
+            json.put(childJson.getFirst(), childJson.getSecond());
+        }
+        */
+        Pair<String, ? extends Object> retVal = processXMLNode(xmlDocument.getDocumentElement());
+        return new JSONObject().put(retVal.aValue, retVal.bValue);
+    }
+
+    private static Pair<String, ? extends Object> processXMLNode(Node node) throws JSONException {
+        JSONObject json = new JSONObject();
+        NodeList children = node.getChildNodes();
+        for (int i = 0; i < children.getLength(); ++i) {
+            Node child = children.item(i);
+            if (child instanceof Element) {
+                Pair<String, ? extends Object> childJson = processXMLNode(child);
+                json.accumulate(childJson.aValue, childJson.bValue);
+            }
+            else if (child instanceof Text && children.getLength() == 1) {
+                return new Pair<String, String>(node.getNodeName(), child.getTextContent());
+//                json.put(node.getNodeName(), child.getTextContent());
+            }
+        }
+        return new Pair<String, JSONObject>(node.getNodeName(), json);
+    }
+
+    /*
+    private static JSONObject processXMLNode(Node node) throws JSONException {
+        JSONObject json = new JSONObject();
+        NodeList children = node.getChildNodes();
+        List<JSONObject> jsonChildren = new ArrayList<JSONObject>();
+        for (int i = 0; i < children.getLength(); ++i) {
+            Node child = children.item(i);
+            if (child instanceof Element) {
+                JSONObject childJson = processXMLNode(child);
+                jsonChildren.add(childJson);
+            }
+            else if (child instanceof Text && children.getLength() == 1) {
+                json.put(node.getNodeName(), child.getTextContent());
+            }
+        }
+        if (jsonChildren.size() > 0) {
+            if (jsonChildren.size() == 1) {
+                JSONObject child = jsonChildren.get(0);
+                json.put(node.getNodeName(), child);
+            }
+            else {
+                JSONArray items = new JSONArray();
+                for (JSONObject child : jsonChildren) {
+                    items.put(child);
+                }
+                json.put(node.getNodeName(), items);
+            }
+        }
+        return json;
+    }
+    */
+
+}
diff --git a/src/groovy/org/bbop/apollo/GFF3Entry.groovy b/src/groovy/org/bbop/apollo/GFF3Entry.groovy
new file mode 100644
index 0000000..8cc6eb9
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/GFF3Entry.groovy
@@ -0,0 +1,124 @@
+package org.bbop.apollo
+/**
+ * Created by Deepak on 3/9/15.
+ */
+public class GFF3Entry {
+
+    private String seqId;
+    private String source;
+    private String type;
+    private int start;
+    private int end;
+    private String score;
+    private String strand;
+    private String phase;
+    private Map<String, String> attributes;
+
+    public GFF3Entry(String seqId, String source, String type, int start, int end, String score, String strand, String phase) {
+        this.seqId = seqId;
+        this.source = source;
+        this.type = type;
+        this.start = start;
+        this.end = end;
+        this.score = score;
+        this.strand = strand;
+        this.phase = phase;
+        this.attributes = new HashMap<String, String>();
+    }
+
+    public String getSeqId() {
+        return seqId;
+    }
+
+    public void setSeqId(String seqId) {
+        this.seqId = seqId;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public int getStart() {
+        return start;
+    }
+
+    public void setStart(int start) {
+        this.start = start;
+    }
+
+    public int getEnd() {
+        return end;
+    }
+
+    public void setEnd(int end) {
+        this.end = end;
+    }
+
+    public String getScore() {
+        return score;
+    }
+
+    public void setScore(String score) {
+        this.score = score;
+    }
+
+    public String getStrand() {
+        return strand;
+    }
+
+    public void setStrand(String strand) {
+        this.strand = strand;
+    }
+
+    public String getPhase() {
+        return phase;
+    }
+
+    public void setPhase(String phase) {
+        this.phase = phase;
+    }
+
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    public void setAttributes(Map<String, String> attributes) {
+        this.attributes = attributes;
+    }
+
+    public void addAttribute(String key, String value) {
+        attributes.put(key, value);
+    }
+
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(String.format("%s\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t", getSeqId(), getSource(), getType(), getStart(), getEnd(), getScore(), getStrand(), getPhase()));
+        Iterator<Map.Entry<String, String>> iter = attributes.entrySet().iterator();
+        if (iter.hasNext()) {
+            Map.Entry<String, String> entry = iter.next();
+            buf.append(entry.getKey());
+            buf.append("=");
+            buf.append(entry.getValue());
+            while (iter.hasNext()) {
+                entry = iter.next();
+                buf.append(";");
+                buf.append(entry.getKey());
+                buf.append("=");
+                buf.append(entry.getValue());
+            }
+        }
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/LeftPaddingStrategy.groovy b/src/groovy/org/bbop/apollo/LeftPaddingStrategy.groovy
new file mode 100644
index 0000000..4eb0af4
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/LeftPaddingStrategy.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+/**
+ * Created by ndunn on 5/9/15.
+ */
+class LeftPaddingStrategy implements PaddingStrategy{
+
+    String paddingText = "0"
+    Integer defaultPaddingCount = 5
+    Integer offset = 1
+
+    String pad(Integer count){
+        return (count+offset).toString().padLeft(defaultPaddingCount,paddingText)
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/LetterPaddingStrategy.groovy b/src/groovy/org/bbop/apollo/LetterPaddingStrategy.groovy
new file mode 100644
index 0000000..cfe29d3
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/LetterPaddingStrategy.groovy
@@ -0,0 +1,18 @@
+package org.bbop.apollo
+/**
+ * Created by ndunn on 5/9/15.
+ */
+class LetterPaddingStrategy implements PaddingStrategy {
+
+
+    String pad(Integer count) {
+
+        char startLetter = 'a';
+
+        for(int i = 0 ; i < count ; i++){
+            ++startLetter
+        }
+
+        return startLetter
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/Ontological.groovy b/src/groovy/org/bbop/apollo/Ontological.groovy
new file mode 100644
index 0000000..79aec60
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/Ontological.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo
+/**
+ * Created by NathanDunn on 10/21/14.
+ */
+interface Ontological {
+
+    static String ontologyId = null // XX:NNNNNNN
+    static String cvTerm = null // may have a link
+
+
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/PaddingStrategy.groovy b/src/groovy/org/bbop/apollo/PaddingStrategy.groovy
new file mode 100644
index 0000000..42d77ae
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/PaddingStrategy.groovy
@@ -0,0 +1,9 @@
+package org.bbop.apollo
+
+/**
+ * Created by ndunn on 5/9/15.
+ */
+interface PaddingStrategy {
+
+    String pad(Integer count4)
+}
diff --git a/src/groovy/org/bbop/apollo/PhoneHomeEnum.groovy b/src/groovy/org/bbop/apollo/PhoneHomeEnum.groovy
new file mode 100644
index 0000000..795d4af
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/PhoneHomeEnum.groovy
@@ -0,0 +1,31 @@
+package org.bbop.apollo
+
+/**
+ * Created by nathandunn on 11/17/16.
+ */
+enum PhoneHomeEnum {
+
+    SERVER,
+    ENVIRONMENT,
+    MESSAGE,
+    START,
+    STOP,
+    RUNNING,
+    NUM_USERS,
+    NUM_ANNOTATIONS,
+    NUM_ORGANISMS,
+
+    private String value;
+
+    PhoneHomeEnum(String value){this.value = value }
+    PhoneHomeEnum(){this.value = name().toLowerCase() }
+
+    String getValue() {
+        return value
+    }
+
+    @Override
+    String toString() {
+        return value
+    }
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/TermTypeEnum.groovy b/src/groovy/org/bbop/apollo/TermTypeEnum.groovy
new file mode 100644
index 0000000..cfd79db
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/TermTypeEnum.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo
+/**
+ * Created by NathanDunn on 9/16/14.
+ */
+enum TermTypeEnum {
+
+    FEATURE_MAPPING,
+    FEATURE_PROPERTY_MAPPING,
+    ATTRIBUTE_MAPPING,
+    RELATIONSHIP_MAPPING
+
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/alteration/SequenceAlterationInContext.groovy b/src/groovy/org/bbop/apollo/alteration/SequenceAlterationInContext.groovy
new file mode 100644
index 0000000..46fcdd4
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/alteration/SequenceAlterationInContext.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo.alteration
+
+/**
+ * Created by Deepak on 7/7/15.
+ */
+class SequenceAlterationInContext {
+    int fmin
+    int fmax
+    int strand
+    String instanceOf
+    String type
+    String name
+    String originalAlterationUniqueName
+    int offset
+    String alterationResidue
+}
diff --git a/src/groovy/org/bbop/apollo/event/AnnotationEvent.groovy b/src/groovy/org/bbop/apollo/event/AnnotationEvent.groovy
new file mode 100644
index 0000000..a714204
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/event/AnnotationEvent.groovy
@@ -0,0 +1,33 @@
+package org.bbop.apollo.event
+
+import grails.validation.Validateable
+import org.codehaus.groovy.grails.web.json.JSONObject
+import org.bbop.apollo.Sequence
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+ at Validateable
+class AnnotationEvent {
+
+    JSONObject features
+    Sequence sequence
+    Operation operation
+    boolean sequenceAlterationEvent
+    String username
+    // toplevel feature?
+
+//    public AnnotationEvent(Object features,Sequence sequence,Operation operation){
+////        super(features)
+//        this.sequence = sequence
+//        this.operation = operation
+//    }
+
+    public enum Operation {
+        ADD,
+        DELETE,
+        UPDATE,
+        ERROR
+    }
+
+}
diff --git a/src/groovy/org/bbop/apollo/event/AnnotationEventListener.groovy b/src/groovy/org/bbop/apollo/event/AnnotationEventListener.groovy
new file mode 100644
index 0000000..45cca3f
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/event/AnnotationEventListener.groovy
@@ -0,0 +1,9 @@
+package org.bbop.apollo.event
+/**
+ * Created by ndunn on 11/7/14.
+ */
+interface AnnotationEventListener {
+
+    def handleEvent(AnnotationEvent annotationEvent)
+
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/event/AnnotationListener.groovy b/src/groovy/org/bbop/apollo/event/AnnotationListener.groovy
new file mode 100644
index 0000000..24e2c6a
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/event/AnnotationListener.groovy
@@ -0,0 +1,9 @@
+package org.bbop.apollo.event
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+interface AnnotationListener extends EventListener{
+
+    public void handleChangeEvent(AnnotationEvent... event);
+}
diff --git a/src/groovy/org/bbop/apollo/filter/Cds3Filter.groovy b/src/groovy/org/bbop/apollo/filter/Cds3Filter.groovy
new file mode 100644
index 0000000..87f0470
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/filter/Cds3Filter.groovy
@@ -0,0 +1,28 @@
+package org.bbop.apollo.filter
+
+import org.bbop.apollo.CDS
+import org.bbop.apollo.Feature
+
+/**
+ * Created by ndunn on 2/3/15.
+ * @E is E-type
+ * @T is T-type
+ *
+ * If any CDS is not exactly divisible 3, put error on feature
+ */
+class Cds3Filter implements FeatureFilter<List<String>, Feature> {
+
+    // TODO: need a unit test for this
+    List<String> filterFeature(Feature feature) {
+        List<String> errorList = new ArrayList<>()
+        if (feature.ontologyId == CDS.ontologyId) {
+//            if(feature.getLength()%3!=0){
+            // TODO: do an actual search at some point
+            if (false) {
+                errorList.add("CDS/3")
+            }
+        }
+        return errorList
+    }
+//    List<E> filterFeatures(List<T> objects)
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/filter/FeatureFilter.groovy b/src/groovy/org/bbop/apollo/filter/FeatureFilter.groovy
new file mode 100644
index 0000000..40be6eb
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/filter/FeatureFilter.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo.filter
+
+/**
+ * Created by ndunn on 2/3/15.
+ * @E is E-type
+ * @T is T-type
+ */
+interface FeatureFilter<E,T> {
+
+
+    E filterFeature(T object)
+//    List<E> filterFeatures(List<T> objects)
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/filter/StopCodonFilter.groovy b/src/groovy/org/bbop/apollo/filter/StopCodonFilter.groovy
new file mode 100644
index 0000000..05af7fe
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/filter/StopCodonFilter.groovy
@@ -0,0 +1,27 @@
+package org.bbop.apollo.filter
+
+import org.bbop.apollo.Feature
+
+/**
+ * Created by ndunn on 2/3/15.
+ * @E is E-type
+ * @T is T-type
+ */
+class StopCodonFilter implements FeatureFilter<List<String>, Feature> {
+
+    Random random = new Random()
+
+    List<String> filterFeature(Feature feature) {
+
+        List<String> errorList = new ArrayList<>()
+
+//        if(random.nextFloat()<0.2){
+        // TODO: do an actual search at some point
+        if (false) {
+            errorList.add("Stop Codon")
+        }
+
+        return errorList
+    }
+//    List<E> filterFeatures(List<T> objects)
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/history/FeatureEventView.groovy b/src/groovy/org/bbop/apollo/history/FeatureEventView.groovy
new file mode 100644
index 0000000..99ebbd7
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/history/FeatureEventView.groovy
@@ -0,0 +1,16 @@
+package org.bbop.apollo.history
+
+import org.bbop.apollo.Feature
+import org.bbop.apollo.FeatureEvent
+import org.bbop.apollo.Organism
+
+/**
+ * Created by nathandunn on 7/21/15.
+ */
+class FeatureEventView {
+
+    FeatureEvent featureEvent
+    Feature feature
+    Long organismId
+    String locString
+}
diff --git a/src/groovy/org/bbop/apollo/history/FeatureOperation.groovy b/src/groovy/org/bbop/apollo/history/FeatureOperation.groovy
new file mode 100644
index 0000000..58eed11
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/history/FeatureOperation.groovy
@@ -0,0 +1,34 @@
+package org.bbop.apollo.history
+
+/**
+ * Created by ndunn on 4/7/15.
+ */
+enum FeatureOperation {
+
+    ADD_FEATURE,
+    DELETE_FEATURE,
+    ADD_TRANSCRIPT,
+    DELETE_TRANSCRIPT,
+    ADD_EXON,
+    DELETE_EXON,
+    MERGE_EXONS,
+    SPLIT_EXON,
+    SET_EXON_BOUNDARIES,
+    MERGE_TRANSCRIPTS,
+    SPLIT_TRANSCRIPT,
+    SET_TRANSLATION_START,
+    UNSET_TRANSLATION_START,
+    SET_TRANSLATION_END,
+    UNSET_TRANSLATION_END,
+    SET_TRANSLATION_ENDS,
+    SET_LONGEST_ORF,
+    FLIP_STRAND,
+    SET_READTHROUGH_STOP_CODON,
+    UNSET_READTHROUGH_STOP_CODON,
+    SET_BOUNDARIES,
+    CHANGE_ANNOTATION_TYPE
+
+    public String toLower(){
+        return name().toLowerCase()
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/operation/OperationEnum.groovy b/src/groovy/org/bbop/apollo/operation/OperationEnum.groovy
new file mode 100644
index 0000000..d8c0c6b
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/operation/OperationEnum.groovy
@@ -0,0 +1,43 @@
+package org.bbop.apollo.operation
+
+/**
+ * Created by ndunn on 3/13/15.
+ */
+enum OperationEnum {
+    ADD_FEATURE(DELETE_FEATURE),
+    DELETE_FEATURE(ADD_FEATURE),
+    ADD_TRANSCRIPT(DELETE_TRANSCRIPT),
+    DELETE_TRANSCRIPT(ADD_TRANSCRIPT),
+    ADD_EXON(DELETE_EXON),
+    DELETE_EXON(ADD_EXON),
+    MERGE_EXONS(SPLIT_EXON),
+    SPLIT_EXON(MERGE_EXONS),
+    SET_EXON_BOUNDARIES,
+    MERGE_TRANSCRIPTS(SPLIT_TRANSCRIPT),
+    SPLIT_TRANSCRIPT(MERGE_TRANSCRIPTS),
+    SET_TRANSLATION_START(UNSET_TRANSLATION_START),
+    UNSET_TRANSLATION_START(SET_TRANSLATION_START),
+    SET_TRANSLATION_END(UNSET_TRANSLATION_END),
+    UNSET_TRANSLATION_END(SET_TRANSLATION_END),
+    SET_TRANSLATION_ENDS(UNSET_TRANSLATION_END),
+    SET_LONGEST_ORF,
+    FLIP_STRAND,
+    SET_READTHROUGH_STOP_CODON(UNSET_READTHROUGH_STOP_CODON),
+    UNSET_READTHROUGH_STOP_CODON(SET_READTHROUGH_STOP_CODON),
+    SET_BOUNDARIES;
+    
+   
+    private OperationEnum reverseValue
+
+    public OperationEnum(){
+        this.reverseValue = this
+    }
+    
+    public OperationEnum(OperationEnum reverseOperation){
+        this.reverseValue = reverseOperation
+    }
+
+    OperationEnum getReverseValue() {
+        return reverseValue
+    }
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/preference/OrganismDTO.groovy b/src/groovy/org/bbop/apollo/preference/OrganismDTO.groovy
new file mode 100644
index 0000000..4fd1664
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/preference/OrganismDTO.groovy
@@ -0,0 +1,8 @@
+package org.bbop.apollo.preference
+
+class OrganismDTO {
+
+    Long id
+    String commonName
+    String directory
+}
diff --git a/src/groovy/org/bbop/apollo/preference/SequenceDTO.groovy b/src/groovy/org/bbop/apollo/preference/SequenceDTO.groovy
new file mode 100644
index 0000000..2c9a60a
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/preference/SequenceDTO.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo.preference
+
+class SequenceDTO {
+
+    Long id
+    String name
+    Long start
+    Long end
+    Long length
+    OrganismDTO organism
+
+}
diff --git a/src/groovy/org/bbop/apollo/preference/UserDTO.groovy b/src/groovy/org/bbop/apollo/preference/UserDTO.groovy
new file mode 100644
index 0000000..7e096ea
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/preference/UserDTO.groovy
@@ -0,0 +1,7 @@
+package org.bbop.apollo.preference
+
+class UserDTO {
+
+    Long id
+    String username
+}
diff --git a/src/groovy/org/bbop/apollo/preference/UserOrganismPreferenceDTO.groovy b/src/groovy/org/bbop/apollo/preference/UserOrganismPreferenceDTO.groovy
new file mode 100644
index 0000000..8935afb
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/preference/UserOrganismPreferenceDTO.groovy
@@ -0,0 +1,43 @@
+package org.bbop.apollo.preference
+
+
+/**
+ * This class mirrors UserOrganismPreference, but NEVER persists, making it lighter-weight
+ */
+class UserOrganismPreferenceDTO {
+
+    Long id
+    OrganismDTO organism
+    Boolean currentOrganism  // this means the "active" client token
+    Boolean nativeTrackList
+    SequenceDTO sequence
+    UserDTO user
+    Integer startbp
+    Integer endbp
+    String clientToken
+
+
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (!(o instanceof UserOrganismPreferenceDTO)) return false
+
+        UserOrganismPreferenceDTO that = (UserOrganismPreferenceDTO) o
+
+//        if (id != that.id) return false
+//        if (organism != that.organism) return false
+//        if (sequence != that.sequence) return false
+//        if (user != that.user) return false
+        if (clientToken != that.clientToken) return false
+
+        return true
+    }
+
+    int hashCode() {
+        int result = clientToken.hashCode()
+//        result = organism.hashCode()
+//        result = 31 * result + sequence.hashCode()
+//        result = 31 * result + id.hashCode()
+//        result = 31 * result + user.hashCode()
+        return result
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/AbstractProjection.groovy b/src/groovy/org/bbop/apollo/projection/AbstractProjection.groovy
new file mode 100644
index 0000000..a4e90fe
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/AbstractProjection.groovy
@@ -0,0 +1,21 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by ndunn on 8/24/15.
+ */
+abstract class AbstractProjection implements Projection{
+
+    public final static Integer UNMAPPED_VALUE = -1
+
+    @Override
+    Track projectTrack(Track trackIn) {
+        Track trackOut = new Track(length: trackIn.length)
+        for(Coordinate coordinate in trackIn.coordinateList.sort()){
+            Coordinate returnCoordinate = new Coordinate()
+            returnCoordinate.min = projectValue(coordinate.min)
+            returnCoordinate.max = projectValue(coordinate.max)
+            trackOut.coordinateList.add(returnCoordinate)
+        }
+        return trackOut
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/Coordinate.groovy b/src/groovy/org/bbop/apollo/projection/Coordinate.groovy
new file mode 100644
index 0000000..f5dfd3b
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/Coordinate.groovy
@@ -0,0 +1,35 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by ndunn on 8/24/15.
+ */
+class Coordinate implements Comparable<Coordinate>{
+
+    Integer min
+    Integer max
+
+
+    @Override
+    int compareTo(Coordinate o) {
+        min <=> o.min ?: max <=> o.max
+    }
+
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (!(o instanceof Coordinate)) return false
+
+        Coordinate that = (Coordinate) o
+
+        if (max != that.max) return false
+        if (min != that.min) return false
+
+        return true
+    }
+
+    int hashCode() {
+        int result
+        result = (min != null ? min.hashCode() : 0)
+        result = 31 * result + (max != null ? max.hashCode() : 0)
+        return result
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/DiscontinuousProjection.groovy b/src/groovy/org/bbop/apollo/projection/DiscontinuousProjection.groovy
new file mode 100644
index 0000000..1756114
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/DiscontinuousProjection.groovy
@@ -0,0 +1,121 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by ndunn on 8/24/15.
+ */
+class DiscontinuousProjection extends AbstractProjection{
+
+    // projection from X -> X'
+    TreeMap<Integer,Coordinate> minMap = new TreeMap<>()
+    TreeMap<Integer,Coordinate> maxMap = new TreeMap<>()
+
+    /**
+     * Get the coordinate value out and add some to min
+     *
+     * To do this efficiently we simply get the interval sizes
+     * and stop when we've hit all of the sizes
+     *
+     * @param input
+     * @return
+     */
+    @Override
+    Integer reverseProjectValue(Integer input){
+
+        Iterator<Integer> minIterator = minMap.keySet().iterator()
+        Iterator<Integer> maxIterator = maxMap.keySet().iterator()
+        Integer min, max
+
+        // TODO: for speed generate a reverse map for quick lookup whilst doing this or another operation
+        // here we can assume that the input maps onto the current length
+        Integer currentLength = 0
+        Integer bucketCount = 0
+        Integer previousLength = 0
+        while(minIterator.hasNext()){
+            min = minIterator.next()
+            max = maxIterator.next()
+            currentLength += (max - min)
+            if(currentLength+bucketCount>=input){
+                return min + input - previousLength - bucketCount
+            }
+            previousLength += (max - min)
+            ++bucketCount
+        }
+        return UNMAPPED_VALUE
+    }
+
+    @Override
+    Integer projectValue(Integer input) {
+        if(!minMap && !maxMap){
+            return input
+        }
+
+        Integer floorMinKey = minMap.floorKey(input)
+        Integer ceilMinKey = minMap.ceilingKey(input)
+
+        Integer floorMaxKey = maxMap.floorKey(input)
+        Integer ceilMaxKey = maxMap.ceilingKey(input)
+
+//        log.debug "input ${input} minKey ${floorMinKey}-${ceilMinKey}"
+//        log.debug "input ${input} maxKey ${floorMaxKey}-${ceilMaxKey}"
+
+        if(floorMinKey==null || ceilMaxKey==null){
+            return UNMAPPED_VALUE
+        }
+
+        // if is a hit for min and no max hit, then it is the left-most
+        if(floorMinKey==ceilMinKey){
+            if(floorMaxKey==null){
+                return 0
+            }
+            else{
+//                return input - floorMaxKey
+                return projectValue(floorMaxKey)+1
+            }
+        }
+
+
+
+        // this is the left-most still
+        if(floorMinKey!=ceilMinKey && floorMaxKey==null){
+            return input - floorMinKey
+        }
+
+        // if we are at the max border
+        if(floorMaxKey==ceilMaxKey){
+            return input - floorMinKey + projectValue(floorMinKey)
+        }
+
+        // if we are inbetween a ceiling max and floor min, then we are in a viable block
+        if(input > floorMinKey && input < ceilMaxKey &&  ceilMinKey >= ceilMaxKey){
+            return input - floorMinKey + projectValue(floorMinKey)
+        }
+
+//        log.debug "${input} unable to find match, returning UNMAPPED"
+        return UNMAPPED_VALUE
+    }
+
+    def addInterval(int min, int max) {
+        assert max>=min
+        Coordinate coordinate = new Coordinate(min:min,max:max)
+        minMap.put(min,coordinate)
+        maxMap.put(max,coordinate)
+    }
+
+    @Override
+    Track projectTrack(Track trackIn) {
+        Track trackOut = new Track()
+        Integer trackLength = 0
+
+        for(Coordinate coordinate in trackIn.coordinateList.sort()){
+            Coordinate returnCoordinate = new Coordinate()
+            returnCoordinate.min = projectValue(coordinate.min)
+            returnCoordinate.max = projectValue(coordinate.max)
+            trackOut.coordinateList.add(returnCoordinate)
+            trackLength = returnCoordinate.max
+        }
+        trackOut.length = trackLength+1
+        return trackOut
+    }
+
+
+}
diff --git a/src/groovy/org/bbop/apollo/projection/DiscontinuousProjectionFactory.groovy b/src/groovy/org/bbop/apollo/projection/DiscontinuousProjectionFactory.groovy
new file mode 100644
index 0000000..fed9bf7
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/DiscontinuousProjectionFactory.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by nathandunn on 8/14/15.
+ */
+ at Singleton
+class DiscontinuousProjectionFactory {
+
+    /**
+     * For each track,
+     * @param inputTrack
+     * @param padding
+     * @return
+     */
+    Projection createProjection(Track inputTrack,Integer padding=0){
+       DiscontinuousProjection projection= new DiscontinuousProjection()
+
+        inputTrack.coordinateList.each {
+//            projection.addInterval(Math.max(it.min-padding,0),Math.min(it.max+padding,inputTrack.length))
+            projection.addInterval(it.min-padding,it.max+padding)
+        }
+
+        return projection
+    }
+
+
+//    Track projectToTrack(Track track,List<Projection> projections) {
+//        Track returnTrack = track
+//
+//        for(Projection projection : projections){
+//            returnTrack = projection.project(returnTrack)
+//        }
+//
+//        return returnTrack
+//    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/DuplicateTrackProjection.groovy b/src/groovy/org/bbop/apollo/projection/DuplicateTrackProjection.groovy
new file mode 100644
index 0000000..b3531af
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/DuplicateTrackProjection.groovy
@@ -0,0 +1,17 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by ndunn on 8/24/15.
+ */
+class DuplicateTrackProjection extends AbstractProjection{
+
+    @Override
+    Integer projectValue(Integer input) {
+        return input
+    }
+
+    @Override
+    Integer reverseProjectValue(Integer input) {
+        return input
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/Projection.groovy b/src/groovy/org/bbop/apollo/projection/Projection.groovy
new file mode 100644
index 0000000..bf64efc
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/Projection.groovy
@@ -0,0 +1,27 @@
+package org.bbop.apollo.projection
+
+
+/**
+ * Created by nathandunn on 8/14/15.
+ */
+interface Projection {
+
+
+    /**
+     *
+     * Probably just works on FeatureLocation
+     *
+     * @param input
+     * @return
+     */
+    Integer projectValue(Integer input)
+
+    Integer reverseProjectValue(Integer input)
+
+    /**
+     * @param trackIn
+     * @return
+     */
+    Track projectTrack(Track trackIn)
+
+}
diff --git a/src/groovy/org/bbop/apollo/projection/RefSeqProjector.groovy b/src/groovy/org/bbop/apollo/projection/RefSeqProjector.groovy
new file mode 100644
index 0000000..1d757d9
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/RefSeqProjector.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by nathandunn on 8/11/15.
+ */
+class RefSeqProjector implements TrackProjector{
+
+
+    ReferenceTrack referenceTrack
+
+    @Override
+    Track projectTrack(Track trackA, Track trackB, Projection projection) {
+        return null
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/ReferenceTrack.groovy b/src/groovy/org/bbop/apollo/projection/ReferenceTrack.groovy
new file mode 100644
index 0000000..cb84cb6
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/ReferenceTrack.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo.projection
+
+/**
+ *
+ * The goal of this track is to grab the designated
+ * track to be folded around.
+ *
+ * Created by nathandunn on 8/11/15.
+ */
+class ReferenceTrack {
+}
diff --git a/src/groovy/org/bbop/apollo/projection/ReverseProjection.groovy b/src/groovy/org/bbop/apollo/projection/ReverseProjection.groovy
new file mode 100644
index 0000000..7ec1597
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/ReverseProjection.groovy
@@ -0,0 +1,29 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by ndunn on 8/24/15.
+ */
+public class ReverseProjection extends AbstractProjection{
+
+
+    Integer trackLength
+
+    public ReverseProjection(Track inputTrack){
+        trackLength = inputTrack.length
+    }
+
+    @Override
+    Integer reverseProjectValue(Integer input) {
+        return input
+    }
+
+    @Override
+    Integer projectValue(Integer input) {
+        if(input < trackLength && input >= 0 ){
+            return trackLength - input - 1
+        }
+
+        return -1
+    }
+
+}
diff --git a/src/groovy/org/bbop/apollo/projection/Track.groovy b/src/groovy/org/bbop/apollo/projection/Track.groovy
new file mode 100644
index 0000000..43be985
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/Track.groovy
@@ -0,0 +1,44 @@
+package org.bbop.apollo.projection
+
+
+/**
+ * Created by nathandunn on 8/14/15.
+ *
+ * This is essentially the same thing as a scaffold.
+ */
+class Track {
+
+    int length
+    String name
+
+    List<Coordinate> coordinateList = new ArrayList<>()
+
+    def addCoordinate(int min, int max) {
+        coordinateList.add(new Coordinate(min: min, max: max))
+    }
+
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (!(o instanceof Track)) return false
+
+        Track track = (Track) o
+
+        if(track.length!=length) {
+            println "different trac lengths ${track.length} vs ${length}"
+            return false
+        }
+
+        // coordinates must be in the same order as well
+        for(int i = 0 ; i < coordinateList.size() ;i++){
+            Coordinate coordinateA = coordinateList.get(i)
+            Coordinate coordinateB = track.coordinateList.get(i)
+            if(!coordinateA.equals(coordinateB)) return false
+        }
+
+        return true
+    }
+
+    int hashCode() {
+        return (coordinateList != null ? coordinateList.hashCode() : 0)
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/projection/TrackProjector.groovy b/src/groovy/org/bbop/apollo/projection/TrackProjector.groovy
new file mode 100644
index 0000000..a39da12
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/projection/TrackProjector.groovy
@@ -0,0 +1,9 @@
+package org.bbop.apollo.projection
+
+/**
+ * Created by nathandunn on 8/11/15.
+ */
+interface TrackProjector {
+
+    Track projectTrack(Track trackA,Track trackB,Projection projection)
+}
diff --git a/src/groovy/org/bbop/apollo/report/AnnotatorSummary.groovy b/src/groovy/org/bbop/apollo/report/AnnotatorSummary.groovy
new file mode 100644
index 0000000..0a6a78e
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/report/AnnotatorSummary.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo.report
+
+import org.bbop.apollo.User
+
+/**
+ * Created by nathandunn on 7/19/15.
+ */
+class AnnotatorSummary extends OrganismSummary{
+    User annotator
+    List<OrganismPermissionSummary> userOrganismPermissionList
+
+    String getUsername(){annotator.username}
+    String getFirstName(){annotator.firstName}
+    String getLastName(){annotator.lastName}
+}
diff --git a/src/groovy/org/bbop/apollo/report/OrganismPermissionSummary.groovy b/src/groovy/org/bbop/apollo/report/OrganismPermissionSummary.groovy
new file mode 100644
index 0000000..290f3ca
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/report/OrganismPermissionSummary.groovy
@@ -0,0 +1,12 @@
+package org.bbop.apollo.report
+
+import org.bbop.apollo.User
+import org.bbop.apollo.UserOrganismPermission
+
+/**
+ * Created by nathandunn on 7/19/15.
+ */
+class OrganismPermissionSummary extends OrganismSummary{
+    UserOrganismPermission userOrganismPermission
+
+}
diff --git a/src/groovy/org/bbop/apollo/report/OrganismSummary.groovy b/src/groovy/org/bbop/apollo/report/OrganismSummary.groovy
new file mode 100644
index 0000000..b8a1e01
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/report/OrganismSummary.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo.report
+
+import org.bbop.apollo.User
+
+/**
+ * Created by nathandunn on 7/17/15.
+ */
+class OrganismSummary {
+    int geneCount
+    int transcriptCount
+    int transposableElementCount
+    int repeatRegionCount
+    int exonCount
+    int sequenceCount
+    long organismId
+    List<User> annotators
+
+    Map<String, Integer> transcriptTypeCount
+    Map<String, Integer> geneTypeCount
+
+    int getTotalFeatureCount() {
+        transcriptCount + repeatRegionCount + transposableElementCount
+    }
+
+    float getProteinCodingFeaturePercent() {
+        totalFeatureCount && transcriptTypeCount.containsKey("MRNA") ? ((float) transcriptTypeCount.get("MRNA") / (float) totalFeatureCount).round(2) : 0
+    }
+
+    float getProteinCodingTranscriptPercent() {
+        transcriptCount && transcriptTypeCount.containsKey("MRNA")? ((float) transcriptTypeCount.get("MRNA") / (float) transcriptCount).round(2) : 0
+    }
+
+    float getExonsPerTranscript() {
+        ((float) exonCount / (float) transcriptCount).round(2)
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/report/PerformanceMetric.groovy b/src/groovy/org/bbop/apollo/report/PerformanceMetric.groovy
new file mode 100644
index 0000000..adb2803
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/report/PerformanceMetric.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo.report
+
+/**
+ * Created by nathandunn on 7/21/15.
+ */
+class PerformanceMetric{
+    String className
+    String methodName
+    Integer count
+    Float min, max, mean, stddev
+    Float total
+    Float totalPercent
+}
diff --git a/src/groovy/org/bbop/apollo/report/SequenceSummary.groovy b/src/groovy/org/bbop/apollo/report/SequenceSummary.groovy
new file mode 100644
index 0000000..04f7e51
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/report/SequenceSummary.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo.report
+
+import org.bbop.apollo.Sequence
+
+/**
+ * Created by nathandunn on 7/17/15.
+ */
+class SequenceSummary extends OrganismSummary{
+
+    Sequence sequence
+    String getName(){sequence.name}
+    Long getId(){sequence.id}
+    Integer getLength(){sequence.length}
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/DownloadFile.groovy b/src/groovy/org/bbop/apollo/sequence/DownloadFile.groovy
new file mode 100644
index 0000000..3c8451b
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/DownloadFile.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by nathandunn on 7/7/15.
+ */
+class DownloadFile {
+    String uuid
+    String path
+    String fileName
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/Overlapper.groovy b/src/groovy/org/bbop/apollo/sequence/Overlapper.groovy
new file mode 100644
index 0000000..a5c2143
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/Overlapper.groovy
@@ -0,0 +1,13 @@
+package org.bbop.apollo.sequence
+
+import org.bbop.apollo.Gene
+import org.bbop.apollo.Transcript
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+interface Overlapper {
+    public boolean overlaps(Transcript transcript, Gene gene);
+
+    public boolean overlaps(Transcript transcript1, Transcript transcript2);
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/Range.groovy b/src/groovy/org/bbop/apollo/sequence/Range.groovy
new file mode 100644
index 0000000..eb20c41
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/Range.groovy
@@ -0,0 +1,25 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by ndunn on 4/6/15.
+ */
+class Range {
+    long start;
+    long end;
+    long length;
+    long total;
+
+    /**
+     * Construct a byte range.
+     *
+     * @param start Start of the byte range.
+     * @param end   End of the byte range.
+     * @param total Total length of the byte source.
+     */
+    public Range(long start, long end, long total) {
+        this.start = start;
+        this.end = end;
+        this.length = end - start + 1;
+        this.total = total;
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/SequenceDTO.groovy b/src/groovy/org/bbop/apollo/sequence/SequenceDTO.groovy
new file mode 100644
index 0000000..8275338
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/SequenceDTO.groovy
@@ -0,0 +1,11 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by nathandunn on 5/5/17.
+ */
+class SequenceDTO {
+
+    String organismCommonName
+    String trackName
+    String sequenceName
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/SequenceLocationDTO.groovy b/src/groovy/org/bbop/apollo/sequence/SequenceLocationDTO.groovy
new file mode 100644
index 0000000..69b01a7
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/SequenceLocationDTO.groovy
@@ -0,0 +1,27 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by nathandunn on 5/17/17.
+ */
+class SequenceLocationDTO {
+    String sequenceName
+    Integer startBp
+    Integer endBp
+    String clientToken
+
+    boolean equals(o) {
+        if (this.is(o)) return true
+        if (getClass() != o.class) return false
+
+        SequenceLocationDTO that = (SequenceLocationDTO) o
+
+        if (clientToken != that.clientToken) return false
+
+        return true
+    }
+
+    int hashCode() {
+        int result = clientToken.hashCode()
+        return result
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/SequenceTranslationHandler.groovy b/src/groovy/org/bbop/apollo/sequence/SequenceTranslationHandler.groovy
new file mode 100644
index 0000000..acc1759
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/SequenceTranslationHandler.groovy
@@ -0,0 +1,230 @@
+package org.bbop.apollo.sequence
+
+import org.apache.commons.io.FileUtils
+import org.apache.commons.io.filefilter.DirectoryFileFilter
+import org.apache.commons.io.filefilter.NameFileFilter
+import org.apache.commons.io.filefilter.TrueFileFilter
+import org.bbop.apollo.AnnotationException
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+class SequenceTranslationHandler {
+
+    private static Map<String, TranslationTable> translationTables = new HashMap<>();
+    private static Set<String> spliceAcceptorSites = new HashSet<String>();
+    private static Set<String> spliceDonorSites = new HashSet<String>();
+
+    public final static String DEFAULT_TRANSLATION_TABLE = "1"
+
+    /** Reverse complement a nucleotide sequence.
+     *
+     * @param sequence - String for the nucleotide sequence to be reverse complemented
+     * @return Reverse complemented nucleotide sequence
+     */
+    public static String reverseComplementSequence(String sequence) {
+        StringBuilder buffer = new StringBuilder(sequence);
+        buffer.reverse();
+        for (int i = 0; i < buffer.length(); ++i) {
+            switch (buffer.charAt(i)) {
+                case 'A':
+                    buffer.setCharAt(i, 'T' as char);
+                    break;
+                case 'C':
+                    buffer.setCharAt(i, 'G' as char);
+                    break;
+                case 'G':
+                    buffer.setCharAt(i, 'C' as char);
+                    break;
+                case 'T':
+                    buffer.setCharAt(i, 'A' as char);
+                    break;
+            }
+        }
+        return buffer.toString();
+    }
+
+    /** Translate a nucleotide sequence into an amino acid sequence using the translation table.  The returned
+     *  translated sequence will not include the stop amino acid and will stop at the first stop codon it
+     *  finds as determined by the translation table.
+     *
+     * @param sequence - Nucleotide sequence to be translated
+     * @param translationTable - TranslationTable that contains the codon translation table
+     * @return Translated amino acid sequence
+     */
+    public static String translateSequence(String sequence, TranslationTable translationTable) {
+        return translateSequence(sequence, translationTable, false, false);
+    }
+
+    /**  Translate a nucleotide sequence into an amino acid sequence using the translation table.
+     *
+     * @param sequence - Nucleotide sequence to be translated
+     * @param translationTable - TranslationTable that contains the codon translation table
+     * @param includeStop - Whether to include the stop amino acid in the translated sequence
+     * @param translateThroughStop - Whether to continue translation through stop codons
+     * @return Translated amino acid sequence
+     */
+    public static String translateSequence(String sequence, TranslationTable translationTable,
+                                           boolean includeStop, boolean translateThroughStop) {
+//        if (sequence.length() % 3 != 0) {
+//            throw new AnnotationException("Sequence to be translated must have length of factor of 3");
+//        }
+        StringBuilder buffer = new StringBuilder();
+        int stopCodonCount = 0;
+        for (int i = 0; i + 3 <= sequence.length(); i += 3) {
+            String codon = sequence.substring(i, i + 3);
+            String aminoAcid = translationTable.translateCodon(codon);
+            if(i==0 && translationTable.isStartCodon(codon)){
+                aminoAcid = "M"
+            }
+
+            if (aminoAcid.equals(TranslationTable.STOP)) {
+                if (includeStop) {
+                    buffer.append(aminoAcid);
+                }
+                if (!translateThroughStop) {
+                    break;
+                }
+                // TODO: not sure why this is written this way . . .clearly a bug
+                else {
+                    if (++stopCodonCount > 1) {
+                        break;
+                    }
+                }
+            } else {
+                buffer.append(aminoAcid);
+            }
+        }
+        return buffer.toString();
+    }
+
+    /** Get the translation table for a NCBI translation table code.
+     *
+     * @param code - NCBI translation table code
+     * @return TranslationTable for the NCBI translation table code
+     * @throws AnnotationException - If an invalid NCBI translation table code is used
+     */
+    public static TranslationTable getTranslationTableForGeneticCode(String code) throws AnnotationException {
+        if (!translationTables.containsKey(code)) {
+            initTranslationTables(code);
+        }
+        if (code < DEFAULT_TRANSLATION_TABLE || !translationTables.containsKey(code)) {
+            throw new AnnotationException("Invalid translation table code");
+        }
+        return translationTables.get(code);
+    }
+
+    /** Get the default translation table (NCBI translation table code 1).
+     *
+     * @return Default translation table
+     */
+    public static TranslationTable getDefaultTranslationTable() {
+        return getTranslationTableForGeneticCode(DEFAULT_TRANSLATION_TABLE)
+    }
+
+    private static void initTranslationTables(String code) {
+        if (code == DEFAULT_TRANSLATION_TABLE) {
+            translationTables.put(code.toString(), new StandardTranslationTable())
+        } else {
+            File parentFile = FileUtils.listFiles(new File("."),new NameFileFilter("ncbi_1_translation_table.txt"),TrueFileFilter.INSTANCE).first().parentFile
+            translationTables.put(code.toString(), readTable(new File(parentFile.absolutePath+"/ncbi_${code}_translation_table.txt")))
+        }
+    }
+
+/** Get the splice acceptor sites.
+ *
+ * @return Set of strings for splice acceptor sites
+ */
+    public static Set<String> getSpliceAcceptorSites() {
+        return spliceAcceptorSites;
+    }
+
+/** Add a splice acceptor site.
+ *
+ * @param spliceAcceptorSite - String for splice acceptor site
+ */
+    public static void addSpliceAcceptorSite(String spliceAcceptorSite) {
+        spliceAcceptorSites.add(spliceAcceptorSite);
+    }
+
+/** Remove a splice acceptor site.
+ *
+ * @param spliceAcceptorSite - String for splice acceptor site
+ */
+    public static void deleteSpliceAcceptorSite(String spliceAcceptorSite) {
+        spliceAcceptorSites.remove(spliceAcceptorSite);
+    }
+
+/** Get the splice donor sites.
+ *
+ * @return Set of string for splice donor sites
+ */
+    public static Set<String> getSpliceDonorSites() {
+        return spliceDonorSites;
+    }
+
+/** Add a splice donor site.
+ *
+ * @param spliceDonorSite - Strings for splice donor site
+ */
+    public static void addSpliceDonorSite(String spliceDonorSite) {
+        spliceDonorSites.add(spliceDonorSite);
+    }
+
+/** Remove a splice donor site.
+ *
+ * @param spliceDonorSite - String for splice donor site
+ */
+    public static void deleteSpliceDonorSite(String spliceDonorSite) {
+        spliceDonorSites.remove(spliceDonorSite);
+    }
+
+    /**
+     * Pocesses deltas from the standard (1) translation table:
+     * http://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi?mode=t
+     *
+     * code, codon <start|stop|none>
+     *
+     * if stop codon (*), add to stop codon table
+     *     if "none" then remove from alternate translation table, else add to alternatetranslation table
+     * else
+     *     if "none", remove codon from stop codon, and from alternate translation table
+     *
+     *
+     * if 3 is start, add to start codons, else remove from start codons
+     *
+     *
+     * @param file
+     * @return
+     */
+    public static TranslationTable readTable(File file) {
+        TranslationTable ttable = new StandardTranslationTable().cloneTable()
+        ttable.name = file.name
+//        BufferedReader reader = new BufferedReader(new InputStreamReader(getServletContext().getResourceAsStream(track.getTranslationTable())));
+        file.text.readLines().each { String line ->
+            String[] tokens = line.split("\t");
+            String codon = tokens[0].toUpperCase();
+            String aa = tokens[1].toUpperCase();
+            ttable.getTranslationTable().put(codon, aa);
+            if (aa.equals(TranslationTable.STOP)) {
+                ttable.getStopCodons().add(codon);
+                if (tokens.length == 3) {
+                    ttable.getAlternateTranslationTable().put(codon, tokens[2]);
+                } else {
+                    ttable.getAlternateTranslationTable().remove(codon);
+                }
+            } else {
+                ttable.getStopCodons().remove(codon);
+                ttable.getAlternateTranslationTable().remove(codon);
+            }
+            if (tokens.length == 3) {
+                if (tokens[2].equals("start")) {
+                    ttable.getStartCodons().add(codon);
+                }
+            } else {
+                ttable.getStartCodons().remove(codon);
+            }
+        }
+        return ttable
+    }
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/StandardTranslationTable.groovy b/src/groovy/org/bbop/apollo/sequence/StandardTranslationTable.groovy
new file mode 100644
index 0000000..0a4f977
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/StandardTranslationTable.groovy
@@ -0,0 +1,86 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+class StandardTranslationTable extends TranslationTable {
+
+    public StandardTranslationTable() {
+        translationTable.put("TTT", "F");
+        translationTable.put("TTC", "F");
+        translationTable.put("TTA", "L");
+        translationTable.put("TTG", "L");
+        translationTable.put("CTT", "L");
+        translationTable.put("CTC", "L");
+        translationTable.put("CTA", "L");
+        translationTable.put("CTG", "L");
+        translationTable.put("ATT", "I");
+        translationTable.put("ATC", "I");
+        translationTable.put("ATA", "I");
+        translationTable.put("ATG", "M");
+        translationTable.put("GTT", "V");
+        translationTable.put("GTC", "V");
+        translationTable.put("GTA", "V");
+        translationTable.put("GTG", "V");
+
+        translationTable.put("TCT", "S");
+        translationTable.put("TCC", "S");
+        translationTable.put("TCA", "S");
+        translationTable.put("TCG", "S");
+        translationTable.put("CCT", "P");
+        translationTable.put("CCC", "P");
+        translationTable.put("CCA", "P");
+        translationTable.put("CCG", "P");
+        translationTable.put("ACT", "T");
+        translationTable.put("ACC", "T");
+        translationTable.put("ACA", "T");
+        translationTable.put("ACG", "T");
+        translationTable.put("GCT", "A");
+        translationTable.put("GCC", "A");
+        translationTable.put("GCA", "A");
+        translationTable.put("GCG", "A");
+
+        translationTable.put("TAT", "Y");
+        translationTable.put("TAC", "Y");
+        translationTable.put("TAA", STOP);
+        translationTable.put("TAG", STOP);
+        translationTable.put("CAT", "H");
+        translationTable.put("CAC", "H");
+        translationTable.put("CAA", "Q");
+        translationTable.put("CAG", "Q");
+        translationTable.put("AAT", "N");
+        translationTable.put("AAC", "N");
+        translationTable.put("AAA", "K");
+        translationTable.put("AAG", "K");
+        translationTable.put("GAT", "D");
+        translationTable.put("GAC", "D");
+        translationTable.put("GAA", "E");
+        translationTable.put("GAG", "E");
+
+        translationTable.put("TGT", "C");
+        translationTable.put("TGC", "C");
+        translationTable.put("TGA", STOP);
+        translationTable.put("TGG", "W");
+        translationTable.put("CGT", "R");
+        translationTable.put("CGC", "R");
+        translationTable.put("CGA", "R");
+        translationTable.put("CGG", "R");
+        translationTable.put("AGT", "S");
+        translationTable.put("AGC", "S");
+        translationTable.put("AGA", "R");
+        translationTable.put("AGG", "R");
+        translationTable.put("GGT", "G");
+        translationTable.put("GGC", "G");
+        translationTable.put("GGA", "G");
+        translationTable.put("GGG", "G");
+
+        startCodons.add("ATG");
+
+        stopCodons.add("TAA");
+        stopCodons.add("TAG");
+        stopCodons.add("TGA");
+
+        alternateTranslationTable.put("TGA", "U");
+    }
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/Strand.groovy b/src/groovy/org/bbop/apollo/sequence/Strand.groovy
new file mode 100644
index 0000000..e844b18
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/Strand.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by ndunn on 2/19/15.
+ */
+enum Strand {
+
+    POSITIVE(1,"+"),
+    NEGATIVE(-1,"-"),
+    NONE(0,".")
+
+    Integer value
+    String display
+
+    public Strand(Integer value,String display) {
+        this.value = value
+        this.display = display
+    }
+
+    static Strand getStrandForValue(int i) {
+        for(strand in values()){
+            if(strand.value==i) {
+                return strand
+            }
+        }
+        return null
+    }
+
+    public getValue() {
+        return this.value
+    }
+
+    String getDisplay() {
+        return display
+    }
+}
\ No newline at end of file
diff --git a/src/groovy/org/bbop/apollo/sequence/TranslationTable.groovy b/src/groovy/org/bbop/apollo/sequence/TranslationTable.groovy
new file mode 100644
index 0000000..c58306b
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/TranslationTable.groovy
@@ -0,0 +1,93 @@
+package org.bbop.apollo.sequence
+
+/**
+ * Created by ndunn on 10/29/14.
+ */
+/** Abstract class that all specific translation tables must inherit from.
+ *
+ * @author elee
+ *
+ */
+class TranslationTable {
+
+    /** Amino acid for a stop codon.
+     *
+     */
+    public static final String STOP = "*";
+
+    String name
+    protected Map<String, String> translationTable;
+    protected Set<String> startCodons;
+    protected Set<String> stopCodons;
+    protected Map<String, String> alternateTranslationTable;
+
+    protected TranslationTable() {
+        translationTable = new HashMap<String, String>();
+        startCodons = new HashSet<String>();
+        stopCodons = new HashSet<String>();
+        alternateTranslationTable = new HashMap<String, String>();
+    }
+
+    public TranslationTable cloneTable() {
+        TranslationTable clone = null;
+        try {
+            clone = getClass().newInstance();
+            clone.translationTable = new HashMap<String, String>(translationTable);
+            clone.startCodons = new HashSet<String>(startCodons);
+            clone.stopCodons = new HashSet<String>(stopCodons);
+            clone.alternateTranslationTable = new HashMap<String, String>(alternateTranslationTable);
+        }
+        catch (InstantiationException e) {
+        }
+        catch (IllegalAccessException e) {
+        }
+        return clone;
+    }
+
+    /** Return the amino acid corresponding to the codon translation.  Returns "X" if the codon doesn't
+     *  exist in the translation table.
+     *
+     * @param codon - Codon to be translated
+     * @return Amino acid corresponding to the the codon
+     */
+    public String translateCodon(String codon) {
+        String aa = translationTable.get(codon);
+        if (aa == null) {
+            return "X";
+        }
+        return aa;
+    }
+
+    /** Return a collection of start codons for the translation table.
+     *
+     * @return Collection of Strings representing the start codons
+     */
+    public Collection<String> getStartCodons() {
+        return startCodons;
+    }
+
+    /** Return a collection of stop codons for the translation table.
+     *
+     * @return Collection of Strings representing the stop codons
+     */
+    public Collection<String> getStopCodons() {
+        return stopCodons;
+    }
+
+    protected boolean isStopCodon(String codon) {
+        return translateCodon(codon).equals(STOP);
+    }
+
+    public Map<String, String> getTranslationTable() {
+        return translationTable;
+    }
+
+    public Map<String, String> getAlternateTranslationTable() {
+        return alternateTranslationTable;
+    }
+
+    boolean isStartCodon(String s) {
+        return startCodons.contains(s)
+    }
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/Alignment.java b/src/groovy/org/bbop/apollo/sequence/search/Alignment.java
new file mode 100644
index 0000000..ae728a9
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/Alignment.java
@@ -0,0 +1,9 @@
+package org.bbop.apollo.sequence.search;
+
+import org.bbop.apollo.Match;
+
+public interface Alignment {
+
+    public Match convertToMatch();
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/AlignmentParsingException.java b/src/groovy/org/bbop/apollo/sequence/search/AlignmentParsingException.java
new file mode 100644
index 0000000..aa6d4f6
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/AlignmentParsingException.java
@@ -0,0 +1,11 @@
+package org.bbop.apollo.sequence.search;
+
+public class AlignmentParsingException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+    
+    public AlignmentParsingException(String error) {
+        super(error);
+    }
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchTool.java b/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchTool.java
new file mode 100644
index 0000000..012b183
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchTool.java
@@ -0,0 +1,22 @@
+package org.bbop.apollo.sequence.search;
+
+
+import java.util.Collection;
+import org.bbop.apollo.sequence.search.blast.BlastAlignment;
+import org.codehaus.groovy.grails.web.json.JSONObject;
+
+import org.bbop.apollo.Match;
+
+public abstract class SequenceSearchTool {
+
+    public abstract void parseConfiguration(JSONObject config) throws SequenceSearchToolException;
+
+    public abstract Collection<BlastAlignment> search(String uniqueToken, String query, String databaseId) throws SequenceSearchToolException;
+    
+    public Collection<BlastAlignment> search(String uniqueToken, String query) throws SequenceSearchToolException {
+        return search(uniqueToken, query, null);
+    }
+
+
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchToolException.java b/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchToolException.java
new file mode 100644
index 0000000..400b0bb
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/SequenceSearchToolException.java
@@ -0,0 +1,15 @@
+package org.bbop.apollo.sequence.search;
+
+public class SequenceSearchToolException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public SequenceSearchToolException(String message) {
+        super(message);
+    }
+    
+    public SequenceSearchToolException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blast/BlastAlignment.java b/src/groovy/org/bbop/apollo/sequence/search/blast/BlastAlignment.java
new file mode 100644
index 0000000..bcfb8a0
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blast/BlastAlignment.java
@@ -0,0 +1,135 @@
+package org.bbop.apollo.sequence.search.blast;
+
+import org.bbop.apollo.sequence.search.Alignment;
+import org.bbop.apollo.Match;
+import org.bbop.apollo.Region;
+import org.bbop.apollo.AnalysisFeature;
+import org.bbop.apollo.Feature;
+
+public class BlastAlignment implements Alignment {
+
+    public String queryId;
+    public String subjectId;
+    public double percentId;
+    public int alignmentLength;
+    public int numMismatches;
+    public int numGaps;
+    public int queryStrand;
+    public int subjectStrand;
+    public int queryStart;
+    public int queryEnd;
+    public int subjectStart;
+    public int subjectEnd;
+    public double eValue;
+    public double bitscore;
+
+    public BlastAlignment(String queryId, String subjectId, double percentId, int alignmentLength, int numMismatches, int numGaps,
+            int queryStart, int queryEnd, int subjectStart, int subjectEnd, double eValue, double bitscore) {
+        init(queryId, subjectId, percentId, alignmentLength, numMismatches, numGaps, queryStart, queryEnd, subjectStart, subjectEnd,
+                eValue, bitscore);
+    }
+    
+    protected void init(String queryId, String subjectId, double percentId, int alignmentLength, int numMismatches, int numGaps,
+            int queryStart, int queryEnd, int subjectStart, int subjectEnd, double eValue, double bitscore) {
+        this.queryId = queryId;
+        this.subjectId = subjectId;
+        this.percentId = percentId;
+        this.alignmentLength = alignmentLength;
+        this.numMismatches = numMismatches;
+        this.numGaps = numGaps;
+        this.queryStart = queryStart;
+        this.queryEnd = queryEnd;
+        this.subjectStart = subjectStart;
+        this.subjectEnd = subjectEnd;
+        this.eValue = eValue;
+        this.bitscore = bitscore;
+    }
+
+    public String getQueryId() {
+        return queryId;
+    }
+
+    public String getSubjectId() {
+        return subjectId;
+    }
+
+    public double getPercentId() {
+        return percentId;
+    }
+
+    public int getAlignmentLength() {
+        return alignmentLength;
+    }
+
+    public int getNumMismatches() {
+        return numMismatches;
+    }
+
+    public int getNumGaps() {
+        return numGaps;
+    }
+
+    public int getQueryStart() {
+        return queryStart;
+    }
+
+    public int getQueryEnd() {
+        return queryEnd;
+    }
+    
+    public int getSubjectStart() {
+        return subjectStart;
+    }
+
+    public int getSubjectEnd() {
+        return subjectEnd;
+    }
+    
+    public double getEValue() {
+        return eValue;
+    }
+
+    public double getBitscore() {
+        return bitscore;
+    }
+    
+    public Match convertToMatch() {
+        AnalysisFeature analysisFeature = new AnalysisFeature();
+        Match match=new Match();
+        match.setAnalysisFeature(analysisFeature);
+        analysisFeature.setRawScore(getBitscore());
+        analysisFeature.setSignificance(getEValue());
+        Feature query = new Feature();
+        query.setUniqueName(getQueryId());
+        int queryFmin = getQueryStart();
+        int queryFmax = getQueryEnd();
+        queryStrand = 1;
+        if (queryFmin > queryFmax) {
+            int tmp = queryFmin;
+            queryFmin = queryFmax;
+            queryFmax = tmp;
+            queryStrand = -1;
+        }
+        --queryFmin;
+        match.setQueryFeatureLocation(queryFmin, queryFmax, queryStrand, query);
+        Feature subject = new Feature();
+        subject.setUniqueName(getSubjectId());
+        int subjectFmin = getSubjectStart();
+        int subjectFmax = getSubjectEnd();
+        subjectStrand = 1;
+        if (subjectFmin > subjectFmax) {
+            int tmp = subjectFmin;
+            subjectFmin = subjectFmax;
+            subjectFmax = tmp;
+            subjectStrand = -1;
+        }
+        --subjectFmin;
+        match.setSubjectFeatureLocation(subjectFmin, subjectFmax, subjectStrand, subject);
+        match.setIdentity(getPercentId());
+        return match;
+    }
+
+    protected BlastAlignment() {
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blast/BlastCommandLine.java b/src/groovy/org/bbop/apollo/sequence/search/blast/BlastCommandLine.java
new file mode 100644
index 0000000..286f7f9
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blast/BlastCommandLine.java
@@ -0,0 +1,156 @@
+package org.bbop.apollo.sequence.search.blast;
+
+import org.bbop.apollo.sequence.search.AlignmentParsingException;
+import org.bbop.apollo.sequence.search.SequenceSearchTool;
+import org.bbop.apollo.sequence.search.SequenceSearchToolException;
+import org.bbop.apollo.sequence.search.blast.BlastAlignment;
+import org.bbop.apollo.sequence.search.blast.TabDelimittedAlignment;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.codehaus.groovy.grails.web.json.JSONObject;
+
+public class BlastCommandLine extends SequenceSearchTool {
+
+    private String blastBin;
+    private String database;
+    private String blastUserOptions;
+    private String tmpDir;
+    private boolean removeTmpDir;
+    protected String [] blastOptions;
+    
+    @Override
+    public void parseConfiguration(JSONObject config) throws SequenceSearchToolException {
+        try {
+            if(config.has("search_exe")) { blastBin = config.getString("search_exe"); }
+            else { throw new SequenceSearchToolException("No blast exe specified"); }
+            if(config.has("database")&&config.getString("database")!="") {database = config.getString("database"); }
+            else { throw new SequenceSearchToolException("No database configured"); }
+            if(config.has("params")) {blastUserOptions = config.getString("params");}
+            else { /* no extra params needed */ }
+            if(config.has("removeTmpDir")) {removeTmpDir=config.getBoolean("removeTmpDir"); }
+            else { removeTmpDir=true; }
+            if(config.has("tmp_dir")) {tmpDir=config.getString("tmp_dir"); }
+        } catch (Exception e) {
+            throw new SequenceSearchToolException("Error parsing configuration: " + e.getMessage(), e);
+        }
+    }
+
+
+    @Override
+    public Collection<BlastAlignment> search(String uniqueToken, String query, String databaseId) throws SequenceSearchToolException {
+        File dir = null;
+        Path p = null;
+        try {
+            if(tmpDir==null) {
+                p = Files.createTempDirectory("blast_tmp");
+            }
+            else {
+                p = Files.createTempDirectory(new File(tmpDir).toPath(),"blast_tmp");
+            }
+            dir = p.toFile();
+
+            return runSearch(dir, query, databaseId);
+        }
+        catch (IOException e) {
+            throw new SequenceSearchToolException("Error running search: " + e.getMessage(), e);
+        }
+        catch (AlignmentParsingException e) {
+            throw new SequenceSearchToolException("Alignment parsing error: " + e.getMessage(), e);
+        }
+        catch (InterruptedException e) {
+            throw new SequenceSearchToolException("Error running search: " + e.getMessage(), e);
+        }
+        finally {
+            if (removeTmpDir && dir!=null) {
+                deleteTmpDir(dir);
+            }
+        }
+    }
+    
+    private Collection<BlastAlignment> runSearch(File dir, String query, String databaseId)
+            throws IOException, AlignmentParsingException, InterruptedException {
+        PrintWriter log = new PrintWriter(new BufferedWriter(new FileWriter(dir + "/search.log")));
+        String queryArg = createQueryFasta(dir, query);
+//        String databaseArg = database + (databaseId != null ? ":" + databaseId : "");
+        String databaseArg = database ;
+        String outputArg = dir.getAbsolutePath() + "/results.tab";
+        List<String> commands = new ArrayList<String>();
+        commands.add(blastBin);
+        if (blastOptions != null) {
+            for (String option : blastOptions) {
+                commands.add(option);
+            }
+        }
+        commands.add("-db");
+        commands.add(databaseArg);
+        commands.add("-query");
+        commands.add(queryArg);
+        commands.add("-out");
+        commands.add(outputArg);
+        commands.add("-outfmt");
+        commands.add("6");
+        if (blastUserOptions != null && blastUserOptions.length() > 0) {
+            for (String option : blastUserOptions.split("\\s+")) {
+                commands.add(option);
+            }
+        }
+        runCommand(commands,log);
+
+
+        Collection<BlastAlignment> matches = new ArrayList<BlastAlignment>();
+        BufferedReader in = new BufferedReader(new FileReader(outputArg));
+        String line;
+        while ((line = in.readLine()) != null) {
+            matches.add(new TabDelimittedAlignment(line));
+        }
+        in.close();
+        return matches;
+    }
+    private void runCommand(List<String> commands, PrintWriter log) throws IOException, InterruptedException {
+        log.println("Command:");
+        for (String arg : commands) {
+            log.print(arg + " ");
+        }
+        ProcessBuilder pb = new ProcessBuilder(commands);
+        Process p = pb.start();
+        p.waitFor();
+        String line;
+        BufferedReader stdout = new BufferedReader(new InputStreamReader(p.getInputStream()));
+        log.println("stdout:");
+        while ((line = stdout.readLine()) != null) {
+            log.println(line);
+        }
+        log.println();
+        BufferedReader stderr = new BufferedReader(new InputStreamReader(p.getErrorStream()));
+        log.println("stderr:");
+        while ((line = stderr.readLine()) != null) {
+            log.println(line);
+        }
+        log.close();
+        p.destroy();
+    }
+    private void deleteTmpDir(File dir) {
+        if (!dir.exists()) {
+            return;
+        }
+        for (File f : dir.listFiles()) {
+            f.delete();
+        }
+        dir.delete();
+    }
+
+
+    private String createQueryFasta(File dir, String query) throws IOException {
+        String queryFileName = dir.getAbsolutePath() + "/query.fa";
+        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(queryFileName)));
+        out.println(">query");
+        out.println(query);
+        out.close();
+        return queryFileName;
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blast/TabDelimittedAlignment.java b/src/groovy/org/bbop/apollo/sequence/search/blast/TabDelimittedAlignment.java
new file mode 100644
index 0000000..2e8560a
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blast/TabDelimittedAlignment.java
@@ -0,0 +1,43 @@
+package org.bbop.apollo.sequence.search.blast;
+
+import org.bbop.apollo.sequence.search.AlignmentParsingException;
+
+public class TabDelimittedAlignment extends BlastAlignment {
+
+    private final static int EXPECTED_NUM_FIELDS = 12;
+    private final static String DELIMITER = "\t";
+    
+    public TabDelimittedAlignment(String tabbedAlignment) throws AlignmentParsingException {
+        String []fields = tabbedAlignment.split(DELIMITER);
+        if (fields.length != EXPECTED_NUM_FIELDS) {
+            throw new AlignmentParsingException("Incorrect number of fields: found " + fields.length + " but expected " + EXPECTED_NUM_FIELDS);
+        }
+        String queryId = fields[0];
+        String subjectId = fields[1];
+        double percentId = Double.parseDouble(fields[2]);
+        int alignmentLength = Integer.parseInt(fields[3]);
+        int numMismatches = Integer.parseInt(fields[4]);
+        int numGaps = Integer.parseInt(fields[5]);
+        int queryStart = Integer.parseInt(fields[6]);
+        int queryEnd = Integer.parseInt(fields[7]);
+        int subjectStart = Integer.parseInt(fields[8]);
+        int subjectEnd = Integer.parseInt(fields[9]);
+        if(subjectEnd<subjectStart) {
+            int swap;
+            swap=subjectStart;
+            subjectStart=subjectEnd;
+            subjectEnd=swap;
+        }
+        if(queryEnd<queryStart) {
+            int swap;
+            swap=queryStart;
+            queryStart=queryEnd;
+            queryEnd=swap;
+        }
+        double eValue = Double.parseDouble(fields[10]);
+        double bitscore = Double.parseDouble(fields[11]);
+        init(queryId, subjectId, percentId, alignmentLength, numMismatches, numGaps,
+                queryStart, queryEnd, subjectStart, subjectEnd, eValue, bitscore);
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLine.java b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLine.java
new file mode 100644
index 0000000..14493d4
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLine.java
@@ -0,0 +1,153 @@
+package org.bbop.apollo.sequence.search.blat;
+
+import org.bbop.apollo.sequence.search.AlignmentParsingException;
+import org.bbop.apollo.sequence.search.SequenceSearchTool;
+import org.bbop.apollo.sequence.search.SequenceSearchToolException;
+import org.bbop.apollo.sequence.search.blast.BlastAlignment;
+import org.bbop.apollo.sequence.search.blast.TabDelimittedAlignment;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.codehaus.groovy.grails.web.json.JSONObject;
+
+public class BlatCommandLine extends SequenceSearchTool {
+
+    private String blatBin;
+    private String database;
+    private String blatUserOptions;
+    private String tmpDir;
+    private boolean removeTmpDir;
+    protected String [] blatOptions;
+    
+    @Override
+    public void parseConfiguration(JSONObject config) throws SequenceSearchToolException {
+        try {
+            if(config.has("search_exe")) { blatBin = config.getString("search_exe"); }
+            else { throw new SequenceSearchToolException("No blat exe specified"); }
+            if(config.has("database")&&config.getString("database")!="") {database = config.getString("database"); }
+            else { throw new SequenceSearchToolException("No database configured"); }
+            if(config.has("params")) {blatUserOptions = config.getString("params");}
+            else { /* no extra params needed */ }
+            if(config.has("removeTmpDir")) {removeTmpDir=config.getBoolean("removeTmpDir"); }
+            else { removeTmpDir=true; }
+            if(config.has("tmp_dir")) {tmpDir=config.getString("tmp_dir"); }
+        } catch (Exception e) {
+            throw new SequenceSearchToolException("Error parsing configuration: " + e.getMessage(), e);
+        }
+    }
+
+
+    @Override
+    public Collection<BlastAlignment> search(String uniqueToken, String query, String databaseId) throws SequenceSearchToolException {
+        File dir = null;
+        Path p = null;
+        try {
+            if(tmpDir==null) {
+                p = Files.createTempDirectory("blat_tmp");
+            }
+            else {
+                p = Files.createTempDirectory(new File(tmpDir).toPath(),"blat_tmp");
+            }
+            dir = p.toFile();
+
+            return runSearch(dir, query, databaseId);
+        }
+        catch (IOException e) {
+            throw new SequenceSearchToolException("Error running search: " + e.getMessage(), e);
+        }
+        catch (AlignmentParsingException e) {
+            throw new SequenceSearchToolException("Alignment parsing error: " + e.getMessage(), e);
+        }
+        catch (InterruptedException e) {
+            throw new SequenceSearchToolException("Error running search: " + e.getMessage(), e);
+        }
+        finally {
+            if (removeTmpDir && dir!=null) {
+                deleteTmpDir(dir);
+            }
+        }
+    }
+    
+    private Collection<BlastAlignment> runSearch(File dir, String query, String databaseId)
+            throws IOException, AlignmentParsingException, InterruptedException {
+        PrintWriter log = new PrintWriter(new BufferedWriter(new FileWriter(dir + "/search.log")));
+        String queryArg = createQueryFasta(dir, query);
+        String databaseArg = database + (databaseId != null ? ":" + databaseId : "");
+        String outputArg = dir.getAbsolutePath() + "/results.tab";
+        List<String> commands = new ArrayList<String>();
+        commands.add(blatBin);
+        if (blatOptions != null) {
+            for (String option : blatOptions) {
+                commands.add(option);
+            }
+        }
+        commands.add(databaseArg);
+        commands.add(queryArg);
+        commands.add(outputArg);
+        commands.add("-out=blast8");
+        if (blatUserOptions != null && blatUserOptions.length() > 0) {
+            for (String option : blatUserOptions.split("\\s+")) {
+                commands.add(option);
+            }
+        }
+        runCommand(commands,log);
+
+        Collection<BlastAlignment> matches = new ArrayList<BlastAlignment>();
+        BufferedReader in = new BufferedReader(new FileReader(outputArg));
+        String line;
+        while ((line = in.readLine()) != null) {
+            matches.add(new TabDelimittedAlignment(line));
+        }
+        in.close();
+        return matches;
+    }
+
+
+    private void runCommand(List<String> commands, PrintWriter log) throws IOException, InterruptedException {
+        log.println("Command:");
+        for (String arg : commands) {
+            log.print(arg + " ");
+        }
+        ProcessBuilder pb = new ProcessBuilder(commands);
+        Process p = pb.start();
+        p.waitFor();
+        String line;
+        BufferedReader stdout = new BufferedReader(new InputStreamReader(p.getInputStream()));
+        log.println("stdout:");
+        while ((line = stdout.readLine()) != null) {
+            log.println(line);
+        }
+        log.println();
+        BufferedReader stderr = new BufferedReader(new InputStreamReader(p.getErrorStream()));
+        log.println("stderr:");
+        while ((line = stderr.readLine()) != null) {
+            log.println(line);
+        }
+        log.close();
+        p.destroy();
+    }
+
+    private void deleteTmpDir(File dir) {
+        if (!dir.exists()) {
+            return;
+        }
+        for (File f : dir.listFiles()) {
+            f.delete();
+        }
+        dir.delete();
+    }
+
+
+    private String createQueryFasta(File dir, String query) throws IOException {
+        String queryFileName = dir.getAbsolutePath() + "/query.fa";
+        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(queryFileName)));
+        out.println(">query");
+        out.println(query);
+        out.close();
+        return queryFileName;
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineNucleotideToNucleotide.java b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineNucleotideToNucleotide.java
new file mode 100644
index 0000000..e6d7923
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineNucleotideToNucleotide.java
@@ -0,0 +1,5 @@
+package org.bbop.apollo.sequence.search.blat;
+
+public class BlatCommandLineNucleotideToNucleotide extends BlatCommandLine {
+
+}
diff --git a/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineProteinToNucleotide.java b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineProteinToNucleotide.java
new file mode 100644
index 0000000..599b6b8
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/sequence/search/blat/BlatCommandLineProteinToNucleotide.java
@@ -0,0 +1,9 @@
+package org.bbop.apollo.sequence.search.blat;
+
+public class BlatCommandLineProteinToNucleotide extends BlatCommandLine {
+
+    public BlatCommandLineProteinToNucleotide() {
+        blatOptions = new String[]{ "-t=dnax", "-q=prot" };
+    }
+    
+}
diff --git a/src/groovy/org/bbop/apollo/track/RenderObject.groovy b/src/groovy/org/bbop/apollo/track/RenderObject.groovy
new file mode 100644
index 0000000..fefae6f
--- /dev/null
+++ b/src/groovy/org/bbop/apollo/track/RenderObject.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo.track
+
+/**
+ * Created by nathandunn on 6/8/17.
+ */
+class RenderObject {
+
+    int globalFmin
+    int globalFmax
+
+
+    int getGlobalWidth() {
+        return globalFmax - globalFmin
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/Annotator.gwt.xml b/src/gwt/org/bbop/apollo/gwt/Annotator.gwt.xml
new file mode 100644
index 0000000..95151de
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/Annotator.gwt.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  When updating your version of GWT, you should also update this DTD reference,
+  so that your app can take advantage of the latest GWT module capabilities.
+-->
+<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.8.0//EN"
+        "http://gwtproject.org/doctype/2.8.0/gwt-module.dtd">
+<module rename-to='annotator'>
+    <!-- Inherit the core Web Toolkit stuff.                        -->
+    <inherits name='com.google.gwt.user.User'/>
+
+    <!-- Inherit the default GWT style sheet.  You can change       -->
+    <!-- the theme of your GWT application by uncommenting          -->
+    <!-- any one of the following lines.                            -->
+
+    <inherits name='com.google.gwt.user.theme.clean.Clean'/>
+    <!--<inherits name='com.google.gwt.user.theme.standard.Standard'/>-->
+    <!--<inherits name='com.google.gwt.user.theme.chrome.Chrome'/>  -->
+    <!--<inherits name='com.google.gwt.user.theme.dark.Dark'/>      -->
+
+
+    <inherits name="com.google.gwt.http.HTTP"/>
+    <inherits name="com.google.gwt.json.JSON"/>
+    <inherits name="org.gwtbootstrap3.GwtBootstrap3"/>
+    <inherits name="org.gwtbootstrap3.extras.bootbox.Bootbox"/>
+    <inherits name="org.gwtbootstrap3.extras.select.Select"/>
+    <inherits name="org.gwtbootstrap3.extras.toggleswitch.ToggleSwitch"/>
+
+
+
+    <!-- Specify the app entry point class.                         -->
+    <entry-point class='org.bbop.apollo.gwt.client.Annotator'/>
+
+    <!-- Specify the paths for translatable code                    -->
+    <source path='client'/>
+    <source path='shared'/>
+
+    <!-- Add some custom CSS for font sizes -->
+    <stylesheet src='theme.css'/>
+
+    <!-- allow Super Dev Mode -->
+    <add-linker name="xsiframe"/>
+</module>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/AnnotationContainerWidget.java b/src/gwt/org/bbop/apollo/gwt/client/AnnotationContainerWidget.java
new file mode 100644
index 0000000..fb01ce9
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/AnnotationContainerWidget.java
@@ -0,0 +1,43 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.user.client.ui.HTML;
+
+/**
+ * Created by ndunn on 1/8/15.
+ */
+public class AnnotationContainerWidget extends HTML{
+
+    private JSONObject internalData ;
+
+    public AnnotationContainerWidget(String string){
+        super(string);
+    }
+
+    public AnnotationContainerWidget(JSONObject object) {
+        internalData = object ;
+        String featureName = "";
+        String featureType = object.get("type").isObject().get("name").isString().stringValue();
+        switch (featureType){
+            case "exon":
+                featureName = "exon" ;
+                break;
+            case "CDS":
+                featureName = "CDS" ;
+                break;
+
+            default:
+                featureName = object.get("name").isString().stringValue();
+                break;
+        }
+        int lastFeature = featureType.lastIndexOf(".");
+        featureType = featureType.substring(lastFeature + 1);
+        HTML html = new HTML(featureName + " <div class='label label-success'>" + featureType + "</div>");
+        setHTML(html.getHTML());
+    }
+
+    public JSONObject getInternalData() {
+        return internalData;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/Annotator.java b/src/gwt/org/bbop/apollo/gwt/client/Annotator.java
new file mode 100644
index 0000000..08ca1ba
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/Annotator.java
@@ -0,0 +1,170 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.i18n.client.Dictionary;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.core.java.util.HashMap_CustomFieldSerializer;
+import com.google.gwt.user.client.ui.RootLayoutPanel;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.bbop.apollo.gwt.shared.ClientTokenGenerator;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.bootbox.client.callback.ConfirmCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Entry point classes define <code>onModuleLoad()</code>.
+ */
+public class Annotator implements EntryPoint {
+
+    public static EventBus eventBus = GWT.create(SimpleEventBus.class);
+    private static Storage preferenceStore = Storage.getSessionStorageIfSupported();
+    private static Map<String,String> backupPreferenceStore = new HashMap<>();
+
+    // check the session once a minute
+    private static Integer DEFAULT_PING_TIME = 60000;
+
+    /**
+     * This is the entry point method.
+     */
+    public void onModuleLoad() {
+        MainPanel mainPanel = MainPanel.getInstance();
+        RootLayoutPanel rp = RootLayoutPanel.get();
+        rp.add(mainPanel);
+
+        Dictionary optionsDictionary = Dictionary.getDictionary("Options");
+        if(optionsDictionary.keySet().contains(FeatureStringEnum.CLIENT_TOKEN.getValue())){
+            String clientToken = optionsDictionary.get(FeatureStringEnum.CLIENT_TOKEN.getValue());
+            if(ClientTokenGenerator.isValidToken(clientToken)){
+                setPreference(FeatureStringEnum.CLIENT_TOKEN.getValue(),clientToken);
+            }
+        }
+        Double height = 100d;
+        Style.Unit heightUnit = Style.Unit.PCT;
+        Double top = 0d;
+        Style.Unit topUnit = Style.Unit.PCT;
+
+        if (optionsDictionary.keySet().contains("top")) {
+            top = Double.valueOf(optionsDictionary.get("top"));
+        }
+        if (optionsDictionary.keySet().contains("topUnit")) {
+            topUnit = Style.Unit.valueOf(optionsDictionary.get("topUnit").toUpperCase());
+        }
+        if (optionsDictionary.keySet().contains("height")) {
+            height = Double.valueOf(optionsDictionary.get("height"));
+        }
+        if (optionsDictionary.keySet().contains("heightUnit")) {
+            heightUnit = Style.Unit.valueOf(optionsDictionary.get("heightUnit").toUpperCase());
+        }
+        rp.setWidgetTopHeight(mainPanel, top, topUnit, height, heightUnit);
+
+        exportStaticMethod();
+    }
+
+    static void startSessionTimer() {
+        startSessionTimer(DEFAULT_PING_TIME);
+    }
+
+    static void startSessionTimer(int i) {
+        Scheduler.get().scheduleFixedPeriod(new Scheduler.RepeatingCommand() {
+
+            private Boolean keepGoing = true ;
+            private Boolean confirmOpen = false ;
+
+            @Override
+            public boolean execute() {
+                if(MainPanel.hasCurrentUser()){
+                    RestService.sendRequest(new RequestCallback() {
+                        @Override
+                        public void onResponseReceived(Request request, Response response) {
+                            int statusCode = response.getStatusCode();
+                            if(statusCode==200 && response.getText().equals("{}")){
+                                GWT.log("Still connected");
+                            }
+                            else{
+                                if(!confirmOpen){
+                                    confirmOpen = true ;
+                                    Bootbox.confirm("Logged out or server failure.  Attempt to reconnect?", new ConfirmCallback() {
+                                        @Override
+                                        public void callback(boolean result) {
+                                            if(result){
+                                                Window.Location.reload();
+                                            }
+
+                                            confirmOpen = false ;
+                                        }
+                                    });
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void onError(Request request, Throwable exception) {
+                            Window.alert("failed to connect: "+exception.toString());
+                            Bootbox.alert("Error: "+exception);
+                        }
+                    },"annotator/ping");
+
+                    return keepGoing ;
+                }
+                else{
+                    return false;
+                }
+            }
+        },i);
+    }
+
+    public static native void exportStaticMethod() /*-{
+        $wnd.setPreference = $entry(@org.bbop.apollo.gwt.client.Annotator::setPreference(Ljava/lang/String;Ljava/lang/Object;));
+        $wnd.getPreference = $entry(@org.bbop.apollo.gwt.client.Annotator::getPreference(Ljava/lang/String;));
+        $wnd.getClientToken = $entry(@org.bbop.apollo.gwt.client.Annotator::getClientToken());
+    }-*/;
+
+    public static void setPreference(String key, Object value) {
+        if (preferenceStore != null) {
+            preferenceStore.setItem(key, value.toString());
+        }
+        else{
+            backupPreferenceStore.put(key,value.toString());
+        }
+    }
+
+    public static String getPreference(String key) {
+        if (preferenceStore != null) {
+            return preferenceStore.getItem(key);
+        }
+        else{
+            return backupPreferenceStore.get(key);
+        }
+    }
+
+
+    public static String getRootUrl(){
+        return GWT.getModuleBaseURL().replace("annotator/","");
+    }
+
+    public static String getClientToken() {
+        String token = getPreference(FeatureStringEnum.CLIENT_TOKEN.getValue());
+        if (!ClientTokenGenerator.isValidToken(token)) {
+            token = ClientTokenGenerator.generateRandomString();
+            setPreference(FeatureStringEnum.CLIENT_TOKEN.getValue(), token);
+        }
+        token = getPreference(FeatureStringEnum.CLIENT_TOKEN.getValue());
+        return token ;
+
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.java b/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.java
new file mode 100644
index 0000000..4f8ef62
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.java
@@ -0,0 +1,836 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.ClickableTextCell;
+import com.google.gwt.cell.client.FieldUpdater;
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.builder.shared.DivBuilder;
+import com.google.gwt.dom.builder.shared.TableCellBuilder;
+import com.google.gwt.dom.builder.shared.TableRowBuilder;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.*;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.http.client.*;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.NumberFormat;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
+import com.google.gwt.text.shared.SafeHtmlRenderer;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.*;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.client.ui.*;
+import com.google.gwt.view.client.*;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfoConverter;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfoConverter;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEventHandler;
+import org.bbop.apollo.gwt.client.event.UserChangeEvent;
+import org.bbop.apollo.gwt.client.event.UserChangeEventHandler;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.Label;
+import org.gwtbootstrap3.client.ui.ListBox;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by ndunn on 12/17/14.
+ */
+public class AnnotatorPanel extends Composite {
+
+
+    interface AnnotatorPanelUiBinder extends UiBinder<com.google.gwt.user.client.ui.Widget, AnnotatorPanel> {
+    }
+
+    private static AnnotatorPanelUiBinder ourUiBinder = GWT.create(AnnotatorPanelUiBinder.class);
+    private DateTimeFormat outputFormat = DateTimeFormat.getFormat("MMM dd, yyyy");
+    private Column<AnnotationInfo, String> nameColumn;
+    private TextColumn<AnnotationInfo> typeColumn;
+    private TextColumn<AnnotationInfo> sequenceColumn;
+    private Column<AnnotationInfo, Number> lengthColumn;
+    private Column<AnnotationInfo, String> dateColumn;
+    private Column<AnnotationInfo, String> showHideColumn;
+    private long requestIndex = 0;
+    private static String selectedChildUniqueName = null;
+
+    private final String COLLAPSE_ICON_UNICODE = "\u25BC";
+    private final String EXPAND_ICON_UNICODE = "\u25C0";
+
+    @UiField
+    TextBox nameSearchBox;
+    @UiField(provided = true)
+    org.gwtbootstrap3.client.ui.SuggestBox sequenceList;
+
+    private static DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+
+    @UiField(provided = true)
+    static DataGrid<AnnotationInfo> dataGrid = new DataGrid<>(20, tablecss);
+    @UiField(provided = true)
+    WebApolloSimplePager pager = null;
+
+    @UiField
+    ListBox typeList;
+    @UiField
+    static GeneDetailPanel geneDetailPanel;
+    @UiField
+    static TranscriptDetailPanel transcriptDetailPanel;
+    @UiField
+    static ExonDetailPanel exonDetailPanel;
+    @UiField
+    static RepeatRegionDetailPanel repeatRegionDetailPanel;
+    @UiField
+    static TabLayoutPanel tabPanel;
+    @UiField
+    ListBox userField;
+    @UiField
+    DockLayoutPanel splitPanel;
+    @UiField
+    Container northPanelContainer;
+    @UiField
+    static Button gotoAnnotation;
+
+    private static AnnotationInfo selectedAnnotationInfo;
+    private MultiWordSuggestOracle sequenceOracle = new ReferenceSequenceOracle();
+
+    private static AsyncDataProvider<AnnotationInfo> dataProvider;
+    private SingleSelectionModel<AnnotationInfo> singleSelectionModel = new SingleSelectionModel<>();
+    private final Set<String> showingTranscripts = new HashSet<String>();
+
+    public AnnotatorPanel() {
+        sequenceList = new org.gwtbootstrap3.client.ui.SuggestBox(sequenceOracle);
+        sequenceList.getElement().setAttribute("placeHolder", "Reference Sequence");
+        dataGrid.setWidth("100%");
+        dataGrid.setTableBuilder(new CustomTableBuilder());
+        dataGrid.setLoadingIndicator(new Label("Loading"));
+        dataGrid.setEmptyTableWidget(new Label("No results"));
+        initializeTable();
+
+
+        pager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+
+        dataGrid.addCellPreviewHandler(new CellPreviewEvent.Handler<AnnotationInfo>() {
+            @Override
+            public void onCellPreview(CellPreviewEvent<AnnotationInfo> event) {
+                AnnotationInfo annotationInfo = event.getValue();
+                if (event.getNativeEvent().getType().equals(BrowserEvents.CLICK)) {
+                    if (event.getContext().getSubIndex() == 0) {
+                        // subIndex from dataGrid will be 0 only when top-level cell values are clicked
+                        // ie. gene, pseudogene
+                        GWT.log("Safe to call updateAnnotationInfo");
+                        updateAnnotationInfo(annotationInfo);
+                    }
+                }
+            }
+        });
+
+
+        exportStaticMethod(this);
+
+
+        initWidget(ourUiBinder.createAndBindUi(this));
+
+        dataProvider = new AsyncDataProvider<AnnotationInfo>() {
+            @Override
+            protected void onRangeChanged(HasData<AnnotationInfo> display) {
+                final Range range = display.getVisibleRange();
+                final ColumnSortList sortList = dataGrid.getColumnSortList();
+                final int start = range.getStart();
+                final int length = range.getLength();
+                String sequenceName = sequenceList.getText().trim();
+
+
+                String url = Annotator.getRootUrl() + "annotator/findAnnotationsForSequence/?sequenceName=" + sequenceName;
+                url += "&request=" + requestIndex;
+                url += "&offset=" + start + "&max=" + length;
+                url += "&annotationName=" + nameSearchBox.getText() + "&type=" + typeList.getSelectedValue();
+                url += "&user=" + userField.getSelectedValue();
+                url += "&clientToken=" + Annotator.getClientToken();
+
+
+                ColumnSortList.ColumnSortInfo nameSortInfo = sortList.get(0);
+                Column<AnnotationInfo, ?> sortColumn = (Column<AnnotationInfo, ?>) sortList.get(0).getColumn();
+                Integer columnIndex = dataGrid.getColumnIndex(sortColumn);
+                String searchColumnString = null;
+                switch (columnIndex) {
+                    case 0:
+                        searchColumnString = "name";
+                        break;
+                    case 1:
+                        searchColumnString = "sequence";
+                        break;
+                    case 3:
+                        searchColumnString = "length";
+                        break;
+                    case 4:
+                        searchColumnString = "date";
+                    default:
+                        break;
+                }
+                Boolean sortNameAscending = nameSortInfo.isAscending();
+                url += "&sortorder=" + (sortNameAscending ? "asc" : "desc");
+                url += "&sort=" + searchColumnString;
+
+                RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
+                builder.setHeader("Content-type", "application/x-www-form-urlencoded");
+                RequestCallback requestCallback = new RequestCallback() {
+                    @Override
+                    public void onResponseReceived(Request request, Response response) {
+                        JSONValue returnValue = null;
+                        try {
+                            returnValue = JSONParser.parseStrict(response.getText());
+                        } catch (Exception e) {
+                            Bootbox.alert(e.getMessage());
+                        }
+                        JSONValue localRequestObject = returnValue.isObject().get(FeatureStringEnum.REQUEST_INDEX.getValue());
+                        if (localRequestObject != null) {
+                            long localRequestValue = (long) localRequestObject.isNumber().doubleValue();
+                            if (localRequestValue <= requestIndex) {
+                                return;
+                            } else {
+                                requestIndex = localRequestValue;
+                            }
+                            Integer annotationCount = (int) returnValue.isObject().get(FeatureStringEnum.ANNOTATION_COUNT.getValue()).isNumber().doubleValue();
+
+                            JSONArray jsonArray = returnValue.isObject().get(FeatureStringEnum.FEATURES.getValue()).isArray();
+
+                            dataGrid.setRowCount(annotationCount, true);
+                            final List<AnnotationInfo> annotationInfoList = AnnotationInfoConverter.convertFromJsonArray(jsonArray);
+                            dataGrid.setRowData(start, annotationInfoList);
+
+                            Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+                                @Override
+                                public void execute() {
+                                    if (selectedAnnotationInfo != null) {
+//                                    Window.alert("setting data: "+selectedAnnotationInfo.getName());
+                                        // refind and update internally
+                                        for (AnnotationInfo annotationInfo : annotationInfoList) {
+                                            GWT.log("top-level: " + annotationInfo.getType());
+                                            // will be found if a top-level selection
+                                            if (annotationInfo.getUniqueName().equals(selectedAnnotationInfo.getUniqueName())) {
+                                                selectedAnnotationInfo = annotationInfo;
+                                                singleSelectionModel.clear();
+                                                singleSelectionModel.setSelected(selectedAnnotationInfo, true);
+                                                updateAnnotationInfo(selectedAnnotationInfo);
+                                                return;
+                                            }
+                                            // if a child, we need to get the index I think?
+                                            final String thisUniqueName = selectedChildUniqueName;
+                                            for (AnnotationInfo annotationInfoChild : annotationInfo.getAnnotationInfoSet()) {
+                                                GWT.log("next-level: " + annotationInfoChild.getType());
+                                                if (annotationInfoChild.getUniqueName().equals(selectedAnnotationInfo.getUniqueName())) {
+//                                                    selectedAnnotationInfo = annotationInfo;
+                                                    selectedAnnotationInfo = getChildAnnotation(annotationInfo, thisUniqueName);
+                                                    singleSelectionModel.clear();
+                                                    singleSelectionModel.setSelected(selectedAnnotationInfo, true);
+                                                    updateAnnotationInfo(selectedAnnotationInfo);
+                                                    return;
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            });
+                        }
+
+                    }
+
+                    @Override
+                    public void onError(Request request, Throwable exception) {
+                        Bootbox.alert("Error loading organisms");
+                    }
+                };
+                try {
+                    if (MainPanel.getInstance().getCurrentUser() != null) {
+                        builder.setCallback(requestCallback);
+                        builder.send();
+                    }
+                } catch (RequestException e) {
+                    // Couldn't connect to server
+                    Bootbox.alert(e.getMessage());
+                }
+            }
+        };
+
+        ColumnSortEvent.AsyncHandler columnSortHandler = new ColumnSortEvent.AsyncHandler(dataGrid);
+        dataGrid.addColumnSortHandler(columnSortHandler);
+        dataGrid.getColumnSortList().push(nameColumn);
+        dataGrid.getColumnSortList().push(sequenceColumn);
+        dataGrid.getColumnSortList().push(lengthColumn);
+        dataGrid.getColumnSortList().push(dateColumn);
+
+        dataProvider.addDataDisplay(dataGrid);
+        pager.setDisplay(dataGrid);
+
+
+        initializeTypes();
+        initializeUsers();
+
+        sequenceList.addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() {
+            @Override
+            public void onSelection(SelectionEvent<SuggestOracle.Suggestion> event) {
+                reload();
+            }
+        });
+
+        sequenceList.addKeyUpHandler(new KeyUpHandler() {
+            @Override
+            public void onKeyUp(KeyUpEvent event) {
+                if (sequenceList.getText() == null || sequenceList.getText().trim().length() == 0) {
+                    reload();
+                }
+            }
+        });
+
+
+        tabPanel.addSelectionHandler(new SelectionHandler<Integer>() {
+            @Override
+            public void onSelection(SelectionEvent<Integer> event) {
+                exonDetailPanel.redrawExonTable();
+            }
+        });
+
+        Annotator.eventBus.addHandler(AnnotationInfoChangeEvent.TYPE, new AnnotationInfoChangeEventHandler() {
+            @Override
+            public void onAnnotationChanged(AnnotationInfoChangeEvent annotationInfoChangeEvent) {
+                reload();
+            }
+        });
+
+        Annotator.eventBus.addHandler(UserChangeEvent.TYPE,
+                new UserChangeEventHandler() {
+                    @Override
+                    public void onUserChanged(UserChangeEvent authenticationEvent) {
+                        switch (authenticationEvent.getAction()) {
+                            case PERMISSION_CHANGED:
+                                PermissionEnum hiPermissionEnum = authenticationEvent.getHighestPermission();
+                                if (MainPanel.getInstance().isCurrentUserAdmin()) {
+                                    hiPermissionEnum = PermissionEnum.ADMINISTRATE;
+                                }
+                                boolean editable = false;
+                                switch (hiPermissionEnum) {
+                                    case ADMINISTRATE:
+                                    case WRITE:
+                                        editable = true;
+                                        break;
+                                    // default is false
+                                }
+                                transcriptDetailPanel.setEditable(editable);
+                                geneDetailPanel.setEditable(editable);
+                                exonDetailPanel.setEditable(editable);
+                                repeatRegionDetailPanel.setEditable(editable);
+                                reload();
+                                break;
+                        }
+                    }
+                }
+        );
+
+        // TODO: not sure if this was necessary, leaving it here until it fails
+//        Annotator.eventBus.addHandler(OrganismChangeEvent.TYPE, new OrganismChangeEventHandler() {
+//            @Override
+//            public void onOrganismChanged(OrganismChangeEvent organismChangeEvent) {
+//                if (organismChangeEvent.getAction() == OrganismChangeEvent.Action.LOADED_ORGANISMS) {
+//                    sequenceList.setText(organismChangeEvent.getCurrentSequence());
+//                    reload();
+//                }
+//            }
+//        });
+
+        Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+            @Override
+            public void execute() {
+                userField.setVisible(true);
+            }
+        });
+
+    }
+
+
+    private void initializeUsers() {
+        userField.clear();
+        userField.addItem("All Users", "");
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+                JSONArray array = returnValue.isArray();
+
+                for (int i = 0; array != null && i < array.size(); i++) {
+                    JSONObject object = array.get(i).isObject();
+                    UserInfo userInfo = UserInfoConverter.convertToUserInfoFromJSON(object);
+                    userField.addItem(userInfo.getName(), userInfo.getEmail());
+                }
+
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error retrieving users: " + exception.fillInStackTrace());
+            }
+        };
+        if (MainPanel.getInstance().getCurrentUser() != null) {
+            UserRestService.loadUsers(requestCallback);
+        }
+    }
+
+    private void initializeTypes() {
+        typeList.addItem("All Types", "");
+        typeList.addItem("Gene");
+        typeList.addItem("Pseudogene");
+        typeList.addItem("Transposable Element", "transposable_element");
+        typeList.addItem("Repeat Region", "repeat_region");
+    }
+
+    private static void hideDetailPanels() {
+        geneDetailPanel.setVisible(false);
+        transcriptDetailPanel.setVisible(false);
+        repeatRegionDetailPanel.setVisible(false);
+//        exonDetailPanel.setVisible(false);
+    }
+
+    private static void updateAnnotationInfo(AnnotationInfo annotationInfo) {
+        if(annotationInfo==null){
+            return ;
+        }
+        String type = annotationInfo.getType();
+        GWT.log("annotation type: " + type);
+        hideDetailPanels();
+        switch (type) {
+            case "gene":
+            case "pseudogene":
+                geneDetailPanel.updateData(annotationInfo);
+                tabPanel.getTabWidget(1).getParent().setVisible(false);
+                break;
+            case "transcript":
+                transcriptDetailPanel.updateData(annotationInfo);
+                tabPanel.getTabWidget(1).getParent().setVisible(true);
+                exonDetailPanel.updateData(annotationInfo,selectedAnnotationInfo);
+                break;
+            case "mRNA":
+            case "miRNA":
+            case "tRNA":
+            case "rRNA":
+            case "snRNA":
+            case "snoRNA":
+            case "ncRNA":
+                transcriptDetailPanel.updateData(annotationInfo);
+                tabPanel.getTabWidget(1).getParent().setVisible(true);
+                exonDetailPanel.updateData(annotationInfo,selectedAnnotationInfo);
+                break;
+            case "transposable_element":
+            case "repeat_region":
+                repeatRegionDetailPanel.updateData(annotationInfo);
+                tabPanel.getTabWidget(1).getParent().setVisible(false);
+                break;
+            default:
+                GWT.log("not sure what to do with " + type);
+        }
+        // if the current selected tb is not visible then select the first one
+        if (tabPanel.getSelectedIndex() != 0 && !tabPanel.getTabWidget(1).getParent().isVisible()) {
+            tabPanel.selectTab(0);
+        }
+
+    }
+
+    public static void fireAnnotationInfoChangeEvent(AnnotationInfo annotationInfo) {
+        // this method is for firing AnnotationInfoChangeEvent for single level features such as transposable_element and repeat_region
+        AnnotationInfoChangeEvent annotationInfoChangeEvent = new AnnotationInfoChangeEvent(annotationInfo, AnnotationInfoChangeEvent.Action.SET_FOCUS);
+        Annotator.eventBus.fireEvent(annotationInfoChangeEvent);
+    }
+
+    public void toggleOpen(int index, AnnotationInfo annotationInfo) {
+        if (showingTranscripts.contains(annotationInfo.getUniqueName())) {
+            showingTranscripts.remove(annotationInfo.getUniqueName());
+        } else {
+            showingTranscripts.add(annotationInfo.getUniqueName());
+        }
+
+        // Redraw the modified row.
+        dataGrid.redrawRow(index);
+    }
+
+    private void initializeTable() {
+        // View friends.
+        SafeHtmlRenderer<String> anchorRenderer = new AbstractSafeHtmlRenderer<String>() {
+            @Override
+            public SafeHtml render(String object) {
+                SafeHtmlBuilder sb = new SafeHtmlBuilder();
+                sb.appendHtmlConstant("<a href=\"javascript:;\">").appendEscaped(object)
+                        .appendHtmlConstant("</a>");
+                return sb.toSafeHtml();
+            }
+        };
+
+
+        nameColumn = new Column<AnnotationInfo, String>(new ClickableTextCell(anchorRenderer)) {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getName();
+            }
+        };
+        nameColumn.setSortable(true);
+
+        showHideColumn = new Column<AnnotationInfo, String>(new ClickableTextCell(anchorRenderer)) {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+                if (annotationInfo.getType().equals("gene") || annotationInfo.getType().equals("pseudogene")) {
+                    SafeHtmlBuilder sb = new SafeHtmlBuilder();
+                    if(showingTranscripts.contains(annotationInfo.getUniqueName())){
+                        sb.appendHtmlConstant(COLLAPSE_ICON_UNICODE);
+                    }
+                    else{
+                        sb.appendHtmlConstant(EXPAND_ICON_UNICODE);
+                    }
+
+                    return sb.toSafeHtml().asString();
+                }
+                return " ";
+            }
+        };
+        showHideColumn.setSortable(false);
+
+        showHideColumn.setFieldUpdater(new FieldUpdater<AnnotationInfo, String>() {
+            @Override
+            public void update(int index, AnnotationInfo annotationInfo, String value) {
+                toggleOpen(index, annotationInfo);
+            }
+        });
+
+        sequenceColumn = new TextColumn<AnnotationInfo>() {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getSequence();
+            }
+        };
+        sequenceColumn.setSortable(true);
+        sequenceColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
+
+        typeColumn = new TextColumn<AnnotationInfo>() {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+
+                String type = annotationInfo.getType();
+                switch (type) {
+                    case "repeat_region":
+                        return "repeat rgn";
+                    case "transposable_element":
+                        return "transp elem";
+                    default:
+                        return type;
+                }
+            }
+        };
+        typeColumn.setSortable(false);
+        typeColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
+
+        lengthColumn = new Column<AnnotationInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getLength();
+            }
+        };
+        lengthColumn.setSortable(true);
+        lengthColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
+        lengthColumn.setCellStyleNames("dataGridLastColumn");
+
+        // unused?
+        dateColumn = new Column<AnnotationInfo, String>(new TextCell()) {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getDate();
+            }
+        };
+        dateColumn.setSortable(true);
+        dateColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
+        dateColumn.setCellStyleNames("dataGridLastColumn");
+
+        dataGrid.addDomHandler(new DoubleClickHandler() {
+            @Override
+            public void onDoubleClick(DoubleClickEvent event) {
+                AnnotationInfo annotationInfo = singleSelectionModel.getSelectedObject();
+                int index = dataGrid.getKeyboardSelectedRow();
+                index += pager.getPage() * pager.getPageSize();
+                toggleOpen(index, annotationInfo);
+
+            }
+        }, DoubleClickEvent.getType());
+
+        singleSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                selectedAnnotationInfo = singleSelectionModel.getSelectedObject();
+                if (selectedAnnotationInfo != null) {
+                    exonDetailPanel.updateData(selectedAnnotationInfo);
+                    gotoAnnotation.setEnabled(true);
+                } else {
+                    exonDetailPanel.updateData();
+                    gotoAnnotation.setEnabled(false);
+                }
+            }
+        });
+
+        dataGrid.addColumn(nameColumn, "Name");
+        dataGrid.addColumn(sequenceColumn, "Seq");
+        dataGrid.addColumn(typeColumn, "Type");
+        dataGrid.addColumn(lengthColumn, "Length");
+        dataGrid.addColumn(dateColumn, "Updated");
+        dataGrid.addColumn(showHideColumn, "");
+        dataGrid.setColumnWidth(0, 75, Unit.PCT);
+        dataGrid.setColumnWidth(1, 25, Unit.PCT);
+        dataGrid.setColumnWidth(2, 45.0, Unit.PX);
+        dataGrid.setColumnWidth(3, 65.0, Unit.PX);
+        dataGrid.setColumnWidth(4, 100.0, Unit.PX);
+        dataGrid.setColumnWidth(5, 30.0, Unit.PX);
+
+        dataGrid.setSelectionModel(singleSelectionModel);
+    }
+
+    private String getType(JSONObject internalData) {
+        return internalData.get("type").isObject().get("name").isString().stringValue();
+    }
+
+    public void reload(Boolean forceReload) {
+        if (MainPanel.annotatorPanel.isVisible() || forceReload) {
+//            updateAnnotationInfo(null);
+            hideDetailPanels();
+            pager.setPageStart(0);
+            dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+        }
+    }
+
+    public void reload() {
+        reload(false);
+    }
+
+
+    @UiHandler(value = {"typeList", "userField"})
+    public void searchType(ChangeEvent changeEvent) {
+        reload();
+    }
+
+    @UiHandler("nameSearchBox")
+    public void searchName(KeyUpEvent keyUpEvent) {
+        reload();
+    }
+
+    @UiHandler("showAllSequences")
+    public void setShowAllSequences(ClickEvent clickEvent) {
+        sequenceList.setText("");
+        reload();
+    }
+
+    @UiHandler("gotoAnnotation")
+    void gotoAnnotation(ClickEvent clickEvent) {
+        Integer min = selectedAnnotationInfo.getMin() - 50;
+        Integer max = selectedAnnotationInfo.getMax() + 50;
+        min = min < 0 ? 0 : min;
+        MainPanel.updateGenomicViewerForLocation(selectedAnnotationInfo.getSequence(), min, max, false,false);
+    }
+
+
+    private static AnnotationInfo getChildAnnotation(AnnotationInfo annotationInfo, String uniqueName) {
+        for (AnnotationInfo childAnnotation : annotationInfo.getAnnotationInfoSet()) {
+            if (childAnnotation.getUniqueName().equalsIgnoreCase(uniqueName)) {
+                return childAnnotation;
+            }
+        }
+        return null;
+    }
+
+
+    // used by javascript function
+    public void enableGoto(int geneIndex, String uniqueName) {
+        AnnotationInfo annotationInfo = dataGrid.getVisibleItem(Math.abs(dataGrid.getVisibleRange().getStart() - geneIndex));
+        selectedAnnotationInfo = getChildAnnotation(annotationInfo, uniqueName);
+        exonDetailPanel.updateData(selectedAnnotationInfo);
+        updateAnnotationInfo(selectedAnnotationInfo);
+        gotoAnnotation.setEnabled(true);
+        selectedChildUniqueName = selectedAnnotationInfo.getUniqueName();
+    }
+
+    // used by javascript function
+    public void displayTranscript(int geneIndex, String uniqueName) {
+
+        // for some reason doesn't like call enableGoto
+        AnnotationInfo annotationInfo = dataGrid.getVisibleItem(Math.abs(dataGrid.getVisibleRange().getStart() - geneIndex));
+        selectedAnnotationInfo = getChildAnnotation(annotationInfo, uniqueName);
+        exonDetailPanel.updateData(selectedAnnotationInfo);
+        gotoAnnotation.setEnabled(true);
+        selectedChildUniqueName = selectedAnnotationInfo.getUniqueName();
+
+        // for some reason doesn't like call gotoAnnotation
+        Integer min = selectedAnnotationInfo.getMin() - 50;
+        Integer max = selectedAnnotationInfo.getMax() + 50;
+        min = min < 0 ? 0 : min;
+        MainPanel.updateGenomicViewerForLocation(selectedAnnotationInfo.getSequence(), min, max, false,false);
+    }
+
+    // also used by javascript function
+    public void displayFeature(int featureIndex) {
+        AnnotationInfo annotationInfo = dataGrid.getVisibleItem(Math.abs(dataGrid.getVisibleRange().getStart() - featureIndex));
+        String type = annotationInfo.getType();
+        if (type.equals("transposable_element") || type.equals("repeat_region")) {
+            // do nothing
+        }
+        else {
+            exonDetailPanel.updateData(annotationInfo);
+        }
+        gotoAnnotation.setEnabled(true);
+        Integer min = selectedAnnotationInfo.getMin() - 50;
+        Integer max = selectedAnnotationInfo.getMax() + 50;
+        min = min < 0 ? 0 : min;
+        MainPanel.updateGenomicViewerForLocation(selectedAnnotationInfo.getSequence(), min, max, false,false);
+    }
+
+    public static native void exportStaticMethod(AnnotatorPanel annotatorPanel) /*-{
+        var that = this;
+        $wnd.displayTranscript = $entry(annotatorPanel. at org.bbop.apollo.gwt.client.AnnotatorPanel::displayTranscript(ILjava/lang/String;));
+        $wnd.displayFeature = $entry(annotatorPanel. at org.bbop.apollo.gwt.client.AnnotatorPanel::displayFeature(I));
+        $wnd.enableGoto = $entry(annotatorPanel. at org.bbop.apollo.gwt.client.AnnotatorPanel::enableGoto(ILjava/lang/String;));
+//        $wnd.showInAnnotatorPanel = $entry(@org.bbop.apollo.gwt.client.AnnotatorPanel::showInAnnotatorPanel(Ljava/lang/String;Ljava/lang/String;));
+    }-*/;
+
+    private class CustomTableBuilder extends AbstractCellTableBuilder<AnnotationInfo> {
+
+        public CustomTableBuilder() {
+            super(dataGrid);
+        }
+
+
+        @Override
+        protected void buildRowImpl(AnnotationInfo rowValue, int absRowIndex) {
+            buildAnnotationRow(rowValue, absRowIndex, false);
+
+            if (showingTranscripts.contains(rowValue.getUniqueName())) {
+                // add some random rows
+                Set<AnnotationInfo> annotationInfoSet = rowValue.getAnnotationInfoSet();
+                if (annotationInfoSet.size() > 0) {
+                    for (AnnotationInfo annotationInfo : annotationInfoSet) {
+                        buildAnnotationRow(annotationInfo, absRowIndex, true);
+                    }
+                }
+            }
+        }
+
+        private void buildAnnotationRow(final AnnotationInfo rowValue, int absRowIndex, boolean showTranscripts) {
+
+            TableRowBuilder row = startRow();
+            TableCellBuilder td = row.startTD();
+
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+            if (showTranscripts) {
+                // TODO: this is ugly, but it works
+                // a custom cell rendering might work as well, but not sure
+
+                String transcriptStyle = "margin-left: 10px; color: green; padding-left: 5px; padding-right: 5px; border-radius: 15px; background-color: #EEEEEE;";
+                String htmlString = "<a style='" + transcriptStyle + "' onclick=\"enableGoto(" + absRowIndex + ",'" + rowValue.getUniqueName() + "');\">" + rowValue.getName() + "</a>";
+                htmlString += "  <button type='button' class='btn btn-primary' onclick=\"displayTranscript(" + absRowIndex + ",'" + rowValue.getUniqueName() + "')\" style=\"line-height: 0; margin-bottom: 5px;\" ><i class='fa fa-arrow-circle-o-right fa-lg'></i></a>";
+                HTML html = new HTML(htmlString);
+                SafeHtml safeHtml = new SafeHtmlBuilder().appendHtmlConstant(html.getHTML()).toSafeHtml();
+                td.html(safeHtml);
+            } else {
+                String type = rowValue.getType();
+                if (type.equals("gene") || type.equals("pseudogene")) {
+                    renderCell(td, createContext(0), nameColumn, rowValue);
+                }
+                else {
+                    // handles singleton features
+                    String featureStyle = "color: #800080;";
+                    HTML html = new HTML("<a style='" + featureStyle + "' ondblclick=\"displayFeature(" + absRowIndex + ")\");\">" + rowValue.getName() + "</a>");
+                    SafeHtml htmlString = new SafeHtmlBuilder().appendHtmlConstant(html.getHTML()).toSafeHtml();
+                    td.html(htmlString);
+                }
+            }
+            td.endTD();
+
+            // Sequence column.
+            td = row.startTD();
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+            if (showTranscripts) {
+                DivBuilder div = td.startDiv();
+                div.style().trustedColor("green").endStyle();
+                td.endDiv();
+            } else {
+                renderCell(td, createContext(1), sequenceColumn, rowValue);
+            }
+            td.endTD();
+
+            // Type column.
+            td = row.startTD();
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+            if (showTranscripts) {
+                DivBuilder div = td.startDiv();
+                div.style().trustedColor("green").endStyle();
+                div.text(rowValue.getType());
+                td.endDiv();
+            } else {
+                renderCell(td, createContext(1), typeColumn, rowValue);
+            }
+            td.endTD();
+
+
+            // Length column.
+            td = row.startTD();
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+            if (showTranscripts) {
+                DivBuilder div = td.startDiv();
+                div.style().trustedColor("green").endStyle();
+                div.text(NumberFormat.getDecimalFormat().format(rowValue.getLength()));
+                td.endDiv();
+                td.endTD();
+
+            } else {
+                td.text(NumberFormat.getDecimalFormat().format(rowValue.getLength())).endTD();
+            }
+
+            // Date column
+            td = row.startTD();
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+            if (showTranscripts) {
+                DivBuilder div = td.startDiv();
+                div.style().trustedColor("green").endStyle();
+                Date date = new Date(Long.parseLong(rowValue.getDate()));
+                div.text(outputFormat.format(date));
+                td.endDiv();
+            } else {
+                Date date = new Date(Long.parseLong(rowValue.getDate()));
+                td.text(outputFormat.format(date));
+            }
+            td.endTD();
+
+            // this is the "warning" column, which isn't used
+            td = row.startTD();
+            td.style().outlineStyle(Style.OutlineStyle.NONE).endStyle();
+
+            renderCell(td, createContext(4), showHideColumn, rowValue);
+
+            td.endTD();
+
+            row.endTR();
+        }
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.ui.xml
new file mode 100644
index 0000000..e8ccb61
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/AnnotatorPanel.ui.xml
@@ -0,0 +1,175 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:apollo="urn:import:org.bbop.apollo.gwt.client"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+>
+    <ui:style>
+        .northPanel {
+            /*padding-right: 0px;*/
+            margin: 5px;
+        }
+
+        .pager {
+            width: 100%;
+            margin-left: auto;
+            margin-right: auto;
+        }
+
+        .typeList {
+            margin-left: 10px;
+        }
+
+        .searchBox {
+            /*margin-left: 20px;*/
+            display: inline;
+            /*margin-bottom: 10px;*/
+        }
+
+        .widgetBox {
+            display: inline;
+            margin: 5px;
+        }
+
+        .rowPadding {
+            margin-bottom: 40px;
+        }
+
+        .dataGrid td {
+            word-wrap: break-word;
+        }
+
+        .assemblage-buttons {
+            margin-right: 10px;
+        }
+    </ui:style>
+    <gwt:DockLayoutPanel ui:field="splitPanel">
+        <gwt:north size="120">
+            <b:Container fluid="true" styleName="{style.northPanel}" ui:field="northPanelContainer">
+                <!--<b:Row styleName="{style.rowPadding}">-->
+                    <!--<b:Column size="MD_6">-->
+                        <!--<b:TextBox placeholder="Annotation Name"-->
+                                   <!--ui:field="nameSearchBox" addStyleNames="{style.widgetBox}"/>-->
+                    <!--</b:Column>-->
+                    <!--<b:Column size="MD_5">-->
+                        <!--<b:ListBox ui:field="typeList"  addStyleNames="{style.widgetBox}"/>-->
+                    <!--</b:Column>-->
+                    <!--<b:Column size="MD_6">-->
+                        <!--<b:SuggestBox ui:field="sequenceList" addStyleNames="{style.widgetBox}"/>-->
+                    <!--</b:Column>-->
+                <!--<!–</b:Row>–>-->
+                <!--<!–<b:Row styleName="{style.rowPadding}">–>-->
+                    <!--<b:Column size="MD_5">-->
+                        <!--<b:ListBox ui:field="userField" addStyleNames="{style.widgetBox}"/>-->
+                    <!--</b:Column>-->
+                <!--</b:Row>-->
+
+                <b:Row styleName="{style.rowPadding}">
+                    <b:Column size="MD_6">
+                        <b:TextBox placeholder="Annotation Name"
+                                   ui:field="nameSearchBox" addStyleNames="{style.widgetBox}"/>
+                    </b:Column>
+                    <b:Column size="MD_3">
+                        <b:ListBox ui:field="typeList" addStyleNames="{style.widgetBox}"/>
+                    </b:Column>
+                </b:Row>
+                <b:Row styleName="{style.rowPadding}">
+                    <b:Column size="MD_5">
+                        <b:SuggestBox ui:field="sequenceList" addStyleNames="{style.widgetBox}"/>
+                    </b:Column>
+                    <b:Column size="MD_1">
+                        <b:Button text="All" ui:field="showAllSequences" styleName="{style.widgetBox}"/>
+                    </b:Column>
+                    <b:Column size="MD_3">
+                        <b:ListBox ui:field="userField" addStyleNames="{style.widgetBox}"/>
+                    </b:Column>
+                    <b:Column size="MD_4">
+                        <!--<gwt:SuggestBox ui:field="sequenceList" width="200px"/>-->
+                        <!--<b:Button type="PRIMARY" icon="BOOKMARK" text="+" size="EXTRA_SMALL" title="Add new assemblage"-->
+                                  <!--ui:field="addNewAssemblage" enabled="false"-->
+                                  <!--addStyleNames="{style.assemblage-buttons}"/>-->
+
+                        <!--<b:ButtonGroup>-->
+                            <!--<b:Button type="PRIMARY" icon="EYE" text="+" size="EXTRA_SMALL" title="Add to view"-->
+                                      <!--ui:field="addToView" enabled="false" addStyleNames="{style.assemblage-buttons}"/>-->
+                            <!--<b:Button type="PRIMARY" icon="EYE" title="View" size="EXTRA_SMALL"-->
+                                      <!--ui:field="viewAnnotation" enabled="false"-->
+                                      <!--addStyleNames="{style.assemblage-buttons}"/>-->
+                        <!--</b:ButtonGroup>-->
+                        <b:Button type="PRIMARY" icon="ARROW_CIRCLE_O_RIGHT" title="Go To" iconSize="LARGE" text="Go to Annotation"
+                                  ui:field="gotoAnnotation" enabled="false" addStyleNames="{style.assemblage-buttons}"/>
+                    </b:Column>
+                </b:Row>
+            </b:Container>
+        </gwt:north>
+        <gwt:center>
+            <gwt:DockLayoutPanel>
+                <gwt:north size="30">
+                    <gwt:HTMLPanel>
+                        <table style="width:100%">
+                            <tr>
+                                <td style="width:20%">
+                                    <!--<b:Button ui:field="selectSelectedButton" size="EXTRA_SMALL" enabled="false" icon="CHECK_CIRCLE" marginLeft="10"/>-->
+                                </td>
+                                <td align="center">
+                                    <apollo:WebApolloSimplePager ui:field="pager" styleName="{style.pager}"/>
+                                </td>
+                                <td style="width:20%"/>
+                            </tr>
+                        </table>
+                    </gwt:HTMLPanel>
+                </gwt:north>
+                <gwt:center>
+                    <cellview:DataGrid ui:field="dataGrid" styleName="{style.dataGrid}"/>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:center>
+        <gwt:south size="280">
+            <gwt:TabLayoutPanel barHeight="35" ui:field="tabPanel">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row>
+                            <apollo:GeneDetailPanel ui:field="geneDetailPanel" visible="false"/>
+                            <apollo:TranscriptDetailPanel ui:field="transcriptDetailPanel" visible="false"/>
+                            <apollo:RepeatRegionDetailPanel ui:field="repeatRegionDetailPanel" visible="false"/>
+                            <!--<apollo:ExonDetailPanel ui:field="exonDetailPanel" visible="false"/>-->
+                            <!--<apollo:CDSDetailPanel ui:field="cdsDetailPanel" visible="false"/>-->
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Coding</gwt:header>
+                    <apollo:ExonDetailPanel ui:field="exonDetailPanel"/>
+                </gwt:tab>
+
+                <!--TODO: 2.1-->
+                <!--<gwt:tab>-->
+                <!--<gwt:header>DbXref</gwt:header>-->
+                <!--<gwt:HTML text="dbxref"/>-->
+                <!--</gwt:tab>-->
+                <!--<gwt:tab>-->
+                <!--<gwt:header>PubMed</gwt:header>-->
+                <!--<gwt:HTML text="pubmed stuf"/>-->
+                <!--</gwt:tab>-->
+                <!--<gwt:tab>-->
+                <!--<gwt:header>Attributes</gwt:header>-->
+                <!--<gwt:HTML text="attributes"/>-->
+                <!--</gwt:tab>-->
+                <!--<gwt:tab>-->
+                <!--<gwt:header>GO Evidence</gwt:header>-->
+                <!--<gwt:HTML text="go evidence"/>-->
+                <!--</gwt:tab>-->
+                <!--<gwt:tab>-->
+                <!--<gwt:header>Comments</gwt:header>-->
+                <!--<gwt:HTML text="comments"/>-->
+                <!--</gwt:tab>-->
+
+            </gwt:TabLayoutPanel>
+            <!--</gwt:VerticalPanel>-->
+        </gwt:south>
+    </gwt:DockLayoutPanel>
+    <!--<gwt:HTMLPanel>-->
+
+    <!--</gwt:HTMLPanel>-->
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/ErrorDialog.java b/src/gwt/org/bbop/apollo/gwt/client/ErrorDialog.java
new file mode 100644
index 0000000..3331ca0
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/ErrorDialog.java
@@ -0,0 +1,57 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.ui.*;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.gwtbootstrap3.client.ui.Modal;
+import org.gwtbootstrap3.client.ui.ModalBody;
+import org.gwtbootstrap3.client.ui.constants.ModalBackdrop;
+
+/**
+ * Created by ndunn on 3/17/15.
+ */
+//  TODO: this needs to be moved into UIBinder into its own class
+public class ErrorDialog extends Modal{
+
+    Button logoutButton;
+
+    public ErrorDialog(String title,String message,boolean showOnConstruct, boolean closeModal) {
+        this(title,message,showOnConstruct,closeModal,false);
+    }
+
+    public ErrorDialog(String title,String message,boolean showOnConstruct, boolean closeModal, boolean showLogoutButton){
+        setTitle(title);
+        setClosable(closeModal);
+        setFade(true);
+        setDataBackdrop(ModalBackdrop.STATIC);
+
+        if(message!=null){
+            HTML content = new HTML(message);
+            ModalBody modalBody = new ModalBody();
+            modalBody.add(content);
+
+            if(showLogoutButton) {
+
+                logoutButton=new Button("Logout", new ClickHandler() {
+                    public void onClick(ClickEvent event) {
+                        UserRestService.logout();
+                    }
+                });
+
+                modalBody.add(logoutButton);
+            }
+            add( modalBody );
+        }
+
+
+
+        if(showOnConstruct){
+            show();
+        }
+    }
+}
+
diff --git a/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.java b/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.java
new file mode 100644
index 0000000..37d2ef6
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.java
@@ -0,0 +1,411 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.ColumnSortEvent;
+import com.google.gwt.user.cellview.client.DataGrid;
+import com.google.gwt.user.cellview.client.TextColumn;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.ListDataProvider;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SingleSelectionModel;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.AnnotationRestService;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.Container;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.client.ui.constants.ButtonType;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/9/15.
+ */
+public class ExonDetailPanel extends Composite {
+
+
+    interface ExonDetailPanelUiBinder extends UiBinder<Widget, ExonDetailPanel> {
+    }
+
+    int inputFmin, inputFmax;
+    int fivePrimeValue, threePrimeValue;
+    private AnnotationInfo internalAnnotationInfo;
+    private AnnotationInfo annotationInfoWithTopLevelFeature;
+    private static ExonDetailPanelUiBinder ourUiBinder = GWT.create(ExonDetailPanelUiBinder.class);
+
+    @UiField
+    Button positiveStrandValue;
+    @UiField
+    Button negativeStrandValue;
+    @UiField
+    TextBox fivePrimeField;
+    @UiField
+    TextBox threePrimeField;
+    @UiField
+    Button increaseFivePrime;
+    @UiField
+    Button decreaseFivePrime;
+    @UiField
+    Button increaseThreePrime;
+    @UiField
+    Button decreaseThreePrime;
+    @UiField
+    Container exonEditContainer;
+    DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+    @UiField(provided = true)
+    DataGrid<AnnotationInfo> dataGrid = new DataGrid<>(200, tablecss);
+    @UiField
+    HTML notePanel;
+    private static ListDataProvider<AnnotationInfo> dataProvider = new ListDataProvider<>();
+    private static List<AnnotationInfo> annotationInfoList = dataProvider.getList();
+    private SingleSelectionModel<AnnotationInfo> selectionModel = new SingleSelectionModel<>();
+
+    private TextColumn<AnnotationInfo> typeColumn;
+    private Column<AnnotationInfo, Number> startColumn;
+    private Column<AnnotationInfo, Number> stopColumn;
+    private Column<AnnotationInfo, Number> lengthColumn;
+
+    private Boolean editable = false;
+
+    public ExonDetailPanel() {
+        dataGrid.setWidth("100%");
+        initializeTable();
+        dataProvider.addDataDisplay(dataGrid);
+        dataGrid.setSelectionModel(selectionModel);
+        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                if (selectionModel.getSelectedSet().isEmpty()) {
+                    exonEditContainer.setVisible(false);
+                } else {
+                    exonEditContainer.setVisible(true);
+                    updateDetailData(selectionModel.getSelectedObject());
+                }
+            }
+        });
+
+
+        initWidget(ourUiBinder.createAndBindUi(this));
+    }
+
+    private void initializeTable() {
+        typeColumn = new TextColumn<AnnotationInfo>() {
+            @Override
+            public String getValue(AnnotationInfo annotationInfo) {
+                String annotationTypeString = annotationInfo.getType();
+                if (annotationTypeString.equals("non_canonical_five_prime_splice_site")) {
+                    annotationTypeString = "NC 5' splice";
+                } else if (annotationTypeString.equals("non_canonical_three_prime_splice_site")) {
+                    annotationTypeString = "NC 3' splice";
+                }
+                return annotationTypeString;
+            }
+        };
+        typeColumn.setSortable(true);
+
+        startColumn = new Column<AnnotationInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(AnnotationInfo annotationInfo) {
+                return getDisplayMin(annotationInfo.getMin());
+            }
+        };
+        startColumn.setSortable(true);
+
+        stopColumn = new Column<AnnotationInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getMax();
+            }
+        };
+        stopColumn.setSortable(true);
+
+        lengthColumn = new Column<AnnotationInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(AnnotationInfo annotationInfo) {
+                return annotationInfo.getLength();
+            }
+        };
+        lengthColumn.setSortable(true);
+
+        dataGrid.addColumn(typeColumn, "Type");
+        dataGrid.addColumn(startColumn, "Start");
+//        dataGrid.addColumn(stopColumn, "Stop");
+        dataGrid.addColumn(lengthColumn, "Length");
+
+        ColumnSortEvent.ListHandler<AnnotationInfo> sortHandler = new ColumnSortEvent.ListHandler<AnnotationInfo>(annotationInfoList);
+        dataGrid.addColumnSortHandler(sortHandler);
+
+        sortHandler.setComparator(typeColumn, new Comparator<AnnotationInfo>() {
+            @Override
+            public int compare(AnnotationInfo o1, AnnotationInfo o2) {
+                return o1.getType().compareTo(o2.getType());
+            }
+        });
+
+        sortHandler.setComparator(startColumn, new Comparator<AnnotationInfo>() {
+            @Override
+            public int compare(AnnotationInfo o1, AnnotationInfo o2) {
+                return o1.getMin() - o2.getMin();
+            }
+        });
+
+        sortHandler.setComparator(stopColumn, new Comparator<AnnotationInfo>() {
+            @Override
+            public int compare(AnnotationInfo o1, AnnotationInfo o2) {
+                return o1.getMax() - o2.getMax();
+            }
+        });
+
+        sortHandler.setComparator(lengthColumn, new Comparator<AnnotationInfo>() {
+            @Override
+            public int compare(AnnotationInfo o1, AnnotationInfo o2) {
+                return o1.getLength() - o2.getLength();
+            }
+        });
+    }
+
+    public boolean updateData() {
+        return updateData(null,null);
+    }
+
+    public boolean updateData(AnnotationInfo annotationInfo) {
+        return updateData(annotationInfo,null);
+    }
+
+    public boolean updateData(AnnotationInfo annotationInfo,AnnotationInfo selectedAnnotationInfo) {
+        if (annotationInfo == null) {
+            return false;
+        }
+        exonEditContainer.setVisible(false);
+        //displayAnnotationInfo(annotationInfo);
+        getAnnotationInfoWithTopLevelFeature(annotationInfo);
+        annotationInfoList.clear();
+        GWT.log("sublist: " + annotationInfo.getAnnotationInfoSet().size());
+        for (AnnotationInfo annotationInfo1 : annotationInfo.getAnnotationInfoSet()) {
+            GWT.log("adding: " + annotationInfo1.getName());
+            annotationInfoList.add(annotationInfo1);
+        }
+
+        dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+        if(selectedAnnotationInfo==null){
+            exonEditContainer.setVisible(true);
+            return false ;
+        }
+
+        return true ;
+    }
+
+    private void updateDetailData(AnnotationInfo annotationInfo) {
+        // updates the detail section (3' and 5' coordinates) when user clicks on any of the types in the table.
+        // mRNA information is not available
+        this.internalAnnotationInfo = annotationInfo;
+        GWT.log("updating exon detail panel");
+        coordinatesToPrime(annotationInfo.getMin(), annotationInfo.getMax());
+        if (internalAnnotationInfo.getStrand() > 0) {
+            positiveStrandValue.setType(ButtonType.PRIMARY);
+            negativeStrandValue.setType(ButtonType.DEFAULT);
+        } else {
+            positiveStrandValue.setType(ButtonType.DEFAULT);
+            negativeStrandValue.setType(ButtonType.PRIMARY);
+        }
+
+        String type = this.internalAnnotationInfo.getType();
+        if (type.equals("exon")) {
+            enableFields(true);
+        }
+        else {
+            enableFields(false);
+        }
+
+        SafeHtmlBuilder safeHtmlBuilder = new SafeHtmlBuilder();
+        for (String note : annotationInfo.getNoteList()) {
+            safeHtmlBuilder.appendHtmlConstant("<div class='label label-warning'>" + note + "</div>");
+        }
+        notePanel.setHTML(safeHtmlBuilder.toSafeHtml());
+
+        setVisible(true);
+    }
+
+    public void redrawExonTable() {
+        dataGrid.redraw();
+    }
+
+    private void enableFields(boolean enabled) {
+        decreaseFivePrime.setEnabled(enabled);
+        increaseFivePrime.setEnabled(enabled);
+        decreaseThreePrime.setEnabled(enabled);
+        increaseThreePrime.setEnabled(enabled);
+    }
+
+    public void setEditable(boolean editable) {
+        this.editable = editable;
+    }
+
+    private boolean isEditableType(String type) {
+        return type.equals("exon");
+    }
+
+    private void updateFeatureLocation(final AnnotationInfo originalInfo) {
+        final AnnotationInfo updatedInfo = this.internalAnnotationInfo;
+        enableFields(false);
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+//                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+
+//                GWT.log("return value: " + returnValue.toString());
+                enableFields(true);
+
+                Annotator.eventBus.fireEvent(new AnnotationInfoChangeEvent(updatedInfo, AnnotationInfoChangeEvent.Action.UPDATE));
+                updateDetailData(updatedInfo);
+                redrawExonTable();
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                //todo: handling different types of errors
+                Bootbox.alert("Error updating exon: " + exception.toString());
+                coordinatesToPrime(originalInfo.getMin(), originalInfo.getMax());
+                enableFields(true);
+            }
+        };
+        RestService.sendRequest(requestCallback, "annotator/setExonBoundaries/", AnnotationRestService.convertAnnotationInfoToJSONObject(this.internalAnnotationInfo));
+    }
+
+    private int getDisplayMin(int min) {
+        // increases the fmin by 1 for display since coordinates are handled as zero-based on server-side
+        return min + 1;
+    }
+
+    private boolean collectFieldValues(int threePrimeDelta, int fivePrimeDelta) {
+        try {
+            fivePrimeValue = Integer.parseInt(fivePrimeField.getText()) + fivePrimeDelta;
+            threePrimeValue = Integer.parseInt(threePrimeField.getText()) + threePrimeDelta;
+        } catch (Exception error) {
+            coordinatesToPrime(this.internalAnnotationInfo.getMin(), this.internalAnnotationInfo.getMax());
+            return false;
+        }
+        return true;
+    }
+
+
+    private void trasformOperation() {
+        if (verifyOperation()) {
+            triggerUpdate(fivePrimeValue, threePrimeValue);
+        } else {
+            coordinatesToPrime(this.internalAnnotationInfo.getMin(), this.internalAnnotationInfo.getMax());
+        }
+    }
+
+    private void handleExonUpdates() {
+        handleExonUpdates(0, 0);
+    }
+
+    private void handleExonUpdates(int threePrimeDelta, int fivePrimeDelta) {
+        if (!collectFieldValues(threePrimeDelta, fivePrimeDelta)) {
+            return;
+        }
+        trasformOperation();
+    }
+
+    @UiHandler("decreaseFivePrime")
+    public void decreaseFivePrimePosition(ClickEvent e) {
+        handleExonUpdates(0, -1);
+    }
+
+    @UiHandler("increaseFivePrime")
+    public void increaseFivePrimePosition(ClickEvent e) {
+        handleExonUpdates(0, 1);
+    }
+
+    @UiHandler("decreaseThreePrime")
+    public void decreaseThreePrime(ClickEvent e) {
+        handleExonUpdates(-1, 0);
+    }
+
+    @UiHandler("increaseThreePrime")
+    public void increaseThreePrime(ClickEvent e) {
+        handleExonUpdates(1, 0);
+    }
+
+
+    @UiHandler(value = {"fivePrimeField", "threePrimeField"})
+    public void fivePrimeTextEntry(KeyDownEvent k) {
+        if (k.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+            handleExonUpdates();
+        }
+    }
+
+    @UiHandler(value = {"fivePrimeField", "threePrimeField"})
+    public void fivePrimeTextFocus(BlurEvent b) {
+        handleExonUpdates();
+    }
+
+
+    private boolean verifyOperation() {
+        if (!isEditableType(this.internalAnnotationInfo.getType())) {
+            return false;
+        }
+        primeToCoordinates(this.fivePrimeValue, this.threePrimeValue);
+        if (!(this.inputFmin < this.internalAnnotationInfo.getMax()) || !(this.inputFmax > this.internalAnnotationInfo.getMin())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void triggerUpdate(int fivePrimeValue, int threePrimeValue) {
+        final AnnotationInfo originalInfo = this.internalAnnotationInfo;
+        fivePrimeField.setText(Integer.toString(fivePrimeValue));
+        this.internalAnnotationInfo.setMin(this.inputFmin);
+        threePrimeField.setText(Integer.toString(threePrimeValue));
+        this.internalAnnotationInfo.setMax(this.inputFmax);
+        updateFeatureLocation(originalInfo);
+    }
+
+    private void primeToCoordinates(int fivePrimeFieldValue, int threePrimeFieldValue) {
+        if (this.internalAnnotationInfo.getStrand() == 1) {
+            this.inputFmin = fivePrimeFieldValue - 1;
+            this.inputFmax = threePrimeFieldValue;
+        } else {
+            this.inputFmin = threePrimeFieldValue - 1;
+            this.inputFmax = fivePrimeFieldValue;
+        }
+    }
+
+    private void coordinatesToPrime(int fmin, int fmax) {
+        if (this.internalAnnotationInfo.getStrand() == 1) {
+            this.fivePrimeField.setText(Integer.toString(fmin + 1));
+            this.threePrimeField.setText(Integer.toString(fmax));
+        } else {
+            this.fivePrimeField.setText(Integer.toString(fmax));
+            this.threePrimeField.setText(Integer.toString(fmin + 1));
+        }
+    }
+
+    private void getAnnotationInfoWithTopLevelFeature(AnnotationInfo annotationInfo) {
+        this.annotationInfoWithTopLevelFeature = annotationInfo;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.ui.xml
new file mode 100644
index 0000000..5c9f537
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/ExonDetailPanel.ui.xml
@@ -0,0 +1,89 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+        >
+    <ui:style>
+
+        .container {
+            margin-left: 20px;
+            margin-top: 10px;
+        }
+
+        .widgetPanel {
+            display: inline-table;
+            margin-right: 10px;
+            margin-bottom: 5px;
+        }
+
+        .row {
+            margin-left: 5px;
+            margin-right: 5px;
+        }
+        </ui:style>
+
+    <gwt:SplitLayoutPanel>
+        <gwt:west size="250">
+            <cellview:DataGrid ui:field="dataGrid"/>
+        </gwt:west>
+        <gwt:center>
+            <b:Container ui:field="exonEditContainer" fluid="true" width="100%" styleName="{style.container}" visible="false">
+                <b:Row styleName="{style.row}">
+                    <b:Column size="MD_4" styleName="{style.widgetPanel}">
+                        <b:InputGroup>
+                            <b:InputGroupAddon>5' End</b:InputGroupAddon>
+                            <b:InputGroupButton>
+                                <b:Button ui:field="decreaseFivePrime" icon="ARROW_LEFT"/>
+                            </b:InputGroupButton>
+                            <b:TextBox ui:field="fivePrimeField" text="-"/>
+                            <b:InputGroupButton>
+                                <b:Button ui:field="increaseFivePrime" icon="ARROW_RIGHT"/>
+                            </b:InputGroupButton>
+                        </b:InputGroup>
+                    </b:Column>
+                </b:Row>
+                <b:Row styleName="{style.row}">
+                    <b:Column size="MD_4" styleName="{style.widgetPanel}">
+                        <b:InputGroup>
+                            <b:InputGroupAddon>3' End</b:InputGroupAddon>
+                            <b:InputGroupButton>
+                                <b:Button ui:field="decreaseThreePrime" icon="ARROW_LEFT"/>
+                            </b:InputGroupButton>
+                            <b:TextBox ui:field="threePrimeField" text="-"/>
+                            <b:InputGroupButton>
+                                <b:Button ui:field="increaseThreePrime" icon="ARROW_RIGHT"/>
+                            </b:InputGroupButton>
+                        </b:InputGroup>
+                    </b:Column>
+                </b:Row>
+                <b:Row styleName="{style.row}">
+                    <b:Column size="MD_4" styleName="{style.widgetPanel}">
+                        <b:InputGroup>
+                            <b:InputGroupAddon>Strand</b:InputGroupAddon>
+                            <b:ButtonGroup dataToggle="BUTTONS" ui:field="strand">
+                                <b:Button ui:field="negativeStrandValue" enabled="false" icon="MINUS"/>
+                                <b:Button ui:field="positiveStrandValue"  enabled="false" icon="PLUS"/>
+                            </b:ButtonGroup>
+                        </b:InputGroup>
+                    </b:Column>
+                </b:Row>
+                <!--<b:Row>-->
+                    <!--<b:Column size="6" styleName="{style.widgetPanel}">-->
+                        <!--<b:InputGroup>-->
+                            <!--<b:InputGroupAddon>Phase</b:InputGroupAddon>-->
+                            <!--<b:ButtonGroup dataToggle="BUTTONS">-->
+                                <!--<b:Button ui:field="phaseButton"  enabled="false" styleName="{style.phaseButton}"/>-->
+                            <!--</b:ButtonGroup>-->
+                        <!--</b:InputGroup>-->
+                    <!--</b:Column>-->
+                <!--</b:Row>-->
+                <b:Row styleName="{style.row}">
+                    <b:Column size="6" styleName="{style.widgetPanel}">
+                        <gwt:HTML ui:field="notePanel"/>
+                    </b:Column>
+                </b:Row>
+            </b:Container>
+        </gwt:center>
+    </gwt:SplitLayoutPanel>
+
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/ExportPanel.java b/src/gwt/org/bbop/apollo/gwt/client/ExportPanel.java
new file mode 100644
index 0000000..5b9c004
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/ExportPanel.java
@@ -0,0 +1,298 @@
+package org.bbop.apollo.gwt.client;
+
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.HTML;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.SequenceInfo;
+import org.bbop.apollo.gwt.client.rest.SequenceRestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.constants.*;
+import org.gwtbootstrap3.client.ui.html.Div;
+import org.gwtbootstrap3.client.ui.html.Paragraph;
+import org.gwtbootstrap3.client.ui.html.Span;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/27/15.
+ */
+public class ExportPanel extends Modal {
+    private String type;
+    private List<SequenceInfo> sequenceList;
+    private Boolean exportAll = false;
+    private OrganismInfo currentOrganismInfo;
+    private Boolean exportAllSequencesToChado = false;
+    HTML sequenceInfoLabel = new HTML();
+    HTML typeLabel = new HTML();
+    HTML sequenceTypeLabel = new HTML();
+    Button closeButton = new Button("Cancel");
+    Button exportButton = new Button("Export");
+    RadioButton gff3Button = new RadioButton("GFF3", "GFF3", true);
+    RadioButton gff3WithFastaButton = new RadioButton("GFF3 with FASTA", "GFF3 with FASTA", true);
+    RadioButton genomicRadioButton = new RadioButton("Genomic", "Genomic", true);
+    RadioButton cdnaRadioButton = new RadioButton("cDNA", "cDNA", true);
+    RadioButton cdsRadioButton = new RadioButton("CDS", "CDS", true);
+    RadioButton peptideRadioButton = new RadioButton("Peptide", "Peptide", true);
+    RadioButton chadoExportButton1 = new RadioButton("chadoExportOption1", "Export all sequences (that have annotations) to Chado", true);
+    RadioButton chadoExportButton2 = new RadioButton("chadoExportOption2", "Export all sequences to Chado", true);
+
+    ModalBody modalBody = new ModalBody();
+    ModalHeader modalHeader = new ModalHeader();
+    ModalFooter modalFooter = new ModalFooter();
+
+    public ExportPanel(OrganismInfo organismInfo, String type, Boolean exportAll, List<SequenceInfo> sequenceInfoList) {
+        setTitle("Export");
+        setClosable(true);
+        setRemoveOnHide(true);
+        setDataBackdrop(ModalBackdrop.FALSE);
+
+        Integer count = exportAll ? -1 : sequenceInfoList.size();
+        String countText = count < 0 ? "all" : count + "";
+        currentOrganismInfo = organismInfo;
+
+        modalHeader.add(new HTML("Export " + countText + " sequence(s) from " + organismInfo.getName() + " as " + type));
+
+
+        add(modalHeader);
+
+
+        ButtonGroup buttonGroup = new ButtonGroup();
+        buttonGroup.setDataToggle(Toggle.BUTTONS);
+        if (type.equals(FeatureStringEnum.TYPE_FASTA.getValue())) {
+            buttonGroup.add(genomicRadioButton);
+            buttonGroup.add(cdnaRadioButton);
+            buttonGroup.add(cdsRadioButton);
+            buttonGroup.add(peptideRadioButton);
+        }
+        else
+        if (type.equals(FeatureStringEnum.TYPE_GFF3.getValue())) {
+            buttonGroup.add(gff3Button);
+            buttonGroup.add(gff3WithFastaButton);
+        }
+        else
+        if (type.equals(FeatureStringEnum.TYPE_CHADO.getValue())) {
+            buttonGroup.add(chadoExportButton1);
+            buttonGroup.add(chadoExportButton2);
+        }
+        modalBody.add(buttonGroup);
+
+        modalBody.add(sequenceTypeLabel);
+
+
+        add(modalBody);
+
+        exportButton.setIcon(IconType.DOWNLOAD);
+        exportButton.setType(ButtonType.PRIMARY);
+        exportButton.setEnabled(false);
+        modalFooter.add(exportButton);
+        modalFooter.add(closeButton);
+        add(modalFooter);
+
+        setType(type);
+        setExportAll(exportAll);
+        setSequenceList(sequenceInfoList);
+
+        setUiHandlers();
+    }
+
+    private class ExportClickHandler implements ClickHandler{
+        @Override
+        public void onClick(ClickEvent event) {
+            exportButton.setEnabled(true);
+        }
+    }
+
+    private void setUiHandlers() {
+        closeButton.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent event) {
+                hide();
+            }
+        });
+
+        exportButton.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent event) {
+                doExport();
+            }
+        });
+
+        ExportClickHandler exportClickHandler = new ExportClickHandler();
+
+        genomicRadioButton.addClickHandler(exportClickHandler);
+        cdnaRadioButton.addClickHandler(exportClickHandler);
+        cdsRadioButton.addClickHandler(exportClickHandler);
+        peptideRadioButton.addClickHandler(exportClickHandler);
+
+        gff3WithFastaButton.addClickHandler(exportClickHandler);
+        gff3Button.addClickHandler(exportClickHandler);
+
+        chadoExportButton1.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent clickEvent) {
+                setExportAllSequencesToChado(false);
+                exportButton.setEnabled(true);
+            }
+        });
+
+        chadoExportButton2.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent clickEvent) {
+                setExportAllSequencesToChado(true);
+                exportButton.setEnabled(true);
+            }
+        });
+    }
+
+
+    public void setSequenceList(List<SequenceInfo> sequenceList) {
+        this.sequenceList = sequenceList;
+        if (exportAll) {
+            sequenceInfoLabel.setHTML("All exported ");
+        } else {
+            sequenceInfoLabel.setHTML(this.sequenceList.size() + " exported ");
+        }
+    }
+
+    public void setExportAll(Boolean exportAll) {
+        this.exportAll = exportAll;
+        this.sequenceInfoLabel.setHTML("All exported ");
+    }
+
+    public Boolean getExportAll() {
+        return exportAll;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+        typeLabel.setHTML("Type: " + this.type);
+    }
+
+    public void setExportUrl(String exportUrlString) {
+        Window.Location.assign(exportUrlString);
+        this.closeButton.click();
+    }
+
+    public void showExportStatus(String exportStatus) {
+        this.chadoExportButton1.setVisible(false);
+        this.chadoExportButton2.setVisible(false);
+        this.exportButton.setVisible(false);
+        this.closeButton.setText("OK");
+        this.closeButton.setWidth("45px");
+        Div status = new Div();
+
+        exportButton.setIconSpin(false);
+        if (exportStatus.contains("error")) {
+            Span span = new Span();
+            span.add(new Label(LabelType.DANGER, "Error"));
+            status.add(span);
+        }
+        else {
+            Span span = new Span();
+            span.add(new Label(LabelType.SUCCESS, "Success"));
+            status.add(span);
+        }
+        Div div = parseStatus(exportStatus);
+        status.add(div);
+        this.modalBody.add(status);
+    }
+
+    public Div parseStatus(String status) {
+        Div div = new Div();
+        JSONObject jsonObject = JSONParser.parseStrict(status).isObject();
+        GWT.log(jsonObject.toString());
+        for(String key : jsonObject.keySet()) {
+            div.add(new Paragraph(key + ": " + jsonObject.get(key).toString().replaceAll("\"", "")));
+        }
+        return div;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getSequenceType() {
+        if(genomicRadioButton.isActive()){
+            return FeatureStringEnum.TYPE_GENOMIC.getValue();
+        }
+        else
+        if(cdnaRadioButton.isActive()){
+            return FeatureStringEnum.TYPE_CDNA.getValue();
+        }
+        else
+        if(cdsRadioButton.isActive()){
+            return FeatureStringEnum.TYPE_CDS.getValue();
+        }
+        else
+        if(peptideRadioButton.isActive()){
+            return FeatureStringEnum.TYPE_PEPTIDE.getValue();
+        }
+        else
+        if(chadoExportButton1.isActive()){
+            return FeatureStringEnum.TYPE_CHADO.getValue();
+        }
+        else
+        if(chadoExportButton2.isActive()) {
+            return FeatureStringEnum.TYPE_CHADO.getValue();
+        }
+        // this is the default . . . may handle to GFF3 with FASTA
+        else{
+            return FeatureStringEnum.TYPE_GENOMIC.getValue();
+        }
+    }
+
+    public String getChadoExportType() {
+        String exportType = null;
+        if (type.equals(FeatureStringEnum.TYPE_CHADO.getValue())) {
+            if (chadoExportButton1.isActive()) {
+                exportType = FeatureStringEnum.EXPORT_CHADO_CLEAN.getValue();
+            }
+            else if (chadoExportButton2.isActive()) {
+                exportType = FeatureStringEnum.EXPORT_CHADO_UPDATE.getValue();
+            }
+        }
+        return exportType;
+    }
+
+    public String getOrganismName() {
+        return this.currentOrganismInfo.getName();
+    }
+
+    public Boolean getExportGff3Fasta() {
+        return gff3WithFastaButton.isActive();
+    }
+
+    public void doExport() {
+        exportButton.setEnabled(false);
+        exportButton.setIcon(IconType.REFRESH);
+        exportButton.setIconSpin(true);
+        generateLink();
+    }
+
+    public void generateLink() {
+        SequenceRestService.generateLink(this);
+    }
+
+    public List<SequenceInfo> getSequenceList() {
+        return sequenceList;
+    }
+
+    public Boolean getExportAllSequencesToChado() {
+        return this.exportAllSequencesToChado;
+    }
+
+    public void setExportAllSequencesToChado(Boolean value) {
+        this.exportAllSequencesToChado = value;
+    }
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.java b/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.java
new file mode 100644
index 0000000..3844acd
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.java
@@ -0,0 +1,135 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.http.client.*;
+import com.google.gwt.i18n.client.Dictionary;
+import com.google.gwt.json.client.*;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.rest.AnnotationRestService;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 1/9/15.
+ */
+public class GeneDetailPanel extends Composite {
+
+    private AnnotationInfo internalAnnotationInfo;
+
+
+    interface AnnotationDetailPanelUiBinder extends UiBinder<Widget, GeneDetailPanel> {
+    }
+
+    private static AnnotationDetailPanelUiBinder ourUiBinder = GWT.create(AnnotationDetailPanelUiBinder.class);
+    @UiField
+    TextBox nameField;
+    @UiField
+    TextBox symbolField;
+    @UiField
+    TextBox descriptionField;
+    @UiField
+    TextBox locationField;
+    @UiField
+    TextBox sequenceField;
+    @UiField
+    TextBox userField;
+
+
+    public GeneDetailPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+    }
+
+    @UiHandler("nameField")
+    void handleNameChange(ChangeEvent e) {
+        String updatedName = nameField.getText();
+        internalAnnotationInfo.setName(updatedName);
+        updateGene();
+    }
+
+    @UiHandler("symbolField")
+    void handleSymbolChange(ChangeEvent e) {
+        String updatedName = symbolField.getText();
+        internalAnnotationInfo.setSymbol(updatedName);
+        updateGene();
+    }
+
+    @UiHandler("descriptionField")
+    void handleDescriptionChange(ChangeEvent e) {
+        String updatedName = descriptionField.getText();
+        internalAnnotationInfo.setDescription(updatedName);
+        updateGene();
+    }
+
+    private void enableFields(boolean enabled) {
+        nameField.setEnabled(enabled);
+        symbolField.setEnabled(enabled);
+        descriptionField.setEnabled(enabled);
+    }
+
+
+    private void updateGene() {
+        final AnnotationInfo updatedInfo = this.internalAnnotationInfo;
+        enableFields(false);
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+                enableFields(true);
+                Annotator.eventBus.fireEvent(new AnnotationInfoChangeEvent(updatedInfo, AnnotationInfoChangeEvent.Action.UPDATE));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating gene: " + exception);
+                enableFields(true);
+            }
+        };
+        RestService.sendRequest(requestCallback, "annotator/updateFeature/", AnnotationRestService.convertAnnotationInfoToJSONObject(this.internalAnnotationInfo));
+
+    }
+
+    /**
+     * updateData
+     */
+    public void updateData(AnnotationInfo annotationInfo) {
+        this.internalAnnotationInfo = annotationInfo;
+        nameField.setText(internalAnnotationInfo.getName());
+        symbolField.setText(internalAnnotationInfo.getSymbol());
+        descriptionField.setText(internalAnnotationInfo.getDescription());
+        sequenceField.setText(internalAnnotationInfo.getSequence());
+        userField.setText(internalAnnotationInfo.getOwner());
+
+        if (internalAnnotationInfo.getMin() != null) {
+            String locationText = internalAnnotationInfo.getMin().toString();
+            locationText += " - ";
+            locationText += internalAnnotationInfo.getMax().toString();
+            locationText += " strand(";
+            locationText += internalAnnotationInfo.getStrand() > 0 ? "+" : "-";
+            locationText += ")";
+            locationField.setText(locationText);
+            locationField.setVisible(true);
+        }
+        else{
+            locationField.setVisible(false);
+        }
+
+
+        setVisible(true);
+    }
+
+    public void setEditable(boolean editable) {
+        nameField.setEnabled(editable);
+        symbolField.setEnabled(editable);
+        descriptionField.setEnabled(editable);
+
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.ui.xml
new file mode 100644
index 0000000..345a622
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/GeneDetailPanel.ui.xml
@@ -0,0 +1,72 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+        >
+    <ui:style>
+
+        .container {
+            margin-left: 20px;
+            margin-right: 20px;
+            margin-top: 10px;
+        }
+
+        .widgetPanel {
+            margin-bottom: 5px;
+        }
+
+        .row {
+            margin-left: 5px;
+            margin-right: 5px;
+        }
+    </ui:style>
+
+    <b:Container fluid="true" styleName="{style.container}">
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup >
+                    <b:InputGroupAddon>Name</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="nameField" />
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Symbol</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="symbolField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Description</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="descriptionField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Location</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="locationField" enabled="false"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Ref Sequence</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="sequenceField" enabled="false"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Owner</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="userField" enabled="false"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+    </b:Container>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.java b/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.java
new file mode 100644
index 0000000..d45ba81
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.java
@@ -0,0 +1,522 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.CheckboxCell;
+import com.google.gwt.cell.client.FieldUpdater;
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.ColumnSortEvent;
+import com.google.gwt.user.cellview.client.DataGrid;
+import com.google.gwt.user.cellview.client.TextColumn;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TabLayoutPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.ListDataProvider;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SingleSelectionModel;
+import org.bbop.apollo.gwt.client.dto.GroupInfo;
+import org.bbop.apollo.gwt.client.dto.GroupOrganismPermissionInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.client.dto.UserOrganismPermissionInfo;
+import org.bbop.apollo.gwt.client.event.GroupChangeEvent;
+import org.bbop.apollo.gwt.client.event.GroupChangeEventHandler;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.GroupRestService;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.bootbox.client.callback.ConfirmCallback;
+import org.gwtbootstrap3.extras.select.client.ui.MultipleSelect;
+import org.gwtbootstrap3.extras.select.client.ui.Option;
+import org.gwtbootstrap3.extras.select.client.ui.Select;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Created by ndunn on 12/17/14.
+ */
+public class GroupPanel extends Composite {
+    interface UserGroupBrowserPanelUiBinder extends UiBinder<Widget, GroupPanel> {
+    }
+
+    private static UserGroupBrowserPanelUiBinder ourUiBinder = GWT.create(UserGroupBrowserPanelUiBinder.class);
+    @UiField
+    TextBox name;
+
+    DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+    @UiField(provided = true)
+    DataGrid<GroupInfo> dataGrid = new DataGrid<GroupInfo>(10, tablecss);
+    @UiField
+    Button deleteButton;
+    @UiField
+    Button createButton;
+    @UiField
+    TabLayoutPanel userDetailTab;
+    //    @UiField
+//    FlexTable userData;
+    @UiField(provided = true)
+    WebApolloSimplePager pager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+    @UiField(provided = true)
+    WebApolloSimplePager organismPager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+    @UiField(provided = true)
+    DataGrid<GroupOrganismPermissionInfo> organismPermissionsGrid = new DataGrid<>(4, tablecss);
+    @UiField
+    TextBox createGroupField;
+    @UiField
+    Button saveButton;
+    @UiField
+    Button cancelButton;
+    @UiField
+    Button updateButton;
+    @UiField
+    Button cancelUpdateButton;
+    @UiField
+    MultipleSelect availableUsers;
+    @UiField
+    Button updateUsers;
+
+    private ListDataProvider<GroupInfo> dataProvider = new ListDataProvider<>();
+    private List<GroupInfo> groupInfoList = dataProvider.getList();
+    private SingleSelectionModel<GroupInfo> selectionModel = new SingleSelectionModel<>();
+    private GroupInfo selectedGroupInfo;
+    private ColumnSortEvent.ListHandler<GroupInfo> groupSortHandler = new ColumnSortEvent.ListHandler<>(groupInfoList);
+    private List<UserInfo> allUsersList = new ArrayList<>();
+
+
+    private ListDataProvider<GroupOrganismPermissionInfo> permissionProvider = new ListDataProvider<>();
+    private List<GroupOrganismPermissionInfo> permissionProviderList = permissionProvider.getList();
+    private ColumnSortEvent.ListHandler<GroupOrganismPermissionInfo> sortHandler = new ColumnSortEvent.ListHandler<GroupOrganismPermissionInfo>(permissionProviderList);
+
+    public GroupPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+        availableUsers.getElement().setAttribute("data-dropup-auto", Boolean.toString(false));
+
+        TextColumn<GroupInfo> firstNameColumn = new TextColumn<GroupInfo>() {
+            @Override
+            public String getValue(GroupInfo employee) {
+                return employee.getName();
+            }
+        };
+        firstNameColumn.setSortable(true);
+
+        Column<GroupInfo, Number> secondNameColumn = new Column<GroupInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(GroupInfo object) {
+                return object.getNumberOfUsers();
+            }
+        };
+        secondNameColumn.setSortable(true);
+
+        dataGrid.addColumn(firstNameColumn, "Name");
+        dataGrid.addColumn(secondNameColumn, "Users");
+
+        dataProvider.addDataDisplay(dataGrid);
+        dataGrid.setSelectionModel(selectionModel);
+        organismPager.setDisplay(organismPermissionsGrid);
+        pager.setDisplay(dataGrid);
+
+
+        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                setSelectedGroup();
+            }
+        });
+
+        dataGrid.addColumnSortHandler(groupSortHandler);
+        groupSortHandler.setComparator(firstNameColumn, new Comparator<GroupInfo>() {
+            @Override
+            public int compare(GroupInfo o1, GroupInfo o2) {
+                return o1.getName().compareTo(o2.getName());
+            }
+        });
+        groupSortHandler.setComparator(secondNameColumn, new Comparator<GroupInfo>() {
+            @Override
+            public int compare(GroupInfo o1, GroupInfo o2) {
+                return o1.getNumberOfUsers() - o2.getNumberOfUsers();
+            }
+
+        });
+
+
+        createOrganismPermissionsTable();
+
+
+        Annotator.eventBus.addHandler(GroupChangeEvent.TYPE, new GroupChangeEventHandler() {
+            @Override
+            public void onGroupChanged(GroupChangeEvent userChangeEvent) {
+                switch (userChangeEvent.getAction()) {
+                    case RELOAD_GROUPS:
+                        selectedGroupInfo = null;
+                        selectionModel.clear();
+                        setSelectedGroup();
+                        reload();
+                        break;
+                    case ADD_GROUP:
+                    case REMOVE_GROUP:
+                        selectedGroupInfo = null;
+                        selectionModel.clear();
+                        setSelectedGroup();
+                        reload();
+                        cancelAddState();
+                        break;
+                    case GROUPS_RELOADED:
+                        selectedGroupInfo = null;
+                        selectionModel.clear();
+                        break;
+
+
+                }
+            }
+        });
+
+        Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
+            @Override
+            public boolean execute() {
+                if (MainPanel.getInstance().getCurrentUser() != null) {
+//        Window.alert("Has current user: " + (MainPanel.getInstance().getCurrentUser()==null ? "is null " : "exists"));
+                    if(MainPanel.getInstance().isCurrentUserAdmin()) {
+                        GroupRestService.loadGroups(groupInfoList);
+                        UserRestService.loadUsers(allUsersList);
+                    }
+                    return false;
+                }
+                return true;
+            }
+        }, 100);
+    }
+
+    @UiHandler("updateUsers")
+    public void updateUsers(ClickEvent clickEvent) {
+        List<Option> selectedValues = availableUsers.getSelectedItems();
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                selectedGroupInfo = null;
+                selectionModel.clear();
+                setSelectedGroup();
+                reload();
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Failed to update users: " + exception.fillInStackTrace().toString());
+            }
+        };
+        GroupRestService.updateUserGroups(requestCallback, selectedGroupInfo, selectedValues);
+    }
+
+    @UiHandler("deleteButton")
+    public void deleteGroup(ClickEvent clickEvent) {
+        Integer numberOfUsers = selectedGroupInfo.getNumberOfUsers();
+        if (numberOfUsers > 0) {
+            Bootbox.confirm("Group '" + selectedGroupInfo.getName() + "' has " + numberOfUsers + " associated with it.  Still remove?", new ConfirmCallback() {
+                @Override
+                public void callback(boolean result) {
+                    if (result) {
+                        GroupRestService.deleteGroup(selectedGroupInfo);
+                        selectionModel.clear();
+                    }
+                }
+            });
+        } else {
+            Bootbox.confirm("Remove group '" + selectedGroupInfo.getName() + "'?", new ConfirmCallback() {
+                @Override
+                public void callback(boolean result) {
+                    if (result) {
+                        GroupRestService.deleteGroup(selectedGroupInfo);
+                        selectionModel.clear();
+                    }
+                }
+            });
+        }
+    }
+
+
+    private GroupInfo getGroupFromUI() {
+        String groupName = name.getText().trim();
+        if (groupName.length() < 3) {
+            Bootbox.alert("Group must be at least 3 characters long");
+            return null;
+        }
+        GroupInfo groupInfo = new GroupInfo();
+        groupInfo.setName(groupName);
+        return groupInfo;
+    }
+
+    @UiHandler("userDetailTab")
+    void onTabSelection(SelectionEvent<Integer> event) {
+        organismPermissionsGrid.redraw();
+
+    }
+
+    @UiHandler("cancelButton")
+    public void cancelNewGroup(ClickEvent clickEvent) {
+        cancelAddState();
+    }
+
+    @UiHandler("saveButton")
+    public void saveNewGroup(ClickEvent clickEvent) {
+        String groupName = createGroupField.getText().trim();
+        if (validateName(groupName)) {
+            GroupInfo groupInfo = new GroupInfo();
+            groupInfo.setName(groupName);
+            GroupRestService.addNewGroup(groupInfo);
+        }
+    }
+
+    void cancelAddState() {
+        createButton.setVisible(true);
+        createGroupField.setVisible(false);
+        saveButton.setVisible(false);
+        cancelButton.setVisible(false);
+        createGroupField.setText("");
+    }
+
+    void setAddState() {
+        createButton.setVisible(false);
+        createGroupField.setVisible(true);
+        saveButton.setVisible(true);
+        cancelButton.setVisible(true);
+        createGroupField.setText("");
+    }
+
+    @UiHandler("createButton")
+    public void createGroup(ClickEvent clickEvent) {
+        setAddState();
+    }
+
+    private Boolean validateName(String groupName) {
+        if (groupName.length() < 3) {
+            Bootbox.alert("Group must be at least 3 characters long");
+            return false;
+        }
+        for (GroupInfo groupInfo : groupInfoList) {
+            if (groupName.equals(groupInfo.getName())) {
+                Bootbox.alert("Group name must be unique");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @UiHandler("updateButton")
+    public void updateGroupName(ClickEvent clickEvent) {
+        if (selectedGroupInfo != null && selectedGroupInfo.getId() != null) {
+            String groupName = name.getText().trim();
+            if (validateName(groupName)) {
+                selectedGroupInfo.setName(groupName);
+                Bootbox.alert("Saving Group '" + groupName + "'");
+                GroupRestService.updateGroup(selectedGroupInfo);
+            }
+        }
+    }
+
+    @UiHandler("cancelUpdateButton")
+    public void cancelUpdateGroupName(ClickEvent clickEvent) {
+        name.setText(selectedGroupInfo.getName());
+        handleNameChange(null);
+    }
+
+    @UiHandler("name")
+    public void handleNameChange(KeyUpEvent changeEvent) {
+        String newName = name.getText().trim();
+        String originalName = selectedGroupInfo.getName();
+
+        updateButton.setEnabled(newName.length() >= 3 && !newName.equals(originalName));
+
+    }
+
+    private void setSelectedGroup() {
+        selectedGroupInfo = selectionModel.getSelectedObject();
+
+        permissionProviderList.clear();
+        if (selectedGroupInfo != null) {
+            name.setText(selectedGroupInfo.getName());
+            deleteButton.setVisible(true);
+            availableUsers.clear();
+//            userData.removeAllRows();
+
+            List<String> optionsList = new ArrayList<>();
+            for (UserInfo userInfo : selectedGroupInfo.getUserInfoList()) {
+                Option option = new Option();
+                option.setText(userInfo.getName() + " (" + userInfo.getEmail() + ")");
+                optionsList.add(option.getValue());
+            }
+
+            for (UserInfo userInfo : allUsersList) {
+                Option option = new Option();
+                option.setText(userInfo.getName() + " (" + userInfo.getEmail() + ")");
+                availableUsers.add(option);
+            }
+
+
+//            Option[] options = optionsList.toArray(new Option[optionsList.size()]);
+            availableUsers.setValue(optionsList);
+//            availableUsers.setValues(options);
+            availableUsers.refresh();
+
+            // only show organisms that this user is an admin on . . . https://github.com/GMOD/Apollo/issues/540
+            if (MainPanel.getInstance().isCurrentUserAdmin()) {
+                permissionProviderList.addAll(selectedGroupInfo.getOrganismPermissionMap().values());
+            } else {
+                List<String> organismsToShow = new ArrayList<>();
+                for (UserOrganismPermissionInfo userOrganismPermission : MainPanel.getInstance().getCurrentUser().getOrganismPermissionMap().values()) {
+                    if (userOrganismPermission.isAdmin()) {
+                        organismsToShow.add(userOrganismPermission.getOrganismName());
+                    }
+                }
+
+                for (GroupOrganismPermissionInfo userOrganismPermission : selectedGroupInfo.getOrganismPermissionMap().values()) {
+                    if (organismsToShow.contains(userOrganismPermission.getOrganismName())) {
+                        permissionProviderList.add(userOrganismPermission);
+                    }
+                }
+            }
+            userDetailTab.setVisible(true);
+        } else {
+            name.setText("");
+            deleteButton.setVisible(false);
+//            userData.removeAllRows();
+            userDetailTab.setVisible(false);
+        }
+    }
+
+    public void reload() {
+        if (MainPanel.getInstance().getCurrentUser() != null) {
+            GroupRestService.loadGroups(groupInfoList);
+        }
+//        dataGrid.redraw();
+    }
+
+
+    private void createOrganismPermissionsTable() {
+        TextColumn<GroupOrganismPermissionInfo> organismNameColumn = new TextColumn<GroupOrganismPermissionInfo>() {
+            @Override
+            public String getValue(GroupOrganismPermissionInfo userOrganismPermissionInfo) {
+                return userOrganismPermissionInfo.getOrganismName();
+            }
+        };
+        organismNameColumn.setSortable(true);
+        organismNameColumn.setDefaultSortAscending(true);
+
+
+        Column<GroupOrganismPermissionInfo, Boolean> adminColumn = new Column<GroupOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(GroupOrganismPermissionInfo object) {
+                return object.isAdmin();
+            }
+        };
+        adminColumn.setSortable(true);
+        adminColumn.setFieldUpdater(new FieldUpdater<GroupOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, GroupOrganismPermissionInfo object, Boolean value) {
+                object.setAdmin(value);
+                GroupRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(adminColumn, new Comparator<GroupOrganismPermissionInfo>() {
+            @Override
+            public int compare(GroupOrganismPermissionInfo o1, GroupOrganismPermissionInfo o2) {
+                return o1.isAdmin().compareTo(o2.isAdmin());
+            }
+        });
+
+        organismPermissionsGrid.setEmptyTableWidget(new Label("Please select a group to view the group's organism permissions"));
+        organismPermissionsGrid.addColumnSortHandler(sortHandler);
+        sortHandler.setComparator(organismNameColumn, new Comparator<GroupOrganismPermissionInfo>() {
+            @Override
+            public int compare(GroupOrganismPermissionInfo o1, GroupOrganismPermissionInfo o2) {
+                return o1.getOrganismName().compareTo(o2.getOrganismName());
+            }
+        });
+
+        Column<GroupOrganismPermissionInfo, Boolean> writeColumn = new Column<GroupOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(GroupOrganismPermissionInfo object) {
+                return object.isWrite();
+            }
+        };
+        writeColumn.setSortable(true);
+        writeColumn.setFieldUpdater(new FieldUpdater<GroupOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, GroupOrganismPermissionInfo object, Boolean value) {
+                object.setWrite(value);
+                object.setGroupId(selectedGroupInfo.getId());
+                GroupRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(writeColumn, new Comparator<GroupOrganismPermissionInfo>() {
+            @Override
+            public int compare(GroupOrganismPermissionInfo o1, GroupOrganismPermissionInfo o2) {
+                return o1.isWrite().compareTo(o2.isWrite());
+            }
+        });
+
+        Column<GroupOrganismPermissionInfo, Boolean> exportColumn = new Column<GroupOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(GroupOrganismPermissionInfo object) {
+                return object.isExport();
+            }
+        };
+        exportColumn.setSortable(true);
+        exportColumn.setFieldUpdater(new FieldUpdater<GroupOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, GroupOrganismPermissionInfo object, Boolean value) {
+                object.setExport(value);
+                GroupRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(exportColumn, new Comparator<GroupOrganismPermissionInfo>() {
+            @Override
+            public int compare(GroupOrganismPermissionInfo o1, GroupOrganismPermissionInfo o2) {
+                return o1.isExport().compareTo(o2.isExport());
+            }
+        });
+
+        Column<GroupOrganismPermissionInfo, Boolean> readColumn = new Column<GroupOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(GroupOrganismPermissionInfo object) {
+                return object.isRead();
+            }
+        };
+        readColumn.setSortable(true);
+        readColumn.setFieldUpdater(new FieldUpdater<GroupOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, GroupOrganismPermissionInfo object, Boolean value) {
+                object.setRead(value);
+                GroupRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(readColumn, new Comparator<GroupOrganismPermissionInfo>() {
+            @Override
+            public int compare(GroupOrganismPermissionInfo o1, GroupOrganismPermissionInfo o2) {
+                return o1.isRead().compareTo(o2.isRead());
+            }
+        });
+
+
+        organismPermissionsGrid.addColumn(organismNameColumn, "Name");
+        organismPermissionsGrid.addColumn(adminColumn, "Admin");
+        organismPermissionsGrid.addColumn(writeColumn, "Write");
+        organismPermissionsGrid.addColumn(exportColumn, "Export");
+        organismPermissionsGrid.addColumn(readColumn, "Read");
+        permissionProvider.addDataDisplay(organismPermissionsGrid);
+
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.ui.xml
new file mode 100644
index 0000000..998af61
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/GroupPanel.ui.xml
@@ -0,0 +1,144 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+             xmlns:wa="urn:import:org.bbop.apollo.gwt.client"
+             xmlns:select="urn:import:org.gwtbootstrap3.extras.select.client.ui"
+>
+    <ui:style>
+
+        .trackTable {
+            margin-left: 10px;
+            margin-top: 10px;
+            vertical-align: middle;
+        }
+
+        .tableKeyName {
+            font-weight: bolder;
+            margin-top: 5px;
+        }
+
+        .center-table {
+            margin: 10px;
+        }
+
+        .paddedTable {
+            padding-left: 30px;
+            padding-top: 10px;
+        }
+
+        .pager {
+            width: 100%;
+            margin-left: auto;
+            margin-right: auto;
+        }
+
+        .northPanel {
+            /*padding: 0px;*/
+            /*margin: 10px;*/
+            margin-top: 10px;
+        }
+
+        .widgetPanel {
+            /*padding: 5px;*/
+            /*margin: 5px;*/
+        }
+
+        .removeButton {
+            padding: 10px;
+        }</ui:style>
+    <gwt:DockLayoutPanel>
+
+        <gwt:north size="40">
+            <b:Container fluid="true" width="100%">
+                <b:Row styleName="{style.northPanel}">
+                    <b:Column size="MD_4">
+                        <b:Button ui:field="createButton" icon="PLUS">Add Group</b:Button>
+                        <b:Button ui:field="saveButton" icon="SAVE" visible="false" type="PRIMARY">Save</b:Button>
+                        <b:Button ui:field="cancelButton" visible="false">Cancel</b:Button>
+                    </b:Column>
+                    <b:Column size="MD_5">
+                        <b:TextBox ui:field="createGroupField" visible="false"/>
+                    </b:Column>
+                    <!--<b:Column size="MD_1" styleName="{style.widgetPanel}">-->
+                    <!--</b:Column>-->
+                </b:Row>
+            </b:Container>
+        </gwt:north>
+        <gwt:center>
+            <gwt:DockLayoutPanel>
+                <gwt:north size="25">
+                    <wa:WebApolloSimplePager ui:field="pager" styleName="{style.pager}"/>
+                </gwt:north>
+                <gwt:center>
+                    <cellview:DataGrid ui:field="dataGrid" styleName="{style.trackTable}"/>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:center>
+        <gwt:south size="300">
+            <gwt:TabLayoutPanel barHeight="35" ui:field="userDetailTab" visible="false">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row>
+                            <b:Column size="MD_1">
+                                <gwt:HTML styleName="{style.tableKeyName}">Name</gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <b:TextBox ui:field="name"/>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <b:Button ui:field="updateButton" icon="SAVE" type="PRIMARY" enabled="false">Save
+                                </b:Button>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <b:Button ui:field="cancelUpdateButton" enabled="true">Reset</b:Button>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.removeButton}">
+                            <b:Column size="MD_1">
+                                <b:Button ui:field="deleteButton" icon="WARNING" type="DANGER" visible="false">Remove
+                                    Group
+                                </b:Button>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Organisms</gwt:header>
+                    <gwt:DockLayoutPanel>
+                        <gwt:north size="25">
+                            <wa:WebApolloSimplePager ui:field="organismPager" styleName="{style.pager}"/>
+                        </gwt:north>
+                        <gwt:center>
+                            <cellview:DataGrid ui:field="organismPermissionsGrid" width="90%"
+                                               styleName="{style.center-table}"/>
+                        </gwt:center>
+                    </gwt:DockLayoutPanel>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Users</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row>
+                            <b:Column size="MD_8">
+                                <select:MultipleSelect width="100%" title="Membership"  selectedTextFormat="COUNT"
+                                                       countSelectedTextFormat="2"
+                                                       ui:field="availableUsers"
+                                                       liveSearch="true">
+                                </select:MultipleSelect>
+                                <!--<select:Select width="100%" title="Membership" multiple="true"-->
+                                               <!--selectedTextFormat="count > 2" visibleSize="5" ui:field="availableUsers"-->
+                                               <!--liveSearch="true">-->
+                                <!--</select:Select>-->
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <b:Button ui:field="updateUsers" icon="SAVE" type="PRIMARY">Update Membership</b:Button>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+            </gwt:TabLayoutPanel>
+        </gwt:south>
+
+    </gwt:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/LinkDialog.java b/src/gwt/org/bbop/apollo/gwt/client/LinkDialog.java
new file mode 100644
index 0000000..69cd35c
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/LinkDialog.java
@@ -0,0 +1,31 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.user.client.ui.HTML;
+import org.gwtbootstrap3.client.ui.Modal;
+import org.gwtbootstrap3.client.ui.ModalBody;
+import org.gwtbootstrap3.client.ui.constants.ModalBackdrop;
+
+/**
+ * Created by ndunn on 4/30/15.
+ */
+public class LinkDialog extends Modal {
+
+
+    public LinkDialog(String title, String message, Boolean showOnConstruct) {
+        setTitle(title);
+        setClosable(true);
+        setFade(true);
+        setDataBackdrop(ModalBackdrop.STATIC);
+        setDataKeyboard(true);
+
+        if (message != null) {
+            HTML content = new HTML(message);
+            ModalBody modalBody = new ModalBody();
+            modalBody.add(content);
+            add(modalBody);
+        }
+        if (showOnConstruct) {
+            show();
+        }
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/LoadingDialog.java b/src/gwt/org/bbop/apollo/gwt/client/LoadingDialog.java
new file mode 100644
index 0000000..743363d
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/LoadingDialog.java
@@ -0,0 +1,44 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import org.gwtbootstrap3.client.ui.Modal;
+import org.gwtbootstrap3.client.ui.ModalBody;
+import org.gwtbootstrap3.client.ui.constants.ModalBackdrop;
+
+/**
+ * Created by ndunn on 4/30/15.
+ */
+public class LoadingDialog extends Modal{
+
+    public LoadingDialog(boolean showOnConstruct){
+        this("Loading ...",null,showOnConstruct);
+    }
+
+    public LoadingDialog(){
+        this("Loading ...",null,true);
+    }
+
+
+    public LoadingDialog(String title){
+        this(title,null,true);
+
+    }
+    public LoadingDialog(String title,String message,Boolean showOnConstruct){
+        setTitle(title);
+        setClosable(false);
+        setFade(true);
+        setDataBackdrop(ModalBackdrop.STATIC);
+
+        if(message!=null){
+            HTML content = new HTML(message);
+            ModalBody modalBody = new ModalBody();
+            modalBody.add(content);
+            add( modalBody );
+        }
+        if(showOnConstruct){
+            show();
+        }
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.java b/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.java
new file mode 100644
index 0000000..c58bd7a
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.java
@@ -0,0 +1,101 @@
+package org.bbop.apollo.gwt.client;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.Event.*;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.uibinder.client.UiBinder;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.Input;
+import org.gwtbootstrap3.client.ui.Icon;
+import org.gwtbootstrap3.client.ui.CheckBox;
+import org.gwtbootstrap3.client.ui.html.Div;
+import org.gwtbootstrap3.client.ui.html.Paragraph;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.client.ui.constants.*;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+
+public class LoginDialog extends DialogBox {
+    private static final Binder binder = GWT.create(Binder.class);
+    interface Binder extends UiBinder<Widget, LoginDialog> {
+    }
+
+
+    @UiField
+    Div errorHtml;
+
+    @UiField
+    Paragraph errorText;
+
+
+
+
+    @UiField
+    Button loginButton;
+
+    @UiField
+    TextBox userBox;
+
+    @UiField
+    Input passBox;
+
+    @UiField
+    CheckBox rememberBox;
+
+
+
+    public LoginDialog() {
+        getElement().setId("loginDialogId");
+        setText("Login");
+        setAnimationEnabled(true);
+        // Enable glass background.
+        setGlassEnabled(true);
+        setWidget(binder.createAndBindUi(this));
+    }
+
+    public void showLogin() {
+        Icon icon = new Icon(IconType.WARNING);
+        errorHtml.add(icon);
+        errorText.setEmphasis(Emphasis.DANGER);
+        clearErrors();
+        center();
+        show();
+        userBox.setFocus(true);
+    }
+
+    public void setError(String errorMessage){
+        errorText.setText(errorMessage);
+        errorHtml.setVisible(true);
+    }
+ 
+    public void clearErrors(){
+        errorText.setText("");
+        errorHtml.setVisible(false);
+    }
+
+    @Override
+    public void onPreviewNativeEvent(NativePreviewEvent e) {
+        NativeEvent nativeEvent = e.getNativeEvent();
+        if ("keydown".equals(nativeEvent.getType())) {
+            if (nativeEvent.getKeyCode() == KeyCodes.KEY_ENTER) {
+                doLogin(userBox.getText().trim(), passBox.getText(), rememberBox.getValue());
+            }
+        }
+    }
+
+    @UiHandler("loginButton")
+    public void submitAction(ClickEvent e) {
+        doLogin(userBox.getText().trim(),passBox.getText(),rememberBox.getValue());
+    }
+
+    public void doLogin(String username,String password,Boolean rememberMe){
+        UserRestService.login(username, password,rememberMe,this);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.ui.xml
new file mode 100644
index 0000000..3bb54d7
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/LoginDialog.ui.xml
@@ -0,0 +1,49 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:bhtml="urn:import:org.gwtbootstrap3.client.ui.html"
+        >
+
+
+    <ui:style>
+        .northPanel {
+            padding: 0px;
+            margin: 10px;
+        }
+        .spacer {
+            padding: 0px;
+            margin: 10px;
+        }
+    </ui:style>
+
+
+<b:Form type="HORIZONTAL">
+  <b:FieldSet>
+    <b:Container fluid="true"  styleName="{style.northPanel}">
+      <b:Row>
+        <b:Column size="MD_3"><b:FormLabel for="username">Username:</b:FormLabel></b:Column>
+        <b:Column size="MD_9"><b:TextBox ui:field="userBox" b:id="formName"/></b:Column>
+      </b:Row>
+
+      <b:Row marginTop="5" >
+        <b:Column size="MD_3"><b:FormLabel  for="formPassword">Password:</b:FormLabel></b:Column>
+        <b:Column size="MD_9"><b:Input type="PASSWORD"  ui:field="passBox" b:id="formPassword"/></b:Column>
+      </b:Row>
+
+      <b:Row styleName="{style.spacer}">
+        <b:Column size="MD_8"><b:CheckBox ui:field="rememberBox" text="Remember me"/></b:Column>
+        <b:Column size="MD_2"><b:Button ui:field="loginButton" type="PRIMARY">Login</b:Button></b:Column>
+      </b:Row>
+    </b:Container>
+
+    <b:Row styleName="{style.spacer}">
+      <b:Anchor text="Browse public genomes" href="../jbrowse/"/>
+    </b:Row>
+
+    <b:Row styleName="{style.spacer}">
+      <b:Column size="MD_1"><b:html.Div ui:field="errorHtml"/></b:Column>
+      <b:Column size="MD_9"><b:html.Paragraph ui:field="errorText"/></b:Column>
+    </b:Row>
+  </b:FieldSet>
+</b:Form>
+
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/MainPanel.java b/src/gwt/org/bbop/apollo/gwt/client/MainPanel.java
new file mode 100644
index 0000000..b1923e9
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/MainPanel.java
@@ -0,0 +1,1138 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.http.client.*;
+import com.google.gwt.json.client.*;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import com.google.gwt.user.client.ui.ListBox;
+import org.bbop.apollo.gwt.client.dto.*;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEventHandler;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEvent;
+import org.bbop.apollo.gwt.client.event.UserChangeEvent;
+import org.bbop.apollo.gwt.client.rest.OrganismRestService;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.bbop.apollo.gwt.client.rest.SequenceRestService;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.Anchor;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.SuggestBox;
+import org.gwtbootstrap3.client.ui.constants.AlertType;
+import org.gwtbootstrap3.client.ui.constants.IconType;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class MainPanel extends Composite {
+
+
+    private static final int DEFAULT_TAB_COUNT = 7;
+
+    interface MainPanelUiBinder extends UiBinder<Widget, MainPanel> {
+    }
+
+    private static MainPanelUiBinder ourUiBinder = GWT.create(MainPanelUiBinder.class);
+
+    private boolean toggleOpen = true;
+
+    private static UserInfo currentUser;
+    private static OrganismInfo currentOrganism;
+    private static SequenceInfo currentSequence;
+    private static Integer currentStartBp; // start base pair
+    private static Integer currentEndBp; // end base pair
+    private static Map<String, List<String>> currentQueryParams; // list of organisms for user
+    static boolean useNativeTracklist; // list native tracks
+    private static List<OrganismInfo> organismInfoList = new ArrayList<>(); // list of organisms for user
+    private static final String trackListViewString = "&tracklist=";
+    private static final String openAnnotatorPanelString = "&openAnnotatorPanel=";
+
+    private static boolean handlingNavEvent = false;
+
+
+    private static MainPanel instance;
+    private final int maxUsernameLength = 15;
+    private static final double UPDATE_DIFFERENCE_BUFFER = 0.3;
+    private static final double GENE_VIEW_BUFFER = 0.4;
+    private static List<String> reservedList = new ArrayList<>();
+
+
+    @UiField
+    Button dockOpenClose;
+    @UiField(provided = false)
+    static NamedFrame frame;
+    @UiField
+    static AnnotatorPanel annotatorPanel;
+    @UiField
+    static TrackPanel trackPanel;
+    @UiField
+    static SequencePanel sequencePanel;
+    @UiField
+    static OrganismPanel organismPanel;
+    @UiField
+    static UserPanel userPanel;
+    @UiField
+    static GroupPanel userGroupPanel;
+    @UiField
+    static DockLayoutPanel eastDockPanel;
+    @UiField(provided = true)
+    static SplitLayoutPanel mainSplitPanel;
+    @UiField
+    static TabLayoutPanel detailTabs;
+    @UiField
+    FlowPanel westPanel;
+    @UiField
+    PreferencePanel preferencePanel;
+    @UiField
+    Button logoutButton;
+    @UiField
+    Button userName;
+    @UiField
+    Button generateLink;
+    @UiField
+    ListBox organismListBox;
+    @UiField(provided = true)
+    static SuggestBox sequenceSuggestBox;
+    @UiField
+    Modal notificationModal;
+    @UiField
+    Alert alertText;
+    @UiField
+    Button logoutButton2;
+    @UiField
+    Anchor logoutAndBrowsePublicGenomes;
+    @UiField
+    Modal editUserModal;
+    @UiField
+    Input editMyPasswordInput;
+    @UiField
+    Button savePasswordButton;
+    @UiField
+    Button cancelPasswordButton;
+    @UiField
+    Input editMyPasswordInputRepeat;
+    @UiField
+    Alert editUserAlertText;
+    @UiField
+    HTML editUserHeader;
+    @UiField
+    Button trackListToggle;
+
+
+    private LoginDialog loginDialog = new LoginDialog();
+    private RegisterDialog registerDialog = new RegisterDialog();
+
+    public static MainPanel getInstance() {
+        if (instance == null) {
+            instance = new MainPanel();
+        }
+        return instance;
+    }
+
+
+    MainPanel() {
+        instance = this;
+        sequenceSuggestBox = new SuggestBox(new ReferenceSequenceOracle());
+
+        mainSplitPanel = new SplitLayoutPanel() {
+            @Override
+            public void onResize() {
+                super.onResize();
+                Annotator.setPreference(FeatureStringEnum.DOCK_WIDTH.getValue(), mainSplitPanel.getWidgetSize(eastDockPanel));
+            }
+        };
+
+        exportStaticMethod();
+
+        initWidget(ourUiBinder.createAndBindUi(this));
+        frame.getElement().setAttribute("id", frame.getName());
+
+        trackListToggle.setWidth(isCurrentUserAdmin() ? "20px" : "25px");
+
+        Annotator.eventBus.addHandler(AnnotationInfoChangeEvent.TYPE, new AnnotationInfoChangeEventHandler() {
+            @Override
+            public void onAnnotationChanged(AnnotationInfoChangeEvent annotationInfoChangeEvent) {
+                switch (annotationInfoChangeEvent.getAction()) {
+                    case SET_FOCUS:
+                        AnnotationInfo annotationInfo = annotationInfoChangeEvent.getAnnotationInfo();
+                        int start = annotationInfo.getMin();
+                        int end = annotationInfo.getMax();
+                        int newLength = end - start;
+                        start -= newLength * GENE_VIEW_BUFFER;
+                        end += newLength * GENE_VIEW_BUFFER;
+                        start = start < 0 ? 0 : start;
+                        updateGenomicViewerForLocation(annotationInfo.getSequence(), start, end);
+                        break;
+                }
+            }
+        });
+
+        sequenceSuggestBox.addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() {
+            @Override
+            public void onSelection(SelectionEvent<SuggestOracle.Suggestion> event) {
+                setCurrentSequence(sequenceSuggestBox.getText().trim(), null, null, true, false);
+            }
+        });
+
+
+        try {
+            String dockOpen = Annotator.getPreference(FeatureStringEnum.DOCK_OPEN.getValue());
+            if (dockOpen != null) {
+                Boolean setDockOpen = Boolean.valueOf(dockOpen);
+                toggleOpen = !setDockOpen;
+                toggleOpen();
+            }
+        } catch (Exception e) {
+            GWT.log("Error setting preference: " + e.fillInStackTrace().toString());
+            Annotator.setPreference(FeatureStringEnum.DOCK_OPEN.getValue(), true);
+        }
+
+
+        try {
+            String dockWidth = Annotator.getPreference(FeatureStringEnum.DOCK_WIDTH.getValue());
+            if (dockWidth != null && toggleOpen) {
+                Integer dockWidthInt = Integer.parseInt(dockWidth);
+                mainSplitPanel.setWidgetSize(eastDockPanel, dockWidthInt);
+            }
+        } catch (NumberFormatException e) {
+            GWT.log("Error setting preference: " + e.fillInStackTrace().toString());
+            Annotator.setPreference(FeatureStringEnum.DOCK_WIDTH.getValue(), 600);
+        }
+
+        setUserNameForCurrentUser();
+
+        String tabPreferenceString = Annotator.getPreference(FeatureStringEnum.CURRENT_TAB.getValue());
+        if(tabPreferenceString!=null){
+            try {
+                int selectedTab = Integer.parseInt(tabPreferenceString);
+                if(selectedTab<detailTabs.getWidgetCount()){
+                    detailTabs.selectTab(selectedTab);
+                    if (selectedTab == TabPanelIndex.TRACKS.index) {
+                        trackPanel.reloadIfEmpty();
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+
+        currentQueryParams = Window.Location.getParameterMap();
+
+        reservedList.add("loc");
+        reservedList.add("trackList");
+
+
+        loginUser();
+
+        checkExtraTabs();
+    }
+
+    private void checkExtraTabs() {
+
+        removeExtraTabs();
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONArray jsonArray = JSONParser.parseStrict(response.getText()).isArray();
+                for (int i = 0; i < jsonArray.size(); i++) {
+                    JSONObject jsonObject = jsonArray.get(i).isObject();
+                    final String title = jsonObject.get("title").isString().stringValue();
+                    if (jsonObject.containsKey("content")) {
+                        HTML htmlContent = new HTML(jsonObject.get("content").isString().stringValue());
+                        detailTabs.add(htmlContent, title);
+                    } else if (jsonObject.containsKey("url")) {
+                        final String url = jsonObject.get("url").isString().stringValue();
+                        Frame frame = new Frame(url);
+                        frame.setWidth("100%");
+                        frame.setHeight("100%");
+                        detailTabs.add(frame,title);
+
+                    } else {
+                        Bootbox.alert("Unsure how to process " + jsonObject.toString());
+                    }
+                }
+                String tabPreferenceString = Annotator.getPreference(FeatureStringEnum.CURRENT_TAB.getValue());
+                if(tabPreferenceString!=null){
+                    int selectedTab = 0 ;
+                    try {
+                        selectedTab = Integer.parseInt(tabPreferenceString);
+                        if(selectedTab >= detailTabs.getWidgetCount()){
+                            selectedTab = 0 ;
+                        }
+                    } catch (NumberFormatException e) {
+                        e.printStackTrace();
+                    }
+                    detailTabs.selectTab(selectedTab);
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+            }
+        };
+        RestService.sendGetRequest(requestCallback, "annotator/getExtraTabs");
+    }
+
+    private void removeExtraTabs() {
+        for(int i = 0 ; i < detailTabs.getWidgetCount()-DEFAULT_TAB_COUNT ; i++){
+            detailTabs.remove(i+DEFAULT_TAB_COUNT);
+        }
+    }
+
+
+    private static void setCurrentSequence(String sequenceNameString, final Integer start, final Integer end) {
+        setCurrentSequence(sequenceNameString, start, end, false, false);
+    }
+
+    private static void sendCurrentSequenceLocation(String sequenceNameString, final Integer start, final Integer end) {
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                currentStartBp = start;
+                currentEndBp = end;
+                handlingNavEvent = false;
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                handlingNavEvent = false;
+                Bootbox.alert("failed to set sequence location: " + exception);
+            }
+        };
+
+        handlingNavEvent = true;
+        SequenceRestService.setCurrentSequenceAndLocation(requestCallback, sequenceNameString, start, end, true);
+
+    }
+
+    private static void setCurrentSequence(String sequenceNameString, final Integer start, final Integer end, final boolean updateViewer, final boolean blocking) {
+
+        final LoadingDialog loadingDialog = new LoadingDialog(false);
+        if (blocking) {
+            loadingDialog.show();
+        }
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                handlingNavEvent = false;
+                JSONObject sequenceInfoJson = JSONParser.parseStrict(response.getText()).isObject();
+                currentSequence = SequenceInfoConverter.convertFromJson(sequenceInfoJson);
+
+                if (start == null) {
+                    currentStartBp = currentSequence.getStartBp() != null ? currentSequence.getStartBp() : 0;
+                } else {
+                    currentStartBp = start;
+                }
+                if (end == null) {
+                    currentEndBp = currentSequence.getEndBp() != null ? currentSequence.getEndBp() : currentSequence.getLength();
+                } else {
+                    currentEndBp = end;
+                }
+                sequenceSuggestBox.setText(currentSequence.getName());
+
+                Annotator.eventBus.fireEvent(new OrganismChangeEvent(OrganismChangeEvent.Action.LOADED_ORGANISMS, currentSequence.getName(), currentOrganism.getName()));
+
+                if (updateViewer) {
+                    updateGenomicViewerForLocation(currentSequence.getName(), currentStartBp, currentEndBp, true, false);
+                }
+                if (blocking) {
+                    loadingDialog.hide();
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                handlingNavEvent = false;
+                if (blocking) {
+                    loadingDialog.hide();
+                }
+                Bootbox.alert("Failed to sequence: " + exception);
+            }
+        };
+
+        handlingNavEvent = true;
+
+        if (start == null && end == null) {
+            SequenceRestService.setCurrentSequenceForString(requestCallback, sequenceNameString, currentOrganism);
+        } else {
+            SequenceRestService.setCurrentSequenceAndLocation(requestCallback, sequenceNameString, start, end);
+        }
+
+    }
+
+
+    private void updatePermissionsForOrganism() {
+        String globalRole = currentUser.getRole();
+        PermissionEnum highestPermission;
+        UserOrganismPermissionInfo userOrganismPermissionInfo = currentUser.getOrganismPermissionMap().get(currentOrganism.getName());
+        if (globalRole.equals("admin")) {
+            highestPermission = PermissionEnum.ADMINISTRATE;
+        } else {
+            highestPermission = PermissionEnum.NONE;
+        }
+        if (userOrganismPermissionInfo != null && highestPermission != PermissionEnum.ADMINISTRATE) {
+            highestPermission = userOrganismPermissionInfo.getHighestPermission();
+        }
+
+        switch (highestPermission) {
+            case ADMINISTRATE:
+                GWT.log("setting to ADMINISTRATE permissions");
+                detailTabs.getTabWidget(TabPanelIndex.USERS.index).getParent().setVisible(true);
+                detailTabs.getTabWidget(TabPanelIndex.GROUPS.index).getParent().setVisible(true);
+                detailTabs.getTabWidget(TabPanelIndex.ORGANISM.index).getParent().setVisible(true);
+                detailTabs.getTabWidget(TabPanelIndex.PREFERENCES.index).getParent().setVisible(true);
+                break;
+            case WRITE:
+                GWT.log("setting to WRITE permissions");
+            case EXPORT:
+                GWT.log("setting to EXPORT permissions");
+            case READ:
+                GWT.log("setting to READ permissions");
+                //break; <-- uncomment if want non-admin users to view panels
+            case NONE:
+            default:
+                GWT.log("setting to no permissions");
+                // let's set the view
+                detailTabs.getTabWidget(TabPanelIndex.USERS.index).getParent().setVisible(false);
+                detailTabs.getTabWidget(TabPanelIndex.GROUPS.index).getParent().setVisible(false);
+                detailTabs.getTabWidget(TabPanelIndex.ORGANISM.index).getParent().setVisible(false);
+                detailTabs.getTabWidget(TabPanelIndex.PREFERENCES.index).getParent().setVisible(false);
+
+                break;
+        }
+
+        UserChangeEvent userChangeEvent = new UserChangeEvent(UserChangeEvent.Action.PERMISSION_CHANGED, highestPermission);
+        Annotator.eventBus.fireEvent(userChangeEvent);
+    }
+
+    private void loginUser() {
+        String url = Annotator.getRootUrl() + "user/checkLogin";
+        url += "?clientToken=" + Annotator.getClientToken();
+        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
+        builder.setHeader("Content-type", "application/x-www-form-urlencoded");
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject returnValue = JSONParser.parseStrict(response.getText()).isObject();
+                if (returnValue.containsKey(FeatureStringEnum.USER_ID.getValue())) {
+                    if (returnValue.containsKey(FeatureStringEnum.ERROR.getValue())) {
+                        String errorText = returnValue.get(FeatureStringEnum.ERROR.getValue()).isString().stringValue();
+                        alertText.setText(errorText);
+                        detailTabs.setVisible(false);
+                        notificationModal.show();
+                    } else {
+                        detailTabs.setVisible(true);
+                        getAppState();
+                        logoutButton.setVisible(true);
+                        currentUser = UserInfoConverter.convertToUserInfoFromJSON(returnValue);
+                        Annotator.startSessionTimer();
+                        if (returnValue.containsKey("tracklist")) {
+                            MainPanel.useNativeTracklist = returnValue.get("tracklist").isBoolean().booleanValue();
+                        } else {
+                            MainPanel.useNativeTracklist = false;
+                        }
+                        trackPanel.updateTrackToggle(MainPanel.useNativeTracklist);
+                        trackListToggle.setActive(MainPanel.useNativeTracklist);
+
+
+                        setUserNameForCurrentUser();
+                    }
+
+
+                } else {
+                    boolean hasUsers = returnValue.get(FeatureStringEnum.HAS_USERS.getValue()).isBoolean().booleanValue();
+                    if (hasUsers) {
+                        currentUser = null;
+                        logoutButton.setVisible(false);
+                        loginDialog.showLogin();
+                    } else {
+                        currentUser = null;
+                        logoutButton.setVisible(false);
+                        registerDialog.center();
+                        registerDialog.show();
+                    }
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                loginDialog.setError(exception.getMessage());
+            }
+        };
+        try {
+            builder.setCallback(requestCallback);
+            builder.send();
+        } catch (RequestException e) {
+            loginDialog.setError(e.getMessage());
+        }
+
+    }
+
+    private void setUserNameForCurrentUser() {
+        if (currentUser == null) return;
+        String displayName = currentUser.getEmail();
+        userName.setText(displayName.length() > maxUsernameLength ?
+                displayName.substring(0, maxUsernameLength - 1) + "..." : displayName);
+    }
+
+    public static void updateGenomicViewerForLocation(String selectedSequence, Integer minRegion, Integer maxRegion) {
+        updateGenomicViewerForLocation(selectedSequence, minRegion, maxRegion, false, false);
+    }
+
+    /**
+     * @param selectedSequence
+     * @param minRegion
+     * @param maxRegion
+     */
+    public static void updateGenomicViewerForLocation(String selectedSequence, Integer minRegion, Integer maxRegion, boolean forceReload, boolean forceUrl) {
+
+        if (!forceReload && currentSequence != null && currentSequence.getName().equals(selectedSequence) && currentStartBp != null && currentEndBp != null && minRegion > 0 && maxRegion > 0 && frame.getUrl().startsWith("http")) {
+            int oldLength = maxRegion - minRegion;
+            double diff1 = (Math.abs(currentStartBp - minRegion)) / (float) oldLength;
+            double diff2 = (Math.abs(currentEndBp - maxRegion)) / (float) oldLength;
+            if (diff1 < UPDATE_DIFFERENCE_BUFFER && diff2 < UPDATE_DIFFERENCE_BUFFER) {
+                return;
+            }
+        }
+
+        currentStartBp = minRegion;
+        currentEndBp = maxRegion;
+
+
+        String trackListString = Annotator.getRootUrl();
+        trackListString += Annotator.getClientToken() + "/";
+        trackListString += "jbrowse/index.html?loc=";
+        trackListString += URL.encodeQueryString(selectedSequence+":") + minRegion + ".." + maxRegion;
+
+        trackListString += getCurrentQueryParamsAsString();
+
+
+        // if the trackList contains a string, it should over-ride and set?
+        if (trackListString.contains(trackListViewString)) {
+            // replace with whatever is in the toggle ? ? ?
+            Boolean showTrackValue = trackPanel.trackListToggle.getValue();
+
+            String positiveString = trackListViewString + "1";
+            String negativeString = trackListViewString + "0";
+            if (trackListString.contains(positiveString) && !showTrackValue) {
+                trackListString = trackListString.replace(positiveString, negativeString);
+            } else if (trackListString.contains(negativeString) && showTrackValue) {
+                trackListString = trackListString.replace(negativeString, positiveString);
+            }
+
+            MainPanel.useNativeTracklist = showTrackValue;
+        }
+        if (trackListString.contains(openAnnotatorPanelString)) {
+            String positiveString = openAnnotatorPanelString + "1";
+            String negativeString = openAnnotatorPanelString + "0";
+            if (trackListString.contains(positiveString)) {
+                trackListString = trackListString.replace(positiveString, "");
+                MainPanel.getInstance().openPanel();
+            } else if (trackListString.contains(negativeString)) {
+                trackListString = trackListString.replace(negativeString, "");
+                MainPanel.getInstance().closePanel();
+            }
+
+
+        }
+        // otherwise we use the nativeTrackList
+        else {
+            trackListString += "&tracklist=" + (MainPanel.useNativeTracklist ? "1" : "0");
+        }
+
+        if (!forceUrl && getInnerDiv() != null) {
+            JSONObject commandObject = new JSONObject();
+            commandObject.put("url", new JSONString(selectedSequence + ":" + currentStartBp + ".." + currentEndBp));
+            MainPanel.getInstance().postMessage("navigateToLocation", commandObject);
+        } else {
+            frame.setUrl(trackListString);
+        }
+
+        if (Window.Location.getParameter("tracks") != null) {
+            String newURL = Window.Location.createUrlBuilder().removeParameter("tracks").buildString();
+            newUrl(newURL);
+        }
+
+        currentQueryParams = Window.Location.getParameterMap();
+    }
+
+    void postMessage(String message, JSONObject object) {
+        object.put(FeatureStringEnum.DESCRIPTION.getValue(), new JSONString(message));
+        postMessage(object.getJavaScriptObject());
+    }
+
+    private native void postMessage(JavaScriptObject message)/*-{
+        var genomeViewer = $wnd.document.getElementById("genomeViewer").contentWindow;
+        var domain = $wnd.location.protocol + "//" + $wnd.location.hostname + ":" + $wnd.location.port;
+        genomeViewer.postMessage(message, domain);
+    }-*/;
+
+    private static native void newUrl(String newUrl)/*-{
+        $wnd.history.pushState(newUrl, "", newUrl);
+    }-*/;
+
+
+    public static native Element getInnerDiv()/*-{
+        var iframe = $doc.getElementById("genomeViewer");
+        var innerDoc = iframe.contentDocument; // .contentWindow.document
+        if (!innerDoc) {
+            innerDoc = iframe.contentWindow.document;
+        }
+        // this is the JBrowse div
+        var genomeBrowser = innerDoc.getElementById("GenomeBrowser");
+        return genomeBrowser;
+    }-*/;
+
+    private static String getCurrentQueryParamsAsString() {
+        String returnString = "";
+        if (currentQueryParams == null) {
+            return returnString;
+        }
+
+        for (String key : currentQueryParams.keySet()) {
+            if (!reservedList.contains(key)) {
+                for (String value : currentQueryParams.get(key)) {
+                    returnString += "&" + key + "=" + value;
+                }
+            }
+        }
+        return returnString;
+    }
+
+    public static void updateGenomicViewer(boolean forceReload, boolean forceUrl) {
+        if (currentSequence == null) {
+            GWT.log("Current sequence not set");
+            return;
+        }
+        if (currentStartBp != null && currentEndBp != null) {
+            updateGenomicViewerForLocation(currentSequence.getName(), currentStartBp, currentEndBp, forceReload, forceUrl);
+        } else {
+            updateGenomicViewerForLocation(currentSequence.getName(), currentSequence.getStart(), currentSequence.getEnd(), forceReload, forceUrl);
+        }
+    }
+
+    public void setAppState(AppStateInfo appStateInfo) {
+        trackPanel.clear();
+        organismInfoList = appStateInfo.getOrganismList();
+        currentSequence = appStateInfo.getCurrentSequence();
+        currentOrganism = appStateInfo.getCurrentOrganism();
+        currentStartBp = appStateInfo.getCurrentStartBp();
+        currentEndBp = appStateInfo.getCurrentEndBp();
+
+        if (currentSequence != null) {
+            sequenceSuggestBox.setText(currentSequence.getName());
+        }
+
+
+        organismListBox.clear();
+        for (OrganismInfo organismInfo : organismInfoList) {
+            organismListBox.addItem(organismInfo.getName(), organismInfo.getId());
+            if (currentOrganism.getId().equals(organismInfo.getId())) {
+                organismListBox.setSelectedIndex(organismListBox.getItemCount() - 1);
+            }
+        }
+
+        if (currentOrganism != null) {
+            updatePermissionsForOrganism();
+            updateGenomicViewer(true, true);
+        }
+
+
+        Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
+            @Override
+            public boolean execute() {
+                Annotator.eventBus.fireEvent(new OrganismChangeEvent(OrganismChangeEvent.Action.LOADED_ORGANISMS));
+                return false;
+            }
+        }, 500);
+    }
+
+    public void getAppState() {
+        String url = Annotator.getRootUrl() + "annotator/getAppState";
+        url += "?" + FeatureStringEnum.CLIENT_TOKEN.getValue() + "=" + Annotator.getClientToken();
+        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
+        builder.setHeader("Content-type", "application/x-www-form-urlencoded");
+        final LoadingDialog loadingDialog = new LoadingDialog();
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue j = JSONParser.parseStrict(response.getText());
+                JSONObject obj = j.isObject();
+                if (obj != null && obj.containsKey("error")) {
+//                    Bootbox.alert(obj.get("error").isString().stringValue());
+                    loadingDialog.hide();
+                } else {
+                    loadingDialog.hide();
+                    AppStateInfo appStateInfo = AppInfoConverter.convertFromJson(obj);
+                    setAppState(appStateInfo);
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                loadingDialog.hide();
+                Bootbox.alert("Error loading organisms");
+            }
+        };
+        try {
+            builder.setCallback(requestCallback);
+            builder.send();
+        } catch (RequestException e) {
+            loadingDialog.hide();
+            Bootbox.alert(e.getMessage());
+        }
+    }
+
+    @UiHandler("cancelPasswordButton")
+    void cancelEditUserPassword(ClickEvent event) {
+        editUserModal.hide();
+    }
+
+
+    @UiHandler("savePasswordButton")
+    void saveEditUserPassword(ClickEvent event) {
+        UserInfo currentUser = MainPanel.getInstance().getCurrentUser();
+        if (editMyPasswordInput.getText().equals(editMyPasswordInputRepeat.getText())) {
+            currentUser.setPassword(editMyPasswordInput.getText());
+        } else {
+            editUserAlertText.setVisible(true);
+            editUserAlertText.setText("Passwords do not match");
+            return;
+        }
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+//                {"error":"Failed to update the user You have insufficient permissions [write < administrate] to perform this operation"}
+                if (response.getText().startsWith("{\"error\":")) {
+                    JSONObject errorJsonObject = JSONParser.parseStrict(response.getText()).isObject();
+                    String errorMessage = errorJsonObject.get("error").isString().stringValue();
+
+                    editUserAlertText.setType(AlertType.DANGER);
+                    editUserAlertText.setVisible(true);
+                    editUserAlertText.setText(errorMessage);
+                    return;
+                }
+                savePasswordButton.setEnabled(false);
+                cancelPasswordButton.setEnabled(false);
+                editUserAlertText.setType(AlertType.SUCCESS);
+                editUserAlertText.setVisible(true);
+                editUserAlertText.setText("Saved!");
+                Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
+                    @Override
+                    public boolean execute() {
+                        editUserModal.setFade(true);
+                        editUserModal.hide();
+                        return false;
+                    }
+                }, 1000);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                editUserAlertText.setVisible(true);
+                editUserAlertText.setText("Error setting user password: " + exception.getMessage());
+                editUserModal.hide();
+            }
+        };
+        UserRestService.updateUser(requestCallback, currentUser);
+    }
+
+    @UiHandler("userName")
+    void editUserPassword(ClickEvent event) {
+        editUserHeader.setHTML("Edit password for " + currentUser.getName() + "(" + currentUser.getEmail() + ")");
+        editUserAlertText.setText("");
+        editUserAlertText.setVisible(false);
+        editMyPasswordInput.setText("");
+        editMyPasswordInputRepeat.setText("");
+        editUserModal.show();
+        savePasswordButton.setEnabled(true);
+        cancelPasswordButton.setEnabled(true);
+    }
+
+    @UiHandler("dockOpenClose")
+    void handleClick(ClickEvent event) {
+        toggleOpen();
+    }
+
+    @UiHandler("organismListBox")
+    void handleOrganismChange(ChangeEvent changeEvent) {
+        OrganismRestService.switchOrganismById(organismListBox.getSelectedValue());
+    }
+
+
+    @UiHandler("detailTabs")
+    public void onSelection(SelectionEvent<Integer> event) {
+        Annotator.setPreference(FeatureStringEnum.CURRENT_TAB.getValue(), event.getSelectedItem());
+        reloadTabPerIndex(event.getSelectedItem());
+    }
+
+    private void reloadTabPerIndex(Integer selectedItem) {
+        switch (selectedItem) {
+            case 0:
+                annotatorPanel.reload(true);
+                break;
+            case 1:
+                trackPanel.reload();
+                break;
+            case 2:
+                sequencePanel.reload(true);
+                break;
+            case 3:
+                organismPanel.reload();
+                break;
+            case 4:
+                userPanel.reload(true);
+                break;
+            case 5:
+                userGroupPanel.reload();
+                break;
+            case 6:
+                preferencePanel.reload();
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void closePanel() {
+        mainSplitPanel.setWidgetSize(eastDockPanel, 20);
+        dockOpenClose.setIcon(IconType.CHEVRON_LEFT);
+    }
+
+    private void openPanel() {
+        String dockWidth = Annotator.getPreference(FeatureStringEnum.DOCK_WIDTH.getValue());
+        if (dockWidth != null) {
+            Integer dockWidthInt = Integer.parseInt(dockWidth);
+            mainSplitPanel.setWidgetSize(eastDockPanel, dockWidthInt);
+        } else {
+            mainSplitPanel.setWidgetSize(eastDockPanel, 550);
+        }
+        dockOpenClose.setIcon(IconType.CHEVRON_RIGHT);
+    }
+
+    private void toggleOpen() {
+        if (mainSplitPanel.getWidgetSize(eastDockPanel) < 100) {
+            toggleOpen = false;
+        }
+
+        if (toggleOpen) {
+            closePanel();
+        } else {
+            openPanel();
+        }
+
+        mainSplitPanel.animate(400);
+
+        toggleOpen = !toggleOpen;
+        Annotator.setPreference(FeatureStringEnum.DOCK_OPEN.getValue(), toggleOpen);
+    }
+
+    @UiHandler("generateLink")
+    public void toggleLink(ClickEvent clickEvent) {
+        String text = "";
+        String publicUrl = URL.encode(generatePublicUrl());
+        String apolloUrl = URL.encode(generateApolloUrl());
+        text += "<div style='margin-left: 10px;'>";
+        text += "<ul>";
+        text += "<li>";
+        text += "<a href='" + publicUrl + "'>Public URL</a>";
+        text += "</li>";
+        text += "<li>";
+        text += "<a href='" + apolloUrl + "'>Logged in URL</a>";
+        text += "</li>";
+        text += "</ul>";
+        text += "</div>";
+        new LinkDialog("Links to this Location", text, true);
+    }
+
+    public String generatePublicUrl() {
+        String url2 = Annotator.getRootUrl();
+        url2 += currentOrganism.getId() + "/";
+        url2 += "jbrowse/index.html";
+        if (currentStartBp != null) {
+            url2 += "?loc=" + currentSequence.getName() + ":" + currentStartBp + ".." + currentEndBp;
+        } else {
+            url2 += "?loc=" + currentSequence.getName() + ":" + currentSequence.getStart() + ".." + currentSequence.getEnd();
+        }
+//        url2 += "&organism=" + currentOrganism.getId();
+        url2 += "&tracks=";
+
+        List<String> trackList = trackPanel.getTrackList();
+        for (int i = 0; i < trackList.size(); i++) {
+            url2 += trackList.get(i);
+            if (i < trackList.size() - 1) {
+                url2 += ",";
+            }
+        }
+        return url2;
+    }
+
+    public String generateApolloUrl() {
+        String url = Annotator.getRootUrl();
+        url += "annotator/loadLink";
+        if (currentStartBp != null) {
+            url += "?loc=" + currentSequence.getName() + ":" + currentStartBp + ".." + currentEndBp;
+        } else {
+            url += "?loc=" + currentSequence.getName() + ":" + currentSequence.getStart() + ".." + currentSequence.getEnd();
+        }
+        url += "&organism=" + currentOrganism.getId();
+        url += "&tracks=";
+
+        List<String> trackList = trackPanel.getTrackList();
+        for (int i = 0; i < trackList.size(); i++) {
+            url += trackList.get(i);
+            if (i < trackList.size() - 1) {
+                url += ",";
+            }
+        }
+        return url;
+    }
+
+    @UiHandler(value = {"logoutAndBrowsePublicGenomes"})
+    public void logoutAndBrowse(ClickEvent clickEvent) {
+        UserRestService.logout("../jbrowse");
+    }
+
+
+    @UiHandler(value = {"logoutButton", "logoutButton2"})
+    public void logout(ClickEvent clickEvent) {
+        UserRestService.logout();
+    }
+
+    public static void reloadAnnotator() {
+        GWT.log("MainPanel reloadAnnotator");
+        annotatorPanel.reload();
+    }
+
+    public static void reloadSequences() {
+        sequencePanel.reload();
+    }
+
+    public static void reloadOrganisms() {
+        organismPanel.reload();
+    }
+
+    public static void reloadUsers() {
+        userPanel.reload();
+    }
+
+    public static void reloadUserGroups() {
+        userGroupPanel.reload();
+    }
+
+    /**
+     * currRegion:{"start":6000,"end":107200,"ref":"chrI"}
+     *
+     * @param payload
+     */
+    public static void handleNavigationEvent(String payload) {
+        if (handlingNavEvent) return;
+
+        handlingNavEvent = true;
+        JSONObject navEvent = JSONParser.parseLenient(payload).isObject();
+
+        final Integer start = (int) navEvent.get("start").isNumber().doubleValue();
+        final Integer end = (int) navEvent.get("end").isNumber().doubleValue();
+        String sequenceNameString = navEvent.get("ref").isString().stringValue();
+
+        if (!sequenceNameString.equals(currentSequence.getName())) {
+            setCurrentSequence(sequenceNameString, start, end, false, true);
+            Scheduler.get().scheduleFixedPeriod(new Scheduler.RepeatingCommand() {
+                @Override
+                public boolean execute() {
+                    return handlingNavEvent;
+                }
+            }, 200);
+
+        } else {
+            sendCurrentSequenceLocation(sequenceNameString, start, end);
+        }
+
+    }
+
+    /**
+     * Features array handed in
+     *
+     * @param payload
+     */
+    public static void handleFeatureAdded(String payload) {
+        if (detailTabs.getSelectedIndex() == 0) {
+            annotatorPanel.reload();
+        }
+    }
+
+    /**
+     * Features array handed in
+     *
+     * @param payload
+     */
+    public static void handleFeatureDeleted(String payload) {
+        if (detailTabs.getSelectedIndex() == 0) {
+            Scheduler.get().scheduleDeferred(new Command() {
+                @Override
+                public void execute() {
+                    annotatorPanel.reload();
+                }
+            });
+        }
+
+    }
+
+    /**
+     * Features array handed in
+     *
+     * @param payload
+     */
+    public static void handleFeatureUpdated(String payload) {
+        if (detailTabs.getSelectedIndex() == 0) {
+            annotatorPanel.reload();
+        }
+    }
+
+
+    public static String getCurrentSequenceAsJson() {
+        if (currentSequence == null) {
+            return "{}";
+        }
+        return currentSequence.toJSON().toString();
+    }
+
+    public static boolean hasCurrentUser() {
+        return currentUser != null;
+    }
+
+    public static String getCurrentUserAsJson() {
+        if (currentUser == null) {
+            return "{}";
+        }
+        return currentUser.getJSONWithoutPassword().toString();
+    }
+
+    public static String getCurrentOrganismAsJson() {
+        if (currentOrganism == null) {
+            return "{}";
+        }
+        return currentOrganism.toJSON().toString();
+    }
+
+    @UiHandler("trackListToggle")
+    public void trackListToggleButtonHandler(ClickEvent event) {
+        useNativeTracklist = !trackListToggle.isActive();
+        trackPanel.updateTrackToggle(useNativeTracklist);
+    }
+
+
+    public static native void exportStaticMethod() /*-{
+        $wnd.reloadAnnotations = $entry(@org.bbop.apollo.gwt.client.MainPanel::reloadAnnotator());
+        $wnd.reloadSequences = $entry(@org.bbop.apollo.gwt.client.MainPanel::reloadSequences());
+        $wnd.reloadOrganisms = $entry(@org.bbop.apollo.gwt.client.MainPanel::reloadOrganisms());
+        $wnd.reloadUsers = $entry(@org.bbop.apollo.gwt.client.MainPanel::reloadUsers());
+        $wnd.reloadUserGroups = $entry(@org.bbop.apollo.gwt.client.MainPanel::reloadUserGroups());
+        $wnd.handleNavigationEvent = $entry(@org.bbop.apollo.gwt.client.MainPanel::handleNavigationEvent(Ljava/lang/String;));
+        $wnd.handleFeatureAdded = $entry(@org.bbop.apollo.gwt.client.MainPanel::handleFeatureAdded(Ljava/lang/String;));
+        $wnd.handleFeatureDeleted = $entry(@org.bbop.apollo.gwt.client.MainPanel::handleFeatureDeleted(Ljava/lang/String;));
+        $wnd.handleFeatureUpdated = $entry(@org.bbop.apollo.gwt.client.MainPanel::handleFeatureUpdated(Ljava/lang/String;));
+        $wnd.getCurrentOrganism = $entry(@org.bbop.apollo.gwt.client.MainPanel::getCurrentOrganismAsJson());
+        $wnd.getCurrentUser = $entry(@org.bbop.apollo.gwt.client.MainPanel::getCurrentUserAsJson());
+        $wnd.getCurrentSequence = $entry(@org.bbop.apollo.gwt.client.MainPanel::getCurrentSequenceAsJson());
+        $wnd.getEmbeddedVersion = $entry(
+            function apolloEmbeddedVersion() {
+                return 'ApolloGwt-2.0';
+            }
+        );
+    }-*/;
+
+    private enum TabPanelIndex {
+        ANNOTATIONS(0),
+        TRACKS(1),
+        SEQUENCES(2),
+        ORGANISM(3),
+        USERS(4),
+        GROUPS(5),
+        PREFERENCES(6),;
+
+        private int index;
+
+        TabPanelIndex(int index) {
+            this.index = index;
+        }
+
+    }
+
+    public boolean isCurrentUserAdmin() {
+        return (currentUser != null && currentUser.getRole().equals("admin"));
+    }
+
+    public UserInfo getCurrentUser() {
+        return currentUser;
+    }
+
+    public void setCurrentUser(UserInfo currentUser) {
+        this.currentUser = currentUser;
+    }
+
+    public OrganismInfo getCurrentOrganism() {
+        return currentOrganism;
+    }
+
+    public void setCurrentOrganism(OrganismInfo currentOrganism) {
+        this.currentOrganism = currentOrganism;
+    }
+
+    public List<OrganismInfo> getOrganismInfoList() {
+        return organismInfoList;
+    }
+
+    public void setOrganismInfoList(List<OrganismInfo> organismInfoList) {
+        this.organismInfoList = organismInfoList;
+    }
+
+    public static SequencePanel getSequencePanel() {
+        return sequencePanel;
+    }
+
+    public static UserPanel getUserPanel() {
+        return userPanel;
+    }
+
+    public static TrackPanel getTrackPanel() {
+        return trackPanel;
+    }
+
+    public static SequenceInfo getCurrentSequence() {
+        return currentSequence;
+    }
+
+    SequenceInfo setCurrentSequenceAndEnds(SequenceInfo newSequence) {
+        currentSequence = newSequence;
+        currentStartBp = currentSequence.getStartBp() != null ? currentSequence.getStartBp() : 0;
+        currentEndBp = currentSequence.getEndBp() != null ? currentSequence.getEndBp() : currentSequence.getLength();
+        currentSequence.setStartBp(currentStartBp);
+        currentSequence.setEndBp(currentEndBp);
+        return currentSequence;
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/MainPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/MainPanel.ui.xml
new file mode 100644
index 0000000..4d60992
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/MainPanel.ui.xml
@@ -0,0 +1,191 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:apollo="urn:import:org.bbop.apollo.gwt.client"
+        >
+    <ui:style>
+
+
+        .details-header {
+            background-color: #cccccc;
+            width: 100%;
+            border-top: 0;
+        }
+
+        .details-button {
+            display: inline;
+            cursor: pointer;
+        }
+
+        .title-tab-headers {
+            font-size: smaller;
+        }
+
+        .emptyPanel {
+            background-color: #cccccc;
+            border: solid;
+            border-color: darkgray;
+            border-width: 1px;
+        }
+
+        .logout-button {
+            margin-left: 10px;
+        }
+
+        .username {
+            display: inline;
+            font-size: smaller;
+        }
+
+        .dropdown-display:focus {
+            background-color: gray;
+        }
+
+        .dropdown-display {
+            margin-left: 5px;
+            margin-right: 5px;
+            display: inline;
+            background-color: gray;
+            color: white;
+            font-size: smaller;
+            height: 25px;
+            width: 100px;
+        }
+
+        .lookup-display:focus {
+            background-color: gray;
+        }
+
+        .lookup-display {
+            margin-left: 5px;
+            margin-right: 5px;
+            display: inline;
+            background-color: gray;
+            color: white;
+            font-size: smaller;
+            height: 25px;
+            width: 100px;
+        }
+
+        .northPanel {
+            display: inline;
+            padding: 2px;
+            background-color: lightgrey;
+        }
+
+        .linkStyle {
+            display: inline;
+            margin-left: 5px;
+            font-size: smaller;
+        }
+
+        .buffer {
+            margin-top: 5px;
+        }
+        .trackButton{
+            outline:none !important;
+            margin-left: 0;
+            margin-right: 0;
+            padding-left: 0;
+            padding-right: 0;
+        }
+
+    </ui:style>
+    <gwt:SplitLayoutPanel ui:field="mainSplitPanel">
+        <gwt:center>
+            <gwt:NamedFrame ui:field="frame" width="100%" height="100%" visible="true" name="genomeViewer"/>
+        </gwt:center>
+        <gwt:east size="600">
+            <gwt:DockLayoutPanel ui:field="eastDockPanel">
+                <gwt:north size="25">
+                    <gwt:FlowPanel styleName="{style.details-header}">
+                        <b:Button icon="CHEVRON_RIGHT" ui:field="dockOpenClose"
+                                  styleName="{style.details-button}"/>
+                        <b:Button icon="LINK" ui:field="generateLink" styleName="{style.details-button}"/>
+                        <gwt:ListBox ui:field="organismListBox" styleName="{style.dropdown-display}"/>
+                        <gwt:SuggestBox ui:field="sequenceSuggestBox" stylePrimaryName="{style.lookup-display}"/>
+                        <b:Panel styleName="pull-right">
+                            <b:Button ui:field="userName" icon="USER"/>
+                            <b:Button ui:field="logoutButton" styleName="{style.logout-button}" icon="SIGN_OUT" title="Logout"/>
+                        </b:Panel>
+                        <b:Modal ui:field="notificationModal" closable="true" fade="true"  dataBackdrop="FALSE">
+                            <b:ModalHeader>
+                                <gwt:HTML>
+                                    <b>Error</b>
+                                </gwt:HTML>
+                            </b:ModalHeader>
+                            <b:ModalBody>
+                                <b:Alert type="WARNING" ui:field="alertText">
+                                    <b:Icon type="WARNING"/>
+                                </b:Alert>
+                                <b:Button text="Logout" ui:field="logoutButton2" styleName="{style.logout-button}"
+                                          icon="SIGN_OUT"/>
+                            </b:ModalBody>
+                            <b:ModalFooter>
+                                <b:Anchor ui:field="logoutAndBrowsePublicGenomes" text="Browse public genomes"
+                                          href="../jbrowse/"/>
+                            </b:ModalFooter>
+                        </b:Modal>
+                        <b:Modal ui:field="editUserModal" closable="true" dataBackdrop="FALSE">
+                            <b:ModalHeader>
+                                <gwt:HTML ui:field="editUserHeader"/>
+                            </b:ModalHeader>
+                            <b:ModalBody>
+                                <b:Alert ui:field="editUserAlertText" visible="false" dismissable="true"/>
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Password</b:InputGroupAddon>
+                                    <b:Input type="PASSWORD" ui:field="editMyPasswordInput"/>
+                                </b:InputGroup>
+                                <b:InputGroup addStyleNames="{style.buffer}">
+                                    <b:InputGroupAddon>Repeat</b:InputGroupAddon>
+                                    <b:Input type="PASSWORD" ui:field="editMyPasswordInputRepeat" text="Repeat"/>
+                                </b:InputGroup>
+                                <b:InputGroup addStyleNames="{style.buffer}">
+                                    <b:Button text="Save" type="PRIMARY" ui:field="savePasswordButton" marginRight="5"/>
+                                    <b:Button text="Cancel" type="DEFAULT" ui:field="cancelPasswordButton"/>
+                                </b:InputGroup>
+                            </b:ModalBody>
+                        </b:Modal>
+                    </gwt:FlowPanel>
+                </gwt:north>
+                <gwt:west size="25">
+                    <gwt:FlowPanel ui:field="westPanel" styleName="{style.emptyPanel}">
+                            <b:Button icon="LIST_UL" ui:field="trackListToggle" dataToggle="BUTTON" title="Show JBrowse Track Selector" addStyleNames="{style.trackButton}"/>
+                    </gwt:FlowPanel>
+                </gwt:west>
+                <gwt:center>
+                    <gwt:TabLayoutPanel barHeight="30" ui:field="detailTabs" addStyleNames="{style.title-tab-headers}">
+                        <gwt:tab>
+                            <gwt:header>Annotations</gwt:header>
+                            <apollo:AnnotatorPanel ui:field="annotatorPanel"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Tracks</gwt:header>
+                            <apollo:TrackPanel ui:field="trackPanel"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Ref Sequence</gwt:header>
+                            <apollo:SequencePanel ui:field="sequencePanel"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Organism</gwt:header>
+                            <apollo:OrganismPanel ui:field="organismPanel"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Users</gwt:header>
+                            <apollo:UserPanel ui:field="userPanel" visible="false"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Groups</gwt:header>
+                            <apollo:GroupPanel ui:field="userGroupPanel" visible="false"/>
+                        </gwt:tab>
+                        <gwt:tab>
+                            <gwt:header>Admin</gwt:header>
+                            <apollo:PreferencePanel ui:field="preferencePanel" visible="false"/>
+                        </gwt:tab>
+                    </gwt:TabLayoutPanel>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:east>
+    </gwt:SplitLayoutPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/MutableBoolean.java b/src/gwt/org/bbop/apollo/gwt/client/MutableBoolean.java
new file mode 100644
index 0000000..d27944d
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/MutableBoolean.java
@@ -0,0 +1,21 @@
+package org.bbop.apollo.gwt.client;
+
+/**
+ * Created by nathandunn on 3/21/16.
+ */
+public class MutableBoolean {
+    private Boolean booleanValue ;
+
+    public MutableBoolean(boolean booleanValue){
+        this.booleanValue = booleanValue ;
+    }
+
+
+    public Boolean getBooleanValue() {
+        return booleanValue;
+    }
+
+    public void setBooleanValue(Boolean booleanValue) {
+        this.booleanValue = booleanValue;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.java b/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.java
new file mode 100644
index 0000000..ed9efdf
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.java
@@ -0,0 +1,486 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.*;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.ListDataProvider;
+import com.google.gwt.view.client.SelectionChangeEvent;
+import com.google.gwt.view.client.SingleSelectionModel;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.OrganismInfoConverter;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEvent;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEventHandler;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.OrganismRestService;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.client.ui.CheckBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.bootbox.client.callback.ConfirmCallback;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Created by ndunn on 12/17/14.
+ */
+public class OrganismPanel extends Composite {
+
+
+    interface OrganismBrowserPanelUiBinder extends UiBinder<Widget, OrganismPanel> {
+    }
+
+
+    private static OrganismBrowserPanelUiBinder ourUiBinder = GWT.create(OrganismBrowserPanelUiBinder.class);
+    @UiField
+    TextBox organismName;
+    @UiField
+    TextBox blatdb;
+    @UiField
+    CheckBox publicMode;
+    @UiField
+    TextBox genus;
+    @UiField
+    TextBox species;
+    @UiField
+    TextBox sequenceFile;
+    DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+    @UiField(provided = true)
+    DataGrid<OrganismInfo> dataGrid = new DataGrid<OrganismInfo>(10, tablecss);
+    @UiField
+    Button newButton;
+    @UiField
+    Button createButton;
+    @UiField
+    Button cancelButton;
+    @UiField
+    Button deleteButton;
+    @UiField(provided = true)
+    WebApolloSimplePager pager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+    @UiField
+    TextBox nonDefaultTranslationTable;
+
+    boolean creatingNewOrganism = false; // a special flag for handling the clearSelection event when filling out new organism info
+    boolean savingNewOrganism = false; // a special flag for handling the clearSelection event when filling out new organism info
+
+    final LoadingDialog loadingDialog;
+    final ErrorDialog errorDialog;
+    private ListDataProvider<OrganismInfo> dataProvider = new ListDataProvider<>();
+    private List<OrganismInfo> organismInfoList = dataProvider.getList();
+    private final SingleSelectionModel<OrganismInfo> singleSelectionModel = new SingleSelectionModel<>();
+
+    public OrganismPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+        loadingDialog = new LoadingDialog("Processing ...", null, false);
+        errorDialog = new ErrorDialog("Error", "Organism directory must be an absolute path pointing to 'trackList.json'", false, true);
+
+        TextColumn<OrganismInfo> organismNameColumn = new TextColumn<OrganismInfo>() {
+            @Override
+            public String getValue(OrganismInfo organism) {
+                return organism.getName();
+            }
+        };
+        Column<OrganismInfo, Number> annotationsNameColumn = new Column<OrganismInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(OrganismInfo object) {
+                return object.getNumFeatures();
+            }
+        };
+        Column<OrganismInfo, Number> sequenceColumn = new Column<OrganismInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(OrganismInfo object) {
+                return object.getNumSequences();
+            }
+        };
+
+        sequenceColumn.setSortable(true);
+        organismNameColumn.setSortable(true);
+        annotationsNameColumn.setSortable(true);
+
+        Annotator.eventBus.addHandler(OrganismChangeEvent.TYPE, new OrganismChangeEventHandler() {
+            @Override
+            public void onOrganismChanged(OrganismChangeEvent organismChangeEvent) {
+                organismInfoList.clear();
+                organismInfoList.addAll(MainPanel.getInstance().getOrganismInfoList());
+            }
+        });
+
+        dataGrid.setLoadingIndicator(new HTML("Calculating Annotations ... "));
+        dataGrid.addColumn(organismNameColumn, "Name");
+        dataGrid.addColumn(annotationsNameColumn, "Annotations");
+        SafeHtmlHeader safeHtmlHeader = new SafeHtmlHeader(new SafeHtml() {
+            @Override
+            public String asString() {
+                return "<div style=\"text-align: right;\">Ref Sequences</p>";
+            }
+        });
+        dataGrid.addColumn(sequenceColumn, safeHtmlHeader);
+//        dataGrid.addColumn(sequenceColumn, "Ref Sequences");
+        dataGrid.setEmptyTableWidget(new Label("No organisms available. Add new organisms using the form field."));
+
+
+        singleSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                if (!creatingNewOrganism) {
+                    loadOrganismInfo();
+                    changeButtonSelection();
+                } else {
+                    creatingNewOrganism = false;
+                }
+            }
+        });
+        dataGrid.setSelectionModel(singleSelectionModel);
+
+        dataProvider.addDataDisplay(dataGrid);
+        pager.setDisplay(dataGrid);
+
+
+        dataGrid.addDomHandler(new DoubleClickHandler() {
+            @Override
+            public void onDoubleClick(DoubleClickEvent event) {
+                if (singleSelectionModel.getSelectedObject() != null) {
+                    String orgId = singleSelectionModel.getSelectedObject().getId();
+                    if (!MainPanel.getInstance().getCurrentOrganism().getId().equals(orgId)) {
+                        OrganismRestService.switchOrganismById(orgId);
+                    }
+                }
+            }
+        }, DoubleClickEvent.getType());
+
+        List<OrganismInfo> trackInfoList = dataProvider.getList();
+
+        ColumnSortEvent.ListHandler<OrganismInfo> sortHandler = new ColumnSortEvent.ListHandler<OrganismInfo>(trackInfoList);
+        dataGrid.addColumnSortHandler(sortHandler);
+        sortHandler.setComparator(organismNameColumn, new Comparator<OrganismInfo>() {
+            @Override
+            public int compare(OrganismInfo o1, OrganismInfo o2) {
+                return o1.getName().compareTo(o2.getName());
+            }
+        });
+        sortHandler.setComparator(annotationsNameColumn, new Comparator<OrganismInfo>() {
+            @Override
+            public int compare(OrganismInfo o1, OrganismInfo o2) {
+                return o1.getNumFeatures() - o2.getNumFeatures();
+            }
+        });
+        sortHandler.setComparator(sequenceColumn, new Comparator<OrganismInfo>() {
+            @Override
+            public int compare(OrganismInfo o1, OrganismInfo o2) {
+                return o1.getNumSequences() - o2.getNumSequences();
+            }
+        });
+
+
+    }
+
+    public void loadOrganismInfo() {
+        loadOrganismInfo(singleSelectionModel.getSelectedObject());
+    }
+
+    public void loadOrganismInfo(OrganismInfo organismInfo) {
+        if (organismInfo == null) {
+            setNoSelection();
+            return;
+        }
+
+        setTextEnabled(organismInfo.isEditable());
+
+        GWT.log("loadOrganismInfo setValue " + organismInfo.getPublicMode());
+        Boolean isEditable = organismInfo.isEditable()||MainPanel.getInstance().isCurrentUserAdmin();
+
+        organismName.setText(organismInfo.getName());
+        organismName.setEnabled(isEditable);
+
+        blatdb.setText(organismInfo.getBlatDb());
+        blatdb.setEnabled(isEditable);
+
+        genus.setText(organismInfo.getGenus());
+        genus.setEnabled(isEditable);
+
+        species.setText(organismInfo.getSpecies());
+        species.setEnabled(isEditable);
+
+        sequenceFile.setText(organismInfo.getDirectory());
+        sequenceFile.setEnabled(isEditable);
+
+        publicMode.setValue(organismInfo.getPublicMode());
+        publicMode.setEnabled(isEditable);
+
+        nonDefaultTranslationTable.setText(organismInfo.getNonDefaultTranslationTable());
+        nonDefaultTranslationTable.setEnabled(isEditable);
+
+        deleteButton.setVisible(isEditable);
+        deleteButton.setEnabled(isEditable);
+    }
+
+    private class UpdateInfoListCallback implements RequestCallback {
+
+        @Override
+        public void onResponseReceived(Request request, Response response) {
+            JSONValue j = JSONParser.parseStrict(response.getText());
+            JSONObject obj = j.isObject();
+            deleteButton.setText("Delete Organism");
+            if (obj != null && obj.containsKey("error")) {
+                Bootbox.alert(obj.get("error").isString().stringValue());
+                changeButtonSelection();
+                setTextEnabled(false);
+                clearTextBoxes();
+                singleSelectionModel.clear();
+            } else {
+                List<OrganismInfo> organismInfoList = OrganismInfoConverter.convertJSONStringToOrganismInfoList(response.getText());
+                dataGrid.setSelectionModel(singleSelectionModel);
+                MainPanel.getInstance().getOrganismInfoList().clear();
+                MainPanel.getInstance().getOrganismInfoList().addAll(organismInfoList);
+                changeButtonSelection();
+                OrganismChangeEvent organismChangeEvent = new OrganismChangeEvent(organismInfoList);
+                organismChangeEvent.setAction(OrganismChangeEvent.Action.LOADED_ORGANISMS);
+                Annotator.eventBus.fireEvent(organismChangeEvent);
+
+                // in the case where we just add one . . .we should refresh the app state
+                if (organismInfoList.size() == 1) {
+                    MainPanel.getInstance().getAppState();
+                }
+            }
+            if (savingNewOrganism) {
+                savingNewOrganism = false;
+                setNoSelection();
+                changeButtonSelection(false);
+                loadingDialog.hide();
+                Window.Location.reload();
+            }
+        }
+
+        @Override
+        public void onError(Request request, Throwable exception) {
+            loadingDialog.hide();
+            Bootbox.alert("Error: " + exception);
+        }
+    }
+
+
+    @UiHandler("newButton")
+    public void handleAddNewOrganism(ClickEvent clickEvent) {
+        creatingNewOrganism = true;
+        clearTextBoxes();
+        singleSelectionModel.clear();
+
+        createButton.setText("Create Organism");
+        deleteButton.setText("Delete Organism");
+        newButton.setEnabled(false);
+        cancelButton.setEnabled(MainPanel.getInstance().isCurrentUserAdmin());
+        createButton.setEnabled(MainPanel.getInstance().isCurrentUserAdmin());
+
+        createButton.setVisible(MainPanel.getInstance().isCurrentUserAdmin());
+        cancelButton.setVisible(MainPanel.getInstance().isCurrentUserAdmin());
+        newButton.setVisible(MainPanel.getInstance().isCurrentUserAdmin());
+        deleteButton.setVisible(MainPanel.getInstance().isCurrentUserAdmin());
+
+
+        setTextEnabled(MainPanel.getInstance().isCurrentUserAdmin());
+    }
+
+    @UiHandler("createButton")
+    public void handleSaveNewOrganism(ClickEvent clickEvent) {
+
+        if (!sequenceFile.getText().startsWith("/")) {
+            errorDialog.show();
+            return;
+        }
+
+        GWT.log("handleSaveNewOrganism " + publicMode.getValue());
+        OrganismInfo organismInfo = new OrganismInfo();
+        organismInfo.setName(organismName.getText());
+        organismInfo.setDirectory(sequenceFile.getText());
+        organismInfo.setGenus(genus.getText());
+        organismInfo.setSpecies(species.getText());
+        organismInfo.setBlatDb(blatdb.getText());
+        organismInfo.setNonDefaultTranslationTable(nonDefaultTranslationTable.getText());
+        organismInfo.setPublicMode(publicMode.getValue());
+
+        createButton.setEnabled(false);
+        createButton.setText("Processing");
+        savingNewOrganism = true;
+
+        OrganismRestService.createOrganism(new UpdateInfoListCallback(), organismInfo);
+        loadingDialog.show();
+    }
+
+
+    @UiHandler("cancelButton")
+    public void handleCancelNewOrganism(ClickEvent clickEvent) {
+        newButton.setEnabled(MainPanel.getInstance().isCurrentUserAdmin());
+        deleteButton.setVisible(false);
+        createButton.setVisible(false);
+        cancelButton.setVisible(false);
+        setNoSelection();
+    }
+
+    @UiHandler("deleteButton")
+    public void handleDeleteOrganism(ClickEvent clickEvent) {
+        OrganismInfo organismInfo = singleSelectionModel.getSelectedObject();
+        if (organismInfo == null) return;
+        if (organismInfo.getNumFeatures() > 0) {
+            new ErrorDialog("Cannot delete organism '" + organismInfo.getName() + "'", "You must first remove " + singleSelectionModel.getSelectedObject().getNumFeatures() + " annotations before deleting organism '"+organismInfo.getName()+"'.  Please see our <a href='../WebServices/'>Web Services API</a> from the 'Help' menu for more details on how to perform this operation in bulk.", true, true);
+            return;
+        }
+        Bootbox.confirm("Are you sure you want to delete organism " + singleSelectionModel.getSelectedObject().getName() + "?", new ConfirmCallback() {
+            @Override
+            public void callback(boolean result) {
+                if (result) {
+                    deleteButton.setEnabled(false);
+                    deleteButton.setText("Processing");
+                    savingNewOrganism = true;
+                    OrganismRestService.deleteOrganism(new UpdateInfoListCallback(), singleSelectionModel.getSelectedObject());
+                    loadingDialog.show();
+                }
+            }
+        });
+    }
+
+
+    @UiHandler("organismName")
+    public void handleOrganismNameChange(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setName(organismName.getText());
+            updateOrganismInfo();
+        }
+    }
+
+    @UiHandler("blatdb")
+    public void handleBlatDbChange(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setBlatDb(blatdb.getText());
+            updateOrganismInfo();
+        }
+    }
+
+    @UiHandler("nonDefaultTranslationTable")
+    public void handleNonDefaultTranslationTable(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setNonDefaultTranslationTable(nonDefaultTranslationTable.getText());
+            updateOrganismInfo();
+        }
+    }
+
+    @UiHandler("publicMode")
+    public void handlePublicModeChange(ChangeEvent changeEvent) {
+        GWT.log("Handling mode change " + publicMode.getValue());
+        if (singleSelectionModel.getSelectedObject() != null) {
+            GWT.log("Handling mode not null " + publicMode.getValue());
+            singleSelectionModel.getSelectedObject().setPublicMode(publicMode.getValue());
+            updateOrganismInfo();
+        }
+    }
+
+    @UiHandler("species")
+    public void handleSpeciesChange(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setSpecies(species.getText());
+            updateOrganismInfo();
+        }
+    }
+
+    @UiHandler("genus")
+    public void handleGenusChange(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setGenus(genus.getText());
+            updateOrganismInfo();
+        }
+    }
+
+
+    @UiHandler("sequenceFile")
+    public void handleOrganismDirectory(ChangeEvent changeEvent) {
+        if (singleSelectionModel.getSelectedObject() != null) {
+            singleSelectionModel.getSelectedObject().setDirectory(sequenceFile.getText());
+            updateOrganismInfo();
+        }
+    }
+
+    private void updateOrganismInfo() {
+        updateOrganismInfo(false);
+    }
+
+    private void updateOrganismInfo(boolean forceReload) {
+        OrganismRestService.updateOrganismInfo(singleSelectionModel.getSelectedObject(), forceReload);
+    }
+
+
+    public void reload() {
+        dataGrid.redraw();
+    }
+
+    // Clear textboxes and make them unselectable
+    private void setNoSelection() {
+
+        clearTextBoxes();
+        setTextEnabled(false);
+
+        deleteButton.setVisible(false);
+    }
+
+    private void changeButtonSelection() {
+        changeButtonSelection(singleSelectionModel.getSelectedObject() != null);
+    }
+
+    // Set the button states/visibility depending on whether there is a selection or not
+    private void changeButtonSelection(boolean selection) {
+        Boolean isAdmin = MainPanel.getInstance().isCurrentUserAdmin();
+        if (selection) {
+            newButton.setEnabled(isAdmin);
+            newButton.setVisible(isAdmin);
+            deleteButton.setVisible(isAdmin);
+            createButton.setVisible(false);
+            cancelButton.setVisible(false);
+        } else {
+            newButton.setEnabled(isAdmin);
+            newButton.setVisible(isAdmin);
+            createButton.setVisible(false);
+            cancelButton.setVisible(false);
+            deleteButton.setVisible(false);
+        }
+    }
+
+    //Utility function for toggling the textboxes (gray out)
+    private void setTextEnabled(boolean enabled) {
+        sequenceFile.setEnabled(enabled);
+        organismName.setEnabled(enabled);
+        genus.setEnabled(enabled);
+        species.setEnabled(enabled);
+        blatdb.setEnabled(enabled);
+        nonDefaultTranslationTable.setEnabled(enabled);
+        publicMode.setEnabled(enabled);
+    }
+
+    //Utility function for clearing the textboxes ("")
+    private void clearTextBoxes() {
+        organismName.setText("");
+        sequenceFile.setText("");
+        genus.setText("");
+        species.setText("");
+        blatdb.setText("");
+        nonDefaultTranslationTable.setText("");
+        publicMode.setValue(false);
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.ui.xml
new file mode 100644
index 0000000..6953183
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/OrganismPanel.ui.xml
@@ -0,0 +1,116 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+             xmlns:wa="urn:import:org.bbop.apollo.gwt.client"
+        >
+    <ui:style>
+
+        .widgetPanel {
+            margin-bottom: 5px;
+        }
+
+        .organismTable {
+            margin-left: 10px;
+            margin-top: 10px;
+            vertical-align: middle;
+        }
+        .container {
+            margin-left: 10px;
+            margin-top: 10px;
+        }
+
+        .row {
+            margin-left: 0px;
+            margin-right: 10px;
+        }
+        .pager {
+            width: 100%;
+            margin-left: auto;
+            margin-right: auto;
+        }
+        .inline-button{
+            display: inline;
+        }
+
+    </ui:style>
+    <gwt:DockLayoutPanel>
+        <gwt:center>
+            <gwt:DockLayoutPanel>
+                <gwt:north size="25">
+                    <wa:WebApolloSimplePager ui:field="pager" styleName="{style.pager}"/>
+                </gwt:north>
+                <gwt:center>
+                    <cellview:DataGrid ui:field="dataGrid" styleName="{style.organismTable}"/>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:center>
+        <gwt:south size="350">
+            <gwt:TabLayoutPanel barHeight="35">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" styleName="{style.container}">
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Name</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="organismName" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Genus</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="genus" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Species</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="species" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Directory</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="sequenceFile" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Search database</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="blatdb" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:InputGroup>
+                                    <b:InputGroupAddon>Non-default Translation Table</b:InputGroupAddon>
+                                    <b:TextBox autoComplete="false" ui:field="nonDefaultTranslationTable" enabled="false"/>
+                                </b:InputGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row styleName="{style.row}">
+                            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                                <b:Button ui:field="newButton" text="Add New Organism" enabled="true"/>
+                                <b:Button ui:field="deleteButton" text="Delete Organism" visible="false"/>
+                                <b:Button ui:field="createButton" text="Save New Organism" visible="false"/>
+                                <b:Button ui:field="cancelButton" text="Cancel" visible="false"/>
+                                <b:CheckBox ui:field="publicMode" text="Public" enabled="true" />
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+            </gwt:TabLayoutPanel>
+        </gwt:south>
+
+    </gwt:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.java b/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.java
new file mode 100644
index 0000000..aa0df31
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.java
@@ -0,0 +1,103 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.*;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 1/11/15.
+ */
+public class PreferencePanel extends Composite {
+    interface PreferencePanelUiBinder extends UiBinder<Widget, PreferencePanel> {
+    }
+
+    private static PreferencePanelUiBinder ourUiBinder = GWT.create(PreferencePanelUiBinder.class);
+    @UiField
+    HTML adminPanel;
+//    @UiField
+//    FlexTable statusList;
+//    @UiField
+//    TextBox newStatusField;
+//    @UiField
+//    Button newStatusButton;
+
+    public void reload(){
+        String url = "annotator/adminPanel";
+        String rootUrl = Annotator.getRootUrl();
+        if(!url.startsWith(rootUrl)){
+            url = rootUrl+url;
+        }
+        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
+
+        try {
+            Request request = builder.sendRequest(null, new RequestCallback() {
+
+                public void onResponseReceived(Request request, Response response) {
+                    if (200 == response.getStatusCode()) {
+                        adminPanel.setHTML(response.getText());
+                        // Process the response in response.getText()
+                    } else {
+                        adminPanel.setHTML("Problem loading admin page");
+                        // Handle the error.  Can get the status text from response.getStatusText()
+                    }
+                }
+
+
+                public void onError(Request request, Throwable exception) {
+                    Bootbox.alert(exception.toString());
+                }
+            });
+        } catch (RequestException e) {
+            Bootbox.alert(e.toString());
+        }
+    }
+
+    public PreferencePanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+
+        reload();
+
+
+//        /annotator/adminPanel
+
+
+
+    }
+
+//    public PreferencePanel() {
+//        initWidget(ourUiBinder.createAndBindUi(this));
+//
+//
+//        statusList.setHTML(0,0,"Status");
+//        statusList.setHTML(0,1,"");
+//        statusList.setHTML(0,2,"# of Annotations");
+//
+//        statusList.setHTML(1,0,"Approve");
+//        statusList.setHTML(2,0,"Delete");
+//        statusList.setHTML(3,0,"Replace");
+//        statusList.setHTML(4,0,"Awaiting");
+//
+////        statusList.setWidget(1, 1, new Button("X"));
+////        statusList.setWidget(2, 1, new Button("X"));
+//        statusList.setWidget(3, 1, new Button("X"));
+////        statusList.setWidget(4, 1, new Button("X"));
+//
+//        statusList.setHTML(1,2,"3");
+//        statusList.setHTML(2,2,"4");
+//        statusList.setHTML(3,2,"0");
+//        statusList.setHTML(4,2,"10");
+//    }
+//
+//    @UiHandler("newStatusButton")
+//    public void newStatusButton(ClickEvent clickEvent){
+//        String newText = newStatusField.getText();
+//        int rowCount = statusList.getRowCount();
+//        statusList.setHTML(rowCount,0,newText);
+//        statusList.setWidget(rowCount,1, new Button("X"));
+//        newStatusField.setText("");
+//    }
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.ui.xml
new file mode 100644
index 0000000..c907053
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/PreferencePanel.ui.xml
@@ -0,0 +1,66 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+        >
+    <ui:style>
+        .preferenceContainer {
+            padding-left: 40px;
+            margin-left: 40px;
+            width: 100%;
+        }
+
+        .statusPanel table {
+            border-top: 1px solid #DFDFDF;
+            border-collapse: collapse;
+            width: 100%;
+            margin-bottom: 1em;
+        }
+
+        .statusPanel td {
+            line-height: 1.5em;
+            padding: 0.5em 0.6em;
+            text-align: left;
+            vertical-align: top;
+        }
+
+        .statusPanel tr{
+            border: 0 ;
+        }
+
+    </ui:style>
+    <b:Container fluid="true" styleName="{style.preferenceContainer}">
+        <gwt:HTML ui:field="adminPanel">
+
+        </gwt:HTML>
+        <!--<b:Row visible="false">-->
+            <!--<b:Column size="MD_4" styleName="{style.widgetPanel}">-->
+                <!--<b:Form type="INLINE">-->
+                    <!--<b:FieldSet>-->
+                        <!--<b:FormGroup>-->
+                            <!--<b:InlineCheckBox text="Use CDS for New Transcripts"/>-->
+                            <!--<b:InlineCheckBox text="Partial Translation Allowed"/>-->
+                        <!--</b:FormGroup>-->
+                    <!--</b:FieldSet>-->
+                <!--</b:Form>-->
+            <!--</b:Column>-->
+        <!--</b:Row>-->
+        <!--<b:Row visible="false">-->
+            <!--<b:Column size="MD_2" styleName="{style.statusPanel}">-->
+                <!--<gwt:HTML>-->
+                    <!--<b>Status</b>-->
+                <!--</gwt:HTML>-->
+            <!--</b:Column>-->
+            <!--<b:Column size="MD_4" styleName="{style.statusPanel}">-->
+                <!--<gwt:FlexTable ui:field="statusList"/>-->
+            <!--</b:Column>-->
+        <!--</b:Row>-->
+        <!--<b:Row visible="false">-->
+            <!--<b:Column size="MD_6" styleName="{style.statusPanel}">-->
+                <!--<gwt:TextBox ui:field="newStatusField"/>-->
+                <!--<gwt:Button ui:field="newStatusButton" text="Add Status"/>-->
+            <!--</b:Column>-->
+        <!--</b:Row>-->
+    </b:Container>
+
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/ReferenceSequenceOracle.java b/src/gwt/org/bbop/apollo/gwt/client/ReferenceSequenceOracle.java
new file mode 100644
index 0000000..64d181b
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/ReferenceSequenceOracle.java
@@ -0,0 +1,74 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
+import com.google.gwt.user.client.ui.SuggestOracle;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ndunn on 4/24/15.
+ */
+public class ReferenceSequenceOracle extends MultiWordSuggestOracle{
+
+    private final String rootUrl = Annotator.getRootUrl() + "sequence/lookupSequenceByName/?q=";
+
+    @Override
+    public void requestSuggestions(final SuggestOracle.Request suggestRequest, final Callback suggestCallback) {
+
+        String url = rootUrl+ suggestRequest.getQuery();
+        url += "&clientToken="+ Annotator.getClientToken();
+        RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);
+//        rb.setHeader("Content-type", "application/x-www-form-urlencoded");
+
+        try {
+            rb.sendRequest(null, new RequestCallback() {
+                @Override
+                public void onResponseReceived(com.google.gwt.http.client.Request request, com.google.gwt.http.client.Response response) {
+                    GWT.log(response.getText());
+                    JSONArray jsonArray = JSONParser.parseStrict(response.getText()).isArray();
+                    createSuggestion(response.getText(), response.getText());
+                    List<Suggestion> suggestionList = new ArrayList<>();
+
+
+                    for(int i = 0 ; i < jsonArray.size() ; i++){
+                        final String value = jsonArray.get(i).isString().stringValue();
+                        Suggestion suggestion = new Suggestion() {
+                            @Override
+                            public String getDisplayString() {
+                                return value ;
+                            }
+
+                            @Override
+                            public String getReplacementString() {
+                                return value ;
+                            }
+                        };
+                        suggestionList.add(suggestion);
+                    }
+
+                    SuggestOracle.Response r = new SuggestOracle.Response();
+                    r.setSuggestions(suggestionList);
+                    suggestCallback.onSuggestionsReady(suggestRequest,r);
+                }
+
+                @Override
+                public void onError(com.google.gwt.http.client.Request request, Throwable exception) {
+                    Bootbox.alert("Error: "+exception);
+                }
+            });
+        } catch (RequestException e) {
+            e.printStackTrace();
+            Bootbox.alert("Request exception via " + e);
+        }
+
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/RegisterDialog.java b/src/gwt/org/bbop/apollo/gwt/client/RegisterDialog.java
new file mode 100644
index 0000000..bc70796
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/RegisterDialog.java
@@ -0,0 +1,144 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 3/17/15.
+ */
+//  TODO: this needs to be moved into UIBinder into its own class
+public class RegisterDialog extends DialogBox {
+
+    // TODO: move to UIBinder
+    private VerticalPanel panel = new VerticalPanel();
+    private Grid grid = new Grid(6, 2);
+    private Button okButton = new Button("Register & Login");
+    private TextBox username = new TextBox();
+    private TextBox firstNameBox = new TextBox();
+    private TextBox lastNameBox = new TextBox();
+    private PasswordTextBox passwordTextBox = new PasswordTextBox();
+    private PasswordTextBox passwordRepeatTextBox = new PasswordTextBox();
+    private HorizontalPanel horizontalPanel = new HorizontalPanel();
+    private CheckBox rememberMeCheckBox = new CheckBox("Remember me");
+    private HTML errorMessage = new HTML("");
+
+    public RegisterDialog() {
+        // Set the dialog box's caption.
+        setText("Register First Admin User");
+        // Enable animation.
+        setAnimationEnabled(true);
+        // Enable glass background.
+        setGlassEnabled(true);
+
+        grid.setHTML(0, 0, "Username (email)");
+        grid.setWidget(0, 1, username);
+        grid.setHTML(1, 0, "Password");
+        grid.setWidget(1, 1, passwordTextBox);
+        grid.setHTML(2, 0, "Repeat Password");
+        grid.setWidget(2, 1, passwordRepeatTextBox);
+        grid.setHTML(3, 0, "First Name");
+        grid.setWidget(3, 1, firstNameBox);
+        grid.setHTML(4, 0, "Last Name");
+        grid.setWidget(4, 1, lastNameBox);
+        grid.setHTML(5, 0, "");
+        grid.setWidget(5, 1, errorMessage);
+        panel.add(grid);
+
+
+        horizontalPanel.add(rememberMeCheckBox);
+        horizontalPanel.add(new HTML("    "));
+        horizontalPanel.add(okButton);
+        panel.add(horizontalPanel);
+        // DialogBox is a SimplePanel, so you have to set its widget property to
+        // whatever you want its contents to be.
+        okButton.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent event) {
+                doRegister();
+            }
+        });
+        setWidget(panel);
+    }
+
+    @Override
+    public void onPreviewNativeEvent(NativePreviewEvent e) {
+        NativeEvent nativeEvent = e.getNativeEvent();
+        if ("keydown".equals(nativeEvent.getType())) {
+            if (nativeEvent.getKeyCode() == KeyCodes.KEY_ENTER) {
+                doRegister();
+            }
+        }
+    }
+
+    public void doRegister() {
+        clearError();
+        if (passwordTextBox.getText().length() < 4) {
+            setError("Passwords must be at least 4 characters");
+        }
+
+        if (!passwordTextBox.getText().equals(passwordRepeatTextBox.getText())) {
+            setError("Passwords do not match");
+            return;
+        }
+
+        String usernameText = username.getText();
+        // TODO: use a better regexp search
+        if (!usernameText.contains("@") || !usernameText.contains(".")) {
+            setError("Username does not appear to be an email");
+            return;
+        }
+        registerAdmin(username.getText().trim(), passwordTextBox.getText(), rememberMeCheckBox.getValue(),firstNameBox.getText().trim(),lastNameBox.getText().trim());
+    }
+
+    public void registerAdmin(String username, String password, Boolean rememberMe, String firstName, String lastName) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+//                Window.alert(response.getStatusCode()+"");
+//                Window.alert(response.getStatusText());
+//                Window.alert(response.getText());
+                if (response.getStatusCode() < 200 || response.getStatusCode() > 299) {
+                    setError("Problem during registration");
+                }
+                else{
+                    Window.Location.reload();
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                setError("Error registering admin: " + exception.getMessage());
+            }
+        };
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("operation", new JSONString("register"));
+        jsonObject.put("username", new JSONString(username));
+        jsonObject.put("password", new JSONString(password));
+        jsonObject.put("rememberMe", JSONBoolean.getInstance(rememberMe));
+        jsonObject.put("firstName", new JSONString(firstName));
+        jsonObject.put("lastName", new JSONString(lastName));
+        UserRestService.registerAdmin(requestCallback, jsonObject);
+    }
+
+    public void setError(String errroString) {
+        errorMessage.setHTML("<font color='red'>" + errroString + "</font>");
+    }
+
+    public void clearError(){
+        errorMessage.setHTML("");
+    }
+}
+
diff --git a/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.java b/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.java
new file mode 100644
index 0000000..d2eec0b
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.java
@@ -0,0 +1,117 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.http.client.*;
+import com.google.gwt.i18n.client.Dictionary;
+import com.google.gwt.json.client.*;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.rest.AnnotationRestService;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by Deepak on 4/28/15.
+ */
+public class RepeatRegionDetailPanel extends Composite {
+    private AnnotationInfo internalAnnotationInfo;
+   
+    interface AnnotationDetailPanelUiBinder extends UiBinder<Widget, RepeatRegionDetailPanel> {
+    }
+
+    private static AnnotationDetailPanelUiBinder ourUiBinder = GWT.create(AnnotationDetailPanelUiBinder.class);
+
+    @UiField
+    TextBox nameField;
+    @UiField
+    TextBox descriptionField;
+    @UiField
+    TextBox locationField;
+    @UiField
+    TextBox sequenceField;
+    @UiField
+    TextBox userField;
+
+    public RepeatRegionDetailPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+    }
+    
+    @UiHandler("nameField")
+    void handleNameChange(ChangeEvent e) {
+        String updatedName = nameField.getText();
+        internalAnnotationInfo.setName(updatedName);
+        updateEntity();
+    }
+    
+    @UiHandler("descriptionField")
+    void handleDescriptionChange(ChangeEvent e) {
+        String updatedDescription = descriptionField.getText();
+        internalAnnotationInfo.setDescription(updatedDescription);
+        updateEntity();
+    }
+    
+    private void enableFields(boolean enabled) {
+        nameField.setEnabled(enabled);
+        descriptionField.setEnabled(enabled);
+    }
+    
+    private void updateEntity() {
+        final AnnotationInfo updatedInfo = this.internalAnnotationInfo;
+        enableFields(false);
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+                GWT.log("successful update: " + returnValue);
+                enableFields(true);
+                Annotator.eventBus.fireEvent(new AnnotationInfoChangeEvent(updatedInfo, AnnotationInfoChangeEvent.Action.UPDATE));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating gene: " + exception);
+                enableFields(true);
+            }
+        };
+        RestService.sendRequest(requestCallback, "annotator/updateFeature/", AnnotationRestService.convertAnnotationInfoToJSONObject(this.internalAnnotationInfo));
+    }
+    
+    public void updateData(AnnotationInfo annotationInfo) {
+        GWT.log("Updating entity");
+        this.internalAnnotationInfo = annotationInfo;
+        nameField.setText(internalAnnotationInfo.getName());
+        descriptionField.setText(internalAnnotationInfo.getDescription());
+        sequenceField.setText(internalAnnotationInfo.getSequence());
+        userField.setText(internalAnnotationInfo.getOwner());
+        
+        if (internalAnnotationInfo.getMin() != null) {
+            String locationText = internalAnnotationInfo.getMin().toString();
+            locationText += " - ";
+            locationText += internalAnnotationInfo.getMax().toString();
+            locationText += " strand(";
+            locationText += internalAnnotationInfo.getStrand() > 0 ? "+" : "-";
+            locationText += ")";
+            locationField.setText(locationText);
+            locationField.setVisible(true);
+        }
+        else {
+            locationField.setVisible(false);
+        }
+        
+        setVisible(true);
+    }
+    
+    public void setEditable(boolean editable) {
+        nameField.setEnabled(editable);
+        descriptionField.setEnabled(editable);
+    }
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.ui.xml
new file mode 100644
index 0000000..0d99a04
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/RepeatRegionDetailPanel.ui.xml
@@ -0,0 +1,64 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+        >
+    <ui:style>
+
+        .container {
+            margin-left: 20px;
+            margin-right: 20px;
+            margin-top: 10px;
+        }
+
+        .widgetPanel {
+            margin-bottom: 5px;
+        }
+
+        .row {
+            margin-left: 5px;
+            margin-right: 5px;
+        }
+    </ui:style>
+
+    <b:Container fluid="true" styleName="{style.container}">
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup >
+                    <b:InputGroupAddon>Name</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="nameField" />
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Description</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="descriptionField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Location</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="locationField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Ref Sequence</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="sequenceField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Owner</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="userField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+    </b:Container>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.java b/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.java
new file mode 100644
index 0000000..2c9d51e
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.java
@@ -0,0 +1,453 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.*;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.*;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.SequenceInfo;
+import org.bbop.apollo.gwt.client.dto.SequenceInfoConverter;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEvent;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEventHandler;
+import org.bbop.apollo.gwt.client.event.UserChangeEvent;
+import org.bbop.apollo.gwt.client.event.UserChangeEventHandler;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.SequenceRestService;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+import org.gwtbootstrap3.client.ui.Button;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.client.ui.constants.ButtonType;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.select.client.ui.MultipleSelect;
+import org.gwtbootstrap3.extras.select.client.ui.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * Created by ndunn on 12/17/14.
+ */
+public class SequencePanel extends Composite {
+
+    interface SequencePanelUiBinder extends UiBinder<Widget, SequencePanel> {
+    }
+
+    private static SequencePanelUiBinder ourUiBinder = GWT.create(SequencePanelUiBinder.class);
+    @UiField
+    TextBox minFeatureLength;
+    @UiField
+    TextBox maxFeatureLength;
+
+    DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+    @UiField(provided = true)
+    DataGrid<SequenceInfo> dataGrid = new DataGrid<SequenceInfo>(50, tablecss);
+    @UiField(provided = true)
+    WebApolloSimplePager pager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+
+
+    @UiField
+    HTML sequenceName;
+    @UiField
+    Button exportAllButton;
+    @UiField
+    Button exportSelectedButton;
+    @UiField
+    MultipleSelect selectedSequenceDisplay;
+    @UiField
+    Button clearSelectionButton;
+    @UiField
+    TextBox nameSearchBox;
+    @UiField
+    HTML sequenceLength;
+    @UiField
+    Button exportGff3Button;
+    @UiField
+    Button exportFastaButton;
+    @UiField
+    Button selectSelectedButton;
+    @UiField
+    Button exportChadoButton;
+
+    private AsyncDataProvider<SequenceInfo> dataProvider;
+    private MultiSelectionModel<SequenceInfo> multiSelectionModel = new MultiSelectionModel<SequenceInfo>();
+    private SequenceInfo selectedSequenceInfo = null;
+    private Integer selectedCount = 0;
+    private Boolean exportAll = false;
+    private Boolean chadoExportStatus = false;
+
+    public SequencePanel() {
+
+        initWidget(ourUiBinder.createAndBindUi(this));
+        dataGrid.setWidth("100%");
+
+        TextColumn<SequenceInfo> nameColumn = new TextColumn<SequenceInfo>() {
+            @Override
+            public String getValue(SequenceInfo employee) {
+                return employee.getName();
+            }
+        };
+        nameColumn.setSortable(true);
+
+        Column<SequenceInfo, Number> lengthColumn = new Column<SequenceInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(SequenceInfo object) {
+                return object.getLength();
+            }
+        };
+        lengthColumn.setDefaultSortAscending(false);
+        lengthColumn.setSortable(true);
+
+        Column<SequenceInfo, Number> annotationCount = new Column<SequenceInfo, Number>(new NumberCell()) {
+            @Override
+            public Integer getValue(SequenceInfo object) {
+                return object.getCount();
+            }
+        };
+
+        annotationCount.setSortable(true);
+        dataGrid.addColumn(nameColumn, "Name");
+        dataGrid.addColumn(lengthColumn, "Length");
+        dataGrid.setColumnWidth(1, "100px");
+        dataGrid.addColumn(annotationCount, "Annotations");
+
+        dataGrid.setSelectionModel(multiSelectionModel);
+        multiSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                Set<SequenceInfo> selectedSequenceInfo = multiSelectionModel.getSelectedSet();
+                if (selectedSequenceInfo.size() == 1) {
+                    setSequenceInfo(selectedSequenceInfo.iterator().next());
+                    selectSelectedButton.setEnabled(true);
+                } else {
+                    setSequenceInfo(null);
+                }
+                if (selectedSequenceInfo.size() > 0) {
+                    exportSelectedButton.setText("Selected (" + selectedSequenceInfo.size() + ")");
+                } else {
+                    exportSelectedButton.setText("Selected");
+                }
+                exportSelectedButton.setEnabled(selectedSequenceInfo.size() > 0);
+                selectSelectedButton.setEnabled(selectedSequenceInfo.size() > 0);
+
+                updateSelectedSequenceDisplay(multiSelectionModel.getSelectedSet());
+            }
+        });
+
+        dataProvider = new AsyncDataProvider<SequenceInfo>() {
+            @Override
+            protected void onRangeChanged(HasData<SequenceInfo> display) {
+                final Range range = display.getVisibleRange();
+                final ColumnSortList sortList = dataGrid.getColumnSortList();
+                final int start = range.getStart();
+                final int length = range.getLength();
+
+                RequestCallback requestCallback = new RequestCallback() {
+                    @Override
+                    public void onResponseReceived(Request request, Response response) {
+                        JSONArray jsonArray = JSONParser.parseLenient(response.getText()).isArray();
+                        Integer sequenceCount = 0;
+                        if (jsonArray != null && jsonArray.size() > 0) {
+                            JSONObject jsonObject = jsonArray.get(0).isObject();
+                            sequenceCount = (int) jsonObject.get("sequenceCount").isNumber().doubleValue();
+                        }
+                        dataGrid.setRowCount(sequenceCount, true);
+                        dataGrid.setRowData(start, SequenceInfoConverter.convertFromJsonArray(jsonArray));
+                    }
+
+                    @Override
+                    public void onError(Request request, Throwable exception) {
+                        Bootbox.alert("error getting sequence info: " + exception);
+                    }
+                };
+
+
+                ColumnSortList.ColumnSortInfo nameSortInfo = sortList.get(0);
+                if (nameSortInfo.getColumn().isSortable()) {
+                    Column<SequenceInfo, ?> sortColumn = (Column<SequenceInfo, ?>) sortList.get(0).getColumn();
+                    Integer columnIndex = dataGrid.getColumnIndex(sortColumn);
+                    String searchColumnString = columnIndex == 0 ? "name" : columnIndex == 1 ? "length" : "count";
+                    Boolean sortNameAscending = nameSortInfo.isAscending();
+                    SequenceRestService.getSequenceForOffsetAndMax(requestCallback, nameSearchBox.getText(), start, length, searchColumnString, sortNameAscending, minFeatureLength.getText(), maxFeatureLength.getText());
+                }
+            }
+        };
+
+        ColumnSortEvent.AsyncHandler columnSortHandler = new ColumnSortEvent.AsyncHandler(dataGrid);
+        dataGrid.addColumnSortHandler(columnSortHandler);
+        dataGrid.getColumnSortList().push(nameColumn);
+        dataGrid.getColumnSortList().push(lengthColumn);
+
+
+        dataProvider.addDataDisplay(dataGrid);
+        pager.setDisplay(dataGrid);
+
+
+        // have to use a special handler instead of UiHandler for this type
+        dataGrid.addDomHandler(new DoubleClickHandler() {
+            @Override
+            public void onDoubleClick(DoubleClickEvent event) {
+                Set<SequenceInfo> sequenceInfoSet = multiSelectionModel.getSelectedSet();
+                if (sequenceInfoSet.size() == 1) {
+                    final SequenceInfo sequenceInfo = sequenceInfoSet.iterator().next();
+
+                    RequestCallback requestCallback = new RequestCallback() {
+                        @Override
+                        public void onResponseReceived(Request request, Response response) {
+                            JSONObject sequenceInfoJson = JSONParser.parseStrict(response.getText()).isObject();
+                            MainPanel mainPanel = MainPanel.getInstance();
+                            SequenceInfo currentSequence = mainPanel.setCurrentSequenceAndEnds(SequenceInfoConverter.convertFromJson(sequenceInfoJson));
+                            mainPanel.sequenceSuggestBox.setText(currentSequence.getName());
+                            Annotator.eventBus.fireEvent(new OrganismChangeEvent(OrganismChangeEvent.Action.LOADED_ORGANISMS, currentSequence.getName(),mainPanel.getCurrentOrganism().getName()));
+                            MainPanel.updateGenomicViewerForLocation(currentSequence.getName(),currentSequence.getStartBp(),currentSequence.getEndBp(),true,false);
+                        }
+
+                        @Override
+                        public void onError(Request request, Throwable exception) {
+                            Bootbox.alert("Error setting current sequence: " + exception);
+                        }
+                    };
+                    SequenceRestService.setCurrentSequence(requestCallback, sequenceInfo);
+
+                }
+            }
+        }, DoubleClickEvent.getType());
+
+        Annotator.eventBus.addHandler(OrganismChangeEvent.TYPE, new OrganismChangeEventHandler() {
+            @Override
+            public void onOrganismChanged(OrganismChangeEvent organismChangeEvent) {
+                if (organismChangeEvent.getAction().equals(OrganismChangeEvent.Action.LOADED_ORGANISMS) && (organismChangeEvent.getCurrentOrganism() == null || !organismChangeEvent.getCurrentOrganism().equals(MainPanel.getInstance().getCurrentOrganism().getName()))) {
+                    Scheduler.get().scheduleDeferred(new Command() {
+                        @Override
+                        public void execute() {
+                            selectedCount = 0;
+                            multiSelectionModel.clear();
+                            updatedExportSelectedButton();
+                            updateSelectedSequenceDisplay(multiSelectionModel.getSelectedSet());
+                            reload();
+                        }
+                    });
+                }
+            }
+        });
+
+        Annotator.eventBus.addHandler(UserChangeEvent.TYPE,
+                new UserChangeEventHandler() {
+                    @Override
+                    public void onUserChanged(UserChangeEvent authenticationEvent) {
+                        switch (authenticationEvent.getAction()) {
+                            case PERMISSION_CHANGED:
+                                PermissionEnum hiPermissionEnum = authenticationEvent.getHighestPermission();
+                                if (MainPanel.getInstance().isCurrentUserAdmin()) {
+                                    hiPermissionEnum = PermissionEnum.ADMINISTRATE;
+                                }
+                                boolean allowExport = false;
+                                switch (hiPermissionEnum) {
+                                    case ADMINISTRATE:
+                                    case WRITE:
+                                    case EXPORT:
+                                        allowExport = true;
+                                        break;
+                                    // default is false
+                                }
+                                exportAllButton.setEnabled(allowExport);
+                                exportSelectedButton.setEnabled(allowExport);
+                                selectedSequenceDisplay.setEnabled(allowExport);
+                                break;
+                        }
+                    }
+                }
+        );
+
+        Scheduler.get().scheduleFixedPeriod(new Scheduler.RepeatingCommand() {
+            @Override
+            public boolean execute() {
+                if(MainPanel.getInstance().getCurrentUser()!=null) {
+                    if (MainPanel.getInstance().isCurrentUserAdmin()) {
+                        exportChadoButton.setVisible(true);
+                        getChadoExportStatus();
+                    } else {
+                        exportChadoButton.setVisible(false);
+                    }
+                    return false ;
+                }
+                return true ;
+            }
+        },100);
+
+    }
+
+    private void updatedExportSelectedButton() {
+        if (selectedCount > 0) {
+            exportSelectedButton.setEnabled(true);
+            exportSelectedButton.setText("Selected (" + multiSelectionModel.getSelectedSet().size() + ")");
+        } else {
+            exportSelectedButton.setEnabled(false);
+            exportSelectedButton.setText("None Selected");
+        }
+    }
+
+    private void setSequenceInfo(SequenceInfo selectedObject) {
+        selectedSequenceInfo = selectedObject;
+        if (selectedSequenceInfo == null) {
+            sequenceName.setText("");
+            sequenceLength.setText("");
+        } else {
+            sequenceName.setHTML(selectedSequenceInfo.getName());
+            sequenceLength.setText(selectedSequenceInfo.getLength().toString());
+        }
+    }
+
+    @UiHandler(value = {"nameSearchBox", "minFeatureLength", "maxFeatureLength"})
+    public void handleNameSearch(KeyUpEvent keyUpEvent) {
+        pager.setPageStart(0);
+        dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+    }
+
+    @UiHandler(value = {"exportGff3Button", "exportFastaButton", "exportChadoButton"})
+    public void handleExportTypeChanged(ClickEvent clickEvent) {
+        exportGff3Button.setType(ButtonType.DEFAULT);
+        exportFastaButton.setType(ButtonType.DEFAULT);
+        exportChadoButton.setType(ButtonType.DEFAULT);
+        Button selectedButton = (Button) clickEvent.getSource();
+        switch (selectedButton.getText()) {
+            case "GFF3":
+                exportGff3Button.setType(ButtonType.PRIMARY);
+                break;
+            case "FASTA":
+                exportFastaButton.setType(ButtonType.PRIMARY);
+                break;
+            case "CHADO":
+                exportChadoButton.setType(ButtonType.PRIMARY);
+                break;
+        }
+    }
+
+
+    @UiHandler("selectSelectedButton")
+    public void handleSetSelections(ClickEvent clickEvent) {
+        GWT.log("handleSetSelection");
+
+        boolean allSelectionsSelected = findAllSelectionsSelected();
+
+        for (SequenceInfo sequenceInfo : multiSelectionModel.getSelectedSet()) {
+            if (allSelectionsSelected) {
+                if (sequenceInfo.getSelected()) {
+                    --selectedCount;
+                }
+                sequenceInfo.setSelected(false);
+            } else {
+                if (!sequenceInfo.getSelected()) {
+                    ++selectedCount;
+                }
+                sequenceInfo.setSelected(true);
+            }
+        }
+        updatedExportSelectedButton();
+        dataGrid.redraw();
+    }
+
+    private boolean findAllSelectionsSelected() {
+        for (SequenceInfo sequenceInfo : multiSelectionModel.getSelectedSet()) {
+            if (!sequenceInfo.getSelected()) return false;
+        }
+        return true;
+    }
+
+    private void exportValues(List<SequenceInfo> sequenceInfoList) {
+        OrganismInfo organismInfo = MainPanel.getInstance().getCurrentOrganism();
+        // get the type based on the active button
+        String type = null;
+        if (exportGff3Button.getType().equals(ButtonType.DANGER.PRIMARY)) {
+            type = exportGff3Button.getText();
+        } else if (exportFastaButton.getType().equals(ButtonType.DANGER.PRIMARY)) {
+            type = exportFastaButton.getText();
+        } else if (exportChadoButton.getType().equals(ButtonType.DANGER.PRIMARY)) {
+            type = exportChadoButton.getText();
+        }
+
+        ExportPanel exportPanel = new ExportPanel(organismInfo, type, exportAll, sequenceInfoList);
+        exportPanel.show();
+    }
+
+    @UiHandler("exportSelectedButton")
+    public void exportSelectedHandler(ClickEvent clickEvent) {
+        exportAll = false;
+        List<SequenceInfo> sequenceInfoList1 = new ArrayList<>();
+        for (SequenceInfo sequenceInfo : multiSelectionModel.getSelectedSet()) {
+            sequenceInfoList1.add(sequenceInfo);
+        }
+
+        GWT.log("adding selected: " + sequenceInfoList1.size());
+        exportValues(sequenceInfoList1);
+    }
+
+    @UiHandler("exportAllButton")
+    public void exportAllHandler(ClickEvent clickEvent) {
+        exportAll = true;
+        GWT.log("exporting gff3");
+
+        exportValues(new ArrayList<SequenceInfo>());
+    }
+
+    public void updateSelectedSequenceDisplay(Set<SequenceInfo> selectedSequenceInfoList) {
+        selectedSequenceDisplay.clear();
+        if (selectedSequenceInfoList.size() == 0) {
+            selectedSequenceDisplay.setEnabled(false);
+        } else {
+            selectedSequenceDisplay.setEnabled(true);
+            for (SequenceInfo s : selectedSequenceInfoList) {
+                Option option = new Option();
+                option.setValue(s.getName());
+                option.setText(s.getName());
+                selectedSequenceDisplay.add(option);
+            }
+        }
+        selectedSequenceDisplay.refresh();
+    }
+
+    @UiHandler("clearSelectionButton")
+    public void clearSelection(ClickEvent clickEvent) {
+        multiSelectionModel.clear();
+    }
+
+    public void reload() {
+        reload(false);
+    }
+
+    public void reload(Boolean forceReload) {
+        if (MainPanel.getInstance().getSequencePanel().isVisible() || forceReload) {
+            pager.setPageStart(0);
+            dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+            dataGrid.redraw();
+        }
+    }
+
+    public void getChadoExportStatus() {
+        SequenceRestService.getChadoExportStatus(this);
+    }
+
+    public void setChadoExportStatus(String exportStatus) {
+        this.chadoExportStatus = exportStatus.equals("true");
+        this.exportChadoButton.setEnabled(this.chadoExportStatus);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.ui.xml
new file mode 100644
index 0000000..aaea925
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/SequencePanel.ui.xml
@@ -0,0 +1,218 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+             xmlns:select="urn:import:org.gwtbootstrap3.extras.select.client.ui"
+             xmlns:wa="urn:import:org.bbop.apollo.gwt.client"
+
+
+>
+    <ui:style>
+        .northPanel {
+            padding: 0px;
+            margin: 10px;
+            display: inline;
+            width: 100%;
+        }
+
+        .widgetPanel {
+            padding: 5px;
+            margin: 5px;
+            display: inline-table;
+        }
+
+        .trackTable {
+            margin-left: 10px;
+            margin-top: 10px;
+            vertical-align: middle;
+        }
+
+        .dockLayoutPanel > div {
+            /* this was added to allow dropdown to be displayed instead of being hidden */
+            overflow: visible !important;
+        }
+
+        /*.trackTable tr:hover{*/
+        /*background-color: #d3d3d3;*/
+        /*}*/
+
+        .tableKeyName {
+            font-weight: bolder;
+        }
+
+        .tableValue {
+            /*font-weight: bolder;*/
+        }
+
+        .buttonPanel {
+            padding: 5px;
+            margin: 5px;
+        }
+
+        .pager {
+            width: 100%;
+            /*display: inline;*/
+            /*border-right: solid;*/
+            /*border-width: medium;*/
+            /*border-color: #000000;*/
+            margin-left: auto;
+            margin-right: auto;
+            /*padding-left: auto;*/
+            /*padding-right: auto;*/
+        }
+
+        .columnPanel {
+            /*border: solid;*/
+            /*border-width: medium;*/
+            /*border-color: #000000;*/
+            display: inline;
+            margin-left: auto;
+            margin-right: auto;
+        }</ui:style>
+    <gwt:DockLayoutPanel addStyleNames="{style.dockLayoutPanel}">
+        <gwt:north size="210">
+            <b:Container fluid="true" styleName="{style.northPanel}">
+                <b:Row>
+                    <b:Column size="MD_6" styleName="{style.widgetPanel}">
+                        <b:TextBox placeholder="Search" width="300px" ui:field="nameSearchBox"/>
+                    </b:Column>
+                </b:Row>
+                <b:Row paddingTop="4">
+                    <b:Column size="MD_2">
+                        <gwt:HTML>
+                            <h5>Length</h5>
+                        </gwt:HTML>
+                    </b:Column>
+                    <b:Column size="MD_3">
+                        <b:TextBox ui:field="minFeatureLength" placeholder="Minimum"/>
+                    </b:Column>
+                    <b:Column size="MD_3">
+                        <b:TextBox ui:field="maxFeatureLength" placeholder="Maximum"/>
+                    </b:Column>
+                </b:Row>
+                <b:Row>
+                    <b:Column size="MD_2">
+                        <gwt:HTML>
+                            <h5>Export</h5>
+                        </gwt:HTML>
+                    </b:Column>
+                    <b:Column size="MD_7" marginTop="8">
+                        <b:ButtonGroup>
+                            <b:Button size="EXTRA_SMALL" ui:field="exportGff3Button" type="PRIMARY" width="56px">GFF3
+                            </b:Button>
+                            <b:Button size="EXTRA_SMALL" ui:field="exportFastaButton" width="56px">FASTA</b:Button>
+                            <b:Button size="EXTRA_SMALL" ui:field="exportChadoButton" width="56px">CHADO</b:Button>
+                        </b:ButtonGroup>
+                    </b:Column>
+                    <b:Column size="MD_7" marginTop="8">
+                        <b:Row marginLeft="1">
+                            <select:MultipleSelect ui:field="selectedSequenceDisplay" liveSearch="true" maxOptions="5"
+                                                   enabled="false" title="Selected sequences" width="165px"
+                                                   marginLeft="1"
+
+                            />
+                            <!--<select:Select ui:field="selectedSequenceDisplay" liveSearch="true" visibleSize="5"-->
+                            <!--enabled="false"  title="Selected sequences" width="165px"-->
+                            <!--marginLeft="1"-->
+
+                            <!--/>-->
+                        </b:Row>
+                        <b:ButtonGroup>
+                            <b:Button ui:field="clearSelectionButton" icon="BAN" size="EXTRA_SMALL"
+                                      title="Clear Selection" marginLeft="1"/>
+                            <b:Button ui:field="exportAllButton" icon="DOWNLOAD" size="EXTRA_SMALL" text="All"
+                                      width="49px"/>
+                            <b:Button ui:field="exportSelectedButton" icon="DOWNLOAD" size="EXTRA_SMALL" text="Selected"
+                                      enabled="false" width="100px"/>
+                        </b:ButtonGroup>
+
+                    </b:Column>
+                </b:Row>
+            </b:Container>
+        </gwt:north>
+        <gwt:center>
+            <gwt:DockLayoutPanel>
+                <gwt:north size="30">
+                    <gwt:HTMLPanel>
+                        <table style="width:100%">
+                            <tr>
+                                <td style="width:20%">
+                                    <b:Button ui:field="selectSelectedButton" size="EXTRA_SMALL" enabled="false"
+                                              icon="CHECK_CIRCLE" marginLeft="10" visible="false"/>
+                                </td>
+                                <td align="center">
+                                    <wa:WebApolloSimplePager ui:field="pager" styleName="{style.pager}"/>
+                                </td>
+                                <td style="width:20%"/>
+                            </tr>
+                        </table>
+                    </gwt:HTMLPanel>
+                </gwt:north>
+                <gwt:center>
+                    <cellview:DataGrid ui:field="dataGrid"/>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:center>
+        <gwt:south size="0">
+            <gwt:TabLayoutPanel barHeight="35" visible="false">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row>
+                            <b:Column size="MD_2">
+                                <gwt:HTML text="Name" styleName="{style.tableKeyName}"/>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <gwt:HTML ui:field="sequenceName" styleName="{style.tableValue}"/>
+                            </b:Column>
+                        </b:Row>
+                        <!--<b:Row>-->
+                        <!--<b:Column size="MD_2">-->
+                        <!--<gwt:HTML styleName="{style.tableKeyName}">-->
+                        <!--Start-->
+                        <!--</gwt:HTML>-->
+                        <!--</b:Column>-->
+                        <!--<b:Column size="MD_2">-->
+                        <!--<gwt:HTML ui:field="sequenceStart" styleName="{style.tableValue}"/>-->
+                        <!--</b:Column>-->
+                        <!--</b:Row>-->
+                        <!--<b:Row>-->
+                        <!--<b:Column size="MD_2">-->
+                        <!--<gwt:HTML styleName="{style.tableKeyName}">-->
+                        <!--Stop-->
+                        <!--</gwt:HTML>-->
+                        <!--</b:Column>-->
+                        <!--<b:Column size="MD_2">-->
+                        <!--<gwt:HTML ui:field="sequenceStop" styleName="{style.tableValue}"/>-->
+                        <!--</b:Column>-->
+                        <!--</b:Row>-->
+                        <b:Row>
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}">
+                                    Length
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <gwt:HTML ui:field="sequenceLength" styleName="{style.tableValue}"/>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Users</gwt:header>
+                    <gwt:HTML text="user persmissions"/>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>UserGroups</gwt:header>
+                    <gwt:HTML text="usergroup persmissions"/>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Annotations</gwt:header>
+                    <gwt:HTML text="annotations"/>
+                    <!--<gwt:FlexTable ui:field="configurationTable"/>-->
+                </gwt:tab>
+            </gwt:TabLayoutPanel>
+        </gwt:south>
+
+    </gwt:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.java b/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.java
new file mode 100644
index 0000000..a44fbe7
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.java
@@ -0,0 +1,501 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.*;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.*;
+import com.google.gwt.view.client.ListDataProvider;
+import org.bbop.apollo.gwt.client.dto.TrackInfo;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEvent;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEventHandler;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.client.shared.event.HiddenEvent;
+import org.gwtbootstrap3.client.shared.event.HiddenHandler;
+import org.gwtbootstrap3.client.shared.event.ShowEvent;
+import org.gwtbootstrap3.client.shared.event.ShowHandler;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.Panel;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.client.ui.constants.HeadingSize;
+import org.gwtbootstrap3.client.ui.constants.Pull;
+import org.gwtbootstrap3.client.ui.constants.Toggle;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.toggleswitch.client.ui.ToggleSwitch;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * Created by ndunn on 12/16/14.
+ */
+public class TrackPanel extends Composite {
+
+    interface TrackUiBinder extends UiBinder<Widget, TrackPanel> {
+    }
+
+    private static TrackUiBinder ourUiBinder = GWT.create(TrackUiBinder.class);
+
+    @UiField
+    static TextBox nameSearchBox;
+    @UiField
+    HTML trackName;
+    @UiField
+    HTML trackType;
+    @UiField
+    HTML trackCount;
+    @UiField
+    HTML trackDensity;
+
+    @UiField
+    ToggleSwitch trackListToggle;
+
+    @UiField
+    DockLayoutPanel layoutPanel;
+    @UiField
+    Tree optionTree;
+    @UiField
+    static PanelGroup dataGrid;
+
+
+    public static ListDataProvider<TrackInfo> dataProvider = new ListDataProvider<>();
+    private static List<TrackInfo> trackInfoList = new ArrayList<>();
+    private static List<TrackInfo> filteredTrackInfoList = dataProvider.getList();
+
+    private static Map<String, List<TrackInfo>> categoryMap = new TreeMap<>();
+    private static Map<String, Boolean> categoryOpen = new TreeMap<>();
+    private static Map<TrackInfo, CheckBoxButton> checkBoxMap = new TreeMap<>();
+    private static Map<TrackInfo, TrackBodyPanel> trackBodyMap = new TreeMap<>();
+
+    public TrackPanel() {
+        exportStaticMethod();
+
+        Widget rootElement = ourUiBinder.createAndBindUi(this);
+        initWidget(rootElement);
+
+        dataGrid.setWidth("100%");
+
+
+        Annotator.eventBus.addHandler(OrganismChangeEvent.TYPE, new OrganismChangeEventHandler() {
+            @Override
+            public void onOrganismChanged(OrganismChangeEvent authenticationEvent) {
+                loadTracks(2000);
+            }
+        });
+
+    }
+
+    public void loadTracks(int delay) {
+        filteredTrackInfoList.clear();
+        trackInfoList.clear();
+        Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
+            @Override
+            public boolean execute() {
+                reload();
+                if (trackInfoList.isEmpty()) {
+                    return true;
+                }
+                return false;
+            }
+        }, delay);
+    }
+
+    public void reloadIfEmpty() {
+        if (dataProvider.getList().isEmpty()) {
+            loadTracks(7000);
+        }
+    }
+
+
+    private void setTrackInfo(TrackInfo selectedObject) {
+        if (selectedObject == null) {
+            trackName.setText("");
+            trackType.setText("");
+            optionTree.clear();
+        } else {
+            trackName.setText(selectedObject.getName());
+            trackType.setText(selectedObject.getType());
+            optionTree.clear();
+            JSONObject jsonObject = selectedObject.getPayload();
+            setOptionDetails(jsonObject);
+        }
+    }
+
+    private void setOptionDetails(JSONObject jsonObject) {
+        for (String key : jsonObject.keySet()) {
+            TreeItem treeItem = new TreeItem();
+            treeItem.setHTML(generateHtmlFromObject(jsonObject, key));
+            if (jsonObject.get(key).isObject() != null) {
+                treeItem.addItem(generateTreeItem(jsonObject.get(key).isObject()));
+            }
+            optionTree.addItem(treeItem);
+        }
+    }
+
+    private String generateHtmlFromObject(JSONObject jsonObject, String key) {
+        if (jsonObject.get(key) == null) {
+            return key;
+        } else if (jsonObject.get(key).isObject() != null) {
+            return key;
+        } else {
+            return "<b>" + key + "</b>: " + jsonObject.get(key).toString().replace("\\", "");
+        }
+    }
+
+    private TreeItem generateTreeItem(JSONObject jsonObject) {
+        TreeItem treeItem = new TreeItem();
+
+        for (String key : jsonObject.keySet()) {
+            treeItem.setHTML(generateHtmlFromObject(jsonObject, key));
+            if (jsonObject.get(key).isObject() != null) {
+                treeItem.addItem(generateTreeItem(jsonObject.get(key).isObject()));
+            }
+        }
+
+
+        return treeItem;
+    }
+
+
+    @UiHandler("nameSearchBox")
+    public void doSearch(KeyUpEvent keyUpEvent) {
+        filterList();
+    }
+
+    static void filterList() {
+        String text = nameSearchBox.getText();
+        for (TrackInfo trackInfo : trackInfoList) {
+            if (trackInfo.getName().toLowerCase().contains(text.toLowerCase()) &&
+                    !isReferenceSequence(trackInfo) &&
+                    !isAnnotationTrack(trackInfo)) {
+                Integer filteredIndex = filteredTrackInfoList.indexOf(trackInfo);
+                if (filteredIndex < 0) {
+                    filteredTrackInfoList.add(trackInfo);
+                } else {
+                    filteredTrackInfoList.get(filteredIndex).setVisible(trackInfo.getVisible());
+                }
+            } else {
+                filteredTrackInfoList.remove(trackInfo);
+            }
+        }
+        renderFiltered();
+    }
+
+    static class TrackBodyPanel extends PanelBody {
+
+        private final TrackInfo trackInfo;
+
+
+        public TrackBodyPanel(TrackInfo trackInfo) {
+            this.trackInfo = trackInfo;
+            decorate();
+        }
+
+        private void decorate() {
+
+            InputGroup inputGroup = new InputGroup();
+            addStyleName("track-entry");
+            final CheckBoxButton selected = new CheckBoxButton();
+            selected.setValue(trackInfo.getVisible());
+
+            InputGroupButton inputGroupButton = new InputGroupButton();
+            inputGroupButton.add(selected);
+            inputGroup.add(inputGroupButton);
+
+            InputGroupAddon label = new InputGroupAddon();
+            label.addStyleName("text-left");
+            label.setText(trackInfo.getName());
+            inputGroup.add(label);
+
+            add(inputGroup);
+
+            selected.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+                @Override
+                public void onValueChange(ValueChangeEvent<Boolean> event) {
+                    Boolean value = selected.getValue();
+                    handleSelect(value);
+                }
+            });
+
+            checkBoxMap.put(trackInfo, selected);
+
+            label.addStyleName("track-link");
+            label.setWidth("100%");
+
+            label.addDomHandler(new ClickHandler() {
+                @Override
+                public void onClick(ClickEvent event) {
+                    MainPanel.getTrackPanel().setTrackInfo(trackInfo);
+                }
+            }, ClickEvent.getType());
+        }
+
+        public void handleSelect(Boolean value) {
+            JSONObject jsonObject = trackInfo.getPayload();
+            trackInfo.setVisible(value);
+            if (value) {
+                jsonObject.put("command", new JSONString("show"));
+            } else {
+                jsonObject.put("command", new JSONString("hide"));
+            }
+            MainPanel.getInstance().postMessage("handleTrackVisibility", jsonObject);
+        }
+    }
+
+    public void clear() {
+        categoryMap.clear();
+        categoryOpen.clear();
+        checkBoxMap.clear();
+        trackBodyMap.clear();
+        dataGrid.clear();
+        dataGrid.add(new org.gwtbootstrap3.client.ui.Label("Loading..."));
+    }
+
+    private static void renderFiltered() {
+        dataGrid.clear();
+        // populate the map of categories
+        for (TrackInfo trackInfo : trackInfoList) {
+            if (!isReferenceSequence(trackInfo) && !isAnnotationTrack(trackInfo)) {
+                categoryMap.put(trackInfo.getStandardCategory(), new ArrayList<TrackInfo>());
+                if (!categoryOpen.containsKey(trackInfo.getStandardCategory())) {
+                    categoryOpen.put(trackInfo.getStandardCategory(), false);
+                }
+            }
+        }
+
+        if (categoryOpen.size() == 1) {
+            categoryOpen.put(categoryOpen.keySet().iterator().next(), true);
+        }
+
+        for (TrackInfo trackInfo : filteredTrackInfoList) {
+            if (!isReferenceSequence(trackInfo) && !isAnnotationTrack(trackInfo)) {
+                List<TrackInfo> trackInfoList = categoryMap.get(trackInfo.getStandardCategory());
+                trackInfoList.add(trackInfo);
+                categoryMap.put(trackInfo.getStandardCategory(), trackInfoList);
+            }
+        }
+
+
+        // build up categories, first, processing any / all levels
+        int count = 1;
+        // handle the uncategorized first
+        if (categoryMap.containsKey(TrackInfo.TRACK_UNCATEGORIZED)) {
+            List<TrackInfo> trackInfoList = categoryMap.get(TrackInfo.TRACK_UNCATEGORIZED);
+            for (TrackInfo trackInfo : trackInfoList) {
+                TrackBodyPanel panelBody = new TrackBodyPanel(trackInfo);
+                dataGrid.add(panelBody);
+            }
+        }
+
+
+        for (final String key : categoryMap.keySet()) {
+            if (!key.equals(TrackInfo.TRACK_UNCATEGORIZED)) {
+
+                final List<TrackInfo> trackInfoList = categoryMap.get(key);
+
+                // if this is a root panel
+                Panel panel = new Panel();
+
+                PanelHeader panelHeader = null;
+                final CheckBoxButton panelSelect = new CheckBoxButton();
+                panelSelect.addStyleName("panel-select");
+                panelSelect.setPull(Pull.RIGHT);
+                Badge totalBadge = null;
+
+                // if we only have a single uncategorized category, then do not add a header
+                if (categoryOpen.size() != 1 || (!key.equals(TrackInfo.TRACK_UNCATEGORIZED) && categoryOpen.size() == 1)) {
+                    panelHeader = new PanelHeader();
+                    panelHeader.setPaddingTop(2);
+                    panelHeader.setPaddingBottom(2);
+                    panelHeader.setDataToggle(Toggle.COLLAPSE);
+                    Heading heading = new Heading(HeadingSize.H4, key);
+                    panelHeader.add(heading);
+                    heading.addStyleName("track-header");
+                    totalBadge = new Badge(Integer.toString(trackInfoList.size()));
+                    totalBadge.setPull(Pull.RIGHT);
+                    panelHeader.add(panelSelect);
+                    panelHeader.add(totalBadge);
+                    panel.add(panelHeader);
+                }
+
+
+                final PanelCollapse panelCollapse = new PanelCollapse();
+                panelCollapse.setIn(categoryOpen.get(key));
+                panelCollapse.setId("collapse" + count++);
+                panel.add(panelCollapse);
+                panelCollapse.setWidth("100%");
+
+                if (panelHeader != null) panelHeader.setDataTarget("#" + panelCollapse.getId());
+
+                panelCollapse.addHiddenHandler(new HiddenHandler() {
+                    @Override
+                    public void onHidden(HiddenEvent event) {
+                        categoryOpen.put(key, false);
+                    }
+                });
+
+                panelCollapse.addShowHandler(new ShowHandler() {
+                    @Override
+                    public void onShow(ShowEvent showEvent) {
+                        categoryOpen.put(key, true);
+                    }
+                });
+
+                Integer numVisible = 0;
+                if (!trackInfoList.isEmpty()) {
+                    for (TrackInfo trackInfo : trackInfoList) {
+                        if (trackInfo.getVisible()) ++numVisible;
+                        TrackBodyPanel panelBody = new TrackBodyPanel(trackInfo);
+                        trackBodyMap.put(trackInfo, panelBody);
+                        panelCollapse.add(panelBody);
+                    }
+                    if (numVisible == 0) {
+                        panelSelect.setValue(false);
+                    } else if (numVisible == trackInfoList.size()) {
+                        panelSelect.setValue(true);
+                    }
+                    panelSelect.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+                        @Override
+                        public void onValueChange(ValueChangeEvent<Boolean> event) {
+                            Boolean value = panelSelect.getValue();
+                            for (TrackInfo trackInfo : trackInfoList) {
+                                checkBoxMap.get(trackInfo).setValue(value);
+                                trackBodyMap.get(trackInfo).handleSelect(value);
+                            }
+                            categoryOpen.put(key, true);
+                        }
+                    });
+                }
+                if (totalBadge != null) {
+                    totalBadge.setText(numVisible + "/" + trackInfoList.size());
+                }
+
+                dataGrid.add(panel);
+            }
+        }
+
+    }
+
+    private static PanelBody getPanelBodyForCategory(String key) {
+        Element element = Document.get().getElementById("#panelBody" + key);
+        if (element != null) {
+            com.google.gwt.user.client.EventListener listener = DOM.getEventListener((com.google.gwt.user.client.Element) element);
+            return (PanelBody) listener;
+        }
+        return null;
+    }
+
+    private static boolean isAnnotationTrack(TrackInfo trackInfo) {
+        return trackInfo.getName().equalsIgnoreCase("User-created Annotations");
+    }
+
+    private static boolean isReferenceSequence(TrackInfo trackInfo) {
+        return trackInfo.getName().equalsIgnoreCase("Reference sequence");
+    }
+
+    public static native void exportStaticMethod() /*-{
+        $wnd.loadTracks = $entry(@org.bbop.apollo.gwt.client.TrackPanel::updateTracks(Ljava/lang/String;));
+    }-*/;
+
+    public void reload() {
+        JSONObject commandObject = new JSONObject();
+        commandObject.put("command", new JSONString("list"));
+        MainPanel.getInstance().postMessage("handleTrackVisibility", commandObject);
+    }
+
+    public static void updateTracks(String jsonString) {
+        JSONArray returnValueObject = JSONParser.parseStrict(jsonString).isArray();
+        updateTracks(returnValueObject);
+    }
+
+    public List<String> getTrackList() {
+        if (trackInfoList.isEmpty()) {
+            reload();
+        }
+        List<String> trackListArray = new ArrayList<>();
+        for (TrackInfo trackInfo : trackInfoList) {
+            if (trackInfo.getVisible() &&
+                    !isReferenceSequence(trackInfo) &&
+                    !isAnnotationTrack(trackInfo)) {
+                trackListArray.add(trackInfo.getLabel());
+            }
+        }
+        return trackListArray;
+    }
+
+    public static void updateTracks(JSONArray array) {
+        trackInfoList.clear();
+
+        for (int i = 0; i < array.size(); i++) {
+            JSONObject object = array.get(i).isObject();
+            TrackInfo trackInfo = new TrackInfo();
+            // track label can never be null, but key can be
+            trackInfo.setName(object.get("key") == null ? object.get("label").isString().stringValue() : object.get("key").isString().stringValue());
+
+            if (object.get("label") != null) trackInfo.setLabel(object.get("label").isString().stringValue());
+            else Bootbox.alert("Track label should not be null, please check your tracklist");
+
+            if (object.get("type") != null) trackInfo.setType(object.get("type").isString().stringValue());
+
+            if (object.get("urlTemplate") != null)
+                trackInfo.setUrlTemplate(object.get("urlTemplate").isString().stringValue());
+
+            if (object.get("visible") != null) trackInfo.setVisible(object.get("visible").isBoolean().booleanValue());
+            else trackInfo.setVisible(false);
+
+            if (object.get("category") != null) trackInfo.setCategory(object.get("category").isString().stringValue());
+
+            trackInfo.setPayload(object);
+            trackInfoList.add(trackInfo);
+        }
+        filterList();
+    }
+
+    @UiHandler("trackListToggle")
+    public void trackListToggle(ValueChangeEvent<Boolean> event) {
+        MainPanel.useNativeTracklist = trackListToggle.getValue();
+        MainPanel.getInstance().trackListToggle.setActive(MainPanel.useNativeTracklist);
+        updateTrackToggle(MainPanel.useNativeTracklist);
+    }
+
+    public void updateTrackToggle(Boolean val) {
+        trackListToggle.setValue(val);
+
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue v = JSONParser.parseStrict(response.getText());
+                JSONObject o = v.isObject();
+                if (o.containsKey(FeatureStringEnum.ERROR.getValue())) {
+                    new ErrorDialog("Error Updating User", o.get(FeatureStringEnum.ERROR.getValue()).isString().stringValue(), true, true);
+                }
+
+                MainPanel.updateGenomicViewer(true, true);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating user: " + exception);
+            }
+        };
+        UserRestService.updateUserTrackPanelPreference(requestCallback, trackListToggle.getValue());
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.ui.xml
new file mode 100644
index 0000000..ed1869d
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/TrackPanel.ui.xml
@@ -0,0 +1,120 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:toggle="urn:import:org.gwtbootstrap3.extras.toggleswitch.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+        >
+    <ui:style>
+        .northPanel {
+            padding: 0px;
+            margin: 10px;
+        }
+
+
+
+        .tableKeyName{
+            font-weight: bolder;
+        }
+
+        .cellTable {
+            border-bottom: 1px solid #ccc;
+            text-align: left;
+            margin-bottom: 4px;
+            outline: none;
+        }
+
+    </ui:style>
+    <gwt:DockLayoutPanel ui:field="layoutPanel">
+        <gwt:north size="60">
+            <b:Container fluid="true" styleName="{style.northPanel}">
+                <b:Row>
+                    <b:Column size="MD_6">
+                        <b:TextBox placeholder="Search" ui:field="nameSearchBox"/>
+                    </b:Column>
+                    <b:Column size="MD_6">
+                        <toggle:ToggleSwitch ui:field="trackListToggle" size="MINI" labelText="JBrowse Track Selector" onText="Hide" offText="Show" visible="true"/>
+                    </b:Column>
+                </b:Row>
+            </b:Container>
+        </gwt:north>
+        <gwt:center>
+            <gwt:ScrollPanel>
+                <b:PanelGroup ui:field="dataGrid">
+                    <b:Label text="Loading..."/>
+                </b:PanelGroup>
+            </gwt:ScrollPanel>
+        </gwt:center>
+        <gwt:south size="200">
+            <gwt:TabLayoutPanel barHeight="35">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row>
+                            <b:Column size="MD_3" >
+                                <gwt:HTML text="Name" styleName="{style.tableKeyName}"/>
+                            </b:Column>
+                            <b:Column size="MD_4" >
+                                <gwt:HTML ui:field="trackName"/>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row>
+                            <b:Column size="MD_3" >
+                                <gwt:HTML styleName="{style.tableKeyName}">
+                                    Type
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_4" >
+                                <gwt:HTML ui:field="trackType"/>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row visible="true">
+                            <b:Column size="MD_3" >
+                                <gwt:HTML styleName="{style.tableKeyName}">
+                                    Configuration
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_7" >
+                                <gwt:Tree ui:field="optionTree"/>
+                            </b:Column>
+                        </b:Row>
+
+                        <b:Row visible="false">
+                            <b:Column size="MD_3" >
+                                <gwt:HTML styleName="{style.tableKeyName}">
+                                    Feature Count
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_4" >
+                                <gwt:HTML ui:field="trackCount"/>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row visible="false">
+                            <b:Column size="MD_3" >
+                                <gwt:HTML styleName="{style.tableKeyName}">
+                                    Feature Density
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_4" >
+                                <gwt:HTML ui:field="trackDensity"/>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <!--<gwt:tab>-->
+                    <!--<gwt:header>Configuration</gwt:header>-->
+                    <!--<gwt:FlexTable ui:field="configurationTable"/>-->
+                <!--</gwt:tab>-->
+                <!--TODO: add back for 2.0-alpha2 or 2.1-->
+                <!--<gwt:tab>-->
+                    <!--<gwt:header>Users</gwt:header>-->
+                    <!--<gwt:HTML text="user permissions on table"/>-->
+                <!--</gwt:tab>-->
+                <!--<gwt:tab>-->
+                    <!--<gwt:header>Groups</gwt:header>-->
+                    <!--<gwt:HTML text="group permissions on table"/>-->
+                <!--</gwt:tab>-->
+            </gwt:TabLayoutPanel>
+        </gwt:south>
+
+    </gwt:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.java b/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.java
new file mode 100644
index 0000000..44269ad
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.java
@@ -0,0 +1,124 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.http.client.*;
+import com.google.gwt.i18n.client.Dictionary;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+import org.bbop.apollo.gwt.client.event.AnnotationInfoChangeEvent;
+import org.bbop.apollo.gwt.client.rest.AnnotationRestService;
+import org.bbop.apollo.gwt.client.rest.RestService;
+import org.gwtbootstrap3.client.ui.InputGroupAddon;
+import org.gwtbootstrap3.client.ui.gwt.CellTable;
+import org.gwtbootstrap3.client.ui.TextBox;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 1/9/15.
+ */
+public class TranscriptDetailPanel extends Composite {
+
+    private AnnotationInfo internalAnnotationInfo;
+
+
+    interface AnnotationDetailPanelUiBinder extends UiBinder<Widget, TranscriptDetailPanel> { }
+
+    private static AnnotationDetailPanelUiBinder ourUiBinder = GWT.create(AnnotationDetailPanelUiBinder.class);
+
+    @UiField
+    TextBox nameField;
+    @UiField
+    TextBox descriptionField;
+    @UiField
+    TextBox locationField;
+    @UiField
+    TextBox userField;
+    @UiField
+    TextBox sequenceField;
+
+    private Boolean editable = false ;
+
+    public TranscriptDetailPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+    }
+
+
+    @UiHandler("nameField")
+    void handleNameChange(ChangeEvent e) {
+        internalAnnotationInfo.setName(nameField.getText());
+        updateTranscript();
+    }
+
+
+    @UiHandler("descriptionField")
+    void handleDescriptionChange(ChangeEvent e) {
+        internalAnnotationInfo.setDescription(descriptionField.getText());
+        updateTranscript();
+    }
+
+
+
+    public void updateData(AnnotationInfo annotationInfo) {
+        this.internalAnnotationInfo = annotationInfo ;
+        nameField.setText(internalAnnotationInfo.getName());
+        descriptionField.setText(internalAnnotationInfo.getDescription());
+        userField.setText(internalAnnotationInfo.getOwner());
+        sequenceField.setText(internalAnnotationInfo.getSequence());
+
+        if (internalAnnotationInfo.getMin() != null) {
+            String locationText = Integer.toString(internalAnnotationInfo.getMin() );
+            locationText += " - ";
+            locationText += internalAnnotationInfo.getMax().toString();
+            locationText += " strand(";
+            locationText += internalAnnotationInfo.getStrand() > 0 ? "+" : "-";
+            locationText += ")";
+            locationField.setText(locationText);
+            locationField.setVisible(true);
+        }
+        else{
+            locationField.setVisible(false);
+        }
+
+        setVisible(true);
+    }
+
+    private void updateTranscript() {
+        final AnnotationInfo updatedInfo = this.internalAnnotationInfo;
+        enableFields(false);
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                Annotator.eventBus.fireEvent(new AnnotationInfoChangeEvent(updatedInfo, AnnotationInfoChangeEvent.Action.UPDATE));
+                enableFields(true);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating transcript: " + exception);
+                enableFields(true);
+            }
+        };
+        RestService.sendRequest(requestCallback, "annotator/updateFeature/", AnnotationRestService.convertAnnotationInfoToJSONObject(this.internalAnnotationInfo));
+    }
+
+    private void enableFields(boolean enabled){
+        nameField.setEnabled(enabled && editable);
+        descriptionField.setEnabled(enabled && editable);
+    }
+
+    public void setEditable(boolean editable) {
+        this.editable = editable ;
+        nameField.setEnabled(this.editable);
+        descriptionField.setEnabled(this.editable);
+    }
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.ui.xml
new file mode 100644
index 0000000..0af12d2
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/TranscriptDetailPanel.ui.xml
@@ -0,0 +1,74 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+        >
+    <ui:style>
+
+        .container {
+            margin-left: 20px;
+            margin-top: 10px;
+            margin-right: 20px;
+            padding: 10px;
+        }
+
+        .widgetPanel {
+            margin-bottom: 5px;
+        }
+
+
+        .row {
+            margin-left: 5px;
+            margin-right: 5px;
+        }
+    </ui:style>
+
+    <b:Container fluid="true" styleName="{style.container}">
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Name</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="nameField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <!--<b:Row>-->
+            <!--<b:Column size="XS_12" styleName="{style.widgetPanel}">-->
+                <!--<b:InputGroup>-->
+                    <!--<b:InputGroupAddon>Symbol</b:InputGroupAddon>-->
+                    <!--<b:TextBox autoComplete="false" ui:field="symbolField"/>-->
+                <!--</b:InputGroup>-->
+            <!--</b:Column>-->
+        <!--</b:Row>-->
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Description</b:InputGroupAddon>
+                    <b:TextBox autoComplete="false" ui:field="descriptionField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Location</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="locationField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Ref Sequence</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="sequenceField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+        <b:Row styleName="{style.row}">
+            <b:Column size="XS_12" styleName="{style.widgetPanel}">
+                <b:InputGroup>
+                    <b:InputGroupAddon>Owner</b:InputGroupAddon>
+                    <b:TextBox enabled="false" ui:field="userField"/>
+                </b:InputGroup>
+            </b:Column>
+        </b:Row>
+    </b:Container>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/UrlDialogBox.java b/src/gwt/org/bbop/apollo/gwt/client/UrlDialogBox.java
new file mode 100644
index 0000000..0f11969
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/UrlDialogBox.java
@@ -0,0 +1,41 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.*;
+
+/**
+ * Created by ndunn on 4/8/15.
+ */
+public class UrlDialogBox extends DialogBox{
+
+    private VerticalPanel panel = new VerticalPanel();
+    private HorizontalPanel buttonPanel = new HorizontalPanel();
+    private TextArea urlView = new TextArea();
+    private Button closeButton = new Button("OK");
+
+    public UrlDialogBox(String url){
+
+        buttonPanel.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
+        buttonPanel.setCellHorizontalAlignment(closeButton,HasHorizontalAlignment.ALIGN_CENTER);
+        buttonPanel.setWidth("100%");
+
+        urlView.setCharacterWidth(100);
+        urlView.setEnabled(false);
+        urlView.setText(url);
+
+        panel.add(urlView);
+        panel.add(buttonPanel);
+        buttonPanel.add(closeButton);
+        panel.add(buttonPanel);
+
+        closeButton.addClickHandler(new ClickHandler() {
+            @Override
+            public void onClick(ClickEvent event) {
+                hide();
+            }
+        });
+
+        setWidget(panel);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/UserPanel.java b/src/gwt/org/bbop/apollo/gwt/client/UserPanel.java
new file mode 100644
index 0000000..dae2089
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/UserPanel.java
@@ -0,0 +1,718 @@
+package org.bbop.apollo.gwt.client;
+
+import com.google.gwt.cell.client.*;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
+import com.google.gwt.text.shared.SafeHtmlRenderer;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.cellview.client.*;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.*;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.view.client.*;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfoConverter;
+import org.bbop.apollo.gwt.client.dto.UserOrganismPermissionInfo;
+import org.bbop.apollo.gwt.client.event.UserChangeEvent;
+import org.bbop.apollo.gwt.client.event.UserChangeEventHandler;
+import org.bbop.apollo.gwt.client.resources.TableResources;
+import org.bbop.apollo.gwt.client.rest.UserRestService;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.client.ui.*;
+import org.gwtbootstrap3.client.ui.constants.IconType;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.bootbox.client.callback.ConfirmCallback;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Created by ndunn on 12/17/14.
+ */
+public class UserPanel extends Composite {
+
+    interface UserBrowserPanelUiBinder extends UiBinder<Widget, UserPanel> {
+    }
+
+    private static UserBrowserPanelUiBinder ourUiBinder = GWT.create(UserBrowserPanelUiBinder.class);
+    @UiField
+    org.gwtbootstrap3.client.ui.TextBox firstName;
+    @UiField
+    org.gwtbootstrap3.client.ui.TextBox lastName;
+    @UiField
+    org.gwtbootstrap3.client.ui.TextBox email;
+
+    DataGrid.Resources tablecss = GWT.create(TableResources.TableCss.class);
+    @UiField(provided = true)
+    DataGrid<UserInfo> dataGrid = new DataGrid<UserInfo>(20, tablecss);
+    @UiField
+    org.gwtbootstrap3.client.ui.Button createButton;
+    @UiField
+    org.gwtbootstrap3.client.ui.Button cancelButton;
+    @UiField
+    org.gwtbootstrap3.client.ui.Button deleteButton;
+    @UiField
+    org.gwtbootstrap3.client.ui.Button saveButton;
+    @UiField
+    Input passwordTextBox;
+    @UiField
+    Row passwordRow;
+    @UiField
+    ListBox roleList;
+    @UiField
+    FlexTable groupTable;
+    @UiField
+    org.gwtbootstrap3.client.ui.ListBox availableGroupList;
+    @UiField
+    org.gwtbootstrap3.client.ui.Button addGroupButton;
+    @UiField
+    TabLayoutPanel userDetailTab;
+    @UiField(provided = true)
+    DataGrid<UserOrganismPermissionInfo> organismPermissionsGrid = new DataGrid<>(4, tablecss);
+    @UiField(provided = true)
+    WebApolloSimplePager pager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+    @UiField(provided = true)
+    WebApolloSimplePager organismPager = new WebApolloSimplePager(WebApolloSimplePager.TextLocation.CENTER);
+    @UiField
+    org.gwtbootstrap3.client.ui.TextBox nameSearchBox;
+    @UiField
+    Row userRow1;
+    @UiField
+    Row userRow2;
+    @UiField
+    org.gwtbootstrap3.client.ui.Label saveLabel;
+
+
+    private AsyncDataProvider<UserInfo> dataProvider;
+    private List<UserInfo> userInfoList = new ArrayList<>();
+//    private List<UserInfo> filteredUserInfoList = dataProvider.getList();
+    private SingleSelectionModel<UserInfo> selectionModel = new SingleSelectionModel<>();
+    private UserInfo selectedUserInfo;
+
+    private ListDataProvider<UserOrganismPermissionInfo> permissionProvider = new ListDataProvider<>();
+    private List<UserOrganismPermissionInfo> permissionProviderList = permissionProvider.getList();
+    private ColumnSortEvent.ListHandler<UserOrganismPermissionInfo> sortHandler = new ColumnSortEvent.ListHandler<UserOrganismPermissionInfo>(permissionProviderList);
+
+
+    public UserPanel() {
+        initWidget(ourUiBinder.createAndBindUi(this));
+
+        if (roleList.getItemCount() == 0) {
+            roleList.addItem("user");
+            roleList.addItem("admin");
+        }
+
+
+        TextColumn<UserInfo> firstNameColumn = new TextColumn<UserInfo>() {
+            @Override
+            public String getValue(UserInfo user) {
+                return user.getName();
+            }
+        };
+        firstNameColumn.setSortable(true);
+
+        SafeHtmlRenderer<String> anchorRenderer = new AbstractSafeHtmlRenderer<String>() {
+            @Override
+            public SafeHtml render(String object) {
+                SafeHtmlBuilder sb = new SafeHtmlBuilder();
+                sb.appendHtmlConstant("<a href=\"javascript:;\">").appendEscaped(object)
+                        .appendHtmlConstant("</a>");
+                return sb.toSafeHtml();
+            }
+        };
+
+        Column<UserInfo, String> secondNameColumn = new Column<UserInfo, String>(new ClickableTextCell(anchorRenderer)) {
+            @Override
+            public String getValue(UserInfo user) {
+                return user.getEmail();
+            }
+        };
+        secondNameColumn.setSortable(true);
+
+        TextColumn<UserInfo> thirdNameColumn = new TextColumn<UserInfo>() {
+            @Override
+            public String getValue(UserInfo user) {
+                return user.getRole();
+            }
+        };
+        thirdNameColumn.setSortable(false);
+
+        dataGrid.addColumn(firstNameColumn, "Name");
+        dataGrid.addColumn(secondNameColumn, "Email");
+        dataGrid.addColumn(thirdNameColumn, "Global Role");
+
+        dataGrid.setSelectionModel(selectionModel);
+        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
+            @Override
+            public void onSelectionChange(SelectionChangeEvent event) {
+                selectedUserInfo = selectionModel.getSelectedObject();
+                updateUserInfo();
+            }
+        });
+        createOrganismPermissionsTable();
+
+        dataProvider = new AsyncDataProvider<UserInfo>() {
+            @Override
+            protected void onRangeChanged(HasData<UserInfo> display) {
+                final Range range = display.getVisibleRange();
+                final ColumnSortList sortList = dataGrid.getColumnSortList();
+                final int start = range.getStart();
+                final int length = range.getLength();
+
+                RequestCallback requestCallback = new RequestCallback() {
+                    @Override
+                    public void onResponseReceived(Request request, Response response) {
+                        JSONArray jsonArray = JSONParser.parseLenient(response.getText()).isArray();
+                        Integer userCount = 0;
+                        if (jsonArray != null && jsonArray.size() > 0) {
+                            JSONObject jsonObject = jsonArray.get(0).isObject();
+                            userCount = (int) jsonObject.get("userCount").isNumber().doubleValue();
+                            if(jsonObject.containsKey("searchName") && jsonObject.get("searchName").isString()!=null){
+                                String searchName = jsonObject.get("searchName").isString().stringValue();
+                                if(searchName.trim().length()>0 && !searchName.trim().equals(nameSearchBox.getText().trim())){
+                                    return ;
+                                }
+                            }
+                        }
+                        dataGrid.setRowCount(userCount, true);
+                        dataGrid.setRowData(start, UserInfoConverter.convertFromJsonArray(jsonArray));
+                    }
+
+                    @Override
+                    public void onError(Request request, Throwable exception) {
+                        Bootbox.alert("error getting sequence info: " + exception);
+                    }
+                };
+
+
+                ColumnSortList.ColumnSortInfo nameSortInfo = sortList.get(0);
+                if (nameSortInfo.getColumn().isSortable()) {
+                    Column<UserInfo, ?> sortColumn = (Column<UserInfo, ?>) sortList.get(0).getColumn();
+                    Integer columnIndex = dataGrid.getColumnIndex(sortColumn);
+                    String searchColumnString = columnIndex == 0 ? "name" : columnIndex == 1 ? "email" : "";
+                    Boolean sortNameAscending = nameSortInfo.isAscending();
+                    UserRestService.loadUsers(requestCallback, start, length,nameSearchBox.getText(), searchColumnString, sortNameAscending);
+                }
+            }
+        };
+
+
+        ColumnSortEvent.AsyncHandler columnSortHandler = new ColumnSortEvent.AsyncHandler(dataGrid);
+        dataGrid.addColumnSortHandler(columnSortHandler);
+        dataGrid.getColumnSortList().push(firstNameColumn);
+        dataGrid.getColumnSortList().push(secondNameColumn);
+
+        dataProvider.addDataDisplay(dataGrid);
+        pager.setDisplay(dataGrid);
+
+        Annotator.eventBus.addHandler(UserChangeEvent.TYPE, new UserChangeEventHandler() {
+            @Override
+            public void onUserChanged(UserChangeEvent userChangeEvent) {
+                switch (userChangeEvent.getAction()) {
+                    case ADD_USER_TO_GROUP:
+                        availableGroupList.removeItem(availableGroupList.getSelectedIndex());
+                        if (availableGroupList.getItemCount() > 0) {
+                            availableGroupList.setSelectedIndex(0);
+                        }
+                        addGroupButton.setEnabled(availableGroupList.getItemCount() > 0);
+
+                        String group = userChangeEvent.getGroup();
+                        addGroupToUi(group);
+                        break;
+                    case RELOAD_USERS:
+                        reload();
+                        break;
+                    case REMOVE_USER_FROM_GROUP:
+                        removeGroupFromUI(userChangeEvent.getGroup());
+                        addGroupButton.setEnabled(availableGroupList.getItemCount() > 0);
+                        break;
+                    case USERS_RELOADED:
+                        selectionModel.clear();
+                        reload();
+                        break;
+
+                }
+            }
+        });
+
+
+        Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
+            @Override
+            public boolean execute() {
+                if (MainPanel.getInstance().getCurrentUser() != null) {
+                    if(MainPanel.getInstance().isCurrentUserAdmin()) {
+                        reload();
+                    }
+                    return false ;
+                }
+                return true ;
+            }
+        }, 100);
+    }
+
+    @UiHandler("userDetailTab")
+    void onTabSelection(SelectionEvent<Integer> event) {
+        organismPermissionsGrid.redraw();
+    }
+
+
+    private void createOrganismPermissionsTable() {
+        TextColumn<UserOrganismPermissionInfo> organismNameColumn = new TextColumn<UserOrganismPermissionInfo>() {
+            @Override
+            public String getValue(UserOrganismPermissionInfo userOrganismPermissionInfo) {
+                return userOrganismPermissionInfo.getOrganismName();
+            }
+        };
+        organismNameColumn.setSortable(true);
+        organismNameColumn.setDefaultSortAscending(true);
+
+
+        Column<UserOrganismPermissionInfo, Boolean> adminColumn = new Column<UserOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(UserOrganismPermissionInfo object) {
+                return object.isAdmin();
+            }
+        };
+        adminColumn.setSortable(true);
+        adminColumn.setFieldUpdater(new FieldUpdater<UserOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, UserOrganismPermissionInfo object, Boolean value) {
+                object.setAdmin(value);
+                UserRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(adminColumn, new Comparator<UserOrganismPermissionInfo>() {
+            @Override
+            public int compare(UserOrganismPermissionInfo o1, UserOrganismPermissionInfo o2) {
+                return o1.isAdmin().compareTo(o2.isAdmin());
+            }
+        });
+
+        organismPermissionsGrid.addColumnSortHandler(sortHandler);
+        organismPermissionsGrid.setEmptyTableWidget(new Label("Please select a user to view organism permissions"));
+        organismPager.setDisplay(organismPermissionsGrid);
+
+        sortHandler.setComparator(organismNameColumn, new Comparator<UserOrganismPermissionInfo>() {
+            @Override
+            public int compare(UserOrganismPermissionInfo o1, UserOrganismPermissionInfo o2) {
+                return o1.getOrganismName().compareTo(o2.getOrganismName());
+            }
+        });
+
+        Column<UserOrganismPermissionInfo, Boolean> writeColumn = new Column<UserOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(UserOrganismPermissionInfo object) {
+                return object.isWrite();
+            }
+        };
+        writeColumn.setSortable(true);
+        writeColumn.setFieldUpdater(new FieldUpdater<UserOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, UserOrganismPermissionInfo object, Boolean value) {
+                object.setWrite(value);
+                object.setUserId(selectedUserInfo.getUserId());
+                UserRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(writeColumn, new Comparator<UserOrganismPermissionInfo>() {
+            @Override
+            public int compare(UserOrganismPermissionInfo o1, UserOrganismPermissionInfo o2) {
+                return o1.isWrite().compareTo(o2.isWrite());
+            }
+        });
+
+        Column<UserOrganismPermissionInfo, Boolean> exportColumn = new Column<UserOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(UserOrganismPermissionInfo object) {
+                return object.isExport();
+            }
+        };
+        exportColumn.setSortable(true);
+        exportColumn.setFieldUpdater(new FieldUpdater<UserOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, UserOrganismPermissionInfo object, Boolean value) {
+                object.setExport(value);
+                UserRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(exportColumn, new Comparator<UserOrganismPermissionInfo>() {
+            @Override
+            public int compare(UserOrganismPermissionInfo o1, UserOrganismPermissionInfo o2) {
+                return o1.isExport().compareTo(o2.isExport());
+            }
+        });
+
+        Column<UserOrganismPermissionInfo, Boolean> readColumn = new Column<UserOrganismPermissionInfo, Boolean>(new CheckboxCell(true, true)) {
+            @Override
+            public Boolean getValue(UserOrganismPermissionInfo object) {
+                return object.isRead();
+            }
+        };
+        readColumn.setSortable(true);
+        readColumn.setFieldUpdater(new FieldUpdater<UserOrganismPermissionInfo, Boolean>() {
+            @Override
+            public void update(int index, UserOrganismPermissionInfo object, Boolean value) {
+                object.setRead(value);
+                UserRestService.updateOrganismPermission(object);
+            }
+        });
+        sortHandler.setComparator(readColumn, new Comparator<UserOrganismPermissionInfo>() {
+            @Override
+            public int compare(UserOrganismPermissionInfo o1, UserOrganismPermissionInfo o2) {
+                return o1.isRead().compareTo(o2.isRead());
+            }
+        });
+
+
+        organismPermissionsGrid.addColumn(organismNameColumn, "Name");
+        organismPermissionsGrid.addColumn(adminColumn, "Admin");
+        organismPermissionsGrid.addColumn(writeColumn, "Write");
+        organismPermissionsGrid.addColumn(exportColumn, "Export");
+        organismPermissionsGrid.addColumn(readColumn, "Read");
+        permissionProvider.addDataDisplay(organismPermissionsGrid);
+    }
+
+    private Boolean isEmail(String emailString){
+        if(!emailString.contains("@") || !emailString.contains(".")){
+            return false ;
+        }
+        if (emailString.indexOf("@") >= emailString.lastIndexOf(".")) {
+            return false;
+        }
+        return true;
+    }
+
+    private Boolean setCurrentUserInfoFromUI() {
+        String emailString = email.getText().trim();
+        final MutableBoolean mutableBoolean = new MutableBoolean(true);
+        if(!isEmail(emailString)){
+            mutableBoolean.setBooleanValue(Window.confirm("'" + emailString + "' does not appear to be a valid email.  Use anyway?"));
+        }
+        if(mutableBoolean.getBooleanValue()) {
+            selectedUserInfo.setEmail(emailString);
+            selectedUserInfo.setFirstName(firstName.getText());
+            selectedUserInfo.setLastName(lastName.getText());
+            selectedUserInfo.setPassword(passwordTextBox.getText());
+            selectedUserInfo.setRole(roleList.getSelectedItemText());
+        }
+
+        return mutableBoolean.getBooleanValue();
+    }
+
+
+    @UiHandler("createButton")
+    public void create(ClickEvent clickEvent) {
+        selectedUserInfo = null;
+        selectionModel.clear();
+        saveButton.setVisible(true);
+        saveButton.setEnabled(true);
+        updateUserInfo();
+        cancelButton.setVisible(true);
+        cancelButton.setEnabled(true);
+        createButton.setEnabled(false);
+        passwordRow.setVisible(true);
+    }
+
+    @UiHandler("addGroupButton")
+    public void addGroupToUser(ClickEvent clickEvent) {
+        String selectedGroup = availableGroupList.getSelectedItemText();
+        UserRestService.addUserToGroup(selectedGroup, selectedUserInfo);
+    }
+
+
+    @UiHandler("cancelButton")
+    public void cancel(ClickEvent clickEvent) {
+        saveButton.setVisible(false);
+        updateUserInfo();
+        cancelButton.setVisible(false);
+        cancelButton.setEnabled(false);
+        createButton.setEnabled(true);
+        passwordRow.setVisible(false);
+    }
+
+    @UiHandler("deleteButton")
+    public void delete(ClickEvent clickEvent) {
+        Bootbox.confirm("Delete user " + selectedUserInfo.getName() + "?", new ConfirmCallback() {
+            @Override
+            public void callback(boolean result) {
+                if (result) {
+                    UserRestService.deleteUser(userInfoList, selectedUserInfo);
+                    selectedUserInfo = null;
+                    updateUserInfo();
+                }
+            }
+        });
+    }
+
+    @UiHandler(value = {"nameSearchBox"})
+    public void handleNameSearch(KeyUpEvent keyUpEvent) {
+        pager.setPageStart(0);
+        dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+    }
+
+
+    @UiHandler(value = {"firstName", "lastName", "email", "passwordTextBox"})
+    public void updateInterface(KeyUpEvent keyUpEvent) {
+        userIsSame();
+    }
+
+    @UiHandler(value = { "roleList"})
+    public void handleRole(ChangeEvent changeEvent) {
+        userIsSame();
+    }
+
+
+    @UiHandler("cancelButton")
+    public void handleCancel(ClickEvent clickEvent) {
+        updateUserInfo();
+    }
+
+    private void userIsSame() {
+        if(selectedUserInfo == null ){
+            return ;
+        }
+        if(selectedUserInfo.getEmail().equals(email.getText().trim())
+                && selectedUserInfo.getFirstName().equals(firstName.getText().trim())
+                && selectedUserInfo.getLastName().equals(lastName.getText().trim())
+                && selectedUserInfo.getRole().equals(roleList.getSelectedValue())
+                && passwordTextBox.getText().trim().length()==0  // we don't the password back here . . !!
+                ){
+            saveButton.setEnabled(false);
+            cancelButton.setEnabled(false);
+        } else {
+            saveButton.setEnabled(true);
+            cancelButton.setEnabled(true);
+        }
+    }
+
+    public void updateUser() {
+        // assume an edit operation
+        if (selectedUserInfo != null) {
+            if(!setCurrentUserInfoFromUI()){
+                handleCancel(null);
+                return ;
+            }
+            RequestCallback requestCallback = new RequestCallback() {
+                @Override
+                public void onResponseReceived(Request request, Response response) {
+                    JSONValue v= JSONParser.parseStrict(response.getText());
+                    JSONObject o=v.isObject();
+                    if(o.containsKey(FeatureStringEnum.ERROR.getValue())) {
+                        new ErrorDialog("Error Updating User",o.get(FeatureStringEnum.ERROR.getValue()).isString().stringValue(),true,true);
+                    }
+                    else{
+                        Bootbox.alert("Saved changes to user "+selectedUserInfo.getName()+"!");
+                        selectedUserInfo = null ;
+                        updateUserInfo();
+                        saveButton.setEnabled(false);
+                        cancelButton.setEnabled(false);
+                        reload(true);
+                    }
+                }
+
+                @Override
+                public void onError(Request request, Throwable exception) {
+                    Bootbox.alert("Error updating user: " + exception);
+                }
+            };
+            UserRestService.updateUser(requestCallback, selectedUserInfo);
+        }
+    }
+
+    private void saveNewUser() {
+        selectedUserInfo = new UserInfo();
+        if(setCurrentUserInfoFromUI()){
+            UserRestService.createUser(userInfoList, selectedUserInfo);
+        }
+        else{
+            handleCancel(null);
+        }
+        createButton.setEnabled(true);
+
+        selectedUserInfo = null;
+        selectionModel.clear();
+        updateUserInfo();
+        saveButton.setVisible(false);
+        cancelButton.setVisible(false);
+        passwordRow.setVisible(false);
+    }
+
+    @UiHandler("saveButton")
+    public void save(ClickEvent clickEvent) {
+        if (selectedUserInfo == null) {
+            saveNewUser();
+        } else {
+            updateUser();
+        }
+    }
+
+    private void updateUserInfo() {
+        passwordTextBox.setText("");
+        groupTable.removeAllRows();
+        if (selectedUserInfo == null) {
+            addGroupButton.setEnabled(false);
+            addGroupButton.setColor("gray");
+            firstName.setText("");
+            lastName.setText("");
+            email.setText("");
+
+            deleteButton.setEnabled(false);
+            deleteButton.setVisible(false);
+            roleList.setVisible(false);
+            permissionProviderList.clear();
+
+
+            if (saveButton.isVisible()) {
+                roleList.setVisible(true);
+                UserInfo currentUser = MainPanel.getInstance().getCurrentUser();
+                roleList.setSelectedIndex(0);
+                roleList.setEnabled(currentUser.getRole().equalsIgnoreCase("admin"));
+
+                userRow1.setVisible(true);
+                userRow2.setVisible(true);
+                passwordRow.setVisible(true);
+            } else {
+                userRow1.setVisible(false);
+                userRow2.setVisible(false);
+                passwordRow.setVisible(false);
+            }
+
+        } else {
+            createButton.setEnabled(true);
+            addGroupButton.setEnabled(true);
+            addGroupButton.setColor("blue");
+            firstName.setText(selectedUserInfo.getFirstName());
+            lastName.setText(selectedUserInfo.getLastName());
+            email.setText(selectedUserInfo.getEmail());
+            cancelButton.setVisible(true);
+            saveButton.setVisible(true);
+            saveButton.setEnabled(false);
+            cancelButton.setEnabled(false);
+            deleteButton.setVisible(true);
+            deleteButton.setEnabled(true);
+            userRow1.setVisible(true);
+            userRow2.setVisible(true);
+            passwordRow.setVisible(true);
+
+            UserInfo currentUser = MainPanel.getInstance().getCurrentUser();
+
+            passwordRow.setVisible(currentUser.getRole().equals("admin") || selectedUserInfo.getEmail().equals(currentUser.getEmail()));
+
+            roleList.setVisible(true);
+            for (int i = 0; i < roleList.getItemCount(); i++) {
+                roleList.setItemSelected(i, selectedUserInfo.getRole().equals(roleList.getItemText(i)));
+            }
+
+            // if user is "user" then make uneditable
+            // if user is admin AND self then make uneditable
+            // if user is admin, but not self, then make editable
+            roleList.setEnabled(currentUser.getRole().equalsIgnoreCase("admin") && currentUser.getUserId() != selectedUserInfo.getUserId());
+
+            List<String> groupList = selectedUserInfo.getGroupList();
+            for (String group : groupList) {
+                addGroupToUi(group);
+            }
+
+
+            availableGroupList.clear();
+            List<String> localAvailableGroupList = selectedUserInfo.getAvailableGroupList();
+            for (String availableGroup : localAvailableGroupList) {
+                availableGroupList.addItem(availableGroup);
+            }
+
+            permissionProviderList.clear();
+            // only show organisms that this user is an admin on . . . https://github.com/GMOD/Apollo/issues/540
+            if (MainPanel.getInstance().isCurrentUserAdmin()) {
+                permissionProviderList.addAll(selectedUserInfo.getOrganismPermissionMap().values());
+            } else {
+                List<String> organismsToShow = new ArrayList<>();
+                for (UserOrganismPermissionInfo userOrganismPermission : MainPanel.getInstance().getCurrentUser().getOrganismPermissionMap().values()) {
+                    if (userOrganismPermission.isAdmin()) {
+                        organismsToShow.add(userOrganismPermission.getOrganismName());
+                    }
+                }
+
+                for (UserOrganismPermissionInfo userOrganismPermission : selectedUserInfo.getOrganismPermissionMap().values()) {
+                    if (organismsToShow.contains(userOrganismPermission.getOrganismName())) {
+                        permissionProviderList.add(userOrganismPermission);
+                    }
+                }
+            }
+        }
+    }
+
+
+    private void addGroupToUi(String group) {
+        int i = groupTable.getRowCount();
+        groupTable.setWidget(i, 0, new RemoveGroupButton(group));
+        groupTable.setWidget(i, 1, new HTML(group));
+    }
+
+    public void reload() {
+        reload(false);
+    }
+
+    public void reload(Boolean forceReload) {
+        if (MainPanel.getInstance().getUserPanel().isVisible() || forceReload) {
+            pager.setPageStart(0);
+            dataGrid.setVisibleRangeAndClearData(dataGrid.getVisibleRange(), true);
+            dataGrid.redraw();
+        }
+    }
+
+    private void removeGroupFromUI(String group) {
+        int rowToRemove = -1;
+        for (int row = 0; rowToRemove < 0 && row < groupTable.getRowCount(); ++row) {
+            RemoveGroupButton removeGroupButton = (RemoveGroupButton) groupTable.getWidget(row, 0);
+            if (removeGroupButton.getGroupName().equals(group)) {
+                rowToRemove = row;
+            }
+        }
+        if (rowToRemove >= 0) {
+            groupTable.removeRow(rowToRemove);
+            availableGroupList.addItem(group);
+        }
+    }
+
+    private class RemoveGroupButton extends org.gwtbootstrap3.client.ui.Button {
+
+        private String groupName;
+
+        public RemoveGroupButton(final String groupName) {
+            this.groupName = groupName;
+            setIcon(IconType.REMOVE);
+            setColor("red");
+            addClickHandler(new ClickHandler() {
+                @Override
+                public void onClick(ClickEvent event) {
+                    UserRestService.removeUserFromGroup(groupName, userInfoList, selectedUserInfo);
+                }
+            });
+        }
+
+        String getGroupName() {
+            return groupName;
+        }
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/UserPanel.ui.xml b/src/gwt/org/bbop/apollo/gwt/client/UserPanel.ui.xml
new file mode 100644
index 0000000..d559fa6
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/UserPanel.ui.xml
@@ -0,0 +1,181 @@
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+             xmlns:gwt="urn:import:com.google.gwt.user.client.ui"
+             xmlns:b="urn:import:org.gwtbootstrap3.client.ui"
+             xmlns:cellview="urn:import:com.google.gwt.user.cellview.client"
+             xmlns:wa="urn:import:org.bbop.apollo.gwt.client"
+        >
+    <ui:style>
+        .northPanel {
+            padding: 0px;
+            margin: 10px;
+            display: inline;
+        }
+
+        .widgetPanel {
+            padding: 5px;
+            margin: 5px;
+            display: inline;
+        }
+
+        .trackTable {
+            margin-left: 10px;
+            margin-top: 10px;
+            vertical-align: middle;
+        }
+
+        .tableKeyName {
+            margin-top: 5px;
+            font-weight: bolder;
+        }
+
+        .details-list {
+            font-size: smaller;
+        }
+
+        .center-table {
+            margin: 10px;
+        }
+
+        .groupList td {
+            padding-right: 5px;
+            border: 1px;
+            /*padding-top: 5px;*/
+        }
+
+        .pager {
+            width: 100%;
+            margin-left: auto;
+            margin-right: auto;
+        }
+
+        .buttonPanel {
+            display: inline-block;
+            margin-top: 5px;
+            margin-left: 10px;
+        }
+    
+    </ui:style>
+    <gwt:DockLayoutPanel>
+        <gwt:north size="60">
+            <b:Container fluid="true" addStyleNames="{style.northPanel}">
+                <b:Row>
+                    <b:Column size="MD_6" addStyleNames="{style.widgetPanel}">
+                        <b:TextBox placeholder="Search" ui:field="nameSearchBox"/>
+                    </b:Column>
+                    <b:Column size="MD_2" addStyleNames="{style.widgetPanel}">
+                        <!--<b:TextBox placeholder="Search" width="300px" ui:field="nameSearchBox"/>-->
+                        <b:Button text="Create User" icon="PLUS" ui:field="createButton"/>
+                    </b:Column>
+                </b:Row>
+            </b:Container>
+        </gwt:north>
+        <gwt:center>
+            <gwt:DockLayoutPanel>
+                <gwt:north size="25">
+                    <wa:WebApolloSimplePager ui:field="pager" styleName="{style.pager}"/>
+                </gwt:north>
+                <gwt:center>
+                    <cellview:DataGrid ui:field="dataGrid" styleName="{style.trackTable}"/>
+                </gwt:center>
+            </gwt:DockLayoutPanel>
+        </gwt:center>
+        <gwt:south size="300">
+            <gwt:TabLayoutPanel barHeight="35" ui:field="userDetailTab">
+                <gwt:tab>
+                    <gwt:header>Details</gwt:header>
+                    <b:Container fluid="true" width="100%">
+                        <b:Row ui:field="userRow1" visible="false">
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}" text="First Name"/>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <b:TextBox ui:field="firstName"/>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}" text="Last Name"/>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <b:TextBox ui:field="lastName"/>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row ui:field="userRow2" visible="false"  marginTop="5">
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}" text="Email"/>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <b:TextBox ui:field="email"/>
+                            </b:Column>
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}" text="Role"/>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <b:FormGroup>
+                                    <b:ListBox ui:field="roleList" />
+                                </b:FormGroup>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row ui:field="passwordRow" visible="false" >
+                            <b:Column size="MD_2">
+                                <gwt:HTML styleName="{style.tableKeyName}" text="Password"/>
+                            </b:Column>
+                            <b:Column size="MD_4">
+                                <!--<b:PasswordTextBox ui:field:field:field:field:field:field:field:field:field:field:field:field="passwordTextBox" />-->
+                                <!--<b:TextBox ui:field="passwordTextBox"/>-->
+                                <b:Input ui:field="passwordTextBox" type="PASSWORD"/>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row>
+                            <b:Column size="MD_6" marginTop="5">
+                                <b:Button text="Save" type="PRIMARY" icon="SAVE" ui:field="saveButton" visible="false" enabled="false" marginRight="5"/>
+                                <b:Button text="Cancel" ui:field="cancelButton" enabled="false" visible="false" marginRight="5"/>
+                                <b:Button text="Delete" icon="REMOVE" type="WARNING" ui:field="deleteButton" enabled="false" visible="false" marginRight="5"/>
+                                <b:Label ui:field="saveLabel" visible="false"/>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Groups</gwt:header>
+                    <b:Container>
+                        <b:Row>
+                            <b:Column size="MD_3">
+                                <gwt:HTML>
+                                    <h5>Join Group</h5>
+                                </gwt:HTML>
+                            </b:Column>
+                            <b:Column size="MD_3">
+                                <gwt:HTML>
+                                    <h5>Included Groups</h5>
+                                </gwt:HTML>
+                            </b:Column>
+                        </b:Row>
+                        <b:Row>
+                            <b:Column size="MD_2">
+                                <b:ListBox ui:field="availableGroupList"/>
+                            </b:Column>
+                            <b:Column size="MD_1">
+                                <b:Button ui:field="addGroupButton" icon="FORWARD" color="blue"/>
+                            </b:Column>
+                            <b:Column size="MD_3">
+                                <gwt:FlexTable ui:field="groupTable" styleName="{style.groupList}"/>
+                            </b:Column>
+                        </b:Row>
+                    </b:Container>
+                </gwt:tab>
+                <gwt:tab>
+                    <gwt:header>Organisms</gwt:header>
+                    <gwt:DockLayoutPanel>
+                        <gwt:north size="25">
+                            <wa:WebApolloSimplePager ui:field="organismPager" styleName="{style.pager}"/>
+                        </gwt:north>
+                        <gwt:center>
+                            <cellview:DataGrid ui:field="organismPermissionsGrid" width="90%"
+                                               styleName="{style.center-table}"/>
+                        </gwt:center>
+                    </gwt:DockLayoutPanel>
+                </gwt:tab>
+            </gwt:TabLayoutPanel>
+        </gwt:south>
+
+    </gwt:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/src/gwt/org/bbop/apollo/gwt/client/WebApolloSimplePager.java b/src/gwt/org/bbop/apollo/gwt/client/WebApolloSimplePager.java
new file mode 100644
index 0000000..e15b8f6
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/WebApolloSimplePager.java
@@ -0,0 +1,37 @@
+package org.bbop.apollo.gwt.client;
+
+
+import com.google.gwt.user.cellview.client.SimplePager;
+import com.google.gwt.view.client.Range;
+
+
+public class WebApolloSimplePager extends SimplePager {
+
+    public WebApolloSimplePager() {
+        this.setRangeLimited(true);
+    }
+
+    public WebApolloSimplePager(TextLocation location, Resources resources, boolean showFastForwardButton, int fastForwardRows, boolean showLastPageButton) {
+        super(location, resources, showFastForwardButton, fastForwardRows, showLastPageButton);
+        this.setRangeLimited(true);
+    }
+    public WebApolloSimplePager(TextLocation location) {
+        super(location, false, true);
+        this.setRangeLimited(true);
+    }
+
+    public void setPageStart(int index) {
+        if (this.getDisplay() != null) {
+            Range range = getDisplay().getVisibleRange();
+            int pageSize = range.getLength();
+            if (!isRangeLimited() && getDisplay().isRowCountExact()) {
+                index = Math.min(index, getDisplay().getRowCount() - pageSize);
+            }
+            index = Math.max(0, index);
+            if (index != range.getStart()) {
+                getDisplay().setVisibleRange(index, pageSize);
+            }
+        }  
+    }
+}
+
diff --git a/src/gwt/org/bbop/apollo/gwt/client/comparators/AlphanumericSorter.java b/src/gwt/org/bbop/apollo/gwt/client/comparators/AlphanumericSorter.java
new file mode 100644
index 0000000..b8c6e05
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/comparators/AlphanumericSorter.java
@@ -0,0 +1,96 @@
+package org.bbop.apollo.gwt.client.comparators;
+
+import java.util.Comparator;
+
+/**
+ * Created by ndunn on 1/27/15.
+ * Inspired / borrowed from:  http://sanjaal.com/java/206/java-data-structure/alphanumeric-string-sorting-in-java-implementation/
+ */
+public class AlphanumericSorter implements Comparator<String> {
+
+    @Override
+    public int compare(String firstString, String secondString) {
+
+        if (secondString == null || firstString == null) {
+            return 0;
+        }
+
+        int lengthFirstStr = firstString.length();
+        int lengthSecondStr = secondString.length();
+
+        int index1 = 0;
+        int index2 = 0;
+
+        while (index1 < lengthFirstStr && index2 < lengthSecondStr) {
+            char ch1 = firstString.charAt(index1);
+            char ch2 = secondString.charAt(index2);
+
+            char[] space1 = new char[lengthFirstStr];
+            char[] space2 = new char[lengthSecondStr];
+
+            int loc1 = 0;
+            int loc2 = 0;
+
+            do {
+                space1[loc1++] = ch1;
+                index1++;
+
+                if (index1 < lengthFirstStr) {
+                    ch1 = firstString.charAt(index1);
+                } else {
+                    break;
+                }
+            } while (Character.isDigit(ch1) == Character.isDigit(space1[0]));
+
+            do {
+                space2[loc2++] = ch2;
+                index2++;
+
+                if (index2 < lengthSecondStr) {
+                    ch2 = secondString.charAt(index2);
+                } else {
+                    break;
+                }
+            } while (Character.isDigit(ch2) == Character.isDigit(space2[0]));
+
+            String str1 = new String(space1);
+            String str2 = new String(space2);
+
+            int result;
+
+            if (Character.isDigit(space1[0]) && Character.isDigit(space2[0])) {
+                Integer firstNumberToCompare = new Integer(
+                        Integer.parseInt(str1.trim()));
+                Integer secondNumberToCompare = new Integer(
+                        Integer.parseInt(str2.trim()));
+                result = firstNumberToCompare.compareTo(secondNumberToCompare);
+            } else {
+                result = str1.compareTo(str2);
+            }
+
+            if (result != 0) {
+                return result;
+            }
+        }
+        return lengthFirstStr - lengthSecondStr;
+    }
+
+    /**
+     * The purpose of this method is to remove any zero padding for numbers.
+     * <p/>
+     * Otherwise returns the input string.
+     *
+     * @param string
+     * @return
+     */
+    private String removePadding(String string) {
+        String result = "";
+        try {
+            result += Integer.parseInt(string.trim());
+        } catch (Exception e) {
+            result = string;
+        }
+        return result;
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfo.java
new file mode 100644
index 0000000..8fa1641
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfo.java
@@ -0,0 +1,149 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import java.util.*;
+
+/**
+ * Created by ndunn on 1/27/15.
+ */
+public class AnnotationInfo {
+    private String uniqueName;
+    private String name;
+    private String type;
+    private Integer min;
+    private Integer max;
+    private Set<AnnotationInfo> annotationInfoSet = new HashSet<>();
+    private String symbol;
+    private String description;
+    private Integer strand;
+    private List<String> noteList = new ArrayList<>();
+    private String sequence;
+    private Integer phase;
+    private String owner;
+    private String date;
+
+    public String getOwner() {
+        return owner;
+    }
+
+    public String getOwnerString() {
+        if (owner == null) {
+            return "";
+        }
+        return owner;
+    }
+
+    public void setOwner(String owner) {
+        this.owner = owner;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Integer getLength() {
+        if (min != null && max != null) {
+            return max - min;
+        }
+        return -1;
+    }
+
+    public void setDate(String date) { this.date = date; }
+
+    public String getDate() { return date; }
+
+    public Integer getMin() {
+        return min;
+    }
+
+    public void setMin(Integer min) {
+        this.min = min;
+    }
+
+    public Integer getMax() {
+        return max;
+    }
+
+    public void setMax(Integer max) {
+        this.max = max;
+    }
+
+    public String getUniqueName() {
+        return uniqueName;
+    }
+
+    public void setUniqueName(String uniqueName) {
+        this.uniqueName = uniqueName;
+    }
+
+    public void addChildAnnotation(AnnotationInfo annotationInfo) {
+        annotationInfoSet.add(annotationInfo);
+    }
+
+    public Set<AnnotationInfo> getAnnotationInfoSet() {
+        return annotationInfoSet;
+    }
+
+    public void setAnnotationInfoSet(Set<AnnotationInfo> annotationInfoSet) {
+        this.annotationInfoSet = annotationInfoSet;
+    }
+
+    public void setSymbol(String symbol) {
+        this.symbol = symbol;
+    }
+
+    public String getSymbol() {
+        return symbol;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Integer getStrand() {
+        return strand;
+    }
+
+    public void setStrand(Integer strand) {
+        this.strand = strand;
+    }
+
+    public List<String> getNoteList() {
+        return noteList;
+    }
+
+    public void setNoteList(List<String> noteList) {
+        this.noteList = noteList;
+    }
+
+    public void setSequence(String sequence) {
+        this.sequence = sequence;
+    }
+
+    public String getSequence() {
+        return sequence;
+    }
+
+    public Integer getPhase() {
+        return phase;
+    }
+
+    public void setPhase(Integer phase) {
+        this.phase = phase;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfoConverter.java b/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfoConverter.java
new file mode 100644
index 0000000..119d18e
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/AnnotationInfoConverter.java
@@ -0,0 +1,72 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import java.util.Date;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by nathandunn on 7/14/15.
+ */
+public class AnnotationInfoConverter {
+
+
+    public static List<AnnotationInfo> convertFromJsonArray(JSONArray array ) {
+        List<AnnotationInfo> annotationInfoList = new ArrayList<>();
+
+        for(int i = 0 ; i < array.size() ;i++){
+            annotationInfoList.add(convertFromJsonArray(array.get(i).isObject()));
+        }
+
+        return annotationInfoList ;
+    }
+
+    public static AnnotationInfo convertFromJsonArray(JSONObject object) {
+        return convertFromJsonArray(object, true);
+    }
+
+    private static AnnotationInfo convertFromJsonArray(JSONObject object, boolean processChildren) {
+        AnnotationInfo annotationInfo = new AnnotationInfo();
+        annotationInfo.setName(object.get("name").isString().stringValue());
+        annotationInfo.setType(object.get("type").isObject().get("name").isString().stringValue());
+        if (object.get("symbol") != null) {
+            annotationInfo.setSymbol(object.get("symbol").isString().stringValue());
+        }
+        if (object.get("description") != null) {
+            annotationInfo.setDescription(object.get("description").isString().stringValue());
+        }
+        annotationInfo.setMin((int) object.get("location").isObject().get("fmin").isNumber().doubleValue());
+        annotationInfo.setMax((int) object.get("location").isObject().get("fmax").isNumber().doubleValue());
+        annotationInfo.setStrand((int) object.get("location").isObject().get("strand").isNumber().doubleValue());
+        annotationInfo.setUniqueName(object.get("uniquename").isString().stringValue());
+        annotationInfo.setSequence(object.get("sequence").isString().stringValue());
+        if (object.get("owner") != null) {
+            annotationInfo.setOwner(object.get("owner").isString().stringValue());
+        }
+        annotationInfo.setDate(object.get("date_last_modified").toString());
+        List<String> noteList = new ArrayList<>();
+        if (object.get("notes") != null) {
+            JSONArray jsonArray = object.get("notes").isArray();
+            for (int i = 0; i < jsonArray.size(); i++) {
+                String note = jsonArray.get(i).isString().stringValue();
+                noteList.add(note);
+            }
+        }
+        annotationInfo.setNoteList(noteList);
+
+        if (processChildren && object.get("children") != null) {
+            JSONArray jsonArray = object.get("children").isArray();
+            for (int i = 0; i < jsonArray.size(); i++) {
+                AnnotationInfo childAnnotation = convertFromJsonArray(jsonArray.get(i).isObject(), true);
+                annotationInfo.addChildAnnotation(childAnnotation);
+            }
+        }
+
+        return annotationInfo;
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/AppInfoConverter.java b/src/gwt/org/bbop/apollo/gwt/client/dto/AppInfoConverter.java
new file mode 100644
index 0000000..2875093
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/AppInfoConverter.java
@@ -0,0 +1,33 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.user.client.Window;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 3/31/15.
+ */
+public class AppInfoConverter {
+
+    public static AppStateInfo convertFromJson(JSONObject object){
+        AppStateInfo appStateInfo = new AppStateInfo() ;
+
+        if(object.get("currentOrganism")!=null) {
+            appStateInfo.setCurrentOrganism(OrganismInfoConverter.convertFromJson(object.isObject().get("currentOrganism").isObject()));
+        }
+
+        if(object.get("currentSequence")!=null ){
+            appStateInfo.setCurrentSequence(SequenceInfoConverter.convertFromJson(object.isObject().get("currentSequence").isObject()));
+        }
+        appStateInfo.setOrganismList(OrganismInfoConverter.convertFromJsonArray(object.get("organismList").isArray()));
+        if(object.containsKey("currentStartBp")){
+            appStateInfo.setCurrentStartBp((int) object.get("currentStartBp").isNumber().doubleValue());
+        }
+        if(object.containsKey("currentEndBp")) {
+            appStateInfo.setCurrentEndBp((int) object.get("currentEndBp").isNumber().doubleValue());
+        }
+
+        return appStateInfo ;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/AppStateInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/AppStateInfo.java
new file mode 100644
index 0000000..500a9a9
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/AppStateInfo.java
@@ -0,0 +1,95 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 4/17/15.
+ */
+public class AppStateInfo implements HasJSON{
+
+    private OrganismInfo currentOrganism ;
+    private List<OrganismInfo> organismList ;
+    private SequenceInfo currentSequence ;
+    private Integer currentStartBp;
+    private Integer currentEndBp;
+
+    public OrganismInfo getCurrentOrganism() {
+        return currentOrganism;
+    }
+
+    public void setCurrentOrganism(OrganismInfo currentOrganism) {
+        this.currentOrganism = currentOrganism;
+    }
+
+    public List<OrganismInfo> getOrganismList() {
+        return organismList;
+    }
+
+    public void setOrganismList(List<OrganismInfo> organismList) {
+        this.organismList = organismList;
+    }
+
+    public SequenceInfo getCurrentSequence() {
+        return currentSequence;
+    }
+
+    public void setCurrentSequence(SequenceInfo currentSequence) {
+        this.currentSequence = currentSequence;
+    }
+
+
+    @Override
+    public JSONObject toJSON() {
+        JSONObject returnObject = new JSONObject();
+
+        if(currentOrganism!=null){
+            returnObject.put("currentOrganism",currentOrganism.toJSON());
+        }
+        if(currentSequence!=null){
+            returnObject.put("currentSequence",currentSequence.toJSON());
+        }
+//        if(currentSequenceList!=null){
+//            JSONArray sequenceListArray = new JSONArray();
+//            for(SequenceInfo sequenceInfo : currentSequenceList){
+//                sequenceListArray.set(sequenceListArray.size(),sequenceInfo.toJSON());
+//            }
+//            returnObject.put("currentSequenceList",sequenceListArray);
+//        }
+        if(organismList!=null){
+            JSONArray organismListArray = new JSONArray();
+            for(OrganismInfo organismInfo : organismList){
+                organismListArray.set(organismListArray.size(),organismInfo.toJSON());
+            }
+            returnObject.put("organismList",organismListArray);
+        }
+        if(currentStartBp!=null){
+            returnObject.put("currentStartBp", new JSONNumber(currentStartBp));
+        }
+        if(currentEndBp!=null){
+            returnObject.put("currentEndBp", new JSONNumber(currentEndBp));
+        }
+
+
+        return returnObject ;
+    }
+
+    public Integer getCurrentStartBp() {
+        return currentStartBp;
+    }
+
+    public void setCurrentStartBp(Integer currentStartBp) {
+        this.currentStartBp = currentStartBp;
+    }
+
+    public Integer getCurrentEndBp() {
+        return currentEndBp;
+    }
+
+    public void setCurrentEndBp(Integer currentEndBp) {
+        this.currentEndBp = currentEndBp;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/ExportInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/ExportInfo.java
new file mode 100644
index 0000000..72fbe19
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/ExportInfo.java
@@ -0,0 +1,37 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 3/31/15.
+ */
+public class ExportInfo {
+
+    private String type ;
+    private List<SequenceInfo> sequenceInfoList ;
+    private String generatedUrl ;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public List<SequenceInfo> getSequenceInfoList() {
+        return sequenceInfoList;
+    }
+
+    public void setSequenceInfoList(List<SequenceInfo> sequenceInfoList) {
+        this.sequenceInfoList = sequenceInfoList;
+    }
+
+    public String getGeneratedUrl() {
+        return generatedUrl;
+    }
+
+    public void setGeneratedUrl(String generatedUrl) {
+        this.generatedUrl = generatedUrl;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/GroupInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/GroupInfo.java
new file mode 100644
index 0000000..90296d4
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/GroupInfo.java
@@ -0,0 +1,118 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class GroupInfo implements HasJSON{
+
+    private String name;
+    private Integer numberOfUsers;
+    private Integer numberOrganisms;
+    private Integer numberSequences;
+    private Long id;
+    private List<UserInfo> userInfoList;
+    private Map<String, GroupOrganismPermissionInfo> organismPermissionMap = new HashMap<>();
+
+//    public GroupInfo(String name){
+//        this.name = name ;
+//        this.numberOfUsers = (int) Math.round(Math.random()*100);
+//        this.numberOrganisms = (int) Math.round(Math.random()*100);
+//        this.numberSequences = (int) Math.round(Math.random()*100);
+//    }
+
+    public Integer getNumberOfUsers() {
+        return numberOfUsers;
+    }
+
+    public void setNumberOfUsers(Integer numberOfUsers) {
+        this.numberOfUsers = numberOfUsers;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getNumberOrganisms() {
+        return numberOrganisms;
+    }
+
+    public void setNumberOrganisms(Integer numberOrganisms) {
+        this.numberOrganisms = numberOrganisms;
+    }
+
+    public Integer getNumberSequences() {
+        return numberSequences;
+    }
+
+    public void setNumberSequences(Integer numberSequences) {
+        this.numberSequences = numberSequences;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public List<UserInfo> getUserInfoList() {
+        return userInfoList;
+    }
+
+    public void setUserInfoList(List<UserInfo> userInfoList) {
+        this.userInfoList = userInfoList;
+    }
+
+    public JSONObject toJSON() {
+        JSONObject jsonObject = new JSONObject();
+
+        if (id != null) {
+            jsonObject.put("id", new JSONNumber(id));
+        }
+        jsonObject.put("name", new JSONString(name));
+
+        if (numberOfUsers != null) {
+            jsonObject.put("numberOfUsers", new JSONNumber(numberOfUsers));
+        }
+
+        JSONArray userInfoArray = new JSONArray();
+        for (int i = 0; userInfoList != null && i < userInfoList.size(); i++) {
+            userInfoArray.set(i,userInfoList.get(i).toJSON());
+        }
+        jsonObject.put("users",userInfoArray);
+
+        JSONArray organismPermissions = new JSONArray();
+        int index = 0 ;
+        for(String organism : organismPermissionMap.keySet()){
+            JSONObject orgPermission = new JSONObject();
+            orgPermission.put(organism,organismPermissionMap.get(organism).toJSON());
+            organismPermissions.set(index,orgPermission);
+            ++index ;
+        }
+        jsonObject.put("organismPermissions",organismPermissions);
+
+        return jsonObject;
+    }
+
+    public void setOrganismPermissionMap(Map<String, GroupOrganismPermissionInfo> organismPermissionMap) {
+        this.organismPermissionMap = organismPermissionMap;
+    }
+
+    public Map<String, GroupOrganismPermissionInfo> getOrganismPermissionMap() {
+        return organismPermissionMap;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/GroupOrganismPermissionInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/GroupOrganismPermissionInfo.java
new file mode 100644
index 0000000..6d8d61b
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/GroupOrganismPermissionInfo.java
@@ -0,0 +1,39 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+/**
+ * Created by ndunn on 3/24/15.
+ */
+public class GroupOrganismPermissionInfo extends OrganismPermissionInfo{
+
+    Long groupId ;
+
+
+    public Long getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(Long groupId) {
+        this.groupId = groupId;
+    }
+
+    public JSONObject toJSON() {
+        JSONObject payload = new JSONObject();
+        payload.put("organism",new JSONString(organismName));
+        payload.put("ADMINISTRATE",JSONBoolean.getInstance(admin));
+        payload.put("WRITE",JSONBoolean.getInstance(write));
+        payload.put("EXPORT",JSONBoolean.getInstance(export));
+        payload.put("READ",JSONBoolean.getInstance(read));
+        if(groupId!=null){
+            payload.put("groupId",new JSONNumber(groupId));
+        }
+        if(id!=null){
+            payload.put("id",new JSONNumber(id));
+        }
+        return payload;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/HasJSON.java b/src/gwt/org/bbop/apollo/gwt/client/dto/HasJSON.java
new file mode 100644
index 0000000..9a01995
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/HasJSON.java
@@ -0,0 +1,13 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONObject;
+
+/**
+ * Created by ndunn on 4/17/15.
+ * TODO: switch the name . .  horrible
+ */
+public interface HasJSON {
+
+    JSONObject toJSON()  ;
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfo.java
new file mode 100644
index 0000000..21b8781
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfo.java
@@ -0,0 +1,184 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsonUtils;
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.user.client.Window;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class OrganismInfo implements HasJSON{
+
+    // permanent key
+    private String id;
+
+    // writeable fields
+    private String name ;
+
+    private String genus ;
+    private String species ;
+    private String directory ;
+    private String blatdb ;
+
+    private Integer numFeatures ;
+    private Integer numSequences;
+    private Boolean valid ;
+    private Boolean current;
+    private Boolean publicMode;
+    private Boolean canEdit ;
+    private boolean editable;
+    private String nonDefaultTranslationTable ;
+
+
+    public OrganismInfo(){
+
+    }
+
+
+    public OrganismInfo(String name) {
+        this.name = name;
+    }
+
+    public String getBlatDb() {
+        return blatdb;
+    }
+
+    public void setBlatDb(String blatdb) {
+        this.blatdb = blatdb;
+    }
+
+    public boolean getPublicMode() {
+        return publicMode;
+    }
+
+    public void setPublicMode(boolean pm) {
+        this.publicMode=pm;
+    }
+
+    public String getGenus() {
+        return genus;
+    }
+
+    public void setGenus(String genus) {
+        this.genus = genus;
+    }
+
+    public String getSpecies() {
+        return species;
+    }
+
+    public void setSpecies(String species) {
+        this.species = species;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getNumFeatures() {
+        return numFeatures;
+    }
+
+    public void setNumFeatures(Integer numFeatures) {
+        this.numFeatures = numFeatures;
+    }
+
+    public Integer getNumSequences() {
+        return numSequences;
+    }
+
+    public void setNumSequences(Integer numSequences) {
+        this.numSequences = numSequences;
+    }
+
+    public String getDirectory() {
+        return directory;
+    }
+
+    public void setDirectory(String directory) {
+        this.directory = directory;
+    }
+
+    public Boolean getValid() {
+        return valid;
+    }
+
+    public void setValid(Boolean valid) {
+        this.valid = valid;
+    }
+
+    public void setCurrent(boolean current) {
+        this.current = current;
+    }
+
+    public boolean isCurrent() {
+        return current;
+    }
+
+    public String getNonDefaultTranslationTable() {
+        return nonDefaultTranslationTable;
+    }
+
+    public void setNonDefaultTranslationTable(String nonDefaultTranslationTable) {
+        this.nonDefaultTranslationTable = nonDefaultTranslationTable;
+    }
+
+    public JSONObject toJSON() {
+        JSONObject payload = new JSONObject();
+
+        if(id!=null) {
+            payload.put("id", new JSONString(id));
+        }
+        if(name!=null) {
+            payload.put("name", new JSONString(name));
+        }
+        if(directory!=null) {
+            payload.put("directory", new JSONString(directory));
+        }
+        if(current!=null) {
+            payload.put("current", JSONBoolean.getInstance(current));
+        }
+        if(blatdb!=null) {
+            payload.put("blatdb", new JSONString(blatdb));
+        }
+        if(genus!=null){
+            payload.put("genus",new JSONString(genus));
+        }
+        if(species!=null){
+            payload.put("species",new JSONString(species));
+        }
+        if(valid!=null){
+            payload.put("valid",JSONBoolean.getInstance(valid));
+        }
+        if(nonDefaultTranslationTable!=null){
+            payload.put("nonDefaultTranslationTable",new JSONString(nonDefaultTranslationTable));
+        }
+
+        payload.put("publicMode",JSONBoolean.getInstance(publicMode != null ? publicMode : false));
+
+        return payload;
+    }
+
+    public void setEditable(boolean editable) {
+        this.editable = editable;
+    }
+
+    public boolean isEditable() {
+        return editable;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfoConverter.java b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfoConverter.java
new file mode 100644
index 0000000..b12b20e
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismInfoConverter.java
@@ -0,0 +1,113 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.core.client.GWT;
+
+import com.google.gwt.json.client.*;
+import com.google.gwt.user.client.Window;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ndunn on 3/31/15.
+ */
+public class OrganismInfoConverter {
+
+
+    public static OrganismInfo convertFromJson(JSONObject object) {
+        OrganismInfo organismInfo = new OrganismInfo();
+        organismInfo.setId(object.get("id").isNumber().toString());
+        organismInfo.setName(object.get("commonName").isString().stringValue());
+        if (object.get("sequences") != null && object.get("sequences").isNumber() != null) {
+            organismInfo.setNumSequences((int) Math.round(object.get("sequences").isNumber().doubleValue()));
+        } else {
+            organismInfo.setNumSequences(0);
+        }
+        if (object.get("annotationCount") != null) {
+            organismInfo.setNumFeatures((int) object.get("annotationCount").isNumber().doubleValue());
+        } else {
+            organismInfo.setNumFeatures(0);
+        }
+        organismInfo.setDirectory(object.get("directory").isString().stringValue());
+        if (object.get("valid") != null) {
+            organismInfo.setValid(object.get("valid").isBoolean().booleanValue());
+        }
+        if (object.get("genus") != null && object.get("genus").isString() != null) {
+            organismInfo.setGenus(object.get("genus").isString().stringValue());
+        }
+        if (object.get("species") != null && object.get("species").isString() != null) {
+            organismInfo.setSpecies(object.get("species").isString().stringValue());
+        }
+        if (object.get("blatdb") != null && object.get("blatdb").isString() != null) {
+            organismInfo.setBlatDb(object.get("blatdb").isString().stringValue());
+        }
+        if (object.get("nonDefaultTranslationTable") != null && object.get("nonDefaultTranslationTable").isString() != null) {
+            organismInfo.setNonDefaultTranslationTable(object.get("nonDefaultTranslationTable").isString().stringValue());
+        }
+        if (object.get("publicMode") != null) {
+            organismInfo.setPublicMode(object.get("publicMode").isBoolean().booleanValue());
+        }
+        if (object.get("editable") != null) {
+            organismInfo.setEditable(object.get("editable").isBoolean().booleanValue());
+        }
+        organismInfo.setCurrent(object.get("currentOrganism") != null && object.get("currentOrganism").isBoolean().booleanValue());
+        return organismInfo;
+    }
+
+    public static List<OrganismInfo> convertFromJsonArray(JSONArray organismList) {
+        List<OrganismInfo> organismInfoList = new ArrayList<>();
+
+        for (int i = 0; i < organismList.size(); i++) {
+            OrganismInfo organismInfo = convertFromJson(organismList.get(i).isObject());
+            organismInfoList.add(organismInfo);
+        }
+
+        return organismInfoList;
+    }
+
+    /**
+     * @param organismInfo
+     * @return
+     */
+    public static JSONObject convertOrganismInfoToJSONObject(OrganismInfo organismInfo) {
+        JSONObject object = new JSONObject();
+        if (organismInfo.getId() != null) {
+            object.put("id", new JSONString(organismInfo.getId()));
+        }
+        object.put("commonName", new JSONString(organismInfo.getName()));
+        object.put("directory", new JSONString(organismInfo.getDirectory()));
+        if (organismInfo.getGenus() != null) {
+            object.put("genus", new JSONString(organismInfo.getGenus()));
+        }
+        if (organismInfo.getSpecies() != null) {
+            object.put("species", new JSONString(organismInfo.getSpecies()));
+        }
+        if (organismInfo.getNumSequences() != null) {
+            object.put("sequences", new JSONNumber(organismInfo.getNumFeatures()));
+        }
+        if (organismInfo.getNumFeatures() != null) {
+            object.put("annotationCount", new JSONNumber(organismInfo.getNumFeatures()));
+        }
+        if (organismInfo.getBlatDb() != null) {
+            object.put("blatdb", new JSONString(organismInfo.getBlatDb()));
+        }
+        if (organismInfo.getNonDefaultTranslationTable() != null) {
+            object.put("nonDefaultTranslationTable", new JSONString(organismInfo.getNonDefaultTranslationTable()));
+        }
+
+        GWT.log("convertOrganismInfoToJSONObject "+organismInfo.getPublicMode());
+        object.put("publicMode", JSONBoolean.getInstance(organismInfo.getPublicMode()));
+
+        return object;
+    }
+
+
+
+    public static List<OrganismInfo> convertJSONStringToOrganismInfoList(String jsonString) {
+        JSONValue returnValue = JSONParser.parseStrict(jsonString);
+        List<OrganismInfo> organismInfoList = new ArrayList<>();
+        JSONArray array = returnValue.isArray();
+        return convertFromJsonArray(array);
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismPermissionInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismPermissionInfo.java
new file mode 100644
index 0000000..f9ef1c8
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/OrganismPermissionInfo.java
@@ -0,0 +1,76 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+
+/**
+ * Created by ndunn on 3/24/15.
+ */
+abstract class OrganismPermissionInfo implements HasJSON{
+
+    String organismName;
+    Long id;
+    Boolean admin = false ;
+    Boolean write= false ;
+    Boolean export= false ;
+    Boolean read= false ;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getOrganismName() {
+        return organismName;
+    }
+
+    public void setOrganismName(String organismName) {
+        this.organismName = organismName;
+    }
+
+    public Boolean isAdmin() {
+        return admin;
+    }
+
+    public void setAdmin(Boolean admin) {
+        this.admin = admin;
+    }
+
+    public Boolean isWrite() {
+        return write;
+    }
+
+    public void setWrite(Boolean write) {
+        this.write = write;
+    }
+
+    public Boolean isExport() {
+        return export;
+    }
+
+    public void setExport(Boolean export) {
+        this.export = export;
+    }
+
+    public Boolean isRead() {
+        return read;
+    }
+
+    public void setRead(Boolean read) {
+        this.read = read;
+    }
+
+    public PermissionEnum getHighestPermission() {
+        if(admin) return PermissionEnum.ADMINISTRATE;
+        if(write) return PermissionEnum.WRITE;
+        if(export) return PermissionEnum.EXPORT;
+        if(read) return PermissionEnum.READ ;
+        return PermissionEnum.NONE;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfo.java
new file mode 100644
index 0000000..f122c1d
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfo.java
@@ -0,0 +1,140 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import org.bbop.apollo.gwt.client.comparators.AlphanumericSorter;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class SequenceInfo implements Comparable<SequenceInfo>,HasJSON{
+
+    private AlphanumericSorter alphanumericSorter = new AlphanumericSorter();
+
+    private Long id ;
+    private String name ;
+    private Integer length ;
+    private Integer start ;
+    private Integer end ;
+    private Integer startBp ; // preference values
+    private Integer endBp ;// preference values
+    private Integer count ;
+    private Boolean selected = false ;
+    private Boolean aDefault = false ;
+
+    public Integer getCount() {
+        return count;
+    }
+
+    public void setCount(Integer count) {
+        this.count = count;
+    }
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getLength() {
+        if(end!=null && start!=null){
+            return end-start;
+        }
+        if(length!=null){
+            return length ;
+        }
+        return -1 ;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Boolean isDefault() {
+        return aDefault;
+    }
+
+    public void setDefault(Boolean aDefault) {
+        this.aDefault = aDefault;
+    }
+
+    public Boolean getSelected() {
+        return selected;
+    }
+
+    public void setSelected(Boolean selected) {
+        this.selected = selected;
+    }
+
+    public Integer getStart() {
+        return start;
+    }
+
+    public void setStart(Integer start) {
+        this.start = start;
+    }
+
+    public Integer getEnd() {
+        return end;
+    }
+
+    public void setEnd(Integer end) {
+        this.end = end;
+    }
+
+    public void setLength(Integer length) {
+        this.length = length;
+    }
+
+    @Override
+    public int compareTo(SequenceInfo o) {
+        return alphanumericSorter.compare(name,o.getName());
+    }
+
+    public Integer getStartBp() {
+        return startBp;
+    }
+
+
+    public void setStartBp(Integer startBp) {
+        this.startBp = startBp;
+    }
+
+    public void setStartBp(Double startBp ) {
+        if(startBp!=null){
+            this.startBp = startBp.intValue();
+        }
+    }
+
+    public Integer getEndBp() {
+        return endBp;
+    }
+
+    public void setEndBp(Integer endBp) {
+        this.endBp = endBp;
+    }
+
+    public void setEndBp(Double endBp) {
+        if(endBp!=null){
+            this.endBp = endBp.intValue();
+        }
+    }
+
+    public JSONObject toJSON() {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("id", new JSONNumber(id));
+        jsonObject.put("name", new JSONString(name));
+        jsonObject.put("start", new JSONNumber(start));
+        if(count != null) jsonObject.put("count", new JSONNumber(count));
+        jsonObject.put("end", new JSONNumber(end));
+        jsonObject.put("length", new JSONNumber(length));
+        return jsonObject;
+
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfoConverter.java b/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfoConverter.java
new file mode 100644
index 0000000..5ea7079
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/SequenceInfoConverter.java
@@ -0,0 +1,46 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ndunn on 4/20/15.
+ */
+public class SequenceInfoConverter {
+    public static SequenceInfo convertFromJson(JSONObject object) {
+        SequenceInfo sequenceInfo = new SequenceInfo();
+        sequenceInfo.setId((long) object.get("id").isNumber().doubleValue());
+        sequenceInfo.setName(object.get("name").isString().stringValue());
+        sequenceInfo.setStart((int) object.get("start").isNumber().doubleValue());
+        sequenceInfo.setEnd((int) object.get("end").isNumber().doubleValue());
+        sequenceInfo.setLength((int) object.get("length").isNumber().doubleValue());
+        sequenceInfo.setSelected(object.get("selected") != null && object.get("selected").isBoolean().booleanValue());
+        if (object.get("count") != null) sequenceInfo.setCount((int) object.get("count").isNumber().doubleValue());
+        sequenceInfo.setDefault(object.get("aDefault") != null && object.get("aDefault").isBoolean().booleanValue());
+
+
+        // set the preferences if they are there
+        if (object.containsKey("startBp")) {
+            sequenceInfo.setStartBp(object.get("startBp").isNumber() != null ? object.get("startBp").isNumber().doubleValue() : null);
+        }
+        if (object.containsKey("endBp")) {
+            sequenceInfo.setEndBp(object.get("endBp").isNumber() != null ? object.get("endBp").isNumber().doubleValue() : null);
+        }
+
+        return sequenceInfo;
+    }
+
+    public static List<SequenceInfo> convertFromJsonArray(JSONArray sequenceList) {
+        List<SequenceInfo> sequenceInfoArrayList = new ArrayList<>();
+
+
+        for (int i = 0; sequenceList != null && i < sequenceList.size(); i++) {
+            sequenceInfoArrayList.add(convertFromJson(sequenceList.get(i).isObject()));
+        }
+
+        return sequenceInfoArrayList;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/TrackInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/TrackInfo.java
new file mode 100644
index 0000000..d2c5f39
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/TrackInfo.java
@@ -0,0 +1,122 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONObject;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class TrackInfo implements Comparable<TrackInfo> {
+
+
+    public static final String TRACK_UNCATEGORIZED = "Uncategorized";
+
+    private String name;
+    private String label;
+    private String type;
+    private Boolean visible;
+    private String urlTemplate ;
+    private String category;
+
+    private JSONObject payload ;
+
+    public TrackInfo(){}
+
+    @Override
+    public int compareTo(TrackInfo o) {
+        return name.compareTo(o.name);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public Boolean getVisible() {
+        return visible;
+    }
+
+    public void setVisible(Boolean visible) {
+        this.visible = visible;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public String getUrlTemplate() {
+        return urlTemplate;
+    }
+
+    public void setUrlTemplate(String urlTemplate) {
+        this.urlTemplate = urlTemplate;
+    }
+
+    public JSONObject getPayload() {
+        return payload;
+    }
+
+    public void setPayload(JSONObject payload) {
+        this.payload = payload;
+    }
+
+    public String getCategory() {
+        return category;
+    }
+
+    public void setCategory(String category) {
+        this.category = category;
+    }
+
+    public String getStandardCategory(){
+        if(category==null || category.trim().length()==0){
+            return TRACK_UNCATEGORIZED ;
+        }
+        else{
+            String categoryString = "";
+            for(String c : category.split("\\/")){
+                categoryString+=c +"/";
+            }
+            return  categoryString.substring(0,categoryString.length()-1);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TrackInfo)) return false;
+
+        TrackInfo trackInfo = (TrackInfo) o;
+
+        if (!getName().equals(trackInfo.getName())) return false;
+        if (getLabel() != null ? !getLabel().equals(trackInfo.getLabel()) : trackInfo.getLabel() != null) return false;
+        if (!getType().equals(trackInfo.getType())) return false;
+        if (!getCategory().equals(trackInfo.getCategory())) return false;
+        return getUrlTemplate() != null ? getUrlTemplate().equals(trackInfo.getUrlTemplate()) : trackInfo.getUrlTemplate() == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = getName().hashCode();
+        result = 31 * result + (getLabel() != null ? getLabel().hashCode() : 0);
+        result = 31 * result + getType().hashCode();
+        result = 31 * result + (getUrlTemplate() != null ? getUrlTemplate().hashCode() : 0);
+        result = 31 * result + (getCategory() != null ? getCategory().hashCode() : 0);
+        return result;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfo.java
new file mode 100644
index 0000000..debec80
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfo.java
@@ -0,0 +1,198 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by ndunn on 12/18/14.
+ */
+public class UserInfo implements HasJSON {
+    private Long userId;
+    private String firstName;
+    private String lastName;
+    private String email;
+    private String role;
+    private Integer numberUserGroups;
+    private String password;
+    private List<String> groupList = new ArrayList<>();
+    private List<String> availableGroupList = new ArrayList<>();
+    private Map<String, UserOrganismPermissionInfo> organismPermissionMap = new HashMap<>();
+
+    public UserInfo() {
+    }
+
+
+    public UserInfo(String firstName) {
+        this.firstName = firstName;
+        this.email = (firstName.replace(" ", "_") + "@place.gov").toLowerCase();
+        this.numberUserGroups = (int) Math.round(Math.random() * 100);
+    }
+
+    public UserInfo(JSONObject userObject) {
+        setEmail(userObject.get("email").isString().stringValue());
+        setFirstName(userObject.get("firstName").isString().stringValue());
+        setLastName(userObject.get("lastName").isString().stringValue());
+        if (userObject.get("userId") != null) {
+            setUserId((long) userObject.get("userId").isNumber().doubleValue());
+        } else if (userObject.get("id") != null) {
+            setUserId((long) userObject.get("id").isNumber().doubleValue());
+        }
+    }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public Integer getNumberUserGroups() {
+        return numberUserGroups;
+    }
+
+    public void setNumberUserGroups(Integer numberUserGroups) {
+        this.numberUserGroups = numberUserGroups;
+    }
+
+    public String getName() {
+        return firstName + " " + lastName;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public String getRole() {
+        return role;
+    }
+
+    public void setRole(String role) {
+        this.role = role;
+    }
+
+    public List<String> getGroupList() {
+        return groupList;
+    }
+
+    public void setGroupList(List<String> groupList) {
+        this.groupList = groupList;
+    }
+
+    public List<String> getAvailableGroupList() {
+        return availableGroupList;
+    }
+
+    public void setAvailableGroupList(List<String> availableGroupList) {
+        this.availableGroupList = availableGroupList;
+    }
+
+    public Map<String, UserOrganismPermissionInfo> getOrganismPermissionMap() {
+        return organismPermissionMap;
+    }
+
+    public void setOrganismPermissionMap(Map<String, UserOrganismPermissionInfo> organismPermissionMap) {
+        this.organismPermissionMap = organismPermissionMap;
+    }
+
+    public JSONObject getJSONWithoutPassword() {
+        JSONObject returnObject = toJSON();
+        returnObject.put("password", new JSONString(""));
+        return returnObject;
+    }
+
+    public JSONObject toJSON() {
+        JSONObject jsonObject = new JSONObject();
+        if (userId != null) {
+            jsonObject.put("userId", new JSONNumber(userId));
+        }
+        jsonObject.put("firstName", new JSONString(firstName));
+        jsonObject.put("lastName", new JSONString(lastName));
+        jsonObject.put("email", new JSONString(email));
+        // TODO: do not use this as it is the "security user" placeholder
+//        jsonObject.put("username", new JSONString(email));
+        if (role != null) {
+            jsonObject.put("role", new JSONString(role));
+        }
+
+        JSONArray groupArray = new JSONArray();
+        for (int i = 0; i < groupList.size(); i++) {
+            groupArray.set(i, new JSONString(groupList.get(i)));
+        }
+        jsonObject.put("groups", groupArray);
+
+        JSONArray availableGroupArray = new JSONArray();
+        for (int i = 0; i < availableGroupList.size(); i++) {
+            availableGroupArray.set(i, new JSONString(availableGroupList.get(i)));
+        }
+        jsonObject.put("availableGroups", availableGroupArray);
+
+        if (password != null) {
+            jsonObject.put("newPassword", new JSONString(password));
+        }
+
+        JSONArray organismPermissions = new JSONArray();
+        int index = 0;
+        for (String organism : organismPermissionMap.keySet()) {
+            JSONObject orgPermission = new JSONObject();
+            orgPermission.put(organism, organismPermissionMap.get(organism).toJSON());
+            organismPermissions.set(index, orgPermission);
+            ++index;
+        }
+        jsonObject.put("organismPermissions", organismPermissions);
+
+
+        return jsonObject;
+    }
+
+    PermissionEnum findHighestPermission() {
+        if (organismPermissionMap == null) return null;
+        PermissionEnum highestPermission = PermissionEnum.NONE;
+
+        for (UserOrganismPermissionInfo userOrganismPermissionInfo : organismPermissionMap.values()) {
+            PermissionEnum thisHighestPermission = userOrganismPermissionInfo.getHighestPermission();
+            if (thisHighestPermission.getRank() > highestPermission.getRank()) {
+                highestPermission = thisHighestPermission;
+            }
+        }
+
+        return highestPermission;
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfoConverter.java b/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfoConverter.java
new file mode 100644
index 0000000..f6c6569
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/UserInfoConverter.java
@@ -0,0 +1,112 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.user.client.Window;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Created by ndunn on 3/31/15.
+ */
+public class UserInfoConverter {
+
+    public static List<UserInfo> convertFromJsonArray(JSONArray jsonArray) {
+
+        List<UserInfo> userInfoList = new ArrayList<>();
+        for(int i = 0 ; i < jsonArray.size() ; i++){
+            userInfoList.add(convertToUserInfoFromJSON(jsonArray.get(i).isObject()));
+        }
+
+        return userInfoList;
+    }
+
+    public static UserInfo convertToUserInfoFromJSON(JSONObject object){
+        UserInfo userInfo = new UserInfo();
+
+        userInfo.setUserId((long) object.get("userId").isNumber().doubleValue());
+        userInfo.setFirstName(object.get("firstName").isString().stringValue());
+        userInfo.setLastName(object.get("lastName").isString().stringValue());
+        userInfo.setEmail(object.get("username").isString().stringValue());
+        if (object.get("role") != null && object.get("role").isString() != null) {
+            userInfo.setRole(object.get("role").isString().stringValue().toLowerCase());
+        } else {
+            userInfo.setRole("user");
+        }
+
+        if(object.get("groups")!=null){
+            JSONArray groupArray = object.get("groups").isArray();
+            List<String> groupList = new ArrayList<>();
+            for (int j = 0; j < groupArray.size(); j++) {
+                String groupValue = groupArray.get(j).isObject().get("name").isString().stringValue();
+                groupList.add(groupValue);
+            }
+            userInfo.setGroupList(groupList);
+        }
+
+        if(object.get("availableGroups")!=null) {
+            JSONArray availableGroupArray = object.get("availableGroups").isArray();
+            List<String> availableGroupList = new ArrayList<>();
+            for (int j = 0; j < availableGroupArray.size(); j++) {
+                String availableGroupValue = availableGroupArray.get(j).isObject().get("name").isString().stringValue();
+                availableGroupList.add(availableGroupValue);
+            }
+            userInfo.setAvailableGroupList(availableGroupList);
+        }
+
+
+        // TODO: use shared permission enums
+        if(object.get("organismPermissions")!=null) {
+            JSONArray organismArray = object.get("organismPermissions").isArray();
+            Map<String, UserOrganismPermissionInfo> organismPermissionMap = new TreeMap<>();
+            for (int j = 0; j < organismArray.size(); j++) {
+                JSONObject organismPermissionJsonObject = organismArray.get(j).isObject();
+                UserOrganismPermissionInfo userOrganismPermissionInfo = new UserOrganismPermissionInfo();
+                if (organismPermissionJsonObject.get("id") != null) {
+                    userOrganismPermissionInfo.setId((long) organismPermissionJsonObject.get("id").isNumber().doubleValue());
+                }
+                if (organismPermissionJsonObject.get("userId") != null) {
+                    userOrganismPermissionInfo.setUserId((long) organismPermissionJsonObject.get("userId").isNumber().doubleValue());
+                }
+//                if (organismPermissionJsonObject.get("groupId") != null) {
+//                    userOrganismPermissionInfo.setUserId((long) organismPermissionJsonObject.get("userId").isNumber().doubleValue());
+//                }
+                userOrganismPermissionInfo.setOrganismName(organismPermissionJsonObject.get("organism").isString().stringValue());
+                if (organismPermissionJsonObject.get("permissions") != null) {
+                    JSONArray permissionsArray = JSONParser.parseStrict(organismPermissionJsonObject.get("permissions").isString().stringValue()).isArray();
+                    for (int permissionIndex = 0; permissionIndex < permissionsArray.size(); ++permissionIndex) {
+                        String permission = permissionsArray.get(permissionIndex).isString().stringValue();
+                        switch (permission) {
+                            case "ADMINISTRATE":
+                                userOrganismPermissionInfo.setAdmin(true);
+                                break;
+                            case "WRITE":
+                                userOrganismPermissionInfo.setWrite(true);
+                                break;
+                            case "EXPORT":
+                                userOrganismPermissionInfo.setExport(true);
+                                break;
+                            case "READ":
+                                userOrganismPermissionInfo.setRead(true);
+                                break;
+
+                            default:
+                                Bootbox.alert("Unrecognized permission '" + permission+"'");
+                        }
+                    }
+                }
+
+
+                organismPermissionMap.put(userOrganismPermissionInfo.getOrganismName(), userOrganismPermissionInfo);
+            }
+            userInfo.setOrganismPermissionMap(organismPermissionMap);
+        }
+        return userInfo ;
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/dto/UserOrganismPermissionInfo.java b/src/gwt/org/bbop/apollo/gwt/client/dto/UserOrganismPermissionInfo.java
new file mode 100644
index 0000000..8c597ab
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/dto/UserOrganismPermissionInfo.java
@@ -0,0 +1,38 @@
+package org.bbop.apollo.gwt.client.dto;
+
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+/**
+ * Created by ndunn on 3/24/15.
+ */
+public class UserOrganismPermissionInfo extends OrganismPermissionInfo{
+
+    Long userId ;
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public JSONObject toJSON() {
+        JSONObject payload = new JSONObject();
+        payload.put("organism",new JSONString(organismName));
+        payload.put("ADMINISTRATE",JSONBoolean.getInstance(admin));
+        payload.put("WRITE",JSONBoolean.getInstance(write));
+        payload.put("EXPORT",JSONBoolean.getInstance(export));
+        payload.put("READ",JSONBoolean.getInstance(read));
+        if(userId!=null){
+            payload.put("userId",new JSONNumber(userId));
+        }
+        if(id!=null){
+            payload.put("id",new JSONNumber(id));
+        }
+        return payload;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEvent.java b/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEvent.java
new file mode 100644
index 0000000..fd37c95
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEvent.java
@@ -0,0 +1,53 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.GwtEvent;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+
+/**
+ * Created by ndunn on 2/2/15.
+ */
+public class AnnotationInfoChangeEvent extends GwtEvent<AnnotationInfoChangeEventHandler>{
+
+    public static Type<AnnotationInfoChangeEventHandler> TYPE = new Type<AnnotationInfoChangeEventHandler>();
+
+    private AnnotationInfo annotationInfo ;
+    private Action action ;
+
+    public enum Action{
+        UPDATE,
+        INSERT,
+        DELETE,
+        SET_FOCUS,
+    }
+
+    public AnnotationInfoChangeEvent(AnnotationInfo annotationInfo,Action action){
+        this.annotationInfo = annotationInfo ;
+        this.action = action ;
+    }
+
+    @Override
+    public Type<AnnotationInfoChangeEventHandler> getAssociatedType() {
+        return TYPE ;
+    }
+
+    @Override
+    protected void dispatch(AnnotationInfoChangeEventHandler handler) {
+       handler.onAnnotationChanged(this);
+    }
+
+    public AnnotationInfo getAnnotationInfo() {
+        return annotationInfo;
+    }
+
+    public void setAnnotationInfo(AnnotationInfo annotationInfo) {
+        this.annotationInfo = annotationInfo;
+    }
+
+    public Action getAction() {
+        return action;
+    }
+
+    public void setAction(Action action) {
+        this.action = action;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEventHandler.java b/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEventHandler.java
new file mode 100644
index 0000000..a8f0cb2
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/AnnotationInfoChangeEventHandler.java
@@ -0,0 +1,13 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public interface AnnotationInfoChangeEventHandler extends EventHandler{
+
+    void onAnnotationChanged(AnnotationInfoChangeEvent annotationInfoChangeEvent);
+
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/ExportEvent.java b/src/gwt/org/bbop/apollo/gwt/client/event/ExportEvent.java
new file mode 100644
index 0000000..0890174
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/ExportEvent.java
@@ -0,0 +1,49 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.GwtEvent;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.SequenceInfo;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public class ExportEvent extends GwtEvent<ExportEventHandler> {
+
+    public static Type<ExportEventHandler> TYPE = new Type<ExportEventHandler>();
+
+    public enum Action {
+        EXPORT_READY,
+        EXPORT_FINISHED,
+    }
+
+    public enum Flavor{
+        GFF3,
+        FASTA,
+        CHADO,
+    }
+
+    private Action thisAction;
+    private Flavor thisFlavor;
+    private OrganismInfo organismInfo ;
+    private List<SequenceInfo> sequenceInfoList ;
+
+    @Override
+    public Type<ExportEventHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    @Override
+    protected void dispatch(ExportEventHandler handler) {
+        handler.onExport(this);
+    }
+
+
+    public ExportEvent(Action action,Flavor flavor,OrganismInfo organismInfo,List<SequenceInfo> sequenceInfoList) {
+        this.thisAction = action;
+        this.thisFlavor = flavor ;
+        this.organismInfo = organismInfo ;
+        this.sequenceInfoList = sequenceInfoList ;
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/ExportEventHandler.java b/src/gwt/org/bbop/apollo/gwt/client/event/ExportEventHandler.java
new file mode 100644
index 0000000..a994be9
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/ExportEventHandler.java
@@ -0,0 +1,13 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public interface ExportEventHandler extends EventHandler{
+
+    void onExport(ExportEvent exportEvent);
+
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEvent.java b/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEvent.java
new file mode 100644
index 0000000..7727fb0
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEvent.java
@@ -0,0 +1,69 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.GwtEvent;
+import org.bbop.apollo.gwt.client.dto.GroupInfo;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public class GroupChangeEvent extends GwtEvent<GroupChangeEventHandler>{
+
+    public static Type<GroupChangeEventHandler> TYPE = new Type<GroupChangeEventHandler>();
+
+    private List<GroupInfo> groupInfoList;
+    private Action action ;
+    private String group ;
+
+    public GroupChangeEvent(){}
+    public GroupChangeEvent(List<GroupInfo> groupInfoList, Action action, String group){
+        this.groupInfoList = groupInfoList ;
+        this.action = action ;
+        this.group = group ;
+    }
+
+    public GroupChangeEvent(List<GroupInfo> groupInfoList, Action action){
+        this.groupInfoList = groupInfoList ;
+        this.action = action ;
+    }
+
+    public GroupChangeEvent(Action action) {
+        this.action = action;
+    }
+
+    public List<GroupInfo> getGroupInfoList() {
+        return groupInfoList;
+    }
+
+    public void setGroupInfoList(List<GroupInfo> groupInfoList) {
+        this.groupInfoList = groupInfoList;
+    }
+
+    @Override
+    public Type<GroupChangeEventHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    @Override
+    protected void dispatch(GroupChangeEventHandler handler) {
+        handler.onGroupChanged(this);
+    }
+
+    public Action getAction() {
+        return action;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public enum Action{
+        ADD_USER_TO_GROUP,
+        REMOVE_USER_FROM_GROUP,
+        RELOAD_GROUPS,
+        ADD_GROUP,
+        REMOVE_GROUP,
+        GROUPS_RELOADED,
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEventHandler.java b/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEventHandler.java
new file mode 100644
index 0000000..4cd85f9
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/GroupChangeEventHandler.java
@@ -0,0 +1,12 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public interface GroupChangeEventHandler extends EventHandler{
+
+    void onGroupChanged(GroupChangeEvent authenticationEvent);
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEvent.java b/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEvent.java
new file mode 100644
index 0000000..4335a07
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEvent.java
@@ -0,0 +1,74 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.GwtEvent;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public class OrganismChangeEvent extends GwtEvent<OrganismChangeEventHandler>{
+
+    public static Type<OrganismChangeEventHandler> TYPE = new Type<OrganismChangeEventHandler>();
+
+    public List<OrganismInfo> organismInfoList;
+    private Action action;
+    private String currentSequence;
+    private String currentOrganism;
+
+    public OrganismChangeEvent(){}
+    public OrganismChangeEvent(List<OrganismInfo> organismInfoList){
+        this.organismInfoList = organismInfoList ;
+    }
+
+    public OrganismChangeEvent(Action action) {
+        this.action = action;
+    }
+
+    public OrganismChangeEvent(Action changedOrganism, String sequenceNameString,String organismNameString) {
+        this.action = changedOrganism ;
+        this.currentSequence = sequenceNameString ;
+        this.currentOrganism = organismNameString ;
+    }
+
+    public String getCurrentOrganism() {
+        return currentOrganism;
+    }
+
+    public List<OrganismInfo> getOrganismInfoList() {
+        return organismInfoList;
+    }
+
+    public void setOrganismInfoList(List<OrganismInfo> organismInfoList) {
+        this.organismInfoList = organismInfoList;
+    }
+
+    @Override
+    public Type<OrganismChangeEventHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    @Override
+    protected void dispatch(OrganismChangeEventHandler handler) {
+        handler.onOrganismChanged(this);
+    }
+
+    public void setAction(Action action) {
+        this.action = action;
+    }
+
+    public Action getAction() {
+        return action;
+    }
+
+    public String getCurrentSequence() {
+        return currentSequence;
+    }
+
+    public enum Action {
+        CHANGED_ORGANISM, LOADED_ORGANISMS
+    }
+
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEventHandler.java b/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEventHandler.java
new file mode 100644
index 0000000..6b3fef7
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/OrganismChangeEventHandler.java
@@ -0,0 +1,13 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public interface OrganismChangeEventHandler extends EventHandler{
+
+    void onOrganismChanged(OrganismChangeEvent authenticationEvent);
+
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEvent.java b/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEvent.java
new file mode 100644
index 0000000..11786fc
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEvent.java
@@ -0,0 +1,89 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.shared.PermissionEnum;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public class UserChangeEvent extends GwtEvent<UserChangeEventHandler>{
+
+    public static Type<UserChangeEventHandler> TYPE = new Type<UserChangeEventHandler>();
+
+    private List<UserInfo> userInfoList;
+    private Action action ;
+    private String group ;
+    private PermissionEnum highestPermission ;
+
+    public UserChangeEvent(Action action){
+        this.action = action ;
+    }
+    public UserChangeEvent(List<UserInfo> userInfoList,Action action,String group){
+        this.userInfoList = userInfoList ;
+        this.action = action ;
+        this.group = group ;
+    }
+
+
+    public UserChangeEvent(Action action,PermissionEnum highestPermission){
+        this.action = action ;
+        this.highestPermission = highestPermission ;
+        GWT.log(highestPermission.getDisplay());
+    }
+
+    public UserChangeEvent(List<UserInfo> userInfoList,Action action){
+        this.userInfoList = userInfoList ;
+        this.action = action ;
+    }
+
+    public List<UserInfo> getUserInfoList() {
+        return userInfoList;
+    }
+
+    public void setUserInfoList(List<UserInfo> userInfoList) {
+        this.userInfoList = userInfoList;
+    }
+
+    @Override
+    public Type<UserChangeEventHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    @Override
+    protected void dispatch(UserChangeEventHandler handler) {
+        handler.onUserChanged(this);
+    }
+
+    public Action getAction() {
+        return action;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public PermissionEnum getHighestPermission() {
+        return highestPermission;
+    }
+
+    public void setHighestPermission(PermissionEnum highestPermission) {
+        this.highestPermission = highestPermission;
+    }
+
+    public enum Action{
+        ADD_USER_TO_GROUP,
+        REMOVE_USER_FROM_GROUP,
+        RELOAD_USERS,
+        PERMISSION_CHANGED,
+        USERS_RELOADED,
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEventHandler.java b/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEventHandler.java
new file mode 100644
index 0000000..6027750
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/event/UserChangeEventHandler.java
@@ -0,0 +1,12 @@
+package org.bbop.apollo.gwt.client.event;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Created by ndunn on 1/19/15.
+ */
+public interface UserChangeEventHandler extends EventHandler{
+
+    void onUserChanged(UserChangeEvent authenticationEvent);
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/resources/Table.css b/src/gwt/org/bbop/apollo/gwt/client/resources/Table.css
new file mode 100644
index 0000000..0508902
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/resources/Table.css
@@ -0,0 +1,141 @@
+ at def selectionBorderWidth 2px;
+.dataGridWidget {
+   background-color: snow;
+}
+
+.dataGridFirstColumn {
+
+}
+
+.dataGridLastColumn {
+  text-align: right;
+}
+
+
+
+.dataGridFooter {
+    border-top: 2px solid #6f7277;
+    padding: 3px 15px;
+    text-align: left;
+    color: #4b4a4a;
+    text-shadow: #ddf 1px 1px 0;
+    overflow: hidden;
+    white-space: nowrap;
+}
+
+.dataGridHeader {
+    /*border-bottom: 2px solid #6f7277;*/
+    /*padding: 3px 15px;*/
+    /*text-align: left;*/
+    /*color: #4b4a4a;*/
+    /*text-shadow: #ddf 1px 1px 0;*/
+    /*overflow: hidden;*/
+    /*white-space: nowrap;*/
+}
+
+.dataGridCell {
+    padding: 2px 15px;
+    overflow: hidden;
+}
+
+.dataGridFirstColumnFooter {
+
+}
+
+.dataGridFirstColumnHeader {
+
+}
+
+.dataGridLastColumnFooter {
+
+}
+
+.dataGridLastColumnHeader {
+
+}
+
+.dataGridSortableHeader {
+    cursor: pointer;
+    cursor: hand;
+}
+
+.dataGridSortableHeader:hover {
+    color: #6c6b6b;
+}
+
+.dataGridSortedHeaderAscending {
+
+}
+
+.dataGridSortedHeaderDescending {
+
+}
+
+.dataGridEvenRow {
+    background: #ffffff;
+}
+
+.dataGridEvenRowCell {
+    /*border: selectionBorderWidth solid #ffffff;*/
+    border: 0;
+}
+
+.dataGridOddRow {
+    background: #f3f7fb;
+}
+
+.dataGridOddRowCell {
+    /*border: selectionBorderWidth solid #f3f7fb;*/
+    border: 0;
+}
+
+.dataGridHoveredRow {
+    /*background: #eee;*/
+    /*background-color: red;*/
+    background-color: powderblue;
+
+    cursor: pointer;
+    cursor: hand;
+}
+
+.dataGridHoveredRowCell {
+    /*border: selectionBorderWidth solid #eee;*/
+    border: 0;
+    color: black;
+    /*background-color: magenta;*/
+    /*background-color: #728ce5;*/
+    background-color: powderblue;
+}
+
+.dataGridKeyboardSelectedRow {
+    /*background: #F0E68C;*/
+    /*background-color: #728ce5;*/
+    background-color: lightskyblue;
+}
+
+.dataGridKeyboardSelectedRowCell {
+    /*border: selectionBorderWidth solid #F0E68C;*/
+    border: 0 ;
+    /*background-color: #728ce5;*/
+    background-color: lightskyblue;
+}
+
+.dataGridSelectedRow {
+    background: #628cd5;
+    color: black;
+    height: auto;
+    overflow: auto;
+}
+
+.dataGridSelectedRowCell {
+    /*border: selectionBorderWidth solid #628cd5;*/
+    border: 0 ;
+}
+
+/**
+ * The keyboard selected cell is visible over selection.
+ */
+.dataGridKeyboardSelectedCell {
+    /*border: selectionBorderWidth solid #d7dde8;*/
+    border: 0;
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/resources/TableResources.java b/src/gwt/org/bbop/apollo/gwt/client/resources/TableResources.java
new file mode 100644
index 0000000..7dfb52a
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/resources/TableResources.java
@@ -0,0 +1,18 @@
+package org.bbop.apollo.gwt.client.resources;
+
+import com.google.gwt.resources.client.ClientBundle;
+import org.gwtbootstrap3.client.ui.gwt.DataGrid;
+
+/**
+ * Created by ndunn on 12/19/14.
+ */
+public class TableResources implements  ClientBundle{
+    // TableCss cell table
+    public interface TableCss extends DataGrid.Resources
+    {
+        @ClientBundle.Source({DataGrid.Style.DEFAULT_CSS,
+                "org/bbop/apollo/gwt/client/resources/Table.css"})
+        DataGridStyle dataGridStyle();
+        interface DataGridStyle extends DataGrid.Style {}
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/resources/Tree.css b/src/gwt/org/bbop/apollo/gwt/client/resources/Tree.css
new file mode 100644
index 0000000..fc660db
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/resources/Tree.css
@@ -0,0 +1,83 @@
+/**/
+ /** Copyright 2010 Google Inc.*/
+ /** */
+ /** Licensed under the Apache License, Version 2.0 (the "License"); you may not*/
+ /** use this file except in compliance with the License. You may obtain a copy of*/
+ /** the License at*/
+ /** */
+ /** http://www.apache.org/licenses/LICENSE-2.0*/
+ /** */
+ /** Unless required by applicable law or agreed to in writing, software*/
+ /** distributed under the License is distributed on an "AS IS" BASIS, WITHOUT*/
+ /** WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the*/
+ /** License for the specific language governing permissions and limitations under*/
+ /** the License.*/
+ /**/
+/*.cellTreeWidget {*/
+
+/*}*/
+
+/*.cellTreeEmptyMessage {*/
+    /*padding-left: 16px;*/
+    /*font-style: italic;*/
+/*}*/
+
+/*.cellTreeItem {*/
+    /*padding-top: 4px;*/
+    /*padding-bottom: 4px;*/
+    /*cursor: hand;*/
+    /*cursor: pointer;*/
+/*}*/
+
+/*.cellTreeItemImage {*/
+
+/*}*/
+
+/*.cellTreeItemImageValue {*/
+
+/*}*/
+
+/*.cellTreeItemValue {*/
+    /*padding-left: 3px;*/
+    /*padding-right: 3px;*/
+/*}*/
+
+/**/
+/*div:focus { outline: none; }*/
+/**/
+/*.cellTreeKeyboardSelectedItem {*/
+    /*  background-color: #ffff00; */
+/*}*/
+
+/*.cellTreeOpenItem {*/
+
+/*}*/
+
+/*.cellTreeTopItem {*/
+    /*font-weight: bold;*/
+    /*color: #4b4a4a;*/
+    /*margin-top: 20px;*/
+    /*padding: 3px 13px 3px 10px !important;*/
+/*}*/
+
+/*.cellTreeTopItemImage {*/
+
+/*}*/
+
+/*.cellTreeTopItemImageValue {*/
+    /*border-bottom: 1px solid #6f7277;*/
+    /*padding-bottom: 1px;*/
+/*}*/
+
+/*@sprite .cellTreeSelectedItem {*/
+    /*gwt-image: 'cellTreeSelectedBackground';*/
+    /*background-color: #628cd5;*/
+    /*color: white;*/
+    /*height: auto;*/
+    /*overflow: auto;*/
+/*}*/
+
+/*.cellTreeShowMoreButton {*/
+    /*padding-left: 16px;*/
+    /*outline: none;*/
+/*}*/
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/client/resources/TreeResources.java b/src/gwt/org/bbop/apollo/gwt/client/resources/TreeResources.java
new file mode 100644
index 0000000..39d0388
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/resources/TreeResources.java
@@ -0,0 +1,9 @@
+package org.bbop.apollo.gwt.client.resources;
+
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.user.cellview.client.CellTree;
+
+public interface TreeResources extends CellTree.Resources {
+    @ClientBundle.Source("org/bbop/apollo/gwt/client/resources/Tree.css")
+    public CellTree.Style cellTreeStyle();
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/AnnotationRestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/AnnotationRestService.java
new file mode 100644
index 0000000..5677232
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/AnnotationRestService.java
@@ -0,0 +1,30 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import org.bbop.apollo.gwt.client.dto.AnnotationInfo;
+
+/**
+ * Created by ndunn on 1/28/15.
+ */
+public class AnnotationRestService {
+
+   public static JSONObject convertAnnotationInfoToJSONObject(AnnotationInfo annotationInfo){
+       JSONObject jsonObject = new JSONObject();
+
+       jsonObject.put("name",new JSONString(annotationInfo.getName()));
+       jsonObject.put("uniquename",new JSONString(annotationInfo.getUniqueName()));
+       jsonObject.put("track", new JSONString(annotationInfo.getSequence()));
+       jsonObject.put("symbol",annotationInfo.getSymbol()!=null ? new JSONString(annotationInfo.getSymbol()):new JSONString(""));
+       jsonObject.put("description",annotationInfo.getDescription()!=null ? new JSONString(annotationInfo.getDescription()):new JSONString(""));
+       jsonObject.put("type",new JSONString(annotationInfo.getType()));
+       jsonObject.put("fmin",annotationInfo.getMin()!=null ? new JSONNumber(annotationInfo.getMin()): null);
+       jsonObject.put("fmax",annotationInfo.getMax()!=null ? new JSONNumber(annotationInfo.getMax()): null);
+       jsonObject.put("strand",annotationInfo.getStrand()!=null ? new JSONNumber(annotationInfo.getStrand()): null);
+
+
+       return jsonObject;
+
+   }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/GroupRestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/GroupRestService.java
new file mode 100644
index 0000000..9d45138
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/GroupRestService.java
@@ -0,0 +1,193 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.*;
+import com.google.gwt.user.client.Window;
+import org.bbop.apollo.gwt.client.Annotator;
+import org.bbop.apollo.gwt.client.AnnotatorPanel;
+import org.bbop.apollo.gwt.client.dto.GroupInfo;
+import org.bbop.apollo.gwt.client.dto.GroupOrganismPermissionInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.client.event.GroupChangeEvent;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+import org.gwtbootstrap3.extras.select.client.ui.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Created by ndunn on 3/30/15.
+ */
+public class GroupRestService {
+
+
+    public static void loadGroups(final List<GroupInfo> groupInfoList) {
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                groupInfoList.clear();
+                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+                JSONArray array = returnValue.isArray();
+
+                for (int i = 0; array!=null && i < array.size(); i++) {
+                    JSONObject object = array.get(i).isObject();
+
+                    GroupInfo groupInfo = new GroupInfo();
+                    groupInfo.setId((long) object.get("id").isNumber().doubleValue());
+                    groupInfo.setName(object.get("name").isString().stringValue());
+                    groupInfo.setNumberOfUsers((int) object.get("numberOfUsers").isNumber().doubleValue());
+
+                    List<UserInfo> userInfoList = new ArrayList<>();
+
+
+                    if (object.get("users") != null) {
+                        JSONArray usersArray = object.get("users").isArray();
+                        for (int j = 0; j < usersArray.size(); j++) {
+                            JSONObject userObject = usersArray.get(j).isObject();
+                            UserInfo userInfo = new UserInfo(userObject);
+                            userInfoList.add(userInfo);
+                        }
+                    }
+
+
+                    groupInfo.setUserInfoList(userInfoList);
+
+
+                    // TODO: use shared permission enums
+                    JSONArray organismArray = object.get("organismPermissions").isArray();
+                    Map<String, GroupOrganismPermissionInfo> organismPermissionMap = new TreeMap<>();
+                    for (int j = 0; j < organismArray.size(); j++) {
+                        JSONObject organismPermissionJsonObject = organismArray.get(j).isObject();
+                        GroupOrganismPermissionInfo groupOrganismPermissionInfo = new GroupOrganismPermissionInfo();
+                        if (organismPermissionJsonObject.get("id") != null) {
+                            groupOrganismPermissionInfo.setId((long) organismPermissionJsonObject.get("id").isNumber().doubleValue());
+                        }
+                        groupOrganismPermissionInfo.setGroupId((long) organismPermissionJsonObject.get("groupId").isNumber().doubleValue());
+                        groupOrganismPermissionInfo.setOrganismName(organismPermissionJsonObject.get("organism").isString().stringValue());
+                        if (organismPermissionJsonObject.get("permissions") != null) {
+                            JSONArray permissionsArray = JSONParser.parseStrict(organismPermissionJsonObject.get("permissions").isString().stringValue()).isArray();
+                            for (int permissionIndex = 0; permissionIndex < permissionsArray.size(); ++permissionIndex) {
+                                String permission = permissionsArray.get(permissionIndex).isString().stringValue();
+                                switch (permission) {
+                                    case "ADMINISTRATE":
+                                        groupOrganismPermissionInfo.setAdmin(true);
+                                        break;
+                                    case "WRITE":
+                                        groupOrganismPermissionInfo.setWrite(true);
+                                        break;
+                                    case "EXPORT":
+                                        groupOrganismPermissionInfo.setExport(true);
+                                        break;
+                                    case "READ":
+                                        groupOrganismPermissionInfo.setRead(true);
+                                        break;
+
+                                    default:
+                                        Bootbox.alert("Unsure how to handle this permission '" + permission+"'");
+                                }
+                            }
+                        }
+
+
+                        organismPermissionMap.put(groupOrganismPermissionInfo.getOrganismName(), groupOrganismPermissionInfo);
+                    }
+                    groupInfo.setOrganismPermissionMap(organismPermissionMap);
+
+
+                    groupInfoList.add(groupInfo);
+                }
+                Annotator.eventBus.fireEvent(new GroupChangeEvent(GroupChangeEvent.Action.GROUPS_RELOADED));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("error retrieving groups");
+            }
+        };
+
+        RestService.sendRequest(requestCallback, "group/loadGroups/");
+    }
+
+    public static void updateGroup(final GroupInfo selectedGroupInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                Annotator.eventBus.fireEvent(new GroupChangeEvent(GroupChangeEvent.Action.RELOAD_GROUPS));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("error updating group " + selectedGroupInfo.getName() + " " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "group/updateGroup/", "data=" + selectedGroupInfo.toJSON().toString());
+    }
+
+    public static void deleteGroup(final GroupInfo selectedGroupInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                Annotator.eventBus.fireEvent(new GroupChangeEvent(GroupChangeEvent.Action.RELOAD_GROUPS));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("error updating group " + selectedGroupInfo.getName() + " " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "group/deleteGroup/", "data=" + selectedGroupInfo.toJSON().toString());
+    }
+
+    public static void addNewGroup(final GroupInfo selectedGroupInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                Annotator.eventBus.fireEvent(new GroupChangeEvent(GroupChangeEvent.Action.ADD_GROUP));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("error updating group " + selectedGroupInfo.getName() + " " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "group/createGroup/", "data=" + selectedGroupInfo.toJSON().toString());
+    }
+
+    public static void updateOrganismPermission(GroupOrganismPermissionInfo object) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                GWT.log("success");
+//                loadUsers(userInfoList);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating permissions: " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "group/updateOrganismPermission", "data=" + object.toJSON());
+    }
+
+    public static void updateUserGroups(RequestCallback requestCallback, GroupInfo selectedGroupInfo, List<Option> selectedValues) {
+//        RestService.sendRequest(requestCallback, "group/updateMembership", "data=" + object.toJSON());
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("groupId", new JSONNumber(selectedGroupInfo.getId()));
+        JSONArray userArray = new JSONArray();
+        for (Option userData : selectedValues) {
+            String emailValue = userData.getValue().split("\\(")[1].trim();
+            emailValue = emailValue.substring(0, emailValue.length() - 1);
+            userArray.set(userArray.size(), new JSONString(emailValue));
+        }
+        jsonObject.put("users", userArray);
+
+        RestService.sendRequest(requestCallback, "group/updateMembership", "data=" + jsonObject);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/OrganismRestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/OrganismRestService.java
new file mode 100644
index 0000000..d5c6ad8
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/OrganismRestService.java
@@ -0,0 +1,129 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.http.client.*;
+import com.google.gwt.json.client.*;
+import com.google.gwt.user.client.Window;
+import org.bbop.apollo.gwt.client.Annotator;
+import org.bbop.apollo.gwt.client.ErrorDialog;
+import org.bbop.apollo.gwt.client.LoadingDialog;
+import org.bbop.apollo.gwt.client.MainPanel;
+import org.bbop.apollo.gwt.client.dto.AppInfoConverter;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.OrganismInfoConverter;
+import org.bbop.apollo.gwt.client.event.OrganismChangeEvent;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ndunn on 1/14/15.
+ */
+public class OrganismRestService {
+
+    public static void loadOrganisms(RequestCallback requestCallback) {
+        RestService.sendRequest(requestCallback, "organism/findAllOrganisms");
+    }
+
+
+    public static void loadOrganisms(final List<OrganismInfo> organismInfoList) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                organismInfoList.clear();
+                organismInfoList.addAll(OrganismInfoConverter.convertJSONStringToOrganismInfoList(response.getText()));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error loading organisms");
+            }
+        };
+        loadOrganisms(requestCallback);
+    }
+
+    public static void updateOrganismInfo(final OrganismInfo organismInfo,boolean forceReload) {
+        final LoadingDialog loadingDialog = new LoadingDialog("Updating Organism Information");
+        JSONObject organismInfoObject = organismInfo.toJSON();
+        organismInfoObject.put("forceReload",JSONBoolean.getInstance(forceReload));
+
+
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                loadingDialog.hide();
+                JSONValue jsonValue = JSONParser.parseStrict(response.getText());
+                if(jsonValue.isObject()!=null && jsonValue.isObject()!=null && jsonValue.isObject().containsKey(FeatureStringEnum.ERROR.getValue())){
+                    String errorMessage = jsonValue.isObject().get(FeatureStringEnum.ERROR.getValue()).isString().stringValue();
+                    ErrorDialog errorDialog = new ErrorDialog("Unable to update the organism",errorMessage,true,true);
+                }
+                else{
+                    OrganismChangeEvent organismChangeEvent = new OrganismChangeEvent(OrganismChangeEvent.Action.LOADED_ORGANISMS);
+                    List<OrganismInfo> organismInfoList  = OrganismInfoConverter.convertJSONStringToOrganismInfoList(response.getText());
+                    organismChangeEvent.setOrganismInfoList(organismInfoList);
+                    Annotator.eventBus.fireEvent(organismChangeEvent);
+                }
+
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                loadingDialog.hide();
+                Bootbox.alert("error updating organism info: "+exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "organism/updateOrganismInfo", "data=" + organismInfoObject.toString());
+    }
+
+
+    public static void createOrganism(RequestCallback requestCallback, OrganismInfo organismInfo) {
+        RestService.sendRequest(requestCallback,"organism/addOrganism", OrganismInfoConverter.convertOrganismInfoToJSONObject(organismInfo));
+    }
+
+    public static void deleteOrganism(RequestCallback requestCallback, OrganismInfo organismInfo) {
+        RestService.sendRequest(requestCallback,"organism/deleteOrganism", OrganismInfoConverter.convertOrganismInfoToJSONObject(organismInfo));
+    }
+
+    public static void switchOrganismById(String newOrganismId) {
+        final LoadingDialog loadingDialog = new LoadingDialog();
+
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject returnValue = JSONParser.parseStrict(response.getText()).isObject();
+                MainPanel.getInstance().setAppState(AppInfoConverter.convertFromJson(returnValue));
+                loadingDialog.hide();
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                loadingDialog.hide();
+                Bootbox.alert("Error changing organisms");
+            }
+        };
+
+        RestService.sendRequest(requestCallback,"annotator/setCurrentOrganism/"+newOrganismId);
+    }
+
+    public static void switchSequenceById(String newSequenceId) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject returnValue = JSONParser.parseStrict(response.getText()).isObject();
+                MainPanel.getInstance().setAppState(AppInfoConverter.convertFromJson(returnValue));
+
+                OrganismChangeEvent organismChangeEvent = new OrganismChangeEvent(OrganismChangeEvent.Action.LOADED_ORGANISMS);
+                Annotator.eventBus.fireEvent(organismChangeEvent);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error changing organisms: "+exception.getMessage());
+            }
+        };
+
+        RestService.sendRequest(requestCallback,"annotator/setCurrentSequence/"+ newSequenceId);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/RestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/RestService.java
new file mode 100644
index 0000000..194dba7
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/RestService.java
@@ -0,0 +1,61 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.json.client.JSONObject;
+import org.bbop.apollo.gwt.client.Annotator;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 1/14/15.
+ */
+public class RestService {
+
+    public static void sendRequest(RequestCallback requestCallback, String url) {
+        sendRequest(requestCallback, url, (String) null);
+    }
+
+
+    public static void sendRequest(RequestCallback requestCallback, String url, JSONObject jsonObject) {
+        sendRequest(requestCallback, url, "data=" + jsonObject.toString());
+    }
+
+    public static void sendRequest(RequestCallback requestCallback, String url, String data) {
+        sendRequest(requestCallback, url, data, RequestBuilder.POST);
+    }
+
+    public static void sendRequest(RequestCallback requestCallback, String url, String data, RequestBuilder.Method method) {
+        String rootUrl = Annotator.getRootUrl();
+        if (!url.startsWith(rootUrl)) {
+            url = rootUrl + url;
+        }
+        // add the clientToken parameter if not exists
+        if (!url.contains(FeatureStringEnum.CLIENT_TOKEN.getValue())) {
+            url += url.contains("?") ? "&" : "?";
+            url += FeatureStringEnum.CLIENT_TOKEN.getValue();
+            url += "=";
+            url += Annotator.getClientToken();
+        }
+        RequestBuilder builder = new RequestBuilder(method, URL.encode(url));
+        if (data != null) {
+            builder.setRequestData(data);
+        }
+        builder.setHeader("Content-type", "application/x-www-form-urlencoded");
+        try {
+            if (requestCallback != null) {
+                builder.setCallback(requestCallback);
+            }
+            builder.send();
+        } catch (RequestException e) {
+            Bootbox.alert(e.getMessage());
+        }
+
+    }
+
+    public static void sendGetRequest(RequestCallback requestCallback, String url) {
+        sendRequest(requestCallback, url, null, RequestBuilder.GET);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/SequenceRestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/SequenceRestService.java
new file mode 100644
index 0000000..c621ebd
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/SequenceRestService.java
@@ -0,0 +1,146 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
+import org.bbop.apollo.gwt.client.Annotator;
+import org.bbop.apollo.gwt.client.ExportPanel;
+import org.bbop.apollo.gwt.client.SequencePanel;
+import org.bbop.apollo.gwt.client.dto.OrganismInfo;
+import org.bbop.apollo.gwt.client.dto.SequenceInfo;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+/**
+ * Created by ndunn on 1/14/15.
+ */
+public class SequenceRestService {
+
+    public static void setCurrentSequence(RequestCallback requestCallback, SequenceInfo sequenceInfo) {
+        RestService.sendRequest(requestCallback, "sequence/setCurrentSequence/" + sequenceInfo.getId());
+    }
+
+    public static void setCurrentSequenceForString(RequestCallback requestCallback, String sequenceName, OrganismInfo organismInfo) {
+        RestService.sendRequest(requestCallback, "sequence/setCurrentSequenceForNameAndOrganism/" +organismInfo.getId() +"?sequenceName="+sequenceName);
+    }
+
+    public static void generateLink(final ExportPanel exportPanel) {
+        JSONObject jsonObject = new JSONObject();
+        String type = exportPanel.getType();
+        jsonObject.put("type", new JSONString(exportPanel.getType()));
+        jsonObject.put("exportAllSequences", new JSONString(exportPanel.getExportAll().toString()));
+
+        if (type.equals(FeatureStringEnum.TYPE_CHADO.getValue())) {
+            jsonObject.put("exportAllSequences", new JSONString(exportPanel.getExportAllSequencesToChado().toString()));
+            jsonObject.put("chadoExportType", new JSONString(exportPanel.getChadoExportType()));
+            jsonObject.put("seqType", new JSONString(""));
+            jsonObject.put("exportGff3Fasta", new JSONString(""));
+            jsonObject.put("output", new JSONString(""));
+            jsonObject.put("format", new JSONString(""));
+        }
+        else {
+            jsonObject.put("chadoExportType", new JSONString(""));
+            jsonObject.put("seqType", new JSONString(exportPanel.getSequenceType()));
+            jsonObject.put("exportGff3Fasta", new JSONString(exportPanel.getExportGff3Fasta().toString()));
+            jsonObject.put("output", new JSONString("file"));
+            jsonObject.put("format", new JSONString("gzip"));
+        }
+
+        JSONArray jsonArray = new JSONArray();
+        for (SequenceInfo sequenceInfo : exportPanel.getSequenceList()) {
+            jsonArray.set(jsonArray.size(), new JSONString(sequenceInfo.getName()));
+        }
+        jsonObject.put("sequences", jsonArray);
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject responseObject = JSONParser.parseStrict(response.getText()).isObject();
+                GWT.log("Received response: "+responseObject.toString());
+                String uuid = responseObject.get("uuid").isString().stringValue();
+                String exportType = responseObject.get("exportType").isString().stringValue();
+                String sequenceType = responseObject.get("seqType").isString().stringValue();
+                String exportUrl = Annotator.getRootUrl() + "IOService/download?uuid=" + uuid + "&exportType=" + exportType + "&seqType=" + sequenceType+"&format=gzip";
+                exportPanel.setExportUrl(exportUrl);
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error: " + exception);
+            }
+        };
+
+        RequestCallback requestCallbackForChadoExport = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject responseObject = JSONParser.parseStrict(response.getText()).isObject();
+                exportPanel.showExportStatus(responseObject.toString());
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error: " + exception);
+            }
+        };
+
+        if (type.equals(FeatureStringEnum.TYPE_CHADO.getValue())) {
+            RestService.sendRequest(requestCallbackForChadoExport, "IOService/write", "data=" + jsonObject.toString());
+        }
+        else {
+            RestService.sendRequest(requestCallback, "IOService/write", "data=" + jsonObject.toString());
+        }
+    }
+
+    public static void setCurrentSequenceAndLocation(RequestCallback requestCallback, String sequenceNameString, Integer start, Integer end) {
+        setCurrentSequenceAndLocation(requestCallback,sequenceNameString,start,end,false) ;
+    }
+
+    public static void setCurrentSequenceAndLocation(RequestCallback requestCallback, String sequenceNameString, Integer start, Integer end,boolean suppressOutput) {
+        String url = "sequence/setCurrentSequenceLocation/?name=" + sequenceNameString + "&start=" + start + "&end=" + end;
+        if(suppressOutput){
+            url += "&suppressOutput=true";
+        }
+
+        RestService.sendRequest(requestCallback, url);
+    }
+
+    public static void getSequenceForOffsetAndMax(RequestCallback requestCallback, String text, int start, int length, String sortBy,Boolean sortNameAscending, String minFeatureLengthText, String maxFeatureLengthText) {
+        String searchString = "sequence/getSequences/?name=" + text + "&start=" + start + "&length=" + length ;
+        if(sortBy!=null && sortBy.length()>1){
+            searchString += "&sort="+sortBy+"&asc=" + sortNameAscending;
+        }
+        try {
+            searchString += "&minFeatureLength=" + Integer.parseInt(minFeatureLengthText);
+        } catch (NumberFormatException nfe) {
+            //
+        }
+        try {
+            searchString += "&maxFeatureLength=" + Integer.parseInt(maxFeatureLengthText);
+        } catch (NumberFormatException nfe) {
+            //
+        }
+        RestService.sendRequest(requestCallback, searchString);
+    }
+
+    public static void getChadoExportStatus(final SequencePanel sequencePanel) {
+        String requestUrl = Annotator.getRootUrl() + "IOService/chadoExportStatus";
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONObject responseObject = JSONParser.parseStrict(response.getText()).isObject();
+                sequencePanel.setChadoExportStatus(responseObject.get("export_status").isString().stringValue());
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                sequencePanel.setChadoExportStatus("false");
+            }
+        };
+        RestService.sendRequest(requestCallback, requestUrl);
+    }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/client/rest/UserRestService.java b/src/gwt/org/bbop/apollo/gwt/client/rest/UserRestService.java
new file mode 100644
index 0000000..68fd1b8
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/client/rest/UserRestService.java
@@ -0,0 +1,252 @@
+package org.bbop.apollo.gwt.client.rest;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.*;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.user.client.Window;
+import org.bbop.apollo.gwt.client.Annotator;
+import org.bbop.apollo.gwt.client.ErrorDialog;
+import org.bbop.apollo.gwt.client.LoginDialog;
+import org.bbop.apollo.gwt.client.dto.UserInfo;
+import org.bbop.apollo.gwt.client.dto.UserInfoConverter;
+import org.bbop.apollo.gwt.client.dto.UserOrganismPermissionInfo;
+import org.bbop.apollo.gwt.client.event.UserChangeEvent;
+import org.bbop.apollo.gwt.shared.FeatureStringEnum;
+import org.gwtbootstrap3.extras.bootbox.client.Bootbox;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Created by ndunn on 1/14/15.
+ */
+public class UserRestService {
+
+
+    public static void login(RequestCallback requestCallback, JSONObject data) {
+        RestService.sendRequest(requestCallback, "Login", data.toString());
+    }
+
+
+    public static void registerAdmin(RequestCallback requestCallback, JSONObject data) {
+        RestService.sendRequest(requestCallback, "login/registerAdmin", data);
+    }
+
+    public static void login(String username, String password, Boolean rememberMe, final LoginDialog loginDialog) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue j= null ;
+                try {
+                    j = JSONParser.parseStrict(response.getText());
+                } catch (Exception e) {
+                    GWT.log("Error parsing login response: "+e);
+//                    Window.alert("Error parsing login response, reloading");
+                    Window.Location.reload();
+                    return ;
+                }
+                JSONObject o=j.isObject();
+                if(o.get("error")!=null) {
+                    loginDialog.setError(o.get("error").isString().stringValue() + "!");
+                }
+                else {
+                    Window.Location.reload();
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error loading organisms");
+            }
+        };
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("operation", new JSONString("login"));
+        jsonObject.put("username", new JSONString(username));
+        jsonObject.put("password", new JSONString(password));
+        jsonObject.put("rememberMe", JSONBoolean.getInstance(rememberMe));
+        login(requestCallback, jsonObject);
+    }
+
+    public static void loadUsers(RequestCallback requestCallback) {
+        loadUsers(requestCallback,-1,-1,"","name",true);
+    }
+
+    public static void loadUsers(RequestCallback requestCallback, Integer start, Integer length, String searchNameString, String searchColumnString, Boolean sortAscending) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("start",new JSONNumber(start < 0 ? 0 : start));
+        jsonObject.put("length",new JSONNumber(length < 0 ? 20 : length));
+        jsonObject.put("name",new JSONString(searchNameString));
+        jsonObject.put("sortColumn",new JSONString(searchColumnString));
+        jsonObject.put("sortAscending",JSONBoolean.getInstance(sortAscending));
+        RestService.sendRequest(requestCallback, "user/loadUsers/",jsonObject);
+    }
+
+    public static void loadUsers(final List<UserInfo> userInfoList) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue returnValue = JSONParser.parseStrict(response.getText());
+                JSONArray array = returnValue.isArray();
+
+                userInfoList.clear();
+
+                for (int i = 0; array != null && i < array.size(); i++) {
+                    JSONObject object = array.get(i).isObject();
+                    UserInfo userInfo = UserInfoConverter.convertToUserInfoFromJSON(object);
+                    userInfoList.add(userInfo);
+                }
+                Annotator.eventBus.fireEvent(new UserChangeEvent(UserChangeEvent.Action.USERS_RELOADED));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error loading organisms");
+            }
+        };
+
+        loadUsers(requestCallback);
+    }
+    public static void logout() {
+        logout(null);
+    }
+
+    public static void logout(final String redirectUrl) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                if(redirectUrl!=null){
+                    Window.Location.replace(redirectUrl);
+                }
+                else{
+                    Window.Location.reload();
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error logging out " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "Login?operation=logout");
+    }
+
+    public static void updateUser(RequestCallback requestCallback, UserInfo selectedUserInfo) {
+        JSONObject jsonObject = selectedUserInfo.toJSON();
+        RestService.sendRequest(requestCallback, "user/updateUser", "data=" + jsonObject.toString());
+    }
+
+    public static void updateUserTrackPanelPreference(RequestCallback requestCallback, boolean tracklist) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("tracklist", JSONBoolean.getInstance(tracklist));
+        RestService.sendRequest(requestCallback, "user/updateTrackListPreference", "data=" + jsonObject.toString());
+    }
+
+
+    public static void deleteUser(final List<UserInfo> userInfoList, UserInfo selectedUserInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue v=JSONParser.parseStrict(response.getText());
+                JSONObject o=v.isObject();
+                if(o.containsKey(FeatureStringEnum.ERROR.getValue())) {
+                    new ErrorDialog("Error Deleting User",o.get(FeatureStringEnum.ERROR.getValue()).isString().stringValue(),true,true);
+                }
+                else{
+                    loadUsers(userInfoList);
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error deleting user: " + exception);
+            }
+        };
+        JSONObject jsonObject = selectedUserInfo.toJSON();
+        RestService.sendRequest(requestCallback, "user/deleteUser", "data=" + jsonObject.toString());
+    }
+
+    public static void createUser(final List<UserInfo> userInfoList, UserInfo selectedUserInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                JSONValue v=JSONParser.parseStrict(response.getText());
+                JSONObject o=v.isObject();
+                if(o.containsKey(FeatureStringEnum.ERROR.getValue())) {
+                    new ErrorDialog("Error Creating User",o.get(FeatureStringEnum.ERROR.getValue()).isString().stringValue(),true,true);
+                }
+                else {
+                    loadUsers(userInfoList);
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error adding user: " + exception);
+            }
+        };
+        JSONObject jsonObject = selectedUserInfo.toJSON();
+        RestService.sendRequest(requestCallback, "user/createUser", "data=" + jsonObject.toString());
+
+    }
+
+    public static void removeUserFromGroup(final String groupName, final List<UserInfo> userInfoList, final UserInfo selectedUserInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                List<UserInfo> userInfoList = new ArrayList<>();
+                userInfoList.add(selectedUserInfo);
+                Annotator.eventBus.fireEvent(new UserChangeEvent(userInfoList, UserChangeEvent.Action.REMOVE_USER_FROM_GROUP, groupName));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error removing group from user: " + exception);
+            }
+        };
+        JSONObject jsonObject = selectedUserInfo.toJSON();
+        jsonObject.put("group", new JSONString(groupName));
+        RestService.sendRequest(requestCallback, "user/removeUserFromGroup", "data=" + jsonObject.toString());
+    }
+
+    public static void addUserToGroup(final String groupName, final UserInfo selectedUserInfo) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                List<UserInfo> userInfoList = new ArrayList<>();
+                userInfoList.add(selectedUserInfo);
+                Annotator.eventBus.fireEvent(new UserChangeEvent(userInfoList, UserChangeEvent.Action.ADD_USER_TO_GROUP, groupName));
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error adding group to user: " + exception);
+            }
+        };
+        JSONObject jsonObject = selectedUserInfo.toJSON();
+        jsonObject.put("group", new JSONString(groupName));
+        RestService.sendRequest(requestCallback, "user/addUserToGroup", "data=" + jsonObject.toString());
+    }
+
+    public static void updateOrganismPermission(UserOrganismPermissionInfo object) {
+        RequestCallback requestCallback = new RequestCallback() {
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                GWT.log("success");
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                Bootbox.alert("Error updating permissions: " + exception);
+            }
+        };
+        RestService.sendRequest(requestCallback, "user/updateOrganismPermission", "data=" + object.toJSON().toString());
+    }
+
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/public/theme.css b/src/gwt/org/bbop/apollo/gwt/public/theme.css
new file mode 100644
index 0000000..d787a16
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/public/theme.css
@@ -0,0 +1,66 @@
+.form-control {
+    font-size: 12px !important;
+    height: 30px !important;
+}
+
+.input-group-addon {
+    font-size: 12px !important;
+}
+
+.btn {
+    font-size: 12px !important;
+}
+
+.snackbarRight {
+    position: fixed;
+    right: 10px;
+    top :48%;
+    z-index: 1000;
+    opacity: 80;
+    /*rotation: 90deg;*/
+    /*background-color: black;*/
+    /*color: yellowgreen;*/
+}
+
+.track-link {
+    width: 5px;
+    /*display: inline !important;*/
+    /*cursor: pointer;*/
+    /*font-size: small;*/
+    /*margin-left: 5px;*/
+    /*padding-left: 5px;*/
+}
+
+.track-link:hover {
+    /*display: inline !important;*/
+    color: blue;
+    text-decoration: underline;
+    cursor: pointer;
+}
+
+.track-slider {
+    /*display: inline ;*/
+    /*margin-right: 5px;*/
+    /*padding-right: 5px;*/
+}
+
+.track-entry {
+    padding-top: 1px !important;
+    padding-bottom: 5px !important;
+}
+.track-header {
+    display: inline;
+    cursor: pointer;
+    font-size: medium;
+}
+
+.text-left {
+    text-align: left !important;
+    background-color: white !important;
+}
+
+.panel-select {
+    background-color: #f5f5f5 !important; 
+    border: none !important;
+    padding: 0 0 0 5px !important;
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/shared/ClientTokenGenerator.java b/src/gwt/org/bbop/apollo/gwt/shared/ClientTokenGenerator.java
new file mode 100644
index 0000000..fe6b902
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/shared/ClientTokenGenerator.java
@@ -0,0 +1,30 @@
+package org.bbop.apollo.gwt.shared;
+
+import java.util.Random;
+
+/**
+ * Created by ndunn on 4/15/16.
+ */
+public class ClientTokenGenerator {
+
+
+    private final static Random random = new Random(); // or SecureRandom
+    public static final int DEFAULT_LENGTH = 20 ;
+    public static final int MIN_TOKEN_LENGTH = 10;
+
+    public static String generateRandomString() {
+        return generateRandomString(DEFAULT_LENGTH);
+    }
+
+    public static String generateRandomString(int length) {
+        StringBuilder stringBuilder = new StringBuilder();
+        while(stringBuilder.length()<length){
+            stringBuilder.append(Math.abs(random.nextInt()));
+        }
+        return stringBuilder.toString();
+    }
+
+    public static boolean isValidToken(String clientID) {
+        return (clientID!=null && clientID.length()>MIN_TOKEN_LENGTH);
+    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/shared/FeatureStringEnum.java b/src/gwt/org/bbop/apollo/gwt/shared/FeatureStringEnum.java
new file mode 100644
index 0000000..25e0125
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/shared/FeatureStringEnum.java
@@ -0,0 +1,166 @@
+package org.bbop.apollo.gwt.shared;
+
+/**
+ * Created by ndunn on 4/2/15.
+ */
+public enum FeatureStringEnum {
+        ID,
+        FEATURES,
+        SUPPRESS_HISTORY,
+        SUPPRESS_EVENTS,
+        FEATURE_PROPERTY,
+        ANNOTATION_COUNT,
+        PARENT,
+        PARENT_ID,
+        PARENT_NAME,
+        USERNAME,
+        EDITOR,
+        TYPE,
+        PARENT_TYPE,
+        PROPERTIES,
+        TIMEACCESSION,
+        DEFAULT,
+        TIMELASTMODIFIED,
+        RESIDUES,
+        CHILDREN,
+        CDS("CDS"),
+        EXON("Exon"),
+        GENE("Gene"),
+        PSEUDOGENE("Pseudogene"),
+        STOP_CODON_READTHROUGH("stop_codon_read_through"),
+        STOP_CODON_READHTHROUGH_SUFFIX("-stop_codon_read_through"),
+        READTHROUGH_STOP_CODON,
+        TRANSCRIPT("Transcript"),
+        NONCANONICALFIVEPRIMESPLICESITE("NonCanonicalFivePrimeSpliceSite"),
+        NONCANONICALTHREEPRIMESPLICESITE("NonCanonicalThreePrimeSpliceSite"),
+        DATE_LAST_MODIFIED,
+        DATE_CREATION,
+        DATE,
+        CURRENT,
+        CURRENT_TAB,
+        COMMENT,
+        OLD_COMMENTS,
+        NEW_COMMENTS,
+        TAG_VALUE_DELIMITER("="),
+        COMMENTS,
+        CANNED_COMMENTS,
+        CANNED_KEYS,
+        CANNED_VALUES,
+        STATUS,
+        AVAILABLE_STATUSES,
+        NOTES,
+        TAG,
+        NON_RESERVED_PROPERTIES,
+        OLD_NON_RESERVED_PROPERTIES,
+        NEW_NON_RESERVED_PROPERTIES,
+        LOCATION,
+        COUNT,
+        CONFIRM,
+        FMIN,
+        FMAX,
+        IS_FMIN_PARTIAL,
+        IS_FMAX_PARTIAL,
+        STRAND,
+        NAME,
+        GENE_NAME,
+        VALUE,
+        REMOTE_USER("REMOTE_USER"),
+        CV,
+        SEQUENCE,
+        TRACK,
+        DB,
+        DBXREFS,
+        CLIENT_TOKEN("clientToken"),
+        IGNORE,
+        PREFERENCE,
+        ACCESSION,
+        CDS_SUFFIX("-CDS"),
+        MINUS1FRAMESHIFT("Minus1Frameshift"),
+        MINUS2FRAMESHIFT("Minus2Frameshift"),
+        PLUS1FRAMESHIFT("Plus1Frameshift"),
+        PLUS2FRAMESHIFT("Plus2Frameshift"),
+        DELETION_PREFIX("Deletion-"),
+        INSERTION_PREFIX("Insertion-"),
+        OWNER("Owner"),
+        ORGANISM,
+        SYMBOL,
+        ALTERNATECVTERM("alternateCvTerm"),
+        DESCRIPTION,
+        ANNOTATION_INFO_EDITOR_CONFIGS,
+        HASDBXREFS("hasDbxrefs"),
+        HASATTRIBUTES("hasAttributes"),
+        HASPUBMEDIDS("hasPubmedIds"),
+        HASGOIDS("hasGoIds"),
+        HASCOMMENTS("hasComments"),
+        SUPPORTED_TYPES,
+        OLD_DBXREFS,
+        NEW_DBXREFS,
+        ATTRIBUTES,
+        PUBMEDIDS("pubmed_ids"),
+        GOIDS("go_ids"),
+        SYNONYMS,
+        HIGHLIGHTED_REGION("highlighted region"),
+        UNIQUENAME,
+        // TODO: move these to a SequenceTypeEnum
+        TYPE_PEPTIDE("peptide"),
+        TYPE_CDS("cds"),
+        TYPE_CDNA("cdna"),
+        TYPE_GENOMIC("genomic"),
+        TYPE_FASTA("FASTA"),
+        TYPE_GFF3("GFF3"),
+        TYPE_CHADO("CHADO"),
+        EXPORT_CHADO_CLEAN("chado_clean"),
+        EXPORT_CHADO_UPDATE("chado_update"),
+        EXPORT_ID("ID"),
+        EXPORT_DBXREF("Dbxref"),
+        EXPORT_NAME("Name"),
+        EXPORT_ALIAS("Alias"),
+        EXPORT_NOTE("Note"),
+        EXPORT_PARENT("Parent"),
+        ORGANISM_JBROWSE_DIRECTORY("organismJBrowseDirectory"),
+        ORGANISM_ID("organismId"),
+        ORGANISM_NAME("commonName"),
+        ORGANISM_DATA("organismData"),
+        SEQUENCE_NAME("sequenceName"),
+        DEFAULT_SEQUENCE_NAME("defaultSequenceName"),
+        PERMISSIONS,
+        ERROR,
+        ERROR_MESSAGE,
+        REQUEST_INDEX,
+        HAS_USERS,
+        USER_ID("userId"),
+        LOCKED,
+        HISTORY,
+        DOCK_OPEN("dockOpen"),
+        DOCK_WIDTH("dockWidth"),
+        USE_CDS,
+        USE_NAME,
+        TRACKS,
+        LABEL,
+        URL_TEMPLATE("urlTemplate"),
+        TRACK_DATA("trackData"),
+        TRACK_CONFIG("trackConfig"),
+        TRACK_LABEL("trackLabel")
+        ;
+
+
+        private String value;
+
+        FeatureStringEnum(String value) {
+            this.value = value;
+        }
+
+        FeatureStringEnum() {
+            this.value = name().toLowerCase();
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        public String getValue() {
+                return value;
+        }
+
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/shared/PermissionEnum.java b/src/gwt/org/bbop/apollo/gwt/shared/PermissionEnum.java
new file mode 100644
index 0000000..76cc2fc
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/shared/PermissionEnum.java
@@ -0,0 +1,72 @@
+package org.bbop.apollo.gwt.shared;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The "value" is mapping for Apollo 1
+ * Relateive ranks are for allowing bulk accesses.   For that WRITE access will have additional permissions (including export).
+ * We will likely be adding additional permissions, as well.
+ *
+ * Created by ndunn on 3/31/15.
+ */
+public enum PermissionEnum implements Comparable<PermissionEnum>{
+
+    NONE(0,0),
+    READ(1,10),
+    EXPORT(7,30),
+    WRITE(3,50),
+    ADMINISTRATE(15,70),
+    ALL_ORGANISM_ADMIN(100,100);
+
+    private Integer value ; // pertains to the 1.0 value
+    private Integer rank ;
+
+
+    PermissionEnum(int oldValue,int rank){
+        this.value = oldValue;
+        this.rank = rank ;
+    }
+
+    public String getDisplay(){
+        return name().toLowerCase();
+    }
+
+
+    public static PermissionEnum getValueForString(String input){
+        for(PermissionEnum permissionEnum : values()){
+            if(permissionEnum.name().equals(input))
+                return permissionEnum;
+        }
+        return null;
+    }
+
+    public static PermissionEnum getValueForOldInteger(Integer input){
+        for(PermissionEnum permissionEnum : values()){
+            if(permissionEnum.value.equals(input))
+                return permissionEnum;
+        }
+        return null;
+    }
+
+    public static List<PermissionEnum> getValueForArray(List<String> inputs){
+        List<PermissionEnum> permissionEnumList = new ArrayList<>();
+        for(String input : inputs){
+            permissionEnumList.add(getValueForString(input));
+        }
+        return permissionEnumList;
+    }
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public Integer getRank() {
+        return rank;
+    }
+
+    //    @Override
+//    public int compareTo(PermissionEnum o) {
+//        return value - o.getValue();
+//    }
+}
diff --git a/src/gwt/org/bbop/apollo/gwt/shared/track/NclistColumnEnum.java b/src/gwt/org/bbop/apollo/gwt/shared/track/NclistColumnEnum.java
new file mode 100644
index 0000000..c6965b7
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/shared/track/NclistColumnEnum.java
@@ -0,0 +1,37 @@
+package org.bbop.apollo.gwt.shared.track;
+
+/**
+ * Created by nathandunn on 12/3/15.
+ */
+public enum NclistColumnEnum {
+
+    START,
+    END,
+    STRAND,
+    SCORE,
+    TYPE,
+    SUBFEATURES,
+    SUBLIST,
+    SEQ_ID,
+    ID,
+    SOURCE,
+    NAME,
+    ALIAS,
+    CHUNK,
+    PHASE;
+
+    private String value;
+
+    NclistColumnEnum(String value) {
+        this.value = value;
+    }
+
+    NclistColumnEnum() {
+        this.value = name().substring(0, 1).toUpperCase() + name().substring(1).toLowerCase();
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+}
\ No newline at end of file
diff --git a/src/gwt/org/bbop/apollo/gwt/shared/track/TrackIndex.java b/src/gwt/org/bbop/apollo/gwt/shared/track/TrackIndex.java
new file mode 100644
index 0000000..e51dce2
--- /dev/null
+++ b/src/gwt/org/bbop/apollo/gwt/shared/track/TrackIndex.java
@@ -0,0 +1,194 @@
+package org.bbop.apollo.gwt.shared.track;
+
+/**
+ * Created by nathandunn on 12/2/15.
+ */
+public class TrackIndex {
+    // index locations . . .
+    private Integer start;
+    private Integer end;
+    private Integer source;
+    private Integer strand;
+    private Integer phase;
+    private Integer type;
+    private Integer score;
+    private Integer chunk;
+    private Integer id;
+    private Integer subFeaturesColumn;
+    private Integer name ;
+    private Integer alias;
+
+    private Integer seqId;
+    private Integer classIndex;
+
+
+    private Integer sublistColumn;// unclear if this has a column . . I think its just the last column . . or just implies "chunk"
+
+    // set from intake
+    private String trackName;
+    private String organism;
+
+
+    public void fixCoordinates() {
+        start = start == 0 ? null : start;
+        end = end == 0 ? null : end;
+        source = source == 0 ? null : source;
+        strand = strand == 0 ? null : strand;
+        phase = phase == 0 ? null : phase;
+        type = type == 0 ? null : type;
+        score = score == 0 ? null : score;
+        chunk = chunk == 0 ? null : chunk;
+        id = id == 0 ? null : id;
+        subFeaturesColumn = subFeaturesColumn == 0 ? null : subFeaturesColumn;
+        sublistColumn = sublistColumn == 0 ? null : sublistColumn;
+    }
+
+    public Boolean hasChunk() {
+        return chunk != null && chunk > 0;
+    }
+
+    public Boolean hasSubFeatures() {
+        return subFeaturesColumn != null && subFeaturesColumn > 0;
+    }
+
+    public Boolean hasSubList() {
+        return sublistColumn != null && sublistColumn > 0;
+    }
+
+    public Integer getStart() {
+        return start;
+    }
+
+    public void setStart(Integer start) {
+        this.start = start;
+    }
+
+    public Integer getEnd() {
+        return end;
+    }
+
+    public void setEnd(Integer end) {
+        this.end = end;
+    }
+
+    public Integer getSource() {
+        return source;
+    }
+
+    public void setSource(Integer source) {
+        this.source = source;
+    }
+
+    public Integer getStrand() {
+        return strand;
+    }
+
+    public void setStrand(Integer strand) {
+        this.strand = strand;
+    }
+
+    public Integer getPhase() {
+        return phase;
+    }
+
+    public void setPhase(Integer phase) {
+        this.phase = phase;
+    }
+
+    public Integer getType() {
+        return type;
+    }
+
+    public void setType(Integer type) {
+        this.type = type;
+    }
+
+    public Integer getScore() {
+        return score;
+    }
+
+    public void setScore(Integer score) {
+        this.score = score;
+    }
+
+    public Integer getChunk() {
+        return chunk;
+    }
+
+    public void setChunk(Integer chunk) {
+        this.chunk = chunk;
+    }
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public Integer getSubFeaturesColumn() {
+        return subFeaturesColumn;
+    }
+
+    public void setSubFeaturesColumn(Integer subFeaturesColumn) {
+        this.subFeaturesColumn = subFeaturesColumn;
+    }
+
+    public Integer getSublistColumn() {
+        return sublistColumn;
+    }
+
+    public void setSublistColumn(Integer sublistColumn) {
+        this.sublistColumn = sublistColumn;
+    }
+
+    public String getTrackName() {
+        return trackName;
+    }
+
+    public void setTrackName(String trackName) {
+        this.trackName = trackName;
+    }
+
+    public String getOrganism() {
+        return organism;
+    }
+
+    public void setOrganism(String organism) {
+        this.organism = organism;
+    }
+
+
+    public Integer getSeqId() {
+        return seqId;
+    }
+
+    public void setSeqId(Integer seqId) {
+        this.seqId = seqId;
+    }
+
+    public Integer getClassIndex() {
+        return classIndex;
+    }
+
+    public void setClassIndex(Integer classIndex) {
+        this.classIndex = classIndex;
+    }
+
+    public Integer getName() {
+        return name;
+    }
+
+    public void setName(Integer name) {
+        this.name = name;
+    }
+
+    public Integer getAlias() {
+        return alias;
+    }
+
+    public void setAlias(Integer alias) {
+        this.alias = alias;
+    }
+}
diff --git a/src/java/org/bbop/apollo/AnnotationException.groovy b/src/java/org/bbop/apollo/AnnotationException.groovy
new file mode 100644
index 0000000..21a82a8
--- /dev/null
+++ b/src/java/org/bbop/apollo/AnnotationException.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+/**
+ * Created by ndunn on 10/28/14.
+ */
+class AnnotationException extends Exception{
+    def AnnotationException(String gString) {
+        super(gString)
+    }
+}
diff --git a/src/java/org/bbop/apollo/ImprovedH2Dialect.java b/src/java/org/bbop/apollo/ImprovedH2Dialect.java
new file mode 100644
index 0000000..9d47b6c
--- /dev/null
+++ b/src/java/org/bbop/apollo/ImprovedH2Dialect.java
@@ -0,0 +1,19 @@
+package org.bbop.apollo;
+
+import org.hibernate.dialect.H2Dialect;
+
+public class ImprovedH2Dialect extends H2Dialect {
+    @Override
+    public String getDropSequenceString(String sequenceName) {
+        // Adding the "if exists" clause to avoid warnings
+        return "drop sequence if exists " + sequenceName;
+    }
+
+    @Override
+    public boolean dropConstraints() {
+        // We don't need to drop constraints before dropping tables, that just
+        // leads to error messages about missing tables when we don't have a
+        // schema in the database
+        return false;
+    }
+}
diff --git a/src/java/org/bbop/apollo/ImprovedPostgresDialect.java b/src/java/org/bbop/apollo/ImprovedPostgresDialect.java
new file mode 100644
index 0000000..a358586
--- /dev/null
+++ b/src/java/org/bbop/apollo/ImprovedPostgresDialect.java
@@ -0,0 +1,19 @@
+package org.bbop.apollo;
+
+import org.hibernate.dialect.PostgresPlusDialect;
+
+public class ImprovedPostgresDialect extends PostgresPlusDialect {
+    @Override
+    public String getDropSequenceString(String sequenceName) {
+        // Adding the "if exists" clause to avoid warnings
+        return "drop sequence if exists " + sequenceName;
+    }
+
+    @Override
+    public boolean dropConstraints() {
+        // We don't need to drop constraints before dropping tables, that just
+        // leads to error messages about missing tables when we don't have a
+        // schema in the database
+        return false;
+    }
+}
diff --git a/src/java/org/bbop/apollo/Pair.java b/src/java/org/bbop/apollo/Pair.java
new file mode 100644
index 0000000..55d8c13
--- /dev/null
+++ b/src/java/org/bbop/apollo/Pair.java
@@ -0,0 +1,29 @@
+package org.bbop.apollo;
+
+public class Pair<T, U> {
+
+    private T first;
+    private U second;
+    
+    public Pair(T first, U second) {
+        this.first = first;
+        this.second = second;
+    }
+    
+    public Pair(Pair<T, U> pair) {
+        this.first = pair.first;
+        this.second = pair.second;
+    }
+    
+    public T getFirst() {
+        return first;
+    }
+    
+    public U getSecond() {
+        return second;
+    }
+    
+    public String toString() {
+        return "[" + first.toString() + ", " + second.toString() + "]";
+    }
+}
diff --git a/src/java/org/bbop/apollo/PermissionException.groovy b/src/java/org/bbop/apollo/PermissionException.groovy
new file mode 100644
index 0000000..1a1d0ea
--- /dev/null
+++ b/src/java/org/bbop/apollo/PermissionException.groovy
@@ -0,0 +1,10 @@
+package org.bbop.apollo
+
+/**
+ * Created by ndunn on 10/28/14.
+ */
+class PermissionException extends Exception{
+    def PermissionException(String gString) {
+        super(gString)
+    }
+}
diff --git a/src/templates/war/web.xml b/src/templates/war/web.xml
new file mode 100644
index 0000000..612a408
--- /dev/null
+++ b/src/templates/war/web.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app version="3.0"
+         metadata-complete="true"
+         xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+
+    <display-name>/@grails.project.key@</display-name>
+
+    <context-param>
+        <param-name>contextConfigLocation</param-name>
+        <param-value>/WEB-INF/applicationContext.xml</param-value>
+    </context-param>
+
+    <context-param>
+        <param-name>webAppRootKey</param-name>
+        <param-value>@grails.project.key@</param-value>
+    </context-param>
+
+    <!-- configuration to increase buffer size for requests sent to Tomcat -->
+    <context-param>
+        <param-name>org.apache.tomcat.websocket.textBufferSize</param-name>
+        <param-value>10000000</param-value>
+    </context-param>
+    <context-param>
+        <param-name>org.apache.tomcat.websocket.binaryBufferSize</param-name>
+        <param-value>10000000</param-value>
+    </context-param>
+
+    <filter>
+        <filter-name>charEncodingFilter</filter-name>
+        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+        <init-param>
+            <param-name>targetBeanName</param-name>
+            <param-value>characterEncodingFilter</param-value>
+        </init-param>
+        <init-param>
+            <param-name>targetFilterLifecycle</param-name>
+            <param-value>true</param-value>
+        </init-param>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>charEncodingFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <listener>
+        <listener-class>org.codehaus.groovy.grails.web.context.GrailsContextLoaderListener</listener-class>
+    </listener>
+
+    <!-- Grails dispatcher servlet -->
+    <servlet>
+        <servlet-name>grails</servlet-name>
+        <servlet-class>org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet</servlet-class>
+        <init-param>
+            <param-name>dispatchOptionsRequest</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+        <async-supported>true</async-supported>
+    </servlet>
+
+    <!-- The Groovy Server Pages servlet -->
+    <servlet>
+        <servlet-name>gsp</servlet-name>
+        <servlet-class>org.codehaus.groovy.grails.web.pages.GroovyPagesServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>gsp</servlet-name>
+        <url-pattern>*.gsp</url-pattern>
+    </servlet-mapping>
+
+    <session-config>
+        <!-- 2 days -->
+        <!--<session-timeout>2880</session-timeout>-->
+        <!-- 16 hours -->
+        <session-timeout>960</session-timeout>
+    </session-config>
+
+    <welcome-file-list>
+        <!--
+        The order of the welcome pages is important.  JBoss deployment will
+        break if index.gsp is first in the list.
+        -->
+        <welcome-file>index.html</welcome-file>
+        <welcome-file>index.jsp</welcome-file>
+        <welcome-file>index.gsp</welcome-file>
+    </welcome-file-list>
+
+</web-app>
diff --git a/test/client/data/MAKER/Group1.33_Amel_4.5.maker.gff b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.gff
new file mode 100755
index 0000000..a87ba6e
--- /dev/null
+++ b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.gff
@@ -0,0 +1,12 @@
+Group1.33	maker	gene	245454	247006	.	+	.	ID=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;Name=maker-Group1%252E33-pred_gff_GNOMON-gene-4.137;
+Group1.33	maker	mRNA	245454	247006	.	+	.	ID=1:gnomon_566853_mRNA;Parent=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;Name=gnomon_566853_mRNA;_AED=0.45;_eAED=0.45;_QI=138|1|1|1|1|1|4|191|259;
+Group1.33	maker	exon	245454	245533	.	+	.	ID=1:gnomon_566853_mRNA:exon:5976;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	exon	245702	245879	.	+	.	ID=1:gnomon_566853_mRNA:exon:5977;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	exon	246046	246278	.	+	.	ID=1:gnomon_566853_mRNA:exon:5978;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	exon	246389	247006	.	+	.	ID=1:gnomon_566853_mRNA:exon:5979;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	five_prime_UTR	245454	245533	.	+	.	ID=1:gnomon_566853_mRNA:five_prime_utr;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	five_prime_UTR	245702	245759	.	+	.	ID=1:gnomon_566853_mRNA:five_prime_utr;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	CDS	245760	245879	.	+	0	ID=1:gnomon_566853_mRNA:cds;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	CDS	246046	246278	.	+	0	ID=1:gnomon_566853_mRNA:cds;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	CDS	246389	246815	.	+	1	ID=1:gnomon_566853_mRNA:cds;Parent=1:gnomon_566853_mRNA;
+Group1.33	maker	three_prime_UTR	246816	247006	.	+	.	ID=1:gnomon_566853_mRNA:three_prime_utr;Parent=1:gnomon_566853_mRNA;
diff --git a/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected.json b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected.json
new file mode 100755
index 0000000..8fb1502
--- /dev/null
+++ b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected.json
@@ -0,0 +1,44 @@
+{
+  "histograms" : {
+    "stats" : [ {
+      "basesPerBin" : "1000000",
+      "max" : 1,
+      "mean" : 1
+    } ],
+    "meta" : [ {
+      "basesPerBin" : "1000000",
+      "arrayParams" : {
+        "length" : 1,
+        "chunkSize" : 10000,
+        "urlTemplate" : "hist-1000000-{Chunk}.json"
+      }
+    } ]
+  },
+  "featureCount" : 1,
+  "intervals" : {
+    "nclist" : [
+    	     [0,245453,247006,1,"maker",null,"mRNA",null,"1:gnomon_566853_mRNA","gnomon_566853_mRNA",[[1,245453,245533,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5976",null,[]],[1,245701,245879,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5977",null,[]],[1,246045,246278,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5978",null,[]],[1,246388,247006,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5979",null,[]],[1,245453,245533,1,"maker",null,"five_prime_UTR", [...]
+    ],
+    "classes" : [ {
+      "isArrayAttr" : {
+        "Subfeatures" : 1
+      },
+      "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+    }, {
+      "isArrayAttr" : {
+      },
+      "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+    }, {
+      "isArrayAttr" : {
+        "Sublist" : 1
+      },
+      "attributes" : [ "Start", "End", "Chunk" ]
+    } ],
+    "maxEnd" : 247006,
+    "count" : 1,
+    "lazyClass" : 2,
+    "urlTemplate" : "lf-{Chunk}.json",
+    "minStart" : 245453
+  },
+  "formatVersion" : 1
+}
diff --git a/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected_PRETTIFIED.json b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected_PRETTIFIED.json
new file mode 100644
index 0000000..d94e806
--- /dev/null
+++ b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.jbrowse.expected_PRETTIFIED.json
@@ -0,0 +1,43 @@
+{
+   "histograms":{
+      "stats":[{
+	"basesPerBin":"1000000",
+	"max":1,
+	"mean":1
+	}],
+      "meta":[{
+        "basesPerBin":"1000000",
+	"arrayParams":{
+		"length":1,
+		"chunkSize":10000,
+		"urlTemplate":"hist-1000000-{Chunk}.json"
+	}
+	}]
+   },
+   "featureCount":1,
+   "intervals":{
+        "nclist":[
+		[0,245453,247006,1,"maker",null,"mRNA",null,"1:gnomon_566853_mRNA","gnomon_566853_mRNA",[[1,245453,245533,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5976",null,[]],[1,245701,245879,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5977",null,[]],[1,246045,246278,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5978",null,[]],[1,246388,247006,1,"maker",null,"exon",null,"1:gnomon_566853_mRNA:exon:5979",null,[]],[1,245453,245533,1,"maker",null,"five_prime_UTR",null,"1: [...]
+        "classes":[{
+		"isArrayAttr":{
+			"Subfeatures":1
+		},
+		"attributes":["Start","End","Strand","Source","Phase","Type","Score","Id","Name","Subfeatures"]
+		},{
+		"isArrayAttr":{
+		},
+		"attributes":["Start","End","Strand","Source","Phase","Type","Score","Id","Name","Subfeatures"]
+		},{
+		"isArrayAttr":{
+		   "Sublist":1
+		},
+		"attributes":["Start","End","Chunk"]
+   }],
+   "maxEnd":247006,
+   "count":1,
+   "lazyClass":2,
+   "urlTemplate":"lf-{Chunk}.json",
+   "minStart":245453
+   },
+   "formatVersion":1
+}
\ No newline at end of file
diff --git a/test/client/data/MAKER/Group1.33_Amel_4.5.maker.webapollo.expected.json b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.webapollo.expected.json
new file mode 100755
index 0000000..2cdbd7b
--- /dev/null
+++ b/test/client/data/MAKER/Group1.33_Amel_4.5.maker.webapollo.expected.json
@@ -0,0 +1,45 @@
+{
+  "histograms" : {
+    "stats" : [ {
+      "basesPerBin" : "1000000",
+      "max" : 1,
+      "mean" : 1
+    } ],
+    "meta" : [ {
+      "basesPerBin" : "1000000",
+      "arrayParams" : {
+        "length" : 1,
+        "chunkSize" : 10000,
+        "urlTemplate" : "hist-1000000-{Chunk}.json"
+      }
+    } ]
+  },
+  "featureCount" : 1,
+  "intervals" : {
+    "nclist" : [
+    	      [ 0, 245453, 247006, 1, "maker", null, "mRNA", null, "1:gnomon_566853_mRNA", "gnomon_566853_mRNA", [ [ 1, 245453, 245533, 1, null, null, "exon", null, null, null, null ], [ 1, 245701, 245879, 1, null, null, "exon", null, null, null, null ], [ 1, 246045, 246278, 1, null, null, "exon", null, null, null, null ], [ 1, 246388, 247006, 1, null, null, "exon", null, null, null, null ], [ 1, 245759, 246815, 1, null, null, "wholeCDS", null, null, null, null ] ] ]
+
+    ],
+    "classes" : [ {
+      "isArrayAttr" : {
+        "Subfeatures" : 1
+      },
+      "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+    }, {
+      "isArrayAttr" : {
+      },
+      "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+    }, {
+      "isArrayAttr" : {
+        "Sublist" : 1
+      },
+      "attributes" : [ "Start", "End", "Chunk" ]
+    } ],
+    "maxEnd" : 247006,
+    "count" : 1,
+    "lazyClass" : 2,
+    "urlTemplate" : "lf-{Chunk}.json",
+    "minStart" : 245453
+  },
+  "formatVersion" : 1
+}
diff --git a/test/client/data/heterozygous_snv.vcf b/test/client/data/heterozygous_snv.vcf
new file mode 100644
index 0000000..191ba63
--- /dev/null
+++ b/test/client/data/heterozygous_snv.vcf
@@ -0,0 +1,9 @@
+##fileformat=VCFv4.0
+##INFO=<ID=DP,Number=1,Type=Integer,Description="Total Depth">
+##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
+##FORMAT=<ID=DP,Number=1,Type=Integer,Description="Read Depth">
+##FORMAT=<ID=VRD,Number=2,Type=Integer,Description="Variant read depth">
+##FILTER=<ID=q10,Description="Quality below 10">
+#CHROM	POS	ID	REF	ALT	QUAL	FILTER	INFO	FORMAT	SAMPLE1	SAMPLE2
+chr1	15883	chr1:SOAP:SNV:15883	C	G,C	.	PASS	DP=100	GT:DP:VRD	0/1:33:17,16	0|0:67:40
+chr3	44444	chr3:SOAP:SNV:44444	A	T	.	PASS	DP=50	GT:DP:VRD	1:24:24	0:26:26
diff --git a/test/client/js_tests/index.html b/test/client/js_tests/index.html
new file mode 100644
index 0000000..669fa6e
--- /dev/null
+++ b/test/client/js_tests/index.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+  "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+  <title>Jasmine Spec Runner</title>
+
+  <!-- jasmine, dojo, etc... -->
+  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.2.0/jasmine_favicon.png">
+  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.2.0/jasmine.css">
+  <script type="text/javascript" src="lib/jasmine-1.2.0/jasmine.js"></script>
+  <script type="text/javascript" src="lib/jasmine-1.2.0/jasmine-html.js"></script>
+  <script type="text/javascript" src="../../../../src/dojo/dojo.js" data-dojo-config="isDebug: 0, async: 1" ></script>
+
+  <!-- source files -->
+  <script type="text/javascript" src="../../js/JSONUtils.js"></script> 
+  <script type="text/javascript" src="../../js/GFF3toJbrowseJson.js"></script> 
+
+  <!-- include spec files here... -->
+  <script type="text/javascript" src="spec/GFF3toJbrowseJsonSpec.js"></script>
+  <script type="text/javascript" src="spec/JSONUtilsSpec.js"></script>
+
+  <script type="text/javascript">
+    (function() {
+      var jasmineEnv = jasmine.getEnv();
+      jasmineEnv.updateInterval = 1000;
+
+      var htmlReporter = new jasmine.HtmlReporter();
+
+      jasmineEnv.addReporter(htmlReporter);
+
+      jasmineEnv.specFilter = function(spec) {
+        return htmlReporter.specFilter(spec);
+      };
+
+      var currentWindowOnload = window.onload;
+
+      window.onload = function() {
+        if (currentWindowOnload) {
+          currentWindowOnload();
+        }
+        execJasmine();
+      };
+
+      function execJasmine() {
+        jasmineEnv.execute();
+      }
+
+    })();
+  </script>
+
+</head>
+
+<body>
+</body>
+</html>
diff --git a/test/client/js_tests/lib/jasmine-1.2.0/MIT.LICENSE b/test/client/js_tests/lib/jasmine-1.2.0/MIT.LICENSE
new file mode 100644
index 0000000..7c435ba
--- /dev/null
+++ b/test/client/js_tests/lib/jasmine-1.2.0/MIT.LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008-2011 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/test/client/js_tests/lib/jasmine-1.2.0/jasmine-html.js b/test/client/js_tests/lib/jasmine-1.2.0/jasmine-html.js
new file mode 100644
index 0000000..a0b0639
--- /dev/null
+++ b/test/client/js_tests/lib/jasmine-1.2.0/jasmine-html.js
@@ -0,0 +1,616 @@
+jasmine.HtmlReporterHelpers = {};
+
+jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) {
+        el.appendChild(child);
+      }
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
+  var results = child.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+
+  return status;
+};
+
+jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
+  var parentDiv = this.dom.summary;
+  var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
+  var parent = child[parentSuite];
+
+  if (parent) {
+    if (typeof this.views.suites[parent.id] == 'undefined') {
+      this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
+    }
+    parentDiv = this.views.suites[parent.id].element;
+  }
+
+  parentDiv.appendChild(childElement);
+};
+
+
+jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
+  for(var fn in jasmine.HtmlReporterHelpers) {
+    ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
+  }
+};
+
+jasmine.HtmlReporter = function(_doc) {
+  var self = this;
+  var doc = _doc || window.document;
+
+  var reporterView;
+
+  var dom = {};
+
+  // Jasmine Reporter Public Interface
+  self.logRunningSpecs = false;
+
+  self.reportRunnerStarting = function(runner) {
+    var specs = runner.specs() || [];
+
+    if (specs.length == 0) {
+      return;
+    }
+
+    createReporterDom(runner.env.versionString());
+    doc.body.appendChild(dom.reporter);
+
+    reporterView = new jasmine.HtmlReporter.ReporterView(dom);
+    reporterView.addSpecs(specs, self.specFilter);
+  };
+
+  self.reportRunnerResults = function(runner) {
+    reporterView && reporterView.complete();
+  };
+
+  self.reportSuiteResults = function(suite) {
+    reporterView.suiteComplete(suite);
+  };
+
+  self.reportSpecStarting = function(spec) {
+    if (self.logRunningSpecs) {
+      self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+    }
+  };
+
+  self.reportSpecResults = function(spec) {
+    reporterView.specComplete(spec);
+  };
+
+  self.log = function() {
+    var console = jasmine.getGlobal().console;
+    if (console && console.log) {
+      if (console.log.apply) {
+        console.log.apply(console, arguments);
+      } else {
+        console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
+      }
+    }
+  };
+
+  self.specFilter = function(spec) {
+    if (!focusedSpecName()) {
+      return true;
+    }
+
+    return spec.getFullName().indexOf(focusedSpecName()) === 0;
+  };
+
+  return self;
+
+  function focusedSpecName() {
+    var specName;
+
+    (function memoizeFocusedSpec() {
+      if (specName) {
+        return;
+      }
+
+      var paramMap = [];
+      var params = doc.location.search.substring(1).split('&');
+
+      for (var i = 0; i < params.length; i++) {
+        var p = params[i].split('=');
+        paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+      }
+
+      specName = paramMap.spec;
+    })();
+
+    return specName;
+  }
+
+  function createReporterDom(version) {
+    dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
+      dom.banner = self.createDom('div', { className: 'banner' },
+        self.createDom('span', { className: 'title' }, "Jasmine "),
+        self.createDom('span', { className: 'version' }, version)),
+
+      dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
+      dom.alert = self.createDom('div', {className: 'alert'}),
+      dom.results = self.createDom('div', {className: 'results'},
+        dom.summary = self.createDom('div', { className: 'summary' }),
+        dom.details = self.createDom('div', { id: 'details' }))
+    );
+  }
+};
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);jasmine.HtmlReporter.ReporterView = function(dom) {
+  this.startedAt = new Date();
+  this.runningSpecCount = 0;
+  this.completeSpecCount = 0;
+  this.passedCount = 0;
+  this.failedCount = 0;
+  this.skippedCount = 0;
+
+  this.createResultsMenu = function() {
+    this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
+      this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
+      ' | ',
+      this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
+
+    this.summaryMenuItem.onclick = function() {
+      dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
+    };
+
+    this.detailsMenuItem.onclick = function() {
+      showDetails();
+    };
+  };
+
+  this.addSpecs = function(specs, specFilter) {
+    this.totalSpecCount = specs.length;
+
+    this.views = {
+      specs: {},
+      suites: {}
+    };
+
+    for (var i = 0; i < specs.length; i++) {
+      var spec = specs[i];
+      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
+      if (specFilter(spec)) {
+        this.runningSpecCount++;
+      }
+    }
+  };
+
+  this.specComplete = function(spec) {
+    this.completeSpecCount++;
+
+    if (isUndefined(this.views.specs[spec.id])) {
+      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
+    }
+
+    var specView = this.views.specs[spec.id];
+
+    switch (specView.status()) {
+      case 'passed':
+        this.passedCount++;
+        break;
+
+      case 'failed':
+        this.failedCount++;
+        break;
+
+      case 'skipped':
+        this.skippedCount++;
+        break;
+    }
+
+    specView.refresh();
+    this.refresh();
+  };
+
+  this.suiteComplete = function(suite) {
+    var suiteView = this.views.suites[suite.id];
+    if (isUndefined(suiteView)) {
+      return;
+    }
+    suiteView.refresh();
+  };
+
+  this.refresh = function() {
+
+    if (isUndefined(this.resultsMenu)) {
+      this.createResultsMenu();
+    }
+
+    // currently running UI
+    if (isUndefined(this.runningAlert)) {
+      this.runningAlert = this.createDom('a', {href: "?", className: "runningAlert bar"});
+      dom.alert.appendChild(this.runningAlert);
+    }
+    this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
+
+    // skipped specs UI
+    if (isUndefined(this.skippedAlert)) {
+      this.skippedAlert = this.createDom('a', {href: "?", className: "skippedAlert bar"});
+    }
+
+    this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
+
+    if (this.skippedCount === 1 && isDefined(dom.alert)) {
+      dom.alert.appendChild(this.skippedAlert);
+    }
+
+    // passing specs UI
+    if (isUndefined(this.passedAlert)) {
+      this.passedAlert = this.createDom('span', {href: "?", className: "passingAlert bar"});
+    }
+    this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
+
+    // failing specs UI
+    if (isUndefined(this.failedAlert)) {
+      this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
+    }
+    this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
+
+    if (this.failedCount === 1 && isDefined(dom.alert)) {
+      dom.alert.appendChild(this.failedAlert);
+      dom.alert.appendChild(this.resultsMenu);
+    }
+
+    // summary info
+    this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
+    this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
+  };
+
+  this.complete = function() {
+    dom.alert.removeChild(this.runningAlert);
+
+    this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
+
+    if (this.failedCount === 0) {
+      dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
+    } else {
+      showDetails();
+    }
+
+    dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
+  };
+
+  return this;
+
+  function showDetails() {
+    if (dom.reporter.className.search(/showDetails/) === -1) {
+      dom.reporter.className += " showDetails";
+    }
+  }
+
+  function isUndefined(obj) {
+    return typeof obj === 'undefined';
+  }
+
+  function isDefined(obj) {
+    return !isUndefined(obj);
+  }
+
+  function specPluralizedFor(count) {
+    var str = count + " spec";
+    if (count > 1) {
+      str += "s"
+    }
+    return str;
+  }
+
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
+
+
+jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
+  this.spec = spec;
+  this.dom = dom;
+  this.views = views;
+
+  this.symbol = this.createDom('li', { className: 'pending' });
+  this.dom.symbolSummary.appendChild(this.symbol);
+
+  this.summary = this.createDom('div', { className: 'specSummary' },
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
+        title: this.spec.getFullName()
+      }, this.spec.description)
+  );
+
+  this.detail = this.createDom('div', { className: 'specDetail' },
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
+        title: this.spec.getFullName()
+      }, this.spec.getFullName())
+  );
+};
+
+jasmine.HtmlReporter.SpecView.prototype.status = function() {
+  return this.getSpecStatus(this.spec);
+};
+
+jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
+  this.symbol.className = this.status();
+
+  switch (this.status()) {
+    case 'skipped':
+      break;
+
+    case 'passed':
+      this.appendSummaryToSuiteDiv();
+      break;
+
+    case 'failed':
+      this.appendSummaryToSuiteDiv();
+      this.appendFailureDetail();
+      break;
+  }
+};
+
+jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
+  this.summary.className += ' ' + this.status();
+  this.appendToSummary(this.spec, this.summary);
+};
+
+jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
+  this.detail.className += ' ' + this.status();
+
+  var resultItems = this.spec.results().getItems();
+  var messagesDiv = this.createDom('div', { className: 'messages' });
+
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      if (result.trace.stack) {
+        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    this.detail.appendChild(messagesDiv);
+    this.dom.details.appendChild(this.detail);
+  }
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
+  this.suite = suite;
+  this.dom = dom;
+  this.views = views;
+
+  this.element = this.createDom('div', { className: 'suite' },
+      this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(this.suite.getFullName()) }, this.suite.description)
+  );
+
+  this.appendToSummary(this.suite, this.element);
+};
+
+jasmine.HtmlReporter.SuiteView.prototype.status = function() {
+  return this.getSpecStatus(this.suite);
+};
+
+jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
+  this.element.className += " " + this.status();
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
+
+/* @deprecated Use jasmine.HtmlReporter instead
+ */
+jasmine.TrivialReporter = function(doc) {
+  this.document = doc || document;
+  this.suiteDivs = {};
+  this.logRunningSpecs = false;
+};
+
+jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) { el.appendChild(child); }
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
+  var showPassed, showSkipped;
+
+  this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
+      this.createDom('div', { className: 'banner' },
+        this.createDom('div', { className: 'logo' },
+            this.createDom('span', { className: 'title' }, "Jasmine"),
+            this.createDom('span', { className: 'version' }, runner.env.versionString())),
+        this.createDom('div', { className: 'options' },
+            "Show ",
+            showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
+            showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
+            )
+          ),
+
+      this.runnerDiv = this.createDom('div', { className: 'runner running' },
+          this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
+          this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
+          this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
+      );
+
+  this.document.body.appendChild(this.outerDiv);
+
+  var suites = runner.suites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    var suiteDiv = this.createDom('div', { className: 'suite' },
+        this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
+        this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
+    this.suiteDivs[suite.id] = suiteDiv;
+    var parentDiv = this.outerDiv;
+    if (suite.parentSuite) {
+      parentDiv = this.suiteDivs[suite.parentSuite.id];
+    }
+    parentDiv.appendChild(suiteDiv);
+  }
+
+  this.startedAt = new Date();
+
+  var self = this;
+  showPassed.onclick = function(evt) {
+    if (showPassed.checked) {
+      self.outerDiv.className += ' show-passed';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
+    }
+  };
+
+  showSkipped.onclick = function(evt) {
+    if (showSkipped.checked) {
+      self.outerDiv.className += ' show-skipped';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
+    }
+  };
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
+  var results = runner.results();
+  var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
+  this.runnerDiv.setAttribute("class", className);
+  //do it twice for IE
+  this.runnerDiv.setAttribute("className", className);
+  var specs = runner.specs();
+  var specCount = 0;
+  for (var i = 0; i < specs.length; i++) {
+    if (this.specFilter(specs[i])) {
+      specCount++;
+    }
+  }
+  var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
+  message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
+  this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
+
+  this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
+};
+
+jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
+  var results = suite.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.totalCount === 0) { // todo: change this to check results.skipped
+    status = 'skipped';
+  }
+  this.suiteDivs[suite.id].className += " " + status;
+};
+
+jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
+  if (this.logRunningSpecs) {
+    this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+  }
+};
+
+jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
+  var results = spec.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+  var specDiv = this.createDom('div', { className: 'spec '  + status },
+      this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(spec.getFullName()),
+        title: spec.getFullName()
+      }, spec.description));
+
+
+  var resultItems = results.getItems();
+  var messagesDiv = this.createDom('div', { className: 'messages' });
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      if (result.trace.stack) {
+        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    specDiv.appendChild(messagesDiv);
+  }
+
+  this.suiteDivs[spec.suite.id].appendChild(specDiv);
+};
+
+jasmine.TrivialReporter.prototype.log = function() {
+  var console = jasmine.getGlobal().console;
+  if (console && console.log) {
+    if (console.log.apply) {
+      console.log.apply(console, arguments);
+    } else {
+      console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
+    }
+  }
+};
+
+jasmine.TrivialReporter.prototype.getLocation = function() {
+  return this.document.location;
+};
+
+jasmine.TrivialReporter.prototype.specFilter = function(spec) {
+  var paramMap = {};
+  var params = this.getLocation().search.substring(1).split('&');
+  for (var i = 0; i < params.length; i++) {
+    var p = params[i].split('=');
+    paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+  }
+
+  if (!paramMap.spec) {
+    return true;
+  }
+  return spec.getFullName().indexOf(paramMap.spec) === 0;
+};
diff --git a/test/client/js_tests/lib/jasmine-1.2.0/jasmine.css b/test/client/js_tests/lib/jasmine-1.2.0/jasmine.css
new file mode 100644
index 0000000..826e575
--- /dev/null
+++ b/test/client/js_tests/lib/jasmine-1.2.0/jasmine.css
@@ -0,0 +1,81 @@
+body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
+
+#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
+#HTMLReporter a { text-decoration: none; }
+#HTMLReporter a:hover { text-decoration: underline; }
+#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
+#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
+#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
+#HTMLReporter .version { color: #aaaaaa; }
+#HTMLReporter .banner { margin-top: 14px; }
+#HTMLReporter .duration { color: #aaaaaa; float: right; }
+#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
+#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
+#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
+#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
+#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
+#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
+#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
+#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
+#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
+#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
+#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
+#HTMLReporter .runningAlert { background-color: #666666; }
+#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
+#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
+#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
+#HTMLReporter .passingAlert { background-color: #a6b779; }
+#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
+#HTMLReporter .failingAlert { background-color: #cf867e; }
+#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
+#HTMLReporter .results { margin-top: 14px; }
+#HTMLReporter #details { display: none; }
+#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
+#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
+#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
+#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
+#HTMLReporter.showDetails .summary { display: none; }
+#HTMLReporter.showDetails #details { display: block; }
+#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
+#HTMLReporter .summary { margin-top: 14px; }
+#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
+#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
+#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
+#HTMLReporter .description + .suite { margin-top: 0; }
+#HTMLReporter .suite { margin-top: 14px; }
+#HTMLReporter .suite a { color: #333333; }
+#HTMLReporter #details .specDetail { margin-bottom: 28px; }
+#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
+#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
+#HTMLReporter .resultMessage span.result { display: block; }
+#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
+
+#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
+#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
+#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
+#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
+#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
+#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
+#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
+#TrivialReporter .runner.running { background-color: yellow; }
+#TrivialReporter .options { text-align: right; font-size: .8em; }
+#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
+#TrivialReporter .suite .suite { margin: 5px; }
+#TrivialReporter .suite.passed { background-color: #dfd; }
+#TrivialReporter .suite.failed { background-color: #fdd; }
+#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
+#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
+#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
+#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
+#TrivialReporter .spec.skipped { background-color: #bbb; }
+#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
+#TrivialReporter .passed { background-color: #cfc; display: none; }
+#TrivialReporter .failed { background-color: #fbb; }
+#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
+#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
+#TrivialReporter .resultMessage .mismatch { color: black; }
+#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
+#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
+#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
+#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
+#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }
diff --git a/test/client/js_tests/lib/jasmine-1.2.0/jasmine.js b/test/client/js_tests/lib/jasmine-1.2.0/jasmine.js
new file mode 100644
index 0000000..03bf89a
--- /dev/null
+++ b/test/client/js_tests/lib/jasmine-1.2.0/jasmine.js
@@ -0,0 +1,2529 @@
+var isCommonJS = typeof window == "undefined";
+
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+if (isCommonJS) exports.jasmine = jasmine;
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+  throw new Error("unimplemented method");
+};
+
+/**
+ * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just
+ * a plain old variable and may be redefined by somebody else.
+ *
+ * @private
+ */
+jasmine.undefined = jasmine.___undefined___;
+
+/**
+ * Show diagnostic messages in the console if set to true
+ *
+ */
+jasmine.VERBOSE = false;
+
+/**
+ * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.
+ *
+ */
+jasmine.DEFAULT_UPDATE_INTERVAL = 250;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+jasmine.getGlobal = function() {
+  function getGlobal() {
+    return this;
+  }
+
+  return getGlobal();
+};
+
+/**
+ * Allows for bound functions to be compared.  Internal use only.
+ *
+ * @ignore
+ * @private
+ * @param base {Object} bound 'this' for the function
+ * @param name {Function} function to find
+ */
+jasmine.bindOriginal_ = function(base, name) {
+  var original = base[name];
+  if (original.apply) {
+    return function() {
+      return original.apply(base, arguments);
+    };
+  } else {
+    // IE support
+    return jasmine.getGlobal()[name];
+  }
+};
+
+jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');
+jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');
+jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');
+jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');
+
+jasmine.MessageResult = function(values) {
+  this.type = 'log';
+  this.values = values;
+  this.trace = new Error(); // todo: test better
+};
+
+jasmine.MessageResult.prototype.toString = function() {
+  var text = "";
+  for (var i = 0; i < this.values.length; i++) {
+    if (i > 0) text += " ";
+    if (jasmine.isString_(this.values[i])) {
+      text += this.values[i];
+    } else {
+      text += jasmine.pp(this.values[i]);
+    }
+  }
+  return text;
+};
+
+jasmine.ExpectationResult = function(params) {
+  this.type = 'expect';
+  this.matcherName = params.matcherName;
+  this.passed_ = params.passed;
+  this.expected = params.expected;
+  this.actual = params.actual;
+  this.message = this.passed_ ? 'Passed.' : params.message;
+
+  var trace = (params.trace || new Error(this.message));
+  this.trace = this.passed_ ? '' : trace;
+};
+
+jasmine.ExpectationResult.prototype.toString = function () {
+  return this.message;
+};
+
+jasmine.ExpectationResult.prototype.passed = function () {
+  return this.passed_;
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function() {
+  var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();
+  return env;
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isArray_ = function(value) {
+  return jasmine.isA_("Array", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isString_ = function(value) {
+  return jasmine.isA_("String", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isNumber_ = function(value) {
+  return jasmine.isA_("Number", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param {String} typeName
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isA_ = function(typeName, value) {
+  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+};
+
+/**
+ * Pretty printer for expecations.  Takes any object and turns it into a human-readable string.
+ *
+ * @param value {Object} an object to be outputted
+ * @returns {String}
+ */
+jasmine.pp = function(value) {
+  var stringPrettyPrinter = new jasmine.StringPrettyPrinter();
+  stringPrettyPrinter.format(value);
+  return stringPrettyPrinter.string;
+};
+
+/**
+ * Returns true if the object is a DOM Node.
+ *
+ * @param {Object} obj object to check
+ * @returns {Boolean}
+ */
+jasmine.isDomNode = function(obj) {
+  return obj.nodeType > 0;
+};
+
+/**
+ * Returns a matchable 'generic' object of the class type.  For use in expecations of type when values don't matter.
+ *
+ * @example
+ * // don't care about which function is passed in, as long as it's a function
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));
+ *
+ * @param {Class} clazz
+ * @returns matchable object of the type clazz
+ */
+jasmine.any = function(clazz) {
+  return new jasmine.Matchers.Any(clazz);
+};
+
+/**
+ * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the
+ * attributes on the object.
+ *
+ * @example
+ * // don't care about any other attributes than foo.
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"});
+ *
+ * @param sample {Object} sample
+ * @returns matchable object for the sample
+ */
+jasmine.objectContaining = function (sample) {
+    return new jasmine.Matchers.ObjectContaining(sample);
+};
+
+/**
+ * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.
+ *
+ * Spies should be created in test setup, before expectations.  They can then be checked, using the standard Jasmine
+ * expectation syntax. Spies can be checked if they were called or not and what the calling params were.
+ *
+ * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).
+ *
+ * Spies are torn down at the end of every spec.
+ *
+ * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.
+ *
+ * @example
+ * // a stub
+ * var myStub = jasmine.createSpy('myStub');  // can be used anywhere
+ *
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // actual foo.not will not be called, execution stops
+ * spyOn(foo, 'not');
+
+ // foo.not spied upon, execution will continue to implementation
+ * spyOn(foo, 'not').andCallThrough();
+ *
+ * // fake example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // foo.not(val) will return val
+ * spyOn(foo, 'not').andCallFake(function(value) {return value;});
+ *
+ * // mock example
+ * foo.not(7 == 7);
+ * expect(foo.not).toHaveBeenCalled();
+ * expect(foo.not).toHaveBeenCalledWith(true);
+ *
+ * @constructor
+ * @see spyOn, jasmine.createSpy, jasmine.createSpyObj
+ * @param {String} name
+ */
+jasmine.Spy = function(name) {
+  /**
+   * The name of the spy, if provided.
+   */
+  this.identity = name || 'unknown';
+  /**
+   *  Is this Object a spy?
+   */
+  this.isSpy = true;
+  /**
+   * The actual function this spy stubs.
+   */
+  this.plan = function() {
+  };
+  /**
+   * Tracking of the most recent call to the spy.
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy.mostRecentCall.args = [1, 2];
+   */
+  this.mostRecentCall = {};
+
+  /**
+   * Holds arguments for each call to the spy, indexed by call count
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy(7, 8);
+   * mySpy.mostRecentCall.args = [7, 8];
+   * mySpy.argsForCall[0] = [1, 2];
+   * mySpy.argsForCall[1] = [7, 8];
+   */
+  this.argsForCall = [];
+  this.calls = [];
+};
+
+/**
+ * Tells a spy to call through to the actual implemenatation.
+ *
+ * @example
+ * var foo = {
+ *   bar: function() { // do some stuff }
+ * }
+ *
+ * // defining a spy on an existing property: foo.bar
+ * spyOn(foo, 'bar').andCallThrough();
+ */
+jasmine.Spy.prototype.andCallThrough = function() {
+  this.plan = this.originalValue;
+  return this;
+};
+
+/**
+ * For setting the return value of a spy.
+ *
+ * @example
+ * // defining a spy from scratch: foo() returns 'baz'
+ * var foo = jasmine.createSpy('spy on foo').andReturn('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() returns 'baz'
+ * spyOn(foo, 'bar').andReturn('baz');
+ *
+ * @param {Object} value
+ */
+jasmine.Spy.prototype.andReturn = function(value) {
+  this.plan = function() {
+    return value;
+  };
+  return this;
+};
+
+/**
+ * For throwing an exception when a spy is called.
+ *
+ * @example
+ * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'
+ * var foo = jasmine.createSpy('spy on foo').andThrow('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'
+ * spyOn(foo, 'bar').andThrow('baz');
+ *
+ * @param {String} exceptionMsg
+ */
+jasmine.Spy.prototype.andThrow = function(exceptionMsg) {
+  this.plan = function() {
+    throw exceptionMsg;
+  };
+  return this;
+};
+
+/**
+ * Calls an alternate implementation when a spy is called.
+ *
+ * @example
+ * var baz = function() {
+ *   // do some stuff, return something
+ * }
+ * // defining a spy from scratch: foo() calls the function baz
+ * var foo = jasmine.createSpy('spy on foo').andCall(baz);
+ *
+ * // defining a spy on an existing property: foo.bar() calls an anonymnous function
+ * spyOn(foo, 'bar').andCall(function() { return 'baz';} );
+ *
+ * @param {Function} fakeFunc
+ */
+jasmine.Spy.prototype.andCallFake = function(fakeFunc) {
+  this.plan = fakeFunc;
+  return this;
+};
+
+/**
+ * Resets all of a spy's the tracking variables so that it can be used again.
+ *
+ * @example
+ * spyOn(foo, 'bar');
+ *
+ * foo.bar();
+ *
+ * expect(foo.bar.callCount).toEqual(1);
+ *
+ * foo.bar.reset();
+ *
+ * expect(foo.bar.callCount).toEqual(0);
+ */
+jasmine.Spy.prototype.reset = function() {
+  this.wasCalled = false;
+  this.callCount = 0;
+  this.argsForCall = [];
+  this.calls = [];
+  this.mostRecentCall = {};
+};
+
+jasmine.createSpy = function(name) {
+
+  var spyObj = function() {
+    spyObj.wasCalled = true;
+    spyObj.callCount++;
+    var args = jasmine.util.argsToArray(arguments);
+    spyObj.mostRecentCall.object = this;
+    spyObj.mostRecentCall.args = args;
+    spyObj.argsForCall.push(args);
+    spyObj.calls.push({object: this, args: args});
+    return spyObj.plan.apply(this, arguments);
+  };
+
+  var spy = new jasmine.Spy(name);
+
+  for (var prop in spy) {
+    spyObj[prop] = spy[prop];
+  }
+
+  spyObj.reset();
+
+  return spyObj;
+};
+
+/**
+ * Determines whether an object is a spy.
+ *
+ * @param {jasmine.Spy|Object} putativeSpy
+ * @returns {Boolean}
+ */
+jasmine.isSpy = function(putativeSpy) {
+  return putativeSpy && putativeSpy.isSpy;
+};
+
+/**
+ * Creates a more complicated spy: an Object that has every property a function that is a spy.  Used for stubbing something
+ * large in one call.
+ *
+ * @param {String} baseName name of spy class
+ * @param {Array} methodNames array of names of methods to make spies
+ */
+jasmine.createSpyObj = function(baseName, methodNames) {
+  if (!jasmine.isArray_(methodNames) || methodNames.length === 0) {
+    throw new Error('createSpyObj requires a non-empty array of method names to create spies for');
+  }
+  var obj = {};
+  for (var i = 0; i < methodNames.length; i++) {
+    obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);
+  }
+  return obj;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the current spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.log = function() {
+  var spec = jasmine.getEnv().currentSpec;
+  spec.log.apply(spec, arguments);
+};
+
+/**
+ * Function that installs a spy on an existing object's method name.  Used within a Spec to create a spy.
+ *
+ * @example
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops
+ *
+ * @see jasmine.createSpy
+ * @param obj
+ * @param methodName
+ * @returns a Jasmine spy that can be chained with all spy methods
+ */
+var spyOn = function(obj, methodName) {
+  return jasmine.getEnv().currentSpec.spyOn(obj, methodName);
+};
+if (isCommonJS) exports.spyOn = spyOn;
+
+/**
+ * Creates a Jasmine spec that will be added to the current suite.
+ *
+ * // TODO: pending tests
+ *
+ * @example
+ * it('should be true', function() {
+ *   expect(true).toEqual(true);
+ * });
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var it = function(desc, func) {
+  return jasmine.getEnv().it(desc, func);
+};
+if (isCommonJS) exports.it = it;
+
+/**
+ * Creates a <em>disabled</em> Jasmine spec.
+ *
+ * A convenience method that allows existing specs to be disabled temporarily during development.
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var xit = function(desc, func) {
+  return jasmine.getEnv().xit(desc, func);
+};
+if (isCommonJS) exports.xit = xit;
+
+/**
+ * Starts a chain for a Jasmine expectation.
+ *
+ * It is passed an Object that is the actual value and should chain to one of the many
+ * jasmine.Matchers functions.
+ *
+ * @param {Object} actual Actual value to test against and expected value
+ */
+var expect = function(actual) {
+  return jasmine.getEnv().currentSpec.expect(actual);
+};
+if (isCommonJS) exports.expect = expect;
+
+/**
+ * Defines part of a jasmine spec.  Used in cominbination with waits or waitsFor in asynchrnous specs.
+ *
+ * @param {Function} func Function that defines part of a jasmine spec.
+ */
+var runs = function(func) {
+  jasmine.getEnv().currentSpec.runs(func);
+};
+if (isCommonJS) exports.runs = runs;
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+var waits = function(timeout) {
+  jasmine.getEnv().currentSpec.waits(timeout);
+};
+if (isCommonJS) exports.waits = waits;
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);
+};
+if (isCommonJS) exports.waitsFor = waitsFor;
+
+/**
+ * A function that is called before each spec in a suite.
+ *
+ * Used for spec setup, including validating assumptions.
+ *
+ * @param {Function} beforeEachFunction
+ */
+var beforeEach = function(beforeEachFunction) {
+  jasmine.getEnv().beforeEach(beforeEachFunction);
+};
+if (isCommonJS) exports.beforeEach = beforeEach;
+
+/**
+ * A function that is called after each spec in a suite.
+ *
+ * Used for restoring any state that is hijacked during spec execution.
+ *
+ * @param {Function} afterEachFunction
+ */
+var afterEach = function(afterEachFunction) {
+  jasmine.getEnv().afterEach(afterEachFunction);
+};
+if (isCommonJS) exports.afterEach = afterEach;
+
+/**
+ * Defines a suite of specifications.
+ *
+ * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared
+ * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization
+ * of setup in some tests.
+ *
+ * @example
+ * // TODO: a simple suite
+ *
+ * // TODO: a simple suite with a nested describe block
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var describe = function(description, specDefinitions) {
+  return jasmine.getEnv().describe(description, specDefinitions);
+};
+if (isCommonJS) exports.describe = describe;
+
+/**
+ * Disables a suite of specifications.  Used to disable some suites in a file, or files, temporarily during development.
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var xdescribe = function(description, specDefinitions) {
+  return jasmine.getEnv().xdescribe(description, specDefinitions);
+};
+if (isCommonJS) exports.xdescribe = xdescribe;
+
+
+// Provide the XMLHttpRequest class for IE 5.x-6.x:
+jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
+  function tryIt(f) {
+    try {
+      return f();
+    } catch(e) {
+    }
+    return null;
+  }
+
+  var xhr = tryIt(function() {
+    return new ActiveXObject("Msxml2.XMLHTTP.6.0");
+  }) ||
+    tryIt(function() {
+      return new ActiveXObject("Msxml2.XMLHTTP.3.0");
+    }) ||
+    tryIt(function() {
+      return new ActiveXObject("Msxml2.XMLHTTP");
+    }) ||
+    tryIt(function() {
+      return new ActiveXObject("Microsoft.XMLHTTP");
+    });
+
+  if (!xhr) throw new Error("This browser does not support XMLHttpRequest.");
+
+  return xhr;
+} : XMLHttpRequest;
+/**
+ * @namespace
+ */
+jasmine.util = {};
+
+/**
+ * Declare that a child class inherit it's prototype from the parent class.
+ *
+ * @private
+ * @param {Function} childClass
+ * @param {Function} parentClass
+ */
+jasmine.util.inherit = function(childClass, parentClass) {
+  /**
+   * @private
+   */
+  var subclass = function() {
+  };
+  subclass.prototype = parentClass.prototype;
+  childClass.prototype = new subclass();
+};
+
+jasmine.util.formatException = function(e) {
+  var lineNumber;
+  if (e.line) {
+    lineNumber = e.line;
+  }
+  else if (e.lineNumber) {
+    lineNumber = e.lineNumber;
+  }
+
+  var file;
+
+  if (e.sourceURL) {
+    file = e.sourceURL;
+  }
+  else if (e.fileName) {
+    file = e.fileName;
+  }
+
+  var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();
+
+  if (file && lineNumber) {
+    message += ' in ' + file + ' (line ' + lineNumber + ')';
+  }
+
+  return message;
+};
+
+jasmine.util.htmlEscape = function(str) {
+  if (!str) return str;
+  return str.replace(/&/g, '&')
+    .replace(/</g, '<')
+    .replace(/>/g, '>');
+};
+
+jasmine.util.argsToArray = function(args) {
+  var arrayOfArgs = [];
+  for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+  return arrayOfArgs;
+};
+
+jasmine.util.extend = function(destination, source) {
+  for (var property in source) destination[property] = source[property];
+  return destination;
+};
+
+/**
+ * Environment for Jasmine
+ *
+ * @constructor
+ */
+jasmine.Env = function() {
+  this.currentSpec = null;
+  this.currentSuite = null;
+  this.currentRunner_ = new jasmine.Runner(this);
+
+  this.reporter = new jasmine.MultiReporter();
+
+  this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;
+  this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+  this.lastUpdate = 0;
+  this.specFilter = function() {
+    return true;
+  };
+
+  this.nextSpecId_ = 0;
+  this.nextSuiteId_ = 0;
+  this.equalityTesters_ = [];
+
+  // wrap matchers
+  this.matchersClass = function() {
+    jasmine.Matchers.apply(this, arguments);
+  };
+  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
+
+  jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
+};
+
+
+jasmine.Env.prototype.setTimeout = jasmine.setTimeout;
+jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;
+jasmine.Env.prototype.setInterval = jasmine.setInterval;
+jasmine.Env.prototype.clearInterval = jasmine.clearInterval;
+
+/**
+ * @returns an object containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.version = function () {
+  if (jasmine.version_) {
+    return jasmine.version_;
+  } else {
+    throw new Error('Version not set');
+  }
+};
+
+/**
+ * @returns string containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.versionString = function() {
+  if (!jasmine.version_) {
+    return "version unknown";
+  }
+
+  var version = this.version();
+  var versionString = version.major + "." + version.minor + "." + version.build;
+  if (version.release_candidate) {
+    versionString += ".rc" + version.release_candidate;
+  }
+  versionString += " revision " + version.revision;
+  return versionString;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSpecId = function () {
+  return this.nextSpecId_++;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSuiteId = function () {
+  return this.nextSuiteId_++;
+};
+
+/**
+ * Register a reporter to receive status updates from Jasmine.
+ * @param {jasmine.Reporter} reporter An object which will receive status updates.
+ */
+jasmine.Env.prototype.addReporter = function(reporter) {
+  this.reporter.addReporter(reporter);
+};
+
+jasmine.Env.prototype.execute = function() {
+  this.currentRunner_.execute();
+};
+
+jasmine.Env.prototype.describe = function(description, specDefinitions) {
+  var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);
+
+  var parentSuite = this.currentSuite;
+  if (parentSuite) {
+    parentSuite.add(suite);
+  } else {
+    this.currentRunner_.add(suite);
+  }
+
+  this.currentSuite = suite;
+
+  var declarationError = null;
+  try {
+    specDefinitions.call(suite);
+  } catch(e) {
+    declarationError = e;
+  }
+
+  if (declarationError) {
+    this.it("encountered a declaration exception", function() {
+      throw declarationError;
+    });
+  }
+
+  this.currentSuite = parentSuite;
+
+  return suite;
+};
+
+jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.beforeEach(beforeEachFunction);
+  } else {
+    this.currentRunner_.beforeEach(beforeEachFunction);
+  }
+};
+
+jasmine.Env.prototype.currentRunner = function () {
+  return this.currentRunner_;
+};
+
+jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.afterEach(afterEachFunction);
+  } else {
+    this.currentRunner_.afterEach(afterEachFunction);
+  }
+
+};
+
+jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {
+  return {
+    execute: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.it = function(description, func) {
+  var spec = new jasmine.Spec(this, this.currentSuite, description);
+  this.currentSuite.add(spec);
+  this.currentSpec = spec;
+
+  if (func) {
+    spec.runs(func);
+  }
+
+  return spec;
+};
+
+jasmine.Env.prototype.xit = function(desc, func) {
+  return {
+    id: this.nextSpecId(),
+    runs: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
+  if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
+    return true;
+  }
+
+  a.__Jasmine_been_here_before__ = b;
+  b.__Jasmine_been_here_before__ = a;
+
+  var hasKey = function(obj, keyName) {
+    return obj !== null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in b) {
+    if (!hasKey(a, property) && hasKey(b, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+  }
+  for (property in a) {
+    if (!hasKey(b, property) && hasKey(a, property)) {
+      mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
+    }
+  }
+  for (property in b) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
+      mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual.");
+    }
+  }
+
+  if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {
+    mismatchValues.push("arrays were not the same length");
+  }
+
+  delete a.__Jasmine_been_here_before__;
+  delete b.__Jasmine_been_here_before__;
+  return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  for (var i = 0; i < this.equalityTesters_.length; i++) {
+    var equalityTester = this.equalityTesters_[i];
+    var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
+    if (result !== jasmine.undefined) return result;
+  }
+
+  if (a === b) return true;
+
+  if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {
+    return (a == jasmine.undefined && b == jasmine.undefined);
+  }
+
+  if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+    return a === b;
+  }
+
+  if (a instanceof Date && b instanceof Date) {
+    return a.getTime() == b.getTime();
+  }
+
+  if (a.jasmineMatches) {
+    return a.jasmineMatches(b);
+  }
+
+  if (b.jasmineMatches) {
+    return b.jasmineMatches(a);
+  }
+
+  if (a instanceof jasmine.Matchers.ObjectContaining) {
+    return a.matches(b);
+  }
+
+  if (b instanceof jasmine.Matchers.ObjectContaining) {
+    return b.matches(a);
+  }
+
+  if (jasmine.isString_(a) && jasmine.isString_(b)) {
+    return (a == b);
+  }
+
+  if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {
+    return (a == b);
+  }
+
+  if (typeof a === "object" && typeof b === "object") {
+    return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
+  }
+
+  //Straight check
+  return (a === b);
+};
+
+jasmine.Env.prototype.contains_ = function(haystack, needle) {
+  if (jasmine.isArray_(haystack)) {
+    for (var i = 0; i < haystack.length; i++) {
+      if (this.equals_(haystack[i], needle)) return true;
+    }
+    return false;
+  }
+  return haystack.indexOf(needle) >= 0;
+};
+
+jasmine.Env.prototype.addEqualityTester = function(equalityTester) {
+  this.equalityTesters_.push(equalityTester);
+};
+/** No-op base class for Jasmine reporters.
+ *
+ * @constructor
+ */
+jasmine.Reporter = function() {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerStarting = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerResults = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecStarting = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecResults = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.log = function(str) {
+};
+
+/**
+ * Blocks are functions with executable code that make up a spec.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {Function} func
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Block = function(env, func, spec) {
+  this.env = env;
+  this.func = func;
+  this.spec = spec;
+};
+
+jasmine.Block.prototype.execute = function(onComplete) {  
+  try {
+    this.func.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+  }
+  onComplete();
+};
+/** JavaScript API reporter.
+ *
+ * @constructor
+ */
+jasmine.JsApiReporter = function() {
+  this.started = false;
+  this.finished = false;
+  this.suites_ = [];
+  this.results_ = {};
+};
+
+jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {
+  this.started = true;
+  var suites = runner.topLevelSuites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    this.suites_.push(this.summarize_(suite));
+  }
+};
+
+jasmine.JsApiReporter.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {
+  var isSuite = suiteOrSpec instanceof jasmine.Suite;
+  var summary = {
+    id: suiteOrSpec.id,
+    name: suiteOrSpec.description,
+    type: isSuite ? 'suite' : 'spec',
+    children: []
+  };
+  
+  if (isSuite) {
+    var children = suiteOrSpec.children();
+    for (var i = 0; i < children.length; i++) {
+      summary.children.push(this.summarize_(children[i]));
+    }
+  }
+  return summary;
+};
+
+jasmine.JsApiReporter.prototype.results = function() {
+  return this.results_;
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {
+  return this.results_[specId];
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {
+  this.finished = true;
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {
+  this.results_[spec.id] = {
+    messages: spec.results().getItems(),
+    result: spec.results().failedCount > 0 ? "failed" : "passed"
+  };
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.log = function(str) {
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){
+  var results = {};
+  for (var i = 0; i < specIds.length; i++) {
+    var specId = specIds[i];
+    results[specId] = this.summarizeResult_(this.results_[specId]);
+  }
+  return results;
+};
+
+jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){
+  var summaryMessages = [];
+  var messagesLength = result.messages.length;
+  for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {
+    var resultMessage = result.messages[messageIndex];
+    summaryMessages.push({
+      text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,
+      passed: resultMessage.passed ? resultMessage.passed() : true,
+      type: resultMessage.type,
+      message: resultMessage.message,
+      trace: {
+        stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined
+      }
+    });
+  }
+
+  return {
+    result : result.result,
+    messages : summaryMessages
+  };
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+  this.env = env;
+  this.actual = actual;
+  this.spec = spec;
+  this.isNot = opt_isNot || false;
+  this.reportWasCalled_ = false;
+};
+
+// todo: @deprecated as of Jasmine 0.11, remove soon [xw]
+jasmine.Matchers.pp = function(str) {
+  throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!");
+};
+
+// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]
+jasmine.Matchers.prototype.report = function(result, failing_message, details) {
+  throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs");
+};
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+  for (var methodName in prototype) {
+    if (methodName == 'report') continue;
+    var orig = prototype[methodName];
+    matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);
+  }
+};
+
+jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+  return function() {
+    var matcherArgs = jasmine.util.argsToArray(arguments);
+    var result = matcherFunction.apply(this, arguments);
+
+    if (this.isNot) {
+      result = !result;
+    }
+
+    if (this.reportWasCalled_) return result;
+
+    var message;
+    if (!result) {
+      if (this.message) {
+        message = this.message.apply(this, arguments);
+        if (jasmine.isArray_(message)) {
+          message = message[this.isNot ? 1 : 0];
+        }
+      } else {
+        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+        message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
+        if (matcherArgs.length > 0) {
+          for (var i = 0; i < matcherArgs.length; i++) {
+            if (i > 0) message += ",";
+            message += " " + jasmine.pp(matcherArgs[i]);
+          }
+        }
+        message += ".";
+      }
+    }
+    var expectationResult = new jasmine.ExpectationResult({
+      matcherName: matcherName,
+      passed: result,
+      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+      actual: this.actual,
+      message: message
+    });
+    this.spec.addMatcherResult(expectationResult);
+    return jasmine.undefined;
+  };
+};
+
+
+
+
+/**
+ * toBe: compares the actual to the expected using ===
+ * @param expected
+ */
+jasmine.Matchers.prototype.toBe = function(expected) {
+  return this.actual === expected;
+};
+
+/**
+ * toNotBe: compares the actual to the expected using !==
+ * @param expected
+ * @deprecated as of 1.0. Use not.toBe() instead.
+ */
+jasmine.Matchers.prototype.toNotBe = function(expected) {
+  return this.actual !== expected;
+};
+
+/**
+ * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toEqual = function(expected) {
+  return this.env.equals_(this.actual, expected);
+};
+
+/**
+ * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual
+ * @param expected
+ * @deprecated as of 1.0. Use not.toEqual() instead.
+ */
+jasmine.Matchers.prototype.toNotEqual = function(expected) {
+  return !this.env.equals_(this.actual, expected);
+};
+
+/**
+ * Matcher that compares the actual to the expected using a regular expression.  Constructs a RegExp, so takes
+ * a pattern or a String.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toMatch = function(expected) {
+  return new RegExp(expected).test(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch
+ * @param expected
+ * @deprecated as of 1.0. Use not.toMatch() instead.
+ */
+jasmine.Matchers.prototype.toNotMatch = function(expected) {
+  return !(new RegExp(expected).test(this.actual));
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeDefined = function() {
+  return (this.actual !== jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+  return (this.actual === jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+  return (this.actual === null);
+};
+
+/**
+ * Matcher that boolean not-nots the actual.
+ */
+jasmine.Matchers.prototype.toBeTruthy = function() {
+  return !!this.actual;
+};
+
+
+/**
+ * Matcher that boolean nots the actual.
+ */
+jasmine.Matchers.prototype.toBeFalsy = function() {
+  return !this.actual;
+};
+
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called.
+ */
+jasmine.Matchers.prototype.toHaveBeenCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to have been called.",
+      "Expected spy " + this.actual.identity + " not to have been called."
+    ];
+  };
+
+  return this.actual.wasCalled;
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */
+jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was not called.
+ *
+ * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead
+ */
+jasmine.Matchers.prototype.wasNotCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('wasNotCalled does not take arguments');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to not have been called.",
+      "Expected spy " + this.actual.identity + " to have been called."
+    ];
+  };
+
+  return !this.actual.wasCalled;
+};
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.
+ *
+ * @example
+ *
+ */
+jasmine.Matchers.prototype.toHaveBeenCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+  this.message = function() {
+    if (this.actual.callCount === 0) {
+      // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw]
+      return [
+        "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.",
+        "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."
+      ];
+    } else {
+      return [
+        "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall),
+        "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall)
+      ];
+    }
+  };
+
+  return this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;
+
+/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasNotCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was",
+      "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was"
+    ];
+  };
+
+  return !this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/**
+ * Matcher that checks that the expected item is an element in the actual Array.
+ *
+ * @param {Object} expected
+ */
+jasmine.Matchers.prototype.toContain = function(expected) {
+  return this.env.contains_(this.actual, expected);
+};
+
+/**
+ * Matcher that checks that the expected item is NOT an element in the actual Array.
+ *
+ * @param {Object} expected
+ * @deprecated as of 1.0. Use not.toContain() instead.
+ */
+jasmine.Matchers.prototype.toNotContain = function(expected) {
+  return !this.env.contains_(this.actual, expected);
+};
+
+jasmine.Matchers.prototype.toBeLessThan = function(expected) {
+  return this.actual < expected;
+};
+
+jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
+  return this.actual > expected;
+};
+
+/**
+ * Matcher that checks that the expected item is equal to the actual item
+ * up to a given level of decimal precision (default 2).
+ *
+ * @param {Number} expected
+ * @param {Number} precision
+ */
+jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) {
+  if (!(precision === 0)) {
+    precision = precision || 2;
+  }
+  var multiplier = Math.pow(10, precision);
+  var actual = Math.round(this.actual * multiplier);
+  expected = Math.round(expected * multiplier);
+  return expected == actual;
+};
+
+/**
+ * Matcher that checks that the expected exception was thrown by the actual.
+ *
+ * @param {String} expected
+ */
+jasmine.Matchers.prototype.toThrow = function(expected) {
+  var result = false;
+  var exception;
+  if (typeof this.actual != 'function') {
+    throw new Error('Actual is not a function');
+  }
+  try {
+    this.actual();
+  } catch (e) {
+    exception = e;
+  }
+  if (exception) {
+    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
+  }
+
+  var not = this.isNot ? "not " : "";
+
+  this.message = function() {
+    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
+      return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' ');
+    } else {
+      return "Expected function to throw an exception.";
+    }
+  };
+
+  return result;
+};
+
+jasmine.Matchers.Any = function(expectedClass) {
+  this.expectedClass = expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.jasmineMatches = function(other) {
+  if (this.expectedClass == String) {
+    return typeof other == 'string' || other instanceof String;
+  }
+
+  if (this.expectedClass == Number) {
+    return typeof other == 'number' || other instanceof Number;
+  }
+
+  if (this.expectedClass == Function) {
+    return typeof other == 'function' || other instanceof Function;
+  }
+
+  if (this.expectedClass == Object) {
+    return typeof other == 'object';
+  }
+
+  return other instanceof this.expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.jasmineToString = function() {
+  return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+jasmine.Matchers.ObjectContaining = function (sample) {
+  this.sample = sample;
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  var env = jasmine.getEnv();
+
+  var hasKey = function(obj, keyName) {
+    return obj != null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in this.sample) {
+    if (!hasKey(other, property) && hasKey(this.sample, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+    else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) {
+      mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual.");
+    }
+  }
+
+  return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () {
+  return "<jasmine.objectContaining(" + jasmine.pp(this.sample) + ")>";
+};
+// Mock setTimeout, clearTimeout
+// Contributed by Pivotal Computer Systems, www.pivotalsf.com
+
+jasmine.FakeTimer = function() {
+  this.reset();
+
+  var self = this;
+  self.setTimeout = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);
+    return self.timeoutsMade;
+  };
+
+  self.setInterval = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);
+    return self.timeoutsMade;
+  };
+
+  self.clearTimeout = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+  self.clearInterval = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+};
+
+jasmine.FakeTimer.prototype.reset = function() {
+  this.timeoutsMade = 0;
+  this.scheduledFunctions = {};
+  this.nowMillis = 0;
+};
+
+jasmine.FakeTimer.prototype.tick = function(millis) {
+  var oldMillis = this.nowMillis;
+  var newMillis = oldMillis + millis;
+  this.runFunctionsWithinRange(oldMillis, newMillis);
+  this.nowMillis = newMillis;
+};
+
+jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {
+  var scheduledFunc;
+  var funcsToRun = [];
+  for (var timeoutKey in this.scheduledFunctions) {
+    scheduledFunc = this.scheduledFunctions[timeoutKey];
+    if (scheduledFunc != jasmine.undefined &&
+        scheduledFunc.runAtMillis >= oldMillis &&
+        scheduledFunc.runAtMillis <= nowMillis) {
+      funcsToRun.push(scheduledFunc);
+      this.scheduledFunctions[timeoutKey] = jasmine.undefined;
+    }
+  }
+
+  if (funcsToRun.length > 0) {
+    funcsToRun.sort(function(a, b) {
+      return a.runAtMillis - b.runAtMillis;
+    });
+    for (var i = 0; i < funcsToRun.length; ++i) {
+      try {
+        var funcToRun = funcsToRun[i];
+        this.nowMillis = funcToRun.runAtMillis;
+        funcToRun.funcToCall();
+        if (funcToRun.recurring) {
+          this.scheduleFunction(funcToRun.timeoutKey,
+              funcToRun.funcToCall,
+              funcToRun.millis,
+              true);
+        }
+      } catch(e) {
+      }
+    }
+    this.runFunctionsWithinRange(oldMillis, nowMillis);
+  }
+};
+
+jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {
+  this.scheduledFunctions[timeoutKey] = {
+    runAtMillis: this.nowMillis + millis,
+    funcToCall: funcToCall,
+    recurring: recurring,
+    timeoutKey: timeoutKey,
+    millis: millis
+  };
+};
+
+/**
+ * @namespace
+ */
+jasmine.Clock = {
+  defaultFakeTimer: new jasmine.FakeTimer(),
+
+  reset: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.reset();
+  },
+
+  tick: function(millis) {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.tick(millis);
+  },
+
+  runFunctionsWithinRange: function(oldMillis, nowMillis) {
+    jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);
+  },
+
+  scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
+    jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);
+  },
+
+  useMock: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      var spec = jasmine.getEnv().currentSpec;
+      spec.after(jasmine.Clock.uninstallMock);
+
+      jasmine.Clock.installMock();
+    }
+  },
+
+  installMock: function() {
+    jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;
+  },
+
+  uninstallMock: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.installed = jasmine.Clock.real;
+  },
+
+  real: {
+    setTimeout: jasmine.getGlobal().setTimeout,
+    clearTimeout: jasmine.getGlobal().clearTimeout,
+    setInterval: jasmine.getGlobal().setInterval,
+    clearInterval: jasmine.getGlobal().clearInterval
+  },
+
+  assertInstalled: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+    }
+  },
+
+  isInstalled: function() {
+    return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;
+  },
+
+  installed: null
+};
+jasmine.Clock.installed = jasmine.Clock.real;
+
+//else for IE support
+jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setTimeout.apply) {
+    return jasmine.Clock.installed.setTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setTimeout(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().setInterval = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setInterval.apply) {
+    return jasmine.Clock.installed.setInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setInterval(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().clearTimeout = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearTimeout(timeoutKey);
+  }
+};
+
+jasmine.getGlobal().clearInterval = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearInterval(timeoutKey);
+  }
+};
+
+/**
+ * @constructor
+ */
+jasmine.MultiReporter = function() {
+  this.subReporters_ = [];
+};
+jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);
+
+jasmine.MultiReporter.prototype.addReporter = function(reporter) {
+  this.subReporters_.push(reporter);
+};
+
+(function() {
+  var functionNames = [
+    "reportRunnerStarting",
+    "reportRunnerResults",
+    "reportSuiteResults",
+    "reportSpecStarting",
+    "reportSpecResults",
+    "log"
+  ];
+  for (var i = 0; i < functionNames.length; i++) {
+    var functionName = functionNames[i];
+    jasmine.MultiReporter.prototype[functionName] = (function(functionName) {
+      return function() {
+        for (var j = 0; j < this.subReporters_.length; j++) {
+          var subReporter = this.subReporters_[j];
+          if (subReporter[functionName]) {
+            subReporter[functionName].apply(subReporter, arguments);
+          }
+        }
+      };
+    })(functionName);
+  }
+})();
+/**
+ * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults
+ *
+ * @constructor
+ */
+jasmine.NestedResults = function() {
+  /**
+   * The total count of results
+   */
+  this.totalCount = 0;
+  /**
+   * Number of passed results
+   */
+  this.passedCount = 0;
+  /**
+   * Number of failed results
+   */
+  this.failedCount = 0;
+  /**
+   * Was this suite/spec skipped?
+   */
+  this.skipped = false;
+  /**
+   * @ignore
+   */
+  this.items_ = [];
+};
+
+/**
+ * Roll up the result counts.
+ *
+ * @param result
+ */
+jasmine.NestedResults.prototype.rollupCounts = function(result) {
+  this.totalCount += result.totalCount;
+  this.passedCount += result.passedCount;
+  this.failedCount += result.failedCount;
+};
+
+/**
+ * Adds a log message.
+ * @param values Array of message parts which will be concatenated later.
+ */
+jasmine.NestedResults.prototype.log = function(values) {
+  this.items_.push(new jasmine.MessageResult(values));
+};
+
+/**
+ * Getter for the results: message & results.
+ */
+jasmine.NestedResults.prototype.getItems = function() {
+  return this.items_;
+};
+
+/**
+ * Adds a result, tracking counts (total, passed, & failed)
+ * @param {jasmine.ExpectationResult|jasmine.NestedResults} result
+ */
+jasmine.NestedResults.prototype.addResult = function(result) {
+  if (result.type != 'log') {
+    if (result.items_) {
+      this.rollupCounts(result);
+    } else {
+      this.totalCount++;
+      if (result.passed()) {
+        this.passedCount++;
+      } else {
+        this.failedCount++;
+      }
+    }
+  }
+  this.items_.push(result);
+};
+
+/**
+ * @returns {Boolean} True if <b>everything</b> below passed
+ */
+jasmine.NestedResults.prototype.passed = function() {
+  return this.passedCount === this.totalCount;
+};
+/**
+ * Base class for pretty printing for expectation results.
+ */
+jasmine.PrettyPrinter = function() {
+  this.ppNestLevel_ = 0;
+};
+
+/**
+ * Formats a value in a nice, human-readable string.
+ *
+ * @param value
+ */
+jasmine.PrettyPrinter.prototype.format = function(value) {
+  if (this.ppNestLevel_ > 40) {
+    throw new Error('jasmine.PrettyPrinter: format() nested too deeply!');
+  }
+
+  this.ppNestLevel_++;
+  try {
+    if (value === jasmine.undefined) {
+      this.emitScalar('undefined');
+    } else if (value === null) {
+      this.emitScalar('null');
+    } else if (value === jasmine.getGlobal()) {
+      this.emitScalar('<global>');
+    } else if (value.jasmineToString) {
+      this.emitScalar(value.jasmineToString());
+    } else if (typeof value === 'string') {
+      this.emitString(value);
+    } else if (jasmine.isSpy(value)) {
+      this.emitScalar("spy on " + value.identity);
+    } else if (value instanceof RegExp) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'function') {
+      this.emitScalar('Function');
+    } else if (typeof value.nodeType === 'number') {
+      this.emitScalar('HTMLNode');
+    } else if (value instanceof Date) {
+      this.emitScalar('Date(' + value + ')');
+    } else if (value.__Jasmine_been_here_before__) {
+      this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+    } else if (jasmine.isArray_(value) || typeof value == 'object') {
+      value.__Jasmine_been_here_before__ = true;
+      if (jasmine.isArray_(value)) {
+        this.emitArray(value);
+      } else {
+        this.emitObject(value);
+      }
+      delete value.__Jasmine_been_here_before__;
+    } else {
+      this.emitScalar(value.toString());
+    }
+  } finally {
+    this.ppNestLevel_--;
+  }
+};
+
+jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+  for (var property in obj) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && 
+                                         obj.__lookupGetter__(property) !== null) : false);
+  }
+};
+
+jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;
+
+jasmine.StringPrettyPrinter = function() {
+  jasmine.PrettyPrinter.call(this);
+
+  this.string = '';
+};
+jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);
+
+jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {
+  this.append(value);
+};
+
+jasmine.StringPrettyPrinter.prototype.emitString = function(value) {
+  this.append("'" + value + "'");
+};
+
+jasmine.StringPrettyPrinter.prototype.emitArray = function(array) {
+  this.append('[ ');
+  for (var i = 0; i < array.length; i++) {
+    if (i > 0) {
+      this.append(', ');
+    }
+    this.format(array[i]);
+  }
+  this.append(' ]');
+};
+
+jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
+  var self = this;
+  this.append('{ ');
+  var first = true;
+
+  this.iterateObject(obj, function(property, isGetter) {
+    if (first) {
+      first = false;
+    } else {
+      self.append(', ');
+    }
+
+    self.append(property);
+    self.append(' : ');
+    if (isGetter) {
+      self.append('<getter>');
+    } else {
+      self.format(obj[property]);
+    }
+  });
+
+  this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+  this.string += value;
+};
+jasmine.Queue = function(env) {
+  this.env = env;
+  this.blocks = [];
+  this.running = false;
+  this.index = 0;
+  this.offset = 0;
+  this.abort = false;
+};
+
+jasmine.Queue.prototype.addBefore = function(block) {
+  this.blocks.unshift(block);
+};
+
+jasmine.Queue.prototype.add = function(block) {
+  this.blocks.push(block);
+};
+
+jasmine.Queue.prototype.insertNext = function(block) {
+  this.blocks.splice((this.index + this.offset + 1), 0, block);
+  this.offset++;
+};
+
+jasmine.Queue.prototype.start = function(onComplete) {
+  this.running = true;
+  this.onComplete = onComplete;
+  this.next_();
+};
+
+jasmine.Queue.prototype.isRunning = function() {
+  return this.running;
+};
+
+jasmine.Queue.LOOP_DONT_RECURSE = true;
+
+jasmine.Queue.prototype.next_ = function() {
+  var self = this;
+  var goAgain = true;
+
+  while (goAgain) {
+    goAgain = false;
+    
+    if (self.index < self.blocks.length && !this.abort) {
+      var calledSynchronously = true;
+      var completedSynchronously = false;
+
+      var onComplete = function () {
+        if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {
+          completedSynchronously = true;
+          return;
+        }
+
+        if (self.blocks[self.index].abort) {
+          self.abort = true;
+        }
+
+        self.offset = 0;
+        self.index++;
+
+        var now = new Date().getTime();
+        if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {
+          self.env.lastUpdate = now;
+          self.env.setTimeout(function() {
+            self.next_();
+          }, 0);
+        } else {
+          if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {
+            goAgain = true;
+          } else {
+            self.next_();
+          }
+        }
+      };
+      self.blocks[self.index].execute(onComplete);
+
+      calledSynchronously = false;
+      if (completedSynchronously) {
+        onComplete();
+      }
+      
+    } else {
+      self.running = false;
+      if (self.onComplete) {
+        self.onComplete();
+      }
+    }
+  }
+};
+
+jasmine.Queue.prototype.results = function() {
+  var results = new jasmine.NestedResults();
+  for (var i = 0; i < this.blocks.length; i++) {
+    if (this.blocks[i].results) {
+      results.addResult(this.blocks[i].results());
+    }
+  }
+  return results;
+};
+
+
+/**
+ * Runner
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ */
+jasmine.Runner = function(env) {
+  var self = this;
+  self.env = env;
+  self.queue = new jasmine.Queue(env);
+  self.before_ = [];
+  self.after_ = [];
+  self.suites_ = [];
+};
+
+jasmine.Runner.prototype.execute = function() {
+  var self = this;
+  if (self.env.reporter.reportRunnerStarting) {
+    self.env.reporter.reportRunnerStarting(this);
+  }
+  self.queue.start(function () {
+    self.finishCallback();
+  });
+};
+
+jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.splice(0,0,beforeEachFunction);
+};
+
+jasmine.Runner.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.splice(0,0,afterEachFunction);
+};
+
+
+jasmine.Runner.prototype.finishCallback = function() {
+  this.env.reporter.reportRunnerResults(this);
+};
+
+jasmine.Runner.prototype.addSuite = function(suite) {
+  this.suites_.push(suite);
+};
+
+jasmine.Runner.prototype.add = function(block) {
+  if (block instanceof jasmine.Suite) {
+    this.addSuite(block);
+  }
+  this.queue.add(block);
+};
+
+jasmine.Runner.prototype.specs = function () {
+  var suites = this.suites();
+  var specs = [];
+  for (var i = 0; i < suites.length; i++) {
+    specs = specs.concat(suites[i].specs());
+  }
+  return specs;
+};
+
+jasmine.Runner.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Runner.prototype.topLevelSuites = function() {
+  var topLevelSuites = [];
+  for (var i = 0; i < this.suites_.length; i++) {
+    if (!this.suites_[i].parentSuite) {
+      topLevelSuites.push(this.suites_[i]);
+    }
+  }
+  return topLevelSuites;
+};
+
+jasmine.Runner.prototype.results = function() {
+  return this.queue.results();
+};
+/**
+ * Internal representation of a Jasmine specification, or test.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {jasmine.Suite} suite
+ * @param {String} description
+ */
+jasmine.Spec = function(env, suite, description) {
+  if (!env) {
+    throw new Error('jasmine.Env() required');
+  }
+  if (!suite) {
+    throw new Error('jasmine.Suite() required');
+  }
+  var spec = this;
+  spec.id = env.nextSpecId ? env.nextSpecId() : null;
+  spec.env = env;
+  spec.suite = suite;
+  spec.description = description;
+  spec.queue = new jasmine.Queue(env);
+
+  spec.afterCallbacks = [];
+  spec.spies_ = [];
+
+  spec.results_ = new jasmine.NestedResults();
+  spec.results_.description = description;
+  spec.matchersClass = null;
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+  return this.suite.getFullName() + ' ' + this.description + '.';
+};
+
+
+jasmine.Spec.prototype.results = function() {
+  return this.results_;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.Spec.prototype.log = function() {
+  return this.results_.log(arguments);
+};
+
+jasmine.Spec.prototype.runs = function (func) {
+  var block = new jasmine.Block(this.env, func, this);
+  this.addToQueue(block);
+  return this;
+};
+
+jasmine.Spec.prototype.addToQueue = function (block) {
+  if (this.queue.isRunning()) {
+    this.queue.insertNext(block);
+  } else {
+    this.queue.add(block);
+  }
+};
+
+/**
+ * @param {jasmine.ExpectationResult} result
+ */
+jasmine.Spec.prototype.addMatcherResult = function(result) {
+  this.results_.addResult(result);
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+  var positive = new (this.getMatchersClass_())(this.env, actual, this);
+  positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
+  return positive;
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+jasmine.Spec.prototype.waits = function(timeout) {
+  var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);
+  this.addToQueue(waitsFunc);
+  return this;
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  var latchFunction_ = null;
+  var optional_timeoutMessage_ = null;
+  var optional_timeout_ = null;
+
+  for (var i = 0; i < arguments.length; i++) {
+    var arg = arguments[i];
+    switch (typeof arg) {
+      case 'function':
+        latchFunction_ = arg;
+        break;
+      case 'string':
+        optional_timeoutMessage_ = arg;
+        break;
+      case 'number':
+        optional_timeout_ = arg;
+        break;
+    }
+  }
+
+  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
+  this.addToQueue(waitsForFunc);
+  return this;
+};
+
+jasmine.Spec.prototype.fail = function (e) {
+  var expectationResult = new jasmine.ExpectationResult({
+    passed: false,
+    message: e ? jasmine.util.formatException(e) : 'Exception',
+    trace: { stack: e.stack }
+  });
+  this.results_.addResult(expectationResult);
+};
+
+jasmine.Spec.prototype.getMatchersClass_ = function() {
+  return this.matchersClass || this.env.matchersClass;
+};
+
+jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
+  var parent = this.getMatchersClass_();
+  var newMatchersClass = function() {
+    parent.apply(this, arguments);
+  };
+  jasmine.util.inherit(newMatchersClass, parent);
+  jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+  this.matchersClass = newMatchersClass;
+};
+
+jasmine.Spec.prototype.finishCallback = function() {
+  this.env.reporter.reportSpecResults(this);
+};
+
+jasmine.Spec.prototype.finish = function(onComplete) {
+  this.removeAllSpies();
+  this.finishCallback();
+  if (onComplete) {
+    onComplete();
+  }
+};
+
+jasmine.Spec.prototype.after = function(doAfter) {
+  if (this.queue.isRunning()) {
+    this.queue.add(new jasmine.Block(this.env, doAfter, this));
+  } else {
+    this.afterCallbacks.unshift(doAfter);
+  }
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+  var spec = this;
+  if (!spec.env.specFilter(spec)) {
+    spec.results_.skipped = true;
+    spec.finish(onComplete);
+    return;
+  }
+
+  this.env.reporter.reportSpecStarting(this);
+
+  spec.env.currentSpec = spec;
+
+  spec.addBeforesAndAftersToQueue();
+
+  spec.queue.start(function () {
+    spec.finish(onComplete);
+  });
+};
+
+jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {
+  var runner = this.env.currentRunner();
+  var i;
+
+  for (var suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.before_.length; i++) {
+      this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));
+    }
+  }
+  for (i = 0; i < runner.before_.length; i++) {
+    this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));
+  }
+  for (i = 0; i < this.afterCallbacks.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this));
+  }
+  for (suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.after_.length; i++) {
+      this.queue.add(new jasmine.Block(this.env, suite.after_[i], this));
+    }
+  }
+  for (i = 0; i < runner.after_.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, runner.after_[i], this));
+  }
+};
+
+jasmine.Spec.prototype.explodes = function() {
+  throw 'explodes function should not have been called';
+};
+
+jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {
+  if (obj == jasmine.undefined) {
+    throw "spyOn could not find an object to spy upon for " + methodName + "()";
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {
+    throw methodName + '() method does not exist';
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {
+    throw new Error(methodName + ' has already been spied upon');
+  }
+
+  var spyObj = jasmine.createSpy(methodName);
+
+  this.spies_.push(spyObj);
+  spyObj.baseObj = obj;
+  spyObj.methodName = methodName;
+  spyObj.originalValue = obj[methodName];
+
+  obj[methodName] = spyObj;
+
+  return spyObj;
+};
+
+jasmine.Spec.prototype.removeAllSpies = function() {
+  for (var i = 0; i < this.spies_.length; i++) {
+    var spy = this.spies_[i];
+    spy.baseObj[spy.methodName] = spy.originalValue;
+  }
+  this.spies_ = [];
+};
+
+/**
+ * Internal representation of a Jasmine suite.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {String} description
+ * @param {Function} specDefinitions
+ * @param {jasmine.Suite} parentSuite
+ */
+jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
+  var self = this;
+  self.id = env.nextSuiteId ? env.nextSuiteId() : null;
+  self.description = description;
+  self.queue = new jasmine.Queue(env);
+  self.parentSuite = parentSuite;
+  self.env = env;
+  self.before_ = [];
+  self.after_ = [];
+  self.children_ = [];
+  self.suites_ = [];
+  self.specs_ = [];
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+  var fullName = this.description;
+  for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+    fullName = parentSuite.description + ' ' + fullName;
+  }
+  return fullName;
+};
+
+jasmine.Suite.prototype.finish = function(onComplete) {
+  this.env.reporter.reportSuiteResults(this);
+  this.finished = true;
+  if (typeof(onComplete) == 'function') {
+    onComplete();
+  }
+};
+
+jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.unshift(beforeEachFunction);
+};
+
+jasmine.Suite.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.unshift(afterEachFunction);
+};
+
+jasmine.Suite.prototype.results = function() {
+  return this.queue.results();
+};
+
+jasmine.Suite.prototype.add = function(suiteOrSpec) {
+  this.children_.push(suiteOrSpec);
+  if (suiteOrSpec instanceof jasmine.Suite) {
+    this.suites_.push(suiteOrSpec);
+    this.env.currentRunner().addSuite(suiteOrSpec);
+  } else {
+    this.specs_.push(suiteOrSpec);
+  }
+  this.queue.add(suiteOrSpec);
+};
+
+jasmine.Suite.prototype.specs = function() {
+  return this.specs_;
+};
+
+jasmine.Suite.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Suite.prototype.children = function() {
+  return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+  var self = this;
+  this.queue.start(function () {
+    self.finish(onComplete);
+  });
+};
+jasmine.WaitsBlock = function(env, timeout, spec) {
+  this.timeout = timeout;
+  jasmine.Block.call(this, env, null, spec);
+};
+
+jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);
+
+jasmine.WaitsBlock.prototype.execute = function (onComplete) {
+  if (jasmine.VERBOSE) {
+    this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');
+  }
+  this.env.setTimeout(function () {
+    onComplete();
+  }, this.timeout);
+};
+/**
+ * A block which waits for some condition to become true, with timeout.
+ *
+ * @constructor
+ * @extends jasmine.Block
+ * @param {jasmine.Env} env The Jasmine environment.
+ * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.
+ * @param {Function} latchFunction A function which returns true when the desired condition has been met.
+ * @param {String} message The message to display if the desired condition hasn't been met within the given time period.
+ * @param {jasmine.Spec} spec The Jasmine spec.
+ */
+jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {
+  this.timeout = timeout || env.defaultTimeoutInterval;
+  this.latchFunction = latchFunction;
+  this.message = message;
+  this.totalTimeSpentWaitingForLatch = 0;
+  jasmine.Block.call(this, env, null, spec);
+};
+jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);
+
+jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;
+
+jasmine.WaitsForBlock.prototype.execute = function(onComplete) {
+  if (jasmine.VERBOSE) {
+    this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));
+  }
+  var latchFunctionResult;
+  try {
+    latchFunctionResult = this.latchFunction.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+    onComplete();
+    return;
+  }
+
+  if (latchFunctionResult) {
+    onComplete();
+  } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {
+    var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');
+    this.spec.fail({
+      name: 'timeout',
+      message: message
+    });
+
+    this.abort = true;
+    onComplete();
+  } else {
+    this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;
+    var self = this;
+    this.env.setTimeout(function() {
+      self.execute(onComplete);
+    }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);
+  }
+};
+
+jasmine.version_= {
+  "major": 1,
+  "minor": 2,
+  "build": 0,
+  "revision": 1337005947
+};
diff --git a/test/client/js_tests/spec/GFF3toJbrowseJsonSpec.js b/test/client/js_tests/spec/GFF3toJbrowseJsonSpec.js
new file mode 100644
index 0000000..5e8d831
--- /dev/null
+++ b/test/client/js_tests/spec/GFF3toJbrowseJsonSpec.js
@@ -0,0 +1,135 @@
+describe("GFF3toJbrowseJson", function() { 
+	// GFF3toNclist takes a data structure such as that returned by GFF3toJson.js 
+	// and makes it into an nested containment list suitable for use in 
+	// WebApollo and possibly Jbrowse. 
+
+	var nclistGen;
+	var parsedGFF3toJbrowseJsonInput, expectedJbrowseJsonOutput, actualJbrowseJsonOutput;
+	var gp;
+
+	beforeEach(function() {
+		gp = new GFF3Parser;
+		nclistGen = new GFF3toJbrowseJson();
+		makerGff3String = "Group1.33	maker	gene	245454	247006	.	+	.	ID=this_parent_id_12345;Name=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;\nGroup1.33	maker	mRNA	245454	247006	.	+	.	ID=1:gnomon_566853_mRNA;Parent=this_parent_id_12345;Name=gnomon_566853_mRNA;_AED=0.45;_eAED=0.45;_QI=138|1|1|1|1|1|4|191|259;\nGroup1.33	maker	exon	245454	245533	.	+	.	ID=1:gnomon_566853_mRNA:exon:5976;Parent=1:gnomon_566853_mRNA;\nGroup1.33	maker	exon	245702	245879	.	+	.	ID=1:gnomon_566853_mRNA:exon:5977;Parent [...]
+
+		// fixtures to test making jbrowse json from parsed GFF3
+		expectedJbrowseJsonOutput = { // just putting this here for reference, I'm going to check most items in this struct manually below
+			"histograms" : {
+			    "stats" : [ {
+				    "basesPerBin" : "1000000",
+				    "max" : 1,
+				    "mean" : 1
+				} ],
+			    "meta" : [ {
+				    "basesPerBin" : "1000000",
+				    "arrayParams" : {
+					"length" : 1,
+					"chunkSize" : 10000,
+					"urlTemplate" : "hist-1000000-{Chunk}.json"
+				    }
+				} ]
+			},
+			"featureCount" : 1,
+			"intervals" : {
+			    "nclist" : [ 
+					[ 0, 0, 14, -1, "maker", null, "mRNA", null, "x0", "x0", 
+					  [ [ 1, 0, 3, -1, null, null, "exon", null, "p0", "p0", null ], 
+					    [ 1, 6, 9, -1, null, null, "exon", null, null, null, null ], 
+					    [ 1, 12, 14, -1, null, null, "exon", null, null, null, null ], 
+					    [ 1, 0, 14, -1, null, null, "wholeCDS", null, null, null, null ] 
+					    ] ]
+					 ],
+			    "classes" : [ {
+				    "isArrayAttr" : {
+					"Subfeatures" : 1
+				    },
+				    "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+				}, {
+				    "isArrayAttr" : {
+				    },
+				    "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+				}, {
+				    "isArrayAttr" : {
+					"Sublist" : 1
+				    },
+				    "attributes" : [ "Start", "End", "Chunk" ]
+				} ],
+			    "maxEnd" : 247006,
+			    "count" : 1,
+			    "lazyClass" : 2,
+			    "urlTemplate" : "lf-{Chunk}.json",
+			    "minStart" : 245453
+			},
+			"formatVersion" : 1
+		    };
+
+		parsedGFF3toJbrowseJsonInput = gp.parse( makerGff3String );
+		actualJbrowseJsonOutput = nclistGen.gff3toJbrowseJson(parsedGFF3toJbrowseJsonInput); 
+
+	    });
+	
+	xit("should respond to gff3toJbrowseJson", function() {
+		expect(nclistGen.gff3toJbrowseJson).toBeDefined();
+	    });
+
+	xit("should correctly set histograms/stats/meta in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["histograms"]).toEqual({"stats" : [ {"basesPerBin" : "1000000","max" : 1,"mean" : 1} ],"meta" : [ { "basesPerBin" : "1000000", "arrayParams" : { "length" : 1, "chunkSize" : 10000, "urlTemplate" : "hist-1000000-{Chunk}.json"}}]});
+	    });
+
+	xit("should correctly set featureCount in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["featureCount"]).toEqual(1);
+	    });
+	
+        xit("should correctly set ['intervals']['nclist'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["nclist"]).toEqual(
+			       [0, 245454, 247006, "+", "maker", ".", "mRNA", ".", "1:gnomon_566853_mRNA", "gnomon_566853_mRNA", 
+				[ [ 1, 0, 245454, 245533, "+", "maker", ".", "exon", ".", "1:gnomon_566853_mRNA:exon:5976", null],
+				  [ 1, 0, 245702, 245879, "+", "maker", ".", "exon", ".", "1:gnomon_566853_mRNA:exon:5977", null] ] ]
+									       );
+	    });
+
+	xit("should correctly set ['intervals']['classes'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["classes"]).toEqual(
+		   [ {
+				    "isArrayAttr" : {
+					"Subfeatures" : 1
+				    },
+				    "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+				}, {
+				    "isArrayAttr" : {
+				    },
+				    "attributes" : [ "Start", "End", "Strand", "Source", "Phase", "Type", "Score", "Id", "Name", "Subfeatures" ]
+				}, {
+				    "isArrayAttr" : {
+					"Sublist" : 1
+				    },
+				    "attributes" : [ "Start", "End", "Chunk" ]
+					} ]);
+
+	    });
+
+	xit("should correctly set ['intervals']['maxEnd'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["maxEnd"]).toEqual( 245879 );
+	    });
+	xit("should correctly set ['intervals']['count'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["count"]).toEqual( 1 )
+    	    });
+
+	xit("should correctly set ['intervals']['lazyClass'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["lazyClass"]).toEqual(2)
+		    });
+	
+	xit("should correctly set ['intervals']['urlTemplate'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["urlTemplate"]).toEqual("lf-{Chunk}.json");
+	    });
+	
+	xit("should correctly set ['intervals']['minStart'] in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["intervals"]["minStart"]).toEqual( 245454 );
+	    });
+	
+	xit("should correctly set formatVersion in jbrowse json", function() {
+		expect(actualJbrowseJsonOutput["formatVersion"]).toEqual(1);
+	    });
+
+    });
+
diff --git a/test/client/js_tests/spec/JSONUtilsSpec.js b/test/client/js_tests/spec/JSONUtilsSpec.js
new file mode 100644
index 0000000..a9c2a9a
--- /dev/null
+++ b/test/client/js_tests/spec/JSONUtilsSpec.js
@@ -0,0 +1,130 @@
+describe("JSONUtils", function() { 
+	/*
+	   JSONUtils was made by Gregg/Ed to manipulate JSON data structures, especially to coerce various
+	   kinds of JSON into JSON suitable for using with jbrowse or Webapollo. 
+	   JR added this spec file in 10/2012 to test convertParsedGFF3JsonToFeatureArray(), but we can add tests 
+	   for the other stuff in JSONUtils later too. 
+
+	   convertParsedGFF3JsonToFeatureArray()
+	   takes one feature and all of its subfeatures (children/grandchildren/great-grandchildren/...) from 
+	   a parsed GFF3 data struct (returned from GFF3toJson()), and returns a a two-level feature array for 
+	   the lowest and next-lowest level. For example, given a data struct for a parsed gene/mRNA/exon GFF3
+	   it would return a two-level feature array for the mRNA and all of it's exons. 
+
+	   An example of input fixture is in fixtures/parsedGff3Json2FeatureArrayTest.parsedGff3Json
+	   An example of expected output is in fixtures/parsedGff3Json2FeatureArrayTest.featureArray
+	   
+	   Both are basically the same as what's used in the tests below.
+        */
+
+	var jsonUtil; 
+	var parsedGFF3StringInput, featureArrayOutput;
+	var parsedGFF3toJbrowseJsonInput, expectedJbrowseJsonOutput;
+
+	beforeEach(function() {
+		jsonUtil = new JSONUtils();
+		parsedGFF3StringInput = {
+		    "parsedData" : [
+		[{
+			'ID':'maker-Group1%2E33-pred_gff_GNOMON-gene-4.137',
+			'data': ['Group1.33','maker','gene','245454','247006','.','+','.','ID=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;Name=maker-Group1%252E33-pred_gff_GNOMON-gene-4.137'],
+			'children': [ 
+				     [{
+					     'ID':'1:gnomon_566853_mRNA',
+					     'data':['Group1.33','maker','mRNA','245454','247006','.','+','.','ID=1:gnomon_566853_mRNA;Parent=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;Name=gnomon_566853_mRNA;_AED=0.45;_eAED=0.45;_QI=138|1|1|1|1|1|4|191|259'],
+					     'children':[
+							 [{'ID':'1:gnomon_566853_mRNA:exon:5976',
+							   'data':['Group1.33','maker','exon','245454','245533','.','+','.','ID=1:gnomon_566853_mRNA:exon:5976;Parent=1:gnomon_566853_mRNA'],
+							   'children':[],
+							     }],
+							 [{'ID':'1:gnomon_566853_mRNA:exon:5977', 
+							   'data': ['Group1.33','maker','exon','245702','245879','.','+','.','ID=1:gnomon_566853_mRNA:exon:5977;Parent=1:gnomon_566853_mRNA'], 
+							   'children':[],} ]
+							 ]
+					 }]
+				      ]}]
+				    ],
+		    "parseErrors" : [""],
+		    "parseWarnings" : [""]
+		};
+	    });
+
+	it("should respond to convertParsedGFF3JsonToFeatureArray", function() {
+		expect(jsonUtil.convertParsedGFF3JsonToFeatureArray).toBeDefined();
+	});
+
+	it("should respond to determineParsedGff3Depth", function() {
+		expect(jsonUtil.determineParsedGff3Depth).toBeDefined();
+	});
+
+	it("should properly determineParsedGff3Depth of gene/mRNA/exon parsedGFF3 struct", function() {
+		expect(jsonUtil.determineParsedGff3Depth( parsedGFF3StringInput ) ).toEqual( 3 );
+	});
+
+	it("should respond to getFeatureAtGivenDepth", function() {
+		expect(jsonUtil.getFeatureAtGivenDepth).toBeDefined();
+	});
+
+	it("should properly getFeatureAtGivenDepth of gene/mRNA/exon parsedGFF3 struct", function() {
+		expect(jsonUtil.getFeatureAtGivenDepth(parsedGFF3StringInput, 2) ).toEqual( 
+											   [ { "ID" : '1:gnomon_566853_mRNA', "data" : [ 'Group1.33', 'maker', 'mRNA', '245454', '247006', '.', '+', '.', 'ID=1:gnomon_566853_mRNA;Parent=maker-Group1%2E33-pred_gff_GNOMON-gene-4.137;Name=gnomon_566853_mRNA;_AED=0.45;_eAED=0.45;_QI=138|1|1|1|1|1|4|191|259' ], children : [ [ { "ID" : '1:gnomon_566853_mRNA:exon:5976', "data" : [ 'Group1.33', 'maker', 'exon', '245454', '245533', '.', '+', '.', 'ID=1:gnomon_566853_mRNA:exon:5976;Parent=1:gnomon_566853_mRNA' ], children : [ [...]
+											    );
+	});
+
+	it("should return an array", function() {
+		featureArrayOutput = jsonUtil.convertParsedGFF3JsonToFeatureArray( parsedGFF3StringInput );
+		expect(featureArrayOutput).toBeDefined();
+	});
+
+	// test parent (mRNA in this case)
+	it("should set first field feature array to 0", function() {
+		featureArrayOutput = jsonUtil.convertParsedGFF3JsonToFeatureArray( parsedGFF3StringInput );
+		expect(featureArrayOutput[0]).toEqual(0);
+	});
+
+	it("should correctly set parent's Start/End/Strand/Source/Phase/Type/Score/Id/Name", function() {
+		featureArrayOutput = jsonUtil.convertParsedGFF3JsonToFeatureArray( parsedGFF3StringInput );
+		expect(featureArrayOutput[1]).toEqual(245454); // Start
+		expect(featureArrayOutput[2]).toEqual(247006); //End
+		expect(featureArrayOutput[3]).toEqual("+"); //Strand
+		expect(featureArrayOutput[4]).toEqual("maker"); //Source
+		expect(featureArrayOutput[5]).toEqual("."); //Phase
+		expect(featureArrayOutput[6]).toEqual("mRNA"); //Type
+		expect(featureArrayOutput[7]).toEqual("."); //Score
+		expect(featureArrayOutput[8]).toEqual("1:gnomon_566853_mRNA"); //Id
+		expect(featureArrayOutput[9]).toEqual("gnomon_566853_mRNA"); //Name
+	    });
+
+	it("should correctly first child's Start/End/Strand/Source/Phase/Type/Score/Id/Name", function() {
+		// first child
+		featureArrayOutput = jsonUtil.convertParsedGFF3JsonToFeatureArray( parsedGFF3StringInput );
+		expect(featureArrayOutput[10][0][0]).toEqual(1); // ?
+		expect(featureArrayOutput[10][0][1]).toEqual(245454); // Start
+		expect(featureArrayOutput[10][0][2]).toEqual(245533); //End
+		expect(featureArrayOutput[10][0][3]).toEqual("+"); //Strand
+		expect(featureArrayOutput[10][0][4]).toEqual("maker"); //Source
+		expect(featureArrayOutput[10][0][5]).toEqual("."); //Phase
+		expect(featureArrayOutput[10][0][6]).toEqual("exon"); //Type
+		expect(featureArrayOutput[10][0][7]).toEqual("."); //Score
+		expect(featureArrayOutput[10][0][8]).toEqual("1:gnomon_566853_mRNA:exon:5976"); //Id
+		expect(featureArrayOutput[10][0][9]).toEqual(null); //Name
+	    });
+
+	it("should correctly second child's Start/End/Strand/Source/Phase/Type/Score/Id/Name", function() {
+		// second child
+		featureArrayOutput = jsonUtil.convertParsedGFF3JsonToFeatureArray( parsedGFF3StringInput );
+		expect(featureArrayOutput[10].length).toEqual(2); // should actually be a second child
+		expect(featureArrayOutput[10][1][0]).toEqual(1); // ?
+		expect(featureArrayOutput[10][1][1]).toEqual(245702); // Start
+		expect(featureArrayOutput[10][1][2]).toEqual(245879); //End
+		expect(featureArrayOutput[10][1][3]).toEqual("+"); //Strand
+		expect(featureArrayOutput[10][1][4]).toEqual("maker"); //Source
+		expect(featureArrayOutput[10][1][5]).toEqual("."); //Phase
+		expect(featureArrayOutput[10][1][6]).toEqual("exon"); //Type
+		expect(featureArrayOutput[10][1][7]).toEqual("."); //Score
+		expect(featureArrayOutput[10][1][8]).toEqual("1:gnomon_566853_mRNA:exon:5977"); //Id
+		expect(featureArrayOutput[10][1][9]).toEqual(null); //Name
+	    });
+
+    });
+
diff --git a/test/client/selenium_tests/lib/WebApolloTest.py b/test/client/selenium_tests/lib/WebApolloTest.py
new file mode 100644
index 0000000..941bc13
--- /dev/null
+++ b/test/client/selenium_tests/lib/WebApolloTest.py
@@ -0,0 +1,35 @@
+import os
+import sys
+import time
+sys.path.append( 'tests/selenium_tests' ) # relative to JBrowse root
+
+from jbrowse_selenium import JBrowseTest
+
+class WebApolloTest (JBrowseTest):
+
+    wa_url = None
+
+    def setUp( self ):
+        super( WebApolloTest, self ).setUp()
+        self.login( os.environ['WA_USER'], os.environ['WA_PASS'] )
+        self.browser.get( self.baseURL() )
+
+    def baseURL( self ):
+        return self.waURL()+'/jbrowse/index.html'
+
+    def waURL( self ):
+        if not self.wa_url:
+            self.wa_url = os.environ['WA_URL']
+        return self.wa_url
+
+    def login( self, username, password ):
+        self.browser.get( self.waURL() + '/Login' );
+        username_input = self.assert_element('//input[@id="username"]')
+        password_input = self.assert_element('//input[@id="password"]')
+        username_input.send_keys(username)
+        password_input.send_keys(password)
+        login_button = self.assert_element('//button[@id="login_button"]')
+        login_button.click()
+        time.sleep( 0.4 )
+        pass
+
diff --git a/test/client/selenium_tests/lib/__init__.py b/test/client/selenium_tests/lib/__init__.py
new file mode 100644
index 0000000..101831e
--- /dev/null
+++ b/test/client/selenium_tests/lib/__init__.py
@@ -0,0 +1,2 @@
+from WebApolloTest import WebApolloTest
+
diff --git a/test/client/selenium_tests/pythium_test.py b/test/client/selenium_tests/pythium_test.py
new file mode 100644
index 0000000..fd7decd
--- /dev/null
+++ b/test/client/selenium_tests/pythium_test.py
@@ -0,0 +1,29 @@
+import unittest
+from lib import WebApolloTest;
+
+class PythiumTest(WebApolloTest, unittest.TestCase):
+
+    data_dir = 'sample_data/json/volvox'
+
+    def setUp( self ):
+        # call( "rm -rf sample_data/json/volvox/", shell=True )
+        # call( "bin/prepare-refseqs.pl --fasta docs/tutorial/data_files/volvox.fa --out sample_data/json/volvox/", shell=True )
+        # call( "bin/biodb-to-json.pl --conf docs/tutorial/conf_files/volvox.json --out sample_data/json/volvox/", shell=True )
+        # call( "bin/wig-to-json.pl --out sample_data/json/volvox/ --wig docs/tutorial/data_files/volvox_microarray.wig", shell=True )
+        # call( "bin/add-track-json.pl sample_data/raw/volvox/volvox_microarray.bw.conf sample_data/json/volvox/trackList.json", shell=True )
+        # call( "bin/add-track-json.pl sample_data/raw/volvox/volvox-sorted.bam.conf sample_data/json/volvox/trackList.json", shell=True )
+        # call( "bin/add-track-json.pl sample_data/raw/volvox/volvox-sorted.bam.coverage.conf sample_data/json/volvox/trackList.json", shell=True )
+        # call( "bin/generate-names.pl --dir sample_data/json/volvox/", shell=True )
+        super( PythiumTest, self ).setUp()
+
+
+    def test_pythium( self ):
+        test_ref = 'scf1117875582023';
+        # select "ctgA from the dropdown
+        self.select_refseq( test_ref )
+
+        # check a good browser title
+        assert test_ref in self.browser.title, "browser title is actually %s" % self.browser.title
+
+        self.browser.close()
+
diff --git a/test/config/mysql.travis b/test/config/mysql.travis
new file mode 100644
index 0000000..04f7eec
--- /dev/null
+++ b/test/config/mysql.travis
@@ -0,0 +1,21 @@
+
+environments {
+    test {
+        dataSource{
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "root"
+            driverClassName = "com.mysql.jdbc.Driver"
+            dialect = org.hibernate.dialect.MySQL5InnoDBDialect
+            url = "jdbc:mysql://localhost/apollo"
+
+           maxActive = 20
+           maxWait = 60000
+           testWhileIdle = true
+           maxAge = 10 * 600000
+           timeBetweenEvictionRunsMillis = 300000
+           minEvictableIdleTimeMillis = 300000
+           testOnBorrow = true
+           validationQuery = "SELECT 1"
+        }
+    }
+}
diff --git a/test/config/postgres.travis b/test/config/postgres.travis
new file mode 100644
index 0000000..d98e096
--- /dev/null
+++ b/test/config/postgres.travis
@@ -0,0 +1,18 @@
+environments {
+    test {
+        dataSource {
+            dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "postgres"
+            driverClassName = "org.postgresql.Driver"
+            dialect = "org.bbop.apollo.ImprovedPostgresDialect"
+            url = "jdbc:postgresql://localhost/apollo"
+        }
+        dataSource_chado {
+            dbCreate = "update" // one of 'create', 'create-drop', 'update', 'validate', ''
+            username = "postgres"
+            driverClassName = "org.postgresql.Driver"
+            dialect = org.hibernate.dialect.ImprovedPostgresDialect
+            url = "jdbc:postgresql://localhost/apollo_chado"
+        }
+    }
+}
diff --git a/test/integration/org/bbop/apollo/AbstractIntegrationSpec.groovy b/test/integration/org/bbop/apollo/AbstractIntegrationSpec.groovy
new file mode 100644
index 0000000..bfa2e09
--- /dev/null
+++ b/test/integration/org/bbop/apollo/AbstractIntegrationSpec.groovy
@@ -0,0 +1,78 @@
+package org.bbop.apollo
+
+import grails.test.spock.IntegrationSpec
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.crypto.hash.Sha256Hash
+import org.apache.shiro.subject.Subject
+import org.apache.shiro.util.ThreadContext
+import org.apache.shiro.web.session.mgt.DefaultWebSessionManager
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+/**
+ * Created by nathandunn on 11/4/15.
+ */
+class AbstractIntegrationSpec extends IntegrationSpec{
+
+    def shiroSecurityManager
+
+    String password = "testPass"
+    String passwordHash = new Sha256Hash(password).toHex()
+
+    def setup() {
+        setupDefaultUserOrg()
+    }
+
+    String getTestCredentials(String clientToken = "1231232"){
+        "\"${FeatureStringEnum.CLIENT_TOKEN.value}\":\"${clientToken}\",\"${FeatureStringEnum.USERNAME.value}\":\"test at test.com\","
+    }
+
+    def setupDefaultUserOrg(){
+        if(User.findByUsername('test at test.com')){
+            return
+        }
+
+        User testUser = new User(
+                username: 'test at test.com'
+                ,firstName: 'Bob'
+                ,lastName: 'Test'
+                ,passwordHash: passwordHash
+        ).save(insert: true,flush: true)
+        def adminRole = Role.findByName(UserService.ADMIN)
+        testUser.addToRoles(adminRole)
+        testUser.save()
+
+        shiroSecurityManager.sessionManager = new DefaultWebSessionManager()
+        ThreadContext.bind(shiroSecurityManager)
+        def authToken = new UsernamePasswordToken(testUser.username,password as String)
+        Subject subject = SecurityUtils.getSubject();
+        subject.login(authToken)
+
+        Organism organism = new Organism(
+                directory: "test/integration/resources/sequences/honeybee-Group1.10/"
+                ,commonName: "sampleAnimal"
+                ,genus: "Sample"
+                ,species: "animal"
+        ).save(failOnError: true)
+
+        Sequence sequence = new Sequence(
+                length: 1405242
+                ,seqChunkSize: 20000
+                ,start: 0
+                ,end: 1405242
+                ,organism: organism
+                ,name: "Group1.10"
+        ).save(failOnError: true)
+
+        organism.addToSequences(sequence)
+        organism.save(flush: true, failOnError: true)
+    }
+
+    JSONArray getCodingArray(JSONObject jsonObject) {
+        JSONArray mrnaArray = jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == mrnaArray.size()
+        return mrnaArray.getJSONObject(0).getJSONArray(FeatureStringEnum.CHILDREN.value)
+    }
+}
diff --git a/test/integration/org/bbop/apollo/CdsServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/CdsServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..6224ae3
--- /dev/null
+++ b/test/integration/org/bbop/apollo/CdsServiceIntegrationSpec.groovy
@@ -0,0 +1,68 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+class CdsServiceIntegrationSpec extends AbstractIntegrationSpec{
+    
+    def sequenceService
+    def requestHandlingService
+    def transcriptService
+
+
+    void "adding a gene model, a stop codon readthrough and getting its modified sequence"() {
+
+        given: "a gene model with 1 mRNA, 3 exons, and UTRs"
+        String jsonString = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":734606,\"strand\":1,\"fmax\":735570},\"name\":\"GB40828-RA\",\"children\":[{\"location\":{\"fmin\":734606,\"strand\":1,\"fmax\":734733},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":735446,\"strand\":1,\"fmax\":735570},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":734606,\"strand\":1,\"fmax\" [...]
+        "{ \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40722-RA\",\"children\":[{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}]}], \"operation\": \"add_tran [...]
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        when: "gene model is added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should see the appropriate model"
+        assert Sequence.count == 1
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert CDS.count == 1
+//        assert FeatureLocation.count == 6 + FlankingRegion.count
+        assert FeatureRelationship.count == 5
+
+        when: "a stopCodonReadThrough is created"
+        Transcript transcript = Transcript.findByName("GB40828-RA-00001")
+        CDS cds = transcriptService.getCDS(transcript)
+        String setReadThroughStopCodonString = "{ ${testCredentials} \"operation\":\"set_readthrough_stop_codon\",\"features\":[{\"readthrough_stop_codon\":true,\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"clientToken\":\"1231232\"}"
+        setReadThroughStopCodonString = setReadThroughStopCodonString.replace("@UNIQUENAME@", transcript.uniqueName)
+        JSONObject setReadThroughRequestObject = JSON.parse(setReadThroughStopCodonString) as JSONObject
+        JSONObject setReadThroughReturnObject = requestHandlingService.setReadthroughStopCodon(setReadThroughRequestObject)
+        println "${setReadThroughReturnObject.toString()}"
+        
+        then: "we have a StopCodonReadThrough feature"
+        assert StopCodonReadThrough.count == 1
+        
+        JSONArray childrenArray = setReadThroughReturnObject.features.children
+        for (def child : childrenArray) {
+            if (child['name'].contains("-CDS")) {
+                println child['children'].location.fmin
+                println child['children'].location.fmax
+                int size = (child['children'].location.fmax[0] - child['children'].location.fmin[0])
+                assert size == 3
+            }
+        }
+        
+        when: "a request is sent for the CDS sequence with the read through stop codon"
+        String getSequenceString = "{ ${testCredentials} \"operation\":\"get_sequence\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"type\":\"@SEQUENCE_TYPE@\"}"
+        String getCdsSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", transcript.uniqueName)
+        getCdsSequenceString = getCdsSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_CDS.value)
+        JSONObject commandObject = JSON.parse(getCdsSequenceString) as JSONObject
+        JSONObject getCDSSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+        
+        then: "we should get the anticipated CDS sequence"
+        assert getCDSSequenceReturnObject.residues != null
+        String expectedCdsSequence = "ATGGAATCTGCTATTGTTCATCTTGAACAAAGCGTGCAAAAGGCTGATGGAAAACTAGACATGATTGCATGGCAAATTGATGCTTTTGAAAAAGAATTTGAAGATCCTGGTAGTGAGATTTCTGTGCTTCGTCTATTACGGTCTGTTCATCAAGTCACAAAAGATTATCAGAACCTTCGGCAAGAAATATTGGAGGTTCAACAATTGCAAAAGCAACTTTCAGATTCCCTTAAAGCACAATTATCTCAAGTGCATGGACATTTTAACTTATTACGCAATAAAATAGTAGGACAAAATAAAAATCTACAATTAAAATAAGATTAA"
+        assert getCDSSequenceReturnObject.residues == expectedCdsSequence
+    }
+}
diff --git a/test/integration/org/bbop/apollo/ChadoHandlerServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/ChadoHandlerServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..1ac611f
--- /dev/null
+++ b/test/integration/org/bbop/apollo/ChadoHandlerServiceIntegrationSpec.groovy
@@ -0,0 +1,358 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+class ChadoHandlerServiceIntegrationSpec extends AbstractIntegrationSpec{
+
+    // NOTE: This is set to prevent rollback at the end of the integration test
+    // for the sake of visual inspection of the database at the end of the test.
+    //static transactional = false
+
+    def chadoHandlerService
+    def requestHandlingService
+    def featureRelationshipService
+    def configWrapperService
+
+    void "test CHADO export for standard annotations"() {
+        /*
+        Standard annotations signifies no modifications/attributes added to the annotations
+         */
+        if (! configWrapperService.isPostgresChadoDataSource()) {
+            log.debug "Skipping test as the currently specified Chado data source is not PostgreSQL."
+            return
+        }
+
+        given: "series of different types of annotations"
+        String gene1transcript1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":136964},\"name\":\"GB40797-RA\",\"children\":[{\"location\":{\"fmin\":136502,\"strand\":1,\"fmax\":136964},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":128768},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":131040,\"strand\ [...]
+        String gene1transcript2String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":136964},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_31_mRNA\",\"children\":[{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":128768},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":131040,\"strand\":1,\"fmax\":131220},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location [...]
+
+        String gene2transcript1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":252004,\"strand\":-1,\"fmax\":255153},\"name\":\"GB40770-RA\",\"children\":[{\"location\":{\"fmin\":255124,\"strand\":-1,\"fmax\":255153},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":252400,\"strand\":-1,\"fmax\":252408},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":252004,\"stra [...]
+        String gene2transcript2String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":252004,\"strand\":-1,\"fmax\":255153},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_62_mRNA\",\"children\":[{\"location\":{\"fmin\":252004,\"strand\":-1,\"fmax\":252303},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":252400,\"strand\":-1,\"fmax\":252462},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"locat [...]
+
+        String pseudogeneString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":747699,\"strand\":1,\"fmax\":747966},\"children\":[{\"location\":{\"fmin\":747699,\"strand\":1,\"fmax\":747966},\"children\":[{\"location\":{\"fmin\":747699,\"strand\":1,\"fmax\":747760},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":747822,\"strand\":1,\"fmax\":747894},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}} [...]
+        String tRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":775035,\"strand\":-1,\"fmax\":775413},\"children\":[{\"location\":{\"fmin\":775035,\"strand\":-1,\"fmax\":775413},\"children\":[{\"location\":{\"fmin\":775035,\"strand\":-1,\"fmax\":775185},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":775344,\"strand\":-1,\"fmax\":775413},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}, [...]
+        String snRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":278556,\"strand\":1,\"fmax\":281052},\"children\":[{\"location\":{\"fmin\":278556,\"strand\":1,\"fmax\":281052},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_67_mRNA\",\"children\":[{\"location\":{\"fmin\":278556,\"strand\":1,\"fmax\":278569},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":280446,\"strand\":1,\"fmax\":280615},\"type\":{\ [...]
+        String snoRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":433518,\"strand\":1,\"fmax\":437436},\"children\":[{\"location\":{\"fmin\":433518,\"strand\":1,\"fmax\":437436},\"children\":[{\"location\":{\"fmin\":433518,\"strand\":1,\"fmax\":433570},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":436576,\"strand\":1,\"fmax\":436641},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\ [...]
+        String ncRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":598161,\"strand\":-1,\"fmax\":598924},\"children\":[{\"location\":{\"fmin\":598161,\"strand\":-1,\"fmax\":598924},\"children\":[{\"location\":{\"fmin\":598161,\"strand\":-1,\"fmax\":598280},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":598782,\"strand\":-1,\"fmax\":598924},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}} [...]
+        String rRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":664614,\"strand\":-1,\"fmax\":665729},\"children\":[{\"location\":{\"fmin\":664614,\"strand\":-1,\"fmax\":665729},\"children\":[{\"location\":{\"fmin\":664614,\"strand\":-1,\"fmax\":664637},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":665671,\"strand\":-1,\"fmax\":665729},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}, [...]
+        String miRNAString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":675719,\"strand\":-1,\"fmax\":680586},\"children\":[{\"location\":{\"fmin\":675719,\"strand\":-1,\"fmax\":680586},\"children\":[{\"location\":{\"fmin\":675719,\"strand\":-1,\"fmax\":676397},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":675719,\"strand\":-1,\"fmax\":676397},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}} [...]
+        String repeatRegionString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":654601,\"strand\":0,\"fmax\":657144},\"type\":{\"name\":\"repeat_region\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String transposableElementString = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":621650,\"strand\":0,\"fmax\":628275},\"type\":{\"name\":\"transposable_element\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        String insertionString = "{ ${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"ATCG\",\"location\":{\"fmin\":689758,\"strand\":1,\"fmax\":689758},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String deletionString = "{ ${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":689725,\"strand\":1,\"fmax\":689735},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String substitutionString = "{ ${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"CCC\",\"location\":{\"fmin\":689699,\"strand\":1,\"fmax\":689702},\"type\":{\"name\":\"substitution\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        when: "we add all these annotations"
+        requestHandlingService.addTranscript(JSON.parse(gene1transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(gene1transcript2String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(gene2transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(gene2transcript2String) as JSONObject)
+
+        requestHandlingService.addFeature(JSON.parse(pseudogeneString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(tRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(snRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(snoRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(ncRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(rRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(miRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(repeatRegionString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(transposableElementString) as JSONObject)
+
+        requestHandlingService.addSequenceAlteration(JSON.parse(insertionString) as JSONObject)
+        requestHandlingService.addSequenceAlteration(JSON.parse(deletionString) as JSONObject)
+        requestHandlingService.addSequenceAlteration(JSON.parse(substitutionString) as JSONObject)
+
+        then: "we should see 9 genes and 1 repeat region, 1 transposable element and 3 sequence alterations"
+        assert Gene.count == 9
+        assert RepeatRegion.count == 1
+        assert TransposableElement.count == 1
+        assert SequenceAlteration.count == 3
+
+        when: "we try to export these annotations as Chado"
+        def features = []
+        Feature.all.each {
+            if (!it.childFeatureRelationships) {
+                // fetching only top-level features
+                features.add(it)
+            }
+        }
+        log.debug "${features}"
+        chadoHandlerService.writeFeatures(Organism.findByCommonName("sampleAnimal"), Sequence.all, features)
+
+
+        then: "we should see the exported annotations in Chado data source"
+        assert org.gmod.chado.Organism.count == 1
+        assert org.gmod.chado.Feature.count > 0
+        assert org.gmod.chado.Featureloc.count > 0
+        assert org.gmod.chado.FeatureRelationship.count > 0
+    }
+
+    void "test CHADO export for annotations with additional information"() {
+
+        if (! configWrapperService.isPostgresChadoDataSource()) {
+            log.debug "Skipping test as the currently specified Chado data source is not PostgreSQL."
+            return
+        }
+
+        given: "series of different types of annotations"
+        String gene1transcript1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":102008,\"strand\":1,\"fmax\":111044},\"name\":\"GB40794-RA\",\"children\":[{\"location\":{\"fmin\":102008,\"strand\":1,\"fmax\":102355},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":107137,\"strand\":1,\"fmax\":111044},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":102008,\"strand\ [...]
+        String gene1transcript2String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":102008,\"strand\":1,\"fmax\":111044},\"name\":\"5:geneid_mRNA_CM000054.5_411\",\"children\":[{\"location\":{\"fmin\":102008,\"strand\":1,\"fmax\":102410},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":104075,\"strand\":1,\"fmax\":104390},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\ [...]
+
+        String addNonReservedPropertyString = "{ ${testCredentials} \"operation\":\"add_non_reserved_properties\",\"features\":[{\"non_reserved_properties\":[{\"tag\":\"@TAG@\",\"value\":\"@VALUE@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonPrimaryDbxrefString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"@DB@\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addPublicationString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"PMID\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addGeneOntologyString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"GO\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addCommentString = "{ ${testCredentials} \"operation\":\"add_comments\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\"}"
+        String addSymbolString = "{ ${testCredentials} \"operation\":\"set_symbol\",\"features\":[{\"symbol\":\"@SYMBOL@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addDescriptionString = "{ ${testCredentials} \"operation\":\"set_description\",\"features\":[{\"description\":\"@DESCRIPTION@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+
+        when: "we add all the annotations"
+        requestHandlingService.addTranscript(JSON.parse(gene1transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(gene1transcript2String) as JSONObject)
+
+        then: "we should see genes and mRNAs"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+
+
+        Gene gene1 = Gene.findByName("GB40794-RA")
+        MRNA mrna1 = MRNA.findByName("GB40794-RA-00001")
+
+        when: "we add dbXrefs"
+        String gene1DbxrefString = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "1312415")
+        String transcript1DbxrefString1 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "XM2131231.1")
+        String transcript1DbxrefString2 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSBTAT000000123")
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(gene1DbxrefString) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(transcript1DbxrefString1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(transcript1DbxrefString2) as JSONObject)
+
+        then: "we should see the dbxrefs"
+        DBXref.count == 3
+
+        when: "we add properties"
+        String gene1PropertyString1 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "256.7")
+        String gene1PropertyString2 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "9302")
+        String transcript1PropertyString1 = addNonReservedPropertyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "11.2")
+        String transcript1PropertyString2 = addNonReservedPropertyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "42")
+
+        requestHandlingService.addNonReservedProperties(JSON.parse(gene1PropertyString1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(gene1PropertyString2) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(transcript1PropertyString1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(transcript1PropertyString2) as JSONObject)
+
+        then: "we should see 3 feature properties"
+        FeatureProperty.count == 4
+
+        when: "we add publications"
+        String gene1PublicationString = addPublicationString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@ACCESSION@", "8324934")
+        String transcript1PublicationString = addPublicationString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@ACCESSION@", "798424")
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(gene1PublicationString) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(transcript1PublicationString) as JSONObject)
+
+        then: "we should see publications"
+        // Unfortunately Apollo puts publications into DBxrefs
+        DBXref.count == 5
+        Publication.count == 0
+
+        when: "we add GO"
+        String gene1GeneOntologyString = addGeneOntologyString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@ACCESSION@", "0015755")
+        String transcript1GeneOntologyString1 = addGeneOntologyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@ACCESSION@", "0015755")
+        String transcript1GeneOntologyString2 = addGeneOntologyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@ACCESSION@", "0030393")
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(gene1GeneOntologyString) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(transcript1GeneOntologyString1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(transcript1GeneOntologyString2) as JSONObject)
+
+        then: "we should see GO in DBxref"
+        assert DBXref.count == 7
+
+        when: "we add comments"
+        String gene1CommentString = addCommentString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@COMMENT@", "Comment for gene")
+        String transcript1CommentString = addCommentString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@COMMENT@", "Comment for transcript isoform")
+
+        requestHandlingService.addComments(JSON.parse(gene1CommentString) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(transcript1CommentString) as JSONObject)
+
+        then: "we should see comments"
+        assert FeatureProperty.count == 6
+
+        when: "we add symbols"
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@SYMBOL@", "GN1S")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@DESCRIPTION@", "GN1S gene for test")) as JSONObject)
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@SYMBOL@", "GN1S-RA")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@DESCRIPTION@", "GN1S isoform 1")) as JSONObject)
+
+        then: "we should see the symbol and description attribute set"
+        assert gene1.symbol == "GN1S"
+        assert gene1.description == "GN1S gene for test"
+        assert mrna1.symbol == "GN1S-RA"
+        assert mrna1.description == "GN1S isoform 1"
+
+        when: "we try Chado export"
+        def features = []
+        Feature.all.each {
+            if (!it.childFeatureRelationships) {
+                // fetching only top-level features
+                features.add(it)
+            }
+        }
+        chadoHandlerService.writeFeatures(Organism.findByCommonName("sampleAnimal"), Sequence.all, features)
+
+        then: "we should see features in Chado data source"
+        assert org.gmod.chado.Feature.count > 0
+        assert org.gmod.chado.Featureloc.count > 0
+        assert org.gmod.chado.Featureprop.count > 0
+        assert org.gmod.chado.FeatureRelationship.count > 0
+
+    }
+
+    void "test CHADO export and re-export"() {
+
+        if (! configWrapperService.isPostgresChadoDataSource()) {
+            log.debug "Skipping test as the currently specified Chado data source is not PostgreSQL."
+            return
+        }
+
+        given: "a set of annotations"
+        String addTranscriptString1 = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":836988},\"name\":\"GB40740-RA\",\"children\":[{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":787740},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":788349},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":789768,\"strand [...]
+        String addTranscriptString2 = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":845782,\"strand\":-1,\"fmax\":847278},\"name\":\"GB40739-RA\",\"children\":[{\"location\":{\"fmin\":845782,\"strand\":-1,\"fmax\":845798},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":847144,\"strand\":-1,\"fmax\":847278},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":845782,\"strand [...]
+        String addFeatureString1 = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":772054,\"strand\":1,\"fmax\":776061},\"children\":[{\"location\":{\"fmin\":772054,\"strand\":1,\"fmax\":776061},\"children\":[{\"location\":{\"fmin\":775816,\"strand\":1,\"fmax\":776061},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":772054,\"strand\":1,\"fmax\":772102},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"} [...]
+        String addFeatureString2 = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequenc [...]
+        String addFeatureString3 = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":747699,\"strand\":0,\"fmax\":747966},\"type\":{\"name\":\"repeat_region\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addFeatureString4 = "{ ${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":734606,\"strand\":0,\"fmax\":735570},\"type\":{\"name\":\"transposable_element\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addTranscriptString3 = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":891279,\"strand\":-1,\"fmax\":933037},\"name\":\"GB40737-RA\",\"children\":[{\"location\":{\"fmin\":891279,\"strand\":-1,\"fmax\":891532},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":893536,\"strand\":-1,\"fmax\":893577},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":931384,\"strand [...]
+
+        String addNonReservedPropertyString = "{ ${testCredentials} \"operation\":\"add_non_reserved_properties\",\"features\":[{\"non_reserved_properties\":[{\"tag\":\"@TAG@\",\"value\":\"@VALUE@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonPrimaryDbxrefString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"@DB@\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addPublicationString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"PMID\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addGeneOntologyString = "{ ${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"GO\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addCommentString = "{ ${testCredentials} \"operation\":\"add_comments\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\"}"
+        String addSymbolString = "{ ${testCredentials} \"operation\":\"set_symbol\",\"features\":[{\"symbol\":\"@SYMBOL@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addDescriptionString = "{ ${testCredentials} \"operation\":\"set_description\",\"features\":[{\"description\":\"@DESCRIPTION@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+
+        when: "we add all the annotations"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString1) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString2) as JSONObject)
+
+        requestHandlingService.addFeature(JSON.parse(addFeatureString1) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addFeatureString2) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addFeatureString3) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addFeatureString4) as JSONObject)
+
+        then: "we should see 4 genes, 1 repeat region and 1 transposable element"
+        assert Gene.count == 4
+        assert RepeatRegion.count == 1
+        assert TransposableElement.count == 1
+        Gene gene1 = Gene.findByName("GB40740-RA")
+        MRNA mrna1 = MRNA.findByName("GB40740-RA-00001")
+        String gene1UniqueName = gene1.uniqueName
+        String mrna1UniqueName = mrna1.uniqueName
+        Gene gene2 = Gene.findByName("GB40739-RA")
+        MRNA mrna2 = MRNA.findByName("GB40739-RA-00001")
+
+        when: "we add metadata"
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@SYMBOL@", "GN1A")) as JSONObject)
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", gene2.uniqueName).replace("@SYMBOL@", "GN1B")) as JSONObject)
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@SYMBOL@", "GN1A isoform 1")) as JSONObject)
+        requestHandlingService.setSymbol(JSON.parse(addSymbolString.replace("@UNIQUENAME@", mrna2.uniqueName).replace("@SYMBOL@", "GN1B isoform 1")) as JSONObject)
+
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@DESCRIPTION@", "Test gene GN1A")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", gene2.uniqueName).replace("@DESCRIPTION@", "Test gene GN1B")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@DESCRIPTION@", "Test transcript isoform GN1A")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(addDescriptionString.replace("@UNIQUENAME@", mrna2.uniqueName).replace("@DESCRIPTION@", "Test transcript isoform GN1B")) as JSONObject)
+
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@COMMENT@", "Gene GN1A is created for test purposes")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", gene2.uniqueName).replace("@COMMENT@", "Gene GN1B is created for test purposes")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@COMMENT@", "Transcript GN1A isoform 1 is created for test purposes")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", mrna2.uniqueName).replace("@COMMENT@", "Transcript GN1B isoform 1 is created for test purposes")) as JSONObject)
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "1312415")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene2.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "3235223")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSG00000000012")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene2.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSG00000000014")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENST0000000032521")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", mrna2.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENST00000000325")) as JSONObject)
+
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@TAG@", "validated").replace("@VALUE@", "false")) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@TAG@", "validated").replace("@VALUE@", "false")) as JSONObject)
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addPublicationString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@ACCESSION@", "8324934")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addPublicationString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@ACCESSION@", "798424")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addGeneOntologyString.replace("@UNIQUENAME@", gene1.uniqueName).replace("@ACCESSION@", "0015755")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addGeneOntologyString.replace("@UNIQUENAME@", mrna1.uniqueName).replace("@ACCESSION@", "0030393")) as JSONObject)
+
+        then: "we should see these metadata"
+        gene1.refresh()
+        gene2.refresh()
+        assert gene1.symbol != null
+        assert gene1.description != null
+        assert gene2.symbol != null
+        assert gene2.description != null
+        assert gene1.featureProperties.size() != 0
+        assert gene2.featureProperties.size() != 0
+        assert gene1.featureDBXrefs.size() != 0
+        assert gene2.featureDBXrefs.size() != 0
+
+        when: "we do a Chado export"
+        def features = []
+        Feature.all.each {
+            if (!it.childFeatureRelationships) {
+                features.add(it)
+            }
+        }
+        chadoHandlerService.writeFeatures(Organism.findByCommonName("sampleAnimal"), Sequence.all, features)
+
+        then: "we should see the annotations in Chado data source"
+        assert org.gmod.chado.Feature.count > 0
+        assert org.gmod.chado.Featureloc.count > 0
+        assert org.gmod.chado.Featureprop.count > 0
+        assert org.gmod.chado.FeatureDbxref.count > 0
+        assert org.gmod.chado.FeatureRelationship.count > 0
+        assert org.gmod.chado.Feature.findByUniquename(gene1.uniqueName) != null
+        assert org.gmod.chado.Feature.findByUniquename(gene2.uniqueName) != null
+
+
+        when: "we add GB40737-RA and re-export to Chado"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString3) as JSONObject)
+        Gene gene3 = Gene.findByName("GB40737-RA")
+
+        features = []
+        Feature.all.each {
+            if (!it.childFeatureRelationships) {
+                features.add(it)
+            }
+        }
+
+        chadoHandlerService.writeFeatures(Organism.findByCommonName("sampleAnimal"), Sequence.all, features)
+
+        then: "we should find GB40737-RA in the Chado data source"
+        assert org.gmod.chado.Feature.findByUniquename(gene3.uniqueName) != null
+
+        when: "we delete an annotation and do another Chado export"
+        gene1.refresh()
+        featureRelationshipService.deleteFeatureAndChildren(gene1)
+        features = []
+        Feature.all.each {
+            if (!it.childFeatureRelationships) {
+                features.add(it)
+            }
+        }
+
+        chadoHandlerService.writeFeatures(Organism.findByCommonName("sampleAnimal"), Sequence.all, features)
+
+        then: "GB40740-RA, its dbxrefs and feature properties should not exist in Chado"
+        assert org.gmod.chado.Feature.findByUniquename(gene1UniqueName) == null
+        assert org.gmod.chado.Feature.findByUniquename(mrna1UniqueName) == null
+        assert org.gmod.chado.Dbxref.findByAccession("1312415") == null
+        assert org.gmod.chado.Dbxref.findByAccession("ENSG00000000012") == null
+        assert org.gmod.chado.Featureprop.findByValue("Gene GN1A is created for test purposes") == null
+
+        // and that GO terms are not deleted as a result
+        assert org.gmod.chado.Dbxref.findByAccession("0015755") != null
+        assert org.gmod.chado.Dbxref.findByAccession("0030393") != null
+    }
+}
diff --git a/test/integration/org/bbop/apollo/ExonServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/ExonServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..a821745
--- /dev/null
+++ b/test/integration/org/bbop/apollo/ExonServiceIntegrationSpec.groovy
@@ -0,0 +1,97 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.sequence.Strand
+
+class ExonServiceIntegrationSpec extends AbstractIntegrationSpec{
+    
+    def exonService
+
+    def setup() {
+        setupDefaultUserOrg()
+        Sequence sequence = new Sequence(
+                length: 300000
+                ,seqChunkSize: 3
+                ,start: 5
+                ,end: 8
+                ,name: "Group1.10"
+        ).save()
+    }
+
+    void "merge to exons for a transcript"() {
+
+        given: "we have 2 exons attached to the same transcript"
+        Exon leftExon = new Exon(name: "left",uniqueName: "left").save()
+        FeatureLocation leftFeatureLocation = new FeatureLocation(
+                fmin: 5
+                ,fmax: 10
+                ,feature: leftExon
+                ,sequence: Sequence.first()
+                ,strand: Strand.POSITIVE.value
+        ).save()
+        leftExon.addToFeatureLocations(leftFeatureLocation)
+        Exon rightExon = new Exon(name: "right",uniqueName: "right").save()
+        FeatureLocation rightFeatureLocation = new FeatureLocation(
+                fmin: 15
+                ,fmax: 20
+                ,feature: rightExon
+                ,sequence: Sequence.first()
+                ,strand: Strand.POSITIVE.value
+        ).save()
+        rightExon.addToFeatureLocations(rightFeatureLocation)
+        MRNA mrna = new MRNA(name: "mrna",uniqueName: "mrna").save()
+        FeatureLocation transcriptFeatureLocation = new FeatureLocation(
+                fmin: 2
+                ,fmax: 25
+                ,feature: mrna
+                ,sequence: Sequence.first()
+                ,strand: Strand.POSITIVE.value
+        ).save()
+        mrna.addToFeatureLocations(transcriptFeatureLocation)
+        FeatureRelationship leftExonFeatureRelationship = new FeatureRelationship(
+                parentFeature: mrna
+                ,childFeature: leftExon
+        ).save()
+        FeatureRelationship rightExonFeatureRelationship = new FeatureRelationship(
+                parentFeature: mrna
+                ,childFeature: rightExon
+        ).save()
+
+        when: "we add the proper relationships"
+        mrna.addToParentFeatureRelationships(leftExonFeatureRelationship)
+        leftExon.addToChildFeatureRelationships(leftExonFeatureRelationship)
+        mrna.addToParentFeatureRelationships(rightExonFeatureRelationship)
+        rightExon.addToChildFeatureRelationships(rightExonFeatureRelationship)
+
+
+        then: "everything is properly saved"
+        Exon.count ==2
+        MRNA.count ==1
+        FeatureLocation.count == 3
+        FeatureRelationship.count == 2
+        mrna.parentFeatureRelationships.size()==2
+        leftExon.childFeatureRelationships.size()==1
+        rightExon.childFeatureRelationships.size()==1
+        Exon.findByName("left").featureLocation.fmin==5
+        Exon.findByName("right").featureLocation.fmin==15
+        MRNA.findByName("mrna").featureLocation.fmin==2
+        assert "mrna"==exonService.getTranscript(leftExon).name
+        assert "mrna"==exonService.getTranscript(rightExon).name
+
+
+        when: "we delete an exon2"
+        exonService.deleteExon(mrna,rightExon)
+        
+        then: "there should be only one exon left"
+        assert Exon.count==1
+        assert FeatureRelationship.count==1
+        assert mrna.parentFeatureRelationships.size()==1
+        
+//        when: "we merge the exons we should still have 2"
+//        exonService.mergeExons(leftExon,rightExon)
+//
+//        then: "we should still have two exons has they don't overlap"
+//        Exon.count==1
+//        assert 0==1
+
+    }
+}
diff --git a/test/integration/org/bbop/apollo/FastaHandlerServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/FastaHandlerServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..947000d
--- /dev/null
+++ b/test/integration/org/bbop/apollo/FastaHandlerServiceIntegrationSpec.groovy
@@ -0,0 +1,48 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+
+class FastaHandlerServiceIntegrationSpec extends AbstractIntegrationSpec{
+
+    def requestHandlingService 
+    def fastaHandlerService
+
+    void "write a fasta of a simple gene model"() {
+        given: "we create a new gene"
+        String json=" { ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+
+        when: "we parse the json"
+        requestHandlingService.addTranscript(JSON.parse(json))
+
+
+        then: "We should have at least one new gene"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert CDS.count == 1
+
+        
+
+
+        when: "we write the feature to test"
+        File tempFile = File.createTempFile("output", ".gff3")
+        tempFile.deleteOnExit()
+        log.debug "${tempFile.absolutePath}"
+        fastaHandlerService.writeFeatures(Gene.findAll(), FeatureStringEnum.TYPE_PEPTIDE.value, ["name"] as Set, tempFile.path, FastaHandlerService.Mode.WRITE, FastaHandlerService.Format.TEXT)
+        String tempFileText = tempFile.text
+
+        then: "we should get a valid fasta file"
+        assert tempFileText.length() > 0
+        log.debug "${tempFileText}"
+        def residues=""
+        def lines = tempFile.readLines().each { line->
+            if(line.indexOf(">")!=0) {
+                residues+=line
+            }
+        }
+        assert residues=="MARDIHRQSLRTEQPSGLDTGGVRFELSRALDLWARNSKLTFQEVNSDRADILVYFHRGYHGDGYPFDGRGQILAHAFFPGRDRGGDVHFDEEEIWLLQGDNNEEGTSLFAVAAHEFGHSLGLAHSSVPGALMYPWYQGLSSNYELPEDDRHGIQQMYEINQDIFFFIFFSHD"
+        assert lines[0].indexOf("GB40856-RA-00001")!=-1
+    }
+
+}
diff --git a/test/integration/org/bbop/apollo/FeatureEventServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/FeatureEventServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..d0fbab4
--- /dev/null
+++ b/test/integration/org/bbop/apollo/FeatureEventServiceIntegrationSpec.groovy
@@ -0,0 +1,1454 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.history.FeatureOperation
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONException
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+class FeatureEventServiceIntegrationSpec extends AbstractIntegrationSpec {
+
+    def requestHandlingService
+    def transcriptService
+    def featureEventService
+
+    protected JSONObject createJSONFeatureContainer(JSONObject... features) throws JSONException {
+        JSONObject jsonFeatureContainer = new JSONObject();
+        JSONArray jsonFeatures = new JSONArray();
+        jsonFeatureContainer.put(FeatureStringEnum.FEATURES.value, jsonFeatures);
+        for (JSONObject feature : features) {
+            jsonFeatures.put(feature);
+        }
+        return jsonFeatureContainer;
+    }
+
+    def setup() {
+        FeatureEvent.deleteAll(FeatureEvent.all)
+        Feature.deleteAll(Feature.all)
+        setupDefaultUserOrg()
+    }
+
+    def cleanup() {
+        FeatureEvent.deleteAll(FeatureEvent.all)
+        Feature.deleteAll(Feature.all)
+    }
+
+    void "we can undo and redo a transcript split"() {
+
+        given: "transcript data"
+        String jsonString = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":938708,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String undoString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String redoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"redo\", \"count\": 1}"
+
+        when: "we insert a transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(JSON.parse(jsonString) as JSONObject)
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+
+        when: "we split the transcript"
+        String exon1UniqueName = Exon.all[0].uniqueName
+        String exon2UniqueName = Exon.all[1].uniqueName
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+
+        when: "when we undo transcript A"
+        String transcript1UniqueName = MRNA.findByName("GB40736-RA-00001").uniqueName
+        String transcript2UniqueName = MRNA.findByName("GB40736-RAa-00001").uniqueName
+        undoString1 = undoString1.replace("@TRANSCRIPT_1@", transcript1UniqueName)
+        undoString2 = undoString2.replace("@TRANSCRIPT_2@", transcript2UniqueName)
+        redoString1 = redoString1.replace("@TRANSCRIPT_1@", transcript1UniqueName)
+        requestHandlingService.undo(JSON.parse(undoString1))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we redo transcript"
+        requestHandlingService.redo(JSON.parse(redoString1))
+        def allFeatures = Feature.all
+
+        then: "we should have two transcripts"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+
+        when: "when we undo transcript B"
+        requestHandlingService.undo(JSON.parse(undoString2))
+        def allFeatureEvents = FeatureEvent.all
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we redo transcript 1"
+        requestHandlingService.redo(JSON.parse(redoString1))
+
+        then: "we should have two transcripts, A3/B2"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+    }
+
+    void "we can undo a split twice"() {
+
+        given: "transcript data"
+        String jsonString = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":938708,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"undo\", \"count\": 1}"
+
+        when: "we insert a transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(JSON.parse(jsonString) as JSONObject)
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+
+        when: "we split the transcript"
+        String exon1UniqueName = Exon.all[0].uniqueName
+        String exon2UniqueName = Exon.all[1].uniqueName
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+
+        when: "when we undo transcript A"
+        String transcript1UniqueName = MRNA.findByName("GB40736-RA-00001").uniqueName
+        String transcript2UniqueName = MRNA.findByName("GB40736-RAa-00001").uniqueName
+        undoString1 = undoString1.replace("@TRANSCRIPT_1@", transcript1UniqueName)
+        requestHandlingService.undo(JSON.parse(undoString1))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we undo the same transcript again"
+        def allFeatures = Feature.all
+        requestHandlingService.undo(JSON.parse(undoString1))
+        allFeatures = Feature.all
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+    }
+
+    void "we can undo and redo a split"() {
+
+        given: "transcript data"
+        String jsonString = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":938708,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String redoString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"redo\", \"count\": 1}"
+
+        when: "we insert a transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(JSON.parse(jsonString) as JSONObject)
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+
+        when: "we split the transcript"
+        String exon1UniqueName = Exon.all[0].uniqueName
+        String exon2UniqueName = Exon.all[1].uniqueName
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+
+        when: "when we undo transcript A"
+        String transcript1UniqueName = MRNA.findByName("GB40736-RA-00001").uniqueName
+        String transcript2UniqueName = MRNA.findByName("GB40736-RAa-00001").uniqueName
+        undoString1 = undoString1.replace("@TRANSCRIPT_1@", transcript1UniqueName)
+        requestHandlingService.undo(JSON.parse(undoString1))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we undo the same transcript again"
+        def allFeatures = Feature.all
+        redoString2 = redoString2.replace("@TRANSCRIPT_2@", transcript2UniqueName)
+        requestHandlingService.redo(JSON.parse(redoString2))
+        allFeatures = Feature.all
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+    }
+
+    void "we can undo and redo and redo other side"() {
+
+        given: "transcript data"
+        String jsonString = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":938708,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String undoString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String redoString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"redo\", \"count\": 1}"
+
+        when: "we insert a transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(JSON.parse(jsonString) as JSONObject)
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+
+        when: "we split the transcript"
+        String exon1UniqueName = Exon.all[0].uniqueName
+        String exon2UniqueName = Exon.all[1].uniqueName
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+
+
+        when: "when we undo transcript A"
+        String transcript1UniqueName = MRNA.findByName("GB40736-RA-00001").uniqueName
+        String transcript2UniqueName = MRNA.findByName("GB40736-RAa-00001").uniqueName
+        undoString1 = undoString1.replace("@TRANSCRIPT_1@", transcript1UniqueName)
+        undoString2 = undoString2.replace("@TRANSCRIPT_2@", transcript2UniqueName)
+        redoString2 = redoString2.replace("@TRANSCRIPT_2@", transcript2UniqueName)
+        requestHandlingService.undo(JSON.parse(undoString1))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we redo transcript 2"
+        def allFeatures = Feature.all
+        requestHandlingService.redo(JSON.parse(redoString2))
+        allFeatures = Feature.all
+
+        then: "we should have two transcripts, A3/B1"
+        assert Exon.count == 2
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+
+        when: "when we undo transcript A"
+        requestHandlingService.undo(JSON.parse(undoString2))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+        when: "when we redo transcript 2 again"
+        requestHandlingService.redo(JSON.parse(redoString2))
+
+        then: "we shuld have A3/B2"
+        assert Exon.count == 2
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+
+    }
+
+    void "we can undo and redo a merge transcript"() {
+
+        given: "transcript data"
+        String addTranscriptString1 = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":938708,\"fmax\":938770,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        String addTranscriptString2 = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40736-RA\",\"children\":[{\"location\":{\"fmin\":939570,\"fmax\":939601,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        String mergeString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" }, { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"merge_transcripts\" }"
+        String undoString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"undo\", \"count\": 1}"
+        String redoString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_1@\" } ], \"operation\": \"redo\", \"count\": 1}"
+        String redoString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_2@\" } ], \"operation\": \"redo\", \"count\": 1}"
+
+        when: "we insert two transcripts"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString1))
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString2))
+
+        then: "we have a transcript"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+        assert FeatureEvent.count == 2
+        def mrnas = MRNA.all.sort() { a, b -> a.name <=> b.name }
+        assert mrnas[0].name == "GB40736-RA-00001"
+        assert mrnas[1].name == "GB40736-RAa-00001"
+
+
+        when: "we merge the transcript"
+        def allFeatures = Feature.all
+        String transcript1UniqueName = mrnas[0].uniqueName
+        String transcript2UniqueName = mrnas[1].uniqueName
+        mergeString = mergeString.replaceAll("@TRANSCRIPT_1@", transcript1UniqueName)
+        mergeString = mergeString.replaceAll("@TRANSCRIPT_2@", transcript2UniqueName)
+        redoString1 = redoString1.replaceAll("@TRANSCRIPT_1@", transcript1UniqueName)
+        redoString2 = redoString2.replaceAll("@TRANSCRIPT_2@", transcript2UniqueName)
+        JSONObject mergeJsonObject = requestHandlingService.mergeTranscripts(JSON.parse(mergeString))
+        FeatureEvent currentFeatureEvent = FeatureEvent.findByCurrent(true)
+        undoString = undoString.replaceAll("@TRANSCRIPT_1@", currentFeatureEvent.uniqueName)
+        allFeatures = Feature.all
+
+        then: "we should have two of everything now"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        assert FeatureEvent.count == 3
+        assert FeatureEvent.countByCurrent(true) == 1
+        assert FeatureEvent.findByCurrent(true).operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        when: "when we undo transcript A"
+        def allFeatureEvents = FeatureEvent.all
+        requestHandlingService.undo(JSON.parse(undoString))
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+        assert FeatureEvent.count == 3
+
+        when: "when we redo transcript on 1"
+        requestHandlingService.redo(JSON.parse(redoString1))
+        allFeatures = Feature.all
+
+        then: "we should have two transcripts"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        assert FeatureEvent.count == 3
+
+
+        when: "when we undo transcript B"
+        requestHandlingService.undo(JSON.parse(undoString))
+        allFeatures = Feature.all
+        allFeatureEvents = FeatureEvent.all
+
+        then: "we should have the original transcript"
+        assert Exon.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Gene.count == 2
+        assert FeatureEvent.count == 3
+
+        when: "when we redo transcript on 2"
+        requestHandlingService.redo(JSON.parse(redoString2))
+
+        then: "we should have two transcripts"
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+
+    }
+
+    /**
+     * https://github.com/GMOD/Apollo/issues/792
+     */
+    void "should handle merge, change on upstream / RHS gene, undo, redo"() {
+
+        given: "two transcripts"
+        // gene 1 - GB40787
+        Integer allFmin = 75270
+        Integer oldFmax = 75367
+        Integer newFmax = 75562
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String setExonBoundaryCommand = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":${allFmin},\"fmax\":${newFmax}}}],\"operation\":\"set_exon_boundaries\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+
+        when: "we add both transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        List<Exon> exonList = transcriptService.getSortedExons(MRNA.findByName("GB40788-RA-00001"), true)
+        String exonUniqueName = exonList.first().uniqueName
+        Exon exon = Exon.findByUniqueName(exonUniqueName)
+        FeatureLocation featureLocation = exon.featureLocation
+
+
+        then: "we should see 2 genes, 2 transcripts, 5 exons, 2 CDS, no noncanonical splice sites"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert allFmin == featureLocation.fmin
+        assert oldFmax == featureLocation.fmax
+
+        when: "we make changes to an exon on gene 1"
+        MRNA secondMRNA = MRNA.findByName("GB40788-RA-00001")
+        exonList = transcriptService.getSortedExons(secondMRNA, true)
+        exonUniqueName = exonList.first().uniqueName
+        setExonBoundaryCommand = setExonBoundaryCommand.replace("@EXON_UNIQUENAME@", exonUniqueName)
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundaryCommand) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = exon.featureLocation
+
+
+        then: "a change was made!"
+        assert allFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge the transcripts"
+        MRNA mrnaGB40787 = MRNA.findByName("GB40787-RA-00001")
+        MRNA mrnaGB40788 = MRNA.findByName("GB40788-RA-00001")
+        String uniqueNameGB40787 = mrnaGB40787.uniqueName
+        String uniqueNameGB40788 = mrnaGB40788.uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueNameGB40787)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueNameGB40788)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "when we get the feature history"
+        JSONObject historyContainer = createJSONFeatureContainer();
+        getHistoryString = getHistoryString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueNameGB40787)
+        List<List<FeatureEvent>> history1 = featureEventService.getHistory(uniqueNameGB40787)
+        List<List<FeatureEvent>> history2 = featureEventService.getHistory(uniqueNameGB40788)
+        historyContainer = featureEventService.generateHistory(historyContainer, (JSON.parse(getHistoryString) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray featuresArray = historyContainer.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONArray historyArray = featuresArray.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+
+
+        then: "we should see 3 events"
+        assert 3 == historyArray.size()
+        assert 3 == history1.size()
+        assert history1 == history2
+
+
+        when: "we retrieve the arrays"
+        def oldestProject = historyArray.getJSONObject(0)
+        def middleProject = historyArray.getJSONObject(1)
+        def recentProject = historyArray.getJSONObject(2)
+        def oldestHistory = history1.get(0)
+        def middleHistory = history1.get(1)
+        def recentHistory = history1.get(2)
+
+        then:
+        // not sure if it should be
+        assert recentHistory.first().operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert oldestHistory.first().operation == FeatureOperation.ADD_TRANSCRIPT
+        assert oldestHistory.size() == 1
+        assert middleHistory.size() == 2
+        assert recentHistory.size() == 1
+        assert 1 == historyArray.getJSONObject(0).getJSONArray(FeatureStringEnum.FEATURES.value).size()
+        assert FeatureOperation.ADD_TRANSCRIPT.name() == oldestProject.getString("operation")
+        assert 1 == recentProject.getJSONArray(FeatureStringEnum.FEATURES.value).size()
+        assert FeatureOperation.MERGE_TRANSCRIPTS.name() == recentProject.getString("operation")
+        assert 1 == middleProject.getJSONArray(FeatureStringEnum.FEATURES.value).size()
+        // should be ADD_TRANSCRIPT and SET_EXON_BOUNDARY
+
+        when: "when we undo the merge"
+        String undoString = undoOperation.replace("@UNIQUENAME@", uniqueNameGB40787)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = FeatureLocation.findByFeature(exon)
+
+
+        then: "we see the changed model"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert allFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+        when: "when we redo the merge on one side"
+        String redoString = redoOperation.replace("@UNIQUENAME@", uniqueNameGB40787)
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "when we undo the merge again"
+        undoString = undoOperation.replace("@UNIQUENAME@", uniqueNameGB40787)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+
+        then: "we see the changed model"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert allFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+        when: "when we redo the merge on the other side"
+        redoString = redoOperation.replace("@UNIQUENAME@", uniqueNameGB40788)
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+    }
+
+    /**
+     * https://github.com/GMOD/Apollo/issues/792
+     */
+    void "should handle merge, change on downstream / LHS , undo, redo"() {
+
+        given: "two transcripts"
+        // gene 1 - GB40787
+        Integer oldFmin = 77860
+        Integer newFmin = 77685
+        Integer newFmax = 77944
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String setExonBoundaryCommand = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":${newFmin},\"fmax\":${newFmax}}}],\"operation\":\"set_exon_boundaries\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+
+        when: "we add both transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        List<Exon> exonList = transcriptService.getSortedExons(MRNA.first(), true)
+        String exonUniqueName = exonList.get(1).uniqueName
+        Exon exon = Exon.findByUniqueName(exonUniqueName)
+        FeatureLocation featureLocation = exon.featureLocation
+
+
+        then: "we should see 2 genes, 2 transcripts, 5 exons, 2 CDS, no noncanonical splice sites"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert oldFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+        when: "we make changes to an exon on gene 1"
+        exonList = transcriptService.getSortedExons(MRNA.first(), true)
+        exonUniqueName = exonList.get(1).uniqueName
+        setExonBoundaryCommand = setExonBoundaryCommand.replace("@EXON_UNIQUENAME@", exonUniqueName)
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundaryCommand) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = exon.featureLocation
+
+
+        then: "a change was made!"
+        assert newFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge the transcripts"
+        String uniqueName1 = MRNA.findByName("GB40787-RA-00001").uniqueName
+        String uniqueName2 = MRNA.findByName("GB40788-RA-00001").uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName1)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName2)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "when we get the feature history"
+        JSONObject historyContainer = createJSONFeatureContainer();
+        getHistoryString = getHistoryString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", MRNA.first().uniqueName)
+        historyContainer = featureEventService.generateHistory(historyContainer, (JSON.parse(getHistoryString) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray historyArray = historyContainer.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+
+        then: "we should see 3 events"
+        assert 3 == historyArray.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value).size()
+
+
+        when: "when we undo the merge"
+        MRNA firstMRNA = MRNA.first()
+        String undoString = undoOperation.replace("@UNIQUENAME@", firstMRNA.uniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = FeatureLocation.findByFeature(exon)
+        def currentFeatureEvent = featureEventService.findCurrentFeatureEvent(firstMRNA.uniqueName)
+        def history = featureEventService.getHistory(firstMRNA.uniqueName)
+
+
+        then: "we see the changed model"
+        assert currentFeatureEvent.size() == 2
+        assert history.size() == 3
+        assert history[0][0].current == false
+        assert history[1][0].current == true
+        assert history[1][1].current == true
+        assert history[2][0].current == false
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert newFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+
+        when: "when we redo the merge on one side"
+        String redoString = redoOperation.replace("@UNIQUENAME@", firstMRNA.uniqueName)
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "when we undo the merge again"
+        undoString = undoOperation.replace("@UNIQUENAME@", firstMRNA.uniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        def lastMRNA = MRNA.last()
+        currentFeatureEvent = featureEventService.findCurrentFeatureEvent(lastMRNA.uniqueName)
+        history = featureEventService.getHistory(lastMRNA.uniqueName)
+
+        then: "we see the changed model"
+        assert currentFeatureEvent.size() == 2
+        assert history.size() == 3
+        assert history[0][0].current == false
+        assert history[1][0].current == true
+        assert history[1][1].current == true
+        assert history[2][0].current == false
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+        when: "when we redo the merge on the other side"
+        redoString = redoOperation.replace("@UNIQUENAME@", lastMRNA.uniqueName)
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+    }
+
+    /**
+     * https://github.com/GMOD/Apollo/issues/792
+     */
+    void "should handle merge, change on downstream / LHS , undo, undo"() {
+
+        given: "two transcripts"
+        // gene 1 - GB40787
+        Integer oldFmin = 77860
+        Integer newFmin = 77685
+        Integer newFmax = 77944
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", ${testCredentials} \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", ${testCredentials} \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", ${testCredentials} \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String setExonBoundaryCommand = "{${testCredentials} \"track\":\"Group1.10\",${testCredentials} \"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":${newFmin},\"fmax\":${newFmax}}}],\"operation\":\"set_exon_boundaries\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", ${testCredentials} \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+
+        when: "we add both transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        MRNA firstMrna = MRNA.first()
+        List<Exon> exonList = transcriptService.getSortedExons(firstMrna, true)
+        String exonUniqueName = exonList.get(1).uniqueName
+        Exon exon = Exon.findByUniqueName(exonUniqueName)
+        FeatureLocation featureLocation = exon.featureLocation
+
+
+        then: "we should see 2 genes, 2 transcripts, 5 exons, 2 CDS, no noncanonical splice sites"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert oldFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+        when: "we make changes to an exon on gene 1"
+        exonList = transcriptService.getSortedExons(firstMrna, true)
+        exonUniqueName = exonList.get(1).uniqueName
+        setExonBoundaryCommand = setExonBoundaryCommand.replace("@EXON_UNIQUENAME@", exonUniqueName)
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundaryCommand) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = exon.featureLocation
+
+
+        then: "a change was made!"
+        assert newFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge the transcripts"
+        String uniqueName1 = MRNA.findByName("GB40787-RA-00001").uniqueName
+        String uniqueName2 = MRNA.findByName("GB40788-RA-00001").uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName1)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName2)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        assert uniqueName1 == firstMrna.uniqueName
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "when we get the feature history"
+        JSONObject historyContainer = createJSONFeatureContainer();
+        getHistoryString = getHistoryString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", firstMrna.uniqueName)
+        historyContainer = featureEventService.generateHistory(historyContainer, (JSON.parse(getHistoryString) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray historyArray = historyContainer.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+
+        then: "we should see 3 events"
+        assert 3 == historyArray.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value).size()
+
+
+        when: "when we undo the merge"
+        MRNA firstMRNA = MRNA.first()
+        def history = featureEventService.getHistory(firstMRNA.uniqueName)
+        def currentFeatureEvent = featureEventService.findCurrentFeatureEvent(firstMRNA.uniqueName)
+        String undoString = undoOperation.replace("@UNIQUENAME@", firstMRNA.uniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = FeatureLocation.findByFeature(exon)
+        currentFeatureEvent = featureEventService.findCurrentFeatureEvent(firstMRNA.uniqueName)
+        history = featureEventService.getHistory(firstMRNA.uniqueName)
+
+
+        then: "we see the changed model"
+        assert currentFeatureEvent.size() == 2
+        assert history.size() == 3
+        assert history[0][0].current == false
+        assert history[1][0].current == true
+        assert history[1][1].current == true
+        assert history[2][0].current == false
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert newFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+
+        when: "when we undo again"
+        undoString = undoOperation.replace("@UNIQUENAME@", firstMRNA.uniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        currentFeatureEvent = featureEventService.findCurrentFeatureEvent(firstMRNA.uniqueName)
+        history = featureEventService.getHistory(firstMRNA.uniqueName)
+        exon = Exon.findByUniqueName(exonUniqueName)
+        featureLocation = FeatureLocation.findByFeature(exon)
+
+        then: "we should see A1 and B1"
+        assert currentFeatureEvent.size() == 2
+        assert history.size() == 3
+        assert history[0].size() == 1
+        assert history[0][0].current == true
+        assert history[1].size() == 2
+        history[1].each {
+            if (it.current) {
+                assert it.operation == FeatureOperation.ADD_TRANSCRIPT
+            } else {
+                assert it.operation == FeatureOperation.SET_EXON_BOUNDARIES
+            }
+        }
+        assert history[2].size() == 1
+        assert history[2][0].current == false
+        // looks like its failing to delete the "inferred" one
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert oldFmin == featureLocation.fmin
+        assert newFmax == featureLocation.fmax
+
+    }
+
+    void "we should be able to create a uniform tree merge and undo it"() {
+
+        given: "two transcripts A = gb40788 and B = gb40787"
+        Integer gb40788Fmin = 75270
+        Integer old40788Fmax = 75367
+        Integer new40788Fmax = 75562
+
+        Integer old40787Fmin = 77860
+        Integer new40787Fmin = 77616
+        Integer gb40787Fmax = 77944
+
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String setExonBoundary40788Command = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":${gb40788Fmin},\"fmax\":${new40788Fmax}}}],\"operation\":\"set_exon_boundaries\"}"
+        String setExonBoundary40787Command = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":${new40787Fmin},\"fmax\":${gb40787Fmax}}}],\"operation\":\"set_exon_boundaries\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+
+
+        when: "we add the two transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        MRNA mrna40787 = MRNA.findByName("GB40787-RA-00001")
+        MRNA mrna40788 = MRNA.findByName("GB40788-RA-00001")
+        List<Exon> exonList40788 = transcriptService.getSortedExons(mrna40788, true)
+        String exon788UniqueName = exonList40788.first().uniqueName
+        Exon exon788 = Exon.findByUniqueName(exon788UniqueName)
+        FeatureLocation featureLocation788 = exon788.featureLocation
+        List<Exon> exonList40787 = transcriptService.getSortedExons(mrna40787, true)
+        String exon787UniqueName = exonList40787.last().uniqueName
+        Exon exon787 = Exon.findByUniqueName(exon787UniqueName)
+        FeatureLocation featureLocation787 = exon787.featureLocation
+
+
+        then: "we verify that they are there and the coordinates (A1/B1)"
+        assert mrna40787 != null
+        assert mrna40788 != null
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert old40788Fmax == featureLocation788.fmax
+        assert old40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+
+        when: "we set the exon boundaries for both"
+        setExonBoundary40788Command = setExonBoundary40788Command.replace("@EXON_UNIQUENAME@", exon788UniqueName)
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundary40788Command) as JSONObject)
+        setExonBoundary40787Command = setExonBoundary40787Command.replace("@EXON_UNIQUENAME@", exon787UniqueName)
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundary40787Command) as JSONObject)
+        exon788 = Exon.findByUniqueName(exon788UniqueName)
+        featureLocation788 = exon788.featureLocation
+        exon787 = Exon.findByUniqueName(exon787UniqueName)
+        featureLocation787 = exon787.featureLocation
+
+
+        then: "we verify that they are there and the NEW coordinates (A2/B2)"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert new40788Fmax == featureLocation788.fmax
+        assert new40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+
+        when: "we merge them"
+        String uniqueName787 = mrna40787.uniqueName
+        String uniqueName788 = mrna40788.uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName787)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName788)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+
+        then: "we confirm that there is just the one transcript (A2B2)"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+
+        when: "we undo the merge"
+        String undoString = undoOperation.replace("@UNIQUENAME@", mrna40787.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon788 = Exon.findByUniqueName(exon788UniqueName)
+        featureLocation788 = exon788.featureLocation
+        exon787 = Exon.findByUniqueName(exon787UniqueName)
+        featureLocation787 = exon787.featureLocation
+
+
+        then: "we verify that it is the most recent values (A2/B2) and that the history is correct"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert new40788Fmax == featureLocation788.fmax
+        assert new40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+
+        when: "we undo A2"
+        undoString = undoOperation.replace("@UNIQUENAME@", mrna40788.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon788 = Exon.findByUniqueName(exon788UniqueName)
+        featureLocation788 = exon788.featureLocation
+        exon787 = Exon.findByUniqueName(exon787UniqueName)
+        featureLocation787 = exon787.featureLocation
+
+        JSONObject historyContainer = createJSONFeatureContainer();
+        def thisHistoryString = getHistoryString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", mrna40787.uniqueName)
+        historyContainer = featureEventService.generateHistory(historyContainer, (JSON.parse(thisHistoryString) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray featuresArray = historyContainer.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONArray historyArray = featuresArray.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+
+
+        then: "we should get B2/A1"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert old40788Fmax == featureLocation788.fmax
+        assert new40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+        assert historyArray.size() == 3
+        assert historyArray[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+        assert !historyArray[0].current
+        assert historyArray[0].features[0].name == "GB40787-RA-00001"
+        assert historyArray[0].features.size() == 1
+        assert historyArray[1].operation == FeatureOperation.SET_EXON_BOUNDARIES.name()
+        assert historyArray[1].features[0].name == "GB40787-RA-00001"
+        assert historyArray[1].features.size() == 1
+        assert historyArray[1].current
+        assert historyArray[2].operation == FeatureOperation.MERGE_TRANSCRIPTS.name()
+        assert historyArray[2].features[0].name == "GB40787-RA-00001"
+        assert historyArray[2].features.size() == 1
+        assert !historyArray[2].current
+
+        when: "we verify the history for the other side"
+        JSONObject historyContainer2 = createJSONFeatureContainer();
+        def historyString2 = getHistoryString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", mrna40788.uniqueName)
+        historyContainer2 = featureEventService.generateHistory(historyContainer2, (JSON.parse(historyString2) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        featuresArray = historyContainer2.getJSONArray(FeatureStringEnum.FEATURES.value)
+        historyArray = featuresArray.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+
+        then: "we should see the hsitory of GB40788"
+        assert historyArray.size() == 3
+        assert historyArray[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+        assert historyArray[0].current
+        assert historyArray[0].features[0].name == "GB40788-RA-00001"
+        assert historyArray[0].features.size() == 1
+        assert historyArray[1].operation == FeatureOperation.SET_EXON_BOUNDARIES.name()
+        assert historyArray[1].features[0].name == "GB40788-RA-00001"
+        assert historyArray[1].features.size() == 1
+        assert !historyArray[1].current
+        assert historyArray[2].operation == FeatureOperation.MERGE_TRANSCRIPTS.name()
+        // NOTE: the merged value is GB40787
+        assert historyArray[2].features[0].name == "GB40787-RA-00001"
+        assert historyArray[2].features.size() == 1
+        assert !historyArray[2].current
+
+
+        when: "we undo B2"
+        undoString = undoOperation.replace("@UNIQUENAME@", mrna40787.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        exon788 = Exon.findByUniqueName(exon788UniqueName)
+        featureLocation788 = exon788.featureLocation
+        exon787 = Exon.findByUniqueName(exon787UniqueName)
+        featureLocation787 = exon787.featureLocation
+
+
+        then: "we should get B1/A1"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert old40788Fmax == featureLocation788.fmax
+        assert old40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+        when: "we redo B to get to B1 (and stay A2)"
+        def redoString = redoOperation.replace("@UNIQUENAME@", mrna40787.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+        exon788 = Exon.findByUniqueName(exon788UniqueName)
+        featureLocation788 = exon788.featureLocation
+        exon787 = Exon.findByUniqueName(exon787UniqueName)
+        featureLocation787 = exon787.featureLocation
+
+        then: "we confirm A2 / B1 "
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert gb40788Fmin == featureLocation788.fmin
+        assert old40788Fmax == featureLocation788.fmax
+        assert new40787Fmin == featureLocation787.fmin
+        assert gb40787Fmax == featureLocation787.fmax
+
+
+        when: "we redo A to A2 -> AB (re-merge)"
+        redoString = redoOperation.replace("@UNIQUENAME@", mrna40788.uniqueName).replace("@COUNT@", "2")
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we confirm that we have a merge AB"
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+    }
+
+    void "we should be able to do a merge THEN split and undo and then redo it"() {
+
+        given: "two transcripts A = gb40788 and B = gb40787"
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+
+
+        when: "we add the two transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        MRNA mrna40787 = MRNA.findByName("GB40787-RA-00001")
+        MRNA mrna40788 = MRNA.findByName("GB40788-RA-00001")
+
+        then: "we verify that they are there and the coordinates (A1/B1)"
+        assert mrna40787 != null
+        assert mrna40788 != null
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge them"
+        String uniqueName787 = mrna40787.uniqueName
+        String uniqueName788 = mrna40788.uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName787)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName788)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+
+        then: "we confirm that there is just the one transcript (A2B2)"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+        when: "we split the transcript"
+        def sortedExons = Exon.all.sort() { a, b ->
+            a.featureLocation.fmin <=> b.featureLocation.fmin
+        }
+        String exon1UniqueName = sortedExons[1].uniqueName
+        String exon2UniqueName = sortedExons[2].uniqueName
+        println "left fmin: ${sortedExons[1].fmin}"
+        println "right fmin: ${sortedExons[2].fmin}"
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have two of everything now"
+        assert Exon.count == 5
+        assert MRNA.count == 2
+        assert Gene.count == 2
+        assert CDS.count == 2
+
+
+        when: "we get the history of the two transcripts"
+        JSONObject history787Container = createJSONFeatureContainer();
+        def thisHistory787String = getHistoryString.replaceAll("@TRANSCRIPT_UNIQUENAME@", mrna40787.uniqueName)
+        history787Container = featureEventService.generateHistory(history787Container, (JSON.parse(thisHistory787String) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray features787Array = history787Container.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONArray history787Array = features787Array.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+
+        JSONObject history788Container = createJSONFeatureContainer();
+        def thisHistory788String = getHistoryString.replaceAll("@TRANSCRIPT_UNIQUENAME@", mrna40788.uniqueName)
+        history788Container = featureEventService.generateHistory(history788Container, (JSON.parse(thisHistory788String) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+        JSONArray features788Array = history788Container.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONArray history788Array = features788Array.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+
+        then: "we confirm that its something sane"
+        assert history787Array.size() == 3
+        assert history787Array[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+        assert !history787Array[0].current
+        assert history787Array[0].features[0].name == "GB40787-RA-00001"
+        assert history787Array[0].features.size() == 1
+        assert history787Array[1].operation == FeatureOperation.MERGE_TRANSCRIPTS.name()
+        assert history787Array[1].features[0].name == "GB40787-RA-00001"
+        assert history787Array[1].features.size() == 1
+        assert !history787Array[1].current
+        assert history787Array[2].operation == FeatureOperation.SPLIT_TRANSCRIPT.name()
+        assert history787Array[2].features[0].name == "GB40787-RA-00001"
+        assert history787Array[2].features.size() == 1
+        assert history787Array[2].current
+
+        assert history788Array.size() == 3
+        assert history788Array[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+        assert !history788Array[0].current
+        assert history788Array[0].features[0].name == "GB40788-RA-00001"
+        assert history788Array[0].features.size() == 1
+        assert history788Array[1].operation == FeatureOperation.MERGE_TRANSCRIPTS.name()
+        assert history788Array[1].features[0].name == "GB40787-RA-00001"
+        assert history788Array[1].features.size() == 1
+        assert !history788Array[1].current
+        assert history788Array[2].operation == FeatureOperation.SPLIT_TRANSCRIPT.name()
+        assert history788Array[2].features.size() == 1
+        // this works in the code, not sure why it fails in the test here
+//        assert  history788Array[2].features[0].name == "GB40787-RAa-00001"
+        assert history788Array[2].current
+
+
+        when: "we undo A2"
+        String undoString = undoOperation.replace("@UNIQUENAME@", mrna40788.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+
+
+        then: "we should get B2/A1"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+
+        when: "we redo B to get to B1 (and stay A2)"
+        def redoString = redoOperation.replace("@UNIQUENAME@", mrna40787.uniqueName).replace("@COUNT@", "1")
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we confirm A2 / B1 "
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+    }
+
+    void "we should be able to do a split THEN merge and undo and then redo it"() {
+
+        given: "two transcripts A = gb40788 and B = gb40787"
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+        String splitString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_1@\" }, { \"uniquename\": \"@EXON_2@\" } ], \"operation\": \"split_transcript\" }"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":@COUNT@,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+        String getHistoryString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_UNIQUENAME@\" } ], \"operation\": \"get_history_for_features\" }"
+        String deleteString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_NAME@\" } ], \"operation\": \"delete_feature\" }"
+
+
+        when: "we add the two transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+        MRNA mrna40787 = MRNA.findByName("GB40787-RA-00001")
+        MRNA mrna40788 = MRNA.findByName("GB40788-RA-00001")
+
+        then: "we verify that they are there and the coordinates (A1/B1)"
+        assert mrna40787 != null
+        assert mrna40788 != null
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+        when: "we split the transcript"
+        def sortedExons = Exon.all.sort() { a, b ->
+            a.featureLocation.fmin <=> b.featureLocation.fmin
+        }
+        String exon1UniqueName = sortedExons[1].uniqueName
+        String exon2UniqueName = sortedExons[2].uniqueName
+        println "left fmin: ${sortedExons[1].fmin}"
+        println "right fmin: ${sortedExons[2].fmin}"
+        splitString = splitString.replace("@EXON_1@", exon1UniqueName)
+        splitString = splitString.replace("@EXON_2@", exon2UniqueName)
+        JSONObject splitJsonObject = requestHandlingService.splitTranscript(JSON.parse(splitString))
+
+        then: "we should have three of everything now"
+        assert Exon.count == 5
+        assert MRNA.count == 3
+        assert Gene.count == 3
+        assert CDS.count == 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+        when: "we merge them"
+        def sortedTranscripts = Transcript.all.sort() { a, b ->
+            a.featureLocation.fmin <=> b.featureLocation.fmin
+        }
+        String transcript1UniqueName = sortedTranscripts[1].uniqueName
+        String transcript2UniqueName = sortedTranscripts[2].uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", transcript1UniqueName)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", transcript2UniqueName)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        requestHandlingService.mergeTranscripts(commandObject)
+
+
+        then: "we confirm that there is just the one transcript (A2B2)"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+
+        // TODO: these are not consistently sorted.   See: https://github.com/GMOD/Apollo/pull/1749
+//        when: "we get the history of the 5' transcript"
+//        JSONObject history787Container = createJSONFeatureContainer();
+//        def thisHistory787String = getHistoryString.replaceAll("@TRANSCRIPT_UNIQUENAME@", mrna40787.uniqueName)
+//        history787Container = featureEventService.generateHistory(history787Container, (JSON.parse(thisHistory787String) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+//        JSONArray features787Array = history787Container.getJSONArray(FeatureStringEnum.FEATURES.value)
+//        JSONArray history787Array = features787Array.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+//
+//        then: "we can confirm that it is correct"
+//        assert history787Array.size() == 3
+//        assert history787Array[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+//        assert !history787Array[0].current
+////        assert history787Array[0].features[0].name == "GB40787-RA-00001"
+//        assert history787Array[0].features.size() == 1
+////        assert history787Array[1].operation == FeatureOperation.SPLIT_TRANSCRIPT.name()
+//        assert history787Array[1].features.size() == 1
+//        // this works in the code, not sure why it fails in the test here
+////        assert history787Array[1].features[0].name == "GB40787-RAa-00001"
+//        assert !history787Array[1].current
+//        assert history787Array[2].operation == FeatureOperation.MERGE_TRANSCRIPTS.name()
+//        assert history787Array[2].features[0].name == "GB40787-RA-00001"
+//        assert history787Array[2].features.size() == 1
+//        assert history787Array[2].current
+//
+//        when: "we get the history of the 3' transcript"
+//        JSONObject history788Container = createJSONFeatureContainer();
+//        def thisHistory788String = getHistoryString.replaceAll("@TRANSCRIPT_UNIQUENAME@", mrna40788.uniqueName)
+//        history788Container = featureEventService.generateHistory(history788Container, (JSON.parse(thisHistory788String) as JSONObject).getJSONArray(FeatureStringEnum.FEATURES.value))
+//        JSONArray features788Array = history788Container.getJSONArray(FeatureStringEnum.FEATURES.value)
+//        JSONArray history788Array = features788Array.getJSONObject(0).getJSONArray(FeatureStringEnum.HISTORY.value)
+//
+//        then: "we confirm that its something sane"
+//        assert history788Array.size() == 2
+//        assert history788Array[0].operation == FeatureOperation.ADD_TRANSCRIPT.name()
+//        assert !history788Array[0].current
+//        assert history788Array[0].features[0].name == "GB40788-RA-00001"
+//        assert history788Array[0].features.size() == 1
+//        assert history788Array[1].operation == FeatureOperation.SPLIT_TRANSCRIPT.name()
+//        assert history788Array[1].features[0].name == "GB40788-RA-00001"
+//        assert history788Array[1].features.size() == 1
+//        assert history788Array[1].current
+//
+//
+
+//        when: "we undo the second transcript"
+//        sortedTranscripts = Transcript.all.sort() { a, b ->
+//            a.featureLocation.fmin <=> b.featureLocation.fmin
+//        }
+//        String undoString = undoOperation.replace("@UNIQUENAME@", sortedTranscripts[1].uniqueName).replace("@COUNT@", "1")
+//        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+
+
+//        then: "we should have 3 of everything again"
+//        assert Exon.count == 5
+//        assert MRNA.count == 3
+//        assert Gene.count == 3
+//        assert CDS.count == 3
+//        assert NonCanonicalFivePrimeSpliceSite.count == 0
+//        assert NonCanonicalThreePrimeSpliceSite.count == 0
+//
+//
+//        when: "we redo"
+//        sortedTranscripts = Transcript.all.sort() { a, b ->
+//            a.featureLocation.fmin <=> b.featureLocation.fmin
+//        }
+//        def redoString = redoOperation.replace("@UNIQUENAME@", sortedTranscripts[1].uniqueName).replace("@COUNT@", "1")
+//        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+//
+////        then: "we should be back to two"
+////        assert Gene.count == 2
+////        assert MRNA.count == 2
+////        assert CDS.count == 2
+////        assert Exon.count == 5
+////        assert NonCanonicalFivePrimeSpliceSite.count == 1
+////        assert NonCanonicalThreePrimeSpliceSite.count == 1
+////
+////        when: "we undo the left transcript instead"
+//        sortedTranscripts = Transcript.all.sort() { a, b ->
+//            a.featureLocation.fmin <=> b.featureLocation.fmin
+//        }
+//        undoString = undoOperation.replace("@UNIQUENAME@", sortedTranscripts[0].uniqueName).replace("@COUNT@", "1")
+//        try {
+//            requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+//            assert false
+//        } catch (e) {
+//            assert true
+//            assert e instanceof AnnotationException
+//        }
+//
+//////        then: "should be the same counts"
+//////        assert Gene.count == 2
+//////        assert MRNA.count == 2
+//////        assert CDS.count == 2
+//////        assert Exon.count == 5
+//////        assert NonCanonicalFivePrimeSpliceSite.count == 1
+//////        assert NonCanonicalThreePrimeSpliceSite.count == 1
+//////
+////        when: "we delete the first transcript"
+//        def mrnaToDelete = MRNA.all.first().uniqueName
+//        requestHandlingService.deleteFeature(JSON.parse(deleteString1.replace("@TRANSCRIPT_NAME@", mrnaToDelete)))
+//
+//        then: "there should be only one MRNA and gene left"
+//        assert Gene.count == 1
+//        assert MRNA.count == 1
+//
+//        when: "we delete the second transcript"
+//        mrnaToDelete = MRNA.all.first().uniqueName
+//        requestHandlingService.deleteFeature(JSON.parse(deleteString1.replace("@TRANSCRIPT_NAME@", mrnaToDelete)))
+//
+//        then: "there should be no features left"
+//        assert Gene.count == 0
+//        assert MRNA.count == 0
+//        assert CDS.count == 0
+//        assert Exon.count == 0
+//        assert NonCanonicalFivePrimeSpliceSite.count == 0
+//        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+    }
+}
diff --git a/test/integration/org/bbop/apollo/FeatureRelationshipServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/FeatureRelationshipServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..0cc0099
--- /dev/null
+++ b/test/integration/org/bbop/apollo/FeatureRelationshipServiceIntegrationSpec.groovy
@@ -0,0 +1,83 @@
+package org.bbop.apollo
+
+class FeatureRelationshipServiceIntegrationSpec extends AbstractIntegrationSpec{
+    
+    def featureRelationshipService
+
+    void "delete all parents and features"(){
+
+        given: "a gene and a transcript"
+        Gene gene = new Gene(name: "asdf",uniqueName: "asdf").save()
+        MRNA mrna = new MRNA(name: "rman",uniqueName: "mrna").save()
+
+        when: "we attach the two"
+        featureRelationshipService.addChildFeature(gene,mrna,false)
+
+        then: "they should be attached"
+        Gene.count ==1
+        MRNA.count ==1
+        FeatureRelationship.count ==1
+
+        when: "we delete from the gene"
+        featureRelationshipService.deleteFeatureAndChildren(gene)
+
+        then: "there should be nothing left"
+        Gene.count ==0
+        MRNA.count ==0
+        FeatureRelationship.count ==0
+    }
+
+    void "delete all parents and features on multiple levels"(){
+
+        given: "a gene and a transcript"
+        Gene gene = new Gene(name: "asdf",uniqueName: "asdf").save()
+        MRNA mrna = new MRNA(name: "rman",uniqueName: "mrna").save()
+        Exon exon = new Exon(name: "exonname",uniqueName: "exon-unique").save()
+
+        when: "we attach the two"
+        featureRelationshipService.addChildFeature(gene,mrna,false)
+        featureRelationshipService.addChildFeature(mrna,exon,false)
+
+        then: "they should be attached"
+        Gene.count ==1
+        MRNA.count ==1
+        Exon.count ==1
+        FeatureRelationship.count ==2
+        Feature.count ==3
+
+        when: "we delete from the gene"
+        featureRelationshipService.deleteFeatureAndChildren(gene)
+
+        then: "there should be nothing left"
+        Gene.count ==0
+        MRNA.count ==0
+        Exon.count ==0
+        FeatureRelationship.count ==0
+        Feature.count ==0
+    }
+
+    void "delete all parents and features for multiple children"(){
+
+        given: "a gene and a transcript"
+        Gene gene = new Gene(name: "asdf",uniqueName: "asdf").save()
+        MRNA mrna = new MRNA(name: "rman",uniqueName: "mrna").save()
+        MRNA mrna2 = new MRNA(name: "rman2",uniqueName: "mrna2").save()
+
+        when: "we attach the two"
+        featureRelationshipService.addChildFeature(gene,mrna,false)
+        featureRelationshipService.addChildFeature(gene,mrna2,false)
+
+        then: "they should be attached"
+        Gene.count ==1
+        MRNA.count ==2
+        FeatureRelationship.count ==2
+
+        when: "we delete from the gene"
+        featureRelationshipService.deleteFeatureAndChildren(gene)
+
+        then: "there should be nothing left"
+        Gene.count ==0
+        MRNA.count ==0
+        FeatureRelationship.count ==0
+    }
+}
diff --git a/test/integration/org/bbop/apollo/FeatureServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/FeatureServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..bf19232
--- /dev/null
+++ b/test/integration/org/bbop/apollo/FeatureServiceIntegrationSpec.groovy
@@ -0,0 +1,318 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Ignore
+import spock.lang.IgnoreRest
+
+class FeatureServiceIntegrationSpec extends AbstractIntegrationSpec{
+
+    def featureService
+    def transcriptService
+    def requestHandlingService
+    def featureRelationshipService
+
+
+    void "convert JSON to Features"() {
+
+        given: "a set string and existing sequence, when we have a complicated mRNA as JSON"
+        String jsonString = "{${testCredentials}  \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"e [...]
+
+        when: "we parse it"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "is is a valid object"
+        assert jsonObject != null
+        JSONArray jsonArray = jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert jsonArray.size() == 1
+        JSONObject mRNAJsonObject = jsonArray.getJSONObject(0)
+        JSONArray childArray = jsonArray.getJSONObject(0).getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert childArray.size() == 7
+
+        when: "we convert it to a feature"
+        Feature feature = featureService.convertJSONToFeature(mRNAJsonObject, Sequence.first())
+
+        then: "it should convert it to the same feature"
+        assert feature != null
+        feature.ontologyId == MRNA.ontologyId
+
+    }
+
+    void "convert Feature to JSON and convert the JSON back to a feature"() {
+
+        given: "a transcript GB40744-RA"
+        String transcriptString = "{${testCredentials}  \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence [...]
+        String setSymbolString = "{${testCredentials}  \"operation\":\"set_symbol\",\"features\":[{\"symbol\":\"@SYMBOL@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String setDescriptionString = "{${testCredentials}  \"operation\":\"set_description\",\"features\":[{\"description\":\"@DESCRIPTION@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonPrimaryDbxrefString = "{${testCredentials}  \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"@DB@\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonReservedPropertyString = "{${testCredentials}  \"operation\":\"add_non_reserved_properties\",\"features\":[{\"non_reserved_properties\":[{\"tag\":\"@TAG@\",\"value\":\"@VALUE@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addCommentString = "{${testCredentials}  \"operation\":\"add_comments\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\"}"
+
+        when: "we add the transcript"
+        requestHandlingService.addFeature(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should see the transcript"
+        assert Gene.count == 1
+        assert Transcript.count == 1
+        Transcript transcript = Transcript.all.get(0)
+        Gene gene = Gene.all.get(0)
+
+        when: "we add feature properties and attributes to the gene"
+        requestHandlingService.setSymbol(JSON.parse(setSymbolString.replace("@UNIQUENAME@", gene.uniqueName).replace("@SYMBOL@", "TGN1")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(setDescriptionString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DESCRIPTION@", "This is a test gene TGN1")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "48734522")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSG0000000131")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "437598")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "GO").replace("@ACCESSION@", "0048564")) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "isValidated").replace("@VALUE@", "false")) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "annotationType").replace("@VALUE@", "Pseudogene")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", gene.uniqueName).replace("@COMMENT@", "This is a test gene")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", gene.uniqueName).replace("@COMMENT@", "This gene is not validated")) as JSONObject)
+
+        then: "we should see the added properties"
+        assert gene.featureDBXrefs.size() > 0
+        assert gene.featureProperties.size() > 0
+
+        when: "we add feature properties and attributes to the transcript"
+        requestHandlingService.setSymbol(JSON.parse(setSymbolString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@SYMBOL@", "TGN1-1A")) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(setDescriptionString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DESCRIPTION@", "This is an isoform for gene TGN1")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "XM_3973451.2")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENST00000031241")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "3749242")) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "GO").replace("@ACCESSION@", "0051497")) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@TAG@", "isValidated").replace("@VALUE@", "false")) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@TAG@", "annotationType").replace("@VALUE@", "Pseudogene transcript")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@COMMENT@", "This is a test transcript")) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@COMMENT@", "This transcript is not validated")) as JSONObject)
+
+        then: "we should see the added properties"
+        assert transcript.featureDBXrefs.size() > 0
+        assert transcript.featureProperties.size() > 0
+
+        when: "we convert the gene to a JSONObject via convertFeatureToJSON()"
+        JSONObject geneFeatureJsonObject = featureService.convertFeatureToJSON(gene)
+
+        then: "we should see the properties of gene and transcript in the JSONObject"
+        JSONArray featureProperties = geneFeatureJsonObject.getJSONArray(FeatureStringEnum.PROPERTIES.value)
+        assert featureProperties.size() > 0
+        def expectedDbxrefForGene = ["NCBI:48734522", "Ensembl:ENSG0000000131", "PMID:437598", "GO:0048564" ]
+        def expectedDbxrefForTranscript = ["NCBI:XM_3973451.2", "Ensembl:ENST00000031241", "PMID:3749242", "GO:0051497"]
+        def expectedFeaturePropertiesForGene = ["isValidated:false", "annotationType:Pseudogene"]
+        def expectedFeaturePropertiesForTranscript = ["isValidated:false", "annotationType:Pseudogene transcript"]
+        def expectedCommentsForGene = ["This is a test gene", "This gene is not validated"]
+        def expectedCommentsForTranscript = ["This is a test transcript", "This transcript is not validated"]
+
+        when: "we delete the gene and transcript and try to add the feature via the JSONObject"
+        featureRelationshipService.deleteFeatureAndChildren(gene)
+        Sequence sequence = Sequence.all.get(0)
+        featureService.convertJSONToFeature(geneFeatureJsonObject, sequence)
+
+        then: "convertJSONToFeature() should interpret the JSONObject properly and we should see all the properties that we added above"
+        assert Gene.count == 1
+        Gene newGene = Gene.all.get(0)
+        Transcript newTranscript = Transcript.all.get(0)
+
+        assert newGene.symbol == "TGN1"
+        assert newGene.description == "This is a test gene TGN1"
+        assert newGene.featureDBXrefs.size() == 4
+        assert newGene.featureProperties.size() == 4
+
+        newGene.featureProperties.each { fp ->
+            if (fp instanceof Comment) {
+                assert expectedCommentsForGene.indexOf(fp.value) != -1
+                expectedCommentsForGene.remove(expectedCommentsForGene.indexOf(fp.value))
+            }
+            else {
+                String key = fp.tag + ":" + fp.value
+                assert expectedFeaturePropertiesForGene.indexOf(key) != -1
+                expectedFeaturePropertiesForGene.remove(expectedFeaturePropertiesForGene.indexOf(key))
+            }
+        }
+
+        assert expectedCommentsForGene.size() == 0
+        assert expectedFeaturePropertiesForGene.size() == 0
+
+        newGene.featureDBXrefs.each { dbxref ->
+            String key = dbxref.db.name + ":" + dbxref.accession
+            assert expectedDbxrefForGene.indexOf(key) != -1
+            expectedDbxrefForGene.remove(expectedDbxrefForGene.indexOf(key))
+        }
+
+        assert expectedDbxrefForGene.size() == 0
+
+
+        assert newTranscript.symbol == "TGN1-1A"
+        assert newTranscript.description == "This is an isoform for gene TGN1"
+        assert newTranscript.featureDBXrefs.size() == 4
+        assert newTranscript.featureProperties.size() == 4
+
+        newTranscript.featureProperties.each { fp ->
+            if (fp instanceof Comment) {
+                assert expectedCommentsForTranscript.indexOf(fp.value) != -1
+                expectedCommentsForTranscript.remove(expectedCommentsForTranscript.indexOf(fp.value))
+            }
+            else {
+                String key = fp.tag + ":" + fp.value
+                assert expectedFeaturePropertiesForTranscript.indexOf(key) != -1
+                expectedFeaturePropertiesForTranscript.remove(expectedFeaturePropertiesForTranscript.indexOf(key))
+            }
+        }
+
+        assert expectedCommentsForTranscript.size() == 0
+        assert expectedFeaturePropertiesForTranscript.size() == 0
+
+        newTranscript.featureDBXrefs.each { dbxref ->
+            String key = dbxref.db.name + ":" + dbxref.accession
+            assert expectedDbxrefForTranscript.indexOf(key) != -1
+            expectedDbxrefForTranscript.remove(expectedDbxrefForTranscript.indexOf(key))
+        }
+
+        assert expectedDbxrefForTranscript.size() == 0
+    }
+
+    void "If an annotation doesn't have strand information then it should, by default, be set to the sense strand"() {
+
+        given: "a transcript with no strand information"
+        String featureString = "{${testCredentials} \"operation\":\"add_feature\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":0,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":761542,\"strand\":0,\"fmax\":768063},\"children\":[{\"location\":{\"fmin\":767945,\"strand\":0,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":0,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\ [...]
+
+        when: "we add a feature that has its strand as 0"
+        requestHandlingService.addFeature(JSON.parse(featureString) as JSONObject)
+
+        then: "we should see the feature, and all of its sub-features, placed on the sense strand"
+        Gene gene = Gene.all.get(0)
+        Transcript transcript = transcriptService.getTranscripts(gene).iterator().next()
+        def exonList = transcriptService.getExons(transcript)
+
+        assert gene.featureLocation.strand == Strand.POSITIVE.value
+        assert transcript.featureLocation.strand == Strand.POSITIVE.value
+
+        exonList.each {
+            it.featureLocation.strand == Strand.POSITIVE.value
+        }
+    }
+
+    void "When adding a transcript (on the reverse strand) and transcript fragment, all the fragments should be isoforms of the main transcript and have their CDS set as expected"() {
+
+        given: "1 transcript and 3 transcript fragments"
+        String addTranscript1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":689640,\"strand\":-1,\"fmax\":693859},\"name\":\"GB40750-RA\",\"children\":[{\"location\":{\"fmin\":693543,\"strand\":-1,\"fmax\":693859},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":692451,\"strand\":-1,\"fmax\":692480},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":689640,\"strand [...]
+        String addTranscriptFragment1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":689640,\"strand\":-1,\"fmax\":690739},\"name\":\"GB40750-RA\",\"children\":[{\"location\":{\"fmin\":689640,\"strand\":-1,\"fmax\":690739},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addTranscriptFragment2String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":691158,\"strand\":-1,\"fmax\":691354},\"name\":\"GB40750-RA\",\"children\":[{\"location\":{\"fmin\":691158,\"strand\":-1,\"fmax\":691354},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addTranscriptFragment3String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":691674,\"strand\":-1,\"fmax\":691846},\"name\":\"GB40750-RA\",\"children\":[{\"location\":{\"fmin\":691674,\"strand\":-1,\"fmax\":691846},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        when: "we add the complete transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscript1String) as JSONObject)
+
+        then: "we should see the transcript"
+        assert MRNA.all.size() == 1
+        Gene parentGene = Gene.all.get(0)
+
+        when: "we add transcript fragment 1"
+        JSONObject addTranscriptFragment1ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment1String) as JSONObject)
+        String transcriptFragment1UniqueName = addTranscriptFragment1ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment"
+        assert MRNA.all.size() == 2
+        MRNA transcriptFragment1 = MRNA.findByUniqueName(transcriptFragment1UniqueName)
+        CDS transcriptFragment1Cds = transcriptService.getCDS(transcriptFragment1)
+
+        assert transcriptService.getGene(transcriptFragment1) == parentGene
+        assert transcriptFragment1Cds.featureLocation.fmin == 690442
+        assert transcriptFragment1Cds.featureLocation.fmax == 690739
+        assert !transcriptFragment1Cds.featureLocation.isFminPartial
+        assert transcriptFragment1Cds.featureLocation.isFmaxPartial
+
+        when: "we add transcript fragment 2"
+        JSONObject addTranscriptFragment2ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment2String) as JSONObject)
+        String transcriptFragment2UniqueName = addTranscriptFragment2ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment"
+        assert MRNA.all.size() == 3
+        MRNA transcriptFragment2 = MRNA.findByUniqueName(transcriptFragment2UniqueName)
+        CDS transcriptFragment2Cds = transcriptService.getCDS(transcriptFragment2)
+
+        assert transcriptService.getGene(transcriptFragment2) == parentGene
+        assert transcriptFragment2Cds.featureLocation.fmin == 691158
+        assert transcriptFragment2Cds.featureLocation.fmax == 691353
+        assert transcriptFragment2Cds.featureLocation.isFminPartial
+        assert transcriptFragment2Cds.featureLocation.isFmaxPartial
+
+        when: "we add transcript fragment 3"
+        JSONObject addTranscriptFragment3ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment3String) as JSONObject)
+        String transcriptFragment3UniqueName = addTranscriptFragment3ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment"
+        assert MRNA.all.size() == 4
+        MRNA transcriptFragment3 = MRNA.findByUniqueName(transcriptFragment3UniqueName)
+        CDS transcriptFragment3Cds = transcriptService.getCDS(transcriptFragment3)
+
+        assert transcriptService.getGene(transcriptFragment3) == parentGene
+        assert transcriptFragment3Cds.featureLocation.fmin == 691674
+        assert transcriptFragment3Cds.featureLocation.fmax == 691846
+        assert transcriptFragment3Cds.featureLocation.isFminPartial
+        assert transcriptFragment3Cds.featureLocation.isFmaxPartial
+    }
+
+    void "When adding a transcript (on the forward strand) and transcript fragment, all the fragments should be isoforms of the main transcript and have their CDS set as expected"() {
+
+        given: "a transcript and 3 transcript fragments"
+        String addTranscriptString = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":577493,\"strand\":1,\"fmax\":583605},\"name\":\"GB40819-RA\",\"children\":[{\"location\":{\"fmin\":583280,\"strand\":1,\"fmax\":583605},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":577493,\"strand\":1,\"fmax\":577643},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":582506,\"strand\":1 [...]
+        String addTranscriptFragment1String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":577493,\"strand\":1,\"fmax\":577643},\"name\":\"GB40819-RA\",\"children\":[{\"location\":{\"fmin\":577493,\"strand\":1,\"fmax\":577643},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addTranscriptFragment2String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":582506,\"strand\":1,\"fmax\":582677},\"name\":\"GB40819-RA\",\"children\":[{\"location\":{\"fmin\":582506,\"strand\":1,\"fmax\":582677},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String addTranscriptFragment3String = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":583187,\"strand\":1,\"fmax\":583605},\"name\":\"GB40819-RA\",\"children\":[{\"location\":{\"fmin\":583187,\"strand\":1,\"fmax\":583605},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        when: "we add transcript GB40819-RA"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the transcript"
+        assert MRNA.all.size() == 1
+        Gene parentGene = Gene.all.get(0)
+
+        when: "we add transcript fragment 1"
+        JSONObject addTranscriptFragment1ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment1String) as JSONObject)
+        String transcriptFragment1UniqueName = addTranscriptFragment1ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment"
+        assert MRNA.all.size() == 2
+        MRNA transcriptFragment1 = MRNA.findByUniqueName(transcriptFragment1UniqueName)
+        CDS transcriptFragment1Cds = transcriptService.getCDS(transcriptFragment1)
+
+        assert transcriptService.getGene(transcriptFragment1) == parentGene
+        assert transcriptFragment1Cds.featureLocation.fmin == 577493
+        assert transcriptFragment1Cds.featureLocation.fmax == 577643
+        assert !transcriptFragment1Cds.featureLocation.isFminPartial
+        assert transcriptFragment1Cds.featureLocation.isFmaxPartial
+
+        when: "we add transcript fragment 2"
+        JSONObject addTranscriptFragment2ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment2String) as JSONObject)
+        String transcriptFragment2UniqueName = addTranscriptFragment2ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment"
+        assert MRNA.all.size() == 3
+        MRNA transcriptFragment2 = MRNA.findByUniqueName(transcriptFragment2UniqueName)
+        CDS transcriptFragment2Cds = transcriptService.getCDS(transcriptFragment2)
+
+        assert transcriptService.getGene(transcriptFragment2) == parentGene
+        assert transcriptFragment2Cds.featureLocation.fmin == 582506
+        assert transcriptFragment2Cds.featureLocation.fmax == 582677
+        assert transcriptFragment2Cds.featureLocation.isFminPartial
+        assert transcriptFragment2Cds.featureLocation.isFmaxPartial
+
+        when: "we add transcript fragment 3"
+        JSONObject addTranscriptFragment3ReturnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptFragment3String) as JSONObject)
+        String transcriptFragment3UniqueName = addTranscriptFragment3ReturnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we should see the transcript fragment but it shouldn't be an isoform of the main transcript"
+        assert MRNA.all.size() == 4
+        MRNA transcriptFragment3 = MRNA.findByUniqueName(transcriptFragment3UniqueName)
+        CDS transcriptFragment3Cds = transcriptService.getCDS(transcriptFragment3)
+
+        assert transcriptService.getGene(transcriptFragment3) != parentGene
+        assert transcriptFragment3Cds.featureLocation.fmin == 583188
+        assert transcriptFragment3Cds.featureLocation.fmax == 583554
+        assert transcriptFragment3Cds.featureLocation.isFminPartial
+        assert !transcriptFragment3Cds.featureLocation.isFmaxPartial
+    }
+}
diff --git a/test/integration/org/bbop/apollo/Gff3HandlerServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/Gff3HandlerServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..4d2ddb5
--- /dev/null
+++ b/test/integration/org/bbop/apollo/Gff3HandlerServiceIntegrationSpec.groovy
@@ -0,0 +1,61 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+
+class Gff3HandlerServiceIntegrationSpec extends AbstractIntegrationSpec{
+   
+    def gff3HandlerService
+    def requestHandlingService
+
+
+    void "write a GFF3 of a simple gene model"() {
+
+
+        given: "we create a new gene"
+        String json=" { ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}} [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"non_reserved_properties\": [{\"tag\": \"justification\", \"value\":\"Sanger sequencing\"}], \"residues\":\"GGG\",\"location\":{\"fmin\":208499,\"strand\":1,\"fmax\":208499},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],${testCredentials} \"track\":\"Group1.10\",\"clientToken\":\"123123\"}"
+        String pseudogene = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":433518,\"fmax\":437436,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"pseudogene\"},\"children\":[{\"location\":{\"fmin\":433518,\"fmax\":437436,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"transcript\"},\"name\":\"GB40815-RA\",\"children\":[{\"location\":{\"fmin\":433518,\"fmax\":433570,\"strand\":1},\"type\":{\"cv\":{\"name\":\"seque [...]
+        String repeat_region = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":414369,\"fmax\":414600,\"strand\":0},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"repeat_region\"},\"name\":\"GB40814-RA\"}], ${testCredentials} \"operation\": \"add_feature\" }"
+
+        when: "we parse the json"
+        requestHandlingService.addTranscript(JSON.parse(json))
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString))
+        requestHandlingService.addFeature(JSON.parse(pseudogene))
+        requestHandlingService.addFeature(JSON.parse(repeat_region))
+        
+
+        then: "We should have at least one new gene"
+
+        log.debug "${Gene.findAll()}"
+        assert Gene.count == 2
+        assert MRNA.count == 1
+        assert RepeatRegion.count == 1
+        assert Pseudogene.count == 1
+        assert Exon.count == 8
+        assert CDS.count == 1
+
+
+        when: "we write the feature to test"
+        File tempFile = File.createTempFile("output", ".gff3")
+        tempFile.deleteOnExit()
+        log.debug "${tempFile.absolutePath}"
+        def featuresToWrite = Gene.list(sort: "class")+SequenceAlteration.findAll()+RepeatRegion.findAll()
+        gff3HandlerService.writeFeaturesToText(tempFile.absolutePath,featuresToWrite,".")
+        String tempFileText = tempFile.text
+
+        then: "we should get a valid gff3 file"
+        log.debug "${tempFileText}"
+        def lines = tempFile.readLines()
+        assert lines[0]=="##gff-version 3"
+        assert lines[2].split("\t")[2]=="gene"
+        assert lines[2].split("\t")[8].indexOf("Name=GB40856-RA")!=-1
+        assert lines[3].split("\t")[2]=="mRNA"
+        assert lines[15].split("\t")[2]=="pseudogene"
+        assert lines[21].split("\t")[2]=="insertion"
+        assert lines[21].split("\t")[8].indexOf("justification=Sanger sequencing")!=-1
+        assert lines[21].split("\t")[8].indexOf("residues=GGG")!=-1
+        assert lines[23].split("\t")[2]=="repeat_region"
+
+        assert tempFileText.length() > 0
+    }
+}
diff --git a/test/integration/org/bbop/apollo/OrganismServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/OrganismServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..d5f235b
--- /dev/null
+++ b/test/integration/org/bbop/apollo/OrganismServiceIntegrationSpec.groovy
@@ -0,0 +1,55 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Ignore
+
+class OrganismServiceIntegrationSpec extends AbstractIntegrationSpec{
+
+    def organismService
+    def requestHandlingService
+
+
+    @Ignore
+    void "deleteAllFeaturesFromOrganism"() {
+
+        given: "a transcript with a UTR"
+        String jsonString = " { ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\" [...]
+        String validJSONString = "{\"features\":[{\"location\":{\"fmin\":1216824,\"strand\":1,\"fmax\":1235616},\"parent_type\":{\"name\":\"gene\",\"cv\":{\"name\":\"sequence\"}},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235237,\"strand\":1,\"fmax\":1235396},\"parent_type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}},\"properties\":[{\"value\":\"demo\",\"type\":{\"name\":\"owner\",\"cv\":{\"name\":\"feature_property\"}}}],\"uniquename\":\"@TRANSCRIPT_NAME@\",\ [...]
+
+
+        when: "You add a transcript via JSON"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "there should be no features"
+        assert Organism.count == 1
+        assert Sequence.count == 1
+        assert Organism.first().sequences?.size() == 1
+        // there are 6 exons, but 2 of them overlap . . . so this is correct
+        assert Exon.count == 5
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        assert FeatureEvent.count == 1
+
+        // this is the new part
+        assert FeatureLocation.count == 8
+        assert Feature.count == 8
+
+        when: "we call delete all for organism"
+        int deleted = organismService.deleteAllFeaturesForOrganism(Organism.first())
+
+
+        then: "we should not have any features left"
+//        assert 1==deleted
+        assert 1==Sequence.count
+        assert 1==Organism.count
+        assert null==Sequence.first()?.featureLocations
+        assert 0==FeatureLocation.count
+        assert 0==FeatureRelationship.count
+        assert 0==Feature.count
+        assert 0==FeatureEvent.count
+
+    }
+}
diff --git a/test/integration/org/bbop/apollo/OverlapperServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/OverlapperServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..6472938
--- /dev/null
+++ b/test/integration/org/bbop/apollo/OverlapperServiceIntegrationSpec.groovy
@@ -0,0 +1,247 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+class OverlapperServiceIntegrationSpec extends AbstractIntegrationSpec{
+
+    def requestHandlingService
+
+
+    void "isoform overlap test for GB40772-RA loci"() {
+
+        given: "A set of isoforms at GB40772-RA loci"
+        String mainTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222245},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222245},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String mainTranscript2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222043,\"strand\":1,\"fmax\":222267},\"name\":\"au9.g284.t1\",\"children\":[{\"location\":{\"fmin\":222043,\"strand\":1,\"fmax\":222267},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        // isoforms for mainTranscript1 : GB40772-RA
+        String isoform1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222211},\"name\":\"au12.g285.t1\",\"children\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222211},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String isoform2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222245},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_57_mRNA\",\"children\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222245},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String isoform3 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222183},\"name\":\"au9.g283.t1\",\"children\":[{\"location\":{\"fmin\":222081,\"strand\":-1,\"fmax\":222183},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        // isoform for mainTranscript2 : au9.g284.t1
+        String isoform4 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":222041,\"strand\":1,\"fmax\":222267},\"name\":\"au12.g286.t1\",\"children\":[{\"location\":{\"fmin\":222041,\"strand\":1,\"fmax\":222267},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        when: "we add mainTranscript1 and mainTranscript2"
+        JSONObject mainTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript1) as JSONObject)
+        JSONObject mainTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript2) as JSONObject)
+
+        then: "we should see 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        JSONObject features1 = mainTranscript1ReturnObject.get("features")
+        JSONObject features2 = mainTranscript2ReturnObject.get("features")
+        String mrna1Parent = features1.parent_id
+        String mrna2Parent = features2.parent_id
+
+        when: "we add isoform1: au12.g285.t1"
+        JSONObject isoform1ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform1) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform1Features = isoform1ReturnObject.get("features")
+        String isoform1Parent = isoform1Features.parent_id
+
+        assert isoform1Parent == mrna1Parent
+
+        when: "we add isoform2: fgeneshpp_with_rnaseq_Group1.10_57_mRNA"
+        JSONObject isoform2ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform2) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform2Features = isoform2ReturnObject.get("features")
+        String isoform2Parent = isoform2Features.parent_id
+
+        assert isoform2Parent == mrna1Parent
+
+        when: "we add isoform3: au9.g283.t1"
+        JSONObject isoform3ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform3) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform3Features = isoform3ReturnObject.get("features")
+        String isoform3Parent = isoform3Features.parent_id
+
+        assert isoform3Parent == mrna1Parent
+
+        when: "we add isoform4: au12.g286.t1"
+        JSONObject isoform4ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform4) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript2"
+        JSONObject isoform4Features = isoform4ReturnObject.get("features")
+        String isoform4Parent = isoform4Features.parent_id
+
+        assert isoform4Parent == mrna2Parent
+    }
+
+    void "isoform overlap test for GB40740-RA loci"() {
+
+        given: "A set of isoforms at GB40740-RA loci"
+        String mainTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":836988},\"name\":\"GB40740-RA\",\"children\":[{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":787740},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":787022,\"strand\":-1,\"fmax\":788349},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":789768,\"strand\":-1, [...]
+
+        // isoforms for mainTranscript1
+        String overlappingTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":828378,\"strand\":-1,\"fmax\":829272},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_188_mRNA\",\"children\":[{\"location\":{\"fmin\":828378,\"strand\":-1,\"fmax\":829272},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",\"clientToken\":\"123123\"}"
+        String overlappingTranscript2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":825210,\"strand\":-1,\"fmax\":831086},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_188_mRNA\",\"children\":[{\"location\":{\"fmin\":827237,\"strand\":-1,\"fmax\":827327},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":825210,\"strand\":-1,\"fmax\":825237},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"locat [...]
+
+        when: "we add mainTranscript1"
+        JSONObject mainTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript1) as JSONObject)
+
+        then: "we should see 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        JSONObject features1 = mainTranscript1ReturnObject.get("features")
+        String mrna1Parent = features1.parent_id
+
+        when: "we add overlappingTranscript1: fgeneshpp_with_rnaseq_Group1.10_188_mRNA"
+        JSONObject overlappingTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(overlappingTranscript1) as JSONObject)
+
+        then: "we should expect the added transcript to have its parent different from mainTranscript1"
+        JSONObject overlappingTranscript1Features = overlappingTranscript1ReturnObject.get("features")
+        String overlappingTranscript1Parent = overlappingTranscript1Features.parent_id
+
+        assert overlappingTranscript1Parent != mrna1Parent
+
+        when: "we add overlappingTranscript2: fgeneshpp_with_rnaseq_Group1.10_188_mRNA"
+        JSONObject overlappingTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(overlappingTranscript2) as JSONObject)
+
+        then: "we should expect the added transcript to have its parent different from mainTranscript1"
+        JSONObject overlappingTranscript2Features = overlappingTranscript2ReturnObject.get("features")
+        String overlappingTranscript2Parent = overlappingTranscript2Features.parent_id
+
+        assert overlappingTranscript2Parent != mrna1Parent
+    }
+
+    void "isoform overlap test for GB40730-RA loci"() {
+
+        given: "A set of isoforms at GB40730-RA loci"
+        String mainTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1003945,\"strand\":-1,\"fmax\":1018115},\"name\":\"GB40730-RA\",\"children\":[{\"location\":{\"fmin\":1003945,\"strand\":-1,\"fmax\":1004025},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1003945,\"strand\":-1,\"fmax\":1004076},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1004578,\"stran [...]
+        String mainTranscript2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1007258,\"strand\":1,\"fmax\":1012113},\"name\":\"GB40834-RA\",\"children\":[{\"location\":{\"fmin\":1007258,\"strand\":1,\"fmax\":1007559},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1009993,\"strand\":1,\"fmax\":1010650},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1011441,\"strand\" [...]
+
+        // isoforms for mainTranscript1 : GB40730-RA
+        String isoform1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1002555,\"strand\":-1,\"fmax\":1011938},\"name\":\"au8.g331.t1\",\"children\":[{\"location\":{\"fmin\":1002555,\"strand\":-1,\"fmax\":1002672},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1011721,\"strand\":-1,\"fmax\":1011938},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1002555,\"strand\":-1 [...]
+
+        // isoforms for mainTranscript2 : GB40834-RA
+        String isoform2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1009768,\"strand\":1,\"fmax\":1010650},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_219_mRNA\",\"children\":[{\"location\":{\"fmin\":1009768,\"strand\":1,\"fmax\":1010650},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}, \"use_cds\":\"true\"}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String isoform3 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1007258,\"strand\":1,\"fmax\":1012113},\"name\":\"fgeneshpp_with_rnaseq_Group1.10_219_mRNA\",\"children\":[{\"location\":{\"fmin\":1007258,\"strand\":1,\"fmax\":1007577},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1009768,\"strand\":1,\"fmax\":1010650},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fm [...]
+
+        when: "we add mainTranscript1 and mainTranscript2"
+        JSONObject mainTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript1) as JSONObject)
+        JSONObject mainTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript2) as JSONObject)
+
+        then: "we should see 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        JSONObject features1 = mainTranscript1ReturnObject.get("features")
+        JSONObject features2 = mainTranscript2ReturnObject.get("features")
+        String mrna1Parent = features1.parent_id
+        String mrna2Parent = features2.parent_id
+
+        when: "we add isoform1: au8.g331.t1"
+        JSONObject isoform1ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform1) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform1Features = isoform1ReturnObject.get("features")
+        String isoform1Parent = isoform1Features.parent_id
+
+        assert isoform1Parent == mrna1Parent
+
+        when: "we add isoform2: fgeneshpp_with_rnaseq_Group1.10_219_mRNA"
+        JSONObject isoform2ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform2) as JSONObject)
+
+        then: "we should expect the added isoform to not have its parent similar to mainTranscript2"
+        JSONObject isoform2Features = isoform2ReturnObject.get("features")
+        String isoform2Parent = isoform2Features.parent_id
+
+        assert isoform2Parent != mrna2Parent
+
+        when: "we add isoform3: fgeneshpp_with_rnaseq_Group1.10_219_mRNA"
+        JSONObject isoform3ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform3) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript2"
+        JSONObject isoform3Features = isoform3ReturnObject.get("features")
+        String isoform3Parent = isoform3Features.parent_id
+
+        assert isoform3Parent == mrna2Parent
+    }
+
+    void "isoform overlap test for GB40797-RA loci"() {
+
+        given: "A set of isoforms at GB40797-RA loci"
+        String mainTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":136964},\"name\":\"GB40797-RA\",\"children\":[{\"location\":{\"fmin\":136502,\"strand\":1,\"fmax\":136964},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":128706,\"strand\":1,\"fmax\":128768},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":131040,\"strand\":1,\"fm [...]
+
+        // isoforms for mainTranscript1 : GB40797-RA
+        String isoform1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":130713,\"strand\":1,\"fmax\":133159},\"name\":\"au9.g263.t1\",\"children\":[{\"location\":{\"fmin\":130713,\"strand\":1,\"fmax\":130945},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":131040,\"strand\":1,\"fmax\":131044},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":132749,\"strand\":1,\"fmax\":1 [...]
+        String isoform2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":133703,\"strand\":1,\"fmax\":139070},\"name\":\"au9.g264.t1\",\"children\":[{\"location\":{\"fmin\":133703,\"strand\":1,\"fmax\":133832},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":134173,\"strand\":1,\"fmax\":134185},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":136502,\"strand\":1,\"fmax\":1 [...]
+        String isoform3 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":130711,\"strand\":1,\"fmax\":130945},\"name\":\"au12.g266.t1\",\"children\":[{\"location\":{\"fmin\":130711,\"strand\":1,\"fmax\":130945},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        when: "we add mainTranscript1"
+        JSONObject mainTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript1) as JSONObject)
+
+        then: "we should see 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        JSONObject features1 = mainTranscript1ReturnObject.get("features")
+        String mrna1Parent = features1.parent_id
+
+        when: "we add isoform1: au9.g263.t1"
+        JSONObject isoform1ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform1) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform1Features = isoform1ReturnObject.get("features")
+        String isoform1Parent = isoform1Features.parent_id
+
+        assert isoform1Parent == mrna1Parent
+
+        when: "we add isoform2: au9.g264.t1"
+        JSONObject isoform2ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform2) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform2Features = isoform2ReturnObject.get("features")
+        String isoform2Parent = isoform2Features.parent_id
+
+        assert isoform2Parent == mrna1Parent
+
+        when: "we add isoform3: au12.g266.t1"
+        JSONObject isoform3ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform3) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform3Features = isoform3ReturnObject.get("features")
+        String isoform3Parent = isoform3Features.parent_id
+
+        assert isoform3Parent != mrna1Parent
+    }
+
+    void "isoform overlap test for GB40810-RA loci"() {
+
+        given: "A set of isoforms at GB40810-RA loci"
+        String mainTranscript1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":335756,\"strand\":1,\"fmax\":337187},\"name\":\"GB40810-RA\",\"children\":[{\"location\":{\"fmin\":335756,\"strand\":1,\"fmax\":336018},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":335756,\"strand\":1,\"fmax\":336120},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":336248,\"strand\":1,\"fm [...]
+
+        // isoforms for mainTranscript1
+        String isoform1 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":335908,\"strand\":1,\"fmax\":336120},\"name\":\"au12.g294.t1\",\"children\":[{\"location\":{\"fmin\":335908,\"strand\":1,\"fmax\":336120},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String isoform2 = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":335944,\"strand\":1,\"fmax\":337531},\"name\":\"au8.g298.t1\",\"children\":[{\"location\":{\"fmin\":335944,\"strand\":1,\"fmax\":336018},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":337187,\"strand\":1,\"fmax\":337531},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":335944,\"strand\":1,\"fmax\":3 [...]
+
+        when: "we add mainTranscript1"
+        JSONObject mainTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(mainTranscript1) as JSONObject)
+
+        then: "we should see 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        JSONObject features1 = mainTranscript1ReturnObject.get("features")
+        String mrna1Parent = features1.parent_id
+
+        when: "we add isoform1: au12.g294.t1"
+        JSONObject isoform1ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform1) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform1Features = isoform1ReturnObject.get("features")
+        String isoform1Parent = isoform1Features.parent_id
+
+        assert isoform1Parent == mrna1Parent
+
+        when: "we add isoform2: au8.g298.t1"
+        JSONObject isoform2ReturnObject = requestHandlingService.addTranscript(JSON.parse(isoform2) as JSONObject)
+
+        then: "we should expect the added isoform to have its parent similar to mainTranscript1"
+        JSONObject isoform2Features = isoform2ReturnObject.get("features")
+        String isoform2Parent = isoform2Features.parent_id
+
+        assert isoform2Parent == mrna1Parent
+    }
+}
diff --git a/test/integration/org/bbop/apollo/PhoneHomeServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/PhoneHomeServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..5d7daac
--- /dev/null
+++ b/test/integration/org/bbop/apollo/PhoneHomeServiceIntegrationSpec.groovy
@@ -0,0 +1,15 @@
+package org.bbop.apollo
+
+class PhoneHomeServiceIntegrationSpec extends AbstractIntegrationSpec {
+
+    def phoneHomeService
+
+    void "test ping"() {
+        when: "we ping the server"
+        def json = phoneHomeService.pingServer()
+
+        then: "we should get an empty response"
+        assert "{}" == json.toString()
+    }
+
+}
diff --git a/test/integration/org/bbop/apollo/PreferenceServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/PreferenceServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..d8b2735
--- /dev/null
+++ b/test/integration/org/bbop/apollo/PreferenceServiceIntegrationSpec.groovy
@@ -0,0 +1,1020 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.apache.shiro.SecurityUtils
+import org.apache.shiro.authc.UsernamePasswordToken
+import org.apache.shiro.subject.Subject
+import org.apache.shiro.util.ThreadContext
+import org.apache.shiro.web.session.mgt.DefaultWebSessionManager
+import org.bbop.apollo.gwt.shared.ClientTokenGenerator
+import org.bbop.apollo.preference.UserOrganismPreferenceDTO
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+class PreferenceServiceIntegrationSpec extends AbstractIntegrationSpec {
+
+
+    def preferenceService
+    def annotatorService
+    def requestHandlingService
+
+
+    def setupDefaultUserOrg() {
+
+        if (User.findByUsername('test at test.com')) {
+            return
+        }
+
+        User testUser = new User(
+                username: 'test at test.com'
+                , firstName: 'Bob'
+                , lastName: 'Test'
+                , passwordHash: passwordHash
+        ).save(insert: true, flush: true)
+        def adminRole = Role.findByName(UserService.ADMIN)
+        testUser.addToRoles(adminRole)
+        testUser.save()
+
+        shiroSecurityManager.sessionManager = new DefaultWebSessionManager()
+        ThreadContext.bind(shiroSecurityManager)
+        def authToken = new UsernamePasswordToken(testUser.username, password as String)
+        Subject subject = SecurityUtils.getSubject()
+        subject.login(authToken)
+
+        Organism organism1 = new Organism(
+                directory: "test/integration/resources/sequences/honeybee-tracks/"
+                , commonName: "honeybee"
+                , genus: "Honey"
+                , species: "bee"
+        ).save(failOnError: true)
+
+        Sequence sequence1Org1 = new Sequence(
+                length: 1405242
+                , seqChunkSize: 20000
+                , start: 0
+                , end: 1405242
+                , organism: organism1
+                , name: "Group1.10"
+        ).save(failOnError: true)
+
+        Sequence sequence2Org1 = new Sequence(
+                length: 78258
+                , seqChunkSize: 20000
+                , start: 0
+                , end: 78258
+                , organism: organism1
+                , name: "GroupUn87"
+        ).save(failOnError: true)
+
+
+        organism1.addToSequences(sequence1Org1)
+        organism1.addToSequences(sequence2Org1)
+        organism1.save(flush: true, failOnError: true)
+
+        Organism organism2 = new Organism(
+                directory: "test/integration/resources/sequences/yeast/"
+                , commonName: "yeast"
+                , genus: "Sample"
+                , species: "animal"
+        ).save(failOnError: true)
+
+        Sequence sequence1Org2 = new Sequence(
+                length: 230208
+                , seqChunkSize: 20000
+                , start: 0
+                , end: 230208
+                , organism: organism2
+                , name: "chrI"
+        ).save(failOnError: true)
+
+        Sequence sequence2Org2 = new Sequence(
+                length: 813178
+                , seqChunkSize: 20000
+                , start: 0
+                , end: 813178
+                , organism: organism2
+                , name: "chrII"
+        ).save(failOnError: true)
+
+        organism2.addToSequences(sequence1Org2)
+        organism2.addToSequences(sequence2Org2)
+        organism2.save(flush: true, failOnError: true)
+
+
+    }
+
+    void "change organisms"() {
+
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to organism 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+
+        when: "we set some location data and flush the preference saved on organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism2.name, 100, 200, token)
+
+        then: "we verify that it is saved on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we change organisms back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on organism 1 flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 300, 400, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we go back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+    }
+
+    void "change sequences between one organism"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1, sequence 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to sequence 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, token)
+        preferenceService.evaluateSaves(true)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence2Organism1.end
+
+        when: "we set some location data and flush the preference saved on sequence 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence2Organism1.name, 300, 400, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it is saved on organism 1, sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we change organisms back to sequence 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence1Organism1, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on sequence 1 and flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 100, 200, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to sequence 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, token)
+        preferenceService.evaluateSaves(true)
+
+        then: "we verify that the location / sequence is as we set it for sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we go back to sequence 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence1Organism1, token)
+        preferenceService.evaluateSaves(true)
+
+        then: "we verify that the location / sequence is as we set it for sequence 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+    }
+
+
+    void "change sequences between organisms and verify they are the same when returning"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1, sequence 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to sequence 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, token)
+        preferenceService.evaluateSaves(true)
+
+        then: "verify some other things on organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence2Organism1.end
+
+        when: "we set some location data and flush the preference saved on sequence 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence2Organism1.name, 300, 400, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it is saved on organism 1, sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+        when: "we change organisms back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+
+        then: "we verify that it has been moved to 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+        when: "we set the location on organism 2 flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism2.name, 100, 200, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+        preferenceService.evaluateSaves(true)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+
+        // NOTE: this line fails for MySQL, yeilding sequence1Organism1.name instead
+//        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+//        assert userOrganismPreferenceDTO.startbp == 300
+//        assert userOrganismPreferenceDTO.endbp == 400
+//
+//        when: "we go back to organism 2"
+//        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+//        preferenceService.evaluateSaves(true)
+//
+//
+//        then: "we verify that the location / sequence is as we set it for organism 2"
+//        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+//        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+//        assert userOrganismPreferenceDTO.startbp == 100
+//        assert userOrganismPreferenceDTO.endbp == 200
+    }
+
+
+    void "repeat changing organism without flushing to DB"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to organism 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+
+        when: "we set some location data and flush the preference saved on organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism2.name, 100, 200, token)
+
+        then: "we verify that it is saved on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we change organisms back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on organism 1 flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 300, 400, token)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we go back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+    }
+
+    void "repeat changing sequence without flushing to DB"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1, sequence 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to sequence 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence2Organism1.end
+
+        when: "we set some location data and flush the preference saved on sequence 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence2Organism1.name, 300, 400, token)
+
+
+        then: "we verify that it is saved on organism 1, sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we change organisms back to sequence 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence1Organism1, token)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on sequence 1 and flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 100, 200, token)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to sequence 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, token)
+
+        then: "we verify that the location / sequence is as we set it for sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we go back to sequence 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence1Organism1, token)
+
+        then: "we verify that the location / sequence is as we set it for sequence 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+    }
+
+    void "repeat changing organism and sequence without flushing to DB"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to organism 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+
+        when: "we set some location data and flush the preference saved on organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism2.name, 100, 200, token)
+
+        then: "we verify that it is saved on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we change organisms back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on organism 1 flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 300, 400, token)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+
+        when: "we go back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we go back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+    }
+
+    void "changing organism and add a feature"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we add a transcript for organism 1"
+        String featureString2 = "{${getTestCredentials(token)} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40733-RA\",\"children\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        requestHandlingService.addTranscript(JSON.parse(featureString2) as JSONObject)
+
+        then: "we expect to see it"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+
+
+        when: "we switch to organism 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+
+        when: "we add a feature on organism 2"
+        String featureString = "{${getTestCredentials(token)} \"track\":\"chrI\",\"features\":[{\"location\":{\"fmin\":114919,\"fmax\":118315,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"YAL019W\",\"children\":[{\"location\":{\"fmin\":114919,\"fmax\":118315,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        requestHandlingService.addTranscript(JSON.parse(featureString) as JSONObject)
+
+        then: "we verify it made it"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+
+        when: "we change organisms back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "when we add a feature onto organism 1"
+        featureString2 = "{${getTestCredentials(token)} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40733-RA\",\"children\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        requestHandlingService.addTranscript(JSON.parse(featureString2) as JSONObject)
+
+
+        then: "we verify that we added one here"
+        assert Gene.count == 2
+        assert MRNA.count == 3
+    }
+
+    void "repeat changing organism and sequence and add features along the way"() {
+        given: "setting up two organisms and sequences"
+        String token = ClientTokenGenerator.generateRandomString()
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject = annotatorService.getAppState(token)
+
+        then: "verify some stuff on organism 1"
+        assert appStateObject.currentOrganism.commonName == organism1.commonName
+        assert appStateObject.currentSequence.name == sequence1Organism1.name
+
+        when: "we switch to organism 2"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+        then: "verify some other things on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+
+
+
+        when: "we set some location data and flush the preference saved on organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism2.name, 100, 200, token)
+
+        then: "we verify that it is saved on organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+
+        when: "we change organisms back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that it has been moved back 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism1.end
+
+
+        when: "we set the location on organism 1 flush preference"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 300, 400, token)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+        when: "we add a transcript for organism 1"
+        String featureString2 = "{${getTestCredentials(token)} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40733-RA\",\"children\":[{\"location\":{\"fmin\":974306,\"fmax\":975778,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        requestHandlingService.addTranscript(JSON.parse(featureString2) as JSONObject)
+
+        then: "we expect to see it"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+
+        when: "we go back to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+
+        when: "we add a feature on organism 2"
+        String featureString = "{${getTestCredentials(token)} \"track\":\"chrII\",\"features\":[{\"location\":{\"fmin\":114919,\"fmax\":118315,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"YAL019W\",\"children\":[{\"location\":{\"fmin\":114919,\"fmax\":118315,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\"}"
+        requestHandlingService.addTranscript(JSON.parse(featureString) as JSONObject)
+
+        then: "we verify it made it"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+
+
+        when: "we go back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, token)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+
+        when: "we add a transcript for organism 1"
+        requestHandlingService.addTranscript(JSON.parse(featureString2) as JSONObject)
+
+        then: "we expect to see it"
+        assert Gene.count == 2
+        assert MRNA.count == 3
+
+    }
+
+
+    void "change sequences between organisms and different tokens should remain independent"() {
+        given: "setting up two organisms and sequences"
+        String tokenA = "AAA"
+        String tokenB = "BBB"
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+//        Sequence sequence1Organism2 = Sequence.findAllByOrganism(organism2, [sort: "end", order: "desc"]).last()
+//        Sequence sequence2Organism2 = Sequence.findAllByOrganism(organism2, [sort: "end", order: "desc"]).first()
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject1 = annotatorService.getAppState(tokenA)
+        JSONObject appStateObject2 = annotatorService.getAppState(tokenB)
+        preferenceService.evaluateSaves(true)
+        def allPRefs = UserOrganismPreference.all
+        def tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        def tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+
+
+        then: "verify that we both start at organism 1, sequence 1"
+        assert appStateObject1.currentOrganism.commonName == organism1.commonName
+        assert appStateObject1.currentSequence.name == sequence1Organism1.name
+        assert appStateObject2.currentOrganism.commonName == organism1.commonName
+        assert appStateObject2.currentSequence.name == sequence1Organism1.name
+        assert tokenAPrefs.size() == 1
+        assert tokenBPrefs.size() == 1
+
+
+        when: "we set the tokenB location on organism 1"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence1Organism1.name, 100, 200, tokenB)
+        preferenceService.evaluateSaves(true, tokenB)
+        allPRefs = UserOrganismPreference.all
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+
+
+        then: "we verify that it has been flushed"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+        assert allPRefs.size() == 2
+        assert tokenAPrefs.size() == 1
+        assert tokenBPrefs.size() == 1
+        assert tokenBPrefs.first().startbp == 100
+        assert tokenBPrefs.first().endbp == 200
+
+
+        when: "we switch token A to sequence 2 of organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequence(user, sequence2Organism1, tokenA)
+        preferenceService.evaluateSaves(true, tokenA)
+        allPRefs = UserOrganismPreference.all
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        def tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        def tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+        then: "verify some other things on organism 1 for sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence2Organism1.end
+        assert allPRefs.size() == 3
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 1
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+        when: "we set location data of token A to sequence 2 and organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentSequenceLocation(sequence2Organism1.name, 300, 400, tokenA)
+        preferenceService.evaluateSaves(true, tokenA)
+        allPRefs = UserOrganismPreference.all
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "we verify that it is saved on organism 1, sequence 2"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence2Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 300
+        assert userOrganismPreferenceDTO.endbp == 400
+        assert allPRefs.size() == 3
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 1
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+        when: "we change token B to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, tokenB)
+        preferenceService.evaluateSaves(true, tokenB)
+        allPRefs = UserOrganismPreference.all
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "we verify that tokenB is organism2 and has the de novo preference"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism2.name
+        assert userOrganismPreferenceDTO.startbp == 0
+        assert userOrganismPreferenceDTO.endbp == sequence1Organism2.end
+        assert allPRefs.size() == 4
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 2
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+
+        when: "token B: go back to organism 1"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism1, tokenB)
+        preferenceService.evaluateSaves(true, tokenB)
+        allPRefs.first().save(flush: true)
+        allPRefs = UserOrganismPreference.findAll()
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1 (not from the previous setting"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.sequence.name == sequence1Organism1.name
+        assert userOrganismPreferenceDTO.startbp == 100
+        assert userOrganismPreferenceDTO.endbp == 200
+        assert allPRefs.size() == 4
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 2
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+    }
+
+    void "changing organisms with two tokens should give 4 preferences"() {
+        given: "setting up two organisms and sequences"
+        String tokenA = "AAA"
+        String tokenB = "BBB"
+        Organism organism1 = Organism.findByCommonName("honeybee") // honeybee
+        Sequence sequence1Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.last() // Group1.10
+        Sequence sequence2Organism1 = organism1.sequences.sort() { a, b -> a.end <=> b.end }.first()  // GroupUn87
+        Organism organism2 = Organism.findByCommonName("yeast")
+//        Sequence sequence1Organism2 = Sequence.findAllByOrganism(organism2, [sort: "end", order: "desc"]).last()
+//        Sequence sequence2Organism2 = Sequence.findAllByOrganism(organism2, [sort: "end", order: "desc"]).first()
+        Sequence sequence1Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.last()  // ChrII
+        Sequence sequence2Organism2 = organism2.sequences.sort() { a, b -> a.end <=> b.end }.first() // ChrI
+        User user = User.first()
+
+        when: "we setup the first two"
+        JSONObject appStateObject1 = annotatorService.getAppState(tokenA)
+        JSONObject appStateObject2 = annotatorService.getAppState(tokenB)
+        preferenceService.setCurrentOrganism(user,organism1,tokenA)
+        preferenceService.evaluateSaves(true)
+        preferenceService.setCurrentOrganism(user,organism1,tokenB)
+        preferenceService.evaluateSaves(true)
+        def allPRefs = UserOrganismPreference.all
+        def tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        def tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        def tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        def tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "verify that we both start at organism 1, sequence 1"
+        assert appStateObject1.currentOrganism.commonName == organism1.commonName
+        assert appStateObject2.currentOrganism.commonName == organism1.commonName
+        assert allPRefs.size() == 2
+        assert tokenAPrefs.size() == 1
+        assert tokenBPrefs.size() == 1
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+        when: "we get it for token A"
+        UserOrganismPreferenceDTO userOrganismPreferenceDTO = preferenceService.getSessionPreference(tokenA)
+
+        then: "we confirm that it is for the first token "
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.clientToken == tokenA
+
+        when: "we get it for token B"
+        userOrganismPreferenceDTO = preferenceService.getSessionPreference(tokenB)
+
+        then: "we confirm that it is for token B"
+        assert userOrganismPreferenceDTO.organism.commonName == organism1.commonName
+        assert userOrganismPreferenceDTO.clientToken == tokenB
+
+
+        when: "we change token A to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, tokenA)
+        preferenceService.evaluateSaves(true, tokenB)
+        allPRefs = UserOrganismPreference.all
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "we verify that tokenB is organism2 and has the de novo preference"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert allPRefs.size() == 3
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 1
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+
+        when: "token A: change to organism 2"
+        userOrganismPreferenceDTO = preferenceService.setCurrentOrganism(user, organism2, tokenB)
+        preferenceService.evaluateSaves(true, tokenB)
+        allPRefs = UserOrganismPreference.findAll()
+        tokenAPrefs = UserOrganismPreference.findAllByClientToken(tokenA)
+        tokenAPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenA, true)
+        tokenBPrefs = UserOrganismPreference.findAllByClientToken(tokenB)
+        tokenBPrefsCurrent = UserOrganismPreference.findAllByClientTokenAndCurrentOrganism(tokenB, true)
+
+
+        then: "we verify that the location / sequence is as we set it for organism 1 (not from the previous setting"
+        assert userOrganismPreferenceDTO.organism.commonName == organism2.commonName
+        assert allPRefs.size() == 4
+        assert tokenAPrefs.size() == 2
+        assert tokenBPrefs.size() == 2
+        assert tokenAPrefsCurrent.size() == 1
+        assert tokenBPrefsCurrent.size() == 1
+
+    }
+}
diff --git a/test/integration/org/bbop/apollo/RequestHandlingServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/RequestHandlingServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..806f8db
--- /dev/null
+++ b/test/integration/org/bbop/apollo/RequestHandlingServiceIntegrationSpec.groovy
@@ -0,0 +1,3899 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.IgnoreRest
+
+class RequestHandlingServiceIntegrationSpec extends AbstractIntegrationSpec {
+
+    def requestHandlingService
+    def featureService
+    def featureRelationshipService
+    def transcriptService
+    def cdsService
+    def sequenceService
+    def gff3HandlerService
+
+
+    void "add transcript with UTR"() {
+
+        given: "a transcript with a UTR"
+        String jsonString = " { ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"fmax\":1235616,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1216824,\"fmax\":1216850,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\" [...]
+        String validJSONString = '{"features":[{"location":{"fmin":1216824,"strand":1,"fmax":1235616},"parent_type":{"name":"gene","cv":{"name":"sequence"}},"name":"GB40856-RA","children":[{"location":{"fmin":1235237,"strand":1,"fmax":1235396},"parent_type":{"name":"mRNA","cv":{"name":"sequence"}},"properties":[{"value":"demo","type":{"name":"owner","cv":{"name":"feature_property"}}}],"uniquename":"@TRANSCRIPT_NAME@","type":{"name":"exon","cv":{"name":"sequence"}},"date_last_modified":14 [...]
+
+
+        when: "You add a transcript via JSON"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+        JSONObject correctJsonReturnObject = JSON.parse(validJSONString) as JSONObject
+
+        then: "there should be no features"
+        assert Feature.count == 0
+        assert FeatureLocation.count == 0
+        assert Sequence.count == 1
+        JSONArray mrnaArray = jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == mrnaArray.size()
+        assert 7 == getCodingArray(jsonObject).size()
+
+
+        when: "you parse add a transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+
+
+        then: "You should see that transcript"
+        assert Sequence.count == 1
+        // there are 6 exons, but 2 of them overlap . . . so this is correct
+        assert Exon.count == 5
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        def allFeatures = Feature.all
+
+        // this is the new part
+        assert FeatureLocation.count == 8
+        assert Feature.count == 8
+
+
+        JSONArray returnedCodingArray = getCodingArray(returnObject)
+        JSONArray validCodingArray = getCodingArray(correctJsonReturnObject)
+        assert returnedCodingArray.size() == validCodingArray.size()
+    }
+
+    JSONArray getCodingArray(JSONObject jsonObject) {
+        JSONArray mrnaArray = jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == mrnaArray.size()
+        return mrnaArray.getJSONObject(0).getJSONArray(FeatureStringEnum.CHILDREN.value)
+    }
+
+    void "add a transcript which is a single exon needs to translate correctly"() {
+
+        given: "the input string "
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB42152-RA\",\"children\":[{\"location\":{\"fmin\":1216824,\"fmax\":1235616,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}], \"operation\": \"add_transcript\" }"
+
+        when: "You add a transcript via JSON"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "there should be no features"
+        assert Feature.count == 0
+        assert FeatureLocation.count == 0
+        assert Sequence.count == 1
+        JSONArray mrnaArray = jsonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == mrnaArray.size()
+        JSONArray codingArray = mrnaArray.getJSONObject(0).getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 1 == codingArray.size()
+
+        when: "it gets added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+
+        then: "we should see the appropriate stuff"
+        assert Sequence.count == 1
+        // there are 6 exons, but 2 of them overlap . . . so this is correct
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        assert Exon.count == 1
+//        int flankingCount = FlankingRegion.count
+        int flankingCount = 0
+        assert Feature.count == 4 + flankingCount
+        assert FeatureLocation.count == 4 + flankingCount
+        assert FeatureRelationship.count == 3
+
+        Gene gene = Gene.first()
+        assert featureRelationshipService.getParentForFeature(gene) == null
+        assert featureRelationshipService.getChildren(gene).size() == 1
+        MRNA mrna = featureRelationshipService.getChildForFeature(gene, MRNA.ontologyId)
+        assert mrna.id == MRNA.first().id
+        List<Feature> childFeatureRelationships = featureRelationshipService.getParentsForFeature(mrna)
+        assert 1 == childFeatureRelationships.size()
+        Feature parentFeature = featureRelationshipService.getParentForFeature(mrna)
+        assert parentFeature != null
+        assert featureRelationshipService.getParentForFeature(mrna).id == gene.id
+        // should be an exon and a CDS . . .
+        assert featureRelationshipService.getChildren(mrna).size() == 2
+        Exon exon = featureRelationshipService.getChildForFeature(mrna, Exon.ontologyId)
+        CDS cds = featureRelationshipService.getChildForFeature(mrna, CDS.ontologyId)
+        assert exon != null
+        assert cds != null
+//        MRNA mrna = featureRelationshipService.getChildForFeature(mrna)
+
+    }
+
+    /**
+     * TODO: note, this sequence is for 1.1
+     */
+//    void "adding a transcript that returns a missing feature location in the mRNA"(){
+//
+//        given: "a input JSON string"
+//        String jsonString = "{ \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":976735,\"fmax\":995721,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB42183-RA\",\"children\":[{\"location\":{\"fmin\":995216,\"fmax\":995721,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":976735,\"fmax\":976888,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{ [...]
+//
+//        when: "we parse the string"
+//        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+//
+//        then: "we get a valid json object and no features"
+//        assert Feature.count == 0
+//
+//        when: "we add it to a UTR"
+//        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+//
+//        then: "we should get a transcript back" // we currently get nothing
+//
+////        def allFeatures = Feature.all
+////        int flankingRegionCount = FlankingRegion.count
+////        assert Feature.count == 7 + flankingRegionCount
+//        assert returnObject.getString('operation')=="ADD"
+//        assert returnObject.getBoolean('sequenceAlterationEvent')==false
+//        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+//        assert 1==featuresArray.size()
+//        JSONObject mrna = featuresArray.getJSONObject(0)
+//        assert "GB42183-RA-00001"==mrna.getString(FeatureStringEnum.NAME.value)
+//        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+//        assert 5==children.size()
+//        for(int i = 0 ; i < 5 ; i++){
+//            JSONObject codingObject = children.get(i)
+//            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+//            assert locationObject!=null
+//        }
+//
+//    }
+
+    /**
+     * TODO: note, this sequence is for 1.1
+     */
+//    void "adding another transcript with UTR fails to add GB42152-RA"(){
+//        given: "a input JSON string"
+//        String jsonString = "{ \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":561645,\"fmax\":566383,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB42152-RA\",\"children\":[{\"location\":{\"fmin\":566169,\"fmax\":566383,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":561645,\"fmax\":562692,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\ [...]
+//
+//        when: "we parse the string"
+//        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+//
+//        then: "we get a valid json object and no features"
+//        assert Feature.count == 0
+//
+//        when: "we add it to a UTR"
+//        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+//
+//        then: "we should get a transcript back" // we currently get nothing
+//        int flankingRegionCount = FlankingRegion.count
+//        assert Feature.count == 7 + flankingRegionCount
+////        log.debug returnObject as JSON
+//        assert returnObject.getString('operation')=="ADD"
+//        assert returnObject.getBoolean('sequenceAlterationEvent')==false
+//        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+//        assert 1==featuresArray.size()
+//        JSONObject mrna = featuresArray.getJSONObject(0)
+//        assert "GB42152-RA-00001"==mrna.getString(FeatureStringEnum.NAME.value)
+//        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+//        assert 5==children.size()
+//        for(int i = 0 ; i < 5 ; i++){
+//            JSONObject codingObject = children.get(i)
+//            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+//            assert locationObject!=null
+//        }
+//
+//    }
+
+    void "add a transcript with UTR"() {
+
+        given: "a valid JSON gtring"
+//        String validInputString = "{ \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1287738,\"fmax\":1289338,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40717-RA\",\"children\":[{\"location\":{\"fmin\":1289034,\"fmax\":1289338,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1287738,\"fmax\":1288189,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}, [...]
+        String apollo2InputString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1287738,\"strand\":-1,\"fmax\":1289338},\"name\":\"GB40717-RA\",\"children\":[{\"location\":{\"fmin\":1289034,\"strand\":-1,\"fmax\":1289338},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1287738,\"strand\":-1,\"fmax\":1288189},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1287738,\"st [...]
+
+        String validResponseString = "{${testCredentials} \"operation\":\"ADD\",\"sequenceAlterationEvent\":false,\"features\":[{\"location\":{\"fmin\":1287738,\"strand\":-1,\"fmax\":1289338},\"parent_type\":{\"name\":\"gene\",\"cv\":{\"name\":\"sequence\"}},\"name\":\"GB40717-RA\",\"children\":[{\"location\":{\"fmin\":1288942,\"strand\":-1,\"fmax\":1289338},\"parent_type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}},\"properties\":[{\"value\":\"demo\",\"type\":{\"name\":\"owner\" [...]
+
+//        String validFeatureString = "{\"features\":[{\"location\":{\"fmin\":1287738,\"strand\":-1,\"fmax\":1289338},\"parent_type\":{\"name\":\"gene\",\"cv\":{\"name\":\"sequence\"}},\"name\":\"GB40717-RA\",\"children\":[{\"location\":{\"fmin\":1288942,\"strand\":-1,\"fmax\":1289338},\"parent_type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}},\"properties\":[{\"value\":\"demo\",\"type\":{\"name\":\"owner\",\"cv\":{\"name\":\"feature_property\"}}}],\"uniquename\":\"57ED4B570156EE [...]
+
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(apollo2InputString) as JSONObject
+        JSONObject validJsonObject = JSON.parse(validResponseString) as JSONObject
+        JSONArray validCodingArray = getCodingArray(validJsonObject)
+//
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+//
+        when: "add UTR transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+        JSONArray returnCodingArray = getCodingArray(returnObject)
+
+        then: "we should get no noncanonical splice sites"
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 4
+//        assert NonCanonicalFivePrimeSpliceSite.count == 0
+//        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+        // not sure if the non-canonical are supposed to be there or not since at the edges
+//        assert validCodingArray.size() == returnCodingArray.size()-2
+
+
+    }
+
+    void "adding an exon to an existing transcript"() {
+
+        given: "a input addTranscriptFeaturesArrayJSON string"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":219994,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222109,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":219994,\"fmax\":220044,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        String exonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ {\"uniquename\": \"@TRANSCRIPT_NAME@\"}, {\"location\":{\"fmin\":218197,\"fmax\":218447,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}], \"operation\": \"add_exon\" }"
+        String validExonString = "{\"features\":[{\"location\":{\"fmin\":218197,\"strand\":-1,\"fmax\":222245},\"parent_type\":{\"name\":\"gene\",\"cv\":{\"name\":\"sequence\"}},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":219994,\"strand\":-1,\"fmax\":222109},\"parent_type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}},\"properties\":[{\"value\":\"demo\",\"type\":{\"name\":\"owner\",\"cv\":{\"name\":\"feature_property\"}}}],\"uniquename\":\"45F17D57F6025D3508087E8 [...]
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a transcript back" // we currently get nothing
+//        log.debug returnObject as JSON
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        // we are losing an exon somewhere!
+        assert Exon.count == 2
+        assert CDS.count == 1
+//        assert NonCanonicalFivePrimeSpliceSite.count==1
+//        assert NonCanonicalThreePrimeSpliceSite.count==1
+//        assert Feature.count == 5
+        assert "GB40772-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.get(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+
+
+        when: "we parse the string"
+        exonString = exonString.replaceAll("@TRANSCRIPT_NAME@", transcriptUniqueName)
+        JSONObject exonJsonObject = JSON.parse(exonString) as JSONObject
+        JSONObject validExonJsonObject = JSON.parse(validExonString) as JSONObject
+//
+//        then: "we get a valid json object and no features"
+//        assert Feature.count == 7
+
+//        when: "we add the exon explicitly"
+        JSONObject returnedAfterExonObject = requestHandlingService.addExon(exonJsonObject)
+
+        then: "we should see an exon added"
+        assert returnedAfterExonObject != null
+        log.debug Feature.count
+        assert Feature.count > 5
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert returnFeaturesArray.size() == 1
+        JSONObject mRNAObject = returnFeaturesArray.get(0)
+        assert mRNAObject.getString(FeatureStringEnum.NAME.value) == "GB40772-RA-00001"
+        JSONArray childrenArray = mRNAObject.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        // we are losing an exon somewhere!
+        assert Exon.count == 3
+        assert CDS.count == 1
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert childrenArray.size() == 6
+
+
+    }
+
+    void "flip strand on an existing transcript"() {
+
+        given: "a input JSON string"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":219994,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222109,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":219994,\"fmax\":220044,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_NAME@\" } ], \"operation\": \"flip_strand\" }"
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a transcript back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40772-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.get(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject.strand == -1
+            assert locationObject != null
+        }
+        assert MRNA.first().featureLocations.first().strand == -1
+        assert Gene.first().featureLocations.first().strand == -1
+        assert Exon.first().featureLocations.first().strand == -1
+        assert Exon.last().featureLocations.first().strand == -1
+
+
+        when: "we flip the strand"
+        commandString = commandString.replaceAll("@TRANSCRIPT_NAME@", transcriptUniqueName)
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.flipStrand(commandObject)
+
+        then: "we should see that we flipped the strand"
+        assert returnedAfterExonObject != null
+        log.debug Feature.count
+        assert Feature.count > 5
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert returnFeaturesArray.size() == 1
+        JSONObject mRNAObject = returnFeaturesArray.get(0)
+        assert mRNAObject.getString(FeatureStringEnum.NAME.value) == "GB40772-RA-00001"
+        JSONArray childrenArray = mRNAObject.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        // we are losing an exon somewhere!
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert childrenArray.size() == 5
+        assert MRNA.first().featureLocations.first().strand == 1
+        assert Gene.first().featureLocations.first().strand == 1
+        assert Exon.first().featureLocations.first().strand == 1
+        assert Exon.last().featureLocations.first().strand == 1
+
+        when: "we flip it back the other way"
+        returnedAfterExonObject = requestHandlingService.flipStrand(commandObject)
+        returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        mRNAObject = returnFeaturesArray.get(0)
+        childrenArray = mRNAObject.getJSONArray(FeatureStringEnum.CHILDREN.value)
+
+        then: "we should have no splice sites"
+        log.debug Feature.count
+        assert Feature.count == 5
+        assert returnFeaturesArray.size() == 1
+        assert mRNAObject.getString(FeatureStringEnum.NAME.value) == "GB40772-RA-00001"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        // we are losing an exon somewhere!
+        assert childrenArray.size() == 3
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert MRNA.first().featureLocations.first().strand == -1
+        assert Gene.first().featureLocations.first().strand == -1
+        assert Exon.first().featureLocations.first().strand == -1
+        assert Exon.last().featureLocations.first().strand == -1
+    }
+
+    void "flip strand on an existing transcript with two isoforms"() {
+
+        given: "a input JSON string"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":219994,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222109,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":219994,\"fmax\":220044,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_NAME@\" } ], \"operation\": \"flip_strand\" }"
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+
+        when: "we add the first transcript"
+        JSONObject returnObject1 = requestHandlingService.addTranscript(jsonObject)
+        JSONObject returnObject2 = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a transcript back"
+        assert returnObject1.getString('operation') == "ADD"
+        assert returnObject2.getString('operation') == "ADD"
+        JSONArray featuresArray1 = returnObject1.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray1.size()
+        JSONObject mrna1Object = featuresArray1.getJSONObject(0)
+        JSONArray featuresArray2 = returnObject2.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray2.size()
+        JSONObject mrna2Object = featuresArray2.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+        assert "GB40772-RA-00001" == mrna1Object.getString(FeatureStringEnum.NAME.value)
+        assert "GB40772-RA-00002" == mrna2Object.getString(FeatureStringEnum.NAME.value)
+
+
+        when: "we get the transcripts back"
+        MRNA mrna00001 = MRNA.findByName("GB40772-RA-00001")
+        MRNA mrna00002 = MRNA.findByName("GB40772-RA-00002")
+        Gene gene = Gene.first()
+        String gene1Name = gene.name
+        String transcript1UniqueName = mrna00001.uniqueName
+        JSONArray children = mrna1Object.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.get(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject.strand == -1
+            assert locationObject != null
+        }
+
+        then: "the strand should be correct"
+        assert mrna00001.featureLocations.first().strand == -1
+        assert mrna00002.featureLocations.first().strand == -1
+        assert gene.featureLocations.first().strand == -1
+        for (exon in Exon.all) {
+            assert exon.featureLocations.first().strand == -1
+        }
+
+
+        when: "we flip the strand for GB40772-RA-00001"
+        commandString = commandString.replaceAll("@TRANSCRIPT_NAME@", transcript1UniqueName)
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.flipStrand(commandObject)
+        Gene newGene = transcriptService.getGene(mrna00001)
+        CDS cds00001 = transcriptService.getCDS(mrna00001)
+        def exons00001 = transcriptService.getSortedExons(mrna00001,true)
+
+        Gene originalGene = transcriptService.getGene(mrna00002)
+        def exons00002 = transcriptService.getSortedExons(mrna00002, true)
+        CDS cds00002 = transcriptService.getCDS(mrna00002)
+
+
+        then: "we should see that we flipped the strand"
+        assert returnedAfterExonObject != null
+        log.debug Feature.count
+        assert Feature.count > 5
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert returnFeaturesArray.size() == 1
+        JSONObject mRNAObject00001 = returnFeaturesArray.get(0)
+        // transcript is named for new gene
+        // no need to rename the transcriptA
+
+
+        assert mRNAObject00001.getString(FeatureStringEnum.NAME.value) == "GB40772-RAa-00001"
+        JSONArray childrenArray = mRNAObject00001.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        // we are losing an exon somewhere!
+        assert Exon.count == 4
+        assert CDS.count == 2
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+
+        // have to rename the new gene
+        assert mrna00001.featureLocations.first().strand == 1
+        assert mrna00001.name == 'GB40772-RAa-00001'
+        assert newGene.name == 'GB40772-RAa'
+        assert newGene.featureLocations.first().strand == 1
+        assert cds00001.featureLocations.first().strand == 1
+        for (exon in exons00001) {
+            assert exon.featureLocations.first().strand == 1
+        }
+
+        assert originalGene.featureLocations.first().strand == -1
+        // sae gene
+        assert originalGene.name == 'GB40772-RA'
+        assert mrna00002.featureLocations.first().strand == -1
+        // sae transcript
+        assert mrna00002.name == 'GB40772-RA-00002'
+        assert cds00002.featureLocations.first().strand == -1
+        for (exon in exons00002) {
+            assert exon.featureLocations.first().strand == -1
+        }
+
+        when: "we flip it back the other way"
+        returnedAfterExonObject = requestHandlingService.flipStrand(commandObject)
+
+        newGene = transcriptService.getGene(mrna00001)
+        cds00001 = transcriptService.getCDS(mrna00001)
+        exons00001 = transcriptService.getSortedExons(mrna00001,true)
+
+        originalGene = transcriptService.getGene(mrna00002)
+        exons00002 = transcriptService.getSortedExons(mrna00002,true)
+        cds00002 = transcriptService.getCDS(mrna00002)
+        childrenArray = mRNAObject00001.getJSONArray(FeatureStringEnum.CHILDREN.value)
+
+
+        then: "we should have no splice sites"
+        log.debug Feature.count
+        assert Feature.count == 4 + 2 + 2 + 1
+        assert returnFeaturesArray.size() == 1
+        assert mRNAObject00001.getString(FeatureStringEnum.NAME.value) == "GB40772-RAa-00001"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        // we are losing an exon somewhere!
+        assert Exon.count == 4
+        assert CDS.count == 2
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+        assert newGene == originalGene
+
+        assert newGene.featureLocations.first().strand == -1
+        assert newGene.name == 'GB40772-RA'
+        assert mrna00001.featureLocations.first().strand == -1
+        assert mrna00001.name == 'GB40772-RA-00001'
+        assert cds00001.featureLocations.first().strand == -1
+        for (exon in exons00001) {
+            assert exon.featureLocations.first().strand == -1
+        }
+
+        assert originalGene.featureLocations.first().strand == -1
+        assert originalGene.name == 'GB40772-RA'
+        assert mrna00002.featureLocations.first().strand == -1
+        assert mrna00002.name == 'GB40772-RA-00002'
+        assert cds00002.featureLocations.first().strand == -1
+        for (exon in exons00002) {
+            assert exon.featureLocations.first().strand == -1
+        }
+
+
+    }
+
+    void "delete an entire transcript"() {
+
+        given: "a input JSON string"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":219994,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222109,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":219994,\"fmax\":220044,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_NAME@\" } ], \"operation\": \"delete_feature\" }"
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a transcript back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40772-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.get(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+
+
+        when: "we delete the transcript"
+        commandString = commandString.replaceAll("@TRANSCRIPT_NAME@", transcriptUniqueName)
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.deleteFeature(commandObject)
+
+        then: "we should see that it is removed"
+        def allFeatures = Feature.all
+        assert returnedAfterExonObject != null
+        assert Feature.count == 0
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert returnFeaturesArray.size() == 0
+    }
+
+    void "we should be able to delete an isoform"() {
+
+        given: "a input JSON string"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":219994,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40772-RA\",\"children\":[{\"location\":{\"fmin\":222109,\"fmax\":222245,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":219994,\"fmax\":220044,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT_NAME@\" } ], \"operation\": \"delete_feature\" }"
+
+        when: "we parse the string"
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        then: "we get a valid json object and no features"
+        assert Feature.count == 0
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a transcript back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40772-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.get(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+
+        when: "we add the second transcript"
+        returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should get a second isoform back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+        assert 1 == MRNA.countByName("GB40772-RA-00002")
+        assert 1 == MRNA.countByName("GB40772-RA-00001")
+
+
+        when: "we delete the transcript"
+        commandString = commandString.replaceAll("@TRANSCRIPT_NAME@", transcriptUniqueName)
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.deleteFeature(commandObject)
+
+        then: "we should see that it is removed with one left"
+//        def allFeatures = Feature.all
+        assert 1 == featuresArray.size()
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40772-RA-00002" == MRNA.first().name
+        assert returnedAfterExonObject != null
+
+        when: "we delete the second transcript"
+        String transcriptUniqueName2 = MRNA.first().uniqueName
+        commandString = commandString.replaceAll(transcriptUniqueName, transcriptUniqueName2)
+        commandObject = JSON.parse(commandString) as JSONObject
+        returnedAfterExonObject = requestHandlingService.deleteFeature(commandObject)
+
+
+        then: "we should see that all are removed"
+        assert returnedAfterExonObject != null
+        assert Feature.count == 0
+    }
+
+
+    void "splitting an exon should work and handle CDS calculations properly"() {
+
+        given: "a input JSON string"
+        String jsonAddTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":202764,\"fmax\":205331,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40806-RA\",\"children\":[{\"location\":{\"fmin\":202764,\"fmax\":203242,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":204576,\"fmax\":205331,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"nam [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_NAME@\", \"location\": { \"fmax\": 204749, \"fmin\": 204750 } } ], \"operation\": \"split_exon\" }"
+        JSONObject jsonAddTranscriptObject = JSON.parse(jsonAddTranscriptString) as JSONObject
+//        String validResponseString = "{\"features\":[{\"location\":{\"fmin\":202764,\"strand\":1,\"fmax\":205331},\"parent_type\":{\"name\":\"gene\",\"cv\":{\"name\":\"sequence\"}},\"name\":\"GB40806-RA\",\"children\":[{\"location\":{\"fmin\":204750,\"strand\":1,\"fmax\":205331},\"parent_type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}},\"uniquename\":\"A183623EE72EA859B745AF2349E8740E\",\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}},\"date_last_modified\":14260043 [...]
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonAddTranscriptObject)
+
+        then: "we should get a transcript back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40806-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+//        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.getJSONObject(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+
+        when: "we get the sorted exons"
+        List<Exon> sortedExons = transcriptService.getSortedExons(MRNA.first(), true)
+
+        then: "there should be 2 and in the right order"
+        assert sortedExons.size() == 2
+        assert sortedExons.get(0).featureLocation.fmax < sortedExons.get(1).featureLocation.fmin
+        String exonToSplitUniqueName = sortedExons.get(1).uniqueName
+        assert CDS.first().featureLocation.fmin == MRNA.first().featureLocation.fmin
+        assert CDS.first().featureLocation.fmax == MRNA.first().featureLocation.fmax
+
+
+
+        when: "we split an exon"
+        commandString = commandString.replaceAll("@EXON_NAME@", exonToSplitUniqueName)
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.splitExon(commandObject)
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        JSONObject returnMRNA = returnFeaturesArray.getJSONObject(0)
+        JSONArray returnedChildren = returnMRNA.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        List<Exon> finalSortedExons = transcriptService.getSortedExons(MRNA.first(), true)
+        Exon lastExon = finalSortedExons.get(2)
+
+        then: "we should see that it is removed"
+        def allFeatures = Feature.all
+        assert returnFeaturesArray.size() == 1
+        assert returnedAfterExonObject != null
+        assert 3 == returnedAfterExonObject.size() // operation update, features, sequence_alt_event, etc.
+        assert Gene.count == 1
+        assert MRNA.count == 1
+
+        // the 6 children
+        assert Exon.count == 3
+        assert CDS.count == 1
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert "GB40806-RA-00001" == returnMRNA.getString(FeatureStringEnum.NAME.value)
+        assert 6 == returnedChildren.size()
+        for (int i = 0; i < 6; i++) {
+            JSONObject codingObject = returnedChildren.getJSONObject(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+        assert finalSortedExons.size() == 3
+        assert CDS.first().featureLocation.fmin == MRNA.first().featureLocation.fmin
+        assert CDS.first().featureLocation.fmax < MRNA.first().featureLocation.fmax
+        assert CDS.first().featureLocation.fmax < MRNA.first().featureLocation.fmax
+
+        // the end of the CDS should be on the last exon
+        assert CDS.first().featureLocation.fmax < lastExon.featureLocation.fmax
+        assert CDS.first().featureLocation.fmax > lastExon.featureLocation.fmin
+
+
+    }
+
+
+    void "splitting a transcript should work properly"() {
+
+        given: "a input JSON string"
+        String jsonAddTranscriptString = "{${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":202764,\"fmax\":205331,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40806-RA\",\"children\":[{\"location\":{\"fmin\":202764,\"fmax\":203242,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":204576,\"fmax\":205331,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name [...]
+        String commandString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON1_UNIQUENAME@\" }, { \"uniquename\": \"@EXON2_UNIQUENAME@\" } ], \"operation\": \"split_transcript\" }"
+        JSONObject jsonAddTranscriptObject = JSON.parse(jsonAddTranscriptString) as JSONObject
+
+        when: "we add the first transcript"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonAddTranscriptObject)
+
+        then: "we should get a transcript back"
+        assert returnObject.getString('operation') == "ADD"
+        assert returnObject.getBoolean('sequenceAlterationEvent') == false
+        JSONArray featuresArray = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+        assert 1 == featuresArray.size()
+        JSONObject mrna = featuresArray.getJSONObject(0)
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        assert CDS.count == 1
+        assert "GB40806-RA-00001" == mrna.getString(FeatureStringEnum.NAME.value)
+//        String transcriptUniqueName = mrna.getString(FeatureStringEnum.UNIQUENAME.value)
+        JSONArray children = mrna.getJSONArray(FeatureStringEnum.CHILDREN.value)
+        assert 3 == children.size()
+        for (int i = 0; i < 3; i++) {
+            JSONObject codingObject = children.getJSONObject(i)
+            JSONObject locationObject = codingObject.getJSONObject(FeatureStringEnum.LOCATION.value)
+            assert locationObject != null
+        }
+
+        when: "we get the sorted exons"
+        List<Exon> sortedExons = transcriptService.getSortedExons(MRNA.first(), true)
+
+        then: "there should be 2 and in the right order"
+        assert sortedExons.size() == 2
+        assert sortedExons.get(0).featureLocation.fmax < sortedExons.get(1).featureLocation.fmin
+//        String exonToSplitUniqueName = sortedExons.get(1).uniqueName
+        assert CDS.first().featureLocation.fmin == MRNA.first().featureLocation.fmin
+        assert CDS.first().featureLocation.fmax == MRNA.first().featureLocation.fmax
+
+
+
+        when: "we split a transcript "
+        String uniqueName1 = sortedExons.get(0).uniqueName
+        String uniqueName2 = sortedExons.get(1).uniqueName
+        commandString = commandString.replaceAll("@EXON1_UNIQUENAME@", uniqueName1)
+        commandString = commandString.replaceAll("@EXON2_UNIQUENAME@", uniqueName2)
+
+        JSONObject commandObject = JSON.parse(commandString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.splitTranscript(commandObject)
+        JSONArray returnFeaturesArray = returnedAfterExonObject.getJSONArray(FeatureStringEnum.FEATURES.value)
+
+
+        then: "we should see 2 genes, each belonging to a transcript and having a single gene and exon"
+        def allFeatures = Feature.all
+        assert MRNA.count == 2
+        assert Exon.count == 2
+        assert Gene.count == 2
+        assert CDS.count == 2
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+
+        List<Gene> allGenes = Gene.all
+        assert allGenes.get(0).id != allGenes.get(1).id
+        assert allGenes.get(0).name != allGenes.get(1).name
+
+    }
+
+    void "merging transcript on the same strand leaves leftover stuff and doesn't update correctly"() {
+
+        given: "the GB40788-RA and GB40787-RA"
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+
+
+        when: "we add both transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+
+
+        then: "we should see 2 genes, 2 transcripts, 5 exons, 2 CDS, no noncanonical splice sites"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge the transcripts"
+        String uniqueName1 = MRNA.findByName("GB40787-RA-00001").uniqueName
+        String uniqueName2 = MRNA.findByName("GB40788-RA-00001").uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName1)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName2)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.mergeTranscripts(commandObject)
+
+
+        then: "we should see 1 gene, 1 transcripts, 5 exons, 1 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert CDS.count == 1
+
+    }
+
+    /**
+     * https://github.com/GMOD/Apollo/issues/413
+     */
+    void "merging transcript with an upstream isoform effects the non-merged isoform"() {
+
+        given: "the GB40788-RA and GB40787-RA"
+        String gb40787String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":77860,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40787-RA\",\"children\":[{\"location\":{\"fmin\":77860,\"fmax\":77944,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":78049,\"fmax\":78076,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40788String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40787String) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40788String) as JSONObject
+        String mergeTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@TRANSCRIPT1_UNIQUENAME@\" }, { \"uniquename\": \"@TRANSCRIPT2_UNIQUENAME@\" } ], \"operation\": \"merge_transcripts\" }"
+
+
+        when: "we add three transcripts"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+
+
+        then: "we should see 2 genes, 3 transcripts, 7 exons, 3 CDS, no noncanonical splice sites"
+        assert Gene.count == 2
+        assert MRNA.count == 3
+        assert CDS.count == 3
+        assert Exon.count == 7
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we merge the transcripts"
+        String uniqueName1 = MRNA.findByName("GB40787-RA-00001").uniqueName
+        String uniqueName2 = MRNA.findByName("GB40788-RA-00001").uniqueName
+        String uniqueName3 = MRNA.findByName("GB40787-RA-00002").uniqueName
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT1_UNIQUENAME@", uniqueName2)
+        mergeTranscriptString = mergeTranscriptString.replaceAll("@TRANSCRIPT2_UNIQUENAME@", uniqueName1)
+        JSONObject commandObject = JSON.parse(mergeTranscriptString) as JSONObject
+        JSONObject returnedAfterExonObject = requestHandlingService.mergeTranscripts(commandObject)
+
+
+        then: "we should see 2 gene, 2 transcripts, 5 exons, 2 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        def allFeatures = Feature.all
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert Exon.count == 7
+        assert CDS.count == 2
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+        when: "we get the transcripts and gene that should be left"
+        // MRNA bigMRNA = MRNA.findByName("GB40788-RA-00001")
+        // the big mRNA will be GB40787-RA-00001, since its the 5' most transcript
+        MRNA bigMRNA = MRNA.findByName("GB40787-RA-00001")
+        def remainingMRNA = MRNA.all - bigMRNA
+        MRNA undisturbedMRNA = remainingMRNA.get(0)
+
+        then: "this one should be long-gone"
+        assert undisturbedMRNA != null
+        assert bigMRNA != null
+        assert undisturbedMRNA.name == "GB40787-RAa-00001"
+        assert undisturbedMRNA.featureLocation.fmax > undisturbedMRNA.featureLocation.fmin
+        assert undisturbedMRNA.featureLocation.fmax - undisturbedMRNA.featureLocation.fmin > 0
+        assert 0 == MRNA.countByName("GB40788-RA-00001")
+        assert undisturbedMRNA.parentFeatureRelationships.size() == 2 + 1 + 0
+        assert bigMRNA.parentFeatureRelationships.size() == 5 + 1 + 2
+
+    }
+
+
+    void "adding transcripts on overlapping opposite strands fails (should just be two genes and transcripts)"() {
+
+        given: "the GB40781-RA and GB40800-RA"
+        String gb40781 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":157004,\"fmax\":160632,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40781-RA\",\"children\":[{\"location\":{\"fmin\":160342,\"fmax\":160632,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":159606,\"fmax\":159737,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"} [...]
+        String gb40800 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":160307,\"fmax\":162089,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40800-RA\",\"children\":[{\"location\":{\"fmin\":160307,\"fmax\":160485,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":161876,\"fmax\":162089,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{ [...]
+        JSONObject jsonAddTranscriptObject1 = JSON.parse(gb40781) as JSONObject
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40800) as JSONObject
+
+        when: "we add GB40781-RA"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+
+        then: "we should see 1 genes, 1 transcript, 5 exons, 1 CDS, no noncanonical splice sites"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+
+
+        when: "we add GB40800-RA"
+        requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+
+        then: "we should see 2 genes, 2 transcripts, 10 exons, 2 CDS, 1 3' noncanonical splice site and 1 5' noncanonical splice site"
+        def allFeatures = Feature.all
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert CDS.count == 2
+        assert Exon.count == 10
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+    }
+
+    void "should be able to delete multiple exons"() {
+
+        given: "the GB40800-RA"
+        String gb40800 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":160307,\"fmax\":162089,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40800-RA\",\"children\":[{\"location\":{\"fmin\":160307,\"fmax\":160485,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":161876,\"fmax\":162089,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{ [...]
+
+
+        String removeExonCommand = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON1_UNIQUENAME@\" } ], \"operation\": \"delete_feature\" }"
+        String removeExonCommand2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON2_UNIQUENAME@\" },{ \"uniquename\": \"@EXON3_UNIQUENAME@\" } ], \"operation\": \"delete_feature\" }"
+//        String removeExon1 = "{ \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON1_UNIQUENAME@\" } ], \"operation\": \"delete_feature\" }"
+        JSONObject jsonAddTranscriptObject2 = JSON.parse(gb40800) as JSONObject
+
+        when: "we add GB40800-RA"
+        JSONObject addedTranscriptObject = requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+
+        then: "we should see 1 genes, 1 transcript, 5 exons, 1 CDS, no noncanonical splice sites"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 5
+        assert NonCanonicalFivePrimeSpliceSite.count == 1
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        String exon1UniqueName
+        String exon2UniqueName
+        String exon3UniqueName
+        JSONArray childrenArray = addedTranscriptObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).getJSONArray(FeatureStringEnum.CHILDREN.value)
+
+        for (int i = 0; i < childrenArray.size(); i++) {
+            JSONObject childObject = childrenArray.getJSONObject(i)
+            if (childObject.type.name == "exon") {
+                switch (childObject.location.fmin) {
+                    case 160961:
+                        exon1UniqueName = childObject.uniquename
+                        break;
+                    case 161746:
+                        exon2UniqueName = childObject.uniquename
+                        break
+                    case 161365:
+                        exon3UniqueName = childObject.uniquename
+                        break
+
+                }
+
+            }
+        }
+        assert exon1UniqueName != null
+        assert exon2UniqueName != null
+        assert exon3UniqueName != null
+
+
+        when: "we delete 1 exon and things are great"
+        JSONObject removeExonObject1 = JSON.parse(removeExonCommand.replace("@EXON1_UNIQUENAME@", exon1UniqueName)) as JSONObject
+        JSONObject deletedObjectCommand = requestHandlingService.deleteFeature(removeExonObject1)
+
+        then: "we delete 2 exons and things are okay"
+        def allFeatures = Feature.all
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 4
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+
+        when: "we delete 2 exon and things are great"
+        removeExonObject1 = JSON.parse(removeExonCommand2.replace("@EXON2_UNIQUENAME@", exon2UniqueName).replace("@EXON3_UNIQUENAME@", exon3UniqueName)) as JSONObject
+        deletedObjectCommand = requestHandlingService.deleteFeature(removeExonObject1)
+        allFeatures = Feature.all
+
+        then: "Deleting objects"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 2
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+    }
+
+    void "add insertion at exon 1 of gene GB40807-RA"() {
+        given: "given a gene GB40807-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":210517},\"name\":\"GB40807-RA\",\"children\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":208322},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":209434,\"strand\":1,\"fmax\":210517},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":208175,\"strand\":1, [...]
+        String addInsertionString = "{${testCredentials}  \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"GGG\",\"location\":{\"fmin\":208499,\"strand\":1,\"fmax\":208499},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        String expectedPeptideString = "MRVFMMQNFHRLTLFIWLVSILTLSISDEIKTDGNTSLTLNINNVDSDSKTEHRISSSSGIQFMPESVNSKKIQNQSIATPLVAGEGGPISLIPPTQQTSTISHLKDVTDNLDLQDNLSQKEDDILYVKKKKNTSKIVSRKGADNGNISIKMTLSNDTKPIIEFSTIASNISNNAKIDINMNNSKSNVSDKNINKASNIIVNNTLYLTNVTQKLLSVTTSSVQEHKPKPTATVIESNNDKQAFIPHTKGSRLGMPKKIDYVLPVIVTLIALPVLGAIIFMVYKQGRDCWDKRHYRRMDFLIDGMYND"
+        String expectedCdnaString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAGGGATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTC [...]
+        String expectedCdsString = "ATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAGGGATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCATTTAAAAGATGTTACTGATAATTTAGATTTACAAGATAATTTATCACAAAAAGAAGATGATATTTTATACGTAAAGAAAAAAAAGAATACTTCTAAAATCGTGTCGAGAAAAGGAGCAGATAATGGAAATATTTCTATTAAAATG [...]
+        String expectedGenomicString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAGGGATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATGTACGTAATATAAATACATAATATTATATATATATATATATATATATATATATAATTATCAATTAACAAATGTATAAATT [...]
+
+        when: "when we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of GGG to exon 1 at position 208500"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40807-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at 5'UTR of gene GB40807-RA"() {
+        given: "given a gene GB40807-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":210517},\"name\":\"GB40807-RA\",\"children\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":208322},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":209434,\"strand\":1,\"fmax\":210517},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":208175,\"strand\":1, [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAA\",\"location\":{\"fmin\":208299,\"strand\":1,\"fmax\":208299},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRVFMMQNFHRLTLFIWLVSILTLSISDEIKTDGNTSLTLNINNVDSDSKTEHRISSSSIQFMPESVNSKKIQNQSIATPLVAGEGGPISLIPPTQQTSTISHLKDVTDNLDLQDNLSQKEDDILYVKKKKNTSKIVSRKGADNGNISIKMTLSNDTKPIIEFSTIASNISNNAKIDINMNNSKSNVSDKNINKASNIIVNNTLYLTNVTQKLLSVTTSSVQEHKPKPTATVIESNNDKQAFIPHTKGSRLGMPKKIDYVLPVIVTLIALPVLGAIIFMVYKQGRDCWDKRHYRRMDFLIDGMYND"
+        String expectedCdnaString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTC [...]
+        String expectedCdsString = "ATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCATTTAAAAGATGTTACTGATAATTTAGATTTACAAGATAATTTATCACAAAAAGAAGATGATATTTTATACGTAAAGAAAAAAAAGAATACTTCTAAAATCGTGTCGAGAAAAGGAGCAGATAATGGAAATATTTCTATTAAAATGACA [...]
+        String expectedGenomicString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATGTACGTAATATAAATACATAATATTATATATATATATATATATATATATATATAATTATCAATTAACAAATGTATAAATT [...]
+
+        when: "when we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of AAA to the 5'UTR at position 208300"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40807-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at exon 2 of gene GB40807-RA"() {
+        given: "given a gene GB40807-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":210517},\"name\":\"GB40807-RA\",\"children\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":208322},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":209434,\"strand\":1,\"fmax\":210517},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":208175,\"strand\":1, [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAA\",\"location\":{\"fmin\":208899,\"strand\":1,\"fmax\":208899},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRVFMMQNFHRLTLFIWLVSILTLSISDEIKTDGNTSLTLNINNVDSDSKTEHRISSSSIQFMPESVNSKKIQNQSIATPLVAGEGGPISLIPPTQQTSTISHLKDVTDNLDLQDNLSQKEDDILYVKKKKKNTSKIVSRKGADNGNISIKMTLSNDTKPIIEFSTIASNISNNAKIDINMNNSKSNVSDKNINKASNIIVNNTLYLTNVTQKLLSVTTSSVQEHKPKPTATVIESNNDKQAFIPHTKGSRLGMPKKIDYVLPVIVTLIALPVLGAIIFMVYKQGRDCWDKRHYRRMDFLIDGMYND"
+        String expectedCdnaString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCA [...]
+        String expectedCdsString = "ATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCATTTAAAAGATGTTACTGATAATTTAGATTTACAAGATAATTTATCACAAAAAGAAGATGATATTTTATACGTAAAGAAAAAAAAAAAGAATACTTCTAAAATCGTGTCGAGAAAAGGAGCAGATAATGGAAATATTTCTATTAAAATG [...]
+        String expectedGenomicString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATGTACGTAATATAAATACATAATATTATATATATATATATATATATATATATATAATTATCAATTAACAAATGTATAAATTATT [...]
+
+        when: "when we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion AAA to exon 2 at position 208900"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40807-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at 3'UTR of gene GB40807-RA"() {
+        given: "given a gene GB40807-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":210517},\"name\":\"GB40807-RA\",\"children\":[{\"location\":{\"fmin\":208175,\"strand\":1,\"fmax\":208322},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":209434,\"strand\":1,\"fmax\":210517},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":208175,\"strand\":1, [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAA\",\"location\":{\"fmin\":209449,\"strand\":1,\"fmax\":209449},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRVFMMQNFHRLTLFIWLVSILTLSISDEIKTDGNTSLTLNINNVDSDSKTEHRISSSSIQFMPESVNSKKIQNQSIATPLVAGEGGPISLIPPTQQTSTISHLKDVTDNLDLQDNLSQKEDDILYVKKKKNTSKIVSRKGADNGNISIKMTLSNDTKPIIEFSTIASNISNNAKIDINMNNSKSNVSDKNINKASNIIVNNTLYLTNVTQKLLSVTTSSVQEHKPKPTATVIESNNDKQAFIPHTKGSRLGMPKKIDYVLPVIVTLIALPVLGAIIFMVYKQGRDCWDKRHYRRMDFLIDGMYND"
+        String expectedCdnaString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCA [...]
+        String expectedCdsString = "ATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATCAAAGTATTGCCACTCCTTTAGTTGCTGGAGAAGGTGGTCCAATATCACTTATACCTCCTACTCAGCAAACATCTACTATTTCCCATTTAAAAGATGTTACTGATAATTTAGATTTACAAGATAATTTATCACAAAAAGAAGATGATATTTTATACGTAAAGAAAAAAAAGAATACTTCTAAAATCGTGTCGAGAAAAGGAGCAGATAATGGAAATATTTCTATTAAAATGACA [...]
+        String expectedGenomicString = "GCAATAGTTGCGTGCTTATGATGGAGCAAACAGTTTCTTAGTGGTTGAGACCACTTTTTTTTTAGTTTTTCTATATTTTTATAAAAGTTTTAACCAGATTTATCTGCAAAGAATCGTATCAGAAAATAAAATTTTATAATTAAAATAATGCGTGTTTTCATGATGCAAAATTTTCATCGATTAACTCTATTTATATGGCTTGTATCTATTCTAACCTTATCTATCAGTGATGAAATTAAAACAGATGGTAATACATCCTTAACATTAAATATAAACAATGTTGATAGTGATTCCAAAACCGAACATCGAATTTCATCTTCATCAATTCAATTTATGCCTGAATCCGTTAATTCTAAAAAAATACAGAATGTACGTAATATAAATACATAATATTATATATATATATATATATATATATATATAATTATCAATTAACAAATGTATAAATTATT [...]
+
+        when: "when we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of AAA to 3'UTR at position 209450"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40807-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at intron of gene GB40721-RA"() {
+        given: "given a gene GB40721-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254801},\"name\":\"GB40721-RA\",\"children\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254586},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254747,\"strand\":-1,\"fmax\":1254801},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254490,\"s [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAAAAAAAA\",\"location\":{\"fmin\":1254599,\"strand\":1,\"fmax\":1254599},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRAAHLPFSGKVEGAHAMFLASDTSLTPCIQNIPSIRFLPSTRLLIDTF"
+        String expectedCdnaString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedCdsString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedGenomicString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGGTATGTCTTAATGTTAGATTATAAGTAGCCTTTTTGTATAAAGTATATTGAGGTATAAAACCCTTAAATTGAATTACTGTACAGAATGTGTAGAAATAGTGAATAATAATATTATGGAATATATTTTATTTCTCATTTAAATGAAACTTTTTTTTTTTAAGTTGTTTCAGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of AAAAAAAAA to an intron at position 1254600"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40721-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at exon 1 of gene GB40721-RA"() {
+        given: "given a gene GB40721-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254801},\"name\":\"GB40721-RA\",\"children\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254586},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254747,\"strand\":-1,\"fmax\":1254801},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254490,\"s [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"TTT\",\"location\":{\"fmin\":1254774,\"strand\":1,\"fmax\":1254774},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRAAHLPFSKGKVEGAHAMFLASDTSLTPCIQNIPSIRFLPSTRLLIDTF"
+        String expectedCdnaString = "ATGCGTGCGGCCCATCTGCCGTTTAGCAAAGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedCdsString = "ATGCGTGCGGCCCATCTGCCGTTTAGCAAAGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedGenomicString = "ATGCGTGCGGCCCATCTGCCGTTTAGCAAAGGTAAGGTGGAGGGCGCTCATGCTATGGTATGTCTTAATGTTAGATTATAAGTAGCCTTTTTGTATAAAGTATATTGAGGTATAAAACCCTTAAATTGAATTACTGTACAGAATGTGTAGAAATAGTGAATAATAATATTATGGAATATATTTTATTTCTCATTTAAATGAAACTTAAGTTGTTTCAGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of TTT to exon 1 at position 1254775"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40721-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at exon 2 of gene GB40721-RA"() {
+        given: "given a gene GB40721-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254801},\"name\":\"GB40721-RA\",\"children\":[{\"location\":{\"fmin\":1254490,\"strand\":-1,\"fmax\":1254586},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254747,\"strand\":-1,\"fmax\":1254801},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1254490,\"s [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"TTT\",\"location\":{\"fmin\":1254549,\"strand\":1,\"fmax\":1254549},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MRAAHLPFSGKVEGAHAMFLASDTSLTPCIQKNIPSIRFLPSTRLLIDTF"
+        String expectedCdnaString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAAAAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedCdsString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAAAAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+        String expectedGenomicString = "ATGCGTGCGGCCCATCTGCCGTTTAGCGGTAAGGTGGAGGGCGCTCATGCTATGGTATGTCTTAATGTTAGATTATAAGTAGCCTTTTTGTATAAAGTATATTGAGGTATAAAACCCTTAAATTGAATTACTGTACAGAATGTGTAGAAATAGTGAATAATAATATTATGGAATATATTTTATTTCTCATTTAAATGAAACTTAAGTTGTTTCAGTTTTTGGCATCTGACACATCTCTGACACCTTGCATTCAAAAGAACATTCCATCCATTAGATTTCTGCCCTCGACCAGGTTACTTATAGATACGTTCTAA"
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+
+        when: "we add an insertion of TTT to exon 2 at position 1254550"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40721-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at 5'UTR of gene GB40749-RA"() {
+        given: "given a gene GB40749-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":694293,\"strand\":-1,\"fmax\":696055},\"name\":\"GB40749-RA\",\"children\":[{\"location\":{\"fmin\":695943,\"strand\":-1,\"fmax\":696055},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":694293,\"strand\":-1,\"fmax\":694440},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":694293,\"strand\" [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAAAAA\",\"location\":{\"fmin\":695949,\"strand\":1,\"fmax\":695949},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MPRCLIKSMTRYRKTDNSSEVEAELPWTPPSSVDAKRKHQIKDNSTKCNNIWTSSRLPIVTRYTFNKENNIFWNKELNIADVELGSRNFSEIENTIPSTTPNVSVNTNQAMVDTSNEQKVEKVQIPLPSNAKKVEYPVNVSNNEIKVAVNLNRMFDGAENQTTSQTLYIATNKKQIDSQNQYLGGNMKTTGVENPQNWKRNKTMHYCPYCRKSFDRPWVLKGHLRLHTGERPFECPVCHKSFADRSNLRAHQRTRNHHQWQWRCGECFKAFSQRRYLERHCPEACRKYRISQRREQNCS"
+        String expectedCdnaString = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCTTTTTTGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGG [...]
+        String expectedCdsString = "ATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGGACACGAGCAATGAGCAAAAGGTGGAAAAAGTGCAAATACCATTGCCCTCGAACGCGAAAAAAGTAGAGTATCCGGTAAACGTGAGTAACAACGAGATCAAGGTGGCTGTGAATTTGAAT [...]
+        String expectedGenomicString = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCTTTTTTGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGGTAAGACAGAATTCTCTTGTGCACACAGTATAGCTACAGTATACTCAGGGGACGACGAGTGAAATTTTGGCGTGGTTATGGAAAAAAAAAAAGTACAACTCGTAAAGTTGTTGGAGTAAATGAGTCCCGTTTTTTCATGGCGAATCGTACGTCTCCTTTCCACTCGACGACACAGTTTTCAATTTCATATAATAAAAGCGAATGTGAAAATACGATGCGTATGATTCGTTCGAAAAAGAAAGGCAAAAAAAAAAAAAAAAAAAAAATGAAATG [...]
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+
+        when: "we add an insertion of AAAAAA to the 5'UTR at position 695950"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40749-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add insertion at 3'UTR of gene GB40749-RA"() {
+        given: "given a gene GB40749-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":694293,\"strand\":-1,\"fmax\":696055},\"name\":\"GB40749-RA\",\"children\":[{\"location\":{\"fmin\":695943,\"strand\":-1,\"fmax\":696055},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":694293,\"strand\":-1,\"fmax\":694440},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":694293,\"strand\" [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAAA\",\"location\":{\"fmin\":694399,\"strand\":1,\"fmax\":694399},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MPRCLIKSMTRYRKTDNSSEVEAELPWTPPSSVDAKRKHQIKDNSTKCNNIWTSSRLPIVTRYTFNKENNIFWNKELNIADVELGSRNFSEIENTIPSTTPNVSVNTNQAMVDTSNEQKVEKVQIPLPSNAKKVEYPVNVSNNEIKVAVNLNRMFDGAENQTTSQTLYIATNKKQIDSQNQYLGGNMKTTGVENPQNWKRNKTMHYCPYCRKSFDRPWVLKGHLRLHTGERPFECPVCHKSFADRSNLRAHQRTRNHHQWQWRCGECFKAFSQRRYLERHCPEACRKYRISQRREQNCS"
+        String expectedCdnaString = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGGACACGA [...]
+        String expectedCdsString = "ATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGGACACGAGCAATGAGCAAAAGGTGGAAAAAGTGCAAATACCATTGCCCTCGAACGCGAAAAAAGTAGAGTATCCGGTAAACGTGAGTAACAACGAGATCAAGGTGGCTGTGAATTTGAAT [...]
+        String expectedGenomicString = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGGTAAGACAGAATTCTCTTGTGCACACAGTATAGCTACAGTATACTCAGGGGACGACGAGTGAAATTTTGGCGTGGTTATGGAAAAAAAAAAAGTACAACTCGTAAAGTTGTTGGAGTAAATGAGTCCCGTTTTTTCATGGCGAATCGTACGTCTCCTTTCCACTCGACGACACAGTTTTCAATTTCATATAATAAAAGCGAATGTGAAAATACGATGCGTATGATTCGTTCGAAAAAGAAAGGCAAAAAAAAAAAAAAAAAAAAAATGAAATGATTTTC [...]
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+
+        when: "we add an insertion of AAAA to the 3'UTR at position 694400"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40749-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+    void "add start codon insertion at 5'UTR of gene GB40843-RA"() {
+        given: "given a gene GB40843-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1092561,\"strand\":1,\"fmax\":1095202},\"name\":\"GB40843-RA\",\"children\":[{\"location\":{\"fmin\":1092561,\"strand\":1,\"fmax\":1092941},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1094825,\"strand\":1,\"fmax\":1095202},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1092561,\"stra [...]
+        String addInsertionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"ATG\",\"location\":{\"fmin\":1092929,\"strand\":1,\"fmax\":1092929},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptideString = "MYLIIMTLDNSWHIIKTAAAENKHELVLSGPAISELIQKRGFDKSLFNLEHLNYLNITQTCLHEVSEEIEKLQNLTTLVLHSNEISKIPSSIGKLEKLKVLDCSRNKLTSLPDEIGKLPQLTTMNFSSNFLKSLPTQIDNVKLTVLDLSNNQFEDFPDVCYTELVHLSEIYVMGNQIKEIPTIINQLPSLKIINVANNRISVIPGEIGDCNKLKELQLKGNPLSDKRLLKLVDQCHNKQILEYVKLHCPKQDGSVNSNSKSKKGKKVQKLSESENNANVMDNLTHKMKILKMANDTPVIKVTKYVKNIRPYIAACIVRNMSFTEDSFKKFIQLQTKLHDGICEKRNAATIATHDLKLITTGDLTYTAKPPNELEIKPLMRNKVYTGSELFKQLQTEADNLRKEKKRNVYSGIHKYLYLLEGKPYFPCLLDASEQVISFPPITNSDITKMSIN [...]
+        String expectedCdnaString = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAAATGTACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTGTTGTCA [...]
+        String expectedCdsString = "ATGTACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTGTTGTCAGGTCCGGCAATCTCTGAATTGATTCAAAAAAGAGGCTTCGACAAGTCTTTATTTAACCTAGAGCATTTGAATTATTTGAATATTACTCAGACATGTTTACACGAAGTGTCAGAAGAAATTGAAAAATTGCAAAATCTAACAACATTGGTATTGCATTCGAATGAGATTTCGAAGATACCTAGCTCAATCGGGAAATTAGAAAAACTAAAGGTTCTCGATTGCTCTAGGAACAAGTTGACGTCTTTACCAGATGAAATCGGTAAACTTCCACAATTAACAACCATGAACTTCAGTTCAAATTTTTTGAAATCATTACCTACGCAAATTGACAATGTTAAGTTGACTGTTTTGGATCTTTCGAACAATCAG [...]
+        String expectedGenomicString = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAAATGTACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTGTTG [...]
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 4
+
+        when: "we add a start codon insertion of ATG to the 5'UTR at position 1092930"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40843-RA-00001")
+
+        String peptideString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdnaString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cdsString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String genomicString = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we should have the expected sequences"
+        assert peptideString == expectedPeptideString
+        assert cdnaString == expectedCdnaString
+        assert cdsString == expectedCdsString
+        assert genomicString == expectedGenomicString
+    }
+
+
+    void "add deletions to GB40738-RA"() {
+        given: "given a gene GB40738-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":874076,\"strand\":-1,\"fmax\":874361},\"name\":\"GB40738-RA\",\"children\":[{\"location\":{\"fmin\":874076,\"strand\":-1,\"fmax\":874091},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":874214,\"strand\":-1,\"fmax\":874252},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":874336,\"strand\" [...]
+        String addDeletion1String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":874349,\"strand\":1,\"fmax\":874352},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addDeletion2String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":874224,\"strand\":1,\"fmax\":874227},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptide1String = "MILCKCINDGKYSFQRCFIQKSII"
+        String expectedCdna1String = "ATGATACTCTGTAAATGTATAAATGATGGAAAATATTCTTTTCAACGATGCTTTATTCAGAAGTCAATCATTTGA"
+        String expectedCds1String = "ATGATACTCTGTAAATGTATAAATGATGGAAAATATTCTTTTCAACGATGCTTTATTCAGAAGTCAATCATTTGA"
+
+        String expectedPeptide2String = "MILCKCINDGKYSFQRFIQKSII"
+        String expectedCdna2String = "ATGATACTCTGTAAATGTATAAATGATGGAAAATATTCTTTTCAACGCTTTATTCAGAAGTCAATCATTTGA"
+        String expectedCds2String = "ATGATACTCTGTAAATGTATAAATGATGGAAAATATTCTTTTCAACGCTTTATTCAGAAGTCAATCATTTGA"
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+
+        when: "we add a deletion of size 3 to exon 1 at position 874350"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion1String) as JSONObject)
+
+        then: "the deletion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40738-RA-00001")
+
+        String peptide1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide1String == expectedPeptide1String
+        assert cdna1String == expectedCdna1String
+        assert cds1String == expectedCds1String
+
+        when: "we add a deletion of size 3 to exon 2 at position 874225"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion2String) as JSONObject)
+
+        then: "the deletion is successfully added"
+        assert SequenceAlteration.count == 2
+
+        when: "we request for the FASTA sequence"
+        String peptide2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide2String == expectedPeptide2String
+        assert cdna2String == expectedCdna2String
+        assert cds2String == expectedCds2String
+
+    }
+
+    void "add combination of sequence alterations to GB40821-RA"() {
+        given: "given a gene GB40821-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":621650,\"strand\":1,\"fmax\":628275},\"name\":\"GB40821-RA\",\"children\":[{\"location\":{\"fmin\":621650,\"strand\":1,\"fmax\":622270},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":628037,\"strand\":1,\"fmax\":628275},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":621650,\"strand\":1, [...]
+        String addDeletion1String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":621999,\"strand\":1,\"fmax\":622010},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion2String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"GGGTGCC\",\"location\":{\"fmin\":622274,\"strand\":1,\"fmax\":622274},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addDeletion3String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":622299,\"strand\":1,\"fmax\":622301},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addDeletion4String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":622749,\"strand\":1,\"fmax\":622774},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion5String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"CCCCCCC\",\"location\":{\"fmin\":623174,\"strand\":1,\"fmax\":623174},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion6String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"TGA\",\"location\":{\"fmin\":623200,\"strand\":1,\"fmax\":623200},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptide1String = "MEIARIHRYYKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITGLMILILLLYSYMNEKATNSNEKDFQELQKETNKKIPRKKSVADIRPIYNCIHKHLQQTDVFQEKTKKMLCKERLELEILCSKINCINKLLRPEAQTEWRRSKLRKVYYRPINSATSK"
+        String expectedCdna1String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds1String = "ATGGAAATCGCAAGGATCCACAGATATTACAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTGGACTCATGATCTTGATCCTCCTCCTCTATTCATACATGAATGAAAAAGCGACCAATTCGAACGAGAAGGATTTTCAAGAGCTTCAAAAGGAAACAAATAAGAAAATTCCCCGGAAAAAAAGCGTGGCGGACATCAGGCCGATCTACAATTGTATTCATAAACACCTCCAGCAGACCGACGTGTTTCAAGAGAAGACGAAGAAAATGCTTTGCAAGGAACGCTTGGAATTGGAGATTCTGTGCAGTAAAATCAATTGCATCAACAAGCTGTTAAGGCCCGAGGCGCAGACCGAATGGCGGCGGAGCAAGTT [...]
+
+        String expectedPeptide2String = "MSWGCQIARIHRYYKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITGLMILILLLYSYMNEKATNSNEKDFQELQKETNKKIPRKKSVADIRPIYNCIHKHLQQTDVFQEKTKKMLCKERLELEILCSKINCINKLLRPEAQTEWRRSKLRKVYYRPINSATSK"
+        String expectedCdna2String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds2String = "ATGTCATGGGGGTGCCAAATCGCAAGGATCCACAGATATTACAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTGGACTCATGATCTTGATCCTCCTCCTCTATTCATACATGAATGAAAAAGCGACCAATTCGAACGAGAAGGATTTTCAAGAGCTTCAAAAGGAAACAAATAAGAAAATTCCCCGGAAAAAAAGCGTGGCGGACATCAGGCCGATCTACAATTGTATTCATAAACACCTCCAGCAGACCGACGTGTTTCAAGAGAAGACGAAGAAAATGCTTTGCAAGGAACGCTTGGAATTGGAGATTCTGTGCAGTAAAATCAATTGCATCAACAAGCTGTTAAGGCCCGAGGCGCAGACCGAATGGCG [...]
+
+        String expectedPeptide3String = "MKNRDMYKSLYRRRDEQQQGEKLKRAHRSLYVKRSSKNCQFFRFSWPVQIHVMLVTIATNYQLRGTKTRCHGGAKSQGSTDIKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITGLMILILLLYSYMNEKATNSNEKDFQELQKETNKKIPRKKSVADIRPIYNCIHKHLQQTDVFQEKTKKMLCKERLELEILCSKINCINKLLRPEAQTEWRRSKLRKVYYRPINSATSK"
+        String expectedCdna3String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds3String = "ATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCAAGGAGAGAAATTGAAGAGGGCGCATCGATCACTTTATGTCAAACGATCCTCCAAAAACTGTCAGTTTTTTCGATTCTCGTGGCCCGTCCAAATTCACGTGATGCTCGTGACAATAGCGACGAATTATCAGCTTCGCGGGACGAAAACTCGATGTCATGGGGGTGCCAAATCGCAAGGATCCACAGATATTAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTGGACTCATGATCTTGATCCTCCTCCTCTATTCATACATGAATGAAAAAGCGACCAATTCGAACGAGAAGGATTTTCAAGAGCTTCAAAAGGAAAC [...]
+
+        String expectedPeptide4String = "MKNRDMYKSLYRRRDEQQQGEKLKRAHRSLYVKRSSKNCQFFRFSWPVQIHVMLVTIATNYQLRGTKTRCHGGAKSQGSTDIKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITGLMILILLLYSYMNEKATNSNEKDFQELQKETNKKIPRKKSVADIRPIYNCIHKHLQQTDVFQEKTKKMLCKERLELEILCSKINCINKLLRPEAQTEWRRSKLRKVYYRPINSATSK"
+        String expectedCdna4String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds4String = "ATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCAAGGAGAGAAATTGAAGAGGGCGCATCGATCACTTTATGTCAAACGATCCTCCAAAAACTGTCAGTTTTTTCGATTCTCGTGGCCCGTCCAAATTCACGTGATGCTCGTGACAATAGCGACGAATTATCAGCTTCGCGGGACGAAAACTCGATGTCATGGGGGTGCCAAATCGCAAGGATCCACAGATATTAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTGGACTCATGATCTTGATCCTCCTCCTCTATTCATACATGAATGAAAAAGCGACCAATTCGAACGAGAAGGATTTTCAAGAGCTTCAAAAGGAAAC [...]
+
+        String expectedPeptide5String = "MKNRDMYKSLYRRRDEQQQGEKLKRAHRSLYVKRSSKNCQFFRFSWPVQIHVMLVTIATNYQLRGTKTRCHGGAKSQGSTDIKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITPPRTHDLDPPPLFIHE"
+        String expectedCdna5String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds5String = "ATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCAAGGAGAGAAATTGAAGAGGGCGCATCGATCACTTTATGTCAAACGATCCTCCAAAAACTGTCAGTTTTTTCGATTCTCGTGGCCCGTCCAAATTCACGTGATGCTCGTGACAATAGCGACGAATTATCAGCTTCGCGGGACGAAAACTCGATGTCATGGGGGTGCCAAATCGCAAGGATCCACAGATATTAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTCCCCCCCGGACTCATGATCTTGATCCTCCTCCTCTATTCATACATGAATGA"
+
+        String expectedPeptide6String = "MKNRDMYKSLYRRRDEQQQGEKLKRAHRSLYVKRSSKNCQFFRFSWPVQIHVMLVTIATNYQLRGTKTRCHGGAKSQGSTDIKTYHFGPRWLLYQYLLTDAKLRNMLDLGPFCGTITFITPPRTHDLDPPP"
+        String expectedCdna6String = "AAAAAAACGAATAACCTAATCTAACCTCCTTTATTTCGTCGATTATGATCGAATTATGATCGAAAATATAAATAAATTTCTCGATTATTGCAAAAAAAAATATGAAGAAAATGAAGAAAAGGAATGAAAGAAAATGGAAAATTGAGTAAATAAAAACATATATATGAAAACATGGATACACCGAAATCAATAGCCAATAAAAAACATGATATTACGAGGATTCGCTTCTTGACACGAATCTCTACTTATCGTCGTTGCTTGAATATGCTCCTTTAATTTGTATCGTCTTTCACAACTAATCAAAAATTCCAATATACAACGAATAAATCTCGAAACTTAAAATTTTCAACGTAAAAAATGTATAATGTTAAGATGCTAAGGACGATTTCAAAAATTCAATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCA [...]
+        String expectedCds6String = "ATGAAAAATCGCGACATGTACAAATCCCTCTATCGAAGACGAGATGAACAACAGCAAGGAGAGAAATTGAAGAGGGCGCATCGATCACTTTATGTCAAACGATCCTCCAAAAACTGTCAGTTTTTTCGATTCTCGTGGCCCGTCCAAATTCACGTGATGCTCGTGACAATAGCGACGAATTATCAGCTTCGCGGGACGAAAACTCGATGTCATGGGGGTGCCAAATCGCAAGGATCCACAGATATTAAAACCTATCATTTTGGGCCACGATGGCTGTTATACCAATATCTGTTGACGGACGCTAAGTTGAGGAATATGCTTGATTTGGGTCCGTTCTGCGGGACCATTACGTTTATAACTCCCCCCCGGACTCATGATCTTGATCCTCCTCCTTGA"
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 7
+
+        when: "we add a Deletion of size 11 in 5'UTR at position 622000"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion1String) as JSONObject)
+
+        then: "the deletion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40821-RA-00001")
+
+        String peptide1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide1String == expectedPeptide1String
+        assert cdna1String == expectedCdna1String
+        assert cds1String == expectedCds1String
+
+        when: "we add an Insertion of GGGTGCC in exon 1 at position 622275"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion2String) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 2
+
+        when: "we request for the FASTA sequence"
+        String peptide2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide2String == expectedPeptide2String
+        assert cdna2String == expectedCdna2String
+        assert cds2String == expectedCds2String
+
+        when: "we add a Deletion of size 2 in exon 1 at position 622300"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion3String) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 3
+
+        when: "we request for the FASTA sequence"
+        String peptide3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide3String == expectedPeptide3String
+        assert cdna3String == expectedCdna3String
+        assert cds3String == expectedCds3String
+
+        when: "we add a Deletion of 25 in intron at position 622750"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion4String) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 4
+
+        when: "we request for the FASTA sequence"
+        String peptide4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide4String == expectedPeptide4String
+        assert cdna4String == expectedCdna4String
+        assert cds4String == expectedCds4String
+
+        when: "we add an Insertion of CCCCCCC in exon 2 at position 623175"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion5String) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 5
+
+        when: "we request for the FASTA sequence"
+        String peptide5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide5String == expectedPeptide5String
+        assert cdna5String == expectedCdna5String
+        assert cds5String == expectedCds5String
+
+        when: "we add an Insertion of stop codon TGA in exon 2 at position 623201"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion6String) as JSONObject)
+
+        then: "the insertion is successfully added"
+        assert SequenceAlteration.count == 6
+
+        when: "we request for the FASTA sequence"
+        String peptide6String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cdna6String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds6String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide6String == expectedPeptide6String
+        assert cdna6String == expectedCdna6String
+        assert cds6String == expectedCds6String
+
+    }
+
+    void "add combination of sequence alterations to GB40744-RA"() {
+        given: "given a gene GB40744-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"name\":\"GB40744-RA\",\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\" [...]
+        String addDeletion1String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":768024,\"strand\":1,\"fmax\":768034},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion2String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AACCC\",\"location\":{\"fmin\":767849,\"strand\":1,\"fmax\":767849},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion3String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"AAAAAAAAAAA\",\"location\":{\"fmin\":767399,\"strand\":1,\"fmax\":767399},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addDeletion4String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":767324,\"strand\":1,\"fmax\":767354},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String addInsertion5String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"CCCGGGA\",\"location\":{\"fmin\":763174,\"strand\":1,\"fmax\":763174},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedPeptide1String = "MEEMDLGSDESTDKNLHEKSVNRKIESEMIENETEEIKDEFDEEQDKDNDDDDDDDDDDDDEEDADEAEVKILETSLAHNPYDYASHVALINKLQKMGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSIGNMGTEKDAAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKS [...]
+        String expectedCds1String = "ATGGAAGAAATGGATTTAGGAAGCGATGAAAGTACCGATAAAAATTTACATGAAAAGTCGGTGAACAGAAAGATCGAATCAGAAATGATTGAAAATGAAACTGAAGAAATTAAGGATGAATTTGATGAAGAACAGGATAAAGATAATGATGATGATGATGATGATGATGATGATGATGATGATGAAGAGGATGCTGATGAAGCAGAAGTTAAAATTTTAGAAACTTCTCTTGCTCACAATCCATATGATTATGCGAGTCATGTAGCTCTTATCAACAAATTACAAAAAATGGGTGAATTAGAACGATTACGCGCTGCCAGAGAAGATATGAGTTCAAAATATCCCTTGAGTCCTGATCTCTGGTTATCTTGGATGCGTGACGAAATTAAATTAGCAACCACCATAGAACAGAAAGCTGAAGTAGTAAAATTATGTGAGCGAGCAGTAAAAGATTA [...]
+
+        String expectedPeptide2String = "MGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSIGNMGTEKDAAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKSFGLEGDPNCIILQYWARTEAIHANNMEKTRSLWADILSQGHSGTASYWLEYISLERCYGDTKHLRKLFQKALTMVKDWPESIANSWIDFERDEGTL [...]
+        String expectedCds2String = "ATGGGTGAATTAGAACGATTACGCGCTGCCAGAGAAGATATGAGTTCAAAATATCCCTTGAGTCCTGATCTCTGGTTATCTTGGATGCGTGACGAAATTAAATTAGCAACCACCATAGAACAGAAAGCTGAAGTAGTAAAATTATGTGAGCGAGCAGTAAAAGATTATCTTGCTGTAGAAGTATGGTTAGAATATTTGCAATTTAGTATTGGTAATATGGGCACTGAAAAAGATGCAGCAAAAAATGTTAGACATCTCTTTGAAAGAGCATTAACAGATGTTGGATTACATACTATTAAAGGTGCAATAATATGGGAAGCATTTCGAGAGTTTGAAGCTGTGTTATATGCTCTGATTGATCCTTTAAATCAAGCTGAAAGGAAAGAGCAATTAGAACGTATTGGTAACTTATTCAAAAGACAATTAGCTTGTCCTCTTTTAGATATGGAAAAAAC [...]
+
+        String expectedPeptide3String = "MGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSIGNMGTEKDAAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKSFGLEGDPNCIILQYWARTEAIHANNMEKTRSLWADILSQGHSGTASYWLEYISLERCYGDTKHLRKLFQKALTMVKDWPESIANSWIDFERDEGTL [...]
+        String expectedCds3String = "ATGGGTGAATTAGAACGATTACGCGCTGCCAGAGAAGATATGAGTTCAAAATATCCCTTGAGTCCTGATCTCTGGTTATCTTGGATGCGTGACGAAATTAAATTAGCAACCACCATAGAACAGAAAGCTGAAGTAGTAAAATTATGTGAGCGAGCAGTAAAAGATTATCTTGCTGTAGAAGTATGGTTAGAATATTTGCAATTTAGTATTGGTAATATGGGCACTGAAAAAGATGCAGCAAAAAATGTTAGACATCTCTTTGAAAGAGCATTAACAGATGTTGGATTACATACTATTAAAGGTGCAATAATATGGGAAGCATTTCGAGAGTTTGAAGCTGTGTTATATGCTCTGATTGATCCTTTAAATCAAGCTGAAAGGAAAGAGCAATTAGAACGTATTGGTAACTTATTCAAAAGACAATTAGCTTGTCCTCTTTTAGATATGGAAAAAAC [...]
+
+        String expectedPeptide4String = "MGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKSFGLEGDPNCIILQYWARTEAIHANNMEKTRSLWADILSQGHSGTASYWLEYISLERCYGDTKHLRKLFQKALTMVKDWPESIANSWIDFERDEGTLEQMEICEIRT [...]
+        String expectedCds4String = "ATGGGTGAATTAGAACGATTACGCGCTGCCAGAGAAGATATGAGTTCAAAATATCCCTTGAGTCCTGATCTCTGGTTATCTTGGATGCGTGACGAAATTAAATTAGCAACCACCATAGAACAGAAAGCTGAAGTAGTAAAATTATGTGAGCGAGCAGTAAAAGATTATCTTGCTGTAGAAGTATGGTTAGAATATTTGCAATTTAGTGCAAAAAATGTTAGACATCTCTTTGAAAGAGCATTAACAGATGTTGGATTACATACTATTAAAGGTGCAATAATATGGGAAGCATTTCGAGAGTTTGAAGCTGTGTTATATGCTCTGATTGATCCTTTAAATCAAGCTGAAAGGAAAGAGCAATTAGAACGTATTGGTAACTTATTCAAAAGACAATTAGCTTGTCCTCTTTTAGATATGGAAAAAACATATGAAGAATATGAAGCTTGGCGTCATGG [...]
+
+        String expectedPeptide5String = "MGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKSFGLEGDPNCIILQYWARTEAIHANNMEKTRSLWADILSQGHSGTASYWLEYISLERCYGDTKHLRKLFQKALTMVKDWPESIANSWIDFERDEGTLEQMEICEIRT [...]
+        String expectedCds5String = "ATGGGTGAATTAGAACGATTACGCGCTGCCAGAGAAGATATGAGTTCAAAATATCCCTTGAGTCCTGATCTCTGGTTATCTTGGATGCGTGACGAAATTAAATTAGCAACCACCATAGAACAGAAAGCTGAAGTAGTAAAATTATGTGAGCGAGCAGTAAAAGATTATCTTGCTGTAGAAGTATGGTTAGAATATTTGCAATTTAGTGCAAAAAATGTTAGACATCTCTTTGAAAGAGCATTAACAGATGTTGGATTACATACTATTAAAGGTGCAATAATATGGGAAGCATTTCGAGAGTTTGAAGCTGTGTTATATGCTCTGATTGATCCTTTAAATCAAGCTGAAAGGAAAGAGCAATTAGAACGTATTGGTAACTTATTCAAAAGACAATTAGCTTGTCCTCTTTTAGATATGGAAAAAACATATGAAGAATATGAAGCTTGGCGTCATGG [...]
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we expect to see the gene added"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 6
+
+        when: "we add a Deletion of size 10 in 5'UTR at position 768025"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion1String) as JSONObject)
+
+        then: "the deletion is successfully added"
+        assert SequenceAlteration.count == 1
+
+        when: "we request for the FASTA sequence"
+        MRNA mrna = MRNA.findByName("GB40744-RA-00001")
+
+        String peptide1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cds1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide1String == expectedPeptide1String
+        assert cds1String == expectedCds1String
+
+        when: "we add an Insertion of AACCC in exon 1 at position 767850"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion2String) as JSONObject)
+
+        then: "the Insertion is successfully added"
+        assert SequenceAlteration.count == 2
+
+        when: "we request for the FASTA sequence"
+        String peptide2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cds2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide2String == expectedPeptide2String
+        assert cds2String == expectedCds2String
+
+        when: "we add Insertion of AAAAAAAAAAA in intron at position 767400"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion3String) as JSONObject)
+
+        then: "the Insertion is successfully added"
+        assert SequenceAlteration.count == 3
+
+        when: "we request for the FASTA sequence"
+        String peptide3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cds3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide3String == expectedPeptide3String
+        assert cds3String == expectedCds3String
+
+        when: "we add a Deletion of size 30 in exon 2 at position 767325"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion4String) as JSONObject)
+
+        then: "the Insertion is successfully added"
+        assert SequenceAlteration.count == 4
+
+        when: "we request for the FASTA sequence"
+        String peptide4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cds4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide4String == expectedPeptide4String
+        assert cds4String == expectedCds4String
+
+        // Test for #442
+        when: "we add an Insertion of CCCGGGA in last exon at position 763175"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertion5String) as JSONObject)
+
+        then: "the Insertion is successfully added"
+        assert SequenceAlteration.count == 5
+
+        when: "we request for the FASTA sequence"
+        String peptide5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String cds5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should have the expected sequences"
+        assert peptide5String == expectedPeptide5String
+        assert cds5String == expectedCds5String
+    }
+
+    void "add a combination of sequence alterations to GB40728-RA"() {
+
+        given: "a transcript GB40728-RA and 5 sequence alterations"
+        String addTranscriptString = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":1087635,\"fmax\":1088108},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":1088739,\"fmax\":1088873},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":1089643,\"fmax\":1089949},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1, [...]
+        String addSequenceAlterationString1 = "{ ${testCredentials} \"features\":[{\"residues\":\"TTG\",\"location\":{\"strand\":1,\"fmin\":1089899,\"fmax\":1089899},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSequenceAlterationString2 = "{ ${testCredentials} \"features\":[{\"location\":{\"strand\":1,\"fmin\":1089849,\"fmax\":1089852},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"deletion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSequenceAlterationString3 = "{ ${testCredentials} \"features\":[{\"location\":{\"strand\":1,\"fmin\":1089824,\"fmax\":1089826},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"deletion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSequenceAlterationString4 = "{ ${testCredentials} \"features\":[{\"residues\":\"C\",\"location\":{\"strand\":1,\"fmin\":1089803,\"fmax\":1089803},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSequenceAlterationString5 = "{ ${testCredentials} \"features\":[{\"residues\":\"TCC\",\"location\":{\"strand\":1,\"fmin\":1090349,\"fmax\":1090349},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+
+        when: "we add a transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the transcript"
+        assert MRNA.all.size() == 1
+        MRNA mrna = MRNA.all.get(0)
+
+        when: "we add an insertion of TTG @ 1089900"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString1) as JSONObject)
+
+        then: "we should see the insertion"
+        assert Insertion.all.size() == 1
+
+        then: "the insertion should be seen in the transcript's sequence"
+        String genomic1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+        String cdna1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String peptide1String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        assert genomic1String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTT [...]
+        assert cdna1String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTTATC [...]
+        assert cds1String == "ATGAATCGTATATCCTTTATCTCTTTTTCCGTTCTACTGGTTTCGGTATCAGCATGGCCATTTCGTCGTCGAGACGTCTTCGACAATGCGGTGGACAGTCCTCACGGCGTGATCGCTCATCTGCAACACTTTGGCGAGATGTTGTACCGGAATCCGGACAACGAAACTGGAGCCATTGTGGCGAATTGGCACGAGGGATGGGACCGCAACCCGGAAGAGTTGGGTAACTACGCGGAGGGTGATATTCTATTTCCGCCCCAGCTCGGCAAGAATGGGCTTAAAGCGGAAGCTGCACGGTGGCCCGGTGGCGTGGTGCCGTATATGATTAGCCCTTATTTCGACACCGCTCAAAGAAACTTGATTTACGAAGCAATGAACGATTACCACAAGCAATATACGTGCGTTAAATTCAAACCTTATACCGGTGAGGAGAGCGATTATATCAGGATCACGGCTGGCAAT [...]
+        assert peptide1String == "MNRISFISFSVLLVSVSAWPFRRRDVFDNAVDSPHGVIAHLQHFGEMLYRNPDNETGAIVANWHEGWDRNPEELGNYAEGDILFPPQLGKNGLKAEAARWPGGVVPYMISPYFDTAQRNLIYEAMNDYHKQYTCVKFKPYTGEESDYIRITAGNTGCWSSVGRIGGKQDVNLQVPGCVLKKGTVIHELMHAIGFLHEQSRFERDEYVTIQWNNILRNHAGNFEKASKETTDAFGIGYDYGSVMHYSANAFSRNGQPTIVPKESGGLLSFIGEIFQGDTRVQLGQREGFSKRDIQKIRKMYKCNKRRRSSY"
+
+        when: "we add a deletion of 3 bases @ 1089850"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString2) as JSONObject)
+
+        then: "we should see the deletion"
+        assert Deletion.all.size() == 1
+
+        then: "the deletion should be seen in the transcript's sequence"
+        String genomic2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+        String cdna2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String peptide2String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        assert genomic2String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTT [...]
+        assert cdna2String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTTATC [...]
+        assert cds2String == "ATGAATCGTATATCCTTTATCTCTTTTTCCGTTCTACTGGTTTCGGTATCAGCATGGCCATTTCGTCGTCGAGACGTCTTCGACAATGCGGTGGACAGTCCTCACGGCGTGATCGCTCATCTGCAACACTTTGGCGAGATGTTGTACCGGAATCCGGACAACGAAACTGGAGCCATTGTGGCGAATTGGCACGAGGGATGGGACCGCAACCCGGAAGAGTTGGGTAACTACGCGGAGGGTGATATTCTATTTCCGCCCCAGCTCGGCAAGAATGGGCTTAAAGCGGAAGCTGCACGGTGGCCCGGTGGCGTGGTGCCGTATATGATTAGCCCTTATTTCGACACCGCTCAAAGAAACTTGATTTACGAAGCAATGAACGATTACCACAAGCAATATACGTGCGTTAAATTCAAACCTTATACCGGTGAGGAGAGCGATTACAGGATCACGGCTGGCAATACT [...]
+        assert peptide2String == "MNRISFISFSVLLVSVSAWPFRRRDVFDNAVDSPHGVIAHLQHFGEMLYRNPDNETGAIVANWHEGWDRNPEELGNYAEGDILFPPQLGKNGLKAEAARWPGGVVPYMISPYFDTAQRNLIYEAMNDYHKQYTCVKFKPYTGEESDYRITAGNTGCWSSVGRIGGKQDVNLQVPGCVLKKGTVIHELMHAIGFLHEQSRFERDEYVTIQWNNILRNHAGNFEKASKETTDAFGIGYDYGSVMHYSANAFSRNGQPTIVPKESGGLLSFIGEIFQGDTRVQLGQREGFSKRDIQKIRKMYKCNKRRRSSY"
+
+        when: "we add a deletion of 2 bases @ 1089825"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString3) as JSONObject)
+
+        then: "we should see the deletion"
+        assert Deletion.all.size() == 2
+
+        then: "the deletion should be seen in the transcript's sequence"
+        String genomic3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+        String cdna3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String peptide3String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        assert genomic3String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTT [...]
+        assert cdna3String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTTATC [...]
+        assert cds3String == "ATGAATCGTATATCCTTTATCTCTTTTTCCGTTCTACTGGTTTCGGTATCAGCATGGCCATTTCGTCGTCGAGACGTCTTCGACAATGCGGTGGACAGTCCTCACGGCGTGATCGCTCATCTGCAACACTTTGGCGAGATGTTGTACCGGAATCCGGACAACGAAACTGGAGCCATTGTGGCGAATTGGCACGAGGGATGGGACCGCAACCCGGAAGAGTTGGGTAACTACGCGGAGGGTGATATTCTATTTCCGCCCCAGCTCGGCAAGAATGGGCTTAAAGCGGAAGCTGCACGGTGGCCCGGTGGCGTGGTGCCGTATATGATTAGCCCTTATTTCGACACCGCTCAAAGAAACTTGATTTACGAAGCAATGAACGATTACCACAAGCAATATACGTGCGTTAAATTCAAACCTTATACCGGTGAGGAGAGCGATTACAGGATCACGGCTGGCAATACT [...]
+        assert peptide3String == "MNRISFISFSVLLVSVSAWPFRRRDVFDNAVDSPHGVIAHLQHFGEMLYRNPDNETGAIVANWHEGWDRNPEELGNYAEGDILFPPQLGKNGLKAEAARWPGGVVPYMISPYFDTAQRNLIYEAMNDYHKQYTCVKFKPYTGEESDYRITAGNTVLEQCGKNRW"
+
+        when: "we add an insertion of C @ 1089804"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString4) as JSONObject)
+
+        then: "we should see the deletion"
+        assert Insertion.all.size() == 2
+
+        then: "the insertion should be seen in the transcript's sequence"
+        String genomic4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+        String cdna4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String peptide4String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        assert genomic4String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTT [...]
+        assert cdna4String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTTATC [...]
+        assert cds4String == "ATGAATCGTATATCCTTTATCTCTTTTTCCGTTCTACTGGTTTCGGTATCAGCATGGCCATTTCGTCGTCGAGACGTCTTCGACAATGCGGTGGACAGTCCTCACGGCGTGATCGCTCATCTGCAACACTTTGGCGAGATGTTGTACCGGAATCCGGACAACGAAACTGGAGCCATTGTGGCGAATTGGCACGAGGGATGGGACCGCAACCCGGAAGAGTTGGGTAACTACGCGGAGGGTGATATTCTATTTCCGCCCCAGCTCGGCAAGAATGGGCTTAAAGCGGAAGCTGCACGGTGGCCCGGTGGCGTGGTGCCGTATATGATTAGCCCTTATTTCGACACCGCTCAAAGAAACTTGATTTACGAAGCAATGAACGATTACCACAAGCAATATACGTGCGTTAAATTCAAACCTTATACCGGTGAGGAGAGCGATTACAGGATCACGGCTGGCAATACT [...]
+        assert peptide4String == "MNRISFISFSVLLVSVSAWPFRRRDVFDNAVDSPHGVIAHLQHFGEMLYRNPDNETGAIVANWHEGWDRNPEELGNYAEGDILFPPQLGKNGLKAEAARWPGGVVPYMISPYFDTAQRNLIYEAMNDYHKQYTCVKFKPYTGEESDYRITAGNTVLEQCGKRSVVNKT"
+
+        when: "we add an insertion of TCC @ 1090350"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString5) as JSONObject)
+
+        then: "we should see the insertion"
+        assert Insertion.all.size() == 3
+
+        then: "the insertion should be seen in the transcript's sequence"
+        String genomic5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_GENOMIC.value)
+        String cdna5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDNA.value)
+        String cds5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        String peptide5String = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        assert genomic5String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTT [...]
+        assert cdna5String == "TATAAATAATATTTATATAAAAAATTTTTTATACTTTGATAAATTTCATTCCATCATCCTCAATATCTTTCTCATAAAATTTCTCAAATGTATACGGAAAAAAAAAAAATGATTCAGAATTATGAATACGATATTATTTACAACCTCACCAATGACATTCGATGATTCCCTCCACGATTATCATCCCATACATCAAAAATATCATATCGCGTAAAGCTATGAGAAATCGTACGAGTAAATCTCGACTTCCTGTTACCCAAAGTCCACTAGTTATCACCAACCGTCCCCGATCGTTTCCATATCAAGCGGAAGCATCGAGCAAATCCACGGGTGGGGTTTTTCAAGTGCGTATAAAATCAGAGAGAACCGGTGAATTTTCGCCAGTGGAATCAACACCTTGCGAAGAACGCTTCTAAATATTCTTTCGAATACAACTCGCTATGAATCGTATATCCTTTATC [...]
+        assert cds5String == "ATGAATCGTATATCCTTTATCTCTTTTTCCGTTCTACTGGTTTCGGTATCAGCATGGCCATTTCGTCGTCGAGACGTCTTCGACAATGCGGTGGACAGTCCTCACGGCGTGATCGCTCATCTGCAACACTTTGGCGAGATGTTGTACCGGAATCCGGACAACGAAACTGGAGCCATTGTGGCGAATTGGCACGAGGGATGGGACCGCAACCCGGAAGAGTTGGGTAACTACGCGGAGGGTGATATTCTATTTCCGCCCCAGCTCGGCAAGAATGGGCTTAAAGCGGGAGAAGCTGCACGGTGGCCCGGTGGCGTGGTGCCGTATATGATTAGCCCTTATTTCGACACCGCTCAAAGAAACTTGATTTACGAAGCAATGAACGATTACCACAAGCAATATACGTGCGTTAAATTCAAACCTTATACCGGTGAGGAGAGCGATTACAGGATCACGGCTGGCAAT [...]
+        assert peptide5String == "MNRISFISFSVLLVSVSAWPFRRRDVFDNAVDSPHGVIAHLQHFGEMLYRNPDNETGAIVANWHEGWDRNPEELGNYAEGDILFPPQLGKNGLKAGEAARWPGGVVPYMISPYFDTAQRNLIYEAMNDYHKQYTCVKFKPYTGEESDYRITAGNTVLEQCGKRSVVNKT"
+
+    }
+
+    /**
+     * From #427
+     *
+     *
+     */
+    void "add sequence alteration should work on intron and all multi-exon genes"() {
+
+        given: "a Gene GB40788-RA"
+        String addTranscriptString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":65107,\"fmax\":75367,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40788-RA\",\"children\":[{\"location\":{\"fmin\":65107,\"fmax\":65286,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":71477,\"fmax\":71651,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"e [...]
+        String peptideSequencePlain = "MKDRPHRPYRDHHGQAMPLEEVQGLLLPPSRTGNRGPLTIVQVGKGNGGGGDGGSDLLRLEPPSDLRPTPSPLSETSATLQSDNNDTFSGGVDPRLLLGANTGGDRNTWEGRYRVKEHRRAGKATFQGQVYNFLERPTGWKCFLYHFSV"
+        String addSAS1String = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 71549, \"fmax\": 71549, \"strand\": 1 }, \"type\": {\"name\": \"insertion\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"AAAAAAAAAAAAAAAAAAAA\" } ], \"operation\": \"add_sequence_alteration\" }"
+        String insertionStringOne = "MKDRPHRPYRDHHGQAMPLEEVQGLLLPPSRTGNRGPLTIVQVGKGNGGGGDGGSDLLRLEPPSDLIFFFFFFGQLRRPCPKRAQHCSPTTMIPLVEVSIHDYCWAQTPGATVTRGRVVTV"
+        String addSASIntronString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 71199, \"fmax\": 71199, \"strand\": 1 }, \"type\": {\"name\": \"insertion\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"GGGGGGGGGGGGGGGGGGGGG\" } ], \"operation\": \"add_sequence_alteration\" }"
+        String insertionStringTwo = "MKDRPHRPYRDHHGQAMPLEEVQGLLLPPSRTGNRGPLTIVQVGKGNGGGGDGGSDLLRLEPPSDLIFFFFFFGQLRRPCPKRAQHCSPTTMIPLVEVSIHDYCWAQTPGATVTRGRVVTV"
+        String addSADeleteString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 71499, \"fmax\": 71509, \"strand\": 1 }, \"type\": {\"name\": \"deletion\", \"cv\": { \"name\":\"sequence\" } } } ], \"operation\": \"add_sequence_alteration\" }"
+        String deletionString = "MKDRPHRPYRDHHGQAMPLEEVQGLLLPPSRTGNRGPLTIVQVGKGNGGGGDGGSDLLRLEPPSDLIFFFFFFGQLRRPCPKRAQH"
+        String addSASubstitutionString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 71624, \"fmax\": 71638, \"strand\": 1 }, \"type\": {\"name\": \"substitution\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"TTTTTTTTTTTTTT\" } ], \"operation\": \"add_sequence_alteration\" }"
+        String finalSubstitutionString = "MKDRPHRPYRDHHGQAMPLEEVQGLLLPPSRTGNRGPKKKKKVGKGNGGGGDGGSDLLRLEPPSDLIFFFFFFGQLRRPCPKRAQH"
+
+
+        when: "we add the gene"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+        MRNA transcript = MRNA.first()
+        String residues = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we expect to see some genomic results and some residues"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert residues == peptideSequencePlain
+
+        when: "we add an insert sequence alteration to an exon"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSAS1String) as JSONObject)
+        transcript = MRNA.first()
+        residues = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we expect the appropriate residues"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert residues == insertionStringOne
+
+        when: "we add an insert sequence alteration to an intron"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSASIntronString) as JSONObject)
+        transcript = MRNA.first()
+        residues = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we expect the same residues"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert residues == insertionStringTwo
+
+        when: "we add a delete sequence alteration"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSADeleteString) as JSONObject)
+        transcript = MRNA.first()
+        residues = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we expect less residues"
+        assert residues == deletionString
+
+        when: "we add a substitution sequence alteration"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSASubstitutionString) as JSONObject)
+        transcript = MRNA.first()
+        residues = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we expect different residues"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 3
+        assert residues == finalSubstitutionString
+
+
+    }
+
+    void "when sequence alterations are added at intron-exon boundaries"() {
+
+        given: "when we add a transcript GB40837-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1037679,\"strand\":1,\"fmax\":1042109},\"name\":\"GB40837-RA\",\"children\":[{\"location\":{\"fmin\":1041232,\"strand\":1,\"fmax\":1042109},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1037679,\"strand\":1,\"fmax\":1037700},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1039030,\"stra [...]
+        String originalPeptideString = "MLGKLLLMLLIQPSEEMSKQIRMELNENVATRDKDVEVIKEWLSKQPHLPQFDDDYRLLTFLRGCKFSLEKCKKKLDMYFTMRTAIPEFFTNRDVTLPELKEITKIIQIPPLPGLTKNGRRVIVMRGINKDLPTPNVAELMKLVLMIGDVRLKEELMGVAGDVYILDASVATPSHFAKFTPALVKKFLVCVQEAYPVKLKEVHVVNISPLVDTIVNFVKPFIKEKIRNRIFMHSDLNTLYEYIPREILPAEYGGDAGPLQNIHETWIKKLEEYGPWFVEQESIKTNEALRPGKPKTHDDLFGLDGSFRQLVID"
+        String originalCdnaString = "ATGTTAGGCAAGCTGCTTCTGATGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTATTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTT [...]
+        String originalCdsString = "ATGTTAGGCAAGCTGCTTCTGATGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTATTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTA [...]
+
+        String addDeletion1String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":1039024,\"strand\":1,\"fmax\":1039031},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String deletion1PeptideString = "VRQAASVLLIQPSEEMSKQIRMELNENVATRDKDVEVIKEWLSKQPHLPQFDDDYRLLTFLRGCKFSLEKCKKKLDMYFTMRTAIPEFFTNRDVTLPELKEITKIIQIPPLPGLTKNGRRVIVMRGINKDLPTPNVAELMKLVLMIGDVRLKEELMGVAGDVYILDASVATPSHFAKFTPALVKKFLVCVQEAYPVKLKEVHVVNISPLVDTIVNFVKPFIKEKIRNRIFMHSDLNTLYEYIPREILPAEYGGDAGPLQNIHETWIKKLEEYGPWFVEQESIKTNEALRPGKPKTHDDLFGLDGSFRQLVID"
+        String deletion1CdnaString = "ATGTTAGGCAAGCTGCTTCTGTGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTATTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTT [...]
+        String deletion1CdsString = "GTTAGGCAAGCTGCTTCTGTGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTATTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAAA [...]
+
+        String addDeletion2String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":1039645,\"strand\":1,\"fmax\":1039648},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String deletion2PeptideString = "MRGINKDLPTPNVAELMKLVLMIGDVRLKEELMGVAGDVYILDASVATPSHFAKFTPALVKKFLVCVQEAYPVKLKEVHVVNISPLVDTIVNFVKPFIKEKIRNRIFMHSDLNTLYEYIPREILPAEYGGDAGPLQNIHETWIKKLEEYGPWFVEQESIKTNEALRPGKPKTHDDLFGLDGSFRQLVID"
+        String deletion2CdnaString = "ATGTTAGGCAAGCTGCTTCTGTGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAA [...]
+        String deletion2CdsString = "ATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAAAAGAAGAATTAATGGGAGTCGCAGGAGACGTGTATATTTTAGATGCTAGTGTCGCAACGCCATCTCACTTCGCCAAATTCACACCAGCTCTCGTGAAGAAATTCCTAGTATGCGTGCAAGAGGCATATCCAGTAAAATTGAAAGAGGTGCATGTAGTGAATATTAGTCCTCTGGTCGATACTATCGTCAATTTCGTGAAACCATTCATTAAAGAAAAAATTCGCAACAGAATTTTCATGCATAGTGATTTGAACACTTTATACGAATATATACCTAGGGAAATATTGCCAGCCGAATATGGCGGCGATGCTGGACCTCTACAGAATATACATGAGACCTGGATAAAGAAATTAGAAGAATATGGTCCTTG [...]
+
+        String addSubstitution3String = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"CCCC\",\"location\":{\"fmin\":1040815,\"strand\":1,\"fmax\":1040819},\"type\":{\"name\":\"substitution\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+        String substitution3PeptideString = "MRGINKDLPTPNVAELMKLVLMIGDVRLKEELMGVAGDVYILDASVATPSHFAKFTPALVKKFLVCVQEAYPVKLKEVHVVNISPLVDTIVNFVKPFIKEKIRNRIFMHSDLNTLYEYIPREILPAEYGGDAGPLQNIHQTWIKKLEEYGPWFVEQESIKTNEALRPGKPKTHDDLFGLDGSFRQLVID"
+        String substitution3CdnaString = "ATGTTAGGCAAGCTGCTTCTGTGTTGCTGATCCAACCGTCGGAAGAGATGTCGAAGCAGATTCGCATGGAGTTGAATGAAAATGTGGCGACACGCGATAAGGACGTGGAAGTCATCAAGGAATGGCTATCTAAGCAACCACATTTACCCCAGTTCGATGATGATTACAGATTATTGACGTTTCTTCGAGGTTGTAAATTCTCCTTGGAAAAATGTAAGAAAAAACTGGACATGTATTTCACGATGAGAACCGCAATCCCAGAGTTCTTTACTAATCGAGATGTCACTTTACCAGAACTGAAAGAAATCACTAAAATTTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGT [...]
+        String substitution3CdsString = "ATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAAAAGAAGAATTAATGGGAGTCGCAGGAGACGTGTATATTTTAGATGCTAGTGTCGCAACGCCATCTCACTTCGCCAAATTCACACCAGCTCTCGTGAAGAAATTCCTAGTATGCGTGCAAGAGGCATATCCAGTAAAATTGAAAGAGGTGCATGTAGTGAATATTAGTCCTCTGGTCGATACTATCGTCAATTTCGTGAAACCATTCATTAAAGAAAAAATTCGCAACAGAATTTTCATGCATAGTGATTTGAACACTTTATACGAATATATACCTAGGGAAATATTGCCAGCCGAATATGGCGGCGATGCTGGACCTCTACAGAATATACACCAGACCTGGATAAAGAAATTAGAAGAATATGGTC [...]
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should have 1 MRNA and its proper sequence"
+        assert MRNA.count == 1
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert Exon.count == 5
+
+        when: "we add a deletion at an intron-exon boundary at position 1039025"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion1String) as JSONObject)
+
+        then: "we should have the deletion added and the proper sequences"
+        assert SequenceAlteration.count == 1
+
+        Transcript transcript = Transcript.findByName("GB40837-RA-00001")
+        String deletion1PeptideSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String deletion1CdnaSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDNA.value)
+        String deletion1CdsSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDS.value)
+
+        assert deletion1PeptideSequence == deletion1PeptideString
+        assert deletion1CdnaSequence == deletion1CdnaString
+        assert deletion1CdsSequence == deletion1CdsString
+
+        when: "we add a deletion at an exon-intron boundary at position 1039646"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletion2String) as JSONObject)
+
+        then: "we should have the deletion added and the proper sequences"
+        assert SequenceAlteration.count == 2
+
+        String deletion2PeptideSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String deletion2CdnaSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDNA.value)
+        String deletion2CdsSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDS.value)
+
+        assert deletion2PeptideSequence == deletion2PeptideString
+        assert deletion2CdnaSequence == deletion2CdnaString
+        assert deletion2CdsSequence == deletion2CdsString
+
+        when: "we add a substitution of at intron-exon bounary at position 1040816"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSubstitution3String) as JSONObject)
+
+        then: "we should have the substitution added and the proper sequences"
+        assert SequenceAlteration.count == 3
+
+        String substitution3PeptideSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String substitution3CdnaSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDNA.value)
+        String substitution3CdsSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDS.value)
+
+        assert substitution3PeptideSequence == substitution3PeptideString
+        assert substitution3CdnaSequence == substitution3CdnaString
+        assert substitution3CdsSequence == substitution3CdsString
+
+    }
+
+    void "when we add a transcript and a sequence alteration, we should be able to get the proper sequence of individual exons"() {
+        given: "The GB40843-RA transcript"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1092561,\"strand\":1,\"fmax\":1095202},\"name\":\"GB40843-RA\",\"children\":[{\"location\":{\"fmin\":1092561,\"strand\":1,\"fmax\":1092941},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1094825,\"strand\":1,\"fmax\":1095202},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1092561,\"stra [...]
+        String addDeletionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"location\":{\"fmin\":1094155,\"strand\":1,\"fmax\":1094167},\"type\":{\"name\":\"deletion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\",'clientToken':'123123'}"
+
+        String expectedTranscriptPeptideSequence = "MTLDNSWHIIKTAAAENKHELVLSGPAISELIQKRGFDKSLFNLEHLNYLNITQTCLHEVSEEIEKLQNLTTLVLHSNEISKIPSSIGKLEKLKVLDCSRNKLTSLPDEIGKLPQLTTMNFSSNFLKSLPTQIDNVKLTVLDLSNNQFEDFPDVCYTELVHLSEIYVMGNQIKEIPTIINQLPSLKIINVANNRISVIPGEIGDCNKLKELQLKGNPLSDKRLLKLVDQCHNKQILEYVKLHCPKQDGSVNSNSKSKKGKKVQKLSESENNANVMDNLTHKMKILKMANDTPVIKVTKYVKNIRPYIAACIVRNMSFTEDSFKKFIQLQTKLHDGICEKRNAATIATHDLKLITTDIHSKTTK"
+        String expectedTranscriptCdnaSequence = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAATACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTA [...]
+        String expectedTranscriptCdsSequence = "ATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTGTTGTCAGGTCCGGCAATCTCTGAATTGATTCAAAAAAGAGGCTTCGACAAGTCTTTATTTAACCTAGAGCATTTGAATTATTTGAATATTACTCAGACATGTTTACACGAAGTGTCAGAAGAAATTGAAAAATTGCAAAATCTAACAACATTGGTATTGCATTCGAATGAGATTTCGAAGATACCTAGCTCAATCGGGAAATTAGAAAAACTAAAGGTTCTCGATTGCTCTAGGAACAAGTTGACGTCTTTACCAGATGAAATCGGTAAACTTCCACAATTAACAACCATGAACTTCAGTTCAAATTTTTTGAAATCATTACCTACGCAAATTGACAATGTTAAGTTGACTGTTTTGGATCTTTCGAACAATCAGTTT [...]
+        String expectedTranscriptGenomicSequence = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAATACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAA [...]
+
+        String expectedExonOnePeptideSequence = "MTLDNSWHIIKTAAAENKHELVLSGPAISELIQKRGFDKSLFNLEHLNYLNITQTCLHEVSEEIEKLQNLTTLVLHSNEISKIPSSIGKLEKLKVLDCSRNKLTSLPDEIGKLPQLTTMNFSSNFLKSLPTQIDNVKLTVLDLSNNQFEDFPDVCYTELVHLSEIYVMGNQIKEIPTIINQLPSLKIINVANNRIS"
+        String expectedExonOneCdnaSequence = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAATACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTG [...]
+        String expectedExonOneCdsSequence = "ATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTAGTGTTGTCAGGTCCGGCAATCTCTGAATTGATTCAAAAAAGAGGCTTCGACAAGTCTTTATTTAACCTAGAGCATTTGAATTATTTGAATATTACTCAGACATGTTTACACGAAGTGTCAGAAGAAATTGAAAAATTGCAAAATCTAACAACATTGGTATTGCATTCGAATGAGATTTCGAAGATACCTAGCTCAATCGGGAAATTAGAAAAACTAAAGGTTCTCGATTGCTCTAGGAACAAGTTGACGTCTTTACCAGATGAAATCGGTAAACTTCCACAATTAACAACCATGAACTTCAGTTCAAATTTTTTGAAATCATTACCTACGCAAATTGACAATGTTAAGTTGACTGTTTTGGATCTTTCGAACAATCAGTTTGAA [...]
+        String expectedExonOneGenomicSequence = "AATTAATAGTATTCATTTTTTATCTTTTGATCAAGCTCATATTTATAATTAAATATCAAAATATAATTCATAGATAACATTAAAATTTGTTTATAGTTTAATTCACTAAAATACGTTTATCATTATTGTTTAATCAAATTATATGTGACGGAAACTATCCGTTATATATCTCTACAAAACTTTTATTAGATTTAGGTTATTTGATGTCTCGTCTTAAATTCATTTATTCTTTTCAATCGCAATTTTTAAATTGCATATGTACGTAGATGTCGTTATAATCGTATAACGCATGTTTCAAATTGATCAACCCGGCAAGTAACCTTGAAACTACGTAAATAACATTACCCTTTTATTAAAAATTCTTTTAATACTTAATAATAATGACGCTCGATAATTCATGGCATATAATCAAAACTGCAGCTGCAGAGAATAAACATGAATTA [...]
+
+        String expectedExonTwoPeptideSequence = "IPGEIGDCNKLKELQLKGNPLSDKRLLKLVDQCHNKQILEYVKLHCPKQDGSVNSNSKSKKGKKVQKLSESENNANVMDNLTHKMKILKMANDTPVIKVTKYVKNIRPYIAACIVRNMSFTEDSFKKFIQLQTKLHDGICEKRNAATIATHDLKLITT"
+        String expectedExonTwoCdnaSequence = "TTATTCCAGGTGAGATTGGTGATTGTAATAAACTCAAAGAACTTCAATTAAAAGGAAATCCATTATCAGACAAAAGATTATTAAAATTAGTTGATCAATGTCATAATAAACAAATATTAGAATATGTAAAATTGCATTGTCCTAAACAAGATGGTTCTGTCAATTCAAATTCAAAATCAAAAAAAGGAAAAAAGGTACAAAAATTATCAGAAAGTGAAAATAATGCTAATGTAATGGATAATTTGACACACAAAATGAAAATATTAAAAATGGCAAATGATACACCAGTAATTAAGGTTACAAAATATGTGAAAAATATCAGACCTTACATTGCAGCTTGTATTGTTAGAAATATGAGCTTCACAGAAGATAGTTTTAAAAAATTTATTCAACTTCAAACTAAATTACATGATGGCATATGTGAAAAAAGAAATGCAGCTACTATT [...]
+        String expectedExonTwoCdsSequence = "TTATTCCAGGTGAGATTGGTGATTGTAATAAACTCAAAGAACTTCAATTAAAAGGAAATCCATTATCAGACAAAAGATTATTAAAATTAGTTGATCAATGTCATAATAAACAAATATTAGAATATGTAAAATTGCATTGTCCTAAACAAGATGGTTCTGTCAATTCAAATTCAAAATCAAAAAAAGGAAAAAAGGTACAAAAATTATCAGAAAGTGAAAATAATGCTAATGTAATGGATAATTTGACACACAAAATGAAAATATTAAAAATGGCAAATGATACACCAGTAATTAAGGTTACAAAATATGTGAAAAATATCAGACCTTACATTGCAGCTTGTATTGTTAGAAATATGAGCTTCACAGAAGATAGTTTTAAAAAATTTATTCAACTTCAAACTAAATTACATGATGGCATATGTGAAAAAAGAAATGCAGCTACTATTG [...]
+        String expectedExonTwoGenomicSequence = "TTATTCCAGGTGAGATTGGTGATTGTAATAAACTCAAAGAACTTCAATTAAAAGGAAATCCATTATCAGACAAAAGATTATTAAAATTAGTTGATCAATGTCATAATAAACAAATATTAGAATATGTAAAATTGCATTGTCCTAAACAAGATGGTTCTGTCAATTCAAATTCAAAATCAAAAAAAGGAAAAAAGGTACAAAAATTATCAGAAAGTGAAAATAATGCTAATGTAATGGATAATTTGACACACAAAATGAAAATATTAAAAATGGCAAATGATACACCAGTAATTAAGGTTACAAAATATGTGAAAAATATCAGACCTTACATTGCAGCTTGTATTGTTAGAAATATGAGCTTCACAGAAGATAGTTTTAAAAAATTTATTCAACTTCAAACTAAATTACATGATGGCATATGTGAAAAAAGAAATGCAGCTACT [...]
+
+        String expectedExonThreePeptideSequence = "IHSKTTK"
+        String expectedExonThreeCdnaSequence = "ACATACACAGCAAAACCACCAAATGAATTAGAAATCAAGCCATTAATGCGCAATAAAGTTTATACTGGTTCTGAACTATTCAAACAGTTACAAACTGAAGCTGATAATTTAAGGAAAGAAAAAAAACGCAATGTATATTCTGGCATTCATAAATATCTTTATCTATTAGAAGGTAAACCTTACTTTCCTTGTTTGTTAGATGCTTCAGAACAAGTTATATCATTTCCACCTATAACGAATAGTGATATTACAAAAATGTCAATAAATACCCAAGCAATATTAATAGAAGTAACCAGTGCTTCATCCTATCAAATTTGCAG"
+        String expectedExonThreeCdsSequence = "ACATACACAGCAAAACCACCAAATGA"
+        String expectedExonThreeGenomicSequence = "ACATACACAGCAAAACCACCAAATGAATTAGAAATCAAGCCATTAATGCGCAATAAAGTTTATACTGGTTCTGAACTATTCAAACAGTTACAAACTGAAGCTGATAATTTAAGGAAAGAAAAAAAACGCAATGTATATTCTGGCATTCATAAATATCTTTATCTATTAGAAGGTAAACCTTACTTTCCTTGTTTGTTAGATGCTTCAGAACAAGTTATATCATTTCCACCTATAACGAATAGTGATATTACAAAAATGTCAATAAATACCCAAGCAATATTAATAGAAGTAACCAGTGCTTCATCCTATCAAATTTGCAG"
+
+        String expectedExonFourPeptideSequence = ""
+        String expectedExonFourCdnaSequence = "GAATGTATTAGATCAATTTTTAAAGGAACTAGTTACTTTTGGTTTAGGATGTGTCTCAGAACAAGAAAATGCTTCAAATTATCATAAATTAATCATAGAACAAGTAAAAGTGGTAGATATGGAAGGTAATATGAAATTAGTATATCCTTCAAGAGCAGATTTAAATTTTGCAGAAAATTTTATAACAGTATTACGCGAGTAATAAATTACTGTAAGTAATTAAATTGTTCTTTAATCTTGATCTAATCTGCATTTTTCTTTCTTAATCTTTTTAACTATTATTTTTTTGATTGATAAGTTGTAAATCTAAATTATTTTCATATTATTACTTTTTCATAAATAACACATTATTTTCATATGCCAAATTGTAATTTTTTATTTGTTACTCTGTGAAAATCTTAAGATTAGTATGCAATGTATATAATATTTGCATATTTTATACAGA [...]
+        String expectedExonFourCdsSequence = ""
+        String expectedExonFourGenomicSequence = "GAATGTATTAGATCAATTTTTAAAGGAACTAGTTACTTTTGGTTTAGGATGTGTCTCAGAACAAGAAAATGCTTCAAATTATCATAAATTAATCATAGAACAAGTAAAAGTGGTAGATATGGAAGGTAATATGAAATTAGTATATCCTTCAAGAGCAGATTTAAATTTTGCAGAAAATTTTATAACAGTATTACGCGAGTAATAAATTACTGTAAGTAATTAAATTGTTCTTTAATCTTGATCTAATCTGCATTTTTCTTTCTTAATCTTTTTAACTATTATTTTTTTGATTGATAAGTTGTAAATCTAAATTATTTTCATATTATTACTTTTTCATAAATAACACATTATTTTCATATGCCAAATTGTAATTTTTTATTTGTTACTCTGTGAAAATCTTAAGATTAGTATGCAATGTATATAATATTTGCATATTTTATAC [...]
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we see the added transcript"
+        assert MRNA.count == 1
+        assert Exon.count == 4
+        MRNA transcript = MRNA.findByName("GB40843-RA-00001")
+        List<Exon> exonList = transcriptService.getSortedExons(transcript, true)
+
+        when: "we add a deletion at position 1094156"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletionString) as JSONObject)
+
+        then: "we see the alteration"
+        SequenceAlteration.count == 1
+
+        when: "we get sequence of the whole transcript"
+        String getTranscriptPeptideSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_PEPTIDE.value)
+        String getTranscriptCdnaSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDNA.value)
+        String getTranscriptCdsSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_CDS.value)
+        String getTranscriptGenomicSequence = sequenceService.getSequenceForFeature(transcript, FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we get the expected sequences"
+        assert getTranscriptPeptideSequence == expectedTranscriptPeptideSequence
+        assert getTranscriptCdnaSequence == expectedTranscriptCdnaSequence
+        assert getTranscriptCdsSequence == expectedTranscriptCdsSequence
+        assert getTranscriptGenomicSequence == expectedTranscriptGenomicSequence
+
+        when: "we get sequence of exon 1"
+        String getExonOnePeptideSequence = sequenceService.getSequenceForFeature(exonList[0], FeatureStringEnum.TYPE_PEPTIDE.value)
+        String getExonOneCdnaSequence = sequenceService.getSequenceForFeature(exonList[0], FeatureStringEnum.TYPE_CDNA.value)
+        String getExonOneCdsSequence = sequenceService.getSequenceForFeature(exonList[0], FeatureStringEnum.TYPE_CDS.value)
+        String getExonOneGenomicSequence = sequenceService.getSequenceForFeature(exonList[0], FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we get the expected sequences"
+        assert getExonOnePeptideSequence == expectedExonOnePeptideSequence
+        assert getExonOneCdnaSequence == expectedExonOneCdnaSequence
+        assert getExonOneCdsSequence == expectedExonOneCdsSequence
+        assert getExonOneGenomicSequence == expectedExonOneGenomicSequence
+
+        when: "we get the sequence of exon 2"
+        String getExonTwoPeptideSequence = sequenceService.getSequenceForFeature(exonList[1], FeatureStringEnum.TYPE_PEPTIDE.value)
+        String getExonTwoCdnaSequence = sequenceService.getSequenceForFeature(exonList[1], FeatureStringEnum.TYPE_CDNA.value)
+        String getExonTwoCdsSequence = sequenceService.getSequenceForFeature(exonList[1], FeatureStringEnum.TYPE_CDS.value)
+        String getExonTwoGenomicSequence = sequenceService.getSequenceForFeature(exonList[1], FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we get the expected sequences"
+        assert getExonTwoPeptideSequence == expectedExonTwoPeptideSequence
+        assert getExonTwoCdnaSequence == expectedExonTwoCdnaSequence
+        assert getExonTwoCdsSequence == expectedExonTwoCdsSequence
+        assert getExonTwoGenomicSequence == expectedExonTwoGenomicSequence
+
+        when: "we get the sequence of exon 3, the affected exon"
+        String getExonThreePeptideSequence = sequenceService.getSequenceForFeature(exonList[2], FeatureStringEnum.TYPE_PEPTIDE.value)
+        String getExonThreeCdnaSequence = sequenceService.getSequenceForFeature(exonList[2], FeatureStringEnum.TYPE_CDNA.value)
+        String getExonThreeCdsSequence = sequenceService.getSequenceForFeature(exonList[2], FeatureStringEnum.TYPE_CDS.value)
+        String getExonThreeGenomicSequence = sequenceService.getSequenceForFeature(exonList[2], FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we get the expected sequences"
+        assert getExonThreePeptideSequence == expectedExonThreePeptideSequence
+        assert getExonThreeCdnaSequence == expectedExonThreeCdnaSequence
+        assert getExonThreeCdsSequence == expectedExonThreeCdsSequence
+        assert getExonThreeGenomicSequence == expectedExonThreeGenomicSequence
+
+        when: "we get the sequence of exon 4"
+        String getExonFourPeptideSequence = sequenceService.getSequenceForFeature(exonList[3], FeatureStringEnum.TYPE_PEPTIDE.value)
+        String getExonFourCdnaSequence = sequenceService.getSequenceForFeature(exonList[3], FeatureStringEnum.TYPE_CDNA.value)
+        String getExonFourCdsSequence = sequenceService.getSequenceForFeature(exonList[3], FeatureStringEnum.TYPE_CDS.value)
+        String getExonFourGenomicSequence = sequenceService.getSequenceForFeature(exonList[3], FeatureStringEnum.TYPE_GENOMIC.value)
+
+        then: "we get the expected sequences"
+        assert getExonFourPeptideSequence == expectedExonFourPeptideSequence
+        assert getExonFourCdnaSequence == expectedExonFourCdnaSequence
+        assert getExonFourCdsSequence == expectedExonFourCdsSequence
+        assert getExonFourGenomicSequence == expectedExonFourGenomicSequence
+    }
+
+    // TODO
+    void "when exons from three isoforms, genes should merge on overlap and split on separation"() {
+
+        given: "Three exons we are turning into three transcripts using Group 1.10 GB40782-RA"
+//        String transcript1 = '{"track":"Group1.10","features":[{"location":{"fmin":143521,"fmax":146600,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"GB40798-RA","children":[{"location":{"fmin":143521,"fmax":143793,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":146021,"fmax":146600,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":143521,"fmax":143802,"strand":1},"type":{"cv":{"name":"sequence"},"nam [...]
+//        String transcript2 = '{"track":"Group1.10","features":[{"location":{"fmin":147460,"fmax":152444,"strand":1},"type":{"cv":{"name":"sequence"},"name":"mRNA"},"name":"GB40799-RA","children":[{"location":{"fmin":147460,"fmax":147608,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":147687,"fmax":147729,"strand":1},"type":{"cv":{"name":"sequence"},"name":"exon"}},{"location":{"fmin":148511,"fmax":148946,"strand":1},"type":{"cv":{"name":"sequence"},"nam [...]
+
+        String moveExonThereCommand
+        String moveExonBackCommand
+
+        when: "we add the three non-overlapping transcripts"
+//        JSONObject jsonAddTranscriptObject1 = JSON.parse(transcript1) as JSONObject
+//        JSONObject jsonAddTranscriptObject2 = JSON.parse(transcript2) as JSONObject
+//        JSONObject returnCommand1 = requestHandlingService.addTranscript(jsonAddTranscriptObject1)
+//        JSONObject returnCommand2 = requestHandlingService.addTranscript(jsonAddTranscriptObject2)
+
+        then: "we should have two transcripts and two genes!"
+//        assert Gene.count == 3
+//        assert MRNA.count == 3
+
+        when: "we move the one so that it overlaps"
+        //
+
+        then: "we should have one gene "
+//        assert Gene.count == 1
+//        assert MRNA.count == 3
+
+
+        when: "we move the exon back"
+
+
+        then: "we should have three genes again"
+//        assert Gene.count == 3
+//        assert MRNA.count == 3
+
+        when: "we move it to overlap one"
+
+
+        then: "we will have 2 genes and 3 transcripts"
+
+
+        when: "we move it to overlap the other one"
+
+
+        then: "we will have the same 2 genes and 3 transcripts, but transcript will belong to the other gene"
+
+    }
+
+    void "Sequence alterations that introduce an in-frame stop codon should properly be handled"() {
+        given: "GB40750-RA and a sequence alteration of type 'deletion' that has an in-frame stop codon TAA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":689640,\"strand\":-1,\"fmax\":693859},\"name\":\"GB40750-RA\",\"children\":[{\"location\":{\"fmin\":693543,\"strand\":-1,\"fmax\":693859},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":692451,\"strand\":-1,\"fmax\":692480},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":689640,\"strand\" [...]
+        String addDeletionString = "{${testCredentials} \"operation\":\"add_sequence_alteration\",\"features\":[{\"residues\":\"ATTTC\",\"location\":{\"fmin\":691208,\"strand\":1,\"fmax\":691208},\"type\":{\"name\":\"insertion\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+
+        String knownTranscriptPeptideSequence = 'MMTETHINSNHVLNSGLNTKSEIDKMDDVGWKAKLKIPPKDKRIKTSDVTDTRGNEFEEFCLKRELLMGIFEKGWEKPSPIQEASIPIALSGKDILARAKNGTGKTGAYSIPVLEQVDPRKDVIQALVLVPTRELALQTSQICIELAKHMEIKVMVTTGGTDLRDDIMRIYQSVQVIIATPGRILDLMDKNVANMDHCKTLVLDEADKLLSQDFKGMLDHVISRLPHERQILLYSATFPLTVKQFMEKHLRDPYEINLMEELTLKGVTQYYA'
+        String knownTranscriptCdsSequence = 'ATGATGACAGAAACACATATAAATTCCAATCATGTCCTAAATTCTGGTTTGAATACTAAATCAGAAATCGACAAAATGGACGATGTAGGTTGGAAAGCTAAATTAAAAATTCCACCAAAGGACAAACGAATTAAAACTAGTGATGTTACTGATACTCGTGGCAATGAATTTGAGGAATTTTGCCTAAAACGAGAATTATTAATGGGCATCTTTGAAAAAGGCTGGGAAAAGCCTTCCCCAATTCAAGAAGCCAGTATTCCCATTGCATTATCTGGTAAAGATATCTTGGCCCGTGCAAAAAATGGGACTGGTAAAACTGGGGCCTATTCAATTCCAGTGCTAGAACAGGTTGATCCACGAAAAGATGTGATTCAGGCACTAGTACTTGTACCTACTAGAGAGTTAGCTCTTCAAACATCACAAATTTGCATTGAACTGGCAAAACAT [...]
+        String knownExon6PeptideSequence = 'LPHERQILLYSATFPLTVKQFMEKHLRDPYEINLMEELTLKGVTQYYA'
+        String knownExon6CdsSequence = 'ATTACCACACGAACGTCAGATACTGCTGTATTCAGCCACATTTCCCCTGACAGTGAAACAATTCATGGAAAAACATTTAAGAGATCCATATGAGATTAATTTAATGGAGGAACTCACATTGAAAGGTGTAACACAATATTATGCCTGA'
+
+        when: "we add GB40750-RA"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the gene and transcript"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+
+        when: "we add the deletion at position 691209"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletionString) as JSONObject)
+
+        then: "we should see the sequence alteration"
+        assert SequenceAlteration.count == 1
+
+        when: "we export the peptide sequence"
+        MRNA mrna = MRNA.all.get(0)
+        String peptideSequence = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we should get the expected peptide sequence"
+        assert knownTranscriptPeptideSequence == peptideSequence
+
+        when: "we export the CDS sequence"
+        String cdsSequence = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should get the expected CDS sequence"
+        assert knownTranscriptCdsSequence == cdsSequence
+
+        when: "we export the peptide sequence and CDS sequence from exon 6 of the transcript"
+        def exons = transcriptService.getSortedExons(mrna, true)
+        String exon6PeptideSequence = sequenceService.getSequenceForFeature(exons.get(5), FeatureStringEnum.TYPE_PEPTIDE.value)
+        String exon6CdsSequence = sequenceService.getSequenceForFeature(exons.get(5), FeatureStringEnum.TYPE_CDS.value)
+
+        then: "we should get the expected peptide and CDS sequences"
+        assert knownExon6PeptideSequence == exon6PeptideSequence
+        assert knownExon6CdsSequence == exon6CdsSequence
+    }
+
+    void "Add a gene, set a stop_codon_read_through and fetch sequence of the exon harboring the stop_codon_read_through"() {
+        given: "GB40833-RA"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":974327,\"strand\":1,\"fmax\":976988},\"name\":\"GB40833-RA\",\"children\":[{\"location\":{\"fmin\":974327,\"strand\":1,\"fmax\":974467},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":974616,\"strand\":1,\"fmax\":974636},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":975982,\"strand\":1, [...]
+        String setReadThroughStopCodonString = "{${testCredentials} \"operation\":\"set_readthrough_stop_codon\",\"features\":[{\"readthrough_stop_codon\":true,\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+
+        String knownExon2CdsSequence = 'ATGAGTAATAGAGAAAAAAGTGATTTACGATTAGACCAATGCGTTAAATCGAAAGAGGAAAGAGAAAGAAAGCAATCAGTGACCTCACAAAGTGACGATTCTGATTCAAAGAAAAAAAGGCGTAATACATGTAGTAAGGATAAAAGAAAAAAATCAAAGCATAGAAGCAGCTCTAGTAGCTCCAGTTCCTCAAGTATCTCTAGCCGTGAGGCTAAGCATCAAAATGATAGAGATAGAAGAAATGACAAGTGGGATAAAAGATTTGAGAACGGTGTTAGACCATACCGACCAAATCCAAGAGAAGGTAGAGGATATTATAAAGGACGGAGTGGATATTTGGATAATCGTAAACGTAACTTTGGTTATCGACCTTATAAACATTCTGGATTTTATGATAGAAATAGACACAACAATTCACAATATAATAGGTATAATAGTGAAAGAAGAGGTAA [...]
+        String knownExon3CdsSequence = 'GTTAATTAAAGGTGATGGAGAGGTCATAGAAGAGATAGTGAGTAGGGAGCGTCATAAAGAGATAAATAAACAAGCAACTAAAGGGGATGGAGAATACTTCCAAGCACGCTTGAAAGCCAATGTTTTATGAACTATTTTTCTATGTATGCTAAAAAAATACCCTTATATAATAATTACTTTATTGTCAGTGTTAAAGTCCTGTATGACAATATGTTTTATAATGTCTCAGAGAAAATACTTATATGTTGTACATTAA'
+        String knownExon2PeptideSequence = 'MSNREKSDLRLDQCVKSKEERERKQSVTSQSDDSDSKKKRRNTCSKDKRKKSKHRSSSSSSSSSSISSREAKHQNDRDRRNDKWDKRFENGVRPYRPNPREGRGYYKGRSGYLDNRKRNFGYRPYKHSGFYDRNRHNNSQYNRYNSERRGNRFLNHNSRRPPYDRSRSRSTLDHDHQRNLKDSSEQSRESNSKERYVKQTSADKADSKRKDIDDMHMKGKEKKKNEDTQEKTDHIARKKLKRKRSLSSSSTSSVSSTSSDSSSSSSSSSSTSSSCSSSSSDTSEDEKKRKKARRKAKKLKKAMKKRRKKKRMKKKLKKKLKKSKKKLRNTDKSKESISKDVSQEISEKSKAMAPMTKEEWEKKQNVIRKVYDEETGRY'
+        String knownExon3PeptideSequence = 'LIKGDGEVIEEIVSRERHKEINKQATKGDGEYFQARLKANVLUTIFLCMLKKYPYIIITLLSVLKSCMTICFIMSQRKYLYVVH'
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we see the transcript"
+        MRNA.count == 1
+
+        when: "we set the read through stop codon for the transcript"
+        MRNA mrna = MRNA.all.get(0)
+        requestHandlingService.setReadthroughStopCodon(JSON.parse(setReadThroughStopCodonString.replace("@UNIQUENAME@", mrna.uniqueName)) as JSONObject)
+
+        then: "we should see the read_through_stop_codon"
+        CDS cds = transcriptService.getCDS(mrna)
+        assert cdsService.getStopCodonReadThrough(cds).size() == 1
+
+        when: "we get the CDS and peptide sequence of the exon 2 and exon 3"
+        def exons = transcriptService.getSortedExons(mrna, false)
+        String exon2CdsSequence = sequenceService.getSequenceForFeature(exons.get(1), FeatureStringEnum.TYPE_CDS.value)
+        String exon3CdsSequence = sequenceService.getSequenceForFeature(exons.get(2), FeatureStringEnum.TYPE_CDS.value)
+
+        String exon2PeptideSequence = sequenceService.getSequenceForFeature(exons.get(1), FeatureStringEnum.TYPE_PEPTIDE.value)
+        String exon3PeptideSequence = sequenceService.getSequenceForFeature(exons.get(2), FeatureStringEnum.TYPE_PEPTIDE.value)
+
+        then: "we should see the proper sequences"
+        assert exon2CdsSequence == knownExon2CdsSequence
+        assert exon3CdsSequence == knownExon3CdsSequence
+
+        assert exon2PeptideSequence == knownExon2PeptideSequence
+        assert exon3PeptideSequence == knownExon3PeptideSequence
+    }
+
+    void "when adding a transcript and then a sequence alteration in the intron, should not create non-canonical splice sites"() {
+
+        given: "two transcripts"
+        String postiveStrandedTranscript = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":1277947,\"fmax\":1278834,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40860-RA\",\"children\":[{\"location\":{\"fmin\":1277947,\"fmax\":1277953,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1278346,\"fmax\":1278499,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\ [...]
+        String postiveSequenceAlterationInsertion = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 1278631, \"fmax\": 1278631, \"strand\": 1 }, \"type\": {\"name\": \"insertion\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"ATCGATA\" } ], \"operation\": \"add_sequence_alteration\" }"
+        String setExonBoundaryCommand = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":1278294,\"fmax\":1278499}}],\"operation\":\"set_exon_boundaries\"}"
+        String setUpstreamSpliceAcceptorCommand = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"uniquename\": \"@EXON_UNIQUENAME@\" } ], \"operation\": \"set_to_upstream_acceptor\"}"
+
+//        String negativeStrandedTranscript = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":1279597,\"fmax\":1282168,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40718-RA\",\"children\":[{\"location\":{\"fmin\":1279597,\"fmax\":1279727,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1279801,\"fmax\":1280160,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequenc [...]
+        String negativeStrandedTranscript = "{${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":1365530,\"fmax\":1367665,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40713-RA\",\"children\":[{\"location\":{\"fmin\":1367417,\"fmax\":1367665,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1365530,\"fmax\":1366050,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\ [...]
+//        String negativeStrandedSequenceInsertion = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 1280277, \"fmax\": 1280277, \"strand\": 1 }, \"type\": {\"name\": \"insertion\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"GAATC\" } ], \"operation\": \"add_sequence_alteration\" }"
+        String negativeStrandedSequenceInsertion = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [ { \"location\": { \"fmin\": 1366134, \"fmax\": 1366134, \"strand\": 1 }, \"type\": {\"name\": \"insertion\", \"cv\": { \"name\":\"sequence\" } }, \"residues\": \"ATAGAC\" } ], \"operation\": \"add_sequence_alteration\" }"
+
+        when: "we add the positive transcript"
+        requestHandlingService.addTranscript(JSON.parse(postiveStrandedTranscript) as JSONObject)
+        List<Exon> exonList = transcriptService.getSortedExons(MRNA.first(), true)
+        String exonUniqueName = exonList.get(1).uniqueName
+        setExonBoundaryCommand = setExonBoundaryCommand.replace("@EXON_UNIQUENAME@", exonUniqueName)
+        setUpstreamSpliceAcceptorCommand = setUpstreamSpliceAcceptorCommand.replace("@EXON_UNIQUENAME@", exonUniqueName)
+
+        then: "we see the added transcript"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert SequenceAlteration.count == 0
+        assert Insertion.count == 0
+        assert MRNA.countByName("GB40860-RA-00001")
+
+        when: "we add a sequence alteration"
+        requestHandlingService.addSequenceAlteration(JSON.parse(postiveSequenceAlterationInsertion) as JSONObject)
+
+        then: "we should see the alteration, but no non-canonical splice sites"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert SequenceAlteration.count == 1
+        assert Insertion.count == 1
+        assert MRNA.countByName("GB40860-RA-00001") == 1
+
+        when: "we set the exon boundary"
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundaryCommand) as JSONObject)
+
+        then: "we should see a non-canonical splice site show up"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 1
+        assert SequenceAlteration.count == 1
+        assert Insertion.count == 1
+        assert MRNA.countByName("GB40860-RA-00001") == 1
+
+        when: "we set the upstream splice acceptor"
+        requestHandlingService.setAcceptor(JSON.parse(setUpstreamSpliceAcceptorCommand) as JSONObject, true)
+
+        then: "acceptor does not go to a canonical acceptor"
+        assert Gene.count == 1
+        assert CDS.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert SequenceAlteration.count == 1
+        assert Insertion.count == 1
+        assert MRNA.countByName("GB40860-RA-00001") == 1
+
+        when: "we add the negative transcript"
+        requestHandlingService.addTranscript(JSON.parse(negativeStrandedTranscript) as JSONObject)
+
+        then: "assert we have one"
+        assert MRNA.countByName("GB40713-RA-00001") == 1
+        assert Gene.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Exon.count == 3 + 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert SequenceAlteration.count == 1
+        assert Insertion.count == 1
+
+        when: "we add an overalapping negative sequence alteration"
+        requestHandlingService.addSequenceAlteration(JSON.parse(negativeStrandedSequenceInsertion) as JSONObject)
+
+        then: "we should only have the insertion exist"
+        assert MRNA.countByName("GB40713-RA-00001") == 1
+        assert Gene.count == 2
+        assert CDS.count == 2
+        assert MRNA.count == 2
+        assert Exon.count == 3 + 3
+        assert NonCanonicalFivePrimeSpliceSite.count == 0
+        assert NonCanonicalThreePrimeSpliceSite.count == 0
+        assert SequenceAlteration.count == 2
+        assert Insertion.count == 2
+    }
+
+    void "when a setTranslationStart action is performed on a transcript, there should be a check of overlapping isoforms"() {
+
+        given: "Two transcripts having separate parent gene"
+        String transcript1 = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":958639,\"fmax\":959315},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":953072,\"fmax\":953075},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":949590,\"fmax\":950737},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":94959 [...]
+        String setTranslationStartForTranscript1 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":959297}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_start\"}"
+        String transcript2 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":956828,\"fmax\":956837},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":958023,\"fmax\":958067},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":958639,\"fmax\":959315},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":958639 [...]
+        String setTranslationStartForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":959297}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_start\"}"
+
+        when: "we add transcript1"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript1) as JSONObject).get("features")
+
+        then: "we should have 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+        CDS initialCDS = transcriptService.getCDS(MRNA.findByUniqueName(transcript1UniqueName))
+        int initialCDSLength = initialCDS.featureLocation.fmax - initialCDS.featureLocation.fmin
+
+        when: "we set translation start for transcript1"
+        setTranslationStartForTranscript1 = setTranslationStartForTranscript1.replace("@UNIQUENAME@", transcript1UniqueName)
+        JSONObject setTranslationStartTranscript1ReturnObject = requestHandlingService.setTranslationStart(JSON.parse(setTranslationStartForTranscript1) as JSONObject).get("features")
+
+        then: "we should see a difference in CDS length for transcript1"
+        CDS alteredCDS = transcriptService.getCDS(MRNA.findByUniqueName(transcript1UniqueName))
+        int alteredCDSLength = alteredCDS.featureLocation.fmax - alteredCDS.featureLocation.fmin
+        assert initialCDSLength != alteredCDSLength
+
+        when: "we add transcript2"
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript2) as JSONObject).get("features")
+
+        then: "we should have 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        String transcript2Name = addTranscript2ReturnObject.name
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+        CDS initialCDS2 = transcriptService.getCDS(MRNA.findByUniqueName(transcript2UniqueName))
+        int initialCDSLength2 = initialCDS2.featureLocation.fmax - initialCDS2.featureLocation.fmin
+
+        when: "we set translation start for transcript2"
+        setTranslationStartForTranscript2 = setTranslationStartForTranscript2.replace("@UNIQUENAME@", transcript2UniqueName)
+        JSONObject setTranslationStartTranscript2ReturnObject = requestHandlingService.setTranslationStart(JSON.parse(setTranslationStartForTranscript2) as JSONObject).get("features")
+
+        then: "we should see a difference in CDS length for transcript2"
+        String transcript2ModifiedName = setTranslationStartTranscript2ReturnObject.name
+        CDS alteredCDS2 = transcriptService.getCDS(MRNA.findByUniqueName(transcript2UniqueName))
+        int alteredCDSLength2 = alteredCDS2.fmax - alteredCDS2.fmin
+        assert initialCDSLength2 != alteredCDSLength2
+
+        then: "isoform overlap check should have occured, thus transcript1 and transcript2 shares the same parent"
+        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName == transcript2Gene.uniqueName
+
+        then: "Name for transcript2 should have changed, in light of the new CDS overlap"
+        assert transcript2Name != transcript2ModifiedName
+
+        then: "the previous gene of transcript2 doesn't exist as it is deleted when it has no connected child features"
+        Gene.count == 1
+    }
+
+    void "when a setTranslationEnd action is performed on a transcript, there should be a check for overlapping isoforms"() {
+
+        given: "two transcripts having separate parent gene"
+        String transcript1 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":592678,\"fmax\":592731},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":593507,\"fmax\":594164},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":588729,\"fmax\":588910},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":592526,\"f [...]
+        String setTranslationStartForTranscript1 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":593550}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_start\"}"
+        String setTranslationEndForTranscript1 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmax\":593579}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_end\"}"
+
+        String transcript2 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":593507,\"fmax\":593733},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}],\"name\":\"au8.g307.t1\",\"location\":{\"strand\":1,\"fmin\":593507,\"fmax\":593733},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"}}],\"track\":\"Group1.10\",\"operation\":\"add_transcript\"}"
+        String setTranslationStartForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":593508}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_start\"}"
+        String setTranslationEndForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmax\":593579}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_end\"}"
+
+        when: "we add transcript1"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript1) as JSONObject).get("features")
+
+        then: "we should have 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+        CDS initialCDS = transcriptService.getCDS(MRNA.findByUniqueName(transcript1UniqueName))
+        int initialCDSLength = initialCDS.featureLocation.fmax - initialCDS.featureLocation.fmin
+
+        when: "we set translation start and end for transcript1"
+        setTranslationStartForTranscript1 = setTranslationStartForTranscript1.replace("@UNIQUENAME@", transcript1UniqueName)
+        setTranslationEndForTranscript1 = setTranslationEndForTranscript1.replace("@UNIQUENAME@", transcript1UniqueName)
+        requestHandlingService.setTranslationStart(JSON.parse(setTranslationStartForTranscript1) as JSONObject)
+        requestHandlingService.setTranslationEnd(JSON.parse(setTranslationEndForTranscript1) as JSONObject)
+
+        then: "we should see a difference in CDS length for transcript 1"
+        CDS alteredCDS = transcriptService.getCDS(MRNA.findByUniqueName(transcript1UniqueName))
+        int alteredCDSLength = alteredCDS.featureLocation.fmax - alteredCDS.featureLocation.fmin
+        assert initialCDSLength != alteredCDSLength
+
+        when: "we add transcript2"
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript2) as JSONObject).get("features")
+
+        then: "we should have 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        String transcript2Name = addTranscript2ReturnObject.name
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+        CDS transcript2InitialCDS = transcriptService.getCDS(MRNA.findByUniqueName(transcript2UniqueName))
+        int transcript2InitialCDSLength = transcript2InitialCDS.featureLocation.fmax - transcript2InitialCDS.featureLocation.fmin
+
+        when: "we set translation start and end for transcript2"
+        setTranslationStartForTranscript2 = setTranslationStartForTranscript2.replace("@UNIQUENAME@", transcript2UniqueName)
+        setTranslationEndForTranscript2 = setTranslationEndForTranscript2.replace("@UNIQUENAME@", transcript2UniqueName)
+        requestHandlingService.setTranslationStart(JSON.parse(setTranslationStartForTranscript2) as JSONObject)
+        JSONObject setTranslationEndTranscript2ReturnObject = requestHandlingService.setTranslationEnd(JSON.parse(setTranslationEndForTranscript2) as JSONObject).get("features")
+
+        then: "isoform overlap check should have occured, thus transcript1 and transcript2 now share the same parent gene"
+        String transcript2ModifiedName = setTranslationEndTranscript2ReturnObject.name
+        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName == transcript2Gene.uniqueName
+
+        then: "Name for transcript2 should have changed, in light of the new CDS overlap"
+        assert transcript2Name != transcript2ModifiedName
+
+        then: "the previous gene of transcript2 doesn't exist as it is deleted when it has no connected child features"
+        Gene.count == 1
+    }
+
+    void "when a setExonBoundaries action is performed on a transcript, there should be a check for overlapping isoforms"() {
+
+        given: "two transcripts that overlap but aren't isoforms of each other"
+        String transcript1 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":775816,\"fmax\":776061},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":772054,\"fmax\":772102},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":772808,\"fmax\":772942},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":773319,\"f [...]
+        String transcript2 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":775543,\"fmax\":776363},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}],\"name\":\"au8.g323.t1\",\"location\":{\"strand\":1,\"fmin\":775543,\"fmax\":776363},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"}}],\"track\":\"Group1.10\",\"operation\":\"add_transcript\"}"
+        String setExonBoundary1ForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":774048,\"fmax\":776363}}],\"track\":\"Group1.10\",\"operation\":\"set_exon_boundaries\"}"
+        String setExonBoundary2ForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":774787,\"fmax\":776363}}],\"track\":\"Group1.10\",\"operation\":\"set_exon_boundaries\"}"
+
+        when: "we add transcript1"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript1) as JSONObject).get("features")
+
+        then: "we should have 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        String transcript1Name = addTranscript1ReturnObject.name
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+
+        when: "we add transcript2"
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript2) as JSONObject).get("features")
+
+        then: "we should have 1 Gene and 2 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        String transcript2Name = addTranscript2ReturnObject.name
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+
+//        when: "we set exon boundary of transcript2"
+//        Exon exon = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript2UniqueName))[0]
+//        setExonBoundary1ForTranscript2 = setExonBoundary1ForTranscript2.replace("@UNIQUENAME@", exon.uniqueName)
+//        JSONObject setExonBoundary1ForTranscript2ReturnObject = requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundary1ForTranscript2) as JSONObject).get("features")
+//
+//        then: "transcript2 should now be an isoform of transcript1 indicated by transcript1 and transcript2 having the same parent gene"
+//        String modifiedTranscript2Name = setExonBoundary1ForTranscript2ReturnObject.name
+//        String modifiedTranscript2UniqueName = setExonBoundary1ForTranscript2ReturnObject.uniquename
+//        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+//        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(modifiedTranscript2UniqueName))
+//        assert transcript1Gene.uniqueName == transcript2Gene.uniqueName
+//
+//        then: "name for transcript2 changes to reflect its updated parent gene name as prefix"
+//        Boolean sameBaseName = false
+//        if (modifiedTranscript2Name.contains(transcript1Gene.name)) {sameBaseName = true}
+//        assert sameBaseName
+
+        when: "we set exon boundary of transcript2"
+        Exon exon = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript2UniqueName), true)[0]
+        setExonBoundary2ForTranscript2 = setExonBoundary2ForTranscript2.replace("@UNIQUENAME@", exon.uniqueName)
+        JSONObject setExonBoundary2ForTranscript2ReturnObject = requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundary2ForTranscript2) as JSONObject).get("features")
+        String modifiedTranscript2Name = setExonBoundary2ForTranscript2ReturnObject.name
+        String modifiedTranscript2UniqueName = setExonBoundary2ForTranscript2ReturnObject.uniquename
+
+        then: "transcript2 should NOT be an isoform of transcript1"
+        Gene updatedTranscript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene updatedTranscript2Gene = transcriptService.getGene(MRNA.findByUniqueName(modifiedTranscript2UniqueName))
+        assert updatedTranscript1Gene.uniqueName != updatedTranscript2Gene.uniqueName
+    }
+
+    void "when a flipStrand action is performed on a transcript, there should be a check for overlapping isoforms"() {
+
+        given: "A transcript"
+        String transcript = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":403882,\"fmax\":404044},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":405031,\"fmax\":405154},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":403882,\"fmax\":405154},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}],\"name\":\"GB40812-RA\",\"location\":{\"strand\" [...]
+        String flipStrandForTranscript = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"flip_strand\"}"
+
+        when: "we add the transcript"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript) as JSONObject).get("features")
+
+        then: "we have 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        String transcript1Name = addTranscript1ReturnObject.name
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+
+        when: "we move the transcript to oppposite strand"
+        String flipStrandForTranscript1 = flipStrandForTranscript.replace("@UNIQUENAME@", transcript1UniqueName)
+        JSONObject flipStrandForTranscript1ReturnObject = requestHandlingService.flipStrand(JSON.parse(flipStrandForTranscript1) as JSONObject).get("features")
+
+        then: "the transcript should be on the negative strand"
+        String flippedTranscript1UniqueName = flipStrandForTranscript1ReturnObject.uniquename
+        assert MRNA.findByUniqueName(flippedTranscript1UniqueName).strand == Strand.NEGATIVE.value
+
+        when: "we add the same transcript again"
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript) as JSONObject).get("features")
+
+        then: "we have 2 Genes and 2 MRNA"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        String transcript2Name = addTranscript2ReturnObject.name
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+
+        when: "we move transcript2 to the opposite strand"
+        String flipStrandForTranscript2 = flipStrandForTranscript.replace("@UNIQUENAME@", transcript2UniqueName)
+        JSONObject flipStrandForTranscript2ReturnObject = requestHandlingService.flipStrand(JSON.parse(flipStrandForTranscript2)).get("features")
+
+        then: "the transcript should be on the negative strand"
+        String flippedTranscript2UniqueName = flipStrandForTranscript2ReturnObject.uniquename
+        assert MRNA.findByUniqueName(flippedTranscript2UniqueName).strand == Strand.NEGATIVE.value
+
+        then: "transcript2 is now an isoform of transcript1"
+        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName == transcript2Gene.uniqueName
+    }
+
+    void "when a mergeExons action is performed on a transcript, there should be a check for overlapping isoforms"() {
+
+        given: "3 transcripts that overlap but aren't isoforms of each other"
+        String transcript = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":729928,\"fmax\":730010},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":730296,\"fmax\":730304},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":729928,\"fmax\":730304},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}],\"name\":\"GB40827-RA\",\"location\":{\"strand\" [...]
+        String mergeExons = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME1@\"},{\"uniquename\":\"@UNIQUENAME2@\"}],\"track\":\"Group1.10\",\"operation\":\"merge_exons\"}"
+
+        when: "we add transcript1"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript) as JSONObject).get("features")
+
+        then: "we should have 1 Gene and 1 MRNA"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 2
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+
+        when: "we merge exon 1 with exon2 of transcript1"
+        String exon1UniqueName = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript1UniqueName), true)[0].uniqueName
+        String exon2UniqueName = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript1UniqueName), true)[1].uniqueName
+        String mergeExonsForTranscript1 = mergeExons.replace("@UNIQUENAME1@", exon1UniqueName).replace("@UNIQUENAME2@", exon2UniqueName)
+        JSONObject mergeExonsForTranscript1ReturnObject = requestHandlingService.mergeExons(JSON.parse(mergeExonsForTranscript1) as JSONObject)
+
+        then: "we have a transcript that has only 1 exon"
+        assert transcriptService.getSortedExons(MRNA.findByUniqueName(transcript1UniqueName), true).size() == 1
+
+        when: "now we add transcript2"
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript) as JSONObject).get("features")
+
+        then: "we should have 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        assert Exon.count == 3
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+
+        then: "even though transcript2 is the same as transcript1 (before merge), they are not considered as isoforms of each other"
+        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName != transcript2Gene.uniqueName
+
+        when: "we merge exon 1 with exon2 of transcript2"
+        exon1UniqueName = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript2UniqueName), true)[0].uniqueName
+        exon2UniqueName = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript2UniqueName), true)[1].uniqueName
+        String mergeExonsForTranscript2 = mergeExons.replace("@UNIQUENAME1@", exon1UniqueName).replace("@UNIQUENAME2@", exon2UniqueName)
+        JSONObject mergeExonsForTranscript2ReturnObject = requestHandlingService.mergeExons(JSON.parse(mergeExonsForTranscript2) as JSONObject).get("features")
+
+        then: "transcript2 should be an isoform of transcript1"
+        Gene updatedTranscript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName == updatedTranscript2Gene.uniqueName
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert MRNA.count == 2
+    }
+
+    void "when a deleteFeature action is performed on a transcript, there should be a check for overlapping isoforms"() {
+
+        given: "two transcripts that are isoforms of each other"
+        String transcript1 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":787455,\"fmax\":787740},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":871534,\"fmax\":871600},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":871708,\"fmax\":871861},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":873893 [...]
+        String transcript2 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":845782,\"fmax\":845798},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":847144,\"fmax\":847278},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":845782,\"fmax\":847278},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}],\"name\":\"GB40739-RA\",\"location\":{\"stra [...]
+        String deleteExon = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"delete_feature\"}"
+
+        when: "we add transcript1 and transcript2"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript1) as JSONObject).get("features")
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript2) as JSONObject).get("features")
+
+        then: "we have 1 Gene and 2 MRNAs"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+
+        when: "we delete the first exon of transcript2"
+        Exon firstExonOfTranscript2 = transcriptService.getSortedExons(MRNA.findByUniqueName(transcript2UniqueName), true)[0]
+        String deleteExonOfTranscript2 = deleteExon.replace("@UNIQUENAME@", firstExonOfTranscript2.uniqueName)
+        JSONObject deleteExonOfTranscript2ReturnObject = requestHandlingService.deleteFeature(JSON.parse(deleteExonOfTranscript2) as JSONObject).get("features")
+
+        then: "transcript1 and transcript2 are no longer isoforms of each other"
+        Gene transcript1Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene transcript2Gene = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert transcript1Gene.uniqueName != transcript2Gene.uniqueName
+        assert Gene.count == 2
+        assert MRNA.count == 2
+    }
+
+    void "when two transcripts become isoforms of each others, the properties of their respective parent gene should be preserved in the merged gene"() {
+
+        given: "Two transcripts that overlap but are not isoforms of each other"
+        String transcript1 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":583280,\"fmax\":583605},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":577493,\"fmax\":577643},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":582506,\"fmax\":582677},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":583187,\"f [...]
+        String transcript2 = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":576138,\"fmax\":576168},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":582506,\"fmax\":582677},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":583187,\"fmax\":583605},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":576138,\"f [...]
+        String setTranslationStartForTranscript2 = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":582521}}],\"track\":\"Group1.10\",\"operation\":\"set_translation_start\"}"
+
+        //String setSymbolOperation = "{\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"symbol\":\"@SYMBOL_VALUE@\"}],\"track\":\"Group1.10\",\"operation\":\"set_symbol\",\"username\":\"demo at demo.com\"}"
+        //String setDescriptionOperation = "{\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"description\":\"@DESCRIPTION_VALUE@\"}],\"track\":\"Group1.10\",\"operation\":\"set_description\"}"
+        String addDbxrefOperation = "{${testCredentials} \"features\":[{\"dbxrefs\":[{\"accession\":\"@XREF_ACCESSION@\",\"db\":\"@XREF_DB@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"add_non_primary_dbxrefs\"}"
+        String addAttributeOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"non_reserved_properties\":[{\"tag\":\"@ATTRIBUTE_TAG@\",\"value\":\"@ATTRIBUTE_VALUE@\"}]}],\"track\":\"Group1.10\",\"operation\":\"add_non_reserved_properties\"}"
+        String addPublicationOperation = "{${testCredentials} \"features\":[{\"dbxrefs\":[{\"accession\":\"@PUBMED_ACCESSION@\",\"db\":\"PMID\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"add_non_primary_dbxrefs\"}"
+        String addGeneOntologyOperation = "{${testCredentials} \"features\":[{\"dbxrefs\":[{\"accession\":\"@GO_ACCESSION@\",\"db\":\"GO\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"add_non_primary_dbxrefs\"}"
+        String addCommentOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\",\"operation\":\"add_comments\"}"
+
+        when: "we add transcript1 and transcript2"
+        JSONObject addTranscript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript1) as JSONObject).get("features")
+        JSONObject addTranscript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcript2) as JSONObject).get("features")
+
+        then: "we should see 2 Genes and 2 MRNAs"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+        String transcript1UniqueName = addTranscript1ReturnObject.uniquename
+        String transcript2UniqueName = addTranscript2ReturnObject.uniquename
+        Gene initialGeneForTranscript1 = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene initialGeneForTranscript2 = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+
+        when: "we add properties to transcript1"
+        String addDbxref1ForTranscript1 = addDbxrefOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@XREF_DB@", "NCBI").replace("@XREF_ACCESSION@", "12937129")
+        String addDbxref2ForTranscript1 = addDbxrefOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@XREF_DB@", "Ensembl").replace("@XREF_ACCESSION@", "ENSG000000000213")
+        String addAttribute1ForTranscript1 = addAttributeOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@ATTRIBUTE_TAG@", "isPredicted").replace("@ATTRIBUTE_VALUE@", "true")
+        String addAttribute2ForTranscript1 = addAttributeOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@ATTRIBUTE_TAG@", "isProteinCoding").replace("@ATTRIBUTE_VALUE@", "true")
+        String addPublicationForTranscript1 = addPublicationOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@PUBMED_ACCESSION@", "8379243")
+        String addGeneOntologyForTranscript1 = addGeneOntologyOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@GO_ACCESSION@", "GO:1902009")
+        String addCommentForTranscript1 = addCommentOperation.replace("@UNIQUENAME@", initialGeneForTranscript1.uniqueName).replace("@COMMENT", "This gene is a test gene and created solely for the purpose of this test")
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addDbxref1ForTranscript1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addDbxref2ForTranscript1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addAttribute1ForTranscript1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addAttribute2ForTranscript1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addPublicationForTranscript1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addGeneOntologyForTranscript1) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForTranscript1) as JSONObject)
+
+        then: "we should have 3 FeatureProperty entities and 4 DBXref entities"
+        assert FeatureProperty.count == 3
+        assert DBXref.count == 4
+
+        when: "we add properties to transcript2"
+        String addDbxref1ForTranscript2 = addDbxrefOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@XREF_DB@", "NCBI").replace("@XREF_ACCESSION@", "83924623")
+        String addDbxref2ForTranscript2 = addDbxrefOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@XREF_DB@", "Ensembl").replace("@XREF_ACCESSION@", "ENSG000000000112")
+        String addAttribute1ForTranscript2 = addAttributeOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@ATTRIBUTE_TAG@", "isAnnotated").replace("@ATTRIBUTE_VALUE@", "false")
+        String addAttribute2ForTranscript2 = addAttributeOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@ATTRIBUTE_TAG@", "isApproved").replace("@ATTRIBUTE_VALUE@", "false")
+        String addPublicationForTranscript2 = addPublicationOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@PUBMED_ACCESSION@", "8923422")
+        String addGeneOntologyForTranscript2 = addGeneOntologyOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@GO_ACCESSION@", "GO:0009372")
+        String addCommentForTranscript2 = addCommentOperation.replace("@UNIQUENAME@", initialGeneForTranscript2.uniqueName).replace("@COMMENT", "This gene is a another test gene and created solely for the purpose of this test")
+
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addDbxref1ForTranscript2) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addDbxref2ForTranscript2) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addAttribute1ForTranscript2) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addAttribute2ForTranscript2) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addPublicationForTranscript2) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addGeneOntologyForTranscript2) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForTranscript2) as JSONObject)
+
+        then: "we should have 6 FeatureProperty entities and 8 DBXref entities"
+        assert FeatureProperty.count == 6
+        assert DBXref.count == 8
+        def xRefInitialGeneForTranscript1 = initialGeneForTranscript1.getFeatureDBXrefs()
+        def fpInitialGeneForTranscript1 = initialGeneForTranscript1.getFeatureProperties()
+        def xRefInitialGeneForTranscript2 = initialGeneForTranscript2.getFeatureDBXrefs()
+        def fpInitialGeneForTranscript2 = initialGeneForTranscript2.getFeatureProperties()
+        def combinedxRefs = (xRefInitialGeneForTranscript1 + xRefInitialGeneForTranscript2).sort()
+        def combinedFeatureProperties = (fpInitialGeneForTranscript1 + fpInitialGeneForTranscript2).sort()
+
+        when: "we set exon boundary of transcript2"
+        setTranslationStartForTranscript2 = setTranslationStartForTranscript2.replace("@UNIQUENAME@", transcript2UniqueName)
+        requestHandlingService.setTranslationStart(JSON.parse(setTranslationStartForTranscript2) as JSONObject)
+
+        then: "transcript1 and transcript2 should be isoforms of each other and they should have the same parent gene"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert FeatureProperty.count == 7
+        assert DBXref.count == 8
+        Gene finalGeneForTranscript1 = transcriptService.getGene(MRNA.findByUniqueName(transcript1UniqueName))
+        Gene finalGeneForTranscript2 = transcriptService.getGene(MRNA.findByUniqueName(transcript2UniqueName))
+        assert finalGeneForTranscript1.uniqueName == finalGeneForTranscript2.uniqueName
+
+        then: "all properties of the parent gene for transcript2, before setTranslationStart, should now be properties of current shared gene"
+        def xRefForMergedGene = finalGeneForTranscript1.getFeatureDBXrefs()
+        def fpForMergedGene = finalGeneForTranscript1.getFeatureProperties()
+        assert combinedxRefs == xRefForMergedGene.sort()
+        assert combinedFeatureProperties == fpForMergedGene.sort()
+    }
+
+    void "split and merge should work for transcripts belonging to same gene"() {
+        given: "GB40812-RA"
+        String transcriptString = "{${testCredentials} \"track\":\"Group1.10\",${testCredentials} \"features\":[{\"location\":{\"fmin\":403882,\"fmax\":405154,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40812-RA\",\"children\":[{\"location\":{\"fmin\":403882,\"fmax\":404044,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":405031,\"fmax\":405154,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\ [...]
+        String setExonBoundaryOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@EXON_UNIQUENAME@\",\"location\":{\"fmin\":@EXON_FMIN@,\"fmax\":@EXON_FMAX@}}],\"track\":\"Group1.10\",\"operation\":\"set_exon_boundaries\"}"
+        String splitTranscriptOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@EXON1_UNIQUENAME@\"},{\"uniquename\":\"@EXON2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"split_transcript\"}"
+        String mergeTranscriptOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@TRANSCRIPT1_UNIQUENAME@\"},{\"uniquename\":\"@TRANSCRIPT2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"merge_transcripts\"}"
+
+        when: "we add transcript GB40812-RA twice"
+        JSONObject transcript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+        JSONObject transcript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should see 1 gene with 2 transcripts"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+
+        when: "we split a transcript"
+        def mrnas = MRNA.all.sort { a, b -> a.name <=> b.name }
+        Transcript transcript1 = mrnas[0]
+        Transcript transcript2 = mrnas[1]
+        ArrayList<Exon> exonList = transcriptService.getSortedExons(transcript2, false)
+        String exon1UniqueName = exonList.get(0).uniqueName
+        String exon2UniqueName = exonList.get(1).uniqueName
+
+        String splitTranscript2String = splitTranscriptOperation.replace("@EXON1_UNIQUENAME@", exon1UniqueName).replace("@EXON2_UNIQUENAME@", exon2UniqueName)
+        JSONObject splitTranscript2ReturnObject = requestHandlingService.splitTranscript(JSON.parse(splitTranscript2String) as JSONObject)
+
+        then: "we should have 1 gene and 3 transcripts"
+        assert Gene.count == 1
+        assert Transcript.count == 3
+        assert Exon.count == 4
+        assert CDS.count == 3
+
+        when: "we merge the sub transcripts"
+        mrnas = MRNA.all.sort { a, b -> a.name <=> b.name }
+        transcript1 = mrnas[0]
+        transcript2 = mrnas[1]
+        Transcript transcript3 = mrnas[2]
+        String mergeTranscriptsString = mergeTranscriptOperation.replace("@TRANSCRIPT1_UNIQUENAME@", transcript2.uniqueName).replace("@TRANSCRIPT2_UNIQUENAME@", transcript3.uniqueName)
+        JSONObject mergeTranscriptReturnObject = requestHandlingService.mergeTranscripts(JSON.parse(mergeTranscriptsString) as JSONObject)
+
+        then: "we should have 1 gene, 1 transcript, 4 exons and 2 CDS"
+        assert Gene.count == 1
+        assert Transcript.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+    }
+
+    void "for a negative stranded feature, after a split, an undo followed by redo should give the same result as that of a split transcript"() {
+        given: "GB40745-RA"
+        String transcriptString = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":733182,\"fmax\":733316},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":731930,\"fmax\":732023},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":731930,\"fmax\":732539},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":7 [...]
+        String splitTranscriptOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@EXON1_UNIQUENAME@\"},{\"uniquename\":\"@EXON2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"split_transcript\"}"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+
+        when: "we add transcript GB40745-RA twice"
+        JSONObject transcript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+        JSONObject transcript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should see 1 gene with 2 transcripts"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+
+        when: "we split the transcript"
+        Transcript transcript1 = MRNA.all[0]
+        Transcript transcript2 = MRNA.all[1]
+        ArrayList<Exon> exonList = transcriptService.getSortedExons(transcript2, false)
+        String exon1UniqueName = exonList.get(0).uniqueName
+        String exon2UniqueName = exonList.get(1).uniqueName
+
+        String splitTranscriptString = splitTranscriptOperation.replace("@EXON1_UNIQUENAME@", exon1UniqueName).replace("@EXON2_UNIQUENAME@", exon2UniqueName)
+        JSONObject splitTranscriptReturnObject = requestHandlingService.splitTranscript(JSON.parse(splitTranscriptString) as JSONObject)
+
+        then: "we should have 1 gene and 3 transcripts"
+        assert Gene.count == 1
+        assert MRNA.count == 3
+        assert Exon.count == 4
+        assert CDS.count == 3
+
+        when: "we do an undo operation on the transcript"
+        String transcriptSplitUndoString = undoOperation.replace("@UNIQUENAME@", transcript2.uniqueName)
+        JSONObject transcriptSplitReturnObject = requestHandlingService.undo(JSON.parse(transcriptSplitUndoString) as JSONObject)
+
+        then: "we should have successfully undid the split transcript"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+
+        when: "we do a redo operation on the transcript"
+        String transcriptSplitRedoString = redoOperation.replace("@UNIQUENAME@", transcript2.uniqueName)
+        JSONObject transcriptSplitRedoReturnObject = requestHandlingService.redo(JSON.parse(transcriptSplitRedoString) as JSONObject)
+
+        then: "we should have the same outcome as that of split transcript"
+        assert Gene.count == 1
+        assert MRNA.count == 3
+        assert Exon.count == 4
+        assert CDS.count == 3
+
+        then: "and there will be no orphan transcripts left behind"
+
+        MRNA.all.each {
+            Gene gene = transcriptService.getGene(it)
+            assert gene != null
+        }
+    }
+
+    void "for a positive stranded feature, after a split, an undo followed by redo should give the same result as that of a split transcript"() {
+        given: "GB40822-RA"
+        String transcriptString = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":654601,\"fmax\":654649},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":657084,\"fmax\":657144},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":654601,\"fmax\":657144},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}],\"name\":\"GB40822-RA\",\"location\":{\"st [...]
+        String splitTranscriptOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@EXON1_UNIQUENAME@\"},{\"uniquename\":\"@EXON2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"split_transcript\"}"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+        String redoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"redo\"}"
+
+        when: "we add transcript GB40822-RA, twice"
+        JSONObject transcript1ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+        JSONObject transcript2ReturnObject = requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should see 1 gene with 2 transcripts"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+
+        when: "we split the transcript"
+        Transcript transcript1 = MRNA.all[0]
+        Transcript transcript2 = MRNA.all[1]
+        ArrayList<Exon> exonList = transcriptService.getSortedExons(transcript2, false)
+        String exon1UniqueName = exonList.get(0).uniqueName
+        String exon2UniqueName = exonList.get(1).uniqueName
+
+        String splitTranscriptString = splitTranscriptOperation.replace("@EXON1_UNIQUENAME@", exon1UniqueName).replace("@EXON2_UNIQUENAME@", exon2UniqueName)
+        JSONObject splitTranscriptReturnObject = requestHandlingService.splitTranscript(JSON.parse(splitTranscriptString) as JSONObject)
+
+        then: "we should have 1 gene and 3 transcripts"
+        assert Gene.count == 1
+        assert MRNA.count == 3
+        assert Exon.count == 4
+        assert CDS.count == 3
+
+        when: "we do an undo operation on the transcript"
+        String transcriptSplitUndoString = undoOperation.replace("@UNIQUENAME@", transcript2.uniqueName)
+        JSONObject transcriptSplitReturnObject = requestHandlingService.undo(JSON.parse(transcriptSplitUndoString) as JSONObject)
+
+        then: "we should have successfully undid the split transcript"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+        assert CDS.count == 2
+
+        when: "we do a redo operation on the transcript"
+        String transcriptSplitRedoString = redoOperation.replace("@UNIQUENAME@", transcript2.uniqueName)
+        JSONObject transcriptSplitRedoReturnObject = requestHandlingService.redo(JSON.parse(transcriptSplitRedoString) as JSONObject)
+
+        then: "we should have the same outcome as that of split transcript"
+        assert Gene.count == 1
+        assert MRNA.count == 3
+        assert Exon.count == 4
+        assert CDS.count == 3
+
+        then: "and there will be no orphan transcripts left behind"
+
+        MRNA.all.each {
+            Gene gene = transcriptService.getGene(it)
+            assert gene != null
+        }
+    }
+
+    void "when we add undo a merge transcript operation, isoform overlap rule should be applied consistently"() {
+        given: "Two transcripts: GB40769-RA and GB40770-RA"
+        String transcript1String = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":262511,\"fmax\":262558},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":258387,\"fmax\":258402},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":258387,\"fmax\":258492},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\": [...]
+        String transcript2String = "{${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":255124,\"fmax\":255153},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":252400,\"fmax\":252408},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":252004,\"fmax\":252303},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\": [...]
+        String mergeTranscriptOperation = " {${testCredentials} \"features\":[{\"uniquename\":\"@TRANSCRIPT1_UNIQUENAME@\"},{\"uniquename\":\"@TRANSCRIPT2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"merge_transcripts\"}"
+        String undoOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"count\":1,\"track\":\"Group1.10\",\"operation\":\"undo\"}"
+
+        when: "we add transcripts"
+        requestHandlingService.addTranscript(JSON.parse(transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript2String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript2String) as JSONObject)
+
+        then: "we should see 2 genes and 4 transcripts"
+        assert Gene.count == 2
+        assert MRNA.count == 4
+
+        Transcript transcript1 = MRNA.findByName("GB40769-RA-00001")
+        Transcript transcript2 = MRNA.findByName("GB40769-RA-00002")
+        Transcript transcript3 = MRNA.findByName("GB40770-RA-00001")
+        Transcript transcript4 = MRNA.findByName("GB40770-RA-00002")
+
+
+        when: "we merge transcript GB40769-RA-00002 with GB40770-RA-00002"
+        String mergeTranscriptString = mergeTranscriptOperation.replace("@TRANSCRIPT1_UNIQUENAME@", transcript4.uniqueName).replace("@TRANSCRIPT2_UNIQUENAME@", transcript2.uniqueName)
+        JSONObject mergeTranscriptReturnObject = requestHandlingService.mergeTranscripts(JSON.parse(mergeTranscriptString) as JSONObject)
+
+        then: "there should be 2 genes and 3 transcripts"
+        assert Gene.count == 2
+        assert MRNA.count == 3
+
+        def genes = Gene.all.sort { a, b -> a.name <=> b.name }
+        assert transcriptService.getTranscripts(genes[0]).size() == 2
+        assert transcriptService.getTranscripts(genes[1]).size() == 1
+
+        when: "we undo the operation"
+        String undoMergeTranscriptString = undoOperation.replace("@UNIQUENAME@", transcript2.uniqueName)
+        requestHandlingService.undo(JSON.parse(undoMergeTranscriptString) as JSONObject)
+
+        then: "we should have 2 genes and 4 transcripts"
+        assert Gene.count == 2
+        assert Transcript.count == 4
+        assert MRNA.count == 4
+        assert Exon.count == 22
+        assert CDS.count == 4
+
+        then: "each of the gene should have two transcripts"
+        assert transcriptService.getTranscripts(genes[0]).size() == 2
+        assert transcriptService.getTranscripts(genes[1]).size() == 2
+
+
+    }
+
+    void "when we merge two transcript where transcript1 is not the 5' most transcript, isoform overlap should be applied consistently"() {
+
+        given: "GB40857-RA and GB40858-RA"
+        String transcript1String = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1242150,\"strand\":1,\"fmax\":1247022},\"name\":\"GB40857-RA\",\"children\":[{\"location\":{\"fmin\":1246797,\"strand\":1,\"fmax\":1247022},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1242150,\"strand\":1,\"fmax\":1242169},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1242732,\"strand [...]
+        String transcript2String = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1248775,\"strand\":1,\"fmax\":1253496},\"name\":\"GB40858-RA\",\"children\":[{\"location\":{\"fmin\":1248775,\"strand\":1,\"fmax\":1248881},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1248959,\"strand\":1,\"fmax\":1249093},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1250262,\"strand [...]
+        String transcript3String = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1245018,\"strand\":1,\"fmax\":1245488},\"name\":\"GB40857-RA\",\"children\":[{\"location\":{\"fmin\":1245018,\"strand\":1,\"fmax\":1245488},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}}],\"type\":{\"name\":\"mRNA\",\"cv\":{\"name\":\"sequence\"}}}],\"track\":\"Group1.10\"}"
+        String mergeTranscriptOperation = "{${testCredentials} \"features\":[{\"uniquename\":\"@TRANSCRIPT1_UNIQUENAME@\"},{\"uniquename\":\"@TRANSCRIPT2_UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"merge_transcripts\"}"
+
+        when: "we add transcripts"
+        requestHandlingService.addTranscript(JSON.parse(transcript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript3String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript2String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(transcript2String) as JSONObject)
+
+        then: "we should have 4 transcripts, 2 genes"
+        assert Gene.count == 2
+        assert MRNA.count == 4
+        assert CDS.count == 4
+
+        Transcript transcript1 = MRNA.findByName("GB40857-RA-00001")
+        Transcript transcript2 = MRNA.findByName("GB40857-RA-00002")
+        Transcript transcript3 = MRNA.findByName("GB40858-RA-00001")
+        Transcript transcript4 = MRNA.findByName("GB40858-RA-00002")
+
+        when: "we merge GB40857-RA-00002 and GB40858-RA-00002"
+        String mergeTranscriptString = mergeTranscriptOperation.replace("@TRANSCRIPT1_UNIQUENAME@", transcript2.uniqueName).replace("@TRANSCRIPT2_UNIQUENAME@", transcript4.uniqueName)
+        JSONObject mergeTranscriptReturnObject = requestHandlingService.mergeTranscripts(JSON.parse(mergeTranscriptString) as JSONObject)
+
+        then: "there should be 2 genes and 3 transcripts"
+        assert Gene.count == 2
+        assert MRNA.count == 3
+
+        List<Gene> geneList = Gene.all.sort { a, b ->
+            a.fmin <=> b.fmin
+        }
+        assert transcriptService.getTranscripts(geneList.get(0)).size() == 1
+        assert transcriptService.getTranscripts(geneList.get(1)).size() == 2
+    }
+
+    void "when we change an annotation type from one form to another, we should see the proper changes and be able to go back in history"() {
+
+        given: "GB40821-RA"
+        String transcriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":621650,\"strand\":1,\"fmax\":628275},\"name\":\"GB40821-RA\",\"children\":[{\"location\":{\"fmin\":621650,\"strand\":1,\"fmax\":622270},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":628037,\"strand\":1,\"fmax\":628275},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":621650,\"strand\":1,\"f [...]
+        String changeAnnotationTypeOperationString = "{${testCredentials} \"operation\":\"change_annotation_type\",\"features\":[{\"type\":\"@TYPE@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String undoOperationString = "{${testCredentials} \"operation\":\"undo\",\"count\":1,\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String redoOperationString = "{${testCredentials} \"operation\":\"redo\",\"count\":1,\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should have 1 gene, 1 MRNA, 7 exons and 1 CDS"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 7
+        assert CDS.count == 1
+        MRNA mrna = MRNA.findByName("GB40821-RA-00001")
+        String featureUniqueName = mrna.uniqueName
+
+        when: "we change the annotation type from a protein coding Gene to a Pseudogene"
+        String changeAnnotationTypeForTranscriptString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "transcript")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeForTranscriptString) as JSONObject)
+
+        then: "we should have 1 gene of type Pseudogene, 1 Transcript and no CDS"
+        assert Pseudogene.count == 1
+        assert Transcript.count == 1
+        assert Exon.count == 7
+
+        assert MRNA.count == 0
+        assert CDS.count == 0
+
+        when: "we again change the annotation type from Pseudogene to ncRNA"
+        changeAnnotationTypeForTranscriptString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "ncRNA")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeForTranscriptString) as JSONObject)
+
+        then: "we should have 1 gene of type Gene, 1 ncRNA and no CDS"
+        assert Gene.count == 1
+        assert NcRNA.count == 1
+
+        assert Pseudogene.count == 0
+
+        when: "we undo twice and redo once"
+        String undoString = undoOperationString.replace("@UNIQUENAME@", featureUniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+
+        String redoString = redoOperationString.replace("@UNIQUENAME@", featureUniqueName)
+        requestHandlingService.redo(JSON.parse(redoString) as JSONObject)
+
+        then: "we should arrive at the state where the current feature is of type Pseudogene"
+        Feature feature = Feature.findByUniqueName(featureUniqueName)
+        assert feature instanceof Transcript
+        Gene gene = transcriptService.getGene((Transcript) feature)
+        assert gene instanceof Pseudogene
+
+        when: "we change the annotation type to transposable_element"
+        changeAnnotationTypeForTranscriptString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "transposable_element")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeForTranscriptString) as JSONObject)
+
+        then: "we should have 1 TransposableElement"
+        assert TransposableElement.count == 1
+        assert Gene.count == 0
+        assert Transcript.count == 0
+        assert Exon.count == 0
+        assert CDS.count == 0
+
+        when: "we change the annotation type to repeat_region"
+        changeAnnotationTypeForTranscriptString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "repeat_region")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeForTranscriptString) as JSONObject)
+
+        then: "we should have 1 RepeatRegion"
+        assert RepeatRegion.count == 1
+        assert TransposableElement.count == 0
+
+        when: "we undo thrice"
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+        then: "we should get back the original mRNA added at the very beginning"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 7
+        assert CDS.count == 1
+    }
+
+    void "When we change the annotation type of a feature, its attributes, dbxrefs and properties, these properties must be preserved throughout the process"() {
+
+        given: "GB40744-RA"
+        String transcriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"name\":\"GB40744-RA\",\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1 [...]
+        String changeAnnotationTypeOperationString = "{${testCredentials} \"operation\":\"change_annotation_type\",\"features\":[{\"type\":\"@TYPE@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String undoOperationString = "{${testCredentials} \"operation\":\"undo\",\"count\":1,\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String redoOperationString = "{${testCredentials} \"operation\":\"redo\",\"count\":1,\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String setSymbolString = "{${testCredentials} \"operation\":\"set_symbol\",\"features\":[{\"symbol\":\"@SYMBOL@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String setDescriptionString = "{${testCredentials} \"operation\":\"set_description\",\"features\":[{\"description\":\"@DESCRIPTION@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonPrimaryDbxrefString = "{${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"@DB@\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonReservedPropertyString = "{${testCredentials} \"operation\":\"add_non_reserved_properties\",\"features\":[{\"non_reserved_properties\":[{\"tag\":\"@TAG@\",\"value\":\"@VALUE@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addCommentString = "{${testCredentials} \"operation\":\"add_comments\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\"}"
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(transcriptString) as JSONObject)
+
+        then: "we should see 1 Gene, 1 MRNA, 6 exons and 1 CDS"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 6
+        assert CDS.count == 1
+        MRNA mrna = MRNA.findByName("GB40744-RA-00001")
+        String featureUniqueName = mrna.uniqueName
+        Gene gene = transcriptService.getGene(mrna)
+
+        when: "we add dbxrefs, attributes and comments to the added feature"
+        String setSymbolForGeneString = setSymbolString.replace("@UNIQUENAME@", gene.uniqueName).replace("@SYMBOL@", "TGN1")
+        String setDescriptionForGeneString = setDescriptionString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DESCRIPTION@", "TGN1 gene")
+        String addNonPrimaryDbxrefForGeneString1 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "9823742")
+        String addNonPrimaryDbxrefForGeneString2 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSG000000000012")
+        String addNonPrimaryDbxrefForGeneString3 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "GO").replace("@ACCESSION@", "0005872")
+        String addNonPrimaryDbxrefForGeneString4 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "2304723")
+        String addNonReservedPropertyForGeneString1 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "type").replace("@VALUE@", "Protein coding")
+        String addNonReservedPropertyForGeneString2 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "validated").replace("@VALUE@", "false")
+        String addCommentForGeneString = addCommentString.replace("@UNIQUENAME@", gene.uniqueName).replace("@COMMENT@", "This is a test gene")
+
+        String setSymbolForTranscriptString = setSymbolString.replace("@UNIQUENAME@", featureUniqueName).replace("@SYMBOL@", "TGN1-1A")
+        String setDescriptionForTranscriptString = setDescriptionString.replace("@UNIQUENAME@", featureUniqueName).replace("@DESCRIPTION@", "TGN1 isoform 1A")
+        String addNonPrimaryDbxrefForTranscriptString1 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", featureUniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "XM_73202812.1")
+        String addNonPrimaryDbxrefForTranscriptString2 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", featureUniqueName).replace("@DB@", "Ensembl").replace("@ACCESSION@", "ENSTAT00000005254")
+        String addNonPrimaryDbxrefForTranscriptString3 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", featureUniqueName).replace("@DB@", "GO").replace("@ACCESSION@", "0005872")
+        String addNonPrimaryDbxrefForTranscriptString4 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", featureUniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "2304723")
+        String addNonReservedPropertyForTranscriptString1 = addNonReservedPropertyString.replace("@UNIQUENAME@", featureUniqueName).replace("@TAG@", "type").replace("@VALUE@", "Protein coding transcript")
+        String addNonReservedPropertyForTranscriptString2 = addNonReservedPropertyString.replace("@UNIQUENAME@", featureUniqueName).replace("@TAG@", "validated").replace("@VALUE@", "false")
+        String addCommentForTranscriptString = addCommentString.replace("@UNIQUENAME@", featureUniqueName).replace("@COMMENT@", "This is a test isoform")
+
+        requestHandlingService.setSymbol(JSON.parse(setSymbolForGeneString) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(setDescriptionForGeneString) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGeneString1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGeneString2) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGeneString3) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGeneString4) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForGeneString1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForGeneString2) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForGeneString) as JSONObject)
+
+        requestHandlingService.setSymbol(JSON.parse(setSymbolForTranscriptString) as JSONObject)
+        requestHandlingService.setDescription(JSON.parse(setDescriptionForTranscriptString) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscriptString1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscriptString2) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscriptString3) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscriptString4) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForTranscriptString1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForTranscriptString2) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForTranscriptString) as JSONObject)
+
+        then: "we should see the added entries"
+        FeatureProperty.count > 0
+        DBXref.count > 0
+
+        when: "we change annotation type from MRNA to miRNA"
+        String changeAnnotationTypeString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "miRNA")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeString) as JSONObject)
+
+        then: "we should see all the previously added properties in the transformed miRNA"
+        Transcript transcript = Transcript.findByUniqueName(featureUniqueName)
+        Gene parentGene = transcriptService.getGene(transcript)
+        def expectedFeaturePropertiesForGene = ["type:Protein coding", "validated:false"]
+        def expectedDbxrefsForGene = ["NCBI:9823742", "Ensembl:ENSG000000000012", "PMID:2304723", "GO:0005872"]
+        def expectedFeaturePropertiesForTranscript = ["type:Protein coding transcript", "validated:false"]
+        def expectedDbxrefsForTranscript = ["NCBI:XM_73202812.1", "Ensembl:ENSTAT00000005254", "PMID:2304723", "GO:0005872"]
+
+        assert parentGene.symbol == "TGN1"
+        assert parentGene.description == "TGN1 gene"
+        parentGene.featureProperties.each { fp ->
+            if (fp instanceof Comment) {
+                assert fp.value == "This is a test gene"
+            } else {
+                String key = fp.tag + ":" + fp.value
+                assert expectedFeaturePropertiesForGene.indexOf(key) != -1
+                expectedFeaturePropertiesForGene.remove(key)
+            }
+        }
+        assert expectedFeaturePropertiesForGene.size() == 0
+
+        parentGene.featureDBXrefs.each { dbxref ->
+            String key = dbxref.db.name + ":" + dbxref.accession
+            assert expectedDbxrefsForGene.indexOf(key) != -1
+            expectedDbxrefsForGene.remove(key)
+        }
+        assert expectedDbxrefsForGene.size() == 0
+
+        assert transcript.symbol == "TGN1-1A"
+        assert transcript.description == "TGN1 isoform 1A"
+        transcript.featureProperties.each { fp ->
+            if (fp instanceof Comment) {
+                assert fp.value == "This is a test isoform"
+            } else {
+                String key = fp.tag + ":" + fp.value
+                assert expectedFeaturePropertiesForTranscript.indexOf(key) != -1
+                expectedFeaturePropertiesForTranscript.remove(key)
+            }
+        }
+        assert expectedFeaturePropertiesForTranscript.size() == 0
+
+        transcript.featureDBXrefs.each { dbxref ->
+            String key = dbxref.db.name + ":" + dbxref.accession
+            assert expectedDbxrefsForTranscript.indexOf(key) != -1
+            expectedDbxrefsForTranscript.remove(key)
+        }
+        assert expectedDbxrefsForTranscript.size() == 0
+
+        when: "we change the annotation type to a singleton feature such as repeat_region"
+        changeAnnotationTypeString = changeAnnotationTypeOperationString.replace("@UNIQUENAME@", featureUniqueName).replace("@TYPE@", "repeat_region")
+        requestHandlingService.changeAnnotationType(JSON.parse(changeAnnotationTypeString) as JSONObject)
+
+        then: "we should see all the properties of the gene transferred to the repeat_region"
+        Feature repeatRegionFeature = RepeatRegion.findByUniqueName(featureUniqueName)
+
+        def expectedFeaturePropertiesForRepeatRegion = ["type:Protein coding transcript", "validated:false"]
+        def expectedDbxrefsForRepeatRegion = ["NCBI:XM_73202812.1", "Ensembl:ENSTAT00000005254", "PMID:2304723", "GO:0005872"]
+
+        assert repeatRegionFeature.symbol == "TGN1-1A"
+        assert repeatRegionFeature.description == "TGN1 isoform 1A"
+        repeatRegionFeature.featureProperties.each { fp ->
+            if (fp instanceof Comment) {
+                assert fp.value == "This is a test isoform"
+            } else {
+                String key = fp.tag + ":" + fp.value
+                assert expectedFeaturePropertiesForRepeatRegion.indexOf(key) != -1
+                expectedFeaturePropertiesForRepeatRegion.remove(key)
+            }
+        }
+        assert expectedFeaturePropertiesForGene.size() == 0
+
+        repeatRegionFeature.featureDBXrefs.each { dbxref ->
+            String key = dbxref.db.name + ":" + dbxref.accession
+            assert expectedDbxrefsForRepeatRegion.indexOf(key) != -1
+            expectedDbxrefsForRepeatRegion.remove(key)
+        }
+        assert expectedDbxrefsForRepeatRegion.size() == 0
+
+        when: "we undo"
+        String undoString = undoOperationString.replace("@UNIQUENAME@", featureUniqueName)
+        requestHandlingService.undo(JSON.parse(undoString) as JSONObject)
+
+        then: "we should see the miRNA features with all its attributes"
+        Transcript newTranscript = Feature.findByUniqueName(featureUniqueName)
+        Gene newGene = transcriptService.getGene(newTranscript)
+        assert newGene != null
+        assert newTranscript.symbol != null
+        assert newTranscript.description != null
+        assert newTranscript.featureDBXrefs.size() == 4
+        assert newTranscript.featureProperties.size() == 3
+
+    }
+
+    void "when we add a coding transcript, its parent information (including metadata) should be used for creating a parent gene"() {
+        given: "GB40856-RA"
+        String addTranscriptString = "{ ${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1216824,\"strand\":1,\"fmax\":1235616},\"name\":\"GB40856-RA\",\"children\":[{\"location\":{\"fmin\":1235534,\"strand\":1,\"fmax\":1235616},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1216824,\"strand\":1,\"fmax\":1216850},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1224676,\"str [...]
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the transcript, its gene and gene metadata, as provided by the JSON"
+        MRNA mrna = MRNA.findByName("GB40856-RA-00001")
+        Gene gene = transcriptService.getGene(mrna)
+
+        assert gene.name == "GB40856-RA"
+        assert gene != null
+        assert gene.symbol == "TGN1"
+        assert gene.description == "TGN1 gene"
+
+        String comment = ""
+        gene.featureProperties.each {
+            if (it instanceof Comment) {
+                comment = it.value
+            }
+        }
+
+        assert comment == "this is a test gene"
+    }
+
+    void "when we add a non-coding transcript, its parent information (including metadata) should be used for creating a parent gene"() {
+        given: "a pseudogene"
+        String addFeatureString = "{ ${testCredentials} \"operation\":\"add_feature\",\"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":1282112,\"strand\":1,\"fmax\":1286907},\"name\":\"GB40861-RA\",\"symbol\":\"PSGN1\",\"description\":\"PSGN1 gene\",\"children\":[{\"location\":{\"fmin\":1282112,\"strand\":1,\"fmax\":1286907},\"name\":\"GB40861-RA-00001\",\"children\":[{\"location\":{\"fmin\":1283249,\"strand\":1,\"fmax\":1283301},\"name\":\"47d2eb21-2893-467c-b49b-7e8b22e177 [...]
+
+        when: "we add the pseudogene"
+        requestHandlingService.addFeature(JSON.parse(addFeatureString) as JSONObject)
+
+        then: "we should see the transcript, its gene and gene metadata, as provided by the JSON"
+        Transcript transcript = Transcript.all.get(0)
+        Gene gene = transcriptService.getGene(transcript)
+
+        assert gene.name == "GB40861-RA"
+        assert gene.symbol == "PSGN1"
+        assert gene.description == "PSGN1 gene"
+
+        String comment = ""
+        gene.featureProperties.each {
+            if (it instanceof Comment) {
+                comment = it.value
+            }
+        }
+
+        assert comment == "this is a test pseudogene"
+    }
+
+    void "a round-trip of GFF3 export and import"() {
+        given: "a fully annotated feature"
+        String addTranscriptString = "{${testCredentials} \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":1296556,\"strand\":1,\"fmax\":1303382},\"name\":\"GB40864-RA\",\"children\":[{\"location\":{\"fmin\":1296556,\"strand\":1,\"fmax\":1296570},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1296818,\"strand\":1,\"fmax\":1296928},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":1297013,\"stra [...]
+        String setSymbolString = "{${testCredentials} \"operation\":\"set_symbol\",\"features\":[{\"symbol\":\"@SYMBOL@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String setDescriptionString = "{${testCredentials} \"operation\":\"set_description\",\"features\":[{\"description\":\"@DESCRIPTION@\",\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonPrimaryDbxrefString = "{${testCredentials} \"operation\":\"add_non_primary_dbxrefs\",\"features\":[{\"dbxrefs\":[{\"db\":\"@DB@\",\"accession\":\"@ACCESSION@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addNonReservedPropertyString = "{${testCredentials} \"operation\":\"add_non_reserved_properties\",\"features\":[{\"non_reserved_properties\":[{\"tag\":\"@TAG@\",\"value\":\"@VALUE@\"}],\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        String addCommentString = "{${testCredentials} \"operation\":\"add_comments\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"comments\":[\"@COMMENT@\"]}],\"track\":\"Group1.10\"}"
+
+        when: "we add the annotation"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the gene and its mRNA"
+        Gene gene = Gene.all.get(0)
+        Transcript transcript = Transcript.all.get(0)
+        assert transcript != null
+
+        when: "we add functional information to the gene"
+        String setSymbolForGene = setSymbolString.replace("@UNIQUENAME@", gene.uniqueName).replace("@SYMBOL@", "PCG1")
+        requestHandlingService.setSymbol(JSON.parse(setSymbolForGene) as JSONObject)
+
+        String setDescriptionForGene = setDescriptionString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DESCRIPTION@", "PCG1 gene")
+        requestHandlingService.setDescription(JSON.parse(setDescriptionForGene) as JSONObject)
+
+        String addNonPrimaryDbxrefForGene1 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "98127312")
+        String addNonPrimaryDbxrefForGene2 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", gene.uniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "7304214")
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGene1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForGene2) as JSONObject)
+
+        String addNonReservedPropertyForGene1 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "2362.3466")
+        String addNonReservedPropertyForGene2 = addNonReservedPropertyString.replace("@UNIQUENAME@", gene.uniqueName).replace("@TAG@", "type").replace("@VALUE@", "protein coding gene")
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForGene1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForGene2) as JSONObject)
+
+        String addCommentForGene1 = addCommentString.replace("@UNIQUENAME@", gene.uniqueName).replace("@COMMENT@", "PCG1 is a protein coding gene")
+        String addCommentForGene2 = addCommentString.replace("@UNIQUENAME@", gene.uniqueName).replace("@COMMENT@", "PCG1 is a gene for test purposes")
+        requestHandlingService.addComments(JSON.parse(addCommentForGene1) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForGene2) as JSONObject)
+
+        then: "we should see the gene's properties"
+        assert gene.symbol != null
+        assert gene.description != null
+
+        when: "we add functional information to the mRNA"
+        String setSymbolForTranscript = setSymbolString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@SYMBOL@", "PCG1-2A")
+        requestHandlingService.setSymbol(JSON.parse(setSymbolForTranscript) as JSONObject)
+
+        String setDescriptionForTranscript = setDescriptionString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DESCRIPTION@", "PCG1 isoform 2A")
+        requestHandlingService.setDescription(JSON.parse(setDescriptionForTranscript) as JSONObject)
+
+        String addNonPrimaryDbxrefForTranscript1 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "NCBI").replace("@ACCESSION@", "XM_1239124.2")
+        String addNonPrimaryDbxrefForTranscript2 = addNonPrimaryDbxrefString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@DB@", "PMID").replace("@ACCESSION@", "8723042")
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscript1) as JSONObject)
+        requestHandlingService.addNonPrimaryDbxrefs(JSON.parse(addNonPrimaryDbxrefForTranscript2) as JSONObject)
+
+        String addNonReservedPropertyForTranscript1 = addNonReservedPropertyString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@TAG@", "score").replace("@VALUE@", "1234.8532")
+        String addNonReservedPropertyForTranscript2 = addNonReservedPropertyString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@TAG@", "type").replace("@VALUE@", "protein coding isoform")
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForTranscript1) as JSONObject)
+        requestHandlingService.addNonReservedProperties(JSON.parse(addNonReservedPropertyForTranscript2) as JSONObject)
+
+        String addCommentForTranscript1 = addCommentString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@COMMENT@", "PCG1-2A is a protein coding isoform")
+        String addCommentForTranscript2 = addCommentString.replace("@UNIQUENAME@", transcript.uniqueName).replace("@COMMENT@", "PCG1-2A is an isoform for test purposes")
+        requestHandlingService.addComments(JSON.parse(addCommentForTranscript1) as JSONObject)
+        requestHandlingService.addComments(JSON.parse(addCommentForTranscript2) as JSONObject)
+
+        then: "we should see the transcript's properties"
+        assert transcript.symbol != null
+        assert transcript.description != null
+
+        when: "we convert the gene to JSON"
+        JSONObject geneJsonObject = featureService.convertFeatureToJSON(gene)
+
+        then: "we should be able to validate the JSON representation of the gene"
+        assert geneJsonObject != null
+        assert gene.symbol == "PCG1"
+        assert gene.description == "PCG1 gene"
+        JSONArray geneDbxrefs = geneJsonObject.getJSONArray(FeatureStringEnum.DBXREFS.value)
+        JSONArray properties = geneJsonObject.getJSONArray(FeatureStringEnum.PROPERTIES.value)
+        JSONArray children = geneJsonObject.getJSONArray(FeatureStringEnum.CHILDREN.value)
+
+        def seenDbxrefs = []
+        def seenProps = []
+
+        for (JSONObject dbxref : geneDbxrefs) {
+            if (dbxref.getJSONObject(FeatureStringEnum.DB.value).name == "PMID") {
+                assert dbxref.accession == "7304214"
+                seenDbxrefs.add("PMID:" + dbxref.accession)
+            } else if (dbxref.getJSONObject(FeatureStringEnum.DB.value).name == "NCBI") {
+                assert dbxref.accession == "98127312"
+                seenDbxrefs.add("NCBI:" + dbxref.accession)
+            }
+        }
+        assert seenDbxrefs.size() == 2
+
+        for (JSONObject prop : properties) {
+            if (prop.name == "score") {
+                assert prop.value == "2362.3466"
+                seenProps.add("score:" + prop.value)
+            } else if (prop.name == "type") {
+                assert prop.value == "protein coding gene"
+                seenProps.add("type:" + prop.value)
+            } else if (prop.name == "comment") {
+                if (prop.value.contains('test')) {
+                    assert prop.value == "PCG1 is a gene for test purposes"
+                    seenProps.add("comment:" + prop.value)
+                } else {
+                    assert prop.value == "PCG1 is a protein coding gene"
+                    seenProps.add("comment:" + prop.value)
+                }
+            }
+        }
+        assert seenProps.size() == 4
+
+        seenDbxrefs.clear()
+        seenProps.clear()
+
+        for (JSONObject child : children) {
+            JSONArray transcriptDbxrefs = child.getJSONArray(FeatureStringEnum.DBXREFS.value)
+            JSONArray transcriptProperties = child.getJSONArray(FeatureStringEnum.PROPERTIES.value)
+
+            for (JSONObject dbxref : transcriptDbxrefs) {
+                if (dbxref.getJSONObject(FeatureStringEnum.DB.value).name == "PMID") {
+                    assert dbxref.accession == "8723042"
+                    seenDbxrefs.add("PMID:" + dbxref.accession)
+                } else if (dbxref.getJSONObject(FeatureStringEnum.DB.value).name == "NCBI") {
+                    assert dbxref.accession == "XM_1239124.2"
+                    seenDbxrefs.add("NCBI:" + dbxref.accession)
+                }
+            }
+            assert seenDbxrefs.size() == 2
+
+            for (JSONObject prop : transcriptProperties) {
+                if (prop.name == "score") {
+                    assert prop.value == "1234.8532"
+                    seenProps.add("score:" + prop.value)
+                } else if (prop.name == "type") {
+                    assert prop.value == "protein coding isoform"
+                    seenProps.add("type:" + prop.value)
+                } else if (prop.name == "comment") {
+                    if (prop.value.contains('test')) {
+                        assert prop.value == "PCG1-2A is an isoform for test purposes"
+                        seenProps.add("comment:" + prop.value)
+                    } else {
+                        assert prop.value == "PCG1-2A is a protein coding isoform"
+                        seenProps.add("comment:" + prop.value)
+                    }
+                }
+            }
+            assert seenProps.size() == 4
+        }
+
+        when: "we do a GFF3 export"
+        File tempFile = File.createTempFile("output", ".gff3")
+        String filePath = "${tempFile.absolutePath}"
+        tempFile.deleteOnExit()
+        def expectedDbxrefForGene = ["PMID:7304214", "NCBI:98127312"]
+        def expectedPropertiesForGene = ["score=2362.3466", "type=protein coding gene"]
+        def expectedNoteForGene = ["PCG1 is a protein coding gene", "PCG1 is a gene for test purposes"]
+
+        def expectedDbxrefForTranscript = ["PMID:8723042", "NCBI:XM_1239124.2"]
+        def expectedPropertiesForTranscript = ["score=1234.8532", "type=protein coding isoform"]
+        def expectedNoteForTranscript = ["PCG1-2A is a protein coding isoform", "PCG1-2A is an isoform for test purposes"]
+
+        def featuresToWrite = [gene]
+        gff3HandlerService.writeFeaturesToText(tempFile.absolutePath, featuresToWrite, ".")
+        String tempFileText = tempFile.text
+
+        then: "we should be able to validate the GFF3"
+        tempFileText.split("\n").each { line ->
+            if (!line.startsWith("#")) {
+                def gff3Record = line.split("\t")
+                String type = gff3Record[2]
+                def gffAttributes = gff3Record[8].split(";")
+                if (type == "gene") {
+                    gffAttributes.each { attribute ->
+                        def (attributeKey, attributeValue) = attribute.split("=")
+                        def valueList = attributeValue.split(",")
+                        if (attributeKey == "Dbxref") {
+                            valueList.each {
+                                expectedDbxrefForGene.remove(it)
+                            }
+                            assert expectedDbxrefForGene.size() == 0
+                        } else if (attributeKey == "Note") {
+                            valueList.each {
+                                expectedNoteForGene.remove(it)
+                            }
+                            assert expectedNoteForGene.size() == 0
+                        } else {
+                            assert attributeKey != "comment"
+                            if (expectedPropertiesForGene.contains(attributeKey + "=" + attributeValue)) {
+                                expectedPropertiesForGene.remove(attributeKey + "=" + attributeValue)
+                            }
+                        }
+                    }
+                    assert expectedPropertiesForGene.size() == 0
+                } else if (type == "mRNA") {
+                    gffAttributes.each { attribute ->
+                        def (attributeKey, attributeValue) = attribute.split("=")
+                        def valueList = attributeValue.split(",")
+                        if (attributeKey == "Dbxref") {
+                            valueList.each {
+                                expectedDbxrefForTranscript.remove(it)
+                            }
+                            assert expectedDbxrefForTranscript.size() == 0
+                        } else if (attributeKey == "Note") {
+                            valueList.each {
+                                expectedNoteForTranscript.remove(it)
+                            }
+                            assert expectedNoteForTranscript.size() == 0
+                        } else {
+                            assert attributeKey != "comment"
+                            if (expectedPropertiesForTranscript.contains(attributeKey + "=" + attributeValue)) {
+                                expectedPropertiesForTranscript.remove(attributeKey + "=" + attributeValue)
+                            }
+                        }
+                    }
+                    assert expectedPropertiesForTranscript.size() == 0
+                }
+            }
+        }
+
+        when: "we import the same annotation via GFF3"
+        def process = "tools/data/add_features_from_gff3_to_annotations.pl --url http://localhost:8080/apollo --username test at test.com --password testPass --input ${filePath} --organism Amel --test".execute()
+        then: "we should get a JSON representation of the feature from GFF3"
+        JSONArray outputJsonArray = JSON.parse(process.text) as JSONArray
+        String preparedJsonString = "{${testCredentials} \"operation\":\"add_transcript\", \"features\":${outputJsonArray.getJSONObject(0).getJSONArray("addTranscript").toString()}, \"track\":\"Group1.10\" }"
+
+        when: "we add this feature via addTranscript"
+        JSONObject addTranscriptJsonObject = JSON.parse(preparedJsonString) as JSONObject
+        requestHandlingService.addTranscript(addTranscriptJsonObject)
+
+        then: "we should see a new mRNA and it should have its metadata preserved"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+
+        Gene updatedGene = Gene.all.get(0)
+        MRNA mrna = MRNA.findByName("GB40864-RA-00002")
+        assert mrna.symbol == "PCG1-2A"
+        assert mrna.description == "PCG1 isoform 2A"
+        assert mrna.featureDBXrefs.size() == 2
+        assert mrna.featureProperties.size() == 4
+    }
+
+    void "a round-trip with all feature types"() {
+        given: "a set of features"
+        String addTranscript1String = " { ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":524298,\"fmax\":525125},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":526023,\"fmax\":526249},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":532689,\"fmax\":532805},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\" [...]
+        String addTranscript2String = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":577493,\"fmax\":577643},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":582506,\"fmax\":582677},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":583187,\"fmax\":583605},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\": [...]
+        String addTranscript3String = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":588729,\"fmax\":588910},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":592526,\"fmax\":592731},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":593507,\"fmax\":594164},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\": [...]
+        String setReadthroughStopCodonString = "{ ${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"readthrough_stop_codon\":true}],\"track\":\"Group1.10\",\"operation\":\"set_readthrough_stop_codon\"}"
+        String addInsertionString = "{ ${testCredentials} \"features\":[{\"residues\":\"TTACTAGTG\",\"location\":{\"strand\":1,\"fmin\":592775,\"fmax\":592775},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addDeletionString = "{ ${testCredentials} \"features\":[{\"location\":{\"strand\":1,\"fmin\":592699,\"fmax\":592714},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"deletion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSubstitutionString = "{ ${testCredentials} \"features\":[{\"residues\":\"TTG\",\"location\":{\"strand\":1,\"fmin\":593549,\"fmax\":593552},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"substitution\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+
+        String addPseudogeneString = " { ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":621650,\"fmax\":622330},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":623090,\"fmax\":623213},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":624547,\"fmax\":624610},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"stran [...]
+        String addTRNAString = "{ ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":664578,\"fmax\":664637},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":665671,\"fmax\":665933},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":666034,\"fmax\":666168},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\": [...]
+        String addSnRNAString = " { ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":704749,\"fmax\":704959},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":705192,\"fmax\":705437},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":705759,\"fmax\":706067},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1 [...]
+        String addSnoRNAString = "{ ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":709038,\"fmax\":709266},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":709337,\"fmax\":709627},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":709706,\"fmax\":709814},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\ [...]
+        String addNcRNAString = "{ ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":719298,\"fmax\":719709},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}],\"name\":\"au12.g310.t1\",\"location\":{\"strand\":1,\"fmin\":719298,\"fmax\":719709},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"ncRNA\"}}],\"location\":{\"strand\":1,\"fmin\":719298,\"fmax\":719709},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"gene\"}}], [...]
+        String addRRNAString = "{ ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":725218,\"fmax\":725849},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}],\"name\":\"au12.g312.t1\",\"location\":{\"strand\":-1,\"fmin\":725218,\"fmax\":725849},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"rRNA\"}}],\"location\":{\"strand\":-1,\"fmin\":725218,\"fmax\":725849},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"gene\"}}] [...]
+        String addMiRNAString = "{ ${testCredentials} \"features\":[{\"children\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":731922,\"fmax\":732539},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":732909,\"fmax\":733259},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":732023,\"fmax\":733182},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"CDS\"}}],\"name\":\"au12.g313.t1\" [...]
+        String addRepeatRegionString = "{${testCredentials} \"features\":[{\"name\":\"gnomon_1494033_mRNA\",\"location\":{\"strand\":0,\"fmin\":734606,\"fmax\":735570},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"repeat_region\"}}],\"track\":\"Group1.10\",\"operation\":\"add_feature\"}"
+        String addTransposableElementString = "{ ${testCredentials} \"features\":[{\"name\":\"gnomon_1984033_mRNA\",\"location\":{\"strand\":0,\"fmin\":729894,\"fmax\":730446},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"transposable_element\"}}],\"track\":\"Group1.10\",\"operation\":\"add_feature\"}"
+
+        when: "we add all the features"
+        requestHandlingService.addTranscript(JSON.parse(addTranscript1String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(addTranscript2String) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(addTranscript3String) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addPseudogeneString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addTRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addSnRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addSnoRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addNcRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addRRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addMiRNAString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addRepeatRegionString) as JSONObject)
+        requestHandlingService.addFeature(JSON.parse(addTransposableElementString) as JSONObject)
+
+        then: "we should see these features"
+        assert Gene.count == 10
+        assert Transcript.count == 10
+        assert RepeatRegion.count == 1
+        assert TransposableElement.count == 1
+
+        when: "we add some modifications"
+        MRNA mrna = MRNA.findByName("GB40819-RA-00001")
+        requestHandlingService.setReadthroughStopCodon(JSON.parse(setReadthroughStopCodonString.replace("@UNIQUENAME@", mrna.uniqueName)) as JSONObject)
+        CDS cds = transcriptService.getCDS(mrna)
+        StopCodonReadThrough scrt = cdsService.getStopCodonReadThrough(cds).first()
+        assert scrt != null
+
+        requestHandlingService.addSequenceAlteration(JSON.parse(addInsertionString) as JSONObject)
+        requestHandlingService.addSequenceAlteration(JSON.parse(addDeletionString) as JSONObject)
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSubstitutionString) as JSONObject)
+
+        then: "we see these modifications"
+        assert StopCodonReadThrough.count == 1
+        assert SequenceAlteration.count == 3
+
+        when: "do a GFF3 export"
+        Organism organism = Gene.all.get(0).featureLocation.sequence.organism
+        Sequence sequence = Gene.all.get(0).featureLocation.sequence
+        File tempFile = File.createTempFile("round-trip-output", ".gff3")
+        String filePath = tempFile.absolutePath
+        tempFile.deleteOnExit()
+
+        def featuresToWrite = []
+        Feature.all.each {
+            if (it.cvTerm in [Gene.cvTerm, Pseudogene.cvTerm, TransposableElement.cvTerm, RepeatRegion.cvTerm, Insertion.cvTerm, Deletion.cvTerm, Substitution.cvTerm]) {
+                featuresToWrite.add(it)
+            }
+        }
+        gff3HandlerService.writeFeaturesToText(tempFile.absolutePath, featuresToWrite, ".")
+
+        then: "we grab the contents of the GFF3"
+        String tempFileText = tempFile.text
+
+        assert tempFileText != ""
+
+        when: "we delete all features"
+        Feature.all.each {
+            if (it.cvTerm in [Gene.cvTerm, Pseudogene.cvTerm, TransposableElement.cvTerm, RepeatRegion.cvTerm, Insertion.cvTerm, Deletion.cvTerm, Substitution.cvTerm]) {
+                featureRelationshipService.deleteFeatureAndChildren(it)
+            }
+        }
+
+        then: "we see no features"
+        assert Feature.count == 0
+
+        when: "we import this GFF3"
+        def process = "tools/data/add_features_from_gff3_to_annotations.pl --url http://localhost:8080/apollo --username test at test.com --password testPass --input ${filePath} --organism Amel --test a -X -x".execute()
+
+        then: "we should get a JSON representation of the features"
+        JSONArray outputJsonArray = JSON.parse(process.text) as JSONArray
+        for (int i = 0; i < outputJsonArray.size(); i++) {
+            if (outputJsonArray.getJSONObject(i).has("addFeature")) {
+                String inputString = "{${testCredentials} \"track\": \"${sequence.name}\", \"operation\": \"add_feature\", \"features\": " + outputJsonArray.getJSONObject(i).getJSONArray("addFeature").toString() + "}"
+                requestHandlingService.addFeature(JSON.parse(inputString) as JSONObject)
+            } else if (outputJsonArray.getJSONObject(i).has("addTranscript")) {
+                String inputString = "{${testCredentials} \"track\": \"${sequence.name}\", \"operation\": \"add_transcript\", \"features\": " + outputJsonArray.getJSONObject(i).getJSONArray("addTranscript").toString() + "}"
+                requestHandlingService.addTranscript(JSON.parse(inputString) as JSONObject)
+            } else if (outputJsonArray.getJSONObject(i).has("addSequenceAlteration")) {
+                String inputString = "{${testCredentials} \"track\": \"${sequence.name}\", \"operation\": \"add_sequence_alteration\", \"features\": " + outputJsonArray.getJSONObject(i).getJSONArray("addSequenceAlteration").toString() + "}"
+                requestHandlingService.addSequenceAlteration(JSON.parse(inputString) as JSONObject)
+            }
+        }
+
+        then: "we restore all the features from GFF3"
+        assert Gene.count == 10
+        assert Transcript.count == 10
+        assert SequenceAlteration.count == 3
+        assert RepeatRegion.count == 1
+        assert TransposableElement.count == 1
+        assert StopCodonReadThrough.count == 1
+    }
+
+    void "while adding a transcript, Apollo should not recalculate its CDS if the JSONObject has the proper flag"() {
+        given: "a transcript whose CDS is incorrect"
+        String addTranscriptString = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":577493,\"fmax\":577643},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":582506,\"fmax\":582677},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":583187,\"fmax\":583605},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":5 [...]
+        String addSequenceAlterationString = "{ ${testCredentials} \"features\":[{\"residues\":\"TGA\",\"location\":{\"strand\":1,\"fmin\":582665,\"fmax\":582665},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String cdsSequenceString = "ATGACAGGTCATATCTGGTTCTGCTTCTTATTGTTCTTAACGGGCTACTGTGATTGCTCACTAACGGGGACAATATCATCGTTCCCGATAAACCTCGAGAACAATCCTTACTGTCGTATTTGCCCCAATCACACCATGTGCCGATTTCCGCTCGATACTGATGGCATTAGATGTATAAACCTCCAGCATGCTGATCTGGACGACAAAGATATTGAAACTATACTCCATTGGCACAATACTTATCGCAATACTGTGGCAAGTGGAAAAGAAATACGAGGAAATCCAGGTCCACAACGTCCAGCAAAATTTATGATGGAAGTG"
+        String recalculatedCdsSequenceString = "ATGACGAACTTGCTCTTATCGCGAGACGATGGGTGGTACAGTGCAACCTTCTCGAGAAAGATCAGTGCAGAGATGTTGGCAAGTAACTCCCACGGATACATATCATTTTTCATTTCCCTTAGTCGTCCCTATCATTTTTCAATATCCATCGCGTATACGTTCGAACACGAAATTGTTCCTCCTGTAACAGGATCAATGTGTCTGTGTGTTCTCCTCTTTTTATCGCAATTTCTCTCCTTCTTGTTTACGAATACCACTCTCTTCCAACCTATCTTCTTCCTTTTCCATCCCCTTATAACTCGTTTTGGCTTCGTTTCAATTTGCAATCGTAAGCCGCAAAAACGATATTGGCGCTTGTAA"
+
+        when: "we add the transcript"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see the transcript"
+        assert Gene.all.size() == 1
+        assert MRNA.all.size() == 1
+        MRNA mrna = MRNA.all.iterator().next()
+
+        then: "the CDS should have the original fmin and fmax"
+        CDS cds = transcriptService.getCDS(mrna)
+        assert cds.fmin == 577493
+        assert cds.fmax == 582677
+        String cdsSequence = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        assert cdsSequence == cdsSequenceString
+
+        when: "we add an insertion that has an in-frame stop codon w.r.t. the current transcript"
+        requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlterationString) as JSONObject)
+
+        then: "we should see the insertion"
+        assert Insertion.all.size() == 1
+
+        then: "previously added transcript's CDS should recalculate itself"
+        cds.attach()
+        assert cds.fmin == 583194
+        assert cds.fmax == 583554
+        String updatedCdsSequence = sequenceService.getSequenceForFeature(mrna, FeatureStringEnum.TYPE_CDS.value)
+        assert updatedCdsSequence == recalculatedCdsSequenceString
+
+        when: "we add the same transcript again"
+        JSONObject returnObject = requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+        String transcript2UniqueName = returnObject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "due to the presence of an overlapping sequence alteration, the transcript's CDS should be recalculated automatically"
+        MRNA mrna2 = MRNA.findByUniqueName(transcript2UniqueName)
+        CDS mrna2Cds = transcriptService.getCDS(mrna2)
+        assert mrna2Cds.fmin == 583194
+        assert mrna2Cds.fmax == 583554
+        String mrna2CdsSequence = sequenceService.getSequenceForFeature(mrna2, FeatureStringEnum.TYPE_CDS.value)
+        assert mrna2CdsSequence == recalculatedCdsSequenceString
+    }
+
+    void "while adding an overlapping sequence alteration to a transcript, Apollo should take pre-existing read_through_stop_codon into consideration while recalculating ORF"() {
+        given: "transcripts and sequence alterations"
+        String addTranscript1String = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":974327,\"fmax\":974467},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":974616,\"fmax\":975772},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":975852,\"fmax\":976988},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\": [...]
+        String addTranscript2String = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":-1,\"fmin\":982839,\"fmax\":984089},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":984503,\"fmax\":984707},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmin\":985678,\"fmax\":985915},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":-1,\"fmi [...]
+        String addSequenceAlteration1String = "{ ${testCredentials} \"features\":[{\"residues\":\"TGA\",\"location\":{\"strand\":1,\"fmin\":975949,\"fmax\":975949},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"insertion\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String addSequenceAlteration2String = "{ ${testCredentials} \"features\":[{\"residues\":\"TTT\",\"location\":{\"strand\":1,\"fmin\":983963,\"fmax\":983966},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"substitution\"}}],\"track\":\"Group1.10\",\"operation\":\"add_sequence_alteration\"}"
+        String setReadThroughStopCodonString = " { ${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"readthrough_stop_codon\":true}],\"track\":\"Group1.10\",\"operation\":\"set_readthrough_stop_codon\"}"
+        String deleteSequenceAlterationString = "{ ${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"delete_sequence_alteration\"}"
+
+        when: "we add the transcripts"
+        JSONObject addTranscript1ReturnOject = requestHandlingService.addTranscript(JSON.parse(addTranscript1String) as JSONObject)
+        JSONObject addTranscript2ReturnOject = requestHandlingService.addTranscript(JSON.parse(addTranscript2String) as JSONObject)
+
+        String transcript1UniqueName = addTranscript1ReturnOject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+        String transcript2UniqueName = addTranscript2ReturnOject.getJSONArray(FeatureStringEnum.FEATURES.value).getJSONObject(0).get(FeatureStringEnum.UNIQUENAME.value)
+
+        then: "we see the transcripts"
+        assert Transcript.count == 2
+
+        when: "we set the read through stop codon"
+        requestHandlingService.setReadthroughStopCodon(JSON.parse(setReadThroughStopCodonString.replace("@UNIQUENAME@", transcript1UniqueName)) as JSONObject)
+        requestHandlingService.setReadthroughStopCodon(JSON.parse(setReadThroughStopCodonString.replace("@UNIQUENAME@", transcript2UniqueName)) as JSONObject)
+
+        then: "we see the read through stop codon"
+        StopCodonReadThrough scrt1 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript1UniqueName))).first()
+        assert scrt1.fmin == 975979
+        assert scrt1.fmax == 975982
+
+        StopCodonReadThrough scrt2 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript2UniqueName))).first()
+        assert scrt2.fmin == 983963
+        assert scrt2.fmax == 983966
+
+        when: "we add the sequence alterations"
+        JSONObject addSequenceAlteration1ReturnObject = requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlteration1String) as JSONObject)
+        JSONObject addSequenceAlteration2ReturnObject = requestHandlingService.addSequenceAlteration(JSON.parse(addSequenceAlteration2String) as JSONObject)
+
+        then: "the stop codon read throughs should still be there"
+        StopCodonReadThrough scrt3 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript1UniqueName))).first()
+        assert scrt3.fmin == 974621
+        assert scrt3.fmax == 974624
+
+        StopCodonReadThrough scrt4 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript2UniqueName))).first()
+        assert scrt4.fmin == 983957
+        assert scrt4.fmax == 983960
+
+        when: "we delete the sequence alterations"
+        requestHandlingService.deleteSequenceAlteration(JSON.parse(deleteSequenceAlterationString.replace("@UNIQUENAME@", Insertion.all.first().uniqueName)) as JSONObject)
+        requestHandlingService.deleteSequenceAlteration(JSON.parse(deleteSequenceAlterationString.replace("@UNIQUENAME@", Substitution.all.first().uniqueName)) as JSONObject)
+
+        then: "the stop codon read throughs should still exist and snap back to the original position"
+        StopCodonReadThrough scrt5 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript1UniqueName))).first()
+        assert scrt5.fmin == 975979
+        assert scrt5.fmax == 975982
+
+        StopCodonReadThrough scrt6 = cdsService.getStopCodonReadThrough(transcriptService.getCDS(Transcript.findByUniqueName(transcript2UniqueName))).first()
+        assert scrt6.fmin == 983963
+        assert scrt6.fmax == 983966
+
+    }
+
+    void "Deleting a transcript should trigger an update exon boundary operation on its parent transcript"() {
+        given: "Transcript GB40828-RA"
+        String addTranscriptString = "{ ${testCredentials} \"features\":[{\"children\":[{\"location\":{\"strand\":1,\"fmin\":734606,\"fmax\":734766},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":734930,\"fmax\":735014},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":735245,\"fmax\":735570},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"strand\":1,\"fmin\":7 [...]
+        String setExonBoundariesString = "{  ${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\",\"location\":{\"fmin\":733999,\"fmax\":734766}}],\"track\":\"Group1.10\",\"operation\":\"set_exon_boundaries\"}"
+        String deleteTranscriptString = "{  ${testCredentials} \"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"operation\":\"delete_feature\"}"
+
+        when: "we add the transcripts"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see a gene and two isoforms"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+
+        when: "we set exon boundaries of one of the isoforms"
+        Transcript transcript = Transcript.findByName("GB40828-RA-00001")
+        requestHandlingService.setExonBoundaries(JSON.parse(setExonBoundariesString.replace("@UNIQUENAME@", transcriptService.getSortedExons(transcript, true).first().uniqueName)) as JSONObject)
+
+        then: "the boundaries should have changed for GB40828-RA-00001 and its parent gene"
+        assert transcript.fmin == 733999
+        assert transcript.fmax == 735570
+
+        Gene gene = transcriptService.getGene(transcript)
+        assert gene.fmin == 733999
+        assert gene.fmax == 735570
+
+        when: "we delete the extended isoform"
+        requestHandlingService.deleteFeature(JSON.parse(deleteTranscriptString.replace("@UNIQUENAME@", transcript.uniqueName)) as JSONObject)
+
+        then: "the gene's boundaries should match its own isoform's boundaries"
+        Transcript remainingIsoform = Transcript.all.first()
+        assert remainingIsoform.fmin == gene.fmin
+        assert remainingIsoform.fmax == gene.fmax
+        assert gene.fmin == 734606
+        assert gene.fmax == 735570
+
+    }
+
+    void "genes should conform to isoform length"(){
+
+        given: "GB40751-RA"
+        String addTranscriptString = "{ ${testCredentials} \"track\":\"Group1.10\",\"features\":[{\"location\":{\"fmin\":675719,\"fmax\":680586,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40751-RA\",\"children\":[{\"location\":{\"fmin\":675719,\"fmax\":676397,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":678693,\"fmax\":680586,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\": [...]
+
+        when: "we add the transcripts"
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+        requestHandlingService.addTranscript(JSON.parse(addTranscriptString) as JSONObject)
+
+        then: "we should see a gene and two isoforms"
+        assert Gene.count == 1
+        assert MRNA.count == 2
+        assert Exon.count == 4
+
+        when: "we delete overlapping exons"
+        def mrna1 = MRNA.all.first()
+        def exon1s = featureRelationshipService.getChildrenForFeatureAndTypes(mrna1,Exon.ontologyId).sort(){ a,b ->
+            a.fmin <=> b.fmin
+        }
+        def firstExon1 = exon1s.first()
+
+        def mrna2 = MRNA.all.last()
+        def exon2s = featureRelationshipService.getChildrenForFeatureAndTypes(mrna2,Exon.ontologyId).sort(){ a,b ->
+            a.fmin <=> b.fmin
+        }
+        def lastExon2 = exon2s.last()
+
+        String deleteFeatureString = "{ ${testCredentials} \"track\":\"Group1.10\",\"operation\":\"delete_feature\",\"features\":[{\"uniquename\":\"@UNIQUE_NAME@\"}]}"
+
+        requestHandlingService.deleteFeature(JSON.parse(deleteFeatureString.replace("@UNIQUE_NAME@",firstExon1.uniqueName)))
+        requestHandlingService.deleteFeature(JSON.parse(deleteFeatureString.replace("@UNIQUE_NAME@",lastExon2.uniqueName)))
+
+        def allMRNA = MRNA.all
+        def allGene = Gene.all
+        def allExon = Exon.all
+
+
+        then: "we expect a different gene"
+        assert MRNA.count == 2
+        assert Exon.count == 2
+        assert Gene.count == 2
+
+        when: "we get the left-most gene / MRNA"
+        def gene1MRNA = featureRelationshipService.getChildrenForFeatureAndTypes(Gene.first(),MRNA.ontologyId).first()
+        def gene2MRNA = featureRelationshipService.getChildrenForFeatureAndTypes(Gene.last(),MRNA.ontologyId).first()
+
+
+        then: "they should have the same fmin / fmax "
+        assert gene2MRNA.fmin == Gene.last().fmin
+        assert gene2MRNA.fmax == Gene.last().fmax
+        assert gene1MRNA.fmin == Gene.first().fmin
+        assert gene1MRNA.fmax == Gene.first().fmax
+
+    }
+}
diff --git a/test/integration/org/bbop/apollo/SequenceServiceIntegrationSpec.groovy b/test/integration/org/bbop/apollo/SequenceServiceIntegrationSpec.groovy
new file mode 100644
index 0000000..7fd5870
--- /dev/null
+++ b/test/integration/org/bbop/apollo/SequenceServiceIntegrationSpec.groovy
@@ -0,0 +1,352 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.codehaus.groovy.grails.web.json.JSONObject
+
+import java.util.zip.CRC32
+
+class SequenceServiceIntegrationSpec extends AbstractIntegrationSpec{
+    
+    def requestHandlingService
+    def sequenceService
+    def transcriptService
+
+
+    void "add a simple gene model to get its sequence and a valid GFF3"() {
+        
+        given: "a simple gene model with 1 mRNA, 1 exon and 1 CDS"
+        String jsonString = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40722-RA\",\"children\":[{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\": [...]
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+        
+        when: "the gene model is added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should see the appropriate model"
+        assert Sequence.count == 1
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 1
+        assert CDS.count == 1
+        assert FeatureRelationship.count == 3
+
+        String getSequenceString = "{${testCredentials} \"operation\":\"get_sequence\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"type\":\"@SEQUENCE_TYPE@\"}"
+        String uniqueName = MRNA.findByName("GB40722-RA-00001").uniqueName
+        JSONObject commandObject = new JSONObject()
+        
+        when: "A request is sent for the peptide sequence of the mRNA"
+        String getPeptideSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getPeptideSequenceString = getPeptideSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getPeptideSequenceString) as JSONObject
+        JSONObject getPeptideSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+        
+        then: "we should get back the expected peptide sequence"
+        assert getPeptideSequenceReturnObject.residues != null
+        String expectedPeptideSequence = "MSSYTFSQKVSTPIPPEKGSFPLDHEGICKRLMIKYMRCLIENNNENTMCRDIIKEYLSCRMDNELMAREEWSNLGFSDEVKET"
+        assert getPeptideSequenceReturnObject.residues == expectedPeptideSequence
+        
+        when: "A request is sent for the CDS sequence of the mRNA"
+        String getCDSSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getCDSSequenceString = getCDSSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_CDS.value)
+        commandObject = JSON.parse(getCDSSequenceString) as JSONObject
+        JSONObject getCDSSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected CDS sequence"
+        assert getCDSSequenceReturnObject.residues != null
+        String expectedCDSSequence = "ATGTCATCGTATACATTTTCGCAAAAAGTGTCTACACCTATACCTCCAGAAAAGGGTAGTTTTCCACTCGACCATGAGGGTATTTGCAAAAGACTTATGATTAAGTATATGCGCTGTTTAATTGAGAATAATAACGAAAACACGATGTGTCGTGATATTATAAAAGAATACCTTTCTTGTCGAATGGATAATGAGCTTATGGCACGAGAAGAATGGTCTAATCTTGGTTTTTCTGACGAGGTCAAGGAGACATAA"
+        assert getCDSSequenceReturnObject.residues == expectedCDSSequence
+
+        when: "A request is sent for the CDNA sequence of the mRNA"
+        String getCDNASequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getCDNASequenceString = getCDNASequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_CDNA.value)
+        commandObject = JSON.parse(getCDNASequenceString) as JSONObject
+        JSONObject getCDNASequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected CDNA sequence"
+        assert getCDNASequenceReturnObject.residues != null
+        String expectedCDNASequence = "ATGTCATCGTATACATTTTCGCAAAAAGTGTCTACACCTATACCTCCAGAAAAGGGTAGTTTTCCACTCGACCATGAGGGTATTTGCAAAAGACTTATGATTAAGTATATGCGCTGTTTAATTGAGAATAATAACGAAAACACGATGTGTCGTGATATTATAAAAGAATACCTTTCTTGTCGAATGGATAATGAGCTTATGGCACGAGAAGAATGGTCTAATCTTGGTTTTTCTGACGAGGTCAAGGAGACATAA"
+        assert getCDNASequenceReturnObject.residues == expectedCDNASequence
+//
+        when: "A request is sent for the genomic sequence of the mRNA"
+        String getGenomicSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getGenomicSequenceString = getGenomicSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_GENOMIC.value)
+        commandObject = JSON.parse(getGenomicSequenceString) as JSONObject
+        JSONObject getGenomicSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+//
+        then: "we should get back the expected genomic sequence"
+        assert getGenomicSequenceReturnObject.residues != null
+        String expectedGenomicSequence = "ATGTCATCGTATACATTTTCGCAAAAAGTGTCTACACCTATACCTCCAGAAAAGGGTAGTTTTCCACTCGACCATGAGGGTATTTGCAAAAGACTTATGATTAAGTATATGCGCTGTTTAATTGAGAATAATAACGAAAACACGATGTGTCGTGATATTATAAAAGAATACCTTTCTTGTCGAATGGATAATGAGCTTATGGCACGAGAAGAATGGTCTAATCTTGGTTTTTCTGACGAGGTCAAGGAGACATAA"
+        assert getGenomicSequenceReturnObject.residues == expectedGenomicSequence
+
+        when: "A request is sent for the genomic sequence of the mRNA with a flank of 500 bp"
+        String getGenomicFlankSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getGenomicFlankSequenceString = getGenomicFlankSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_GENOMIC.value)
+        commandObject = JSON.parse(getGenomicFlankSequenceString) as JSONObject
+        commandObject.put("flank", 500)
+        JSONObject getGenomicFlankSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected genomic sequence including the flanking regions"
+        assert getGenomicFlankSequenceReturnObject.residues != null
+        String expectedGenomicSequenceWithFlank = "TAATAAATTTACTGTGTATTTAATCTGTATTTCTATCTTAAATGTAAGTGCAAAATTTTTTGAAAAAAATTTCATTAATTTTTCTAATGGCAAATGTAAGGACCTCATTTTTTTTATTGACTTATTATTTTTTTAATTAGAATGCAATAGAAATTTTTCTATTTTCATTGTTCAGTAAATAATTATATTTTAAGTTTTCTTTAATTCTATTAAGAATAAAATAAATTCCTGTAAAACTAAAATAAATTATCGAAATTATAATTATTATTTATTTTATATTTAATTTAAATTGGATATCTTTTTTATATTATATAACACGGAGCGTGCGTAAAATGAAGTTAGAAATATCATCGTAAGTGGCGCTAGTATATTTATTATCCCGTAATGGCGGTTATGATTTATCGTGTATTTTGTAATAAGTTTACTTAAAAGAAGTTTATT [...]
+        assert getGenomicFlankSequenceReturnObject.residues == expectedGenomicSequenceWithFlank
+        
+        when: "A request is sent for the GFF3 of a simple gene model"
+        String getGff3String = "{${testCredentials} \"operation\":\"get_gff3\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        getGff3String = getGff3String.replaceAll("@UNIQUENAME@", uniqueName)
+        JSONObject inputObject = JSON.parse(getGff3String) as JSONObject
+        Sequence refSequence = Sequence.first()
+        FeatureLocation.all.each { featureLocation->
+            refSequence.addToFeatureLocations(featureLocation)
+        }
+        File gffFile = File.createTempFile("feature", ".gff3")
+        sequenceService.getGff3ForFeature(inputObject, gffFile)
+
+        then: "we should get a proper GFF3 for the feature"
+        String gffFileText = gffFile.text
+        assert gffFileText.length() > 0
+        log.debug gffFileText
+    }
+
+    void "add a gene model with UTRs to get its sequence and a valid GFF3"() {
+        
+        given: "a gene with a 5' UTR and a 3' UTR"
+        String jsonString = "{${testCredentials}  \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":694293,\"fmax\":696055,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40749-RA\",\"children\":[{\"location\":{\"fmin\":695943,\"fmax\":696055,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":694293,\"fmax\":694440,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon [...]
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+        
+        when: "the gene model is added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+        
+        then: "we should see the appropriate model"
+        assert Sequence.count == 1
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 3
+        assert CDS.count == 1
+//        assert FeatureLocation.count == 6 + FlankingRegion.count
+        assert FeatureRelationship.count == 5
+
+        String getSequenceString = "{${testCredentials} \"operation\":\"get_sequence\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"type\":\"@SEQUENCE_TYPE@\"}"
+        String uniqueName = MRNA.findByName("GB40749-RA-00001").uniqueName
+        JSONObject commandObject = new JSONObject()
+        
+        when: "A request is sent for the peptide sequence of the mRNA"
+        String getPeptideSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getPeptideSequenceString = getPeptideSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getPeptideSequenceString) as JSONObject
+        JSONObject getPeptideSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected peptide sequence"
+        assert getPeptideSequenceReturnObject.residues != null
+        String expectedPeptideSequence = "MPRCLIKSMTRYRKTDNSSEVEAELPWTPPSSVDAKRKHQIKDNSTKCNNIWTSSRLPIVTRYTFNKENNIFWNKELNIADVELGSRNFSEIENTIPSTTPNVSVNTNQAMVDTSNEQKVEKVQIPLPSNAKKVEYPVNVSNNEIKVAVNLNRMFDGAENQTTSQTLYIATNKKQIDSQNQYLGGNMKTTGVENPQNWKRNKTMHYCPYCRKSFDRPWVLKGHLRLHTGERPFECPVCHKSFADRSNLRAHQRTRNHHQWQWRCGECFKAFSQRRYLERHCPEACRKYRISQRREQNCS"
+        assert getPeptideSequenceReturnObject.residues == expectedPeptideSequence
+
+        when: "A request is sent for the CDS sequence of the mRNA"
+        String getCDSSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getCDSSequenceString = getCDSSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_CDS.value)
+        commandObject = JSON.parse(getCDSSequenceString) as JSONObject
+        JSONObject getCDSSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected CDS sequence"
+        assert getCDSSequenceReturnObject.residues != null
+        String expectedCDSSequence = "ATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGGACACGAGCAATGAGCAAAAGGTGGAAAAAGTGCAAATACCATTGCCCTCGAACGCGAAAAAAGTAGAGTATCCGGTAAACGTGAGTAACAACGAGATCAAGGTGGCTGTGAATTTGA [...]
+        assert getCDSSequenceReturnObject.residues == expectedCDSSequence
+
+        when: "A request is sent for the CDNA sequence of the mRNA"
+        String getCDNASequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getCDNASequenceString = getCDNASequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_CDNA.value)
+        commandObject = JSON.parse(getCDNASequenceString) as JSONObject
+        JSONObject getCDNASequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected CDNA sequence"
+        assert getCDNASequenceReturnObject.residues != null
+        String expectedCDNASequence = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGTAGAGGCAGAATTACCATGGACTCCGCCATCGTCGGTCGACGCGAAGAGAAAACATCAGATTAAAGACAATTCCACGAAATGCAATAATATATGGACCTCCTCGAGATTGCCAATTGTAACACGTTACACGTTCAATAAAGAGAACAACATATTTTGGAACAAGGAGTTGAATATAGCAGACGTGGAATTGGGCTCGAGAAATTTTTCCGAGATTGAGAATACGATACCGTCGACCACTCCGAATGTCTCTGTGAATACCAATCAGGCAATGGTGGACAC [...]
+        assert getCDNASequenceReturnObject.residues == expectedCDNASequence
+//
+        when: "A request is sent for the genomic sequence of the mRNA"
+        String getGenomicSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getGenomicSequenceString = getGenomicSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_GENOMIC.value)
+        commandObject = JSON.parse(getGenomicSequenceString) as JSONObject
+        JSONObject getGenomicSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+//
+        then: "we should get back the expected genomic sequence"
+        assert getGenomicSequenceReturnObject.residues != null
+        String expectedGenomicSequence = "CAAATGCCTGTCGAACGTGTGACAGTGGTTTGCTCCATCGCTGTTGCAACGGCCAACACTTTATCGTATTTCGTTCTTTTTTTAGCTGTGGCCGTTTCATCGTCGCGAAAATATGCCTCGTTGCTTGATCAAATCGATGACAAGGTATAGGAAGACCGATAATTCTTCCGAAGGTAAGACAGAATTCTCTTGTGCACACAGTATAGCTACAGTATACTCAGGGGACGACGAGTGAAATTTTGGCGTGGTTATGGAAAAAAAAAAAGTACAACTCGTAAAGTTGTTGGAGTAAATGAGTCCCGTTTTTTCATGGCGAATCGTACGTCTCCTTTCCACTCGACGACACAGTTTTCAATTTCATATAATAAAAGCGAATGTGAAAATACGATGCGTATGATTCGTTCGAAAAAGAAAGGCAAAAAAAAAAAAAAAAAAAAAATGAAATGATTT [...]
+        assert getGenomicSequenceReturnObject.residues == expectedGenomicSequence
+
+        when: "A request is sent for the genomic sequence of the mRNA with a flank of 500 bp"
+        String getGenomicFlankSequenceString = getSequenceString.replaceAll("@UNIQUENAME@", uniqueName)
+        getGenomicFlankSequenceString = getGenomicFlankSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_GENOMIC.value)
+        commandObject = JSON.parse(getGenomicFlankSequenceString) as JSONObject
+        commandObject.put("flank", 500)
+        JSONObject getGenomicFlankSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+
+        then: "we should get back the expected genomic sequence including the flanking regions"
+        assert getGenomicFlankSequenceReturnObject.residues != null
+        String expectedGenomicSequenceWithFlank = "GAAACGTACACAAACCGTTTCGACTGTTTGTACGTTACTCTTGAGTTCAAATTTACTCGACCTTTTCGAAACTAGCTGAACTTGAATATACTACTTTTGTACCTCTTAATAATAATTGATAATTATTATAAAATTAACAATAATTAATTCTATATAAAAAAAAAAAAAAAAAATAATAATATATTCCTCTACTATATTAAATTTATTTTTCAAATTACGTGGTTTCTCATATAAAAAAAAATTATTAATTTTATTAATCATAATTTTATATATATATATATATATATATATATATATATATATATAAAAAATAAAAATATATTCTTGAAATGAATAATTTTAAAAAACGATTCGATAATTCGAAAAATTATCATTTTTCATTTCCTATTATGAATGGGAAAAGAAATCGGCATTAATGGTACAGATAGAAGGTACGCATTT [...]
+        assert getGenomicFlankSequenceReturnObject.residues == expectedGenomicSequenceWithFlank
+        
+        when: "A request is sent for the GFF3 of a gene with UTRs"
+        String getGff3String = "{\"operation\":\"get_gff3\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\"}"
+        getGff3String = getGff3String.replaceAll("@UNIQUENAME@", uniqueName)
+        JSONObject inputObject = JSON.parse(getGff3String) as JSONObject
+        Sequence refSequence = Sequence.first()
+        FeatureLocation.all.each { featureLocation->
+            refSequence.addToFeatureLocations(featureLocation)
+        }
+        
+        File gffFile = File.createTempFile("feature", ".gff3")
+        sequenceService.getGff3ForFeature(inputObject, gffFile)
+
+        then: "we should get a proper GFF3 for the feature"
+        String gffFileText = gffFile.text
+        assert gffFileText.length() > 0
+        log.debug gffFileText
+    }
+    
+
+    void "Add 2 genes and get their GFF3"() {
+        
+        given: "a simple gene model with 1 mRNA, 1 exon and 1 CDS"
+        String jsonString1 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40722-RA\",\"children\":[{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":1248797,\"fmax\":1249052,\"strand\":-1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\" [...]
+        JSONObject jsonObject1 = JSON.parse(jsonString1) as JSONObject
+
+        String jsonString2 = "{ ${testCredentials} \"track\": \"Group1.10\", \"features\": [{\"location\":{\"fmin\":729928,\"fmax\":730304,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"GB40827-RA\",\"children\":[{\"location\":{\"fmin\":729928,\"fmax\":730010,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}},{\"location\":{\"fmin\":730296,\"fmax\":730304,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\" [...]
+        JSONObject jsonObject2 = JSON.parse(jsonString2) as JSONObject
+
+        when: "the gene model is added"
+        JSONObject returnObject1 = requestHandlingService.addTranscript(jsonObject1)
+        JSONObject returnObject2 = requestHandlingService.addTranscript(jsonObject2)
+        
+        then: "we should see the appropriate model"
+        assert Gene.count == 2
+        assert MRNA.count == 2
+
+        when: "A request is sent for the GFF3 of a list of genes"
+        String uniqueName1 = MRNA.findByName("GB40722-RA-00001").uniqueName
+        String uniqueName2 = MRNA.findByName("GB40827-RA-00001").uniqueName
+        String getGff3String = "{\"operation\":\"get_gff3\",\"features\":[{\"uniquename\":\"@UNIQUENAME1@\"}, {\"uniquename\":\"@UNIQUENAME2@\"}],\"track\":\"Group1.10\"}"
+        getGff3String = getGff3String.replaceAll("@UNIQUENAME1@", uniqueName1)
+        getGff3String = getGff3String.replaceAll("@UNIQUENAME2@", uniqueName2)
+        JSONObject inputObject = JSON.parse(getGff3String) as JSONObject
+        Sequence refSequence = Sequence.first()
+        FeatureLocation.all.each { featureLocation->
+            refSequence.addToFeatureLocations(featureLocation)
+        }
+        File gffFile = File.createTempFile("feature", ".gff3");
+        sequenceService.getGff3ForFeature(inputObject, gffFile)
+        
+        then: "we should get a proper GFF3 for each feature"
+        String gffFileText = gffFile.text
+        assert gffFileText.length() > 0
+        log.debug gffFileText
+    }
+
+    void "Add a gene and get the complete peptide sequence as well as peptide sequence of each exon"() {
+        //GB40744-RA
+        given: "a gene model with 6 exons, a 5'UTR and a 3'UTR"
+        String jsonString = "{ ${testCredentials}  \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"name\":\"GB40744-RA\",\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"f [...]
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+        
+        when: "The gene model is added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+        
+        then: "we should see the appropriate model"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 6
+        
+        when: "a request is sent for the peptide sequence"
+        String uniqueName = MRNA.findByName("GB40744-RA-00001").uniqueName
+        String getPeptideSequenceTemplateString = "{\"operation\":\"get_sequence\",\"features\":[{\"uniquename\":\"@UNIQUENAME@\"}],\"track\":\"Group1.10\",\"type\":\"@SEQUENCE_TYPE@\"}"
+        String getPeptideSequenceString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", uniqueName)
+        getPeptideSequenceString = getPeptideSequenceString.replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        JSONObject commandObject = JSON.parse(getPeptideSequenceString) as JSONObject
+        JSONObject getPeptideSequenceReturnObject = sequenceService.getSequenceForFeatures(commandObject)
+        
+        then: "we should get back the expected peptide sequence"
+        assert getPeptideSequenceReturnObject.residues != null
+        String expectedPeptideSequence = "MEEMDLGSDESTDKNLHEKSVNRKIESEMIENETEEIKDEFDEEQDKDNDDDDDDDDDDDDEEDADEAEVKILETSLAHNPYDYASHVALINKLQKMGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYLAVEVWLEYLQFSIGNMGTEKDAAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYALIDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAK [...]
+        assert getPeptideSequenceReturnObject.residues == expectedPeptideSequence
+        
+        when: "a request is sent for the peptide sequence of each exon"
+        Transcript transcript = Transcript.findByName("GB40744-RA-00001")
+        def exons = transcriptService.getSortedExons(transcript, true)
+        def exonUniqueNameList = []
+        for (Exon exon : exons) {
+            exonUniqueNameList.add(exon.uniqueName)
+        }
+
+        String getExon1PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[0]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        println "GET EXON STRING: ${getExon1PeptideString}"
+        commandObject = JSON.parse(getExon1PeptideString) as JSONObject
+        String exon1PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+
+        String getExon2PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[1]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getExon2PeptideString) as JSONObject
+        String exon2PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+        
+        String getExon3PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[2]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getExon3PeptideString) as JSONObject
+        String exon3PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+        
+        String getExon4PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[3]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getExon4PeptideString) as JSONObject
+        String exon4PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+        
+        String getExon5PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[4]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getExon5PeptideString) as JSONObject
+        String exon5PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+        
+        String getExon6PeptideString = getPeptideSequenceTemplateString.replaceAll("@UNIQUENAME@", exonUniqueNameList[5]).replaceAll("@SEQUENCE_TYPE@", FeatureStringEnum.TYPE_PEPTIDE.value)
+        commandObject = JSON.parse(getExon6PeptideString) as JSONObject
+        String exon6PeptideSequence = sequenceService.getSequenceForFeatures(commandObject).residues
+        
+        
+        then: "we should get the expected peptide sequence"
+        assert exon1PeptideSequence == "MEEMDLGSDESTDKNLHEKSVNRKIESEMIENETEEIKDEFDEEQDKDNDDDDDDDDDDDDEEDADEAEVKILETSLAHNPYDYASHVALINKLQKMGELERLRAAREDMSSKYPLSPDLWLSWMRDEIKLATTIEQKAEVVKLCERAVKDYL"
+        assert exon2PeptideSequence == "VEVWLEYLQFSIGNMGTEKDAAKNVRHLFERALTDVGLHTIKGAIIWEAFREFEAVLYAL"
+        assert exon3PeptideSequence == "IDPLNQAERKEQLERIGNLFKRQLACPLLDMEKTYEEYEAWRHGDGTEAVIDDKIIIGGYNRALSKLNLRLPYEEKIVSAQTENELLDSYKIYLSYEQRNGDPGRITVLYERAITDLSLEMSIWLDYLKYLEENIKIESVLDQVYQRALRNVPWCAKIWQKWIRSYEKWNKSVLEVQTLLENALAAGFSTAEDYRNLWITYLEYLRRKIDRYSTDEGKQLEILRNTFNRACEHLAKSFGLEGDPNCIILQYWARTEAIHANNMEKTRSLWADILSQGHSGTASYWLEYISLE"
+        assert exon4PeptideSequence == "CYGDTKHLRKLFQKALTMVKDWPESIANSWIDFERDEGTLEQMEICEIRTKEKLDKVAEERQKMQQMSNHELSPLQNKKTLKRKQDETGKWKNLGSSPTKITKVEMQIKPKIRESRLNFEKNADSEEQKLKTAPPPGFKMPENEQMEIDNMNEMDDKSTVFISNLDYTASEEEVRNALQPAGPITMFKMIRDYKGRSKGYCYVQLSNI"
+        assert exon5PeptideSequence == "EAIDKALQLDRTPIRGRPMFVSKCDPNRTRGSGFKYSCSLEKNKLFVK"
+        assert exon6PeptideSequence == "LPVSTTKEDLEEIFKVHGALKEVRIVTYRNGHSKGLAYIEFKDENSAAKALLATDGMKIADKIISVAISQPPERKKVPATEEPLLVKSLGGTTVSRTTFGMPKTLLSMVPRTVKTAATNGSANVPGNGVAPKMNNQDFRNMLLNKK"
+    }
+    
+    void "testing sequence retrieval functions"() {
+        //GB40744-RA
+        given: "a gene model with 6 exons, a 5'UTR and a 3'UTR"
+        String jsonString = "{ ${testCredentials}  \"operation\":\"add_transcript\",\"features\":[{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":768063},\"name\":\"GB40744-RA\",\"children\":[{\"location\":{\"fmin\":767945,\"strand\":-1,\"fmax\":768063},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"fmax\":763070},\"type\":{\"name\":\"exon\",\"cv\":{\"name\":\"sequence\"}}},{\"location\":{\"fmin\":761542,\"strand\":-1,\"f [...]
+        JSONObject jsonObject = JSON.parse(jsonString) as JSONObject
+
+        when: "The gene model is added"
+        JSONObject returnObject = requestHandlingService.addTranscript(jsonObject)
+
+        then: "we should see the appropriate model"
+        assert Gene.count == 1
+        assert MRNA.count == 1
+        assert Exon.count == 6
+   
+        when: "we getSequences"
+        CRC32 crc = new CRC32();
+        crc.update("Group1.1".getBytes());
+        String hex = String.format("%08x", crc.getValue())
+
+        then: "it is split appropriately"
+        String []dirs = sequenceService.splitStringByNumberOfCharacters(hex, 3)
+        assert dirs == ['e15','221','82']
+
+
+        when: "we call getResiduesForFeature"
+        String geneSequence = sequenceService.getResiduesFromFeature(Gene.findByName("GB40744-RA"))
+        
+        
+        Gene gene = Gene.findByName("GB40744-RA")
+        def featureLocationList = gene.getFeatureLocation()
+        println "featureLocationList : ${featureLocationList}"
+
+        then: "we have it"
+        println "Gene Sequence: ${geneSequence}"
+        assert geneSequence.length() == 6521
+    }
+    
+    
+}
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-0.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-0.txt
new file mode 100644
index 0000000..b19418a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-0.txt
@@ -0,0 +1 @@
+GCCAGGGAATGGCTTGTCATTAGGGACAACTTGTCAAGTCCCTAGCTTTTTATGATGTATACCTTAGACGCATAAACAGGATCTATAGTTTTGCAATATATGCCTTTGGACAGGTATTCTACAGGCAGAAAGACCCCTCACTTAGGGCTGGCTATCCCTTCGGGCGATCCTTCCACCGCCCTTATTCCACGATATTTGCATAATGATTTATGGATGAAACGTGACCACGTTTTCGTGGGATCGGGACACGCACCGAAAACTTGCCGGAATTTCAATTACGCGTGGAAAGATTTCAATGGAAATTGCGCTACGATCGTATATTATTCGAGGCGAATGAATTTTGGCACGTTGAGTATTTTAATTGTTGGAAAATCGTTTTAATTTTCGATGATGTAAGATTTCCCGAAGAAATTGCTCTCACGAGTGAGCAAGTTTTATTTATAGTTTAGAGAATCGAACGGTATTAATTGATAAAACTAATAATCGTTACAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-1.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-1.txt
new file mode 100644
index 0000000..9645146
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-1.txt
@@ -0,0 +1 @@
+TGCGCCCCGTTGTCCTCCGTGTCCTCCGTCTCGCTTCCAGGCGTTTTCAGCGCCGCGTCCTCGGACTCGAGGCAGTGCTCCGCGATCAGCTCATCCTCGCTCGCCTGGAGGGCCGCGTATGCGGCCGCGGCGCTCGAACGCTTCTCCGTCAGCCTCGAGCATTGCTCCTTGCCTCGTGGAAGACTCTCGTTGCTCTCACCTGGACAATTTTTTTTTTTTCCAGCACGATTGGCCCCTTTGTCTTCCATCCTCTTTTTTTTATTATTTTATTCCCGATTTTCTCTCGGTTTTTTTTTTATTTTCGATTTCGATGGAAAAGTGTTGATATATTCGTGGGAGGATCTTTTAGAGGAGTAAATGGAATTCTAATTCAACGAAGTCAAATGTTTAAAATTTTTTTTTTTTTTTTTTTTTGGTCGAAAATCGTCCTGTGAATATATCAACTTGTTAAATCAAGGGAAAGAGAGGTCTTGAAATTATGGAAACAATGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-10.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-10.txt
new file mode 100644
index 0000000..f42fde9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-10.txt
@@ -0,0 +1 @@
+AGCAGGAAAAATTTCCTTCAAGTATGTAAAAACTTTTAATATGGATGAATATGTTGATTTACCCAGAGATCATCCAGAATCTTATCATTATTATATGTATAATAATTTCTTTAAGCACATAGATATAGATCCACAAAATGTGCATATTCTTGATGGTAATGCACCTGATCTTATAAAGGAATGTGATGATTTTGAAAACAAAATTAAAGAAGCTGGTGGTATTGAATTATTTATTGGTGGTATGCAATTTTATTATATGGAAATAAATAAATAAATATAAAAAAAATTTATATGAATAAAAAAATGTCATGTTTTAGGTATTGGTCCAGATGGTCACATAGCTTTCAATGAACCAGGTTCTTCCTTAGCATCACGTACAAGAGTAAAGACATTGGCGCAAGATACATTAGAAGCTAATGCAAGATTTTTTGGAAATGATATTTCCAAAGTACCAAATCAAGCATTGACTGTTGGTGTTGGCACAGTGATGGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-11.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-11.txt
new file mode 100644
index 0000000..35bfb0e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-11.txt
@@ -0,0 +1 @@
+GTAATGCCTTTTATCTTCATATATTACATGAATATTTAATTTCACTGAGAAATATAATTATTAATTATTAATTATATATAAATCAATATTTTGATTTATTATATATAACANNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNCGAAAAATCAATAATAAATATATCAATATTAAATGAATGACGAAAAATTTGATGATTTTAAAAAAAATCTGATGATATAATACTAATCCATTATATTTAATTTATTTGTACGCTATAGATAAAAAGATCCAAATTTCAGATGTCAAATATAATATTTAATAACTTAACAATTATTTACAAAAAATACATGTATACACATAAATAAACATGTGATGTTTTGGAAACATTATTATGCTCATGAATATGCAATAAAAGATATAACATTCTATTCTATAAAAAGTTTAAATATAACTTTCATAATTTTCGCAAGTCATTTAAGTTGAATAATTAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-12.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-12.txt
new file mode 100644
index 0000000..4304ca2
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-12.txt
@@ -0,0 +1 @@
+TGCATAGAAAATGATGGTCCGTTACTATTTGTAGTATATGCAACTTCTTCGTGATCATTCTGATATTCAGGATTCTCTAAATTGTTCGAGTTACCTTTCCCACACTTCAAAGCGAATAACATCATTATACCAACAATGAAAAGGCCAAAAATAATGGACATCACTGCAATTGCTGGACCAGAATACTGTTTCAATTCCTCAACATTCGTATAAGAGAGTGCTAATGGATCAGTTTCGCACCAACTGGTCCAAGTATTATTCCAAAGTGTCGGATCCGCTCTAGGATCTCCATCTAAATTACTAGCAGGTATCGCGGTTATTCTGTACTTCATTAGAATTCTTTCCAGAATAGCCATAATCACTGGTCTTCTTTCGGCAAGATTCACCATTTCGCAAGGGTCTTTTTCTATGTTAAAGAGACATGGTGCTGTCATTGGTTCACAAGGGATCTATGGCAAAATTATAGAGAAATTTTTTAAAATTTTATCAAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-13.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-13.txt
new file mode 100644
index 0000000..499c92c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-13.txt
@@ -0,0 +1 @@
+AGTTCCGTGCCTAAAGTGCATAGACCACTTTTAGATGCGCCATACATGCCCAATGGTACAAAAATGGCATCGAGATGAGATTCAGATATACTATAAGTCAATACAAATGTAAATGAAATTAAAATTTTTTATTATAAAGATTGACTAATATTTGATTTATATATATTAAAAAAAAGTAAAAACGATAAGTAGAAAATAAAAATGATGATCAGACAATATATTTTTCATATGATTGGAATATCACGAATATGTACAATACAAATGATACAATACAATACAATAAAAATGATTGTACTTTCCATTTTTATATATAAATTATCATATAAAAATAAATTACATATTAATAAAGAATCATTATAAATTAATCAATAATGTTTATAATAATGAGTTTTAAACTTTATTTACATTTGATTTAAAATCAAACAGTATTATGAATGAAGATTTAATAAATAAGTATAATTTTTATAAATGATTAATTATGATAGAGTTTAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-14.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-14.txt
new file mode 100644
index 0000000..af3c633
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-14.txt
@@ -0,0 +1 @@
+TTTCTTTTTGAATAACAATAACAAACGTGTTAATATTGTGAATTCTAAAGAGCAAAACAAATACAAAAGTTAATATACAAAAATTTTAGATTATTTACTTACATATTTATTACTTATAGTATATATGAGTAATTTAAGGTTAGATAAAATGAGATAAGAACACTAGATAGTGTAAAAAAGATAGAGAAATCTCACTTTGTGTAACGCAAACAATTGTTTATAACTTAACAAATAAGGATTTAATCGACATTTTTGTATACCAACTTTTGTGTTATTCTTCGATTCTCCAAAGAATTTCATGATATTAATACCTTGTATATATGAAATATATAAGAAATATATTCATAATAAATATTTAACACGTAATTTTTGAAACAAATCTATTAATTTACGATATGGAACGTGCCAATTCTATGAAAATTAATCTTTTTTTTTTTTTTACATAGATCCATTTTTCTGCAGACACGTGGAAAGGAGGAACGTAATAAAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-15.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-15.txt
new file mode 100644
index 0000000..5638def
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-15.txt
@@ -0,0 +1 @@
+TGAAATTTCGAGATTAACTTCTTAAAATATAAAAATCTACAGGAAAGTATTCTAACAATATTGTAGTGTCTTTACTCTTAGATATTCTAAAAATGCACGTATTTTTATCAAATAATTATAGACAAATAATTTTTAATCTTTTATAAAATATCTTATTTTAATATTTTTTTCAAAAATACTATTGATATCTAAACTATCATTTTATAATTTTAATAATAAAAAATAGATTTTATTTTTACTTAAAAAATTTGTAGCAAATTTATTAATAATACTTTATTTCTTACGATGTTAAAAAAAATTACTTTTTCTAATTATTTCTTTAAGACCAATCGTAATATCTAGATAATTTTCCATGTGAATGTTGGCAATCGCGTTATGAATCTATCTAACCTCTCTAAATTCGAAAATATAAATTCGGAATACGCATGCTCTCTTAGTGACAAAAGATCGCGAATTCGCAGCACGATTTCACGGATGGCAAGAGTAAGTAGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-16.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-16.txt
new file mode 100644
index 0000000..1e7f473
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-16.txt
@@ -0,0 +1 @@
+TAATGTCTTAAAGCGACATCGAGTATCAAATACTATATCTATATTATGATTATTCCTATCATTAATATACGTTTAGCATTTTTGAAATTATTTTTTTAATTTATCTAAAATGTAACATTCAAGTTTTCTTATTATTACAGTTTTTTTTTTACAACTTTCTATGCATTACTAAAGCTCTAGTATCTAATAAAAAATATATAATTTTTAAATCGACTTTTATATTATTTTATAATTATATAGAAATTACATGTGTAATAGTATATATATATATATATACTAAATATATTAATATTATTAACAAAAAGAATTTTTATATCTGCATTTGTAGCAATAAAATATGCATTGAAATCATTTAAATATTACACTGTTTTTATTAATTTTTAATAATAATGTGTTTATCAAAACCAACTTCTATCTACTTCAGAATCTTTTTCAAATAGAATATTTTTAATGATTACTTTTTACTTTCTTAAAAATACTAATAATGCCTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-17.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-17.txt
new file mode 100644
index 0000000..bfa6702
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-17.txt
@@ -0,0 +1 @@
+ACCTTTTCCACTGTGTTAGTAGTATTTCTACTAGTACTTTGTCTGCCTGTGGTAGTGCTTTTTAAATTTTCTTTTATAGGCTCCGAATCTTTTCGTTTTTTTCGACGATTCTCTTCATCCTGCTCTTCTGTATTGTCGTTTTCAAATGCACGCTTAGACGAAGGCGATGCTGAAACCGCAGGGGCATCATCGGTAGTAATCGATGCGGTTGTTGTTTCACCGGAAGATGCTGCACTATTAGCGGTAGACACGTTGTTACCACCATTCTGTCCTCCTTGATTCCCAACAGACTTGCTACCATCCTTATCTTTCTCGGGATTAGGGTTTGGTGATGAGCTTGCACCTCTAACTGGTGTGGGCTCTTTCGAACTTTTGAATTCGTACACGTCGCTACTATCGCTAGTAGACGATGCGTTTCCAGAAGTACTGGAACCCGAGACGGCATTCACGGTCCCCGATGAGACGTTTGATGACGTCTTCGACGAAGTTTCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-18.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-18.txt
new file mode 100644
index 0000000..915ae4f
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-18.txt
@@ -0,0 +1 @@
+TTATTTTCAAAAATGTTAATTTCCTTAACTATGAGCAAATTTCATTACATTTCAATCAATATATGTGTCACTTTTAAAGTGTCTAATTTTATAGCGGTGAGTCTACCAGTCTTTTTTCCTTCTCCAAACCTAGGCATTTTAATTATTAAAATCGTCATATTGAACCCTGTGTTTGACTGTATCTCGTTTTCAACAACATTTGTGTATGGCACAAATGGATATAGAGCTCACATAACTAATATGTGTAAATACTTTGTTTTACTAGTTCCACACAAACATATGACGAAGAAGAATAATTTTTTCATTCTTCTTTCGCTATTATTTAACCCTGTCGCAAAATTTTATATTACAATTCGCCAATCTTGAATTTGTATTCTTTTCATTACTCAAAGATATACATAAGTATATCCATTTGTGTTAGAATAGATTTTTTTACTTTGTTTATTATAGGCATTAATACATTTAATATTTGCGGCAATAAAATTATTACTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-19.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-19.txt
new file mode 100644
index 0000000..f2b5bd6
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-19.txt
@@ -0,0 +1 @@
+TTGTCAAATTAATAAAATTGATCAGTAAACCAAAAAATTAAACAATCAAAAATAATTTCCATTTTTAACAAATTACCAAAAAATTAATCAAAAAATATTTTCCATATATTGATCGATTAATAATGATTAACGATCAATAAATTTTTTCCGTTATAGCGTGCAATTTTTTTGCGATTAAAATTACGCTTCGATCCGAGAACCGATAGACACAGCGACCTGGCAGCGTTCGACAACTGGAACTATCTGGCAGACTCCACGTTAGCGGATCAGGCCGATTGCCAATTGCAAAATAGGGCGACCTGGATTTCGTATCTCGAGACATCCAGGTACCGCTGCAGGGACGATTACCAAACTCCGTGATAGGCCTTCGAAACAGAAGAAGAATCCTCGAAGCAATAAGAAGTATACTCATTACGCCGTCCACGTAGCAGAACTTGGCAGAATTCGTAAATTCATGGACCGTTTCGACTAGATAGATCTCGAAATCTTTCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-2.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-2.txt
new file mode 100644
index 0000000..561fb2f
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-2.txt
@@ -0,0 +1 @@
+ATCTTATTCTTAATGTCAATATTTGAAAATCTTTAATACATCAACTATTTTGTGCCAAAATCTTTCATTGAAATTTAAAAATTCGCAATCTAAATTTCAAAATTTTCAAAATAAAAATTTTTCCCCAATATTTTCATTTATATCGTGTCAAAATTAATCTACAACAGATACATATTTCGTGTGCAAATATATTAAAACGAAGATTAAAAATTTTTTCCTCGGAAAAAAATCTGACATGTTAATCATTGTATGAAAAGGATTCTTTTAACCACGCTTTTCGTTTCGAAACGAAAAGCGAAGAAAAGGTTTCGTGGAACTTGTGCAATTCGAATCGACGTTTCCTAGAATCAGTTCATTCCAGTCACGAATCGGCGAAGGAAGGAACGAGCACGGGCAAAGTTTCACGTCAAGCTCGATTAATCGAGCATCGTTCCCGTTTCCACACCGAGAGAAGAAATTTTCGATGGTTTTGTGCTTTCGAGCTCGCACGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-20.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-20.txt
new file mode 100644
index 0000000..28c5f33
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-20.txt
@@ -0,0 +1 @@
+GTTGATTTTGAATATAAATAATTTGTGCAATTTATGATAATGATGATAATTTATTATTAACACGTTCGTAATTATAGGTTAAGATTGTGATAAAATTATCACAAATTGTGGAAATAGAAAAGAAAACAGGAAAAGAAAAAGAAAATAAAAAATGTGAGAAGGATAAATTGTAGATTGTTTAGAAAAATGCAACACTTCTTTTATCGAGCACTCTTGAACCGGGGGCTATCTGTTATTGTGAATTATCACCGCTGTCTGTTTTTATGAATACAATTTCATACTTCCGCTCCTATCATTTTTTTCTTATCTTTCTACGAGTGTCAACAAAGTTATTATCACGTCTACTATGTTATTGTATGATAACTAACAAAACATAATAAGCAATGAAAAGTTAAATATCAAATGTACAACAACATTGTTGAATACTAATAAGAGCAGCATTATCCAATTGTTGAACTGATTTTAGAAAAATAAGTTTGTTGTTATGATTTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-21.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-21.txt
new file mode 100644
index 0000000..2e0900b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-21.txt
@@ -0,0 +1 @@
+AGGGAGGAGCTCTCTCTCTCTCTCTCTCTCTCTCTCGGTCGAAGGGAACTATCGATTGCAGCCGAGCAAGATAGGATCCAACCGTGGTCCTAGGAACTGGGATGTGACCGGAAGGAGCATACACCGGAAGGGGTCACTGAAACTTTTGATTGGGGAGAGAGGAGGGTGAGGAGCTACTTTTTCTTCTTTTAATAAAAATTGGTTTTTGAATTGATATATCGATTGAAAGTATCGTTTATTAATTTTAGATTAGATTGGAGAGAGAAATGTCAATTTTATTGTATTTTTGATAGGAATGGAGATGAATTGATTGTTCAATTGTTTATTGTGTTTGAGAATTATTGGAAGAAGATTGGGGAGAAGATTTTTATTCACGAGGCGAGGGTTAATTTTGATTCCTGAGAGGTGGCGTGTGTCACGCGGCTTTTTGAATTTTAACGCGTGTCGTGGATCGCCGACGCTTGTGCGTACGCAAATGTTAAGAATTTATCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-22.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-22.txt
new file mode 100644
index 0000000..b4153b5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-22.txt
@@ -0,0 +1 @@
+AGGATACAGTGTTTTTAAACTATTGAAATGAATTTTTGAAGGGGGATGTGGAAATATTCAGCTCTCCTTTGGATGATTAATGAGAATTCCTCCACACCCTATAATTGAGATTGATCTTAAAAGATTCTGATTGATTTTAAATTATTAAATATAATATTTTTGAACTATTGAAATGAATTTTTGAAGGGGGATGTGGAAATATTCAGCTCTGCTTTTAATGATTCCTGGATAGTCCATGAATTTATTGAAAAAATAGAGATTGTGATTGATTAGAATTTGTCATTTTAAATTAATAAATTGGAATTGAAAAATACTTTTAAATTATTCAAATGAATTTTTAAATGGGGATGTGGAAATGTTCAATTCTTTAGCTTTGGATAATTAATGCAAATTCCTGACAGTCCATGAATCTATTGAAAGGATGGAGATTTTGATTGATTAAAATTATTTGTCACTTTAAATTAGCAACTGGGCATCAATATTTTATAAACT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-23.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-23.txt
new file mode 100644
index 0000000..acb95bb
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-23.txt
@@ -0,0 +1 @@
+TAAAAAATAGGCGACGCAATTCCACAGCCTAACCTCACACACAATGTGGAAAATACGTCGTGCACGATACACTAGCATCGTCAGTCGCTGAAAAATTGTTATAAACTTATGAACTATGGTCATCATACCCATTGCCTTTCTCTCATCCAAATAACACCTGGGCATTTGAATGAGATCCTGGTAATGGACATGGAAAGGGTTAAATCTGAAAATCCTGAGCTAAAAAAGATGATTAGAGTATTTGTTTTTTTTTCTAACGGATTAATGTTTTCTGATTCAAAATTTGTTTATTTTATTTTAAATTTTAATAAATTTATTTTATAAAATCAATGAACTTCTATTAATTTCAGAAAGGAAAAATATATAGGCAATAAATAAGAAATTTATTTATAACAATTAATAATTAAATTTATATATGATAATTAATAATGAGAAATAATTGAAACTTTAAATGGAATATTATTGAAAATAACCTCAGGCGATCTCTTTATC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-24.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-24.txt
new file mode 100644
index 0000000..fef3068
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-24.txt
@@ -0,0 +1 @@
+AGTACACATAATAGTGCACAATCGTATCGTTGAGTTGATATGTACATACACGTATTACGTTTGACCATTGTTAAAAGATACAAAGACTGTCCGTAATTGTTCCTAATTTATTTTCCAGCTGGGGAGATTGGAAATAATTATCTGCCGTCCCATTTATTAATGGTCCATGTACGAGTTAATTTCGTTAAGAGTATCAAAGACAGTGAGATACAATTTCTTTCCTCGTATCGCGAGATTTCCAACAAGACAAATCGAATTTCTAGATGAGAAATGGAACAATTCTTATACAGTTATATAAACTGGATTAAATCGAACGATTAAAACACGTTGTTTGAACTTTACGAAAAATTGAATAATCAAGAATATTAAATATAATTCGCTTCAATCTGCGACTTATGAATTAAATAATAATAAAAAAAAAAAATAATACAAGTCCATTTTTGCAAACAAGATAAGTATTTTCAAAAATTCCATAAAATATTTCGTAAGTCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-25.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-25.txt
new file mode 100644
index 0000000..fddd071
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-25.txt
@@ -0,0 +1 @@
+GTACCACGAGTACGTGCTTGATGCCAAAGACGTAATAACTCGGAAGTACTCGCGTGTAACGTAACGTTTAGGCGCGAGTGCGCGACCCTTTATGCTCGAGGGCTCGTTAATTACGTGAAAAGACGCGACCGTACGCGCACGCGAAGTTGCGCAGCGTTTATGCGTACAGAGGAGGGGGAAAACGGTGGCAGTGGTGGGAGAAGGTTGCTCGCTGAGGATAACGTTTCTATGAACGTTAGGAGAATTAAAAGTTATGTTATGAAATTCTAGAATTCAATAGAGTATCTTAGATTTCTTTCAAAAATATCCTAAAGATATTTTTCTCGTTTTCTTACAAGCATTATACGGTATACGTTCGATACATATATTTCAGATTTCAATAAGAATCTTTAAATATAGATAATAAGATTTTTAATAATTTACCAGGTTTATTATTTTTCTTCTCACAAATATTTTATCATATCCGAACGAAAACGTATTTTCCACTCATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-26.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-26.txt
new file mode 100644
index 0000000..0a99596
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-26.txt
@@ -0,0 +1 @@
+CTCATTTACGTTAAAAGTCGTTAACTCGGAAAACATCTGTACATGTATCTGTAGTAGTATCAAAGTTGTATTTCTCTATCATTCAAATAATTGCGAGTGAAAATGTATGCACGTACAAATAAGAGATAAATGTCTAATGCTCTTATCATGGTACTTGATGGAATGAGTTTCTTTACTCAATGTACATTTTCACAGGATTAGTTTCGTTCCAACGTTGAATTTAGCTTCGCTTGACTCTCTTTTCAACCAGTTGTGCATATATATGTACACACACACACAATGCTTTCCAATATAAAGTTTATATGATTCTAATCATCACATTATGCATGAGAATAAAGATAACGAATAAAGCTAATATGTATAATTATTATCGTAATCAAATTTTAGATTTTCTTTTTTAATACAAGAATGATCATTTTATGACAGAAAGAATGAAAATTTTAAATTATCAAATTATTAAACCATTAAACTGATTTAACGAAAAATAAAAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-27.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-27.txt
new file mode 100644
index 0000000..d1efde8
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-27.txt
@@ -0,0 +1 @@
+TATATATATATTATATATACATTTTTTGTTGGATAAATATTAAAAGTAATTGGAAAATGATAATTTCAAATAGATTTGTAACATTTTATTTACATATTCCTCAAAAAACTTTATTATTACATCGTAACGAATAAATGTATAAAGTGCCGATTGGTGAATAATTGCATTGAAAAAAATTGACGTGAGTCACAGAGTCAACCTTCAACATTTATCGAAAAATGAAATATTGTAGCAAGGAGAAAAGATAAGAATTCACCGTTTGACGTTAAAATTAAATTTAAAAAAAAAAAAAAGAAAATTCTATGAAGTTTAACATCAATTTAACTTTTATATTTTATCAGTAATTAAATTTTAATTCAATTAGTTTTTTTTTTTTTTCAAATTTAATCTTAAATTTTTAAATTTCTCTCATATTCATAAAATAAATCAATTTTGTACACTGTAATTTGATTCCATCGATCAATTTTATAAATTTTCATTTCTCATTTTTTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-28.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-28.txt
new file mode 100644
index 0000000..82d0cd2
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-28.txt
@@ -0,0 +1 @@
+TTTTATCTATAATTTCATAATTTTATTAGAGTTATAGGTACAATTTTAAATATCATATTAGAAAACATTCGTATTAATTTTAATAGGATTTTAATTTCGATTTATTTTAATGCCAAAGAAATTATAGAAAATTATCCAGATACTTAACAGCTTTTCCATCGATTCGGTAATTATCACGATATATATTTAGCATAATATTTCGAACTTTTTTAATTACCGAACTTAAATAATTAGACAAAATGACTTCAGATGGTTTTAATATCTCGAGAAGTATTAAAGGCATTAGAGCCTGAATCATTTAAATTTTATGTGTTATATTTTACGATAAATACGAGAAGGCGTAACATTAAGAATGGATATTATACGACGTACGTATTACCATTACAAAAATCGAATAAAATCGCAAGTTTTATATTTTTTTTGTAATCGATATATCGGTGTTTCTTAAAATATTTATGGTAAACCGCGACATGCATATTCTGTTCGATTTCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-29.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-29.txt
new file mode 100644
index 0000000..ff8d239
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-29.txt
@@ -0,0 +1 @@
+GATAAATAAATAAAGCATAATTATTGCATCATCATAATATAAAATTTTTGATTCTGAAAAATTTTATTTTCTTCCAATGCTTTTATTTTGAGATGATTTAATTTGAATAAAATTAGGCATTCTTATTTTTGGAATTGCGATTTATAATTAATAACAAAGAATTGTTTGTATAATCAATAGGAAAGGGACATCATTATTTTTATATCATATCAAATAGGTTAGCAAAATAGTTTATTTCCAAATTAATTTTTTTTTTAATAAAATGCTTGATAATTATATTAATTTTTAATATTTCCTAAATTCCGTTAACTAATTTATTCAACAAAACAAGATTAAATAATGATTACATTGTGTTACTTAACTTTTTAGAAATGAATTATCATTTTACATATTTCGTATGCACATTGGTGTGAAATGCGACAAGAAGTTTCGACGTTGGATCGGGCAGCGCAAAATTTCAACGGGTTTAAATTTATGAAGCGAGAAAATAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-3.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-3.txt
new file mode 100644
index 0000000..556cbe9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-3.txt
@@ -0,0 +1 @@
+ACGACTATGATGTTTTCTCAGGTTGAAATGGAAAAGGAAAGGCATATTGTCCTTGGAAGAGTTTGTTTCGACCGATTGGAAAAAATGTTGATAAAATGTCGCGAATGAAAAAAATTTTTTTCCACGAAATAATACATCAGAATTATATTTTTAAATACGTCATACGTCTCCTCGATCGGTGTATTAAAAATTTCGATAAAAAAAAAAATGATCAAATATCGCGATATATGTTCTCTCGATTTACAACAAAAAAAAAAAATATATTTTTATTCCAAAATTTGAATACATTTCTGGAATACGTCCATATCAACAAAATATACCGTTTCGAGAAGCGCAATTCTAAGTTTAATATCGTTTCGAGCAAAACGAGCAGCGTAGCAGCTCCCATTAGACGACGTGTAATTTTGCTTTATTCGTACTTACAACGGTTGAACACTTTGCAAAATGTCTCGCTATATTTTGTTATATTGCTGAAACGACACAATGCAAATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-30.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-30.txt
new file mode 100644
index 0000000..26bfc54
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-30.txt
@@ -0,0 +1 @@
+GTTCCTCTATAAAAATAACCTTTATTTCCAAACCATTGTACAACAGTGTTATCAAAAGTTAATAAAGGATCGACATTTCTTCGTTTTAAGAGAAAAAGAGAGAGAGAGAAAGAGACTGTTACAGGATATTTCGAGCATGGACAAGCACTGCTTGTCGATAATTACCTATGAAACGGTTAGTGTTCTTTCGATGTGTGCCCGTAACAGAAACGTATTGTCGACTCGTGGAGAAGTATTAATCGATACGCGGTTTAGATATATTTCGATTTATAAGAGAGGGTGAAATTGGAAACAGTTTTCATTCGATTGTTATATTCGGTTATACACTCTTCGGTTGGACACGCGTTATTCAGACTCTTGCAATCGTTTCTCCTCTTTGATCAGCTCCAGAATCCCGATTCTCATCTCGGAAACCACGTCGTTCTCGGTCAAACCCAGCCGACGTTTGTTACTGATGTCGAATACGCCTTCTTCGCCCTCGGTGTGCTCACC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-31.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-31.txt
new file mode 100644
index 0000000..8d52e79
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-31.txt
@@ -0,0 +1 @@
+TCCGGACGTCCAACAAATAGTCATGAAATTCTATTTCCGACGTGGTTAAGACGGGGGTTAATACAACATTCTACGCGACCTCGATCGAAAATGATTGGAAAGTGGGGGAAATCTGCCCCTTAATTCCGTCTGTGATTCCAAACGATTTTTTTTTTCAAATAGCCTTTTATCAAAATTTTAATCATCGAGAATACGAGAAAAGAAAATTTGATCGATAATTTTGATATACGGGAAAAAATCGTTATGTGGAAAATTTTGAATCTTAATTTGGATAATTTTAGTTATATCGAAAATTTTAATTGTTAATAACATTAATATTTAATATTATAATTCGTTTCGTAACTCGATCGAGAAAAAAGAAATTTAAGAGGTAAGAAATTTGAATTTAATTATAAATGGATAATTTTAGTTTCATTGTATCGAAAATTTTAATTATTAATGAAATATTCTACGAAAAAAAAAAGAAAAGAAATTCGAAAGAAGAGAATCTTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-32.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-32.txt
new file mode 100644
index 0000000..1134656
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-32.txt
@@ -0,0 +1 @@
+AGGGGGGAAACGAATCAATTATACAAGGAGTCTTGACTGGTTCGAAGGAAGATTCGTCACGTTAAATATTCCTATTAACAGGTGGAAGTATCGAACATTTGAATCAGAGATTTTTGGACTCGATAAAAAAGATGTTTGAACAGGGAAGTAGAACTTGAGGTGATAATGTTAATATTCCATGGTGGACTTGGATCCATTTGGACATTTCGAATCAATTTTATTTTTTTCGAAAATTGTACGAAAATTTCGTTTTTGAAAAATTAATTAACACTATCGTGGTATTAAAATTCTCTAGCTTTATCCTTAGAGAAAAATTAAAGATAAATAAAAAAAAATATGGATTAAAAATTAGTAATTAAGTAATAATTTATTGGATCATAGAGGATAGAGAAATTTGTATAAAAAGGAAAAAAAAAAGGAAGAAAAATGGATGCAGTGTATGTGAAAGCAATAAAAAAGCAATGCTCGTTGATGAATAAATGGAAAACGTAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-33.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-33.txt
new file mode 100644
index 0000000..d28e02b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-33.txt
@@ -0,0 +1 @@
+ATAAGAATACATAAAGTGTGTAAAAAATGAATAAAATTTGTTCGTTTAAAGCTAAAAGTGAACTATTTCTCAATTTTAATATATGTATATATATGTATATCCAATTCCAATTTAAACGAAAAAATTCTACTTATTATTTATTAAAATTTTAAACTACTTTTTATTACACTCATCACCAAAAAAAAAAATATGAATTATGCAAATAATACCTCTTGGTATTTTGCATATGAAAAATTTAACATGAATCTATTAAAAAACTTAAAATTCTATTTCTATGAATTTTAAGATATCTGGTATAATGCATTGAAATTTTATTGCCCAGAATACGTTTCCTTTTTTTCACGAAAGAAGTTCTATATTAAATACGGTTTTGGTCGATAACAGGATTAGATTTCAAATTCGAAAGTAGCGCGACATATAAACGAGAAACCGTTTAATTTTACTGTTTTATTAATAAAGTTGCAATCGTCGAACATCTTGTATCTCGAATCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-34.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-34.txt
new file mode 100644
index 0000000..049d52b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-34.txt
@@ -0,0 +1 @@
+CGAACGTGCCCGGGTGAAGGGTCGACAATTGATTGTTGTTCAACTTCAAACTTTCCAGCAAATCCAACCCGCGAAACCCTTTCGCCTCGATTTCGACCAGTTGCGTGTGAGACAAGTCCAACTTCACCAAATTCGGTGTACTCTTGAACGCGTGCGAGTGGACCCTCTTCAGGGGATTGTAGGAGAGTACGAGGTCCCTGAGGAACGGCGTGTCCAAGAAACTGGCGGTCGGGACTACGGTTAAGAGGTTATGGCTTAAATCGAGTTCAACGAGATTCGTAAGGCCCGCCAGTGCCTCGCTATCGATTCGGTCGATACGGCACTCGCGTAGGTAGAGACGCTGTAGATTCGTCAACCGTACGCGTACGAAGATGTTGCTCGGCAGAGTGCGAATGTCGTTGCCGCTCGTGTCCAACACTTGCGTCTCCGGATCGACCCACTCCGGTATGCTGGTCAGGGCACGATTTACACACTCGACCGTGCGCTTCCCGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-35.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-35.txt
new file mode 100644
index 0000000..42875c1
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-35.txt
@@ -0,0 +1 @@
+TTCATCTATTTACTTTCATTGATATTACAATTACAAAAATTCCAATTTATCATAGATCATAGAAAAATCAAATTCTATTTTTAGAAAGTTTCTAGAAATGTATTTAAAAAAAATACAATTGTTTTGTTAATATAAAATTTCATCTATTTACCTTCATTGATTATCCACAGGTGGGGAAGCTACTATGGTATTTTCAATTAAATCAGAAATCCATGGTTATTATCTTGATTCTCAGATATACTTCCCGATATCTCGAAATTTGCAACATGCTGTCGCTGTTTCTTTGGATGCAAATTATATATATTGGTCCGATATCGAGAATGGAAACGAAGCAATAATCAAAAGTTTGGAAGATGGTTCACAGCGAGAAGTTATTGTTACAACAGGTTAAATTATATTAAATTTACACAAGATTAGATTTAAGATTAGATTCCAATTTTTATCTACTACACTTTATTGCAGAAATATTTCTATATTTATACATTTGTGTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-36.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-36.txt
new file mode 100644
index 0000000..481d5b6
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-36.txt
@@ -0,0 +1 @@
+AAATACAATTTTACCTTGGTTCCTTTGCTTTTTCTTGTCAAATACCACTACTATTTTAGATTCTTGGAGCTGCCAGTTATAGGGATGTTCTACGATAAATTTCATCAAGGGGAACTACTATTTTAAATTTCATTCAGCATTCTATGTGTTTATATTCAAAGGAATCAGGACCTGCTAAACTGGAGAATAATGGAATAATGGATGGCATATCAAATTATTGTTTGTTATATACAACTTATAAAATCATATATATTATTTACTAATTTTATCAAATGGTTATTCACACGAATGTATTAATTATCGTAAATAATTGTCAACAAATTTTACTAGTCATATGTAAAACCTCCAAATCATTTAAATTTTATCAAAATTGAATTTGTAAATTTTATTATCACCTACAATTAAGATATTTTTTATATTATATATATTATACATATATATTATGTTAATCAGGAAAGATATATTTGCAAAAATGACTGATAATTCTTTTGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-37.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-37.txt
new file mode 100644
index 0000000..51aa0e3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-37.txt
@@ -0,0 +1 @@
+TTTGTCAGAATTCTAGAAACATTTTATCGTTTAGATTTTTATATAATAAATTTAACACATCATTTTCATATTTTTTCAGCATTTTTTTTTTTTTGAAAAAAAATCAAAATTACCTAACCTAATTATCTAAAATACCACATGTTAAATACTAAGTGAAATAGCGTAATCAATTACTCATATCCTCTTACCATCATTCAGAATGTGGCGGCCTTGCAAACTCTTTGATTAGCAGGTGATCATCGTTTAGAATGGCCAAGCCATTTTCTTGATGAGAAATTCTAGTAGTTGCGTATTATTTCTTTACTAGGGTAGGTCTGAAAGTTGATATATCAACAAGGTGAAACGCGCCATTTTTCGTGATATTCGCGTATGCTTTTTTAGGTCACGTGACTCGAGTACGGTCGAGGGATTTGCGATAGGAAATGAGATGACTGCGTGCTGAAGATTTTTTGACAAATTTAAAAACTGAATTTTTAATTAGAATAAAATGAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-38.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-38.txt
new file mode 100644
index 0000000..ebe839c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-38.txt
@@ -0,0 +1 @@
+TTATGAATTACCAAAGTATATACCAAGTATATCTATTTATATCCTATTTATAAAATGAATTCTTCTATCATTTATAACATTCAAATAAATTATAAAATCCATGGTTGTTAAAATAATGTGGGAATAAAAAGAGAATTAATTTCACAATATATGGTGATCAAAAATTACAAGTATCAACTTCGTTAAAAAAAAAATTACAACAAAAGTAAATATAAATTTCAATTTCATTTACAGCCTCGTTTGCACGAGACAGGTGGCAAATACGGAAATGGTGTGATAGTGAGAAAGTATCGAACAGGCTCTGTGATACCGGTCCGTGTGGAACTCACGGCGAATCACCACGGTTACTTCGAGTTCCGTACGTGTGCGATGACCTACAAGGACAAGGAAGTGGACCAAGATTGCTTGGACCAACATCTGCTTCGGTCGAAAAATGGATCGATCAGATATTATCCGGGGCCAGGAAACAAGATCTTCGAGGCTTTCTACAAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-39.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-39.txt
new file mode 100644
index 0000000..399734a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-39.txt
@@ -0,0 +1 @@
+GTTCGCCAGGGCAGTTTTCGTTTTTGCATTAACGACCAATTGTTGGTCCGGTGGTATTTGGGCGTTATTCGATAACGAGGTCAAAGAAGGCTCCTGTTGTGCACCCTGTTGTTGCTGTTGTTGTTGCTGCTGCTGCTGTAGATGCTGCAATTGTTGCAACCTTGCCATTTGTTGCTGCTTTTGCATATGTAATTGTGCTATGTGTTGAGGTGTCAAAGGTTGACCTTGCTGAAAGGTTTGACCAGGTTGACCAGCTTGTGGACCTTGCACATTTTGACCGGCTTGCACAATCACTTGATTAGAAGAATTAATCGGCGTTAAAGCAGGCGGTTGAACTCCTGAATGAACTGGTTTTTGACCAGGAGTTTGGGCAATTTGAGGTCCTATCATTTGAGGACGCGGTTGCTGAATCCACTGCAAACCACCAGGTTGAACTGGCTTTCCTGGACTTCTTATGACAGCGATATGCGCACCTTGTTGAGCTTGACGATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-4.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-4.txt
new file mode 100644
index 0000000..debfc54
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-4.txt
@@ -0,0 +1 @@
+TTTCACCAATGATACGATATTCCATCCAAGAAAAAAAAAGGTTTCCTCGAACAATTTCACAAAAGCAGACAAAAAGGCACAAAAAGAACAAAAAGATACACAAGAATGAAACATTTCTATCTTAGAAAGCTGCTATTCATCAGCTTTAGAGGGGGAAAAAAAGCTTAAACTGAATTCACACTTTCTTACAAATTCTTTTCTTAATACATAGAATTGCAGAGGACAAGAGATTTATTTACAAAATATTATATAGAGAGAGATTGTCAATTGTAACAAAAATAAAAGAATTCAATCAATGAAAAAAAAGAATTATATAGCAAAGAAATATATTACATACAATCTGGTGATATTAGATATTATAAGATTTGACCTAATCCCTACAGAAATCCTTAATTAAAACTACTAAAACTCCTTCCATTAGATACACTAAAATCTTTCTATTAATTGTATGTTTTCCTTCTCTAGATCCACTAAAATCTTTCCAAAAAACTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-40.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-40.txt
new file mode 100644
index 0000000..3585975
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-40.txt
@@ -0,0 +1 @@
+ATTGTAGGATACATATGATTTTTATTTACTTGAAAGAAAAATTATTAAATTTTTTTAAATTTCAATAAATTAAAAATAAAATTATATAGAATATTATAATAATAAAATTATTAAACGTAATAAATAATAATAATTTTAATATTAATGATTTAATGAAATGAGAAGCTTCTTTTAAGAAATTTAAATTTTCTTATCGACGTCGAAATTCTTATAATAATGATTTAAAGGCCAAAATTTAAAAAATCGAGTACTATTTTGAGTTTTATTTATTTTATTTAGATTTTATTTAAGTTTTAAAAAAATTAAATTAAAAAATTAATTATTATGCATGTTATTCATGTTTGTTTATTTATGATTTATATAATTAGTATAAAAAATTAACTTAAATTTTGAATATTTTTTAATTTTTAAAAATATTTTTAAATAATAGTTTAATAAATATTTATATATCAATATATACATATATATATATATTTATTAATATAAAATATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-41.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-41.txt
new file mode 100644
index 0000000..369ea36
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-41.txt
@@ -0,0 +1 @@
+AGATTTCCCTAGATAGGAGATTTCTCGATCTAGGGAAATTTTGTGAAACGAATGTTCCATATTATTTTCCTAGATAGAAGATTTCTCGATCTAGGGAAATTTTGTGAAACGAATGTTCCATATTATTTCCTGATGTGCAACGAATTTTCTTATTAGTCTTTTCAAACTATCTTACAGCACAAAATGTTGAAATTAAAATATATAGTATATTATTATAATTAAGTATATTATATTATAATTATTTAAATATATAAGTATATCATAAATGTATATATGAAAATACATAAATGAATTTATCACAGCTTTCTTTAACCCATTTTTGATTTAAAAAAAAGGAAATGTAACGAATCTTAAACATCGATGTACAAAAACCAAAAAATTTACGCGACTCGTAAAATTCCTCTAATAATATCTGCTCGATCGCGCAAGAGGGAGCAAAAAACTATCCACCCACAGAGATCAACGTATCCTCCCCCCTCCACTTATCTCTCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-42.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-42.txt
new file mode 100644
index 0000000..56716b5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-42.txt
@@ -0,0 +1 @@
+CAATCTTCAACTTTAAGCAATTCGCACATATAATTCCCTAGCGTGAACGAGATAAGAATCATGGCGAAATTATCTAAATATTAGGATGAAGTTGTGAGCAAAGAAAAAGAAGTATTAAGAAAAGGAGAAAAGACAGGGAAAAATAAAAAAGAGTTTGTTAGATAAAATATCCTTTTCGAAATATGATAAACTTATTTTAAAAATTTTAAAATTAACTTTCTTTTTTTTTACGACTAGTAAAAAAATATTTTTCTTTCTAATATTCCTTCTCTTTAATTTTCCTTCCAATTTTTCCAAAAGAATAAAAAAAAGGATAATGGATAAAAGCATAATTTCTTCTTGCTCATAGATTGATCATCTTCGACCAATTTTAATTAACAGATCGGAAAACCATCGTCGCAGCACGATTTTTTATCGAGCAATAAATTTCCACTGTATTTTAAATGTATCGTGGCTGAGAGACCCACCAGGGAGTTATATATCACACGAAGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-43.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-43.txt
new file mode 100644
index 0000000..dc91b12
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-43.txt
@@ -0,0 +1 @@
+TTGAATATGCGTGCGTTGGCATGTAATGAATAGTTGTTGGATCAGTTGTTGAATTTTGGAGGAAACTTTATTCGTATTCTATATATATATATATATATATGGGGTTTGGGTTTTTTGAGTTTTATTTGGATTTTTTTGAGTTTTATTTAAATTTAAAAAAAATTAAATTAAAAAATTAATTATTATTTAATTATATAATTATATAATATATAATATAATAAATAATATAATATAATAATAATTATATAATAAATATATATATATTATATATAAATTAATAATTATTATGCATATTATTCATATTTGTTTATTTATGATTTATATAATTAGTATAAAAAATTAACTTAAATTTTAATATTTTTTAATTTTTGAAAAAATTTTTAAATAATAGTTTAATAAATATTTATATATCAATATATTAATATTAATATATAATTATTATATAATATATAATTATATATTATAATATATTAAATATATTTATATTTATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-44.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-44.txt
new file mode 100644
index 0000000..03b8df4
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-44.txt
@@ -0,0 +1 @@
+NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-45.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-45.txt
new file mode 100644
index 0000000..c719724
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-45.txt
@@ -0,0 +1 @@
+TCGAGTCGGACACGATACGCGAGTGATACGAATTAATAATGGGCGAGCGAGTGAACAGTAAGTAGCGTAAGACAGTTCGAATCGGACACGAGTGATACGAATTAATAATGGACGAGCGAGTGGAACGAATCGAGTGTTGATGGTACGTAAGTGGTACAAGTCGATCAATTTGGCCAATGATAGATCAATTTATCAATTTATCAAACCAATAACATACGATTCGGAATTTAGTGGATAAGTGATATAAAATTAATTGTTGGTTGAGCTAGTAGCATAAATCGATTATGGTCAAACGTATAATAGAGATCTTTTCGGCTGATTAATTAATAATAATTGAAAATACGCATTTGTCGAATTGTAGAAAATCGAGGAAATGATTATATCACGCGGTGTTCCAACAAAGGGGGGAATTTCTGTGAATAAGCCTGGGCTGAGATAAAATTCATTAAGTTTCGCCTCCAACTAGGGAGTTCTCGAATGGAACGGTTTCTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-46.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-46.txt
new file mode 100644
index 0000000..7012c0d
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-46.txt
@@ -0,0 +1 @@
+AGGAATAATAATGTTTTTTTCAAGAGATAAAAATTTTGTGAAATAAAATGATTGAATTAAAAGTGAATTATCTAGTTTAATGCATACAGATCATGAATAAAATTGGAGTGGCACGAAAAGGAATATTTTGTTCGATTAAATAACGGAGTACATTTGGAGAATGGAAAAATGTAAAATAATTTAAAATAATTTCGTATAAATAATTATTTCCGTTTGTGTGTTGCGTTTAATTGAGAATGGATTAAAAATTGTTTGGTGCATTTCAATCGATAAACGATCCATGGTGAATATAAAACGACTATTTTTTCTTTTTTTTTTTTTTTTTTTAATTTTGCGTAATCTTTCCATGCTTTGATAGAATTTTGTATTCTTTTTTTTTTTTTTTTTTTTTTTTAGTAACTCAAGGATAAATTGAATTGGAAATATTCCATAAAACATTTTTTGGCAAATATTTACGTNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-47.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-47.txt
new file mode 100644
index 0000000..b5fc46b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-47.txt
@@ -0,0 +1 @@
+TTGAAAAATAGAACTGTTTCGATCAGTGCAGTACGTTTGGGAAAAAGCTGCTAAAGTAATGAAACTGGAACTCCAAATATTTTTAGCTAAAAGAGACTATTGATGATTCAAAATATATGTGTGTTTACGATGATTCCAAACGGTGAAAAGTGAGAAAATTTTCGATAAAGCAAATAGTGGAAAATAAAAAAAGTGATCAAGGAATGTGCAACAGTGGAAAAAAAGATCAAAAAGATCAAAGTGATAGTGACAAACGTCGGTGATCAACCGTGTAATCCATTTAACCGTGGAAATTAGTCATTGTAGAGAGGAAAAATGGAACTGTTGTGATTACTCGTTATTTCCTCTCAATTGAATATTGAGAACAATTGATTGATAATCAAAAGTGGAAAATTAGAAAATTAATTGGAACTCATTATTAACTGCTAATGCTTTTTGTTTCTTCGCCTATTTGGTAAGTTTTTTTCGAATATTTTTAAATAATCAATCGAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-48.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-48.txt
new file mode 100644
index 0000000..97add7c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-48.txt
@@ -0,0 +1 @@
+ATAGTCAACTCTTGCAGACCTTCATTTGCAAATTTGCCAACTTCCGATTGAATAACTGACACTCAGAATTTATATTTTTTTTCTATTTATTTAGTATGCACGATTTTCAAATCATTATATCCCAAATATTTAAAGTTGTTTGATTCTAATAATTCTATTGAAAAATATAGCAGATTCTTGTAATGCATATTTAATTGAGCTTTCTATTTTCATGAATAGAGAAATAGAGCCAAAGTCCTTTTATATATTTGGATATTGCAATAAATATACTATAAGTTAAAAAGAAAGAAATTAATATAATCCCTCTTCTATTAAAATCTTATATTTCAAAACAGTAATATCAAATTGTATTAATAATATAATAATTCTTCAATTATGATAAAATTAATATTGGAAAAATTTTTGTCTATTTCATATATATAAAATATTTCGATTATTACAAAGTAATAATTTTAATTAGATATACAAAGTATTATAGCAGCCTTTCAATCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-49.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-49.txt
new file mode 100644
index 0000000..48948f3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-49.txt
@@ -0,0 +1 @@
+TTGAAGAGAAAAATGTTTTGATTTATTAATTTAAAAATTTTTTTTTGCAAAAATATGCAAGGATAATTTACTTACGCGTTCAAAACAATATATAGTCGAAGAAAGTCTACCAATGAATTTTTTTGATATTCCGAAATATCAAATGTATTATATAAACATTCTGATAGAAATTGTACCCAATCACCAGTATCTTCTTTCAAATTCTTTAATTCTCCTTTAGCTATTAGACCGACTATTGCTGGTAGCGTTGCTAATAGTTCCATGCTATCTTCGTATGTGTACTTCAAAAATAAAAAAAAAATTCCAATTTTCTACTTATCAAATAAATAAAAATAAATATTTTTTACAAAATTAATTATTCATTCTAAAATTATGCAATTGTCTATTTTTAATAATCAGAATGTAATAAAAATTTTATAATTTTATTATCACTAAAATATTCTTTTTTCAATTTCCAGAATGTATTAATTTATTAATTTTCAAATTAAGAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-5.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-5.txt
new file mode 100644
index 0000000..894c699
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-5.txt
@@ -0,0 +1 @@
+CACAGACAAATAACGGATAAGATAGCAATGATGCAACTGACGAAGACGGCTGATCGTAATTCCGAAGTTAGACATAATTCTTATCCTCAAAAGTAGGGTCAGATTTATTCCGTTTTATGGAGATAATTAAACGAACGGTCATTGCCATTTATCTGTTATAATAAAATTGATTAAAATTCGATTACACAATAGCTGTTTTGAACTTAACTTTTATTTAAATTTTTGATGACATGTATAATATCGATTGATAAAAATTAATAAATATAAAATCATTGTAAATAATTCTTCAAAGTTAATCGATAAAAATAACAAAGAGATATTCAGAATAAAACTGTCTTTATATATTTTTTAATAAAACTTGCGAAAATGTTCTAGTTAAAGATTTAATAACAATTTCGATAAAAGTTCAAAAATAATAGCAGCTATTTATCATTCGTGTGATCCAATTTATTTAAAAAATGCATTCAATGTTGCACACGTTATGTCACAGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-50.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-50.txt
new file mode 100644
index 0000000..0974f20
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-50.txt
@@ -0,0 +1 @@
+GAGATAACTGCTATTTATATGTATCTTATGTTTAAGGAAAGAAAGGATGCTGTAATAGAACAGGAAGACCTATTTATCATACGACGAGGCCCAGCCGATCGAAACTGCTTTCCGGTAAAGCCACACTCCGAATGGGCATGCGCATTAAACAATCACGTAAGGATTTATGTTAAACAATAAAGTCGTATCGATTCACCTGTTTCTTCTATTTTTAATATCCAAGTCTCTCCAAGATAAATTTTCTTATTCCCATTTCTTTTAAGTGCAAAAAATTTTTTTTTATCTTTTATTTAAAATATAAATATATCACATAATTTTTAACTTAATTTTCGAAATTGTATAATTCAAAACATTTACTTGGATATAATAAAAATTTTGATTAGTTTGATCGAATAGAATAAATTGTATGGAATATTATATTATAAATATTTATAATTGTTTTATAATGTTAATATAATTGTTTTAATATAATTTTTTAATAATAATATTTTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-51.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-51.txt
new file mode 100644
index 0000000..ad6515e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-51.txt
@@ -0,0 +1 @@
+TTTTAGAATTATCTGAAGAATTAATGGCCTAACTCGATTGTGCCCTACCCTCTTCTTCTTAACAAGTTTCTTTCAACAAGTTGTACAGATTTTTGCGTGACAAACGGACTAACCACAAACGACTACGTTTTGTGATTCCACTAACTTTTCTGAAAAGACCACAGATAGAAAGACCATAAACGAAACCACAAATAATAGAAATTATATTTCTATTATATTGGCCATAAATTACAGCAGATTTCGTTATCCTTTGATCACTAAAAAATGCTAGATATTTTAATATTATCTATAAATTGATCACAACTCGTTGAAATTTTAATAAATTTCTCAATTTTGGAAATTCTTTCAACTATTTTGCATGCAATTCTTCAAACAACTGCACGAATGTAATCTAAAATAATATCATTTCTAGTCTCAAATAGTGGAAGAGGAAAGAATATTAGATGAAAAATCTTAAAATTTCTCAATCTTAATCTATTCAATTCTTATGCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-52.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-52.txt
new file mode 100644
index 0000000..d740d62
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-52.txt
@@ -0,0 +1 @@
+ACATAACAAGAAAAGTTCTAATTCGTTTTAATAAACCGTTTTAATACTAAACAACTGTTTAGTATTAACCTCTATTTCAGAAAGAAGTAATTACTCAGAAGAGAAAAATGGTTTTAATCTTTAAGTTTCGAATATATGTGTTCTTGAAAAAATAAAAAAATTAATAACTTTAGTTATTTCTTCTTTTCTTCTCAAATTTATGAACAAAATTTTTAATTATTTTCATTCTTTTTCTGCATTTTTCTACCTCCTCGTTTCTTCCATCAATTTTTTCTTATGCTCACAATTTTCTTTTTCTTCATGATGTACATAACCACAATAACACGATTCTTTTTTTTTTTTTTCAGTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAAAAGAAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-53.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-53.txt
new file mode 100644
index 0000000..dc87d23
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-53.txt
@@ -0,0 +1 @@
+CGCGAACAACCCGTTACACTCGCGGAAAATCCATTTCGCGATACTTTGGTAAGTAGAAGGTTAGAAATTTTGGTGAAAAATTCGATGAAAGGGAACGAATCCGTGACTCGTGACAATAATGGCGAGTTGCGTTTTATTATGCGTGCGCATCGTTAAATTATGTTATACGAATTCCACCATTCGGCTGGACCGAAATTTATGACGTGCAATATGTACGCGTGGGACGTGAGATGAATCGTTCGTGATAAAAATGAAGCGATATTATTGGATGAAAATAAAAAATGGGATAGTTTGTACAGGACGGTTTCTGAAATGTTAATATTTTAAAAAGTGAATTTACTACAAAGAAAAATGAGATAGGAAGCTTATTCGAGCAATACCATGTAATAAAATATTCCATTGTTATAATTTTTCAAATTTTTTTCTCTTTCGATACTCGGATTTTTTAATTATGGTATAATGGTAATAAGATTAAGATTAAAGAATTCGTTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-54.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-54.txt
new file mode 100644
index 0000000..9989740
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-54.txt
@@ -0,0 +1 @@
+GCTTGTTAGGTGTTATTTTTGTGATTTGAATTTTAGCTATTATTTTTTTTTTTTTCAAATTATTCTGATTTGGATTATTCTTATTTTTACAAGTGAAATTTATTTACATAAATGAAATTTTAAAATCGTTTCATTTTATAATTCGATTTAAAACGTTTTAAATCATTTTACAAATCATTTTATATTCTATTAAAAGAAAAAATTGGAAATATTTGCATAAATATCTATAAATAACTGTAATAAATAACGATAGATTTTATCAATGAATATTTCTTTTTATTATATAGAACTATTTGCAATATTGATGTATTTTTTTAAATAAAAATGGATAATGATTTCTTTTACAAGATTATATTAGATTATTAGATTATAATAGTTAGTAAGTTTATATTAAGATTGAACTTATCATGTTCAATGTTAAACATGCATTTATATCATATTGCTAAGATATGCATTAATTTACATTGATACAAAAAACTGATTTTTATTTGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-55.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-55.txt
new file mode 100644
index 0000000..83e1094
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-55.txt
@@ -0,0 +1 @@
+TCGTTTGGGACAAATACGTTTTTGCGTCTTCCATCCGTATACGATTGCAATATCATTACAAGTAAATTGCATTTCGTATGAAAAAAAATTCACAAGAAACGTACATGAGAAAAATGTGCATTTTAATATCGTTAGCGGCCATTTTGAACTTATGAGTATCTCGAACTACGACGATTATCAACTTCGTCTTATAATTCCAAAATTGAACCTTTTTTTTTATGTCTTTCAATAGAGTACAAAAGCGTAAAGAACTTATTTATCATGTCATCTGAAAATATTTATTATTTTCTTTTTTTAAAATGAAATTCTAATAAGTGCTACAGAAAGCATTCCATTCATTTTATATTTTATATTTTAATATATATCTACTATATTATAATCTTGGTACGAATTTGAATACACGTTCAATTATAATTTGTTCATGACGAACGAAAGTAAATAACAAAAAAAGTAAATAAATTGCAAATTTTCTTTTATTCTTTATTATTTATG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-56.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-56.txt
new file mode 100644
index 0000000..cf630ae
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-56.txt
@@ -0,0 +1 @@
+TAATAATAGTTATCATTCGTACATCGAAGAACAAAATGAAACGTTATTTGAACACACCGATAGTACAAGTTTAAAACCTTTGTCCAATTTTACGATTGATTCGAACTCGGAACAAGAAAATTCGGATAATTTGGAAGATGAACAACGTACAAATATCGAAGAGGAGAATCATGAAGGATACATTCCATTGAGAACGCTGTTAAGGAACATGTTTGAGGAAATTTTAGGAAAGTATAATGGAATCGCAAGACAGTCAAATGTAAGTTCGATTAACAGAAGTTCAAACAGTGATTATTCAATGGATAATTGGGACAATGCAGATGAAGTAAATAATAATCTTTTAATTTGTCAAACACAAAAATCTAATAATATTGTTATAACCACTATCGATAATGTAAAAGATCCTGATAAAATTATTGCTTTATCTTCACAGCATATATTAGATTCTAGAAGCATAAATACAATATTAGAAAATTCCAATGAAAGTTCTAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-57.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-57.txt
new file mode 100644
index 0000000..865a6aa
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-57.txt
@@ -0,0 +1 @@
+AAAAAATAATTAGATACAAAAATGAAAAAATTATATTGAATACAAATCTTCTCTCTCTATTACTAAAATATTACTAAAATTACTAAAATAAATTACTAAAATAATTACTAAAATATTACTAAAAATAATTACTAAAATATTACTAAAAATAATTACTAAAATATTACTAAAATAATTACTAAAATAAATTACTAAAATAATTACTAAAATATTACTAAAATAAATTACTAAAATAATTACTAAAATAAATTACTAAAATAATTACTAAAATAAATTACTAAAATATTATTAAAATAATTCGTTATATAAATTTGTGATTATCTCATTATATAGATATCCAAATTTAAAGAAAAAGAAAGCATTTTTTGAGCATTTCAAAATCAAAGATTAAGAACAATAACAGCTGATTCTCCACAAAGATGTAAGAATAAAATGAAATACGTAAAAAACAAATTCAAAAATTTATCATACGTTCAATAAACGTAATAATAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-58.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-58.txt
new file mode 100644
index 0000000..fd51750
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-58.txt
@@ -0,0 +1 @@
+TATCGAAATAATATTAATCCTAATTTTAAACTTGATTCACTTTATTAAAATAATAGATCTCTATCTATAATAACGTATTCAATTACATGCTGTAACAATTTTCCATGAAGCGAGGGTTATGTTCCTCTTCCTATCACGTTAATGTTTCTAGTTATCTTCTATCTCACTAATCATGTCCTGATAAAACCGTAGATTTATTAGACTTAAAACTTTCTCAAGATACCGTATAAAATTCAATAATAATTATTTTTATTGGAAGAATTTAAAGAGTACTATAAAGTACTAGAAATTATAAAAATAAAAAAATATATATATATATTTTCATTAAAACAATAAAATTTATCTAAATTACCATTTTCATCACTGTTTAATCTAATAATAAAATTTCACAGGATATCTTTATCACTTTTAAATATTTAAAATAGTATAAAATATAAAATAATGCTTTTTGTCAACTTAATATTACAAATCAAAGGAAGTGAAAATAATTTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-59.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-59.txt
new file mode 100644
index 0000000..f3fcf7b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-59.txt
@@ -0,0 +1 @@
+TGTTAAATAATAATTTTTTTTTAAATAGATATTAATAAGTTTATATAAATCAATTCTTTAATTCTGAAAATTAATTTATAAAAATCTAAAATTTCTTTCTACACAACATAGTTATAATAATATATTTTCTTTTATTATTACAATTATACTAGAATAAAAATAACTATCAGTAGGAAAATATTCTTTATTAAATTCGATATAAGAGAGAATAGAAAAATTATTTTAACGTTTCATTATCTATCTTAATCGAGAACTAAATCAGAAAAAGTTTAATTTGAAGAAAATTAAATGATAAAAAGCAGATTGAACGGATAATGAGAATCCCTAGGGATGTTTTTTCAAAATCTTCAGTCTTGTAATCATTCGACTGCGAACATTGGCGCGCATGCGTAGAGCTTCTCCCCACCTGAGACGCAAATACACAGAGTCAGTGTAGAAACGGTCTGGCTTTGCCTCAGTATTTCGCCGCGTCTTGTTTCGTGAATATTGTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-6.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-6.txt
new file mode 100644
index 0000000..4e92b72
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-6.txt
@@ -0,0 +1 @@
+TAAAAAAAGTAATAATTATATATATTAATTTTACCTCATTAGTAAATTGTTTCATATGATTCAATACAGCATTCACATCTGGCATTACATCTTTATTATCAACCAATATTGGTTTATTAAGTCTATTACGTAAAGCAATATGTAAAACTGCTCTATTTTCTGTAAAATTAATTTTTTCTCCTTTAAACATAGCATCTCTTGCAGCTTCTACTTCACGTGCTTTTGCCTTAAAATTAATAAAATTTGATAAAATATATTAAAATATATTACATTTATATAAATTTTTGTAATTATACAATTTTAATAATTTCAATACATACCAATTCCAATAGTAATTGCAATGCTTCTTTAGTAAGACGATTTTTAGAATAATCAAGTAAAATTGGTCCATCTTCAGGAGTAGAAATCTCTAAGCTAATAACAATTAATATATTAAATATTAATAAAATACTTTTTAAATTAAATTTTTTAATTTCATTATAATAAAGCTAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-60.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-60.txt
new file mode 100644
index 0000000..aa8b773
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-60.txt
@@ -0,0 +1 @@
+AGATTTTTTTAGAAACTTTTGTAATTTCCAATTATATGACATATTCTATCAAAGATATAAAAATGAAAAAAAAATTAAAATCTATAAATAATATTAAACAAAGATATACATTAGAAAAGAATAAATCATTCTGTCCTTTCAAATAAGAAATTTTCAGCAACATCGATGTTTAATAGTACAATTAATTCGAACGAAAAATGTCGAGCGAAGCGTTAAATAGACAATAGAGAAAGGCGCATTAATTCGAAAAATCAAGCAAAAGAGAAGCAATTCGGTTCTGTGTGTGCGGATTGTTTGATTCTTCTCGCTAGCCTAGAGGTTCTTTTTAAACGGGGTCGTGACCCAATTTATTTAACCGGTTGAAAGACAGAGGAGAGACATTTTAGCCTTGGATGCAGAGCAATTAAGTTCTTATTCTTCTCGGTGGTGTGCACTTGCCACCGATTGGGCATCGTTGAAACCAATTTCGCTGTATAAGAGGACGCTATCCAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-61.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-61.txt
new file mode 100644
index 0000000..86411be
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-61.txt
@@ -0,0 +1 @@
+AGTAAAATTTATGGATAAAAGTGCAGTTTTATAATCTTATTAAAATAAAATTTTTGACTCAACAAACTAAGCAATTTTATTTAAATGTCTCGTTTTTTAAATTAAATTATGAATTTTCTAAACTATTTGGTCATCGTCTACATTTTTAATGCAAATTTTATTATCAATGAAATTGAAAAACATTCTTTCAAAAAATATTAAGAAAAAAATTGCAATTATCTATAAAGAATAATAATTAATCTATTATTAATCTAGATTAATTATTAACTTATATACATAAGTTAAATTAGGTTCACAGTCTTTAACGTATAGAGATTTTAATCATATAGAAAGAACAAAGTAAGGATCTAAACGATGAAGAAAGTTTGTTTTATGGATGAAATATAAACTTCTTTTTTATTATGTAATTACTTCATAAAAACTTTTACACAGTTTCACTTTATGGAGATTGATTGTTACTTGGCTATGGCAAGATGTATTGCAATTTTCGCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-62.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-62.txt
new file mode 100644
index 0000000..df52eec
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-62.txt
@@ -0,0 +1 @@
+GTGCAATGCTGTTGGTGTTTCTTTTATCATTTCGTGTTTCTTTGTCGATCGTTGTTTCTCCCCATAGGGTCTATCTTTGTTGGTGAAGAAACTTAAATTTTAATGTACTATTTATTTTAAGGAGTTAACAATTATTAGTAATCTTGAGAAGATAGTGAATGAATGAAATTTTATTTTTGAATATGTTCATTGATATTACTTTATTTTCATTGAAGAAACCTTTTGATAATTAATTTAAGATATTGAAAAATATTTTGTCATGAATATGATTTAAAAATTAAAAATTATTCTAAAAAAAGCATAGAATAATAATAGAAATATATATATATAAATGATAATAAGAAAGATTATTTAATTAATTAAAGATTTTATTATGTTACATTTTTAGCATAGTAGTTATTAATTTTTAGTATATTTATTTATTAATATATTAATAAATAAACAATTTTATGCATCAATTTCAAGTTCTTTTATTAATTATTACATCTTTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-63.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-63.txt
new file mode 100644
index 0000000..eab8f92
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-63.txt
@@ -0,0 +1 @@
+AAATTTAAATTAAAAATTAATTTACTTATTTAAAGATTTTAAAATATACAAAATAAATTGTCATTATTTTGATAAAAATGTTAATATTATGTATAGGAATTGTTGAAATAATTAATACAATTAAATTTATTAAATAATAAAATTCGTATAAAGCATTGTAAAAAATTCAAAATATGTTTTATTAATTTAAAAGTGAATTATAAATTATAAAATATTGTTATTGGTAAATTCATATCATGTGTTTTATTATATTATTTTATTATATTTAAAAAATTTATTTTATTTCTTTTCACATTGCATAAAAAGTATAAGATATATATATAATATATGTATAAAATAAATACATTTTATTAATGATTTGTATATTAAATATGTTATTTCGAGTAAATGTATGGAATAAGAAAAATAGTTAAAAAAAAATAATTATAAGAAATTTTCATTTTAAATTAGTTTACTTTATCAACAATTTGATAAAAATTAAAACAATATTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-64.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-64.txt
new file mode 100644
index 0000000..69d9934
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-64.txt
@@ -0,0 +1 @@
+TTGCTGCTTCTTCGCTAAGGCCAACACAACCATATTCTAAAGGACTAAATACTGTTGTTGCTACATTCACATAATCCATTTGTTCCGTCGAATTTCCAAATAATCTTCTTGCTAATAATCGACCCGCATGTATAGCAACAGGAGTTAACTCTGGTTTTTTCTATAATACACAAGAAGAATTACAAATAATTACGAATTATAAATAATAATTTTACTTTAGTAATTTATATAATATCATGATATAATATCATAATTTATAATGATAATTATAATATATATTTTAATAATAATATAAAGTTAAATATAATTTTACATATTGAATAATGTATAAAATTAAATATAAATTGTAATAAAGTAGAGAAAGAAATCTATTGTTGAAAAAATTGAAAATTTAAAACTCACATGAAGTACATCACCAACAGCATATACATTTGGAACATTTGTTTGCTCATCTATTGCATCTATTTTTGCAGTTTCAGGAACAAGTTTAAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-65.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-65.txt
new file mode 100644
index 0000000..dcd9cd5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-65.txt
@@ -0,0 +1 @@
+TAAAAATTTACATTTTTAATAATAAATATCGTTTTGTTCTTATAAATTTTTAAATTATTTTATTACTAGTTCGTATTTATACTGTATAGTGATTTATTTTTGCGGTTTTTGAAACTTGATTCTATTATTCATCTTGATGTGTAAATTTCAAACTATTTACTATAATTATTATTTTATTTTAGTTTTTATTTTAAAGAATTAATTTGATTCAAACTATTATAATAAATATTATATAAATAAATATATTTTAATAAATTTAAATAATACTTAATTAAATATTATTTTCAAGAATGTTATATATATAACAAATATTATATTCAAGTTATACACTATTTTATGTGAAAACAACCGAACGATAAAAAAGTAACATATATCGATAAATGAAATTTAAATATCAATGATTTTTTAAAATTATTTTATTTATAACTTTTCGTGATATATATCAGATTAAAACATAACTTAAATACAATTTAAAAATTATTTACAAATTAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-66.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-66.txt
new file mode 100644
index 0000000..ff7ab82
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-66.txt
@@ -0,0 +1 @@
+ATACGAATTATAATTAATAATCGCTGTGCGAGTGTATTAATTTTGTTATTGCTATCATTGTTATCAGTTTTTAATTATTCAACGCTTATTGTATTTCTATACAATCTAAAAAAATTTTAAAAAGAATTATTTAATTTTTAGAATTTTCTTTATCCTCGTGTGAAAGAGAATTTTATTTCAAGTTTTTTGTATTGACATACGTACGTGCGTAGACGACTAGTTAGGTTAGGTTACACTGGTGGTACAATTATATACGAATTATAATTAATAATCGCAGTGCGAGTGTATTAATTTTGTTACTGCTATCATTGTTATAAGTTTTTAATTATTCAACGCTTATTGTATTTCTATACAATCTAAAAAAAATTTTAAAAAGAATTATTTAATTTTTAGAATTTTCTTTATCCTCGCGTTTTATTTCATGTTTTTTTATTCGAACGTAAAAGTGAATCGAGACTGAGAAAAAGATCGAGCAACGCTGTAATATAATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-67.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-67.txt
new file mode 100644
index 0000000..92dbb38
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-67.txt
@@ -0,0 +1 @@
+AGGAGACCCGGTATTAACACAACTGTCTATTGTCCGTTGTTGCATTACCGTGCCACGTGTAATCCAATATTTCCGTGTAATTTAAACTCTTCTCGTCTGTGCAGTACATTGCACGATGAAGAGGTAATTTCTCACCATGATGTTAGCACATTCTCTCATTGCACAGAAAATTTCATTCGCGTTTTAACACCCTCGCTCATGAAAAGTCTCATGATATATCATATTATATTTCATCGAAAATTAAATCATTACGCGTATTAGATAATTTAAGCGATAAGGAAAATTCATAGCTCAACTTAACACTTTGAAATATTTAAAAAAAAAAATGAAATTTTATCATTTTCATCTTTCTTTACCTAAATATGTAATTCTTTTTTCAATATGTAAATGTTATGATAATATATATATAAATATTAATAATAAATTATTTTATCATGGATATTTTTTACATTTATTATTATATATTTAAACAATAGCATATTTTAATTAGGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-68.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-68.txt
new file mode 100644
index 0000000..a4a4486
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-68.txt
@@ -0,0 +1 @@
+TTGCAGCACACGGATTACAGCACGGATTTCCTTGCCGACGGCAAGCAAAAAGGGGGGGCGGCCCCCGTTTCAGGGAATATGGACGATTCTGAGATATCGTCGACGAAGGCCGAATTCGGGGACGAGTCGTCGGTCAGTTTCCTAACGGCTAGCGAATTCCACAGAACGGGCATCTCGGCGATAGACGAGTCGTTCTCGAGCTGCGAGCGTGAATACGACATCGCCAAAGATCCGATGGCAATGTCTTTCACGCCGAGCGACTTCGAGGCGGCGTTCGACAAGGGTGTAGATTTGAACGCCGTTCACGACTTGAGCAATACCGATTTAGAGGAAGCGAATGGTATTTTGGACAAAGAGGAGGAGGAGGAGGAGGAGGAGGAGGAGGTTATGGAATCTCCAAACTTGGAAAACGATACTGTTAACGACAAAATGTCGAATATTATGTGTAACGTTGTGGAAAAGGCAGCAGAAAAGTCTGAAATTCGCGCAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-69.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-69.txt
new file mode 100644
index 0000000..b526e95
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-69.txt
@@ -0,0 +1 @@
+TATAATAATAATTTAACAAAAAAGTTTGTAGTATTGTTTTTTAAATTAAAGCATTATTAATGTTTAGTTTCTAAAAACAATTTCTAAATTATTTTAATTGATTTTTTCATCATTATTTTAATATTTCATTTTTATGAATTAATATTTTACACAAATTTTTCAAAATGAATTTATAAATACTAATGAAAATTTTCTGAATACATAAATGATGAAATCAATATATATATTGGAAATATGTAGTAGTATCTATTATATGTTGGCTGATAAGTCTGTGAATAAGGGCGCTGAGCCATTTGCCTCACTAAATGTTGATATATATTATTGGTAGTAGTAATAAGTAAATTCAGTCAATTATAAATTATAAAAATTTGTGACTCGACTATATGTTTGTGTAAGTTGATTTATTTCATTAGACCTTATATTTTTTTTGATGTCACTTATCAGCAATGTATAATGTATGCAAGCGCAGTTATAAAAATTTCTATCATAATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-7.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-7.txt
new file mode 100644
index 0000000..62f94f0
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-7.txt
@@ -0,0 +1 @@
+TTAATAATTATAACAATTTTAAGTAAGTTATACAATTTGTAAAATAAAGAAAAAAAAAACTTGAAAATTTAGTTAATATTTCTCAATTAATAATTGAAATAAATATAATAGAACTTACATTTGTTTCATTTTTTAATTTTGTTAGTATAGGCATTAACCATATTGGCTTTTTATTGAGTATATCATTTTGTTGCTCATGATTATTTACAACTGAATTACCAACAATTTTTGAACATGATTTTGTGTTCTTATTATTGAATGAACAAATAGGTGCCTCTACATGATCAGAAACATTCCACTTCCCAGTATTTTTTAATTTTATTGATTTTTTCTTCTTTGCTTGAGTGATAATATTTAATGTTTTTACAGTTGTATTTTTATTTTTTTTTAATTTTATTTTTTTATTTTCATCAGTATTCTCAATTAATAAATGATTCTTTCTTTTGTAATTATTTTTTAAATTTGATTTATAAGTATTTTGATTCTGATTCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-70.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-70.txt
new file mode 100644
index 0000000..b2d4a28
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-70.txt
@@ -0,0 +1 @@
+AATTTTTCTTATTATATAATATATATAGGTTAAAAGTTATTATCTTGAAAATTAAGAAGGGGGAAGAGAGGAGGAGGTCTTCTGATACTTGAAGAGATAATTCTTGACTATTTGGCTTAATATTTTTCTTAATAATTGTAAAAATCGTGTCAATTTTTTGATTTCTAAATTTATTAATTAGCTTGGAAATTGGTGGAAAATTTTTGATTAATTTTTAATTTTAATTATCATTTTATCGTGCCAAAATATATTCTTTTAATTATTGTTGATATTAGAAAGAAACTTATTATCAAGAACGTGATTAATTTAGTTTTTTATTTTCCAAGAATTTGATCAAATACTACCTTTTTTTTTTTTATCCGTGCGTAGGAATTGGCTACGCAGTTGTATTAATTGCGTTCTACGTCGATTTCTATTACAACGTGATCATCGCCTGGGCGCTGAGGTACTTCTTCGCCTCGTTCGCGAGTCTTTTACCTTGGACTACCTGCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-8.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-8.txt
new file mode 100644
index 0000000..88613af
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-8.txt
@@ -0,0 +1 @@
+TAAAATAGATTTTATTTATATATTTATAATTTATATATAAAATTTATATAGTATTCCTCATAATATTTATAAATTATACAAACTTATTAATTTATTTACTTGTTAATTTAAACATTCAAATTATAGATAACAAGAATTATTTATCAATTATTATTTATGTCATTAATTTTGTAATATTTTATTTTATAAAAAATATAAATACATTTTTATAAAGTAAATAATAAATAAATAGTAAATAATAAATAAATTTTTACATGTGATGTAGGTAAATTGTGATTATTGAATTTAAGTTTACTTTATTTGTGAACGCGTTACATATTCGGTTTATTTTCGAACACTAACCCGGTATTCCACCGTGTCGAAGTGCTGACGTTGAAAAAATAAATTTAGGAGAAATTAATTTTGATTAAAATTTTTTAATGCATAATTTAATTAATAAATTTATTAAGTTTTGACATTTTATTTATTGATTATAAAATATAATAATGCATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-9.txt b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-9.txt
new file mode 100644
index 0000000..847e9e9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/f78/c6f/0c/Group1.10-9.txt
@@ -0,0 +1 @@
+AATTATAAATAATGAAATAAATAATAAGTAAATAAAAAATATATATGACATGAATTATGAAATTAAATTTTTATTATAATATATTATATTTATTTATTATCCTATACTATTCTATATCTGTATTCTATATATGTATAGTAGATAGTAACCATAGAAACTCAGTCAAAACATTACAATTTTAAATAATGATTTCATGTATGTTTATGATTATTATAATTCTTTAAACACGATTATATAATTAGATTTATTTATTTTATAAATATGGAAGGCAATATTTCTCCAAATCTTCAAAAGTGGATAAAAAAATGTTTAAAAAGAGAATTTGAAAATAATATACAAAGATCATTAAAAGAAGATACAGATAATGTTAATATAGCTCAAAGTATTATACATTCAAAGGAAATGTCTATTTTAGTAAATTCTATGAAAGCTACAATTCTTGAACAATCTGCTTCGAGATCTGAATCAACTATAATTTCTGATTCTCGACCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/seq/refSeqs.json b/test/integration/resources/sequences/honeybee-Group1.10/seq/refSeqs.json
new file mode 100644
index 0000000..eab4512
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/seq/refSeqs.json
@@ -0,0 +1 @@
+[{"length":1405242,"name":"Group1.10","seqChunkSize":20000,"end":1405242,"start":0}]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/trackList.json b/test/integration/resources/sequences/honeybee-Group1.10/trackList.json
new file mode 100644
index 0000000..9bff072
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-Group1.10/trackList.json
@@ -0,0 +1 @@
+{"tracks":[{"chunkSize":20000,"storeClass":"JBrowse/Store/Sequence/StaticChunked","urlTemplate":"seq/{refseq_dirpath}/{refseq}-","category":"Reference sequence","type":"SequenceTrack","label":"DNA","key":"Reference sequence"}],"formatVersion":1}
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-Group1.10/tracks.conf b/test/integration/resources/sequences/honeybee-Group1.10/tracks.conf
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-0.txt b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-0.txt
new file mode 100644
index 0000000..3edb164
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-0.txt
@@ -0,0 +1 @@
+TGAGAAATAAATATTGAATTGTATTATAACATTAATAATGTAATTAAGTTTTATTTTTGCAAATTAATTTAACAATATAATTTTTTTGTTTATAAACTTTCAATCGTATATACAAAATTGATGATGCATCAAAATAAATAATATTAATATAATTCTTTATTTTTGATTATAAAATTGTATTATCTTGATACTTTTCAAATATTTAAAGAAATATATTATTAATAGTAAAATATAAAAAAAAAGTTTGTAAAGTAATATTACCAACTTATATAAAACAAAATCATAAAATACTGATTGTCTGGTAAGACTTATAAGCGAGTGATATTTATTTTGCGTGAAACGAAATAATCCTAGAGAACAATCATATTTCCCGCGGTAACTTCGGCGCGCGAATTGCGATCTAATTTAAATTTAGAAAAGGATTTTCGTTGAAATTTGTTTGAACGGCTTGAAATAAAATTCGATAAAATTAATAATCGGAAGAAATTTTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-1.txt b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-1.txt
new file mode 100644
index 0000000..f477227
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-1.txt
@@ -0,0 +1 @@
+TATCATTTACTCTTCTATAATACTTTTTAATCTTATTTGATTTCTCCTCTAAAATTTAATGATTATCATTTCCTTTATAATCATACAGTTTGTTTATAATAAATAATTTATATTTTTCTACTGCAACGTACTTGTATTGGATAAAATTTGTTTCAAGTCAACATCATTTTTCCTAAAAAAAAAAAAAAACAGTTGAATTAGATAAAAAAGTAATCATTTTGAAACAATTGTTTCAAATAATTTTTAAAAATATAATTTATAATATATAATTCAATTTATAAATTCATTTGAAATATTATTGAATTTAACGTGTACAGTATCTCTATACAGTAATTAAAACTACATTATCGGCATGGCTGCCAGAAACAGCTTCCAGCTTGCGTATGTAACACATGCTTGGATAAGTCCATCGATTATCGAAACCATTGGCATACTAGCCCCGTATGAAACCAGCCCATGCCTCTCGTGAAACCTGCGAACTCCAGAATAGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-2.txt b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-2.txt
new file mode 100644
index 0000000..9161473
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-2.txt
@@ -0,0 +1 @@
+TATGGGGGATGAAAAGGAAGAAGAGAGGTCCTTCACTCCACAGACCGATGATATTATTTTCACTGCCACTCCCCGTTGCATTCGTGGCTCGTGTGGATCAATAAGAGAACCGAGAAGACCACTGTGATTCGAAACTCTTTAATTAATTACTGATTACAGAGGGAGATAAGAGAAATTAATTAAGGAAGGAAAATAAGAATAATAAACTGTTTAATTAGAATGAAGGTAAATATTTTAAATTTTATTTCATCATAATAAATGATACAGTTTTATTAATTTTATAATAACATGTATATAAGAAATATGCTTTGATGAAAAAAATTGTTAAAGAATATAATAATATATAATAATAATTCTATGAGTACAAGAAAACTATATTCAAGATCTATTAGGAGTTTCTTACCAAAAGAAAAATTGTTGTTGAAAAGTAATTTAATTAAAAACAATGATTTACAATAACACGTTATATCAATTCATGTAAAAAGTTGCACA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-3.txt b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-3.txt
new file mode 100644
index 0000000..3f3f609
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/2ac/141/c6/Group11.4-3.txt
@@ -0,0 +1 @@
+NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNTCAATATTAGATACATTTTTATTATCATTAGGAATATCATTTCTGGATTGAATTTGTTAAATTCTGTTTCAATATTTCTGTTTGTTGTATTTTATTATGATCATTGCTATAATAAATTATAAACTCTTTAAATTTATAAAATTATTTATAAATTTATAAATTTTTTTTATAATTATTTTTATTAGAAATCATTTTAATAAAGAATTTATTATAAATAATACTATGAATATTATTTAATTAAAGAATACCTTGTTGATTCTTTAACTGTTATATCATAGAAATTCAATTCATTATAAAAATTCATCATTCTTTGGAATGTGTTTGTATAATCAGTAATATTTGGTGTAGGAATATCTATTTTAAATTGTTCCTCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-0.txt b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-0.txt
new file mode 100644
index 0000000..d128f9e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-0.txt
@@ -0,0 +1 @@
+TCAATTTCGTCGTAAATGTTGGTATAATTTGTGATCTTTTCTTATTAGAAATAGATAACACACAAAAACATATATATGTATATATTCATATTATTATTTATATATATATATATATATATATATATATTCTTATTATATATATATTTAAGGAATAGTTAATATAATATCTATCTGGTATTTTAATTTGTTGTATCTATATCAAATATAATCTTTAGCGATTTGTAAGGTGATATTAGATAGAAGTTTGAATATGTAATAAATGATATATAATTGCCAAAAATTGATTATAGATAAAAATTGAACATAAATTTTTCCTTGAAGATAAATATAATTCTCATGTTATTTTCGTTGACAATATTTCTACACATTTAGAATTATTCGAAAGAAAAGAAAAATAAAAAAGGAAGAAATACAACATAGTAGAAGCAATTTTGAAAATTAGTTTGGTAAAAAGAAGGGCATATCAAAAAAATTGTTTGAAACAGATATGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-1.txt b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-1.txt
new file mode 100644
index 0000000..7d2d2b7
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-1.txt
@@ -0,0 +1 @@
+TTTTATTCCCTCTTTTTCGTTTTCATTACGTTCATCTCAATTTTCTATTCTTATGATTATTCGATCGATTTTTTTTTCTTTTTTTTACTTTCTTCTACTTGCACTTTATATTGTAATTTCGTTTAATAATAATGATATTTTTAATTTTTCTTTTTCCGCTTTAAGTTACATGATAATATTTTTGATACGGATATTATATTAATAGAAATGTTACATATTACGAACTTCGCTGTTAAGATATTAGTTATTAAGTTAATTTAAGATAATTTAATATCTCGATGGACAAGATGGGACGACGTGGGACTTAATCTTTAATTTTATTATATATTTCTTATAATTAATGCAAATTTTATATGCGATATAGATATATTACATATCGAAAAATGTAAAAAATTTTTGTACAAATTTATTGCATGTCCATTAAGTTATATAATTAATTTTTATTTTTTATTTCAGATTTGTTTAATTTTTATATAATATTTTTTTTATGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-2.txt b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-2.txt
new file mode 100644
index 0000000..fc5ad44
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-2.txt
@@ -0,0 +1 @@
+TTTCGCAATAAATGATATATTTTTTAAAGATAAATGAAGTGTATAAGTAATTATTTGTGCGGAGCGCGTGTCTATTAGAGAGCAAGATCTCTTACTATATCGCAACAGATAATCATATCTGACGATAATAGTACACACATGAACATACACATCTTCACAAATTATCTAATTATATTTGGCGATCAGACTATGATGTAATTATCATGAGATTTTATTTGCAAGCGAAGTTTGTGCCTTCAGAAAAATCCTTTCAATGGTTTAATTGGGAACATTACAGTTTCTTTGTGAGGCACGCCAATAACGAATAATACCATGAATAATATCAGTGCCTGGTTCGCAATACCGCCTAAGTTCCTACCCTAAGGAAATTCCTCGCTTAGATGATAATCTGTATACAAAGATCTCACGATAAACAAGAATAGATCGGTGCATACTTCAGCAGGCTGAAATATGTCAAAAATATACAATTATAAGTGTCAATTAAATTCTCAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-3.txt b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-3.txt
new file mode 100644
index 0000000..e169882
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/7cf/4ac/72/GroupUn87-3.txt
@@ -0,0 +1 @@
+TATACAATTTATTTATTTTCATATGTACAAAAAAATCTCAAATTTGAATCAATTTGAAATTCACAGATCAATATATCAGTTGAAAAATTTTTGTCTTATCTTATTTCAATTCAATATTTTTTATTTAATGAATTATTGAAAAAAATAATAATCAAAATTCAATATCTATATTTTAATCATATTATATCAATATTACATTATACATCCATTAAATTCGTATAAGAATTGCATTTTTATTTTTCAATTTTATTATTGTATATTTTTATATTGCAAATTTTTATTTATTATTCACATTAATTTTTTTGAATAAAATTATTTCAATAATCGATTATTAGTAATTATCATATAACTTAATTTTATATGAAAATTATTAAATTATTAAAAATTTATTTTAAACGTATATTGTAATATTTTAAACGATTATAATAAATCATTTCATTAAATATATCTAATGATGTCCTATATTCAATTAAATATTTTTAACTATATCTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-0.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-0.txt
new file mode 100644
index 0000000..b19418a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-0.txt
@@ -0,0 +1 @@
+GCCAGGGAATGGCTTGTCATTAGGGACAACTTGTCAAGTCCCTAGCTTTTTATGATGTATACCTTAGACGCATAAACAGGATCTATAGTTTTGCAATATATGCCTTTGGACAGGTATTCTACAGGCAGAAAGACCCCTCACTTAGGGCTGGCTATCCCTTCGGGCGATCCTTCCACCGCCCTTATTCCACGATATTTGCATAATGATTTATGGATGAAACGTGACCACGTTTTCGTGGGATCGGGACACGCACCGAAAACTTGCCGGAATTTCAATTACGCGTGGAAAGATTTCAATGGAAATTGCGCTACGATCGTATATTATTCGAGGCGAATGAATTTTGGCACGTTGAGTATTTTAATTGTTGGAAAATCGTTTTAATTTTCGATGATGTAAGATTTCCCGAAGAAATTGCTCTCACGAGTGAGCAAGTTTTATTTATAGTTTAGAGAATCGAACGGTATTAATTGATAAAACTAATAATCGTTACAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-1.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-1.txt
new file mode 100644
index 0000000..9645146
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-1.txt
@@ -0,0 +1 @@
+TGCGCCCCGTTGTCCTCCGTGTCCTCCGTCTCGCTTCCAGGCGTTTTCAGCGCCGCGTCCTCGGACTCGAGGCAGTGCTCCGCGATCAGCTCATCCTCGCTCGCCTGGAGGGCCGCGTATGCGGCCGCGGCGCTCGAACGCTTCTCCGTCAGCCTCGAGCATTGCTCCTTGCCTCGTGGAAGACTCTCGTTGCTCTCACCTGGACAATTTTTTTTTTTTCCAGCACGATTGGCCCCTTTGTCTTCCATCCTCTTTTTTTTATTATTTTATTCCCGATTTTCTCTCGGTTTTTTTTTTATTTTCGATTTCGATGGAAAAGTGTTGATATATTCGTGGGAGGATCTTTTAGAGGAGTAAATGGAATTCTAATTCAACGAAGTCAAATGTTTAAAATTTTTTTTTTTTTTTTTTTTTGGTCGAAAATCGTCCTGTGAATATATCAACTTGTTAAATCAAGGGAAAGAGAGGTCTTGAAATTATGGAAACAATGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-10.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-10.txt
new file mode 100644
index 0000000..f42fde9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-10.txt
@@ -0,0 +1 @@
+AGCAGGAAAAATTTCCTTCAAGTATGTAAAAACTTTTAATATGGATGAATATGTTGATTTACCCAGAGATCATCCAGAATCTTATCATTATTATATGTATAATAATTTCTTTAAGCACATAGATATAGATCCACAAAATGTGCATATTCTTGATGGTAATGCACCTGATCTTATAAAGGAATGTGATGATTTTGAAAACAAAATTAAAGAAGCTGGTGGTATTGAATTATTTATTGGTGGTATGCAATTTTATTATATGGAAATAAATAAATAAATATAAAAAAAATTTATATGAATAAAAAAATGTCATGTTTTAGGTATTGGTCCAGATGGTCACATAGCTTTCAATGAACCAGGTTCTTCCTTAGCATCACGTACAAGAGTAAAGACATTGGCGCAAGATACATTAGAAGCTAATGCAAGATTTTTTGGAAATGATATTTCCAAAGTACCAAATCAAGCATTGACTGTTGGTGTTGGCACAGTGATGGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-11.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-11.txt
new file mode 100644
index 0000000..35bfb0e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-11.txt
@@ -0,0 +1 @@
+GTAATGCCTTTTATCTTCATATATTACATGAATATTTAATTTCACTGAGAAATATAATTATTAATTATTAATTATATATAAATCAATATTTTGATTTATTATATATAACANNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNCGAAAAATCAATAATAAATATATCAATATTAAATGAATGACGAAAAATTTGATGATTTTAAAAAAAATCTGATGATATAATACTAATCCATTATATTTAATTTATTTGTACGCTATAGATAAAAAGATCCAAATTTCAGATGTCAAATATAATATTTAATAACTTAACAATTATTTACAAAAAATACATGTATACACATAAATAAACATGTGATGTTTTGGAAACATTATTATGCTCATGAATATGCAATAAAAGATATAACATTCTATTCTATAAAAAGTTTAAATATAACTTTCATAATTTTCGCAAGTCATTTAAGTTGAATAATTAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-12.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-12.txt
new file mode 100644
index 0000000..4304ca2
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-12.txt
@@ -0,0 +1 @@
+TGCATAGAAAATGATGGTCCGTTACTATTTGTAGTATATGCAACTTCTTCGTGATCATTCTGATATTCAGGATTCTCTAAATTGTTCGAGTTACCTTTCCCACACTTCAAAGCGAATAACATCATTATACCAACAATGAAAAGGCCAAAAATAATGGACATCACTGCAATTGCTGGACCAGAATACTGTTTCAATTCCTCAACATTCGTATAAGAGAGTGCTAATGGATCAGTTTCGCACCAACTGGTCCAAGTATTATTCCAAAGTGTCGGATCCGCTCTAGGATCTCCATCTAAATTACTAGCAGGTATCGCGGTTATTCTGTACTTCATTAGAATTCTTTCCAGAATAGCCATAATCACTGGTCTTCTTTCGGCAAGATTCACCATTTCGCAAGGGTCTTTTTCTATGTTAAAGAGACATGGTGCTGTCATTGGTTCACAAGGGATCTATGGCAAAATTATAGAGAAATTTTTTAAAATTTTATCAAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-13.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-13.txt
new file mode 100644
index 0000000..499c92c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-13.txt
@@ -0,0 +1 @@
+AGTTCCGTGCCTAAAGTGCATAGACCACTTTTAGATGCGCCATACATGCCCAATGGTACAAAAATGGCATCGAGATGAGATTCAGATATACTATAAGTCAATACAAATGTAAATGAAATTAAAATTTTTTATTATAAAGATTGACTAATATTTGATTTATATATATTAAAAAAAAGTAAAAACGATAAGTAGAAAATAAAAATGATGATCAGACAATATATTTTTCATATGATTGGAATATCACGAATATGTACAATACAAATGATACAATACAATACAATAAAAATGATTGTACTTTCCATTTTTATATATAAATTATCATATAAAAATAAATTACATATTAATAAAGAATCATTATAAATTAATCAATAATGTTTATAATAATGAGTTTTAAACTTTATTTACATTTGATTTAAAATCAAACAGTATTATGAATGAAGATTTAATAAATAAGTATAATTTTTATAAATGATTAATTATGATAGAGTTTAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-14.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-14.txt
new file mode 100644
index 0000000..af3c633
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-14.txt
@@ -0,0 +1 @@
+TTTCTTTTTGAATAACAATAACAAACGTGTTAATATTGTGAATTCTAAAGAGCAAAACAAATACAAAAGTTAATATACAAAAATTTTAGATTATTTACTTACATATTTATTACTTATAGTATATATGAGTAATTTAAGGTTAGATAAAATGAGATAAGAACACTAGATAGTGTAAAAAAGATAGAGAAATCTCACTTTGTGTAACGCAAACAATTGTTTATAACTTAACAAATAAGGATTTAATCGACATTTTTGTATACCAACTTTTGTGTTATTCTTCGATTCTCCAAAGAATTTCATGATATTAATACCTTGTATATATGAAATATATAAGAAATATATTCATAATAAATATTTAACACGTAATTTTTGAAACAAATCTATTAATTTACGATATGGAACGTGCCAATTCTATGAAAATTAATCTTTTTTTTTTTTTTACATAGATCCATTTTTCTGCAGACACGTGGAAAGGAGGAACGTAATAAAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-15.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-15.txt
new file mode 100644
index 0000000..5638def
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-15.txt
@@ -0,0 +1 @@
+TGAAATTTCGAGATTAACTTCTTAAAATATAAAAATCTACAGGAAAGTATTCTAACAATATTGTAGTGTCTTTACTCTTAGATATTCTAAAAATGCACGTATTTTTATCAAATAATTATAGACAAATAATTTTTAATCTTTTATAAAATATCTTATTTTAATATTTTTTTCAAAAATACTATTGATATCTAAACTATCATTTTATAATTTTAATAATAAAAAATAGATTTTATTTTTACTTAAAAAATTTGTAGCAAATTTATTAATAATACTTTATTTCTTACGATGTTAAAAAAAATTACTTTTTCTAATTATTTCTTTAAGACCAATCGTAATATCTAGATAATTTTCCATGTGAATGTTGGCAATCGCGTTATGAATCTATCTAACCTCTCTAAATTCGAAAATATAAATTCGGAATACGCATGCTCTCTTAGTGACAAAAGATCGCGAATTCGCAGCACGATTTCACGGATGGCAAGAGTAAGTAGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-16.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-16.txt
new file mode 100644
index 0000000..1e7f473
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-16.txt
@@ -0,0 +1 @@
+TAATGTCTTAAAGCGACATCGAGTATCAAATACTATATCTATATTATGATTATTCCTATCATTAATATACGTTTAGCATTTTTGAAATTATTTTTTTAATTTATCTAAAATGTAACATTCAAGTTTTCTTATTATTACAGTTTTTTTTTTACAACTTTCTATGCATTACTAAAGCTCTAGTATCTAATAAAAAATATATAATTTTTAAATCGACTTTTATATTATTTTATAATTATATAGAAATTACATGTGTAATAGTATATATATATATATATACTAAATATATTAATATTATTAACAAAAAGAATTTTTATATCTGCATTTGTAGCAATAAAATATGCATTGAAATCATTTAAATATTACACTGTTTTTATTAATTTTTAATAATAATGTGTTTATCAAAACCAACTTCTATCTACTTCAGAATCTTTTTCAAATAGAATATTTTTAATGATTACTTTTTACTTTCTTAAAAATACTAATAATGCCTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-17.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-17.txt
new file mode 100644
index 0000000..bfa6702
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-17.txt
@@ -0,0 +1 @@
+ACCTTTTCCACTGTGTTAGTAGTATTTCTACTAGTACTTTGTCTGCCTGTGGTAGTGCTTTTTAAATTTTCTTTTATAGGCTCCGAATCTTTTCGTTTTTTTCGACGATTCTCTTCATCCTGCTCTTCTGTATTGTCGTTTTCAAATGCACGCTTAGACGAAGGCGATGCTGAAACCGCAGGGGCATCATCGGTAGTAATCGATGCGGTTGTTGTTTCACCGGAAGATGCTGCACTATTAGCGGTAGACACGTTGTTACCACCATTCTGTCCTCCTTGATTCCCAACAGACTTGCTACCATCCTTATCTTTCTCGGGATTAGGGTTTGGTGATGAGCTTGCACCTCTAACTGGTGTGGGCTCTTTCGAACTTTTGAATTCGTACACGTCGCTACTATCGCTAGTAGACGATGCGTTTCCAGAAGTACTGGAACCCGAGACGGCATTCACGGTCCCCGATGAGACGTTTGATGACGTCTTCGACGAAGTTTCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-18.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-18.txt
new file mode 100644
index 0000000..915ae4f
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-18.txt
@@ -0,0 +1 @@
+TTATTTTCAAAAATGTTAATTTCCTTAACTATGAGCAAATTTCATTACATTTCAATCAATATATGTGTCACTTTTAAAGTGTCTAATTTTATAGCGGTGAGTCTACCAGTCTTTTTTCCTTCTCCAAACCTAGGCATTTTAATTATTAAAATCGTCATATTGAACCCTGTGTTTGACTGTATCTCGTTTTCAACAACATTTGTGTATGGCACAAATGGATATAGAGCTCACATAACTAATATGTGTAAATACTTTGTTTTACTAGTTCCACACAAACATATGACGAAGAAGAATAATTTTTTCATTCTTCTTTCGCTATTATTTAACCCTGTCGCAAAATTTTATATTACAATTCGCCAATCTTGAATTTGTATTCTTTTCATTACTCAAAGATATACATAAGTATATCCATTTGTGTTAGAATAGATTTTTTTACTTTGTTTATTATAGGCATTAATACATTTAATATTTGCGGCAATAAAATTATTACTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-19.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-19.txt
new file mode 100644
index 0000000..f2b5bd6
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-19.txt
@@ -0,0 +1 @@
+TTGTCAAATTAATAAAATTGATCAGTAAACCAAAAAATTAAACAATCAAAAATAATTTCCATTTTTAACAAATTACCAAAAAATTAATCAAAAAATATTTTCCATATATTGATCGATTAATAATGATTAACGATCAATAAATTTTTTCCGTTATAGCGTGCAATTTTTTTGCGATTAAAATTACGCTTCGATCCGAGAACCGATAGACACAGCGACCTGGCAGCGTTCGACAACTGGAACTATCTGGCAGACTCCACGTTAGCGGATCAGGCCGATTGCCAATTGCAAAATAGGGCGACCTGGATTTCGTATCTCGAGACATCCAGGTACCGCTGCAGGGACGATTACCAAACTCCGTGATAGGCCTTCGAAACAGAAGAAGAATCCTCGAAGCAATAAGAAGTATACTCATTACGCCGTCCACGTAGCAGAACTTGGCAGAATTCGTAAATTCATGGACCGTTTCGACTAGATAGATCTCGAAATCTTTCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-2.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-2.txt
new file mode 100644
index 0000000..561fb2f
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-2.txt
@@ -0,0 +1 @@
+ATCTTATTCTTAATGTCAATATTTGAAAATCTTTAATACATCAACTATTTTGTGCCAAAATCTTTCATTGAAATTTAAAAATTCGCAATCTAAATTTCAAAATTTTCAAAATAAAAATTTTTCCCCAATATTTTCATTTATATCGTGTCAAAATTAATCTACAACAGATACATATTTCGTGTGCAAATATATTAAAACGAAGATTAAAAATTTTTTCCTCGGAAAAAAATCTGACATGTTAATCATTGTATGAAAAGGATTCTTTTAACCACGCTTTTCGTTTCGAAACGAAAAGCGAAGAAAAGGTTTCGTGGAACTTGTGCAATTCGAATCGACGTTTCCTAGAATCAGTTCATTCCAGTCACGAATCGGCGAAGGAAGGAACGAGCACGGGCAAAGTTTCACGTCAAGCTCGATTAATCGAGCATCGTTCCCGTTTCCACACCGAGAGAAGAAATTTTCGATGGTTTTGTGCTTTCGAGCTCGCACGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-20.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-20.txt
new file mode 100644
index 0000000..28c5f33
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-20.txt
@@ -0,0 +1 @@
+GTTGATTTTGAATATAAATAATTTGTGCAATTTATGATAATGATGATAATTTATTATTAACACGTTCGTAATTATAGGTTAAGATTGTGATAAAATTATCACAAATTGTGGAAATAGAAAAGAAAACAGGAAAAGAAAAAGAAAATAAAAAATGTGAGAAGGATAAATTGTAGATTGTTTAGAAAAATGCAACACTTCTTTTATCGAGCACTCTTGAACCGGGGGCTATCTGTTATTGTGAATTATCACCGCTGTCTGTTTTTATGAATACAATTTCATACTTCCGCTCCTATCATTTTTTTCTTATCTTTCTACGAGTGTCAACAAAGTTATTATCACGTCTACTATGTTATTGTATGATAACTAACAAAACATAATAAGCAATGAAAAGTTAAATATCAAATGTACAACAACATTGTTGAATACTAATAAGAGCAGCATTATCCAATTGTTGAACTGATTTTAGAAAAATAAGTTTGTTGTTATGATTTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-21.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-21.txt
new file mode 100644
index 0000000..2e0900b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-21.txt
@@ -0,0 +1 @@
+AGGGAGGAGCTCTCTCTCTCTCTCTCTCTCTCTCTCGGTCGAAGGGAACTATCGATTGCAGCCGAGCAAGATAGGATCCAACCGTGGTCCTAGGAACTGGGATGTGACCGGAAGGAGCATACACCGGAAGGGGTCACTGAAACTTTTGATTGGGGAGAGAGGAGGGTGAGGAGCTACTTTTTCTTCTTTTAATAAAAATTGGTTTTTGAATTGATATATCGATTGAAAGTATCGTTTATTAATTTTAGATTAGATTGGAGAGAGAAATGTCAATTTTATTGTATTTTTGATAGGAATGGAGATGAATTGATTGTTCAATTGTTTATTGTGTTTGAGAATTATTGGAAGAAGATTGGGGAGAAGATTTTTATTCACGAGGCGAGGGTTAATTTTGATTCCTGAGAGGTGGCGTGTGTCACGCGGCTTTTTGAATTTTAACGCGTGTCGTGGATCGCCGACGCTTGTGCGTACGCAAATGTTAAGAATTTATCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-22.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-22.txt
new file mode 100644
index 0000000..b4153b5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-22.txt
@@ -0,0 +1 @@
+AGGATACAGTGTTTTTAAACTATTGAAATGAATTTTTGAAGGGGGATGTGGAAATATTCAGCTCTCCTTTGGATGATTAATGAGAATTCCTCCACACCCTATAATTGAGATTGATCTTAAAAGATTCTGATTGATTTTAAATTATTAAATATAATATTTTTGAACTATTGAAATGAATTTTTGAAGGGGGATGTGGAAATATTCAGCTCTGCTTTTAATGATTCCTGGATAGTCCATGAATTTATTGAAAAAATAGAGATTGTGATTGATTAGAATTTGTCATTTTAAATTAATAAATTGGAATTGAAAAATACTTTTAAATTATTCAAATGAATTTTTAAATGGGGATGTGGAAATGTTCAATTCTTTAGCTTTGGATAATTAATGCAAATTCCTGACAGTCCATGAATCTATTGAAAGGATGGAGATTTTGATTGATTAAAATTATTTGTCACTTTAAATTAGCAACTGGGCATCAATATTTTATAAACT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-23.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-23.txt
new file mode 100644
index 0000000..acb95bb
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-23.txt
@@ -0,0 +1 @@
+TAAAAAATAGGCGACGCAATTCCACAGCCTAACCTCACACACAATGTGGAAAATACGTCGTGCACGATACACTAGCATCGTCAGTCGCTGAAAAATTGTTATAAACTTATGAACTATGGTCATCATACCCATTGCCTTTCTCTCATCCAAATAACACCTGGGCATTTGAATGAGATCCTGGTAATGGACATGGAAAGGGTTAAATCTGAAAATCCTGAGCTAAAAAAGATGATTAGAGTATTTGTTTTTTTTTCTAACGGATTAATGTTTTCTGATTCAAAATTTGTTTATTTTATTTTAAATTTTAATAAATTTATTTTATAAAATCAATGAACTTCTATTAATTTCAGAAAGGAAAAATATATAGGCAATAAATAAGAAATTTATTTATAACAATTAATAATTAAATTTATATATGATAATTAATAATGAGAAATAATTGAAACTTTAAATGGAATATTATTGAAAATAACCTCAGGCGATCTCTTTATC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-24.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-24.txt
new file mode 100644
index 0000000..fef3068
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-24.txt
@@ -0,0 +1 @@
+AGTACACATAATAGTGCACAATCGTATCGTTGAGTTGATATGTACATACACGTATTACGTTTGACCATTGTTAAAAGATACAAAGACTGTCCGTAATTGTTCCTAATTTATTTTCCAGCTGGGGAGATTGGAAATAATTATCTGCCGTCCCATTTATTAATGGTCCATGTACGAGTTAATTTCGTTAAGAGTATCAAAGACAGTGAGATACAATTTCTTTCCTCGTATCGCGAGATTTCCAACAAGACAAATCGAATTTCTAGATGAGAAATGGAACAATTCTTATACAGTTATATAAACTGGATTAAATCGAACGATTAAAACACGTTGTTTGAACTTTACGAAAAATTGAATAATCAAGAATATTAAATATAATTCGCTTCAATCTGCGACTTATGAATTAAATAATAATAAAAAAAAAAAATAATACAAGTCCATTTTTGCAAACAAGATAAGTATTTTCAAAAATTCCATAAAATATTTCGTAAGTCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-25.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-25.txt
new file mode 100644
index 0000000..fddd071
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-25.txt
@@ -0,0 +1 @@
+GTACCACGAGTACGTGCTTGATGCCAAAGACGTAATAACTCGGAAGTACTCGCGTGTAACGTAACGTTTAGGCGCGAGTGCGCGACCCTTTATGCTCGAGGGCTCGTTAATTACGTGAAAAGACGCGACCGTACGCGCACGCGAAGTTGCGCAGCGTTTATGCGTACAGAGGAGGGGGAAAACGGTGGCAGTGGTGGGAGAAGGTTGCTCGCTGAGGATAACGTTTCTATGAACGTTAGGAGAATTAAAAGTTATGTTATGAAATTCTAGAATTCAATAGAGTATCTTAGATTTCTTTCAAAAATATCCTAAAGATATTTTTCTCGTTTTCTTACAAGCATTATACGGTATACGTTCGATACATATATTTCAGATTTCAATAAGAATCTTTAAATATAGATAATAAGATTTTTAATAATTTACCAGGTTTATTATTTTTCTTCTCACAAATATTTTATCATATCCGAACGAAAACGTATTTTCCACTCATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-26.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-26.txt
new file mode 100644
index 0000000..0a99596
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-26.txt
@@ -0,0 +1 @@
+CTCATTTACGTTAAAAGTCGTTAACTCGGAAAACATCTGTACATGTATCTGTAGTAGTATCAAAGTTGTATTTCTCTATCATTCAAATAATTGCGAGTGAAAATGTATGCACGTACAAATAAGAGATAAATGTCTAATGCTCTTATCATGGTACTTGATGGAATGAGTTTCTTTACTCAATGTACATTTTCACAGGATTAGTTTCGTTCCAACGTTGAATTTAGCTTCGCTTGACTCTCTTTTCAACCAGTTGTGCATATATATGTACACACACACACAATGCTTTCCAATATAAAGTTTATATGATTCTAATCATCACATTATGCATGAGAATAAAGATAACGAATAAAGCTAATATGTATAATTATTATCGTAATCAAATTTTAGATTTTCTTTTTTAATACAAGAATGATCATTTTATGACAGAAAGAATGAAAATTTTAAATTATCAAATTATTAAACCATTAAACTGATTTAACGAAAAATAAAAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-27.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-27.txt
new file mode 100644
index 0000000..d1efde8
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-27.txt
@@ -0,0 +1 @@
+TATATATATATTATATATACATTTTTTGTTGGATAAATATTAAAAGTAATTGGAAAATGATAATTTCAAATAGATTTGTAACATTTTATTTACATATTCCTCAAAAAACTTTATTATTACATCGTAACGAATAAATGTATAAAGTGCCGATTGGTGAATAATTGCATTGAAAAAAATTGACGTGAGTCACAGAGTCAACCTTCAACATTTATCGAAAAATGAAATATTGTAGCAAGGAGAAAAGATAAGAATTCACCGTTTGACGTTAAAATTAAATTTAAAAAAAAAAAAAAGAAAATTCTATGAAGTTTAACATCAATTTAACTTTTATATTTTATCAGTAATTAAATTTTAATTCAATTAGTTTTTTTTTTTTTTCAAATTTAATCTTAAATTTTTAAATTTCTCTCATATTCATAAAATAAATCAATTTTGTACACTGTAATTTGATTCCATCGATCAATTTTATAAATTTTCATTTCTCATTTTTTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-28.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-28.txt
new file mode 100644
index 0000000..82d0cd2
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-28.txt
@@ -0,0 +1 @@
+TTTTATCTATAATTTCATAATTTTATTAGAGTTATAGGTACAATTTTAAATATCATATTAGAAAACATTCGTATTAATTTTAATAGGATTTTAATTTCGATTTATTTTAATGCCAAAGAAATTATAGAAAATTATCCAGATACTTAACAGCTTTTCCATCGATTCGGTAATTATCACGATATATATTTAGCATAATATTTCGAACTTTTTTAATTACCGAACTTAAATAATTAGACAAAATGACTTCAGATGGTTTTAATATCTCGAGAAGTATTAAAGGCATTAGAGCCTGAATCATTTAAATTTTATGTGTTATATTTTACGATAAATACGAGAAGGCGTAACATTAAGAATGGATATTATACGACGTACGTATTACCATTACAAAAATCGAATAAAATCGCAAGTTTTATATTTTTTTTGTAATCGATATATCGGTGTTTCTTAAAATATTTATGGTAAACCGCGACATGCATATTCTGTTCGATTTCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-29.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-29.txt
new file mode 100644
index 0000000..ff8d239
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-29.txt
@@ -0,0 +1 @@
+GATAAATAAATAAAGCATAATTATTGCATCATCATAATATAAAATTTTTGATTCTGAAAAATTTTATTTTCTTCCAATGCTTTTATTTTGAGATGATTTAATTTGAATAAAATTAGGCATTCTTATTTTTGGAATTGCGATTTATAATTAATAACAAAGAATTGTTTGTATAATCAATAGGAAAGGGACATCATTATTTTTATATCATATCAAATAGGTTAGCAAAATAGTTTATTTCCAAATTAATTTTTTTTTTAATAAAATGCTTGATAATTATATTAATTTTTAATATTTCCTAAATTCCGTTAACTAATTTATTCAACAAAACAAGATTAAATAATGATTACATTGTGTTACTTAACTTTTTAGAAATGAATTATCATTTTACATATTTCGTATGCACATTGGTGTGAAATGCGACAAGAAGTTTCGACGTTGGATCGGGCAGCGCAAAATTTCAACGGGTTTAAATTTATGAAGCGAGAAAATAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-3.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-3.txt
new file mode 100644
index 0000000..556cbe9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-3.txt
@@ -0,0 +1 @@
+ACGACTATGATGTTTTCTCAGGTTGAAATGGAAAAGGAAAGGCATATTGTCCTTGGAAGAGTTTGTTTCGACCGATTGGAAAAAATGTTGATAAAATGTCGCGAATGAAAAAAATTTTTTTCCACGAAATAATACATCAGAATTATATTTTTAAATACGTCATACGTCTCCTCGATCGGTGTATTAAAAATTTCGATAAAAAAAAAAATGATCAAATATCGCGATATATGTTCTCTCGATTTACAACAAAAAAAAAAAATATATTTTTATTCCAAAATTTGAATACATTTCTGGAATACGTCCATATCAACAAAATATACCGTTTCGAGAAGCGCAATTCTAAGTTTAATATCGTTTCGAGCAAAACGAGCAGCGTAGCAGCTCCCATTAGACGACGTGTAATTTTGCTTTATTCGTACTTACAACGGTTGAACACTTTGCAAAATGTCTCGCTATATTTTGTTATATTGCTGAAACGACACAATGCAAATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-30.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-30.txt
new file mode 100644
index 0000000..26bfc54
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-30.txt
@@ -0,0 +1 @@
+GTTCCTCTATAAAAATAACCTTTATTTCCAAACCATTGTACAACAGTGTTATCAAAAGTTAATAAAGGATCGACATTTCTTCGTTTTAAGAGAAAAAGAGAGAGAGAGAAAGAGACTGTTACAGGATATTTCGAGCATGGACAAGCACTGCTTGTCGATAATTACCTATGAAACGGTTAGTGTTCTTTCGATGTGTGCCCGTAACAGAAACGTATTGTCGACTCGTGGAGAAGTATTAATCGATACGCGGTTTAGATATATTTCGATTTATAAGAGAGGGTGAAATTGGAAACAGTTTTCATTCGATTGTTATATTCGGTTATACACTCTTCGGTTGGACACGCGTTATTCAGACTCTTGCAATCGTTTCTCCTCTTTGATCAGCTCCAGAATCCCGATTCTCATCTCGGAAACCACGTCGTTCTCGGTCAAACCCAGCCGACGTTTGTTACTGATGTCGAATACGCCTTCTTCGCCCTCGGTGTGCTCACC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-31.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-31.txt
new file mode 100644
index 0000000..8d52e79
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-31.txt
@@ -0,0 +1 @@
+TCCGGACGTCCAACAAATAGTCATGAAATTCTATTTCCGACGTGGTTAAGACGGGGGTTAATACAACATTCTACGCGACCTCGATCGAAAATGATTGGAAAGTGGGGGAAATCTGCCCCTTAATTCCGTCTGTGATTCCAAACGATTTTTTTTTTCAAATAGCCTTTTATCAAAATTTTAATCATCGAGAATACGAGAAAAGAAAATTTGATCGATAATTTTGATATACGGGAAAAAATCGTTATGTGGAAAATTTTGAATCTTAATTTGGATAATTTTAGTTATATCGAAAATTTTAATTGTTAATAACATTAATATTTAATATTATAATTCGTTTCGTAACTCGATCGAGAAAAAAGAAATTTAAGAGGTAAGAAATTTGAATTTAATTATAAATGGATAATTTTAGTTTCATTGTATCGAAAATTTTAATTATTAATGAAATATTCTACGAAAAAAAAAAGAAAAGAAATTCGAAAGAAGAGAATCTTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-32.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-32.txt
new file mode 100644
index 0000000..1134656
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-32.txt
@@ -0,0 +1 @@
+AGGGGGGAAACGAATCAATTATACAAGGAGTCTTGACTGGTTCGAAGGAAGATTCGTCACGTTAAATATTCCTATTAACAGGTGGAAGTATCGAACATTTGAATCAGAGATTTTTGGACTCGATAAAAAAGATGTTTGAACAGGGAAGTAGAACTTGAGGTGATAATGTTAATATTCCATGGTGGACTTGGATCCATTTGGACATTTCGAATCAATTTTATTTTTTTCGAAAATTGTACGAAAATTTCGTTTTTGAAAAATTAATTAACACTATCGTGGTATTAAAATTCTCTAGCTTTATCCTTAGAGAAAAATTAAAGATAAATAAAAAAAAATATGGATTAAAAATTAGTAATTAAGTAATAATTTATTGGATCATAGAGGATAGAGAAATTTGTATAAAAAGGAAAAAAAAAAGGAAGAAAAATGGATGCAGTGTATGTGAAAGCAATAAAAAAGCAATGCTCGTTGATGAATAAATGGAAAACGTAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-33.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-33.txt
new file mode 100644
index 0000000..d28e02b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-33.txt
@@ -0,0 +1 @@
+ATAAGAATACATAAAGTGTGTAAAAAATGAATAAAATTTGTTCGTTTAAAGCTAAAAGTGAACTATTTCTCAATTTTAATATATGTATATATATGTATATCCAATTCCAATTTAAACGAAAAAATTCTACTTATTATTTATTAAAATTTTAAACTACTTTTTATTACACTCATCACCAAAAAAAAAAATATGAATTATGCAAATAATACCTCTTGGTATTTTGCATATGAAAAATTTAACATGAATCTATTAAAAAACTTAAAATTCTATTTCTATGAATTTTAAGATATCTGGTATAATGCATTGAAATTTTATTGCCCAGAATACGTTTCCTTTTTTTCACGAAAGAAGTTCTATATTAAATACGGTTTTGGTCGATAACAGGATTAGATTTCAAATTCGAAAGTAGCGCGACATATAAACGAGAAACCGTTTAATTTTACTGTTTTATTAATAAAGTTGCAATCGTCGAACATCTTGTATCTCGAATCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-34.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-34.txt
new file mode 100644
index 0000000..049d52b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-34.txt
@@ -0,0 +1 @@
+CGAACGTGCCCGGGTGAAGGGTCGACAATTGATTGTTGTTCAACTTCAAACTTTCCAGCAAATCCAACCCGCGAAACCCTTTCGCCTCGATTTCGACCAGTTGCGTGTGAGACAAGTCCAACTTCACCAAATTCGGTGTACTCTTGAACGCGTGCGAGTGGACCCTCTTCAGGGGATTGTAGGAGAGTACGAGGTCCCTGAGGAACGGCGTGTCCAAGAAACTGGCGGTCGGGACTACGGTTAAGAGGTTATGGCTTAAATCGAGTTCAACGAGATTCGTAAGGCCCGCCAGTGCCTCGCTATCGATTCGGTCGATACGGCACTCGCGTAGGTAGAGACGCTGTAGATTCGTCAACCGTACGCGTACGAAGATGTTGCTCGGCAGAGTGCGAATGTCGTTGCCGCTCGTGTCCAACACTTGCGTCTCCGGATCGACCCACTCCGGTATGCTGGTCAGGGCACGATTTACACACTCGACCGTGCGCTTCCCGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-35.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-35.txt
new file mode 100644
index 0000000..42875c1
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-35.txt
@@ -0,0 +1 @@
+TTCATCTATTTACTTTCATTGATATTACAATTACAAAAATTCCAATTTATCATAGATCATAGAAAAATCAAATTCTATTTTTAGAAAGTTTCTAGAAATGTATTTAAAAAAAATACAATTGTTTTGTTAATATAAAATTTCATCTATTTACCTTCATTGATTATCCACAGGTGGGGAAGCTACTATGGTATTTTCAATTAAATCAGAAATCCATGGTTATTATCTTGATTCTCAGATATACTTCCCGATATCTCGAAATTTGCAACATGCTGTCGCTGTTTCTTTGGATGCAAATTATATATATTGGTCCGATATCGAGAATGGAAACGAAGCAATAATCAAAAGTTTGGAAGATGGTTCACAGCGAGAAGTTATTGTTACAACAGGTTAAATTATATTAAATTTACACAAGATTAGATTTAAGATTAGATTCCAATTTTTATCTACTACACTTTATTGCAGAAATATTTCTATATTTATACATTTGTGTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-36.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-36.txt
new file mode 100644
index 0000000..481d5b6
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-36.txt
@@ -0,0 +1 @@
+AAATACAATTTTACCTTGGTTCCTTTGCTTTTTCTTGTCAAATACCACTACTATTTTAGATTCTTGGAGCTGCCAGTTATAGGGATGTTCTACGATAAATTTCATCAAGGGGAACTACTATTTTAAATTTCATTCAGCATTCTATGTGTTTATATTCAAAGGAATCAGGACCTGCTAAACTGGAGAATAATGGAATAATGGATGGCATATCAAATTATTGTTTGTTATATACAACTTATAAAATCATATATATTATTTACTAATTTTATCAAATGGTTATTCACACGAATGTATTAATTATCGTAAATAATTGTCAACAAATTTTACTAGTCATATGTAAAACCTCCAAATCATTTAAATTTTATCAAAATTGAATTTGTAAATTTTATTATCACCTACAATTAAGATATTTTTTATATTATATATATTATACATATATATTATGTTAATCAGGAAAGATATATTTGCAAAAATGACTGATAATTCTTTTGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-37.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-37.txt
new file mode 100644
index 0000000..51aa0e3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-37.txt
@@ -0,0 +1 @@
+TTTGTCAGAATTCTAGAAACATTTTATCGTTTAGATTTTTATATAATAAATTTAACACATCATTTTCATATTTTTTCAGCATTTTTTTTTTTTTGAAAAAAAATCAAAATTACCTAACCTAATTATCTAAAATACCACATGTTAAATACTAAGTGAAATAGCGTAATCAATTACTCATATCCTCTTACCATCATTCAGAATGTGGCGGCCTTGCAAACTCTTTGATTAGCAGGTGATCATCGTTTAGAATGGCCAAGCCATTTTCTTGATGAGAAATTCTAGTAGTTGCGTATTATTTCTTTACTAGGGTAGGTCTGAAAGTTGATATATCAACAAGGTGAAACGCGCCATTTTTCGTGATATTCGCGTATGCTTTTTTAGGTCACGTGACTCGAGTACGGTCGAGGGATTTGCGATAGGAAATGAGATGACTGCGTGCTGAAGATTTTTTGACAAATTTAAAAACTGAATTTTTAATTAGAATAAAATGAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-38.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-38.txt
new file mode 100644
index 0000000..ebe839c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-38.txt
@@ -0,0 +1 @@
+TTATGAATTACCAAAGTATATACCAAGTATATCTATTTATATCCTATTTATAAAATGAATTCTTCTATCATTTATAACATTCAAATAAATTATAAAATCCATGGTTGTTAAAATAATGTGGGAATAAAAAGAGAATTAATTTCACAATATATGGTGATCAAAAATTACAAGTATCAACTTCGTTAAAAAAAAAATTACAACAAAAGTAAATATAAATTTCAATTTCATTTACAGCCTCGTTTGCACGAGACAGGTGGCAAATACGGAAATGGTGTGATAGTGAGAAAGTATCGAACAGGCTCTGTGATACCGGTCCGTGTGGAACTCACGGCGAATCACCACGGTTACTTCGAGTTCCGTACGTGTGCGATGACCTACAAGGACAAGGAAGTGGACCAAGATTGCTTGGACCAACATCTGCTTCGGTCGAAAAATGGATCGATCAGATATTATCCGGGGCCAGGAAACAAGATCTTCGAGGCTTTCTACAAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-39.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-39.txt
new file mode 100644
index 0000000..399734a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-39.txt
@@ -0,0 +1 @@
+GTTCGCCAGGGCAGTTTTCGTTTTTGCATTAACGACCAATTGTTGGTCCGGTGGTATTTGGGCGTTATTCGATAACGAGGTCAAAGAAGGCTCCTGTTGTGCACCCTGTTGTTGCTGTTGTTGTTGCTGCTGCTGCTGTAGATGCTGCAATTGTTGCAACCTTGCCATTTGTTGCTGCTTTTGCATATGTAATTGTGCTATGTGTTGAGGTGTCAAAGGTTGACCTTGCTGAAAGGTTTGACCAGGTTGACCAGCTTGTGGACCTTGCACATTTTGACCGGCTTGCACAATCACTTGATTAGAAGAATTAATCGGCGTTAAAGCAGGCGGTTGAACTCCTGAATGAACTGGTTTTTGACCAGGAGTTTGGGCAATTTGAGGTCCTATCATTTGAGGACGCGGTTGCTGAATCCACTGCAAACCACCAGGTTGAACTGGCTTTCCTGGACTTCTTATGACAGCGATATGCGCACCTTGTTGAGCTTGACGATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-4.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-4.txt
new file mode 100644
index 0000000..debfc54
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-4.txt
@@ -0,0 +1 @@
+TTTCACCAATGATACGATATTCCATCCAAGAAAAAAAAAGGTTTCCTCGAACAATTTCACAAAAGCAGACAAAAAGGCACAAAAAGAACAAAAAGATACACAAGAATGAAACATTTCTATCTTAGAAAGCTGCTATTCATCAGCTTTAGAGGGGGAAAAAAAGCTTAAACTGAATTCACACTTTCTTACAAATTCTTTTCTTAATACATAGAATTGCAGAGGACAAGAGATTTATTTACAAAATATTATATAGAGAGAGATTGTCAATTGTAACAAAAATAAAAGAATTCAATCAATGAAAAAAAAGAATTATATAGCAAAGAAATATATTACATACAATCTGGTGATATTAGATATTATAAGATTTGACCTAATCCCTACAGAAATCCTTAATTAAAACTACTAAAACTCCTTCCATTAGATACACTAAAATCTTTCTATTAATTGTATGTTTTCCTTCTCTAGATCCACTAAAATCTTTCCAAAAAACTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-40.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-40.txt
new file mode 100644
index 0000000..3585975
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-40.txt
@@ -0,0 +1 @@
+ATTGTAGGATACATATGATTTTTATTTACTTGAAAGAAAAATTATTAAATTTTTTTAAATTTCAATAAATTAAAAATAAAATTATATAGAATATTATAATAATAAAATTATTAAACGTAATAAATAATAATAATTTTAATATTAATGATTTAATGAAATGAGAAGCTTCTTTTAAGAAATTTAAATTTTCTTATCGACGTCGAAATTCTTATAATAATGATTTAAAGGCCAAAATTTAAAAAATCGAGTACTATTTTGAGTTTTATTTATTTTATTTAGATTTTATTTAAGTTTTAAAAAAATTAAATTAAAAAATTAATTATTATGCATGTTATTCATGTTTGTTTATTTATGATTTATATAATTAGTATAAAAAATTAACTTAAATTTTGAATATTTTTTAATTTTTAAAAATATTTTTAAATAATAGTTTAATAAATATTTATATATCAATATATACATATATATATATATTTATTAATATAAAATATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-41.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-41.txt
new file mode 100644
index 0000000..369ea36
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-41.txt
@@ -0,0 +1 @@
+AGATTTCCCTAGATAGGAGATTTCTCGATCTAGGGAAATTTTGTGAAACGAATGTTCCATATTATTTTCCTAGATAGAAGATTTCTCGATCTAGGGAAATTTTGTGAAACGAATGTTCCATATTATTTCCTGATGTGCAACGAATTTTCTTATTAGTCTTTTCAAACTATCTTACAGCACAAAATGTTGAAATTAAAATATATAGTATATTATTATAATTAAGTATATTATATTATAATTATTTAAATATATAAGTATATCATAAATGTATATATGAAAATACATAAATGAATTTATCACAGCTTTCTTTAACCCATTTTTGATTTAAAAAAAAGGAAATGTAACGAATCTTAAACATCGATGTACAAAAACCAAAAAATTTACGCGACTCGTAAAATTCCTCTAATAATATCTGCTCGATCGCGCAAGAGGGAGCAAAAAACTATCCACCCACAGAGATCAACGTATCCTCCCCCCTCCACTTATCTCTCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-42.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-42.txt
new file mode 100644
index 0000000..56716b5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-42.txt
@@ -0,0 +1 @@
+CAATCTTCAACTTTAAGCAATTCGCACATATAATTCCCTAGCGTGAACGAGATAAGAATCATGGCGAAATTATCTAAATATTAGGATGAAGTTGTGAGCAAAGAAAAAGAAGTATTAAGAAAAGGAGAAAAGACAGGGAAAAATAAAAAAGAGTTTGTTAGATAAAATATCCTTTTCGAAATATGATAAACTTATTTTAAAAATTTTAAAATTAACTTTCTTTTTTTTTACGACTAGTAAAAAAATATTTTTCTTTCTAATATTCCTTCTCTTTAATTTTCCTTCCAATTTTTCCAAAAGAATAAAAAAAAGGATAATGGATAAAAGCATAATTTCTTCTTGCTCATAGATTGATCATCTTCGACCAATTTTAATTAACAGATCGGAAAACCATCGTCGCAGCACGATTTTTTATCGAGCAATAAATTTCCACTGTATTTTAAATGTATCGTGGCTGAGAGACCCACCAGGGAGTTATATATCACACGAAGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-43.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-43.txt
new file mode 100644
index 0000000..dc91b12
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-43.txt
@@ -0,0 +1 @@
+TTGAATATGCGTGCGTTGGCATGTAATGAATAGTTGTTGGATCAGTTGTTGAATTTTGGAGGAAACTTTATTCGTATTCTATATATATATATATATATATGGGGTTTGGGTTTTTTGAGTTTTATTTGGATTTTTTTGAGTTTTATTTAAATTTAAAAAAAATTAAATTAAAAAATTAATTATTATTTAATTATATAATTATATAATATATAATATAATAAATAATATAATATAATAATAATTATATAATAAATATATATATATTATATATAAATTAATAATTATTATGCATATTATTCATATTTGTTTATTTATGATTTATATAATTAGTATAAAAAATTAACTTAAATTTTAATATTTTTTAATTTTTGAAAAAATTTTTAAATAATAGTTTAATAAATATTTATATATCAATATATTAATATTAATATATAATTATTATATAATATATAATTATATATTATAATATATTAAATATATTTATATTTATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-44.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-44.txt
new file mode 100644
index 0000000..03b8df4
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-44.txt
@@ -0,0 +1 @@
+NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-45.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-45.txt
new file mode 100644
index 0000000..c719724
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-45.txt
@@ -0,0 +1 @@
+TCGAGTCGGACACGATACGCGAGTGATACGAATTAATAATGGGCGAGCGAGTGAACAGTAAGTAGCGTAAGACAGTTCGAATCGGACACGAGTGATACGAATTAATAATGGACGAGCGAGTGGAACGAATCGAGTGTTGATGGTACGTAAGTGGTACAAGTCGATCAATTTGGCCAATGATAGATCAATTTATCAATTTATCAAACCAATAACATACGATTCGGAATTTAGTGGATAAGTGATATAAAATTAATTGTTGGTTGAGCTAGTAGCATAAATCGATTATGGTCAAACGTATAATAGAGATCTTTTCGGCTGATTAATTAATAATAATTGAAAATACGCATTTGTCGAATTGTAGAAAATCGAGGAAATGATTATATCACGCGGTGTTCCAACAAAGGGGGGAATTTCTGTGAATAAGCCTGGGCTGAGATAAAATTCATTAAGTTTCGCCTCCAACTAGGGAGTTCTCGAATGGAACGGTTTCTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-46.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-46.txt
new file mode 100644
index 0000000..7012c0d
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-46.txt
@@ -0,0 +1 @@
+AGGAATAATAATGTTTTTTTCAAGAGATAAAAATTTTGTGAAATAAAATGATTGAATTAAAAGTGAATTATCTAGTTTAATGCATACAGATCATGAATAAAATTGGAGTGGCACGAAAAGGAATATTTTGTTCGATTAAATAACGGAGTACATTTGGAGAATGGAAAAATGTAAAATAATTTAAAATAATTTCGTATAAATAATTATTTCCGTTTGTGTGTTGCGTTTAATTGAGAATGGATTAAAAATTGTTTGGTGCATTTCAATCGATAAACGATCCATGGTGAATATAAAACGACTATTTTTTCTTTTTTTTTTTTTTTTTTTAATTTTGCGTAATCTTTCCATGCTTTGATAGAATTTTGTATTCTTTTTTTTTTTTTTTTTTTTTTTTAGTAACTCAAGGATAAATTGAATTGGAAATATTCCATAAAACATTTTTTGGCAAATATTTACGTNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-47.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-47.txt
new file mode 100644
index 0000000..b5fc46b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-47.txt
@@ -0,0 +1 @@
+TTGAAAAATAGAACTGTTTCGATCAGTGCAGTACGTTTGGGAAAAAGCTGCTAAAGTAATGAAACTGGAACTCCAAATATTTTTAGCTAAAAGAGACTATTGATGATTCAAAATATATGTGTGTTTACGATGATTCCAAACGGTGAAAAGTGAGAAAATTTTCGATAAAGCAAATAGTGGAAAATAAAAAAAGTGATCAAGGAATGTGCAACAGTGGAAAAAAAGATCAAAAAGATCAAAGTGATAGTGACAAACGTCGGTGATCAACCGTGTAATCCATTTAACCGTGGAAATTAGTCATTGTAGAGAGGAAAAATGGAACTGTTGTGATTACTCGTTATTTCCTCTCAATTGAATATTGAGAACAATTGATTGATAATCAAAAGTGGAAAATTAGAAAATTAATTGGAACTCATTATTAACTGCTAATGCTTTTTGTTTCTTCGCCTATTTGGTAAGTTTTTTTCGAATATTTTTAAATAATCAATCGAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-48.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-48.txt
new file mode 100644
index 0000000..97add7c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-48.txt
@@ -0,0 +1 @@
+ATAGTCAACTCTTGCAGACCTTCATTTGCAAATTTGCCAACTTCCGATTGAATAACTGACACTCAGAATTTATATTTTTTTTCTATTTATTTAGTATGCACGATTTTCAAATCATTATATCCCAAATATTTAAAGTTGTTTGATTCTAATAATTCTATTGAAAAATATAGCAGATTCTTGTAATGCATATTTAATTGAGCTTTCTATTTTCATGAATAGAGAAATAGAGCCAAAGTCCTTTTATATATTTGGATATTGCAATAAATATACTATAAGTTAAAAAGAAAGAAATTAATATAATCCCTCTTCTATTAAAATCTTATATTTCAAAACAGTAATATCAAATTGTATTAATAATATAATAATTCTTCAATTATGATAAAATTAATATTGGAAAAATTTTTGTCTATTTCATATATATAAAATATTTCGATTATTACAAAGTAATAATTTTAATTAGATATACAAAGTATTATAGCAGCCTTTCAATCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-49.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-49.txt
new file mode 100644
index 0000000..48948f3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-49.txt
@@ -0,0 +1 @@
+TTGAAGAGAAAAATGTTTTGATTTATTAATTTAAAAATTTTTTTTTGCAAAAATATGCAAGGATAATTTACTTACGCGTTCAAAACAATATATAGTCGAAGAAAGTCTACCAATGAATTTTTTTGATATTCCGAAATATCAAATGTATTATATAAACATTCTGATAGAAATTGTACCCAATCACCAGTATCTTCTTTCAAATTCTTTAATTCTCCTTTAGCTATTAGACCGACTATTGCTGGTAGCGTTGCTAATAGTTCCATGCTATCTTCGTATGTGTACTTCAAAAATAAAAAAAAAATTCCAATTTTCTACTTATCAAATAAATAAAAATAAATATTTTTTACAAAATTAATTATTCATTCTAAAATTATGCAATTGTCTATTTTTAATAATCAGAATGTAATAAAAATTTTATAATTTTATTATCACTAAAATATTCTTTTTTCAATTTCCAGAATGTATTAATTTATTAATTTTCAAATTAAGAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-5.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-5.txt
new file mode 100644
index 0000000..894c699
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-5.txt
@@ -0,0 +1 @@
+CACAGACAAATAACGGATAAGATAGCAATGATGCAACTGACGAAGACGGCTGATCGTAATTCCGAAGTTAGACATAATTCTTATCCTCAAAAGTAGGGTCAGATTTATTCCGTTTTATGGAGATAATTAAACGAACGGTCATTGCCATTTATCTGTTATAATAAAATTGATTAAAATTCGATTACACAATAGCTGTTTTGAACTTAACTTTTATTTAAATTTTTGATGACATGTATAATATCGATTGATAAAAATTAATAAATATAAAATCATTGTAAATAATTCTTCAAAGTTAATCGATAAAAATAACAAAGAGATATTCAGAATAAAACTGTCTTTATATATTTTTTAATAAAACTTGCGAAAATGTTCTAGTTAAAGATTTAATAACAATTTCGATAAAAGTTCAAAAATAATAGCAGCTATTTATCATTCGTGTGATCCAATTTATTTAAAAAATGCATTCAATGTTGCACACGTTATGTCACAGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-50.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-50.txt
new file mode 100644
index 0000000..0974f20
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-50.txt
@@ -0,0 +1 @@
+GAGATAACTGCTATTTATATGTATCTTATGTTTAAGGAAAGAAAGGATGCTGTAATAGAACAGGAAGACCTATTTATCATACGACGAGGCCCAGCCGATCGAAACTGCTTTCCGGTAAAGCCACACTCCGAATGGGCATGCGCATTAAACAATCACGTAAGGATTTATGTTAAACAATAAAGTCGTATCGATTCACCTGTTTCTTCTATTTTTAATATCCAAGTCTCTCCAAGATAAATTTTCTTATTCCCATTTCTTTTAAGTGCAAAAAATTTTTTTTTATCTTTTATTTAAAATATAAATATATCACATAATTTTTAACTTAATTTTCGAAATTGTATAATTCAAAACATTTACTTGGATATAATAAAAATTTTGATTAGTTTGATCGAATAGAATAAATTGTATGGAATATTATATTATAAATATTTATAATTGTTTTATAATGTTAATATAATTGTTTTAATATAATTTTTTAATAATAATATTTTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-51.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-51.txt
new file mode 100644
index 0000000..ad6515e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-51.txt
@@ -0,0 +1 @@
+TTTTAGAATTATCTGAAGAATTAATGGCCTAACTCGATTGTGCCCTACCCTCTTCTTCTTAACAAGTTTCTTTCAACAAGTTGTACAGATTTTTGCGTGACAAACGGACTAACCACAAACGACTACGTTTTGTGATTCCACTAACTTTTCTGAAAAGACCACAGATAGAAAGACCATAAACGAAACCACAAATAATAGAAATTATATTTCTATTATATTGGCCATAAATTACAGCAGATTTCGTTATCCTTTGATCACTAAAAAATGCTAGATATTTTAATATTATCTATAAATTGATCACAACTCGTTGAAATTTTAATAAATTTCTCAATTTTGGAAATTCTTTCAACTATTTTGCATGCAATTCTTCAAACAACTGCACGAATGTAATCTAAAATAATATCATTTCTAGTCTCAAATAGTGGAAGAGGAAAGAATATTAGATGAAAAATCTTAAAATTTCTCAATCTTAATCTATTCAATTCTTATGCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-52.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-52.txt
new file mode 100644
index 0000000..d740d62
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-52.txt
@@ -0,0 +1 @@
+ACATAACAAGAAAAGTTCTAATTCGTTTTAATAAACCGTTTTAATACTAAACAACTGTTTAGTATTAACCTCTATTTCAGAAAGAAGTAATTACTCAGAAGAGAAAAATGGTTTTAATCTTTAAGTTTCGAATATATGTGTTCTTGAAAAAATAAAAAAATTAATAACTTTAGTTATTTCTTCTTTTCTTCTCAAATTTATGAACAAAATTTTTAATTATTTTCATTCTTTTTCTGCATTTTTCTACCTCCTCGTTTCTTCCATCAATTTTTTCTTATGCTCACAATTTTCTTTTTCTTCATGATGTACATAACCACAATAACACGATTCTTTTTTTTTTTTTTCAGTCAGATTCCTCCATTGCCTGGCTTAACAAAGAATGGACGACGAGTAATTGTAATGCGTGGTATCAACAAGGATCTTCCAACCCCAAACGTGGCAGAATTGATGAAACTGGTTCTAATGATCGGCGACGTACGTTTAAAAGAAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-53.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-53.txt
new file mode 100644
index 0000000..dc87d23
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-53.txt
@@ -0,0 +1 @@
+CGCGAACAACCCGTTACACTCGCGGAAAATCCATTTCGCGATACTTTGGTAAGTAGAAGGTTAGAAATTTTGGTGAAAAATTCGATGAAAGGGAACGAATCCGTGACTCGTGACAATAATGGCGAGTTGCGTTTTATTATGCGTGCGCATCGTTAAATTATGTTATACGAATTCCACCATTCGGCTGGACCGAAATTTATGACGTGCAATATGTACGCGTGGGACGTGAGATGAATCGTTCGTGATAAAAATGAAGCGATATTATTGGATGAAAATAAAAAATGGGATAGTTTGTACAGGACGGTTTCTGAAATGTTAATATTTTAAAAAGTGAATTTACTACAAAGAAAAATGAGATAGGAAGCTTATTCGAGCAATACCATGTAATAAAATATTCCATTGTTATAATTTTTCAAATTTTTTTCTCTTTCGATACTCGGATTTTTTAATTATGGTATAATGGTAATAAGATTAAGATTAAAGAATTCGTTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-54.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-54.txt
new file mode 100644
index 0000000..9989740
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-54.txt
@@ -0,0 +1 @@
+GCTTGTTAGGTGTTATTTTTGTGATTTGAATTTTAGCTATTATTTTTTTTTTTTTCAAATTATTCTGATTTGGATTATTCTTATTTTTACAAGTGAAATTTATTTACATAAATGAAATTTTAAAATCGTTTCATTTTATAATTCGATTTAAAACGTTTTAAATCATTTTACAAATCATTTTATATTCTATTAAAAGAAAAAATTGGAAATATTTGCATAAATATCTATAAATAACTGTAATAAATAACGATAGATTTTATCAATGAATATTTCTTTTTATTATATAGAACTATTTGCAATATTGATGTATTTTTTTAAATAAAAATGGATAATGATTTCTTTTACAAGATTATATTAGATTATTAGATTATAATAGTTAGTAAGTTTATATTAAGATTGAACTTATCATGTTCAATGTTAAACATGCATTTATATCATATTGCTAAGATATGCATTAATTTACATTGATACAAAAAACTGATTTTTATTTGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-55.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-55.txt
new file mode 100644
index 0000000..83e1094
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-55.txt
@@ -0,0 +1 @@
+TCGTTTGGGACAAATACGTTTTTGCGTCTTCCATCCGTATACGATTGCAATATCATTACAAGTAAATTGCATTTCGTATGAAAAAAAATTCACAAGAAACGTACATGAGAAAAATGTGCATTTTAATATCGTTAGCGGCCATTTTGAACTTATGAGTATCTCGAACTACGACGATTATCAACTTCGTCTTATAATTCCAAAATTGAACCTTTTTTTTTATGTCTTTCAATAGAGTACAAAAGCGTAAAGAACTTATTTATCATGTCATCTGAAAATATTTATTATTTTCTTTTTTTAAAATGAAATTCTAATAAGTGCTACAGAAAGCATTCCATTCATTTTATATTTTATATTTTAATATATATCTACTATATTATAATCTTGGTACGAATTTGAATACACGTTCAATTATAATTTGTTCATGACGAACGAAAGTAAATAACAAAAAAAGTAAATAAATTGCAAATTTTCTTTTATTCTTTATTATTTATG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-56.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-56.txt
new file mode 100644
index 0000000..cf630ae
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-56.txt
@@ -0,0 +1 @@
+TAATAATAGTTATCATTCGTACATCGAAGAACAAAATGAAACGTTATTTGAACACACCGATAGTACAAGTTTAAAACCTTTGTCCAATTTTACGATTGATTCGAACTCGGAACAAGAAAATTCGGATAATTTGGAAGATGAACAACGTACAAATATCGAAGAGGAGAATCATGAAGGATACATTCCATTGAGAACGCTGTTAAGGAACATGTTTGAGGAAATTTTAGGAAAGTATAATGGAATCGCAAGACAGTCAAATGTAAGTTCGATTAACAGAAGTTCAAACAGTGATTATTCAATGGATAATTGGGACAATGCAGATGAAGTAAATAATAATCTTTTAATTTGTCAAACACAAAAATCTAATAATATTGTTATAACCACTATCGATAATGTAAAAGATCCTGATAAAATTATTGCTTTATCTTCACAGCATATATTAGATTCTAGAAGCATAAATACAATATTAGAAAATTCCAATGAAAGTTCTAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-57.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-57.txt
new file mode 100644
index 0000000..865a6aa
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-57.txt
@@ -0,0 +1 @@
+AAAAAATAATTAGATACAAAAATGAAAAAATTATATTGAATACAAATCTTCTCTCTCTATTACTAAAATATTACTAAAATTACTAAAATAAATTACTAAAATAATTACTAAAATATTACTAAAAATAATTACTAAAATATTACTAAAAATAATTACTAAAATATTACTAAAATAATTACTAAAATAAATTACTAAAATAATTACTAAAATATTACTAAAATAAATTACTAAAATAATTACTAAAATAAATTACTAAAATAATTACTAAAATAAATTACTAAAATATTATTAAAATAATTCGTTATATAAATTTGTGATTATCTCATTATATAGATATCCAAATTTAAAGAAAAAGAAAGCATTTTTTGAGCATTTCAAAATCAAAGATTAAGAACAATAACAGCTGATTCTCCACAAAGATGTAAGAATAAAATGAAATACGTAAAAAACAAATTCAAAAATTTATCATACGTTCAATAAACGTAATAATAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-58.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-58.txt
new file mode 100644
index 0000000..fd51750
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-58.txt
@@ -0,0 +1 @@
+TATCGAAATAATATTAATCCTAATTTTAAACTTGATTCACTTTATTAAAATAATAGATCTCTATCTATAATAACGTATTCAATTACATGCTGTAACAATTTTCCATGAAGCGAGGGTTATGTTCCTCTTCCTATCACGTTAATGTTTCTAGTTATCTTCTATCTCACTAATCATGTCCTGATAAAACCGTAGATTTATTAGACTTAAAACTTTCTCAAGATACCGTATAAAATTCAATAATAATTATTTTTATTGGAAGAATTTAAAGAGTACTATAAAGTACTAGAAATTATAAAAATAAAAAAATATATATATATATTTTCATTAAAACAATAAAATTTATCTAAATTACCATTTTCATCACTGTTTAATCTAATAATAAAATTTCACAGGATATCTTTATCACTTTTAAATATTTAAAATAGTATAAAATATAAAATAATGCTTTTTGTCAACTTAATATTACAAATCAAAGGAAGTGAAAATAATTTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-59.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-59.txt
new file mode 100644
index 0000000..f3fcf7b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-59.txt
@@ -0,0 +1 @@
+TGTTAAATAATAATTTTTTTTTAAATAGATATTAATAAGTTTATATAAATCAATTCTTTAATTCTGAAAATTAATTTATAAAAATCTAAAATTTCTTTCTACACAACATAGTTATAATAATATATTTTCTTTTATTATTACAATTATACTAGAATAAAAATAACTATCAGTAGGAAAATATTCTTTATTAAATTCGATATAAGAGAGAATAGAAAAATTATTTTAACGTTTCATTATCTATCTTAATCGAGAACTAAATCAGAAAAAGTTTAATTTGAAGAAAATTAAATGATAAAAAGCAGATTGAACGGATAATGAGAATCCCTAGGGATGTTTTTTCAAAATCTTCAGTCTTGTAATCATTCGACTGCGAACATTGGCGCGCATGCGTAGAGCTTCTCCCCACCTGAGACGCAAATACACAGAGTCAGTGTAGAAACGGTCTGGCTTTGCCTCAGTATTTCGCCGCGTCTTGTTTCGTGAATATTGTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-6.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-6.txt
new file mode 100644
index 0000000..4e92b72
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-6.txt
@@ -0,0 +1 @@
+TAAAAAAAGTAATAATTATATATATTAATTTTACCTCATTAGTAAATTGTTTCATATGATTCAATACAGCATTCACATCTGGCATTACATCTTTATTATCAACCAATATTGGTTTATTAAGTCTATTACGTAAAGCAATATGTAAAACTGCTCTATTTTCTGTAAAATTAATTTTTTCTCCTTTAAACATAGCATCTCTTGCAGCTTCTACTTCACGTGCTTTTGCCTTAAAATTAATAAAATTTGATAAAATATATTAAAATATATTACATTTATATAAATTTTTGTAATTATACAATTTTAATAATTTCAATACATACCAATTCCAATAGTAATTGCAATGCTTCTTTAGTAAGACGATTTTTAGAATAATCAAGTAAAATTGGTCCATCTTCAGGAGTAGAAATCTCTAAGCTAATAACAATTAATATATTAAATATTAATAAAATACTTTTTAAATTAAATTTTTTAATTTCATTATAATAAAGCTAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-60.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-60.txt
new file mode 100644
index 0000000..aa8b773
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-60.txt
@@ -0,0 +1 @@
+AGATTTTTTTAGAAACTTTTGTAATTTCCAATTATATGACATATTCTATCAAAGATATAAAAATGAAAAAAAAATTAAAATCTATAAATAATATTAAACAAAGATATACATTAGAAAAGAATAAATCATTCTGTCCTTTCAAATAAGAAATTTTCAGCAACATCGATGTTTAATAGTACAATTAATTCGAACGAAAAATGTCGAGCGAAGCGTTAAATAGACAATAGAGAAAGGCGCATTAATTCGAAAAATCAAGCAAAAGAGAAGCAATTCGGTTCTGTGTGTGCGGATTGTTTGATTCTTCTCGCTAGCCTAGAGGTTCTTTTTAAACGGGGTCGTGACCCAATTTATTTAACCGGTTGAAAGACAGAGGAGAGACATTTTAGCCTTGGATGCAGAGCAATTAAGTTCTTATTCTTCTCGGTGGTGTGCACTTGCCACCGATTGGGCATCGTTGAAACCAATTTCGCTGTATAAGAGGACGCTATCCAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-61.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-61.txt
new file mode 100644
index 0000000..86411be
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-61.txt
@@ -0,0 +1 @@
+AGTAAAATTTATGGATAAAAGTGCAGTTTTATAATCTTATTAAAATAAAATTTTTGACTCAACAAACTAAGCAATTTTATTTAAATGTCTCGTTTTTTAAATTAAATTATGAATTTTCTAAACTATTTGGTCATCGTCTACATTTTTAATGCAAATTTTATTATCAATGAAATTGAAAAACATTCTTTCAAAAAATATTAAGAAAAAAATTGCAATTATCTATAAAGAATAATAATTAATCTATTATTAATCTAGATTAATTATTAACTTATATACATAAGTTAAATTAGGTTCACAGTCTTTAACGTATAGAGATTTTAATCATATAGAAAGAACAAAGTAAGGATCTAAACGATGAAGAAAGTTTGTTTTATGGATGAAATATAAACTTCTTTTTTATTATGTAATTACTTCATAAAAACTTTTACACAGTTTCACTTTATGGAGATTGATTGTTACTTGGCTATGGCAAGATGTATTGCAATTTTCGCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-62.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-62.txt
new file mode 100644
index 0000000..df52eec
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-62.txt
@@ -0,0 +1 @@
+GTGCAATGCTGTTGGTGTTTCTTTTATCATTTCGTGTTTCTTTGTCGATCGTTGTTTCTCCCCATAGGGTCTATCTTTGTTGGTGAAGAAACTTAAATTTTAATGTACTATTTATTTTAAGGAGTTAACAATTATTAGTAATCTTGAGAAGATAGTGAATGAATGAAATTTTATTTTTGAATATGTTCATTGATATTACTTTATTTTCATTGAAGAAACCTTTTGATAATTAATTTAAGATATTGAAAAATATTTTGTCATGAATATGATTTAAAAATTAAAAATTATTCTAAAAAAAGCATAGAATAATAATAGAAATATATATATATAAATGATAATAAGAAAGATTATTTAATTAATTAAAGATTTTATTATGTTACATTTTTAGCATAGTAGTTATTAATTTTTAGTATATTTATTTATTAATATATTAATAAATAAACAATTTTATGCATCAATTTCAAGTTCTTTTATTAATTATTACATCTTTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-63.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-63.txt
new file mode 100644
index 0000000..eab8f92
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-63.txt
@@ -0,0 +1 @@
+AAATTTAAATTAAAAATTAATTTACTTATTTAAAGATTTTAAAATATACAAAATAAATTGTCATTATTTTGATAAAAATGTTAATATTATGTATAGGAATTGTTGAAATAATTAATACAATTAAATTTATTAAATAATAAAATTCGTATAAAGCATTGTAAAAAATTCAAAATATGTTTTATTAATTTAAAAGTGAATTATAAATTATAAAATATTGTTATTGGTAAATTCATATCATGTGTTTTATTATATTATTTTATTATATTTAAAAAATTTATTTTATTTCTTTTCACATTGCATAAAAAGTATAAGATATATATATAATATATGTATAAAATAAATACATTTTATTAATGATTTGTATATTAAATATGTTATTTCGAGTAAATGTATGGAATAAGAAAAATAGTTAAAAAAAAATAATTATAAGAAATTTTCATTTTAAATTAGTTTACTTTATCAACAATTTGATAAAAATTAAAACAATATTGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-64.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-64.txt
new file mode 100644
index 0000000..69d9934
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-64.txt
@@ -0,0 +1 @@
+TTGCTGCTTCTTCGCTAAGGCCAACACAACCATATTCTAAAGGACTAAATACTGTTGTTGCTACATTCACATAATCCATTTGTTCCGTCGAATTTCCAAATAATCTTCTTGCTAATAATCGACCCGCATGTATAGCAACAGGAGTTAACTCTGGTTTTTTCTATAATACACAAGAAGAATTACAAATAATTACGAATTATAAATAATAATTTTACTTTAGTAATTTATATAATATCATGATATAATATCATAATTTATAATGATAATTATAATATATATTTTAATAATAATATAAAGTTAAATATAATTTTACATATTGAATAATGTATAAAATTAAATATAAATTGTAATAAAGTAGAGAAAGAAATCTATTGTTGAAAAAATTGAAAATTTAAAACTCACATGAAGTACATCACCAACAGCATATACATTTGGAACATTTGTTTGCTCATCTATTGCATCTATTTTTGCAGTTTCAGGAACAAGTTTAAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-65.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-65.txt
new file mode 100644
index 0000000..dcd9cd5
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-65.txt
@@ -0,0 +1 @@
+TAAAAATTTACATTTTTAATAATAAATATCGTTTTGTTCTTATAAATTTTTAAATTATTTTATTACTAGTTCGTATTTATACTGTATAGTGATTTATTTTTGCGGTTTTTGAAACTTGATTCTATTATTCATCTTGATGTGTAAATTTCAAACTATTTACTATAATTATTATTTTATTTTAGTTTTTATTTTAAAGAATTAATTTGATTCAAACTATTATAATAAATATTATATAAATAAATATATTTTAATAAATTTAAATAATACTTAATTAAATATTATTTTCAAGAATGTTATATATATAACAAATATTATATTCAAGTTATACACTATTTTATGTGAAAACAACCGAACGATAAAAAAGTAACATATATCGATAAATGAAATTTAAATATCAATGATTTTTTAAAATTATTTTATTTATAACTTTTCGTGATATATATCAGATTAAAACATAACTTAAATACAATTTAAAAATTATTTACAAATTAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-66.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-66.txt
new file mode 100644
index 0000000..ff7ab82
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-66.txt
@@ -0,0 +1 @@
+ATACGAATTATAATTAATAATCGCTGTGCGAGTGTATTAATTTTGTTATTGCTATCATTGTTATCAGTTTTTAATTATTCAACGCTTATTGTATTTCTATACAATCTAAAAAAATTTTAAAAAGAATTATTTAATTTTTAGAATTTTCTTTATCCTCGTGTGAAAGAGAATTTTATTTCAAGTTTTTTGTATTGACATACGTACGTGCGTAGACGACTAGTTAGGTTAGGTTACACTGGTGGTACAATTATATACGAATTATAATTAATAATCGCAGTGCGAGTGTATTAATTTTGTTACTGCTATCATTGTTATAAGTTTTTAATTATTCAACGCTTATTGTATTTCTATACAATCTAAAAAAAATTTTAAAAAGAATTATTTAATTTTTAGAATTTTCTTTATCCTCGCGTTTTATTTCATGTTTTTTTATTCGAACGTAAAAGTGAATCGAGACTGAGAAAAAGATCGAGCAACGCTGTAATATAATAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-67.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-67.txt
new file mode 100644
index 0000000..92dbb38
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-67.txt
@@ -0,0 +1 @@
+AGGAGACCCGGTATTAACACAACTGTCTATTGTCCGTTGTTGCATTACCGTGCCACGTGTAATCCAATATTTCCGTGTAATTTAAACTCTTCTCGTCTGTGCAGTACATTGCACGATGAAGAGGTAATTTCTCACCATGATGTTAGCACATTCTCTCATTGCACAGAAAATTTCATTCGCGTTTTAACACCCTCGCTCATGAAAAGTCTCATGATATATCATATTATATTTCATCGAAAATTAAATCATTACGCGTATTAGATAATTTAAGCGATAAGGAAAATTCATAGCTCAACTTAACACTTTGAAATATTTAAAAAAAAAAATGAAATTTTATCATTTTCATCTTTCTTTACCTAAATATGTAATTCTTTTTTCAATATGTAAATGTTATGATAATATATATATAAATATTAATAATAAATTATTTTATCATGGATATTTTTTACATTTATTATTATATATTTAAACAATAGCATATTTTAATTAGGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-68.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-68.txt
new file mode 100644
index 0000000..a4a4486
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-68.txt
@@ -0,0 +1 @@
+TTGCAGCACACGGATTACAGCACGGATTTCCTTGCCGACGGCAAGCAAAAAGGGGGGGCGGCCCCCGTTTCAGGGAATATGGACGATTCTGAGATATCGTCGACGAAGGCCGAATTCGGGGACGAGTCGTCGGTCAGTTTCCTAACGGCTAGCGAATTCCACAGAACGGGCATCTCGGCGATAGACGAGTCGTTCTCGAGCTGCGAGCGTGAATACGACATCGCCAAAGATCCGATGGCAATGTCTTTCACGCCGAGCGACTTCGAGGCGGCGTTCGACAAGGGTGTAGATTTGAACGCCGTTCACGACTTGAGCAATACCGATTTAGAGGAAGCGAATGGTATTTTGGACAAAGAGGAGGAGGAGGAGGAGGAGGAGGAGGAGGTTATGGAATCTCCAAACTTGGAAAACGATACTGTTAACGACAAAATGTCGAATATTATGTGTAACGTTGTGGAAAAGGCAGCAGAAAAGTCTGAAATTCGCGCAGAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-69.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-69.txt
new file mode 100644
index 0000000..b526e95
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-69.txt
@@ -0,0 +1 @@
+TATAATAATAATTTAACAAAAAAGTTTGTAGTATTGTTTTTTAAATTAAAGCATTATTAATGTTTAGTTTCTAAAAACAATTTCTAAATTATTTTAATTGATTTTTTCATCATTATTTTAATATTTCATTTTTATGAATTAATATTTTACACAAATTTTTCAAAATGAATTTATAAATACTAATGAAAATTTTCTGAATACATAAATGATGAAATCAATATATATATTGGAAATATGTAGTAGTATCTATTATATGTTGGCTGATAAGTCTGTGAATAAGGGCGCTGAGCCATTTGCCTCACTAAATGTTGATATATATTATTGGTAGTAGTAATAAGTAAATTCAGTCAATTATAAATTATAAAAATTTGTGACTCGACTATATGTTTGTGTAAGTTGATTTATTTCATTAGACCTTATATTTTTTTTGATGTCACTTATCAGCAATGTATAATGTATGCAAGCGCAGTTATAAAAATTTCTATCATAATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-7.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-7.txt
new file mode 100644
index 0000000..62f94f0
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-7.txt
@@ -0,0 +1 @@
+TTAATAATTATAACAATTTTAAGTAAGTTATACAATTTGTAAAATAAAGAAAAAAAAAACTTGAAAATTTAGTTAATATTTCTCAATTAATAATTGAAATAAATATAATAGAACTTACATTTGTTTCATTTTTTAATTTTGTTAGTATAGGCATTAACCATATTGGCTTTTTATTGAGTATATCATTTTGTTGCTCATGATTATTTACAACTGAATTACCAACAATTTTTGAACATGATTTTGTGTTCTTATTATTGAATGAACAAATAGGTGCCTCTACATGATCAGAAACATTCCACTTCCCAGTATTTTTTAATTTTATTGATTTTTTCTTCTTTGCTTGAGTGATAATATTTAATGTTTTTACAGTTGTATTTTTATTTTTTTTTAATTTTATTTTTTTATTTTCATCAGTATTCTCAATTAATAAATGATTCTTTCTTTTGTAATTATTTTTTAAATTTGATTTATAAGTATTTTGATTCTGATTCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-70.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-70.txt
new file mode 100644
index 0000000..b2d4a28
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-70.txt
@@ -0,0 +1 @@
+AATTTTTCTTATTATATAATATATATAGGTTAAAAGTTATTATCTTGAAAATTAAGAAGGGGGAAGAGAGGAGGAGGTCTTCTGATACTTGAAGAGATAATTCTTGACTATTTGGCTTAATATTTTTCTTAATAATTGTAAAAATCGTGTCAATTTTTTGATTTCTAAATTTATTAATTAGCTTGGAAATTGGTGGAAAATTTTTGATTAATTTTTAATTTTAATTATCATTTTATCGTGCCAAAATATATTCTTTTAATTATTGTTGATATTAGAAAGAAACTTATTATCAAGAACGTGATTAATTTAGTTTTTTATTTTCCAAGAATTTGATCAAATACTACCTTTTTTTTTTTTATCCGTGCGTAGGAATTGGCTACGCAGTTGTATTAATTGCGTTCTACGTCGATTTCTATTACAACGTGATCATCGCCTGGGCGCTGAGGTACTTCTTCGCCTCGTTCGCGAGTCTTTTACCTTGGACTACCTGCG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-8.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-8.txt
new file mode 100644
index 0000000..88613af
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-8.txt
@@ -0,0 +1 @@
+TAAAATAGATTTTATTTATATATTTATAATTTATATATAAAATTTATATAGTATTCCTCATAATATTTATAAATTATACAAACTTATTAATTTATTTACTTGTTAATTTAAACATTCAAATTATAGATAACAAGAATTATTTATCAATTATTATTTATGTCATTAATTTTGTAATATTTTATTTTATAAAAAATATAAATACATTTTTATAAAGTAAATAATAAATAAATAGTAAATAATAAATAAATTTTTACATGTGATGTAGGTAAATTGTGATTATTGAATTTAAGTTTACTTTATTTGTGAACGCGTTACATATTCGGTTTATTTTCGAACACTAACCCGGTATTCCACCGTGTCGAAGTGCTGACGTTGAAAAAATAAATTTAGGAGAAATTAATTTTGATTAAAATTTTTTAATGCATAATTTAATTAATAAATTTATTAAGTTTTGACATTTTATTTATTGATTATAAAATATAATAATGCATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-9.txt b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-9.txt
new file mode 100644
index 0000000..847e9e9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/seq/f78/c6f/0c/Group1.10-9.txt
@@ -0,0 +1 @@
+AATTATAAATAATGAAATAAATAATAAGTAAATAAAAAATATATATGACATGAATTATGAAATTAAATTTTTATTATAATATATTATATTTATTTATTATCCTATACTATTCTATATCTGTATTCTATATATGTATAGTAGATAGTAACCATAGAAACTCAGTCAAAACATTACAATTTTAAATAATGATTTCATGTATGTTTATGATTATTATAATTCTTTAAACACGATTATATAATTAGATTTATTTATTTTATAAATATGGAAGGCAATATTTCTCCAAATCTTCAAAAGTGGATAAAAAAATGTTTAAAAAGAGAATTTGAAAATAATATACAAAGATCATTAAAAGAAGATACAGATAATGTTAATATAGCTCAAAGTATTATACATTCAAAGGAAATGTCTATTTTAGTAAATTCTATGAAAGCTACAATTCTTGAACAATCTGCTTCGAGATCTGAATCAACTATAATTTCTGATTCTCGACCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/trackList.json b/test/integration/resources/sequences/honeybee-tracks/trackList.json
new file mode 100644
index 0000000..9bff072
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/trackList.json
@@ -0,0 +1 @@
+{"tracks":[{"chunkSize":20000,"storeClass":"JBrowse/Store/Sequence/StaticChunked","urlTemplate":"seq/{refseq_dirpath}/{refseq}-","category":"Reference sequence","type":"SequenceTrack","label":"DNA","key":"Reference sequence"}],"formatVersion":1}
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/hist-100000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/hist-100000-0.json
new file mode 100644
index 0000000..3498468
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/hist-100000-0.json	
@@ -0,0 +1 @@
+[2,1,3,13,1,8,1,1,3,6,1,5,7,9]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/names.txt
new file mode 100644
index 0000000..901462a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/names.txt	
@@ -0,0 +1,56 @@
+[["GB42164-RA","GB42164-RA"],"Official Gene Set v3.2","GB42164-RA","Group1.1",377,79932]
+[["GB42163-RA","GB42163-RA"],"Official Gene Set v3.2","GB42163-RA","Group1.1",90329,92340]
+[["GB42162-RA","GB42162-RA"],"Official Gene Set v3.2","GB42162-RA","Group1.1",139699,155458]
+[["GB42165-RA","GB42165-RA"],"Official Gene Set v3.2","GB42165-RA","Group1.1",204920,223005]
+[["GB42161-RA","GB42161-RA"],"Official Gene Set v3.2","GB42161-RA","Group1.1",212881,230560]
+[["GB42166-RA","GB42166-RA"],"Official Gene Set v3.2","GB42166-RA","Group1.1",261663,261884]
+[["GB42167-RA","GB42167-RA"],"Official Gene Set v3.2","GB42167-RA","Group1.1",300388,328828]
+[["GB42160-RA","GB42160-RA"],"Official Gene Set v3.2","GB42160-RA","Group1.1",308583,311615]
+[["GB42168-RA","GB42168-RA"],"Official Gene Set v3.2","GB42168-RA","Group1.1",329332,330416]
+[["GB42169-RA","GB42169-RA"],"Official Gene Set v3.2","GB42169-RA","Group1.1",332404,340627]
+[["GB42170-RA","GB42170-RA"],"Official Gene Set v3.2","GB42170-RA","Group1.1",342059,346812]
+[["GB42159-RA","GB42159-RA"],"Official Gene Set v3.2","GB42159-RA","Group1.1",347914,350517]
+[["GB42157-RA","GB42157-RA"],"Official Gene Set v3.2","GB42157-RA","Group1.1",355088,356284]
+[["GB42171-RA","GB42171-RA"],"Official Gene Set v3.2","GB42171-RA","Group1.1",364284,365201]
+[["GB42172-RA","GB42172-RA"],"Official Gene Set v3.2","GB42172-RA","Group1.1",371835,377442]
+[["GB42173-RA","GB42173-RA"],"Official Gene Set v3.2","GB42173-RA","Group1.1",380471,381221]
+[["GB42174-RA","GB42174-RA"],"Official Gene Set v3.2","GB42174-RA","Group1.1",381618,382653]
+[["GB42175-RA","GB42175-RA"],"Official Gene Set v3.2","GB42175-RA","Group1.1",382783,383335]
+[["GB42156-RA","GB42156-RA"],"Official Gene Set v3.2","GB42156-RA","Group1.1",383802,501601]
+[["GB42155-RA","GB42155-RA"],"Official Gene Set v3.2","GB42155-RA","Group1.1",507598,515039]
+[["GB42176-RA","GB42176-RA"],"Official Gene Set v3.2","GB42176-RA","Group1.1",509862,511494]
+[["GB42177-RA","GB42177-RA"],"Official Gene Set v3.2","GB42177-RA","Group1.1",516440,518622]
+[["GB42154-RA","GB42154-RA"],"Official Gene Set v3.2","GB42154-RA","Group1.1",519898,521017]
+[["GB42153-RA","GB42153-RA"],"Official Gene Set v3.2","GB42153-RA","Group1.1",522636,523368]
+[["GB42178-RA","GB42178-RA"],"Official Gene Set v3.2","GB42178-RA","Group1.1",559153,559540]
+[["GB42152-RA","GB42152-RA"],"Official Gene Set v3.2","GB42152-RA","Group1.1",561645,566383]
+[["GB42179-RA","GB42179-RA"],"Official Gene Set v3.2","GB42179-RA","Group1.1",645334,645703]
+[["GB42180-RA","GB42180-RA"],"Official Gene Set v3.2","GB42180-RA","Group1.1",724385,726232]
+[["GB42151-RA","GB42151-RA"],"Official Gene Set v3.2","GB42151-RA","Group1.1",822509,823418]
+[["GB42181-RA","GB42181-RA"],"Official Gene Set v3.2","GB42181-RA","Group1.1",885400,886380]
+[["GB42150-RA","GB42150-RA"],"Official Gene Set v3.2","GB42150-RA","Group1.1",898823,927500]
+[["GB42149-RA","GB42149-RA"],"Official Gene Set v3.2","GB42149-RA","Group1.1",932426,933252]
+[["GB42148-RA","GB42148-RA"],"Official Gene Set v3.2","GB42148-RA","Group1.1",937351,939602]
+[["GB42182-RA","GB42182-RA"],"Official Gene Set v3.2","GB42182-RA","Group1.1",940409,942604]
+[["GB42147-RA","GB42147-RA"],"Official Gene Set v3.2","GB42147-RA","Group1.1",962621,972777]
+[["GB42183-RA","GB42183-RA"],"Official Gene Set v3.2","GB42183-RA","Group1.1",976735,995721]
+[["GB42146-RA","GB42146-RA"],"Official Gene Set v3.2","GB42146-RA","Group1.1",1016795,1017215]
+[["GB42184-RA","GB42184-RA"],"Official Gene Set v3.2","GB42184-RA","Group1.1",1119385,1128829]
+[["GB42145-RA","GB42145-RA"],"Official Gene Set v3.2","GB42145-RA","Group1.1",1129346,1139006]
+[["GB42186-RA","GB42186-RA"],"Official Gene Set v3.2","GB42186-RA","Group1.1",1140477,1142948]
+[["GB42187-RA","GB42187-RA"],"Official Gene Set v3.2","GB42187-RA","Group1.1",1144386,1163392]
+[["GB42188-RA","GB42188-RA"],"Official Gene Set v3.2","GB42188-RA","Group1.1",1185776,1252627]
+[["GB42144-RA","GB42144-RA"],"Official Gene Set v3.2","GB42144-RA","Group1.1",1256648,1259085]
+[["GB42189-RA","GB42189-RA"],"Official Gene Set v3.2","GB42189-RA","Group1.1",1261060,1262599]
+[["GB42143-RA","GB42143-RA"],"Official Gene Set v3.2","GB42143-RA","Group1.1",1263434,1269094]
+[["GB42190-RA","GB42190-RA"],"Official Gene Set v3.2","GB42190-RA","Group1.1",1269634,1272952]
+[["GB42191-RA","GB42191-RA"],"Official Gene Set v3.2","GB42191-RA","Group1.1",1274047,1274996]
+[["GB42142-RA","GB42142-RA"],"Official Gene Set v3.2","GB42142-RA","Group1.1",1278463,1339411]
+[["GB42141-RA","GB42141-RA"],"Official Gene Set v3.2","GB42141-RA","Group1.1",1341560,1344251]
+[["GB42192-RA","GB42192-RA"],"Official Gene Set v3.2","GB42192-RA","Group1.1",1345269,1347149]
+[["GB42140-RA","GB42140-RA"],"Official Gene Set v3.2","GB42140-RA","Group1.1",1348275,1355790]
+[["GB42193-RA","GB42193-RA"],"Official Gene Set v3.2","GB42193-RA","Group1.1",1355527,1359457]
+[["GB42139-RA","GB42139-RA"],"Official Gene Set v3.2","GB42139-RA","Group1.1",1360316,1362726]
+[["GB42194-RA","GB42194-RA"],"Official Gene Set v3.2","GB42194-RA","Group1.1",1363377,1372583]
+[["GB42138-RA","GB42138-RA"],"Official Gene Set v3.2","GB42138-RA","Group1.1",1368797,1373985]
+[["GB42195-RA","GB42195-RA"],"Official Gene Set v3.2","GB42195-RA","Group1.1",1374622,1377247]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/trackData.json
new file mode 100644
index 0000000..0f90139
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.1/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"100000","max":13,"mean":4.35714285714286}],"meta":[{"basesPerBin":"100000","arrayParams":{"length":14,"chunkSize":10000,"urlTemplate":"hist-100000-{Chunk}.json"}}]},"featureCount":56,"intervals":{"nclist":[[0,377,79932,-1,"amel_OGSv3.2","Group1.1","GB42164-RA",0.786254,"GB42164-RA","mRNA",[[1,79851,79932,-1,"amel_OGSv3.2","Group1.1",0,"CDS"],[1,45148,45343,-1,"amel_OGSv3.2","Group1.1",0,"CDS"],[1,10315,10534,-1,"amel_OGSv3.2","Group1.1",0,"CDS"],[1 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/hist-50000-0.json
new file mode 100644
index 0000000..e9e4014
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/hist-50000-0.json	
@@ -0,0 +1 @@
+[4,3,9,15,7,4,5,3,4,6,3,7,2,6,9,6,2,2,3,6,5,7,8,3,6,14,3,6,1]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-1.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-1.json
new file mode 100644
index 0000000..6d414ed
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-1.json	
@@ -0,0 +1 @@
+[[0,19636,31167,-1,"amel_OGSv3.2","Group1.10","GB40789-RA",0.998677,"GB40789-RA","mRNA",[[1,30857,31167,-1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,19636,20199,-1,"amel_OGSv3.2","Group1.10",2,"CDS"],[3,19636,20199,-1,"amel_OGSv3.2","Group1.10","exon"],[3,30857,31167,-1,"amel_OGSv3.2","Group1.10","exon"]],{"Sublist":[[0,25715,29058,1,"amel_OGSv3.2","Group1.10","GB40791-RA",1,"GB40791-RA","mRNA",[[1,26946,27041,1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,27467,27550,1,"amel_OGSv3.2","Group1.10" [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-2.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-2.json
new file mode 100644
index 0000000..26774b9
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-2.json	
@@ -0,0 +1 @@
+[[0,588729,594164,1,"amel_OGSv3.2","Group1.10","GB40820-RA",0.999995,"GB40820-RA","mRNA",[[1,588729,588910,1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,592526,592678,1,"amel_OGSv3.2","Group1.10",2,"CDS"],[2,592678,592731,1,"amel_OGSv3.2","Group1.10",0.999995,"three_prime_UTR"],[2,593507,594164,1,"amel_OGSv3.2","Group1.10",0.999995,"three_prime_UTR"],[2,588729,588910,1,"amel_OGSv3.2","Group1.10",0.999995,"exon"],[2,592526,592731,1,"amel_OGSv3.2","Group1.10",0.999995,"exon"],[2,593507,594164,1 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-3.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-3.json
new file mode 100644
index 0000000..6508931
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/lf-3.json	
@@ -0,0 +1 @@
+[[0,1268021,1277382,-1,"amel_OGSv3.2","Group1.10","GB40719-RA",0.999996,"GB40719-RA","mRNA",[[1,1277244,1277382,-1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,1277003,1277150,-1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,1276790,1276894,-1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,1276341,1276496,-1,"amel_OGSv3.2","Group1.10",1,"CDS"],[1,1275970,1276086,-1,"amel_OGSv3.2","Group1.10",2,"CDS"],[1,1275660,1275781,-1,"amel_OGSv3.2","Group1.10",0,"CDS"],[1,1275397,1275533,-1,"amel_OGSv3.2","Group1.10",2, [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/names.txt
new file mode 100644
index 0000000..c2dc146
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/names.txt	
@@ -0,0 +1,143 @@
+[["GB40789-RA","GB40789-RA"],"Official Gene Set v3.2","GB40789-RA","Group1.10",19636,31167]
+[["GB40791-RA","GB40791-RA"],"Official Gene Set v3.2","GB40791-RA","Group1.10",25715,29058]
+[["GB40792-RA","GB40792-RA"],"Official Gene Set v3.2","GB40792-RA","Group1.10",30204,30902]
+[["GB40793-RA","GB40793-RA"],"Official Gene Set v3.2","GB40793-RA","Group1.10",30903,34222]
+[["GB40788-RA","GB40788-RA"],"Official Gene Set v3.2","GB40788-RA","Group1.10",65107,75367]
+[["GB40787-RA","GB40787-RA"],"Official Gene Set v3.2","GB40787-RA","Group1.10",77860,78076]
+[["GB40785-RA","GB40785-RA"],"Official Gene Set v3.2","GB40785-RA","Group1.10",95047,99274]
+[["GB40794-RA","GB40794-RA"],"Official Gene Set v3.2","GB40794-RA","Group1.10",102008,111044]
+[["GB40784-RA","GB40784-RA"],"Official Gene Set v3.2","GB40784-RA","Group1.10",114139,117112]
+[["GB40783-RA","GB40783-RA"],"Official Gene Set v3.2","GB40783-RA","Group1.10",117611,121048]
+[["GB40795-RA","GB40795-RA"],"Official Gene Set v3.2","GB40795-RA","Group1.10",121624,123083]
+[["GB40796-RA","GB40796-RA"],"Official Gene Set v3.2","GB40796-RA","Group1.10",124105,128011]
+[["GB40797-RA","GB40797-RA"],"Official Gene Set v3.2","GB40797-RA","Group1.10",128706,136964]
+[["GB40782-RA","GB40782-RA"],"Official Gene Set v3.2","GB40782-RA","Group1.10",139540,142804]
+[["GB40798-RA","GB40798-RA"],"Official Gene Set v3.2","GB40798-RA","Group1.10",143521,146600]
+[["GB40799-RA","GB40799-RA"],"Official Gene Set v3.2","GB40799-RA","Group1.10",147460,152444]
+[["GB40781-RA","GB40781-RA"],"Official Gene Set v3.2","GB40781-RA","Group1.10",157004,160632]
+[["GB40800-RA","GB40800-RA"],"Official Gene Set v3.2","GB40800-RA","Group1.10",160307,162089]
+[["GB40780-RA","GB40780-RA"],"Official Gene Set v3.2","GB40780-RA","Group1.10",162108,164110]
+[["GB40779-RA","GB40779-RA"],"Official Gene Set v3.2","GB40779-RA","Group1.10",165151,168088]
+[["GB40778-RA","GB40778-RA"],"Official Gene Set v3.2","GB40778-RA","Group1.10",169218,170512]
+[["GB40801-RA","GB40801-RA"],"Official Gene Set v3.2","GB40801-RA","Group1.10",171552,172893]
+[["GB40777-RA","GB40777-RA"],"Official Gene Set v3.2","GB40777-RA","Group1.10",172990,173822]
+[["GB40776-RA","GB40776-RA"],"Official Gene Set v3.2","GB40776-RA","Group1.10",173842,175041]
+[["GB40802-RA","GB40802-RA"],"Official Gene Set v3.2","GB40802-RA","Group1.10",176220,179786]
+[["GB40803-RA","GB40803-RA"],"Official Gene Set v3.2","GB40803-RA","Group1.10",180261,184514]
+[["GB40775-RA","GB40775-RA"],"Official Gene Set v3.2","GB40775-RA","Group1.10",184853,189006]
+[["GB40804-RA","GB40804-RA"],"Official Gene Set v3.2","GB40804-RA","Group1.10",190024,193580]
+[["GB40774-RA","GB40774-RA"],"Official Gene Set v3.2","GB40774-RA","Group1.10",194334,198677]
+[["GB40805-RA","GB40805-RA"],"Official Gene Set v3.2","GB40805-RA","Group1.10",199720,200913]
+[["GB40806-RA","GB40806-RA"],"Official Gene Set v3.2","GB40806-RA","Group1.10",202764,205331]
+[["GB40807-RA","GB40807-RA"],"Official Gene Set v3.2","GB40807-RA","Group1.10",208175,210517]
+[["GB40773-RA","GB40773-RA"],"Official Gene Set v3.2","GB40773-RA","Group1.10",210511,218447]
+[["GB40772-RA","GB40772-RA"],"Official Gene Set v3.2","GB40772-RA","Group1.10",219994,222245]
+[["GB40808-RA","GB40808-RA"],"Official Gene Set v3.2","GB40808-RA","Group1.10",223558,238576]
+[["GB40771-RA","GB40771-RA"],"Official Gene Set v3.2","GB40771-RA","Group1.10",239544,244130]
+[["GB40770-RA","GB40770-RA"],"Official Gene Set v3.2","GB40770-RA","Group1.10",252004,255153]
+[["GB40769-RA","GB40769-RA"],"Official Gene Set v3.2","GB40769-RA","Group1.10",258387,262558]
+[["GB40768-RA","GB40768-RA"],"Official Gene Set v3.2","GB40768-RA","Group1.10",283883,288448]
+[["GB40809-RA","GB40809-RA"],"Official Gene Set v3.2","GB40809-RA","Group1.10",291358,315160]
+[["GB40767-RA","GB40767-RA"],"Official Gene Set v3.2","GB40767-RA","Group1.10",315224,318933]
+[["GB40766-RA","GB40766-RA"],"Official Gene Set v3.2","GB40766-RA","Group1.10",321945,334304]
+[["GB40810-RA","GB40810-RA"],"Official Gene Set v3.2","GB40810-RA","Group1.10",335756,337187]
+[["GB40765-RA","GB40765-RA"],"Official Gene Set v3.2","GB40765-RA","Group1.10",337745,356288]
+[["GB40764-RA","GB40764-RA"],"Official Gene Set v3.2","GB40764-RA","Group1.10",358028,380412]
+[["GB40811-RA","GB40811-RA"],"Official Gene Set v3.2","GB40811-RA","Group1.10",367040,371901]
+[["GB40812-RA","GB40812-RA"],"Official Gene Set v3.2","GB40812-RA","Group1.10",403882,405154]
+[["GB40814-RA","GB40814-RA"],"Official Gene Set v3.2","GB40814-RA","Group1.10",414369,414600]
+[["GB40763-RA","GB40763-RA"],"Official Gene Set v3.2","GB40763-RA","Group1.10",414596,419617]
+[["GB40815-RA","GB40815-RA"],"Official Gene Set v3.2","GB40815-RA","Group1.10",433518,437436]
+[["GB40762-RA","GB40762-RA"],"Official Gene Set v3.2","GB40762-RA","Group1.10",450633,450881]
+[["GB40761-RA","GB40761-RA"],"Official Gene Set v3.2","GB40761-RA","Group1.10",453521,461017]
+[["GB40816-RA","GB40816-RA"],"Official Gene Set v3.2","GB40816-RA","Group1.10",463936,470649]
+[["GB40760-RA","GB40760-RA"],"Official Gene Set v3.2","GB40760-RA","Group1.10",471180,473615]
+[["GB40759-RA","GB40759-RA"],"Official Gene Set v3.2","GB40759-RA","Group1.10",475225,477671]
+[["GB40758-RA","GB40758-RA"],"Official Gene Set v3.2","GB40758-RA","Group1.10",490752,515455]
+[["GB40817-RA","GB40817-RA"],"Official Gene Set v3.2","GB40817-RA","Group1.10",521898,522200]
+[["GB40818-RA","GB40818-RA"],"Official Gene Set v3.2","GB40818-RA","Group1.10",524298,572691]
+[["GB40757-RA","GB40757-RA"],"Official Gene Set v3.2","GB40757-RA","Group1.10",568852,572554]
+[["GB40819-RA","GB40819-RA"],"Official Gene Set v3.2","GB40819-RA","Group1.10",577493,583605]
+[["GB40756-RA","GB40756-RA"],"Official Gene Set v3.2","GB40756-RA","Group1.10",582938,588668]
+[["GB40820-RA","GB40820-RA"],"Official Gene Set v3.2","GB40820-RA","Group1.10",588729,594164]
+[["GB40755-RA","GB40755-RA"],"Official Gene Set v3.2","GB40755-RA","Group1.10",598161,598924]
+[["GB40754-RA","GB40754-RA"],"Official Gene Set v3.2","GB40754-RA","Group1.10",599959,614037]
+[["GB40821-RA","GB40821-RA"],"Official Gene Set v3.2","GB40821-RA","Group1.10",621650,628275]
+[["GB40822-RA","GB40822-RA"],"Official Gene Set v3.2","GB40822-RA","Group1.10",654601,657144]
+[["GB40752-RA","GB40752-RA"],"Official Gene Set v3.2","GB40752-RA","Group1.10",664614,665729]
+[["GB40751-RA","GB40751-RA"],"Official Gene Set v3.2","GB40751-RA","Group1.10",675719,680586]
+[["GB40750-RA","GB40750-RA"],"Official Gene Set v3.2","GB40750-RA","Group1.10",689640,693859]
+[["GB40749-RA","GB40749-RA"],"Official Gene Set v3.2","GB40749-RA","Group1.10",694293,696055]
+[["GB40823-RA","GB40823-RA"],"Official Gene Set v3.2","GB40823-RA","Group1.10",697781,709269]
+[["GB40748-RA","GB40748-RA"],"Official Gene Set v3.2","GB40748-RA","Group1.10",708924,711780]
+[["GB40824-RA","GB40824-RA"],"Official Gene Set v3.2","GB40824-RA","Group1.10",713033,718259]
+[["GB40747-RA","GB40747-RA"],"Official Gene Set v3.2","GB40747-RA","Group1.10",721503,724550]
+[["GB40746-RA","GB40746-RA"],"Official Gene Set v3.2","GB40746-RA","Group1.10",725213,728130]
+[["GB40827-RA","GB40827-RA"],"Official Gene Set v3.2","GB40827-RA","Group1.10",729928,730304]
+[["GB40745-RA","GB40745-RA"],"Official Gene Set v3.2","GB40745-RA","Group1.10",731930,733316]
+[["GB40828-RA","GB40828-RA"],"Official Gene Set v3.2","GB40828-RA","Group1.10",734606,735570]
+[["GB40829-RA","GB40829-RA"],"Official Gene Set v3.2","GB40829-RA","Group1.10",747699,747966]
+[["GB40830-RA","GB40830-RA"],"Official Gene Set v3.2","GB40830-RA","Group1.10",751352,762365]
+[["GB40744-RA","GB40744-RA"],"Official Gene Set v3.2","GB40744-RA","Group1.10",761542,768063]
+[["GB40831-RA","GB40831-RA"],"Official Gene Set v3.2","GB40831-RA","Group1.10",772054,776061]
+[["GB40743-RA","GB40743-RA"],"Official Gene Set v3.2","GB40743-RA","Group1.10",775035,775413]
+[["GB40741-RA","GB40741-RA"],"Official Gene Set v3.2","GB40741-RA","Group1.10",776533,783352]
+[["GB40740-RA","GB40740-RA"],"Official Gene Set v3.2","GB40740-RA","Group1.10",787022,836988]
+[["GB40739-RA","GB40739-RA"],"Official Gene Set v3.2","GB40739-RA","Group1.10",845782,847278]
+[["GB40738-RA","GB40738-RA"],"Official Gene Set v3.2","GB40738-RA","Group1.10",874076,874361]
+[["GB40737-RA","GB40737-RA"],"Official Gene Set v3.2","GB40737-RA","Group1.10",891279,933037]
+[["GB40736-RA","GB40736-RA"],"Official Gene Set v3.2","GB40736-RA","Group1.10",938708,939601]
+[["GB40735-RA","GB40735-RA"],"Official Gene Set v3.2","GB40735-RA","Group1.10",949590,959315]
+[["GB40734-RA","GB40734-RA"],"Official Gene Set v3.2","GB40734-RA","Group1.10",969795,973976]
+[["GB40733-RA","GB40733-RA"],"Official Gene Set v3.2","GB40733-RA","Group1.10",974306,978671]
+[["GB40833-RA","GB40833-RA"],"Official Gene Set v3.2","GB40833-RA","Group1.10",974327,976988]
+[["GB40732-RA","GB40732-RA"],"Official Gene Set v3.2","GB40732-RA","Group1.10",978976,981616]
+[["GB40731-RA","GB40731-RA"],"Official Gene Set v3.2","GB40731-RA","Group1.10",982839,992713]
+[["GB40730-RA","GB40730-RA"],"Official Gene Set v3.2","GB40730-RA","Group1.10",1003945,1018115]
+[["GB40834-RA","GB40834-RA"],"Official Gene Set v3.2","GB40834-RA","Group1.10",1007258,1012113]
+[["GB40836-RA","GB40836-RA"],"Official Gene Set v3.2","GB40836-RA","Group1.10",1025091,1032236]
+[["GB40837-RA","GB40837-RA"],"Official Gene Set v3.2","GB40837-RA","Group1.10",1037679,1042109]
+[["GB40838-RA","GB40838-RA"],"Official Gene Set v3.2","GB40838-RA","Group1.10",1044550,1054985]
+[["GB40839-RA","GB40839-RA"],"Official Gene Set v3.2","GB40839-RA","Group1.10",1056924,1063615]
+[["GB40841-RA","GB40841-RA"],"Official Gene Set v3.2","GB40841-RA","Group1.10",1071210,1071492]
+[["GB40842-RA","GB40842-RA"],"Official Gene Set v3.2","GB40842-RA","Group1.10",1072289,1085667]
+[["GB40728-RA","GB40728-RA"],"Official Gene Set v3.2","GB40728-RA","Group1.10",1087635,1091074]
+[["GB40843-RA","GB40843-RA"],"Official Gene Set v3.2","GB40843-RA","Group1.10",1092561,1095202]
+[["GB40727-RA","GB40727-RA"],"Official Gene Set v3.2","GB40727-RA","Group1.10",1096497,1099052]
+[["GB40845-RA","GB40845-RA"],"Official Gene Set v3.2","GB40845-RA","Group1.10",1101319,1109687]
+[["GB40846-RA","GB40846-RA"],"Official Gene Set v3.2","GB40846-RA","Group1.10",1110369,1110654]
+[["GB40847-RA","GB40847-RA"],"Official Gene Set v3.2","GB40847-RA","Group1.10",1110704,1114148]
+[["GB40848-RA","GB40848-RA"],"Official Gene Set v3.2","GB40848-RA","Group1.10",1119759,1121258]
+[["GB40849-RA","GB40849-RA"],"Official Gene Set v3.2","GB40849-RA","Group1.10",1123210,1124008]
+[["GB40850-RA","GB40850-RA"],"Official Gene Set v3.2","GB40850-RA","Group1.10",1131869,1133808]
+[["GB40726-RA","GB40726-RA"],"Official Gene Set v3.2","GB40726-RA","Group1.10",1136700,1139093]
+[["GB40851-RA","GB40851-RA"],"Official Gene Set v3.2","GB40851-RA","Group1.10",1142654,1145352]
+[["GB40853-RA","GB40853-RA"],"Official Gene Set v3.2","GB40853-RA","Group1.10",1160594,1163814]
+[["GB40854-RA","GB40854-RA"],"Official Gene Set v3.2","GB40854-RA","Group1.10",1176903,1179001]
+[["GB40855-RA","GB40855-RA"],"Official Gene Set v3.2","GB40855-RA","Group1.10",1180589,1183235]
+[["GB40724-RA","GB40724-RA"],"Official Gene Set v3.2","GB40724-RA","Group1.10",1214039,1214249]
+[["GB40856-RA","GB40856-RA"],"Official Gene Set v3.2","GB40856-RA","Group1.10",1216824,1235616]
+[["GB40857-RA","GB40857-RA"],"Official Gene Set v3.2","GB40857-RA","Group1.10",1242150,1247022]
+[["GB40723-RA","GB40723-RA"],"Official Gene Set v3.2","GB40723-RA","Group1.10",1247493,1248208]
+[["GB40858-RA","GB40858-RA"],"Official Gene Set v3.2","GB40858-RA","Group1.10",1248775,1253496]
+[["GB40722-RA","GB40722-RA"],"Official Gene Set v3.2","GB40722-RA","Group1.10",1248797,1249052]
+[["GB40721-RA","GB40721-RA"],"Official Gene Set v3.2","GB40721-RA","Group1.10",1254490,1254801]
+[["GB40720-RA","GB40720-RA"],"Official Gene Set v3.2","GB40720-RA","Group1.10",1255246,1258835]
+[["GB40859-RA","GB40859-RA"],"Official Gene Set v3.2","GB40859-RA","Group1.10",1261785,1267170]
+[["GB40719-RA","GB40719-RA"],"Official Gene Set v3.2","GB40719-RA","Group1.10",1268021,1277382]
+[["GB40860-RA","GB40860-RA"],"Official Gene Set v3.2","GB40860-RA","Group1.10",1277947,1278834]
+[["GB40718-RA","GB40718-RA"],"Official Gene Set v3.2","GB40718-RA","Group1.10",1279597,1282168]
+[["GB40861-RA","GB40861-RA"],"Official Gene Set v3.2","GB40861-RA","Group1.10",1282112,1286907]
+[["GB40717-RA","GB40717-RA"],"Official Gene Set v3.2","GB40717-RA","Group1.10",1287738,1289338]
+[["GB40862-RA","GB40862-RA"],"Official Gene Set v3.2","GB40862-RA","Group1.10",1290367,1291342]
+[["GB40863-RA","GB40863-RA"],"Official Gene Set v3.2","GB40863-RA","Group1.10",1291955,1293149]
+[["GB40716-RA","GB40716-RA"],"Official Gene Set v3.2","GB40716-RA","Group1.10",1293342,1295254]
+[["GB40864-RA","GB40864-RA"],"Official Gene Set v3.2","GB40864-RA","Group1.10",1296556,1303382]
+[["GB40715-RA","GB40715-RA"],"Official Gene Set v3.2","GB40715-RA","Group1.10",1297782,1298484]
+[["GB40714-RA","GB40714-RA"],"Official Gene Set v3.2","GB40714-RA","Group1.10",1304894,1347482]
+[["GB40865-RA","GB40865-RA"],"Official Gene Set v3.2","GB40865-RA","Group1.10",1340115,1364870]
+[["GB40713-RA","GB40713-RA"],"Official Gene Set v3.2","GB40713-RA","Group1.10",1365530,1367665]
+[["GB40712-RA","GB40712-RA"],"Official Gene Set v3.2","GB40712-RA","Group1.10",1374882,1376243]
+[["GB40866-RA","GB40866-RA"],"Official Gene Set v3.2","GB40866-RA","Group1.10",1379118,1383492]
+[["GB40711-RA","GB40711-RA"],"Official Gene Set v3.2","GB40711-RA","Group1.10",1383896,1388510]
+[["GB40867-RA","GB40867-RA"],"Official Gene Set v3.2","GB40867-RA","Group1.10",1389396,1405215]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/trackData.json
new file mode 100644
index 0000000..eeb7de3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group1.10/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":15,"mean":5.48275862068965}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":29,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":143,"intervals":{"nclist":[[4,19636,588668,1],[4,588729,1267170,2],[4,1268021,1405215,3]],"classes":[{"isArrayAttr":{"Subfeatures":1},"attributes":["Start","End","Strand","Source","Seq_id","Name","Score","Id","Type","Subfeatures"]},{"isArrayAttr":{},"attributes":["Start","En [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/hist-50000-0.json
new file mode 100644
index 0000000..8ec0143
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/hist-50000-0.json	
@@ -0,0 +1 @@
+[3,4]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/names.txt
new file mode 100644
index 0000000..b49004e
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/names.txt	
@@ -0,0 +1,6 @@
+[["GB52238-RA","GB52238-RA"],"Official Gene Set v3.2","GB52238-RA","Group11.4",10257,18596]
+[["GB52239-RA","GB52239-RA"],"Official Gene Set v3.2","GB52239-RA","Group11.4",18905,56055]
+[["GB52237-RA","GB52237-RA"],"Official Gene Set v3.2","GB52237-RA","Group11.4",23630,25038]
+[["GB52236-RA","GB52236-RA"],"Official Gene Set v3.2","GB52236-RA","Group11.4",52853,58962]
+[["GB52235-RA","GB52235-RA"],"Official Gene Set v3.2","GB52235-RA","Group11.4",60154,61204]
+[["GB52234-RA","GB52234-RA"],"Official Gene Set v3.2","GB52234-RA","Group11.4",62507,64197]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/trackData.json
new file mode 100644
index 0000000..12f3c27
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.4/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":4,"mean":3.5}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":2,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":6,"intervals":{"nclist":[[0,10257,18596,1,"amel_OGSv3.2","Group11.4","GB52238-RA",0.710701,"GB52238-RA","mRNA",[[1,10257,10302,1,"amel_OGSv3.2","Group11.4",0,"CDS"],[1,12721,13053,1,"amel_OGSv3.2","Group11.4",0,"CDS"],[1,13233,13361,1,"amel_OGSv3.2","Group11.4",1,"CDS"],[1,13705,14372,1,"a [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/hist-50000-0.json
new file mode 100644
index 0000000..aad2e29
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/hist-50000-0.json	
@@ -0,0 +1 @@
+[7,4,2,1,1,3,1,3,2,2,0,1,2,3,2,5,9,6,8,3,7,8,7,11,8,2,2,8,6,3]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-1.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-1.json
new file mode 100644
index 0000000..d2853d3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-1.json	
@@ -0,0 +1 @@
+[[0,6958,8455,1,"amel_OGSv3.2","Group11.6","GB55191-RA",1,"GB55191-RA","mRNA",[[1,7124,7148,1,"amel_OGSv3.2","Group11.6",0,"CDS"],[1,7219,7301,1,"amel_OGSv3.2","Group11.6",0,"CDS"],[1,7369,7541,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[1,7621,7901,1,"amel_OGSv3.2","Group11.6",1,"CDS"],[2,6958,7124,1,"amel_OGSv3.2","Group11.6",1,"five_prime_UTR"],[2,7901,8455,1,"amel_OGSv3.2","Group11.6",1,"three_prime_UTR"],[2,6958,7148,1,"amel_OGSv3.2","Group11.6",1,"exon"],[2,7219,7301,1,"amel_OGSv3.2","G [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-2.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-2.json
new file mode 100644
index 0000000..2f00253
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/lf-2.json	
@@ -0,0 +1 @@
+[[0,1083799,1102753,1,"amel_OGSv3.2","Group11.6","GB55225-RA",0.999991,"GB55225-RA","mRNA",[[1,1084622,1084677,1,"amel_OGSv3.2","Group11.6",0,"CDS"],[1,1093908,1094026,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[1,1094705,1094764,1,"amel_OGSv3.2","Group11.6",1,"CDS"],[1,1096355,1096565,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[1,1096716,1096812,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[1,1097181,1097304,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[1,1097396,1097560,1,"amel_OGSv3.2","Group11.6",2,"CDS"],[ [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/names.txt
new file mode 100644
index 0000000..e0f24d7
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/names.txt	
@@ -0,0 +1,109 @@
+[["GB55191-RA","GB55191-RA"],"Official Gene Set v3.2","GB55191-RA","Group11.6",6958,8455]
+[["GB55190-RA","GB55190-RA"],"Official Gene Set v3.2","GB55190-RA","Group11.6",9777,18134]
+[["GB55189-RA","GB55189-RA"],"Official Gene Set v3.2","GB55189-RA","Group11.6",20170,23586]
+[["GB55188-RA","GB55188-RA"],"Official Gene Set v3.2","GB55188-RA","Group11.6",35572,41228]
+[["GB55187-RA","GB55187-RA"],"Official Gene Set v3.2","GB55187-RA","Group11.6",41827,44487]
+[["GB55192-RA","GB55192-RA"],"Official Gene Set v3.2","GB55192-RA","Group11.6",44858,46673]
+[["GB55186-RA","GB55186-RA"],"Official Gene Set v3.2","GB55186-RA","Group11.6",45799,51375]
+[["GB55193-RA","GB55193-RA"],"Official Gene Set v3.2","GB55193-RA","Group11.6",52308,55437]
+[["GB55185-RA","GB55185-RA"],"Official Gene Set v3.2","GB55185-RA","Group11.6",54127,55961]
+[["GB55184-RA","GB55184-RA"],"Official Gene Set v3.2","GB55184-RA","Group11.6",63112,73596]
+[["GB55194-RA","GB55194-RA"],"Official Gene Set v3.2","GB55194-RA","Group11.6",112863,113759]
+[["GB55183-RA","GB55183-RA"],"Official Gene Set v3.2","GB55183-RA","Group11.6",146415,250375]
+[["GB55195-RA","GB55195-RA"],"Official Gene Set v3.2","GB55195-RA","Group11.6",260134,261118]
+[["GB55196-RA","GB55196-RA"],"Official Gene Set v3.2","GB55196-RA","Group11.6",288780,349151]
+[["GB55197-RA","GB55197-RA"],"Official Gene Set v3.2","GB55197-RA","Group11.6",352853,353857]
+[["GB55181-RA","GB55181-RA"],"Official Gene Set v3.2","GB55181-RA","Group11.6",362085,362901]
+[["GB55180-RA","GB55180-RA"],"Official Gene Set v3.2","GB55180-RA","Group11.6",384530,462365]
+[["GB55198-RA","GB55198-RA"],"Official Gene Set v3.2","GB55198-RA","Group11.6",403343,438354]
+[["GB55179-RA","GB55179-RA"],"Official Gene Set v3.2","GB55179-RA","Group11.6",474003,477572]
+[["GB55199-RA","GB55199-RA"],"Official Gene Set v3.2","GB55199-RA","Group11.6",598286,616835]
+[["GB55178-RA","GB55178-RA"],"Official Gene Set v3.2","GB55178-RA","Group11.6",640524,647862]
+[["GB55177-RA","GB55177-RA"],"Official Gene Set v3.2","GB55177-RA","Group11.6",652023,652149]
+[["GB55200-RA","GB55200-RA"],"Official Gene Set v3.2","GB55200-RA","Group11.6",681018,758031]
+[["GB55175-RA","GB55175-RA"],"Official Gene Set v3.2","GB55175-RA","Group11.6",681877,685392]
+[["GB55174-RA","GB55174-RA"],"Official Gene Set v3.2","GB55174-RA","Group11.6",728954,732319]
+[["GB55172-RA","GB55172-RA"],"Official Gene Set v3.2","GB55172-RA","Group11.6",772246,772480]
+[["GB55201-RA","GB55201-RA"],"Official Gene Set v3.2","GB55201-RA","Group11.6",780757,791467]
+[["GB55171-RA","GB55171-RA"],"Official Gene Set v3.2","GB55171-RA","Group11.6",782260,785762]
+[["GB55202-RA","GB55202-RA"],"Official Gene Set v3.2","GB55202-RA","Group11.6",798636,806542]
+[["GB55203-RA","GB55203-RA"],"Official Gene Set v3.2","GB55203-RA","Group11.6",811675,816314]
+[["GB55204-RA","GB55204-RA"],"Official Gene Set v3.2","GB55204-RA","Group11.6",819374,822918]
+[["GB55205-RA","GB55205-RA"],"Official Gene Set v3.2","GB55205-RA","Group11.6",825495,828771]
+[["GB55206-RA","GB55206-RA"],"Official Gene Set v3.2","GB55206-RA","Group11.6",831410,834950]
+[["GB55207-RA","GB55207-RA"],"Official Gene Set v3.2","GB55207-RA","Group11.6",837526,840637]
+[["GB55208-RA","GB55208-RA"],"Official Gene Set v3.2","GB55208-RA","Group11.6",842501,845748]
+[["GB55209-RA","GB55209-RA"],"Official Gene Set v3.2","GB55209-RA","Group11.6",845839,846433]
+[["GB55210-RA","GB55210-RA"],"Official Gene Set v3.2","GB55210-RA","Group11.6",848765,849806]
+[["GB55211-RA","GB55211-RA"],"Official Gene Set v3.2","GB55211-RA","Group11.6",852487,855360]
+[["GB55212-RA","GB55212-RA"],"Official Gene Set v3.2","GB55212-RA","Group11.6",860287,864039]
+[["GB55213-RA","GB55213-RA"],"Official Gene Set v3.2","GB55213-RA","Group11.6",866691,870062]
+[["GB55214-RA","GB55214-RA"],"Official Gene Set v3.2","GB55214-RA","Group11.6",871713,874748]
+[["GB55215-RA","GB55215-RA"],"Official Gene Set v3.2","GB55215-RA","Group11.6",880668,884668]
+[["GB55216-RA","GB55216-RA"],"Official Gene Set v3.2","GB55216-RA","Group11.6",888425,900344]
+[["GB55217-RA","GB55217-RA"],"Official Gene Set v3.2","GB55217-RA","Group11.6",902694,914685]
+[["GB55218-RA","GB55218-RA"],"Official Gene Set v3.2","GB55218-RA","Group11.6",919973,922784]
+[["GB55219-RA","GB55219-RA"],"Official Gene Set v3.2","GB55219-RA","Group11.6",929234,930496]
+[["GB55170-RA","GB55170-RA"],"Official Gene Set v3.2","GB55170-RA","Group11.6",931803,934117]
+[["GB55169-RA","GB55169-RA"],"Official Gene Set v3.2","GB55169-RA","Group11.6",934562,937542]
+[["GB55168-RA","GB55168-RA"],"Official Gene Set v3.2","GB55168-RA","Group11.6",937649,937859]
+[["GB55220-RA","GB55220-RA"],"Official Gene Set v3.2","GB55220-RA","Group11.6",938560,940738]
+[["GB55166-RA","GB55166-RA"],"Official Gene Set v3.2","GB55166-RA","Group11.6",950412,952176]
+[["GB55165-RA","GB55165-RA"],"Official Gene Set v3.2","GB55165-RA","Group11.6",958675,971020]
+[["GB55164-RA","GB55164-RA"],"Official Gene Set v3.2","GB55164-RA","Group11.6",972515,1000575]
+[["GB55163-RA","GB55163-RA"],"Official Gene Set v3.2","GB55163-RA","Group11.6",1001062,1003211]
+[["GB55162-RA","GB55162-RA"],"Official Gene Set v3.2","GB55162-RA","Group11.6",1003425,1036226]
+[["GB55161-RA","GB55161-RA"],"Official Gene Set v3.2","GB55161-RA","Group11.6",1037674,1039361]
+[["GB55222-RA","GB55222-RA"],"Official Gene Set v3.2","GB55222-RA","Group11.6",1039504,1040657]
+[["GB55160-RA","GB55160-RA"],"Official Gene Set v3.2","GB55160-RA","Group11.6",1043001,1047696]
+[["GB55159-RA","GB55159-RA"],"Official Gene Set v3.2","GB55159-RA","Group11.6",1048829,1050569]
+[["GB55223-RA","GB55223-RA"],"Official Gene Set v3.2","GB55223-RA","Group11.6",1050588,1055871]
+[["GB55158-RA","GB55158-RA"],"Official Gene Set v3.2","GB55158-RA","Group11.6",1055390,1059138]
+[["GB55157-RA","GB55157-RA"],"Official Gene Set v3.2","GB55157-RA","Group11.6",1059456,1060485]
+[["GB55156-RA","GB55156-RA"],"Official Gene Set v3.2","GB55156-RA","Group11.6",1060976,1062575]
+[["GB55224-RA","GB55224-RA"],"Official Gene Set v3.2","GB55224-RA","Group11.6",1069893,1078065]
+[["GB55155-RA","GB55155-RA"],"Official Gene Set v3.2","GB55155-RA","Group11.6",1078032,1080855]
+[["GB55225-RA","GB55225-RA"],"Official Gene Set v3.2","GB55225-RA","Group11.6",1083799,1102753]
+[["GB55226-RA","GB55226-RA"],"Official Gene Set v3.2","GB55226-RA","Group11.6",1104002,1105468]
+[["GB55154-RA","GB55154-RA"],"Official Gene Set v3.2","GB55154-RA","Group11.6",1115722,1118243]
+[["GB55227-RA","GB55227-RA"],"Official Gene Set v3.2","GB55227-RA","Group11.6",1125631,1128135]
+[["GB55228-RA","GB55228-RA"],"Official Gene Set v3.2","GB55228-RA","Group11.6",1130601,1143781]
+[["GB55153-RA","GB55153-RA"],"Official Gene Set v3.2","GB55153-RA","Group11.6",1145660,1146121]
+[["GB55229-RA","GB55229-RA"],"Official Gene Set v3.2","GB55229-RA","Group11.6",1145911,1160703]
+[["GB55152-RA","GB55152-RA"],"Official Gene Set v3.2","GB55152-RA","Group11.6",1158819,1161703]
+[["GB55230-RA","GB55230-RA"],"Official Gene Set v3.2","GB55230-RA","Group11.6",1161532,1161748]
+[["GB55151-RA","GB55151-RA"],"Official Gene Set v3.2","GB55151-RA","Group11.6",1161973,1168003]
+[["GB55231-RA","GB55231-RA"],"Official Gene Set v3.2","GB55231-RA","Group11.6",1162898,1163997]
+[["GB55150-RA","GB55150-RA"],"Official Gene Set v3.2","GB55150-RA","Group11.6",1170112,1170298]
+[["GB55149-RA","GB55149-RA"],"Official Gene Set v3.2","GB55149-RA","Group11.6",1170299,1170963]
+[["GB55148-RA","GB55148-RA"],"Official Gene Set v3.2","GB55148-RA","Group11.6",1174355,1179544]
+[["GB55147-RA","GB55147-RA"],"Official Gene Set v3.2","GB55147-RA","Group11.6",1182762,1187123]
+[["GB55232-RA","GB55232-RA"],"Official Gene Set v3.2","GB55232-RA","Group11.6",1188368,1192343]
+[["GB55146-RA","GB55146-RA"],"Official Gene Set v3.2","GB55146-RA","Group11.6",1188591,1201455]
+[["GB55145-RA","GB55145-RA"],"Official Gene Set v3.2","GB55145-RA","Group11.6",1201850,1206398]
+[["GB55233-RA","GB55233-RA"],"Official Gene Set v3.2","GB55233-RA","Group11.6",1206691,1209698]
+[["GB55144-RA","GB55144-RA"],"Official Gene Set v3.2","GB55144-RA","Group11.6",1207031,1221773]
+[["GB55143-RA","GB55143-RA"],"Official Gene Set v3.2","GB55143-RA","Group11.6",1221975,1230778]
+[["GB55235-RA","GB55235-RA"],"Official Gene Set v3.2","GB55235-RA","Group11.6",1235622,1236513]
+[["GB55142-RA","GB55142-RA"],"Official Gene Set v3.2","GB55142-RA","Group11.6",1237124,1243571]
+[["GB55236-RA","GB55236-RA"],"Official Gene Set v3.2","GB55236-RA","Group11.6",1244024,1248917]
+[["GB55237-RA","GB55237-RA"],"Official Gene Set v3.2","GB55237-RA","Group11.6",1258559,1293613]
+[["GB55141-RA","GB55141-RA"],"Official Gene Set v3.2","GB55141-RA","Group11.6",1296923,1297147]
+[["GB55238-RA","GB55238-RA"],"Official Gene Set v3.2","GB55238-RA","Group11.6",1309204,1324803]
+[["GB55239-RA","GB55239-RA"],"Official Gene Set v3.2","GB55239-RA","Group11.6",1337152,1357348]
+[["GB55240-RA","GB55240-RA"],"Official Gene Set v3.2","GB55240-RA","Group11.6",1360027,1361183]
+[["GB55140-RA","GB55140-RA"],"Official Gene Set v3.2","GB55140-RA","Group11.6",1360165,1361954]
+[["GB55241-RA","GB55241-RA"],"Official Gene Set v3.2","GB55241-RA","Group11.6",1364028,1385802]
+[["GB55242-RA","GB55242-RA"],"Official Gene Set v3.2","GB55242-RA","Group11.6",1388370,1392039]
+[["GB55139-RA","GB55139-RA"],"Official Gene Set v3.2","GB55139-RA","Group11.6",1392306,1393293]
+[["GB55243-RA","GB55243-RA"],"Official Gene Set v3.2","GB55243-RA","Group11.6",1394442,1396185]
+[["GB55244-RA","GB55244-RA"],"Official Gene Set v3.2","GB55244-RA","Group11.6",1396859,1398690]
+[["GB55245-RA","GB55245-RA"],"Official Gene Set v3.2","GB55245-RA","Group11.6",1400396,1401134]
+[["GB55246-RA","GB55246-RA"],"Official Gene Set v3.2","GB55246-RA","Group11.6",1401141,1402272]
+[["GB55247-RA","GB55247-RA"],"Official Gene Set v3.2","GB55247-RA","Group11.6",1404403,1408577]
+[["GB55138-RA","GB55138-RA"],"Official Gene Set v3.2","GB55138-RA","Group11.6",1411536,1411799]
+[["GB55248-RA","GB55248-RA"],"Official Gene Set v3.2","GB55248-RA","Group11.6",1417489,1419116]
+[["GB55137-RA","GB55137-RA"],"Official Gene Set v3.2","GB55137-RA","Group11.6",1429052,1430239]
+[["GB55136-RA","GB55136-RA"],"Official Gene Set v3.2","GB55136-RA","Group11.6",1452494,1453043]
+[["GB55250-RA","GB55250-RA"],"Official Gene Set v3.2","GB55250-RA","Group11.6",1464577,1470495]
+[["GB55251-RA","GB55251-RA"],"Official Gene Set v3.2","GB55251-RA","Group11.6",1494115,1494475]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/trackData.json
new file mode 100644
index 0000000..25b3a74
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group11.6/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":11,"mean":4.23333333333333}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":30,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":109,"intervals":{"nclist":[[4,6958,1080855,1],[4,1083799,1494475,2]],"classes":[{"isArrayAttr":{"Subfeatures":1},"attributes":["Start","End","Strand","Source","Seq_id","Name","Score","Id","Type","Subfeatures"]},{"isArrayAttr":{},"attributes":["Start","End","Strand","Source", [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/hist-50000-0.json
new file mode 100644
index 0000000..c2df5df
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/hist-50000-0.json	
@@ -0,0 +1 @@
+[11,9,10,5,9,3,5,2,4,6,10,7,7,1,1,6,13,15,11,12,5,1,1,1,7,2,7,4,7,2,4,1,5,4,2,4,4,8,5,6,6,12,2,5,5,1,5,10,4,2,1,2,3,1,9,8,2,2,1,3,1,0,0,1,0,0,2,5,2,7,4,6,6,4,2,2,1,2]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-1.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-1.json
new file mode 100644
index 0000000..7e02f70
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-1.json	
@@ -0,0 +1 @@
+[[84,1643,6749,1,"GB55506-RA","mRNA","amel_OGSv3.2","Group2.19",[[42,2008,2104,1,"CDS",0,"Group2.19","amel_OGSv3.2"],[44,2440,2755,1,"Group2.19","amel_OGSv3.2","CDS",0],[34,3335,3548,1,0,"CDS","amel_OGSv3.2","Group2.19"],[95,3617,3839,1,"Group2.19","amel_OGSv3.2","CDS",0],[68,3940,4442,1,0,"CDS","amel_OGSv3.2","Group2.19"],[69,4560,5024,1,"CDS",2,"Group2.19","amel_OGSv3.2"],[25,1643,2008,1,"amel_OGSv3.2","Group2.19","five_prime_UTR",1],[70,5024,6749,1,"three_prime_UTR",1,"amel_OGSv3.2"," [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-2.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-2.json
new file mode 100644
index 0000000..78fa576
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-2.json	
@@ -0,0 +1 @@
+[[93,310956,384922,-1,[[13,380353,380666,-1,"Group2.19","amel_OGSv3.2","CDS",0],[41,380039,380061,-1,"amel_OGSv3.2","Group2.19",2,"CDS"],[14,378101,378264,-1,1,"CDS","amel_OGSv3.2","Group2.19"],[31,377420,377562,-1,"CDS",0,"Group2.19","amel_OGSv3.2"],[30,375863,375999,-1,"CDS",2,"Group2.19","amel_OGSv3.2"],[62,375413,375617,-1,"amel_OGSv3.2","Group2.19",1,"CDS"],[69,373712,374014,-1,"CDS",1,"Group2.19","amel_OGSv3.2"],[95,373081,373305,-1,"Group2.19","amel_OGSv3.2","CDS",2],[67,372315,37 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-3.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-3.json
new file mode 100644
index 0000000..ac14d44
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-3.json	
@@ -0,0 +1 @@
+[[101,759950,766520,1,1,[[22,759950,760035,1,"amel_OGSv3.2","Group2.19",0,"CDS"],[22,760678,760879,1,"amel_OGSv3.2","Group2.19",2,"CDS"],[16,761930,762111,1,"amel_OGSv3.2","Group2.19",2,"CDS"],[40,763768,763930,1,1,"CDS","amel_OGSv3.2","Group2.19"],[43,765883,766049,1,"CDS",1,"Group2.19","amel_OGSv3.2"],[44,766301,766520,1,"Group2.19","amel_OGSv3.2","CDS",0],[51,759950,760035,1,"amel_OGSv3.2","Group2.19","exon"],[49,760678,760879,1,"exon","amel_OGSv3.2","Group2.19"],[45,761930,762111,1," [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-4.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-4.json
new file mode 100644
index 0000000..9da9cde
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-4.json	
@@ -0,0 +1 @@
+[[105,1201804,1216947,1,"amel_OGSv3.2","Group2.19","mRNA","GB55578-RA",0.836166,[[64,1201804,1201909,1,0,"CDS","amel_OGSv3.2","Group2.19"],[42,1202137,1202263,1,"CDS",0,"Group2.19","amel_OGSv3.2"],[95,1205363,1205544,1,"Group2.19","amel_OGSv3.2","CDS",0],[39,1205710,1206030,1,"Group2.19","amel_OGSv3.2","CDS",2],[15,1206294,1206559,1,0,"CDS","amel_OGSv3.2","Group2.19"],[12,1208445,1209151,1,"Group2.19","amel_OGSv3.2","CDS",2],[67,1209555,1209785,1,"CDS",1,"Group2.19","amel_OGSv3.2"],[65,1 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-5.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-5.json
new file mode 100644
index 0000000..7961465
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-5.json	
@@ -0,0 +1 @@
+[[111,1921525,1934106,-1,"amel_OGSv3.2","Group2.19","GB55405-RA","mRNA",[[40,1933625,1934106,-1,0,"CDS","amel_OGSv3.2","Group2.19"],[12,1928506,1928683,-1,"Group2.19","amel_OGSv3.2","CDS",2],[62,1924128,1924269,-1,"amel_OGSv3.2","Group2.19",2,"CDS"],[62,1922725,1923118,-1,"amel_OGSv3.2","Group2.19",2,"CDS"],[23,1922454,1922639,-1,"CDS",2,"Group2.19","amel_OGSv3.2"],[30,1921525,1922185,-1,"CDS",0,"Group2.19","amel_OGSv3.2"],[57,1921525,1922185,-1,"exon","amel_OGSv3.2","Group2.19"],[51,192 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-6.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-6.json
new file mode 100644
index 0000000..14e39ae
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-6.json	
@@ -0,0 +1 @@
+[[102,2666654,2696444,-1,[[65,2691776,2691839,-1,"Group2.19","amel_OGSv3.2","CDS",0],[22,2691468,2691692,-1,"amel_OGSv3.2","Group2.19",0,"CDS"],[16,2691233,2691376,-1,"amel_OGSv3.2","Group2.19",1,"CDS"],[43,2691020,2691151,-1,"CDS",2,"Group2.19","amel_OGSv3.2"],[22,2690548,2690934,-1,"amel_OGSv3.2","Group2.19",0,"CDS"],[33,2690155,2690467,-1,1,"CDS","amel_OGSv3.2","Group2.19"],[1,2689808,2690067,-1,"amel_OGSv3.2","Group2.19",1,"CDS"],[40,2689511,2689709,-1,0,"CDS","amel_OGSv3.2","Group2. [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-7.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-7.json
new file mode 100644
index 0000000..7c91e7c
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/lf-7.json	
@@ -0,0 +1 @@
+[[85,3468637,3475086,-1,"Group2.19","amel_OGSv3.2","mRNA","GB55359-RA",1,[[1,3474484,3474652,-1,"amel_OGSv3.2","Group2.19",0,"CDS"],[41,3473911,3474029,-1,"amel_OGSv3.2","Group2.19",0,"CDS"],[40,3473699,3473839,-1,2,"CDS","amel_OGSv3.2","Group2.19"],[65,3473498,3473597,-1,"Group2.19","amel_OGSv3.2","CDS",0],[34,3473299,3473413,-1,0,"CDS","amel_OGSv3.2","Group2.19"],[30,3472736,3473105,-1,"CDS",0,"Group2.19","amel_OGSv3.2"],[44,3472334,3472664,-1,"Group2.19","amel_OGSv3.2","CDS",0],[95,34 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/names.txt
new file mode 100644
index 0000000..1b554e4
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/names.txt	
@@ -0,0 +1,313 @@
+[["GB55506-RA"],"Official Gene Set v3.2","GB55506-RA","Group2.19",1643,6749]
+[["GB55505-RA"],"Official Gene Set v3.2","GB55505-RA","Group2.19",5815,11412]
+[["GB55504-RA"],"Official Gene Set v3.2","GB55504-RA","Group2.19",14822,18887]
+[["GB55507-RA"],"Official Gene Set v3.2","GB55507-RA","Group2.19",19994,22872]
+[["GB55508-RA"],"Official Gene Set v3.2","GB55508-RA","Group2.19",24014,29595]
+[["GB55503-RA"],"Official Gene Set v3.2","GB55503-RA","Group2.19",31853,35056]
+[["GB55502-RA"],"Official Gene Set v3.2","GB55502-RA","Group2.19",35760,37229]
+[["GB55501-RA"],"Official Gene Set v3.2","GB55501-RA","Group2.19",39830,41853]
+[["GB55500-RA"],"Official Gene Set v3.2","GB55500-RA","Group2.19",42168,44674]
+[["GB55509-RA"],"Official Gene Set v3.2","GB55509-RA","Group2.19",45639,46848]
+[["GB55499-RA"],"Official Gene Set v3.2","GB55499-RA","Group2.19",47893,51361]
+[["GB55498-RA"],"Official Gene Set v3.2","GB55498-RA","Group2.19",54122,62468]
+[["GB55497-RA"],"Official Gene Set v3.2","GB55497-RA","Group2.19",62605,67980]
+[["GB55510-RA"],"Official Gene Set v3.2","GB55510-RA","Group2.19",69445,71492]
+[["GB55511-RA"],"Official Gene Set v3.2","GB55511-RA","Group2.19",72574,76883]
+[["GB55512-RA"],"Official Gene Set v3.2","GB55512-RA","Group2.19",78893,81821]
+[["GB55496-RA"],"Official Gene Set v3.2","GB55496-RA","Group2.19",81542,83883]
+[["GB55495-RA"],"Official Gene Set v3.2","GB55495-RA","Group2.19",84796,98958]
+[["GB55513-RA"],"Official Gene Set v3.2","GB55513-RA","Group2.19",99632,102626]
+[["GB55514-RA"],"Official Gene Set v3.2","GB55514-RA","Group2.19",104450,109977]
+[["GB55515-RA"],"Official Gene Set v3.2","GB55515-RA","Group2.19",111697,113707]
+[["GB55516-RA"],"Official Gene Set v3.2","GB55516-RA","Group2.19",116186,124612]
+[["GB55494-RA"],"Official Gene Set v3.2","GB55494-RA","Group2.19",122883,126623]
+[["GB55517-RA"],"Official Gene Set v3.2","GB55517-RA","Group2.19",127233,132949]
+[["GB55493-RA"],"Official Gene Set v3.2","GB55493-RA","Group2.19",133201,135484]
+[["GB55518-RA"],"Official Gene Set v3.2","GB55518-RA","Group2.19",136687,137806]
+[["GB55519-RA"],"Official Gene Set v3.2","GB55519-RA","Group2.19",138627,139940]
+[["GB55520-RA"],"Official Gene Set v3.2","GB55520-RA","Group2.19",141572,167817]
+[["GB55492-RA"],"Official Gene Set v3.2","GB55492-RA","Group2.19",162182,169716]
+[["GB55491-RA"],"Official Gene Set v3.2","GB55491-RA","Group2.19",173293,178073]
+[["GB55521-RA"],"Official Gene Set v3.2","GB55521-RA","Group2.19",179184,185756]
+[["GB55490-RA"],"Official Gene Set v3.2","GB55490-RA","Group2.19",181557,201376]
+[["GB55522-RA"],"Official Gene Set v3.2","GB55522-RA","Group2.19",202820,205677]
+[["GB55523-RA"],"Official Gene Set v3.2","GB55523-RA","Group2.19",206525,211611]
+[["GB55489-RA"],"Official Gene Set v3.2","GB55489-RA","Group2.19",212763,226107]
+[["GB55488-RA"],"Official Gene Set v3.2","GB55488-RA","Group2.19",230213,233256]
+[["GB55487-RA"],"Official Gene Set v3.2","GB55487-RA","Group2.19",237033,238485]
+[["GB55486-RA"],"Official Gene Set v3.2","GB55486-RA","Group2.19",239499,244273]
+[["GB55525-RA"],"Official Gene Set v3.2","GB55525-RA","Group2.19",244499,249111]
+[["GB55485-RA"],"Official Gene Set v3.2","GB55485-RA","Group2.19",247344,255812]
+[["GB55526-RA"],"Official Gene Set v3.2","GB55526-RA","Group2.19",261309,277698]
+[["GB55527-RA"],"Official Gene Set v3.2","GB55527-RA","Group2.19",279084,293709]
+[["GB55528-RA"],"Official Gene Set v3.2","GB55528-RA","Group2.19",300192,302537]
+[["GB55529-RA"],"Official Gene Set v3.2","GB55529-RA","Group2.19",303441,305651]
+[["GB55484-RA"],"Official Gene Set v3.2","GB55484-RA","Group2.19",303945,308520]
+[["GB55530-RA"],"Official Gene Set v3.2","GB55530-RA","Group2.19",309688,312900]
+[["GB55483-RA"],"Official Gene Set v3.2","GB55483-RA","Group2.19",310956,384922]
+[["GB55482-RA"],"Official Gene Set v3.2","GB55482-RA","Group2.19",391447,399945]
+[["GB55531-RA"],"Official Gene Set v3.2","GB55531-RA","Group2.19",402055,407745]
+[["GB55532-RA"],"Official Gene Set v3.2","GB55532-RA","Group2.19",408948,417285]
+[["GB55481-RA"],"Official Gene Set v3.2","GB55481-RA","Group2.19",418221,427048]
+[["GB55480-RA"],"Official Gene Set v3.2","GB55480-RA","Group2.19",430862,450697]
+[["GB55533-RA"],"Official Gene Set v3.2","GB55533-RA","Group2.19",457388,461850]
+[["GB55535-RA"],"Official Gene Set v3.2","GB55535-RA","Group2.19",463804,464948]
+[["GB55536-RA"],"Official Gene Set v3.2","GB55536-RA","Group2.19",468088,468956]
+[["GB55537-RA"],"Official Gene Set v3.2","GB55537-RA","Group2.19",469759,473660]
+[["GB55538-RA"],"Official Gene Set v3.2","GB55538-RA","Group2.19",490226,511010]
+[["GB55479-RA"],"Official Gene Set v3.2","GB55479-RA","Group2.19",511018,512408]
+[["GB55539-RA"],"Official Gene Set v3.2","GB55539-RA","Group2.19",512843,515993]
+[["GB55478-RA"],"Official Gene Set v3.2","GB55478-RA","Group2.19",517155,522993]
+[["GB55540-RA"],"Official Gene Set v3.2","GB55540-RA","Group2.19",524518,529916]
+[["GB55477-RA"],"Official Gene Set v3.2","GB55477-RA","Group2.19",530211,531506]
+[["GB55541-RA"],"Official Gene Set v3.2","GB55541-RA","Group2.19",534317,535427]
+[["GB55476-RA"],"Official Gene Set v3.2","GB55476-RA","Group2.19",535519,541725]
+[["GB55542-RA"],"Official Gene Set v3.2","GB55542-RA","Group2.19",542816,543776]
+[["GB55475-RA"],"Official Gene Set v3.2","GB55475-RA","Group2.19",545219,550459]
+[["GB55544-RA"],"Official Gene Set v3.2","GB55544-RA","Group2.19",551653,555454]
+[["GB55545-RA"],"Official Gene Set v3.2","GB55545-RA","Group2.19",557523,559168]
+[["GB55546-RA"],"Official Gene Set v3.2","GB55546-RA","Group2.19",559865,563088]
+[["GB55474-RA"],"Official Gene Set v3.2","GB55474-RA","Group2.19",567092,570441]
+[["GB55547-RA"],"Official Gene Set v3.2","GB55547-RA","Group2.19",575064,579322]
+[["GB55548-RA"],"Official Gene Set v3.2","GB55548-RA","Group2.19",580461,596017]
+[["GB55549-RA"],"Official Gene Set v3.2","GB55549-RA","Group2.19",604781,619923]
+[["GB55473-RA"],"Official Gene Set v3.2","GB55473-RA","Group2.19",617652,620862]
+[["GB55550-RA"],"Official Gene Set v3.2","GB55550-RA","Group2.19",621084,628913]
+[["GB55472-RA"],"Official Gene Set v3.2","GB55472-RA","Group2.19",629304,631053]
+[["GB55471-RA"],"Official Gene Set v3.2","GB55471-RA","Group2.19",639062,639628]
+[["GB55470-RA"],"Official Gene Set v3.2","GB55470-RA","Group2.19",642757,673416]
+[["GB55551-RA"],"Official Gene Set v3.2","GB55551-RA","Group2.19",642933,643402]
+[["GB55469-RA"],"Official Gene Set v3.2","GB55469-RA","Group2.19",726827,735513]
+[["GB55552-RA"],"Official Gene Set v3.2","GB55552-RA","Group2.19",759950,766520]
+[["GB55553-RA"],"Official Gene Set v3.2","GB55553-RA","Group2.19",769625,770027]
+[["GB55554-RA"],"Official Gene Set v3.2","GB55554-RA","Group2.19",770073,770210]
+[["GB55555-RA"],"Official Gene Set v3.2","GB55555-RA","Group2.19",777077,777748]
+[["GB55556-RA"],"Official Gene Set v3.2","GB55556-RA","Group2.19",782306,789250]
+[["GB55468-RA"],"Official Gene Set v3.2","GB55468-RA","Group2.19",799706,802439]
+[["GB55467-RA"],"Official Gene Set v3.2","GB55467-RA","Group2.19",803869,807329]
+[["GB55466-RA"],"Official Gene Set v3.2","GB55466-RA","Group2.19",807722,811323]
+[["GB55557-RA"],"Official Gene Set v3.2","GB55557-RA","Group2.19",811548,815808]
+[["GB55558-RA"],"Official Gene Set v3.2","GB55558-RA","Group2.19",815860,817712]
+[["GB55465-RA"],"Official Gene Set v3.2","GB55465-RA","Group2.19",817643,823582]
+[["GB55559-RA"],"Official Gene Set v3.2","GB55559-RA","Group2.19",821595,823617]
+[["GB55560-RA"],"Official Gene Set v3.2","GB55560-RA","Group2.19",824672,828854]
+[["GB55561-RA"],"Official Gene Set v3.2","GB55561-RA","Group2.19",830479,831314]
+[["GB55464-RA"],"Official Gene Set v3.2","GB55464-RA","Group2.19",831435,833210]
+[["GB55463-RA"],"Official Gene Set v3.2","GB55463-RA","Group2.19",835338,838636]
+[["GB55461-RA"],"Official Gene Set v3.2","GB55461-RA","Group2.19",841576,847571]
+[["GB55460-RA"],"Official Gene Set v3.2","GB55460-RA","Group2.19",847778,848649]
+[["GB55459-RA"],"Official Gene Set v3.2","GB55459-RA","Group2.19",850777,853196]
+[["GB55562-RA"],"Official Gene Set v3.2","GB55562-RA","Group2.19",855517,856745]
+[["GB55457-RA"],"Official Gene Set v3.2","GB55457-RA","Group2.19",857505,863055]
+[["GB55563-RA"],"Official Gene Set v3.2","GB55563-RA","Group2.19",863687,865238]
+[["GB55456-RA"],"Official Gene Set v3.2","GB55456-RA","Group2.19",865013,868721]
+[["GB55564-RA"],"Official Gene Set v3.2","GB55564-RA","Group2.19",869030,874931]
+[["GB55455-RA"],"Official Gene Set v3.2","GB55455-RA","Group2.19",873134,876219]
+[["GB55454-RA"],"Official Gene Set v3.2","GB55454-RA","Group2.19",877049,879463]
+[["GB55453-RA"],"Official Gene Set v3.2","GB55453-RA","Group2.19",880832,886491]
+[["GB55452-RA"],"Official Gene Set v3.2","GB55452-RA","Group2.19",887201,887947]
+[["GB55451-RA"],"Official Gene Set v3.2","GB55451-RA","Group2.19",888814,890431]
+[["GB55450-RA"],"Official Gene Set v3.2","GB55450-RA","Group2.19",891194,892771]
+[["GB55449-RA"],"Official Gene Set v3.2","GB55449-RA","Group2.19",893169,893932]
+[["GB55448-RA"],"Official Gene Set v3.2","GB55448-RA","Group2.19",895617,897417]
+[["GB55447-RA"],"Official Gene Set v3.2","GB55447-RA","Group2.19",897808,901901]
+[["GB55446-RA"],"Official Gene Set v3.2","GB55446-RA","Group2.19",902419,902796]
+[["GB55565-RA"],"Official Gene Set v3.2","GB55565-RA","Group2.19",903801,904838]
+[["GB55445-RA"],"Official Gene Set v3.2","GB55445-RA","Group2.19",905974,912554]
+[["GB55444-RA"],"Official Gene Set v3.2","GB55444-RA","Group2.19",914274,918172]
+[["GB55566-RA"],"Official Gene Set v3.2","GB55566-RA","Group2.19",918604,920409]
+[["GB55443-RA"],"Official Gene Set v3.2","GB55443-RA","Group2.19",919972,922378]
+[["GB55442-RA"],"Official Gene Set v3.2","GB55442-RA","Group2.19",927251,927539]
+[["GB55567-RA"],"Official Gene Set v3.2","GB55567-RA","Group2.19",928053,947820]
+[["GB55441-RA"],"Official Gene Set v3.2","GB55441-RA","Group2.19",946147,948309]
+[["GB55568-RA"],"Official Gene Set v3.2","GB55568-RA","Group2.19",948811,951472]
+[["GB55440-RA"],"Official Gene Set v3.2","GB55440-RA","Group2.19",951577,954692]
+[["GB55569-RA"],"Official Gene Set v3.2","GB55569-RA","Group2.19",955767,961020]
+[["GB55570-RA"],"Official Gene Set v3.2","GB55570-RA","Group2.19",965795,967415]
+[["GB55439-RA"],"Official Gene Set v3.2","GB55439-RA","Group2.19",967648,969936]
+[["GB55438-RA"],"Official Gene Set v3.2","GB55438-RA","Group2.19",974426,978468]
+[["GB55437-RA"],"Official Gene Set v3.2","GB55437-RA","Group2.19",979070,979313]
+[["GB55571-RA"],"Official Gene Set v3.2","GB55571-RA","Group2.19",980433,982329]
+[["GB55436-RA"],"Official Gene Set v3.2","GB55436-RA","Group2.19",982605,983889]
+[["GB55435-RA"],"Official Gene Set v3.2","GB55435-RA","Group2.19",986281,993190]
+[["GB55572-RA"],"Official Gene Set v3.2","GB55572-RA","Group2.19",996958,998949]
+[["GB55573-RA"],"Official Gene Set v3.2","GB55573-RA","Group2.19",999051,1005184]
+[["GB55434-RA"],"Official Gene Set v3.2","GB55434-RA","Group2.19",1002499,1007358]
+[["GB55574-RA"],"Official Gene Set v3.2","GB55574-RA","Group2.19",1007787,1026398]
+[["GB55575-RA"],"Official Gene Set v3.2","GB55575-RA","Group2.19",1026853,1036422]
+[["GB55576-RA"],"Official Gene Set v3.2","GB55576-RA","Group2.19",1036844,1112823]
+[["GB55577-RA"],"Official Gene Set v3.2","GB55577-RA","Group2.19",1181087,1187932]
+[["GB55578-RA"],"Official Gene Set v3.2","GB55578-RA","Group2.19",1201804,1216947]
+[["GB55579-RA"],"Official Gene Set v3.2","GB55579-RA","Group2.19",1219658,1225582]
+[["GB55580-RA"],"Official Gene Set v3.2","GB55580-RA","Group2.19",1226863,1227162]
+[["GB55581-RA"],"Official Gene Set v3.2","GB55581-RA","Group2.19",1228723,1230421]
+[["GB55582-RA"],"Official Gene Set v3.2","GB55582-RA","Group2.19",1233782,1235445]
+[["GB55433-RA"],"Official Gene Set v3.2","GB55433-RA","Group2.19",1239502,1241009]
+[["GB55583-RA"],"Official Gene Set v3.2","GB55583-RA","Group2.19",1241036,1257477]
+[["GB55584-RA"],"Official Gene Set v3.2","GB55584-RA","Group2.19",1264049,1268558]
+[["GB55585-RA"],"Official Gene Set v3.2","GB55585-RA","Group2.19",1302245,1313525]
+[["GB55586-RA"],"Official Gene Set v3.2","GB55586-RA","Group2.19",1316111,1330643]
+[["GB55432-RA"],"Official Gene Set v3.2","GB55432-RA","Group2.19",1324236,1330811]
+[["GB55587-RA"],"Official Gene Set v3.2","GB55587-RA","Group2.19",1331967,1334079]
+[["GB55588-RA"],"Official Gene Set v3.2","GB55588-RA","Group2.19",1337231,1337909]
+[["GB55589-RA"],"Official Gene Set v3.2","GB55589-RA","Group2.19",1337978,1339673]
+[["GB55431-RA"],"Official Gene Set v3.2","GB55431-RA","Group2.19",1339851,1353505]
+[["GB55590-RA"],"Official Gene Set v3.2","GB55590-RA","Group2.19",1354265,1355201]
+[["GB55591-RA"],"Official Gene Set v3.2","GB55591-RA","Group2.19",1365209,1391506]
+[["GB55592-RA"],"Official Gene Set v3.2","GB55592-RA","Group2.19",1393223,1396228]
+[["GB55429-RA"],"Official Gene Set v3.2","GB55429-RA","Group2.19",1400640,1409580]
+[["GB55593-RA"],"Official Gene Set v3.2","GB55593-RA","Group2.19",1423355,1425169]
+[["GB55428-RA"],"Official Gene Set v3.2","GB55428-RA","Group2.19",1426879,1430943]
+[["GB55594-RA"],"Official Gene Set v3.2","GB55594-RA","Group2.19",1431690,1434562]
+[["GB55427-RA"],"Official Gene Set v3.2","GB55427-RA","Group2.19",1433865,1435844]
+[["GB55595-RA"],"Official Gene Set v3.2","GB55595-RA","Group2.19",1436343,1439042]
+[["GB55426-RA"],"Official Gene Set v3.2","GB55426-RA","Group2.19",1437529,1471755]
+[["GB55425-RA"],"Official Gene Set v3.2","GB55425-RA","Group2.19",1477705,1502199]
+[["GB55424-RA"],"Official Gene Set v3.2","GB55424-RA","Group2.19",1512676,1513571]
+[["GB55423-RA"],"Official Gene Set v3.2","GB55423-RA","Group2.19",1536721,1538631]
+[["GB55422-RA"],"Official Gene Set v3.2","GB55422-RA","Group2.19",1541368,1543197]
+[["GB55596-RA"],"Official Gene Set v3.2","GB55596-RA","Group2.19",1552858,1638042]
+[["GB55421-RA"],"Official Gene Set v3.2","GB55421-RA","Group2.19",1642267,1643959]
+[["GB55420-RA"],"Official Gene Set v3.2","GB55420-RA","Group2.19",1644040,1646651]
+[["GB55419-RA"],"Official Gene Set v3.2","GB55419-RA","Group2.19",1647001,1648279]
+[["GB55597-RA"],"Official Gene Set v3.2","GB55597-RA","Group2.19",1648771,1649544]
+[["GB55417-RA"],"Official Gene Set v3.2","GB55417-RA","Group2.19",1650247,1657109]
+[["GB55416-RA"],"Official Gene Set v3.2","GB55416-RA","Group2.19",1659539,1660682]
+[["GB55415-RA"],"Official Gene Set v3.2","GB55415-RA","Group2.19",1660960,1661549]
+[["GB55598-RA"],"Official Gene Set v3.2","GB55598-RA","Group2.19",1681615,1700512]
+[["GB55599-RA"],"Official Gene Set v3.2","GB55599-RA","Group2.19",1701958,1709038]
+[["GB55600-RA"],"Official Gene Set v3.2","GB55600-RA","Group2.19",1752712,1778680]
+[["GB55414-RA"],"Official Gene Set v3.2","GB55414-RA","Group2.19",1778469,1781059]
+[["GB55413-RA"],"Official Gene Set v3.2","GB55413-RA","Group2.19",1781359,1784104]
+[["GB55601-RA"],"Official Gene Set v3.2","GB55601-RA","Group2.19",1784957,1803030]
+[["GB55411-RA"],"Official Gene Set v3.2","GB55411-RA","Group2.19",1803084,1820190]
+[["GB55602-RA"],"Official Gene Set v3.2","GB55602-RA","Group2.19",1835118,1838525]
+[["GB55603-RA"],"Official Gene Set v3.2","GB55603-RA","Group2.19",1846814,1851344]
+[["GB55604-RA"],"Official Gene Set v3.2","GB55604-RA","Group2.19",1860986,1865358]
+[["GB55409-RA"],"Official Gene Set v3.2","GB55409-RA","Group2.19",1865414,1868947]
+[["GB55605-RA"],"Official Gene Set v3.2","GB55605-RA","Group2.19",1868297,1873777]
+[["GB55606-RA"],"Official Gene Set v3.2","GB55606-RA","Group2.19",1891932,1892220]
+[["GB55607-RA"],"Official Gene Set v3.2","GB55607-RA","Group2.19",1896558,1898326]
+[["GB55408-RA"],"Official Gene Set v3.2","GB55408-RA","Group2.19",1897059,1904590]
+[["GB55608-RA"],"Official Gene Set v3.2","GB55608-RA","Group2.19",1899508,1900894]
+[["GB55407-RA"],"Official Gene Set v3.2","GB55407-RA","Group2.19",1905748,1905957]
+[["GB55406-RA"],"Official Gene Set v3.2","GB55406-RA","Group2.19",1907009,1908963]
+[["GB55405-RA"],"Official Gene Set v3.2","GB55405-RA","Group2.19",1921525,1934106]
+[["GB55609-RA"],"Official Gene Set v3.2","GB55609-RA","Group2.19",1951770,1953686]
+[["GB55404-RA"],"Official Gene Set v3.2","GB55404-RA","Group2.19",1952253,1953722]
+[["GB55403-RA"],"Official Gene Set v3.2","GB55403-RA","Group2.19",1954780,1962711]
+[["GB55402-RA"],"Official Gene Set v3.2","GB55402-RA","Group2.19",1963731,1967055]
+[["GB55401-RA"],"Official Gene Set v3.2","GB55401-RA","Group2.19",1972953,1974308]
+[["GB55400-RA"],"Official Gene Set v3.2","GB55400-RA","Group2.19",1991212,1999163]
+[["GB55399-RA"],"Official Gene Set v3.2","GB55399-RA","Group2.19",2002838,2003479]
+[["GB55398-RA"],"Official Gene Set v3.2","GB55398-RA","Group2.19",2005054,2005887]
+[["GB55610-RA"],"Official Gene Set v3.2","GB55610-RA","Group2.19",2006951,2015709]
+[["GB55397-RA"],"Official Gene Set v3.2","GB55397-RA","Group2.19",2006951,2009410]
+[["GB55396-RA"],"Official Gene Set v3.2","GB55396-RA","Group2.19",2014504,2029224]
+[["GB55611-RA"],"Official Gene Set v3.2","GB55611-RA","Group2.19",2038628,2042446]
+[["GB55612-RA"],"Official Gene Set v3.2","GB55612-RA","Group2.19",2050198,2059652]
+[["GB55613-RA"],"Official Gene Set v3.2","GB55613-RA","Group2.19",2065290,2067591]
+[["GB55614-RA"],"Official Gene Set v3.2","GB55614-RA","Group2.19",2068785,2070248]
+[["GB55615-RA"],"Official Gene Set v3.2","GB55615-RA","Group2.19",2071983,2074811]
+[["GB55616-RA"],"Official Gene Set v3.2","GB55616-RA","Group2.19",2075093,2076647]
+[["GB55617-RA"],"Official Gene Set v3.2","GB55617-RA","Group2.19",2077022,2079962]
+[["GB55395-RA"],"Official Gene Set v3.2","GB55395-RA","Group2.19",2079968,2084002]
+[["GB55394-RA"],"Official Gene Set v3.2","GB55394-RA","Group2.19",2087984,2089910]
+[["GB55393-RA"],"Official Gene Set v3.2","GB55393-RA","Group2.19",2090631,2092235]
+[["GB55392-RA"],"Official Gene Set v3.2","GB55392-RA","Group2.19",2094731,2096839]
+[["GB55391-RA"],"Official Gene Set v3.2","GB55391-RA","Group2.19",2097110,2097932]
+[["GB55390-RA"],"Official Gene Set v3.2","GB55390-RA","Group2.19",2099443,2102521]
+[["GB55618-RA"],"Official Gene Set v3.2","GB55618-RA","Group2.19",2133696,2180980]
+[["GB55389-RA"],"Official Gene Set v3.2","GB55389-RA","Group2.19",2182375,2186427]
+[["GB55619-RA"],"Official Gene Set v3.2","GB55619-RA","Group2.19",2190154,2197886]
+[["GB55388-RA"],"Official Gene Set v3.2","GB55388-RA","Group2.19",2196564,2207682]
+[["GB55620-RA"],"Official Gene Set v3.2","GB55620-RA","Group2.19",2199917,2203182]
+[["GB55621-RA"],"Official Gene Set v3.2","GB55621-RA","Group2.19",2205934,2209468]
+[["GB55622-RA"],"Official Gene Set v3.2","GB55622-RA","Group2.19",2209661,2211672]
+[["GB55387-RA"],"Official Gene Set v3.2","GB55387-RA","Group2.19",2212884,2281557]
+[["GB55623-RA"],"Official Gene Set v3.2","GB55623-RA","Group2.19",2312892,2313255]
+[["GB55386-RA"],"Official Gene Set v3.2","GB55386-RA","Group2.19",2313848,2326374]
+[["GB55384-RA"],"Official Gene Set v3.2","GB55384-RA","Group2.19",2327488,2328856]
+[["GB55383-RA"],"Official Gene Set v3.2","GB55383-RA","Group2.19",2331685,2343701]
+[["GB55624-RA"],"Official Gene Set v3.2","GB55624-RA","Group2.19",2342852,2348681]
+[["GB55382-RA"],"Official Gene Set v3.2","GB55382-RA","Group2.19",2350774,2355195]
+[["GB55625-RA"],"Official Gene Set v3.2","GB55625-RA","Group2.19",2355646,2358153]
+[["GB55626-RA"],"Official Gene Set v3.2","GB55626-RA","Group2.19",2359550,2361242]
+[["GB55381-RA"],"Official Gene Set v3.2","GB55381-RA","Group2.19",2362315,2367864]
+[["GB55627-RA"],"Official Gene Set v3.2","GB55627-RA","Group2.19",2368755,2372109]
+[["GB55628-RA"],"Official Gene Set v3.2","GB55628-RA","Group2.19",2372338,2372884]
+[["GB55629-RA"],"Official Gene Set v3.2","GB55629-RA","Group2.19",2374077,2378485]
+[["GB55630-RA"],"Official Gene Set v3.2","GB55630-RA","Group2.19",2381515,2385506]
+[["GB55631-RA"],"Official Gene Set v3.2","GB55631-RA","Group2.19",2388773,2392286]
+[["GB55632-RA"],"Official Gene Set v3.2","GB55632-RA","Group2.19",2392801,2399392]
+[["GB55633-RA"],"Official Gene Set v3.2","GB55633-RA","Group2.19",2400293,2402416]
+[["GB55634-RA"],"Official Gene Set v3.2","GB55634-RA","Group2.19",2402434,2403284]
+[["GB55635-RA"],"Official Gene Set v3.2","GB55635-RA","Group2.19",2404371,2419077]
+[["GB55380-RA"],"Official Gene Set v3.2","GB55380-RA","Group2.19",2424900,2477777]
+[["GB55379-RA"],"Official Gene Set v3.2","GB55379-RA","Group2.19",2489990,2492726]
+[["GB55636-RA"],"Official Gene Set v3.2","GB55636-RA","Group2.19",2512397,2513189]
+[["GB55637-RA"],"Official Gene Set v3.2","GB55637-RA","Group2.19",2566483,2567132]
+[["GB55378-RA"],"Official Gene Set v3.2","GB55378-RA","Group2.19",2598314,2618190]
+[["GB55377-RA"],"Official Gene Set v3.2","GB55377-RA","Group2.19",2644495,2645723]
+[["GB55376-RA"],"Official Gene Set v3.2","GB55376-RA","Group2.19",2646126,2646871]
+[["GB55375-RA"],"Official Gene Set v3.2","GB55375-RA","Group2.19",2666654,2696444]
+[["GB55638-RA"],"Official Gene Set v3.2","GB55638-RA","Group2.19",2704760,2708498]
+[["GB55374-RA"],"Official Gene Set v3.2","GB55374-RA","Group2.19",2708235,2711084]
+[["GB55639-RA"],"Official Gene Set v3.2","GB55639-RA","Group2.19",2711803,2713196]
+[["GB55373-RA"],"Official Gene Set v3.2","GB55373-RA","Group2.19",2713157,2715995]
+[["GB55640-RA"],"Official Gene Set v3.2","GB55640-RA","Group2.19",2716737,2718765]
+[["GB55372-RA"],"Official Gene Set v3.2","GB55372-RA","Group2.19",2718889,2722202]
+[["GB55641-RA"],"Official Gene Set v3.2","GB55641-RA","Group2.19",2722773,2732234]
+[["GB55371-RA"],"Official Gene Set v3.2","GB55371-RA","Group2.19",2733174,2734808]
+[["GB55370-RA"],"Official Gene Set v3.2","GB55370-RA","Group2.19",2736041,2741841]
+[["GB55642-RA"],"Official Gene Set v3.2","GB55642-RA","Group2.19",2755089,2764401]
+[["GB55643-RA"],"Official Gene Set v3.2","GB55643-RA","Group2.19",2765298,2767533]
+[["GB55369-RA"],"Official Gene Set v3.2","GB55369-RA","Group2.19",2767461,2771711]
+[["GB55368-RA"],"Official Gene Set v3.2","GB55368-RA","Group2.19",2772138,2776581]
+[["GB55367-RA"],"Official Gene Set v3.2","GB55367-RA","Group2.19",2778118,2782756]
+[["GB55644-RA"],"Official Gene Set v3.2","GB55644-RA","Group2.19",2782990,2790183]
+[["GB55645-RA"],"Official Gene Set v3.2","GB55645-RA","Group2.19",2792305,2797337]
+[["GB55366-RA"],"Official Gene Set v3.2","GB55366-RA","Group2.19",2797413,2801286]
+[["GB55646-RA"],"Official Gene Set v3.2","GB55646-RA","Group2.19",2802918,2849047]
+[["GB55365-RA"],"Official Gene Set v3.2","GB55365-RA","Group2.19",2850068,2851283]
+[["GB55647-RA"],"Official Gene Set v3.2","GB55647-RA","Group2.19",2860135,2860747]
+[["GB55648-RA"],"Official Gene Set v3.2","GB55648-RA","Group2.19",2904180,2971762]
+[["GB55364-RA"],"Official Gene Set v3.2","GB55364-RA","Group2.19",2979051,2993545]
+[["GB55649-RA"],"Official Gene Set v3.2","GB55649-RA","Group2.19",2998190,2998367]
+[["GB55363-RA"],"Official Gene Set v3.2","GB55363-RA","Group2.19",3010298,3022188]
+[["GB55362-RA"],"Official Gene Set v3.2","GB55362-RA","Group2.19",3179959,3182829]
+[["GB55650-RA"],"Official Gene Set v3.2","GB55650-RA","Group2.19",3323684,3355223]
+[["GB55361-RA"],"Official Gene Set v3.2","GB55361-RA","Group2.19",3325247,3326622]
+[["GB55651-RA"],"Official Gene Set v3.2","GB55651-RA","Group2.19",3355562,3358957]
+[["GB55652-RA"],"Official Gene Set v3.2","GB55652-RA","Group2.19",3361498,3370211]
+[["GB55653-RA"],"Official Gene Set v3.2","GB55653-RA","Group2.19",3383668,3387849]
+[["GB55654-RA"],"Official Gene Set v3.2","GB55654-RA","Group2.19",3393193,3405222]
+[["GB55360-RA"],"Official Gene Set v3.2","GB55360-RA","Group2.19",3449907,3451489]
+[["GB55656-RA"],"Official Gene Set v3.2","GB55656-RA","Group2.19",3466825,3468555]
+[["GB55359-RA"],"Official Gene Set v3.2","GB55359-RA","Group2.19",3468637,3475086]
+[["GB55657-RA"],"Official Gene Set v3.2","GB55657-RA","Group2.19",3475642,3485227]
+[["GB55358-RA"],"Official Gene Set v3.2","GB55358-RA","Group2.19",3475992,3477531]
+[["GB55658-RA"],"Official Gene Set v3.2","GB55658-RA","Group2.19",3485271,3496878]
+[["GB55357-RA"],"Official Gene Set v3.2","GB55357-RA","Group2.19",3497296,3500621]
+[["GB55356-RA"],"Official Gene Set v3.2","GB55356-RA","Group2.19",3501634,3524777]
+[["GB55355-RA"],"Official Gene Set v3.2","GB55355-RA","Group2.19",3537834,3545795]
+[["GB55659-RA"],"Official Gene Set v3.2","GB55659-RA","Group2.19",3547635,3551361]
+[["GB55660-RA"],"Official Gene Set v3.2","GB55660-RA","Group2.19",3552355,3555735]
+[["GB55661-RA"],"Official Gene Set v3.2","GB55661-RA","Group2.19",3556821,3558822]
+[["GB55354-RA"],"Official Gene Set v3.2","GB55354-RA","Group2.19",3559443,3561103]
+[["GB55662-RA"],"Official Gene Set v3.2","GB55662-RA","Group2.19",3561493,3562540]
+[["GB55663-RA"],"Official Gene Set v3.2","GB55663-RA","Group2.19",3570890,3585510]
+[["GB55353-RA"],"Official Gene Set v3.2","GB55353-RA","Group2.19",3630692,3633188]
+[["GB55664-RA"],"Official Gene Set v3.2","GB55664-RA","Group2.19",3631484,3639676]
+[["GB55352-RA"],"Official Gene Set v3.2","GB55352-RA","Group2.19",3641641,3645114]
+[["GB55665-RA"],"Official Gene Set v3.2","GB55665-RA","Group2.19",3645793,3647683]
+[["GB55666-RA"],"Official Gene Set v3.2","GB55666-RA","Group2.19",3648081,3649467]
+[["GB55351-RA"],"Official Gene Set v3.2","GB55351-RA","Group2.19",3649670,3652276]
+[["GB55667-RA"],"Official Gene Set v3.2","GB55667-RA","Group2.19",3655709,3656037]
+[["GB55668-RA"],"Official Gene Set v3.2","GB55668-RA","Group2.19",3668524,3673880]
+[["GB55350-RA"],"Official Gene Set v3.2","GB55350-RA","Group2.19",3675685,3721341]
+[["GB55349-RA"],"Official Gene Set v3.2","GB55349-RA","Group2.19",3734321,3766050]
+[["GB55669-RA"],"Official Gene Set v3.2","GB55669-RA","Group2.19",3777732,3779223]
+[["GB55348-RA"],"Official Gene Set v3.2","GB55348-RA","Group2.19",3802191,3849410]
+[["GB55670-RA"],"Official Gene Set v3.2","GB55670-RA","Group2.19",3856666,3856876]
+[["GB55347-RA"],"Official Gene Set v3.2","GB55347-RA","Group2.19",3865672,3865858]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/trackData.json
new file mode 100644
index 0000000..996bc8a
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group2.19/trackData.json	
@@ -0,0 +1 @@
+{"featureCount":313,"formatVersion":1,"intervals":{"classes":[{"isArrayAttr":{"Subfeatures":1},"attributes":["Start","End","Strand","Score","Subfeatures","Type","Id","Seq_id","Source"]},{"isArrayAttr":{},"attributes":["Start","End","Strand","Source","Seq_id","Phase","Type"]},{"attributes":["Start","End","Strand","Seq_id","Source","Type","Phase"],"isArrayAttr":{}},{"attributes":["Start","End","Strand","Score","Type","Seq_id","Source"],"isArrayAttr":{}},{"isArrayAttr":{},"attributes":["Sta [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/hist-50000-0.json
new file mode 100644
index 0000000..fc412c3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/hist-50000-0.json	
@@ -0,0 +1 @@
+[5,3,1,1,1,1,7,1,6,9]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/names.txt
new file mode 100644
index 0000000..067f85b
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/names.txt	
@@ -0,0 +1,29 @@
+[["GB49645-RA","GB49645-RA"],"Official Gene Set v3.2","GB49645-RA","Group4.1",1472,2081]
+[["GB49646-RA","GB49646-RA"],"Official Gene Set v3.2","GB49646-RA","Group4.1",10553,34542]
+[["GB49647-RA","GB49647-RA"],"Official Gene Set v3.2","GB49647-RA","Group4.1",35538,38515]
+[["GB49648-RA","GB49648-RA"],"Official Gene Set v3.2","GB49648-RA","Group4.1",39406,40709]
+[["GB49644-RA","GB49644-RA"],"Official Gene Set v3.2","GB49644-RA","Group4.1",40350,51754]
+[["GB49649-RA","GB49649-RA"],"Official Gene Set v3.2","GB49649-RA","Group4.1",54786,68026]
+[["GB49650-RA","GB49650-RA"],"Official Gene Set v3.2","GB49650-RA","Group4.1",76348,264940]
+[["GB49643-RA","GB49643-RA"],"Official Gene Set v3.2","GB49643-RA","Group4.1",314102,317365]
+[["GB49651-RA","GB49651-RA"],"Official Gene Set v3.2","GB49651-RA","Group4.1",317799,320046]
+[["GB49652-RA","GB49652-RA"],"Official Gene Set v3.2","GB49652-RA","Group4.1",321491,325428]
+[["GB49642-RA","GB49642-RA"],"Official Gene Set v3.2","GB49642-RA","Group4.1",326074,339198]
+[["GB49653-RA","GB49653-RA"],"Official Gene Set v3.2","GB49653-RA","Group4.1",340494,343735]
+[["GB49641-RA","GB49641-RA"],"Official Gene Set v3.2","GB49641-RA","Group4.1",343590,344868]
+[["GB49654-RA","GB49654-RA"],"Official Gene Set v3.2","GB49654-RA","Group4.1",344532,346479]
+[["GB49640-RA","GB49640-RA"],"Official Gene Set v3.2","GB49640-RA","Group4.1",352510,399304]
+[["GB49639-RA","GB49639-RA"],"Official Gene Set v3.2","GB49639-RA","Group4.1",404541,416121]
+[["GB49638-RA","GB49638-RA"],"Official Gene Set v3.2","GB49638-RA","Group4.1",417196,419746]
+[["GB49655-RA","GB49655-RA"],"Official Gene Set v3.2","GB49655-RA","Group4.1",420568,422314]
+[["GB49637-RA","GB49637-RA"],"Official Gene Set v3.2","GB49637-RA","Group4.1",422298,428104]
+[["GB49656-RA","GB49656-RA"],"Official Gene Set v3.2","GB49656-RA","Group4.1",422958,423300]
+[["GB49657-RA","GB49657-RA"],"Official Gene Set v3.2","GB49657-RA","Group4.1",431443,471518]
+[["GB49635-RA","GB49635-RA"],"Official Gene Set v3.2","GB49635-RA","Group4.1",459805,460997]
+[["GB49634-RA","GB49634-RA"],"Official Gene Set v3.2","GB49634-RA","Group4.1",476007,477582]
+[["GB49658-RA","GB49658-RA"],"Official Gene Set v3.2","GB49658-RA","Group4.1",478995,480440]
+[["GB49659-RA","GB49659-RA"],"Official Gene Set v3.2","GB49659-RA","Group4.1",480470,482227]
+[["GB49633-RA","GB49633-RA"],"Official Gene Set v3.2","GB49633-RA","Group4.1",482383,484197]
+[["GB49632-RA","GB49632-RA"],"Official Gene Set v3.2","GB49632-RA","Group4.1",484496,488321]
+[["GB49660-RA","GB49660-RA"],"Official Gene Set v3.2","GB49660-RA","Group4.1",484618,485251]
+[["GB49661-RA","GB49661-RA"],"Official Gene Set v3.2","GB49661-RA","Group4.1",489122,493760]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/trackData.json
new file mode 100644
index 0000000..f7d0ad3
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group4.1/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":9,"mean":3.5}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":10,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":29,"intervals":{"nclist":[[0,1472,2081,1,"amel_OGSv3.2","Group4.1","GB49645-RA",1,"GB49645-RA","mRNA",[[1,1772,1838,1,"amel_OGSv3.2","Group4.1",0,"CDS"],[1,1927,1939,1,"amel_OGSv3.2","Group4.1",0,"CDS"],[2,1472,1772,1,"amel_OGSv3.2","Group4.1",1,"five_prime_UTR"],[2,1939,2081,1,"amel_OGSv [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/hist-50000-0.json
new file mode 100644
index 0000000..0dab9e8
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/hist-50000-0.json	
@@ -0,0 +1 @@
+[3,3,0,2,5,5,7,5,6,3]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/names.txt
new file mode 100644
index 0000000..9e62ed0
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/names.txt	
@@ -0,0 +1,35 @@
+[["GB54471-RA","GB54471-RA"],"Official Gene Set v3.2","GB54471-RA","Group6.10",17552,30921]
+[["GB54472-RA","GB54472-RA"],"Official Gene Set v3.2","GB54472-RA","Group6.10",33437,34952]
+[["GB54473-RA","GB54473-RA"],"Official Gene Set v3.2","GB54473-RA","Group6.10",48471,54484]
+[["GB54474-RA","GB54474-RA"],"Official Gene Set v3.2","GB54474-RA","Group6.10",63768,72901]
+[["GB54475-RA","GB54475-RA"],"Official Gene Set v3.2","GB54475-RA","Group6.10",79556,80781]
+[["GB54476-RA","GB54476-RA"],"Official Gene Set v3.2","GB54476-RA","Group6.10",152914,154693]
+[["GB54470-RA","GB54470-RA"],"Official Gene Set v3.2","GB54470-RA","Group6.10",189186,193356]
+[["GB54477-RA","GB54477-RA"],"Official Gene Set v3.2","GB54477-RA","Group6.10",204072,235441]
+[["GB54478-RA","GB54478-RA"],"Official Gene Set v3.2","GB54478-RA","Group6.10",235824,241556]
+[["GB54469-RA","GB54469-RA"],"Official Gene Set v3.2","GB54469-RA","Group6.10",241984,242839]
+[["GB54480-RA","GB54480-RA"],"Official Gene Set v3.2","GB54480-RA","Group6.10",244223,246099]
+[["GB54468-RA","GB54468-RA"],"Official Gene Set v3.2","GB54468-RA","Group6.10",246859,267949]
+[["GB54467-RA","GB54467-RA"],"Official Gene Set v3.2","GB54467-RA","Group6.10",279087,293203]
+[["GB54481-RA","GB54481-RA"],"Official Gene Set v3.2","GB54481-RA","Group6.10",288947,289488]
+[["GB54466-RA","GB54466-RA"],"Official Gene Set v3.2","GB54466-RA","Group6.10",294921,295203]
+[["GB54465-RA","GB54465-RA"],"Official Gene Set v3.2","GB54465-RA","Group6.10",299748,302144]
+[["GB54464-RA","GB54464-RA"],"Official Gene Set v3.2","GB54464-RA","Group6.10",305545,305978]
+[["GB54482-RA","GB54482-RA"],"Official Gene Set v3.2","GB54482-RA","Group6.10",311468,311687]
+[["GB54483-RA","GB54483-RA"],"Official Gene Set v3.2","GB54483-RA","Group6.10",317499,343465]
+[["GB54463-RA","GB54463-RA"],"Official Gene Set v3.2","GB54463-RA","Group6.10",336028,336271]
+[["GB54462-RA","GB54462-RA"],"Official Gene Set v3.2","GB54462-RA","Group6.10",342632,344079]
+[["GB54484-RA","GB54484-RA"],"Official Gene Set v3.2","GB54484-RA","Group6.10",345031,346576]
+[["GB54485-RA","GB54485-RA"],"Official Gene Set v3.2","GB54485-RA","Group6.10",357789,361956]
+[["GB54461-RA","GB54461-RA"],"Official Gene Set v3.2","GB54461-RA","Group6.10",363578,363797]
+[["GB54486-RA","GB54486-RA"],"Official Gene Set v3.2","GB54486-RA","Group6.10",371610,377542]
+[["GB54460-RA","GB54460-RA"],"Official Gene Set v3.2","GB54460-RA","Group6.10",377621,391368]
+[["GB54459-RA","GB54459-RA"],"Official Gene Set v3.2","GB54459-RA","Group6.10",396290,396539]
+[["GB54487-RA","GB54487-RA"],"Official Gene Set v3.2","GB54487-RA","Group6.10",410378,412013]
+[["GB54458-RA","GB54458-RA"],"Official Gene Set v3.2","GB54458-RA","Group6.10",437818,439601]
+[["GB54457-RA","GB54457-RA"],"Official Gene Set v3.2","GB54457-RA","Group6.10",442396,443194]
+[["GB54488-RA","GB54488-RA"],"Official Gene Set v3.2","GB54488-RA","Group6.10",444753,446128]
+[["GB54456-RA","GB54456-RA"],"Official Gene Set v3.2","GB54456-RA","Group6.10",446882,447629]
+[["GB54489-RA","GB54489-RA"],"Official Gene Set v3.2","GB54489-RA","Group6.10",449785,450547]
+[["GB54455-RA","GB54455-RA"],"Official Gene Set v3.2","GB54455-RA","Group6.10",459677,460463]
+[["GB54454-RA","GB54454-RA"],"Official Gene Set v3.2","GB54454-RA","Group6.10",466659,470982]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/trackData.json
new file mode 100644
index 0000000..2c37519
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/Group6.10/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":7,"mean":3.9}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":10,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":35,"intervals":{"nclist":[[0,17552,30921,1,"amel_OGSv3.2","Group6.10","GB54471-RA",0.963801,"GB54471-RA","mRNA",[[1,17552,17580,1,"amel_OGSv3.2","Group6.10",0,"CDS"],[1,30685,30921,1,"amel_OGSv3.2","Group6.10",2,"CDS"],[3,17552,17580,1,"amel_OGSv3.2","Group6.10","exon"],[3,30685,30921,1," [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/hist-50000-0.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/hist-50000-0.json
new file mode 100644
index 0000000..ea86305
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/hist-50000-0.json	
@@ -0,0 +1 @@
+[4]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/names.txt b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/names.txt
new file mode 100644
index 0000000..051f2d4
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/names.txt	
@@ -0,0 +1,4 @@
+[["GB53496-RA","GB53496-RA"],"Official Gene Set v3.2","GB53496-RA","GroupUn87",9966,10179]
+[["GB53497-RA","GB53497-RA"],"Official Gene Set v3.2","GB53497-RA","GroupUn87",10511,26719]
+[["GB53498-RA","GB53498-RA"],"Official Gene Set v3.2","GB53498-RA","GroupUn87",29396,30329]
+[["GB53499-RA","GB53499-RA"],"Official Gene Set v3.2","GB53499-RA","GroupUn87",45455,45575]
diff --git a/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/trackData.json b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/trackData.json
new file mode 100644
index 0000000..9cc75d8
--- /dev/null
+++ b/test/integration/resources/sequences/honeybee-tracks/tracks/Official Gene Set v3.2/GroupUn87/trackData.json	
@@ -0,0 +1 @@
+{"histograms":{"stats":[{"basesPerBin":"50000","max":4,"mean":4}],"meta":[{"basesPerBin":"50000","arrayParams":{"length":1,"chunkSize":10000,"urlTemplate":"hist-50000-{Chunk}.json"}}]},"featureCount":4,"intervals":{"nclist":[[0,9966,10179,1,"amel_OGSv3.2","GroupUn87","GB53496-RA",1,"GB53496-RA","mRNA",[[1,9966,10179,1,"amel_OGSv3.2","GroupUn87",0,"CDS"],[3,9966,10179,1,"amel_OGSv3.2","GroupUn87","exon"]]],[0,10511,26719,1,"amel_OGSv3.2","GroupUn87","GB53497-RA",0.960984,"GB53497-RA","mRN [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/.htaccess b/test/integration/resources/sequences/yeast/.htaccess
new file mode 100644
index 0000000..19e183c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/.htaccess
@@ -0,0 +1,10 @@
+# This Apache .htaccess file is generated by JBrowse (GenomeDB) for
+# allowing cross-origin requests as defined by the Cross-Origin
+# Resource Sharing working draft from the W3C
+# (http://www.w3.org/TR/cors/).  In order for Apache to pay attention
+# to this, it must have mod_headers enabled, and its AllowOverride
+# configuration directive must allow FileInfo overrides.
+<IfModule mod_headers.c>
+    Header onsuccess set Access-Control-Allow-Origin *
+    Header onsuccess set Access-Control-Allow-Headers X-Requested-With,Range
+</IfModule>
diff --git a/test/integration/resources/sequences/yeast/names/0.json b/test/integration/resources/sequences/yeast/names/0.json
new file mode 100644
index 0000000..9d8566b
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/0.json
@@ -0,0 +1 @@
+{"mrps5":{"exact":[["MRPS5",0,"YBR251W","chrII",721384,722308]],"prefix":[]},"hht1":{"prefix":[],"exact":[["HHT1",0,"YBR010W","chrII",256328,256739]]},"rps8":{"prefix":["RPS8A"],"exact":[]},"hht":{"prefix":["HHT1"],"exact":[]},"cly1":{"exact":[],"prefix":["CLY15"]},"taf90":{"prefix":[],"exact":[["TAF90",0,"YBR198C","chrII",616121,618518]]},"erd2":{"exact":[["ERD2",0,"YBL040C","chrII",142114,142871]],"prefix":[]},"yal005c":{"exact":[["YAL005C",0,"YAL005C","chrI",139504,141433]],"prefix":[ [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/1.json b/test/integration/resources/sequences/yeast/names/1.json
new file mode 100644
index 0000000..d3044f7
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/1.json
@@ -0,0 +1 @@
+{"yal020":{"prefix":["YAL020C"],"exact":[]},"yal063c-":{"prefix":["YAL063C-A"],"exact":[]},"urp":{"exact":[],"prefix":["URP1"]},"lon":{"prefix":["LON1"],"exact":[]},"ola1":{"prefix":[],"exact":[["OLA1",0,"YBR025C","chrII",290680,291865]]},"ybr200":{"prefix":["YBR200W","YBR200W-A"],"exact":[]},"tss1":{"exact":[["TSS1",0,"YBR126C","chrII",488898,490386]],"prefix":[]},"ybr294":{"exact":[],"prefix":["YBR294W"]},"haf":{"exact":[],"prefix":["HAF4"]},"ybr163w":{"prefix":[],"exact":[["YBR163W",0 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/2.json b/test/integration/resources/sequences/yeast/names/2.json
new file mode 100644
index 0000000..7f306af
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/2.json
@@ -0,0 +1 @@
+{"tpd3":{"prefix":[],"exact":[["TPD3",0,"YAL016W","chrI",124879,126787]]},"ybr235w":{"prefix":[],"exact":[["YBR235W",0,"YBR235W","chrII",686895,690258]]},"hc":{"exact":[],"prefix":["HCP1"]},"ybr012":{"prefix":["YBR012C"],"exact":[]},"rbg":{"exact":[],"prefix":["RBG1"]},"ybr238c":{"exact":[["YBR238C",0,"YBR238C","chrII",695101,697297]],"prefix":[]},"ybl101":{"exact":[],"prefix":["YBL101C","YBL101W-C"]},"ybr207":{"exact":[],"prefix":["YBR207W"]},"ybr046c":{"prefix":[],"exact":[["YBR046C",0 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/3.json b/test/integration/resources/sequences/yeast/names/3.json
new file mode 100644
index 0000000..181ec62
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/3.json
@@ -0,0 +1 @@
+{"drs2":{"prefix":[],"exact":[["DRS2",0,"YAL026C","chrI",95630,99698]]},"pet1":{"exact":[],"prefix":["PET112","PET161"]},"yar070c":{"exact":[["YAR070C",0,"YAR070C","chrI",224553,224853]],"prefix":[]},"ybr298c-a":{"prefix":[],"exact":[["YBR298C-A",0,"YBR298C-A","chrII",805028,805250]]},"yal037":{"prefix":["YAL037C-B","YAL037C-A","YAL037W"],"exact":[]},"yal065c-":{"prefix":["YAL065C-A"],"exact":[]},"ybr050c":{"prefix":[],"exact":[["YBR050C",0,"YBR050C","chrII",337180,338197]]},"cdc1":{"pre [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/4.json b/test/integration/resources/sequences/yeast/names/4.json
new file mode 100644
index 0000000..6dadbd8
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/4.json
@@ -0,0 +1 @@
+{"yar060c":{"prefix":[],"exact":[["YAR060C",0,"YAR060C","chrI",217147,217483]]},"ybr302c":{"prefix":[],"exact":[["YBR302C",0,"YBR302C","chrII",810333,811473]]},"cps15":{"prefix":[],"exact":[["CPS15",0,"YBR258C","chrII",729728,730157]]},"ybr102":{"prefix":["YBR102C"],"exact":[]},"cns":{"exact":[],"prefix":["CNS1"]},"yal056c-":{"exact":[],"prefix":["YAL056C-A"]},"ba":{"exact":[],"prefix":["BAP2"]},"ybl015w":{"exact":[["YBL015W",0,"YBL015W","chrII",194124,195705]],"prefix":[]},"sup47":{"pre [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/5.json b/test/integration/resources/sequences/yeast/names/5.json
new file mode 100644
index 0000000..f6813ae
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/5.json
@@ -0,0 +1 @@
+{"yar003":{"exact":[],"prefix":["YAR003W"]},"oaf1":{"exact":[["OAF1",0,"YAL051W","chrI",48564,51708]],"prefix":[]},"ybr221":{"exact":[],"prefix":["YBR221C","YBR221W-A"]},"ybr197c":{"prefix":[],"exact":[["YBR197C",0,"YBR197C","chrII",615197,615851]]},"ybr088":{"prefix":["YBR088C"],"exact":[]},"ybr277":{"exact":[],"prefix":["YBR277C"]},"mcm19":{"prefix":[],"exact":[["MCM19",0,"YBR107C","chrII",453786,454524]]},"kap1":{"prefix":["KAP104"],"exact":[]},"rad1":{"prefix":["RAD16"],"exact":[]}," [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/6.json b/test/integration/resources/sequences/yeast/names/6.json
new file mode 100644
index 0000000..219d588
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/6.json
@@ -0,0 +1 @@
+{"pho3":{"prefix":[],"exact":[["PHO3",0,"YBR092C","chrII",427691,429095]]},"gin":{"exact":[],"prefix":["GIN7"]},"ybr169c":{"prefix":[],"exact":[["YBR169C",0,"YBR169C","chrII",573909,575991]]},"ybr270c":{"prefix":[],"exact":[["YBR270C",0,"YBR270C","chrII",742755,744393]]},"od":{"prefix":["ODP1"],"exact":[]},"rib":{"exact":[],"prefix":["RIB1","RIB7","RIB5"]},"sen":{"exact":[],"prefix":["SEN34"]},"fig1":{"exact":[["FIG1",0,"YBR040W","chrII",316967,317864]],"prefix":[]},"ys":{"prefix":["YSA1 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/7.json b/test/integration/resources/sequences/yeast/names/7.json
new file mode 100644
index 0000000..31808a8
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/7.json
@@ -0,0 +1 @@
+{"mak":{"prefix":["MAK16","MAK5"],"exact":[]},"gct1":{"exact":[["GCT1",0,"YBR125C","chrII",487192,488374]],"prefix":[]},"fzo1":{"exact":[["FZO1",0,"YBR179C","chrII",586541,589109]],"prefix":[]},"rim":{"prefix":["RIM2"],"exact":[]},"ifa38":{"prefix":[],"exact":[["IFA38",0,"YBR159W","chrII",558678,559722]]},"ybl035c":{"exact":[["YBL035C",0,"YBL035C","chrII",151495,153613]],"prefix":[]},"yal068":{"prefix":["YAL068W-A","YAL068C"],"exact":[]},"acs":{"exact":[],"prefix":["ACS1"]},"ybr075":{"pr [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/8.json b/test/integration/resources/sequences/yeast/names/8.json
new file mode 100644
index 0000000..b30f5f3
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/8.json
@@ -0,0 +1 @@
+{"arp1":{"prefix":["ARP100"],"exact":[]},"tmp":{"exact":[],"prefix":["TMP3"]},"hrd4":{"prefix":[],"exact":[["HRD4",0,"YBR170C","chrII",576338,578081]]},"cif":{"prefix":["CIF1"],"exact":[]},"rrr":{"exact":[],"prefix":["RRR1"]},"ybl039c-":{"prefix":["YBL039C-A"],"exact":[]},"yal034c":{"prefix":["YAL034C-B"],"exact":[["YAL034C",0,"YAL034C","chrI",80710,81952]]},"ybr153w":{"exact":[["YBR153W",0,"YBR153W","chrII",547453,548188]],"prefix":[]},"yal023c":{"prefix":[],"exact":[["YAL023C",0,"YAL02 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/9.json b/test/integration/resources/sequences/yeast/names/9.json
new file mode 100644
index 0000000..2de48cb
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/9.json
@@ -0,0 +1 @@
+{"ybr057":{"exact":[],"prefix":["YBR057C"]},"yar015w":{"prefix":[],"exact":[["YAR015W",0,"YAR015W","chrI",169369,170290]]},"ybl054w":{"exact":[["YBL054W",0,"YBL054W","chrII",117591,119169]],"prefix":[]},"ybr001c":{"prefix":[],"exact":[["YBR001C",0,"YBR001C","chrII",238942,241285]]},"kh":{"prefix":["KHD1"],"exact":[]},"ybr166c":{"prefix":[],"exact":[["YBR166C",0,"YBR166C","chrII",569836,571195]]},"rbp1":{"exact":[["RBP1",0,"YBR212W","chrII",647880,649899]],"prefix":[]},"agp":{"prefix":["A [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/a.json b/test/integration/resources/sequences/yeast/names/a.json
new file mode 100644
index 0000000..cba20f3
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/a.json
@@ -0,0 +1 @@
+{"yar031":{"exact":[],"prefix":["YAR031W"]},"yal043c":{"prefix":["YAL043C-A"],"exact":[["YAL043C",0,"YAL043C","chrI",58695,61053]]},"yal018c":{"prefix":[],"exact":[["YAL018C",0,"YAL018C","chrI",118564,119542]]},"sfp2":{"prefix":[],"exact":[["SFP2",0,"YBR294W","chrII",789229,791809]]},"shp1":{"exact":[["SHP1",0,"YBL058W","chrII",111438,112710]],"prefix":[]},"ybr054w":{"prefix":[],"exact":[["YBR054W",0,"YBR054W","chrII",343098,344133]]},"yal065":{"exact":[],"prefix":["YAL065C","YAL065C-A"] [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/b.json b/test/integration/resources/sequences/yeast/names/b.json
new file mode 100644
index 0000000..9a4f0fa
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/b.json
@@ -0,0 +1 @@
+{"yg":{"prefix":["YG100","YG106"],"exact":[]},"nhp6":{"exact":[],"prefix":["NHP6B"]},"sec":{"exact":[],"prefix":["SEC17","SEC18","SEC66","SEC71"]},"ybr297":{"prefix":["YBR297W"],"exact":[]},"apg12":{"exact":[["APG12",0,"YBR217W","chrII",657826,658387]],"prefix":[]},"csd2":{"exact":[["CSD2",0,"YBR023C","chrII",284427,287925]],"prefix":[]},"ybl075":{"prefix":["YBL075C"],"exact":[]},"ybr040":{"exact":[],"prefix":["YBR040W"]},"ts":{"exact":[],"prefix":["TSV115","TSM7269","TSC3","TSS1","TSC10 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/c.json b/test/integration/resources/sequences/yeast/names/c.json
new file mode 100644
index 0000000..d54aa89
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/c.json
@@ -0,0 +1 @@
+{"htb":{"prefix":["HTB2"],"exact":[]},"yi":{"prefix":["yIF2"],"exact":[]},"tap1":{"exact":[["TAP1",0,"YBR069C","chrII",376570,378430]],"prefix":[]},"ybl091":{"exact":[],"prefix":["YBL091C-A","YBL091C"]},"pet161":{"exact":[["PET161",0,"YBR037C","chrII",310563,311451]],"prefix":[]},"mo":{"exact":[],"prefix":["MOH1"]},"ybr01":{"exact":[],"prefix":["YBR010W","YBR011C","YBR012C","YBR013C","YBR014C","YBR015C","YBR016W","YBR017C","YBR018C","YBR019C"]},"ggs1":{"prefix":[],"exact":[["GGS1",0,"YBR [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/d.json b/test/integration/resources/sequences/yeast/names/d.json
new file mode 100644
index 0000000..2aacc61
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/d.json
@@ -0,0 +1 @@
+{"ybr119":{"exact":[],"prefix":["YBR119W"]},"ybr117c":{"exact":[["YBR117C",0,"YBR117C","chrII",474385,476431]],"prefix":[]},"ybr072c-a":{"prefix":[],"exact":[["YBR072C-A",0,"YBR072C-A","chrII",382854,383016]]},"yar008":{"prefix":["YAR008W"],"exact":[]},"yar020":{"prefix":["YAR020C"],"exact":[]},"ami1":{"exact":[["AMI1",0,"YBR244W","chrII",707522,708011]],"prefix":[]},"ybr220c":{"prefix":[],"exact":[["YBR220C",0,"YBR220C","chrII",662989,664672]]},"ng":{"prefix":["NGR1"],"exact":[]},"prs3" [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/e.json b/test/integration/resources/sequences/yeast/names/e.json
new file mode 100644
index 0000000..8310e14
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/e.json
@@ -0,0 +1 @@
+{"rma2":{"exact":[["RMA2",0,"YBR111C","chrII",461171,461867]],"prefix":[]},"pdr3":{"exact":[["PDR3",0,"YBL005W","chrII",217472,220403]],"prefix":[]},"npk":{"exact":[],"prefix":["NPK1"]},"yal012":{"prefix":["YAL012W"],"exact":[]},"ybr028c":{"prefix":[],"exact":[["YBR028C",0,"YBR028C","chrII",294424,296002]]},"ade1":{"prefix":[],"exact":[["ADE1",0,"YAR015W","chrI",169369,170290]]},"yal032c":{"prefix":[],"exact":[["YAL032C",0,"YAL032C","chrI",83335,84475]]},"ybl100c":{"prefix":[],"exact":[[ [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/f.json b/test/integration/resources/sequences/yeast/names/f.json
new file mode 100644
index 0000000..0de53e0
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/f.json
@@ -0,0 +1 @@
+{"rif1":{"exact":[["RIF1",0,"YBR275C","chrII",751350,757101]],"prefix":[]},"nth2":{"exact":[["NTH2",0,"YBR001C","chrII",238942,241285]],"prefix":[]},"st":{"exact":[],"prefix":["STR1","STT1","STU1"]},"sop":{"exact":[],"prefix":["SOP2"]},"ybl039c-a":{"exact":[["YBL039C-A",0,"YBL039C-A","chrII",144950,145034]],"prefix":[]},"trm":{"exact":[],"prefix":["TRM4","TRM7"]},"psk1":{"exact":[["PSK1",0,"YAL017W","chrI",120225,124296]],"prefix":[]},"cln":{"exact":[],"prefix":["CLN3"]},"ybr111":{"exact [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/names/meta.json b/test/integration/resources/sequences/yeast/names/meta.json
new file mode 100644
index 0000000..bef7c0d
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/names/meta.json
@@ -0,0 +1 @@
+{"compress":0,"lowercase_keys":1,"format":"json","track_names":["Genes"],"hash_bits":"4"}
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-0.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-0.txt
new file mode 100644
index 0000000..696640f
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-0.txt
@@ -0,0 +1 @@
+CCACACCACACCCACACACCCACACACCACACCACACACCACACCACACCCACACACACACATCCTAACACTACCCTAACACAGCCCTAATCTAACCCTGGCCAACCTGTCTCTCAACTTACCCTCCATTACCCTGCCTCCACTCGTTACCCTGTCCCATTCAACCATACCACTCCGAACCACCATCCATCCCTCTACTTACTACCACTCACCCACCGTTACCCTCCAATTACCCATATCCAACCCACTGCCACTTACCCTACCATTACCCTACCATCCACCATGACCTACTCACCATACTGTTCTTCTACCCACCATATTGAAACGCTAACAAATGATCGTAAATAACACACACGTGCTTACCCTACCACTTTATACCACCACCACATGCCATACTCACCCTCACTTGTATACTGATTTTACGTACGCACACGGATGCTACAGTATATACCATCTCAAACTTACCCTACTCTCAGATTCCACTTCACTCCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-1.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-1.txt
new file mode 100644
index 0000000..f634827
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-1.txt
@@ -0,0 +1 @@
+ATTATAATCCTCGGTAGACAACAGATTTATTGTACTAAAGTTACTCTTCCTGTTATCTTCCTTGATTTTACTGTTATAGCAATGACCCACCGCAATCAGGAGAGCCGCCGTATGGAATAGCATACCAAGTCATAAAATCGTCAACCTATTAACGGGGTTCAGGTTCTTTTTCAGCGTAGTAGCCCTTTAACAAGCGCTGACAAAGTTGACACTCAGAGAAAATTCAGGATTTATTGTAATCCAGCTACTCATCCTTAGATCCGCTTGCAGGCATGGTTTTTTTCACCTTGAGAGGCTATTTTGGGTAAGCCAGGAAGGCTGAAAAATCCCAAAAGGACACAGTAATAAGAAATTGTTGTTGTTGTATGATGCATTTAGAACTCAAAAGACGAGTTTCTGAAAATGCTTACAATACTCCATAGGTAACATGATTTTTTTATTAAAAAAGTATACTGTTCCTTTGGGTAAAAATTATGCAACCCTTGAGTGTCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-10.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-10.txt
new file mode 100644
index 0000000..94e5c10
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-10.txt
@@ -0,0 +1 @@
+ATTAAACTATTTGATAAAATATTACACTGAAAGGTGTTACAACTTTTCCTAGACCAACCTTAATATAAACGTTACGATATAAAATAAACAGTAATTTTAAGATTCCATTGTTGAATGTGACACGTAACTCTAGGCCATACCTGGTTTAGTATTTTTTAAAAGAACTCTTGCTACAGCATATTTTACGTTTGCATAAAAATAAAAGAAAAGCTTCCTTATGAAACACTGGTTAAGAAAAGAAAATGGAGTGGTTACATACTCCAAAATAAATTACTCTTAATATCTGTTTATCCAATGAGGAGCAGATCTCAAAGATAATCAGACTTGTTATTGAAGCAATTGAAGTAAGTCTTCCATTGAAATCACGCTAATTATTAAGCAATATGGTGGATTAAACAACTCTGACGGGACCGTACTACTTGATGAAAAGTTATATACAATATTGAATAACAGCATAATGCTGTATGATGTTGAGCGGAAGATTGTATAAAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-11.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-11.txt
new file mode 100644
index 0000000..a2ba213
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-11.txt
@@ -0,0 +1 @@
+CTTCATTTTTTTTTTGTTTTTCCCTTTTGTCTTTTGCACCGCTTATATATGGGTATGAAACAAGTTCAAGAATTTATAATGGAACCCAAAGGTTCAGTCTTTGTAGTTCGAGCGACATTGCGCGTTTCCTTAGAAAACGCTGGAAAGATATTCTTTAACGAGACGGAGTAATTCTCGTCAGGAATAGGATGTTGATTGATTTTTGCTGTAGTTATATAGCAGGGACCCACGGAAGAGAGCGAGCGCCTTCTTTCACAGGGACTTTTGTCAGCCACGTCTCCGGGGAAAACAATTGCCGTCCGCGTCGCAGTGAGATTACGCAGCCGTGCGCTTCAGGGACAGAAAAGAAGCATTTCGCGGCTACGGAGAAACCGTGCACTAACTCTCTCGAGGGTAGCCGCAAAGATTTCTTGTCTCTTCCATTAGGACATAGCTATCTTTTTCTTTTCTGTTTTTGGCGTATGATCTGTTCTGAGCCAAAGTTATAGATCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-2.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-2.txt
new file mode 100644
index 0000000..c45856c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-2.txt
@@ -0,0 +1 @@
+ACTCTAGATGGTAAAGGGTGTGCCTCTAAGGGGTATGACATTTCTTCCGGTAATATGATTATCCCATCCCTATTTTCTGAAGATAAGCTGCCGGCTTTAACTTATCATTGTTCCGTAGAATTAAATGGAAACATTTACATATTTGGGGGATTGATGCCATGCTACAGCTATGAGGAGGATGCGCCGATGCTGAACGATTTTTTTGTAGACGGAATAAAGAACTTACCTCCCCCTTTACTACCTCAAGTGATTAATAATCCATCAATGGTCAATAATCCTCATCTTTATGTCGCTTCTATACCATCATGCCGGTTTAGCAAACCTAAAATGGGGGGTTATATACCGCCTCCATTGCTATGTGTTCAAGGATCCAAATTAACGGACCGACATATTTTCTTTTATGGCGGATTTGAAATCAGGACAGAAACCCGTGGTGATGAAAATGGGAAGTATCATCTCAAGAAAAGATTATATGTGAATAACACTGGTTAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-3.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-3.txt
new file mode 100644
index 0000000..c57ea8a
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-3.txt
@@ -0,0 +1 @@
+CCTGGACAACGTTTTCTTGCTATTGCCCTTGGAAGGGTCGAAGTTCAAAATTCCCTTGCTCTTGGTCTCTTCGCCAATAACGTGTAAAGTTTGAGAAATCTTGGTCAGCTTGGAGTAGATCGATGACCCTGATCCGGATGAGAGGGATTTTGTAATGATTTGATTTTTTAGCCCAAATTGCACAAAGTTCTTGTACGCCCTTTCAACAAATCTCTTGGATAGTTTGTAGTTCAAGTCAGACTTGCCCTCTAGGGGAAACTTGGCGTCGACGTTGAAACGCAACAGCCCGGAAAGAATTCTTATTGTTGTCTGCGGCCTTCTTTTGATGACGAAGGATAAAGAATTGATGATACCAATGAAAACGGACGAGACCATGTACTGTTCCTCAATTAGGTAGTTTAGCAACATATCAAGAAGCCTCTTAGCCTCGCTCTCCAAAGCCGGTTTGTTCAACACAGGGTGGTTATCCGGGATGGTAGATGAATTAATCTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-4.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-4.txt
new file mode 100644
index 0000000..59cb440
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-4.txt
@@ -0,0 +1 @@
+CCTGAAGAGTATTTGGACGCCAATGTTTTTAGATTGGAGAACCAAAAGGATCTGGTCATTGTAGATGAGAATGAGTTGAAGAAAAGTGAGGAGAAACTTCGAGAGAAAGTGAACGACGTGGAGTTAGCGTTCAAAAAGAATGAAATGCTATTGAAAAGAGTTACAAAAGTGAAAAGACTGTTGTTTACGATAAGAGGATTCAAACAAAAGCTAAACGAGTTACTGAAATGCAAAGACGATGTACAATTGCAGAAAATTTTGGAGTCGTTAAAACCTATAGATGACACAATGACTCTACTGACTGATTCATTACGTAAACTATATGTTGATAGTGAAAGTACCAGTTCAACAGAGGAGGTAGAGGCACTACTGCAGAGATTGAAGACCAACGGGAAGCAAAATAATAAGGATTTCAGAACACGATATATCGATATAAGGACGAATAATGTCCTACGAAAATTGGGGCTACTAGGTGATAAAGAGGACGAAAAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-5.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-5.txt
new file mode 100644
index 0000000..056670b
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-5.txt
@@ -0,0 +1 @@
+AGGTATTATTTTTTTTTTTTTTGATAAGAAATTTAAGTGTTACAGAATGGGCCATCTTACAAAAATAATAGTCTTTATGTATTTTTATATATGTAAAAGAATTGAAATATTTTATAACTGGTTGTTATTATGGTACAGTGCGCTGCCCAATCCACGTGGAAAAATCCTGTTCATTCAATAATAGAAACTGAAATCATGTAATGTTGCGTAGTATAGTGCGTGAGCTTATTGTGCCACTTCTTGCTCAGCATTTTGAACTTCGTGTTCCTCCTCGTATTCGATTTCGACTTTAGGACGTTTAGTTTTGGCACTAGTTCCCTTCTTACGTCTCTTGGCTGAATTCTTATTTTCTTCATCACTATCGCTGTCGCTCTCGCTTTCACTGTCGCTTTCGCTTTGACTGGCGCTGGAAGCTTCTCTGTCAGAGTCAGCTAACCACTTTTCTAAGTCATCCACGTCAACGTATTCACCTTCGCCATCATCTGCAACGTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-6.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-6.txt
new file mode 100644
index 0000000..b431489
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-6.txt
@@ -0,0 +1 @@
+AAACCCCGGATATCGTCACAACAGCGGTATATTATAGTTATTTTGCATCTTTTTGGTAAGTCAGAAATTAGAAACAGTAGCTTACCAATTGAGTGAAAGTTTCCGTTCGTCATCTCCCTCTTTTGTTTTTTTTCACCTTTGTTTTTCTGTTTCTTCTATTTTTGTTTTGTTTTGTTTTGTTTTGTTCTCTCCCTAATTTGCATATAGGTAAACATCAAAGAAGTAATGCCCTACATCGGTGCTTCCAACCTCTCAGAACATTCATTTGTTAATTTGAAGGAAAAACATGCGATTACACATAAAGGTACGAGCAGTTCTGTAGCATCTTTGCAGACACCACCGAGCCCCGATCAAGAGAACCATATTGACAATGAATTAGAAAATTACGATACGTCTTTAAGTGACGTTTCAACCCCGAATAAAAAGGAAGGTGATGAGTTCCAGCAAAGTTTAAGAGATACATTTGCGAGCTTTCGGAAGACTAAACCCCCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-7.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-7.txt
new file mode 100644
index 0000000..295c460
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-7.txt
@@ -0,0 +1 @@
+TCGACATCGAAAGTGACTTCAATTTGTGGGACACCTCTTGGAGCTGGTGGAATACCACTCAATTCGAACTTACCCAACAAGTTGTTGTCCTTAGTCTTGGCTCTTTCACCTTCAAAGACTTGAATCAAGACACCTGGTTGGTTATCAGCATAAGTGGAAAAGATCTCGGACTTCTTTGTTGGAATGGTAGAGTTTCTTGGAATCAACTTGGTCATGACACCACCAGCAGTTTCAATACCCAAGGATAATGGAGCGACATCCAACAACAATAGATCTTGAGTCTTGGAAGATTCGTCACCAGTCAAAATAGCAGCTTGAACAGCAGCACCGTAAGCAACAGCTTCATCTGGGTTGATAGATCTGTTTGGTTCCTTACCGTTGAAGTAGTCAGTGACCAATTTTTGGACCTTTGGAATTCTGGTAGAACCACCGACCAAGACAATTTCATCGACTTGAGATTTGTCCAATTTAGCATCTCTCAAGACCTTTTCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-8.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-8.txt
new file mode 100644
index 0000000..803f1e5
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-8.txt
@@ -0,0 +1 @@
+ACCGAAAAAAAAGCTCTTCTAAACTGTTGACATCCAGTTCATTTACTTCCACGTGTAGATGTGAAGGAACAAATATTTTAGCATCGTTCATACAAGTAATTATGCTATATTATCGATCCTCGGATTTCAGCTTCCGTTATATCGGATGATTGTTACTCGACCTTTATGTCGTCTTTTTACATCATATATGATAATATGCTAGCAGTTTTAATACAAATTGATCGAAGATAGTTGGTTCTGAGAAATGGGTGAATGTTGAGATAATTGTTGGGATTCCATTGTTGATAAAGGCTATAATATTAGGTATACAGAATATACTAGAAGTTCTCCTCGAGGATATAGGAATCCTCAAAATGGAATCTATATTTCTACATACTAATATTACGATTATTCCTCATTCCGTTTTATATGTTTATATTCATTGATCCTATTACATTATCAATCCTTGCGTTTCAGCTTCCACTAATTTAGATGACTATTTCTCATCATTTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-9.txt b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-9.txt
new file mode 100644
index 0000000..b4c52d2
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/5c1/aff/29/chrI-9.txt
@@ -0,0 +1 @@
+GTTTAACAAACGATTTAACTTGCTTTTGCTTACAAGTCAAGTAAACCTTATCCTGATAGCTTAGGAAAAATAGACTTGAATGTGTCGAACATTTCAAACCTCAATTGGTATTTTCCTTTTTTTCAACTGTACGTACATAGCTTTTCGCTTTCTTTAGCGCCCCCAGATGAAAGTATATATCGTAACAAGGATGGGAACATGAAAGGTACTGAAAAAACATCTGTATTTATTAAAAGTAAATCAAAAGCAGACTGGGAAGTTCTGTCGTAGGGATTTTTTTTTTAATGTTATGTGTGTAGGATTATTCTATTTCCTTGAATTTCTCGATCGAGATTTTTCGTACCTGTGTATTTTTGGATATAAGAGTGTTTCTGATCTATTGAGTGAGCAGGTCTCCAGCGGAATATAGAGTAGATTGAATATGGAAGAGGACTACATTAAGGCTTATTGTTAGTTAGTTACTGTTAGGACGCTTCGGCGAGCTGATGTCTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-0.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-0.txt
new file mode 100644
index 0000000..5bb66e4
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-0.txt
@@ -0,0 +1 @@
+AAATAGCCCTCATGTACGTCTCCTCCAAGCCCTGTTGTCTCTTACCCGGATGTTCAACCAAAAGCTACTTACTACCTTTATTTTATGTTTACTTTTTATAGGTTGTCTTTTTATCCCACTTCTTCGCACTTGTCTCTCGCTACTGCCGTGCAACAAACACTAAATCAAAACAATGAAATACTACTACATCAAAACGCATTTTCCCTAGAAAAAAAATTTTCTTACAATATACTATACTACACAATACATAATCACTGACTTTCGTAACAACAATTTCCTTCACTCTCCAACTTCTCTGCTCGAATCTCTACATAGTAATATTATATCAAATCTACCGTCTGGAACATCATCGCTATCCAGCTCTTTGTGAACCGCTACCATCAGCATGTACAGTGGTACCCTCGTGTTATCTGCAGCGAGAACTTCAACGTTTGCCAAATCAAGCCAATGTGGTAACAACCACATCTCCGAAATCTGCTCCAAAAGATATTC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-1.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-1.txt
new file mode 100644
index 0000000..ee0eba3
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-1.txt
@@ -0,0 +1 @@
+CTCATTGTTGACTCTAATTTCATCTATCTCAGTATTTTCAAAATTTGACAATAAGAGAGAGTTCCTATTATTGAGTATAGCCTTGGAGCATACCTCTGAAATTGGCATTCTGTATATTGTTCCCGATTGTCTCATACAGATTAGACTTGTTCCATTATTACTTCTTGGAATATAATCAAATGTAGCAACTCTATCGTACATCGTATTTGTGTCATGCACGGAAGAAACAAACAAATTCTCAATATTCATCTCGTTATCGTCATCTGCCGCAATATCACGATTGCTATCGCAGTAATAGCCTAACCTCCATCTTTTGATGGTATCACCTCTATGTAAGGTGGCAAATTCATTGTTCCTCACACATGACCACCTAAAGCAAGAGTTCATGTATTTCCTTGACGCTGCACCGGATCCAACCAATTTTTCAAAAGTTAATAAAGGGGAAGCTACATTTAAATCACCAAGTGAAGCTTGGTCAGATAATTTTCTCCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-10.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-10.txt
new file mode 100644
index 0000000..42a1b89
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-10.txt
@@ -0,0 +1 @@
+CGTTCATCAGCGTTATTGAAAAGTGACGCCCATTCGGGATGGTCGACAATGATGGGGGTTTCTTGTTTGTTCGGTTCGGAGTTTTGAGTGTTTGATAAAGCATAGTAATAAACCTCGGATGAGTTCCTTAGTTTGACTAAAAAATCTACAACTAACTCAAAATTTTCATCTGCATCCTCCTCTCTCCGAGATTCATCGTCTATATGATCCAGTGTTAAGATTGTCTCTATACCGGTGGGGGTCCCACCCGGAATTTCAAGAACTGTTGAGCATCCTAAGGATTGAAACAAGTTTGCTTTCCTATGGGAAAATACATGCATATAGATGCGTTTATGCCGCATCGAATATACAAAGGCCACCAGAAGAATATGGTCAGGCTTTTTAACCTTTTGTACAGTGATTCTGAGCGTAGTATCATCTGGATCCAGATCGTGTTTCCAAGATATTCTTCTCACTGGATCATTTGATTCTGATGCCCCTACGATGATTATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-11.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-11.txt
new file mode 100644
index 0000000..a1b2544
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-11.txt
@@ -0,0 +1 @@
+AGAAAGCGACTATAAATTTTTGTATAGAGACAAATTGGCCACTGGTAATATTCCAGACCAAGGAAATTCAAGCCAAATTTCTCAGTTGTATGACAGTACTGCTCCTTCATACAACAATGCTTCTGCCTCAGCAGCAAACTCACCGTTGAAGTTATCGTCTTTGTTGAACTCTGGAGAGGAATCGTACACTCAAGACGCATCAGAAAATGTTCCATGTAATCTGCGGCATCAAGATCGATCGTTACAACAGACAAAAAGACAACATTCTGCGCCTAGCCAAATAAGCGCTAATGAGAATAATATATACAACTTGGGTACTTTAGAGGAGTTTGTCAGCAGTGGTGACCTGACTGATTTATATCATACTCTGTGGAATGACAATACTTCATATCCCTTCTTATGAAAACGCAAAAGAAATAGGGAAGCAGAGCATAACCATAGTAAATGGGACACTTATAACTCAAGAATTTAATTGACCCCCTACTCATACTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-12.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-12.txt
new file mode 100644
index 0000000..a09671d
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-12.txt
@@ -0,0 +1 @@
+TCCACACCTCTTTATATTCTTTAATAGCAGCTCTGAACGCGCGTTTTAACAACTGAATAGCGTTTGGATTATTCTTACCTCCTATCTTTTCAAATACGAGCAGAGCCATATCGGTTAGGAATGGTGGCTGAGATCTGCAAAGATAATAACTCCTATTAGCATTCAAAATTTTACTATAGTGATCAATTTCGAATATAAAATGCTCCACCATACCTCTTGCAACATCAACCTTATTACTTTCTATGAGTCCCAGTGCCATTAAATATGAATCCCAGCCATACAATTCATTGAATCTACCGCCAGGTACCGCATAAGGATAACCAACTAGGGACCTTTCACCAGTAGATGGATTAACGTGTTCTTCCATAGCCAGGGCTAACAAACCCGGTGTATCATTTAATGATTTAACATACTCTGCTGTAATATCTTTTGGTAAGTACTCCACTTCAAGTTTTAAGGACGGGTTCATTTGAGATGCCTGAATGTAAAATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-13.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-13.txt
new file mode 100644
index 0000000..7a14e93
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-13.txt
@@ -0,0 +1 @@
+GGCTTCCACTAAGGCTAACTCTCAACAGACAACAACACCTGCTTCATCAGCTGTTCCAGAGAACCCCCATCATGCCTCTCCTCAACCTGCTTCAGTACCACCTCCACAGAATGGGCCGTACCCACAGCAGTGCATGATGACCCAAAACCAAGCCAATCCATCTGGTTGGTCATTTTACGGACACCCATCTATGATTCCGTATACACCTTATCAAATGTCGCCTATGTACTTTCCACCTGGGCCACAATCACAGTTTCCGCAGTATCCATCATCAGTTGGAACGCCTCTGAGCACTCCATCACCTGAGTCAGGTAATACATTTACTGATTCATCCTCAGCGGACTCTGATATGACATCCACTAAAAAATATGTCAGACCACCACCAATGTTAACCTCACCTAATGACTTTCCAAATTGGGTTAAAACATACATCAAATTTTTACAAAACTCGAATCTCGGTGGTATTATTCCGACAGTAAACGGAAAACCCGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-14.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-14.txt
new file mode 100644
index 0000000..56b7fda
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-14.txt
@@ -0,0 +1 @@
+CTGGAACGGCGATATTGAATCCGGCATCGAACGGTTAACAAAGATGCTAGTACTAGTTGAAGAGTCTCTCGCCAATAAGAAACAGGGCTTTAGTGTTGACGATGTCGCACAATCCTTGAATTGTTCTCGCGAAGAATTCACAAGAGACTACTTAACAACATCTCCAGTGAGATTTCAAGTCTTAAAGCTATATCAGAGGGCTAAGCATGTGTATTCTGAATCTTTAAGAGTCTTGAAGGCTGTGAAATTAATGACTACAGCGAGCTTTACTGCCGACGAAGACTTTTTCAAGCAATTTGGTGCCTTGATGAACGAGTCTCAAGCTTCTTGCGATAAACTTTACGAATGTTCTTGTCCAGAGATTGACAAAATTTGTTCCATTGCTTTGTCAAATGGATCATATGGTTCCCGTTTGACCGGAGCTGGCTGGGGTGGTTGTACTGTTCACTTGGTTCCAGGGGGCCCAAATGGCAACATAGAAAAGGTAAAAGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-15.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-15.txt
new file mode 100644
index 0000000..93be46c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-15.txt
@@ -0,0 +1 @@
+ACAGTGAGAAGTGAAAAATTTTTTTTCAATCTGAAAAAAAAAAAAAAAAAAAAAAAAAATTTATATAAACGAATGGTATCTCCATCACATTTCTTTTAGCCTCGCAACTTGTACTTTTCATCACTTTTCTTGTAATTTAGCAATATCCCAAGAACAATCATCGAAATGTCCCGTCCACAAGTTACTGTTCACTCTTTGACTGGTGAAGCTACTGCCAATGCCTTGCCATTGCCAGCTGTCTTCTCCGCTCCTATCCGTCCAGACATTGTCCACACTGTTTTCACCTCTGTGAACAAGAACAAGAGACAAGCTTACGCTGTTTCTGAAAAGGCTGGTCACCAAACCTCCGCTGAATCCTGGGGTACCGGTCGTGCCGTCGCTCGTATTCCAAGAGTTGGTGGTGGTGGTACCGGTAGATCCGGTCAAGGTGCCTTCGGTAACATGTGTCGTGGTGGTCGTATGTTTGCTCCAACTAAGACCTGGAGAAAGTGG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-16.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-16.txt
new file mode 100644
index 0000000..41d235f
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-16.txt
@@ -0,0 +1 @@
+ACTCTCTTGACATCACTGCAAAGACCAAATTATTAAATGATTCCTTGAGCCGGTTAAATCTACCGTCTTATGCTATGCCCCTATTTGTTAAATTTGTTGATGAAATTAAAATGACAGATAATCATAAAATTTTGAAGAAGGTTTATAGAGAGCAAAAATTACCAAAGGGTTTGGATGGAAATGACACTATTTTTTGGCTCAAGAATTACAAGCGCTATGAAGTCTTGACCGCTGCTGATTGGGAAGCCATCGATGCACAAACAATTAAATTATGATTTGATGTTCCTGAATTAGGATAAAGTATTTTTCCTTGCACGAGAGTACGTATGTAAACGTAATATTGATAAAGAATCAAAAAAAGTTTTATATCTATCTATATATGTACCTAATAACATATGGAAAGGATTATTTAAATTCAAAAAATAAAACAATAAAGTTTATAAACTAACCAAATTATTGTTAGAGTTGGTAATATTAGCACTTCAGTTAACT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-17.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-17.txt
new file mode 100644
index 0000000..82a794d
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-17.txt
@@ -0,0 +1 @@
+GGAGACTCAAAAGATTGATTGTTGGAGTTCTTCACGTCAATTAACTCGTCACGTTTTAGCAAGTCGCCACCAGGACATTTCCAAATAGTGAAGTTTAAAGAATCAGTCACGTACATCGTCTTTTGATCACTTTCATCCCAGTGGATCGCGTTAGGAATTAACAAGCAATTCCAAACAAGCTCTATCTTGTGAGCTAATAGGTCAACACGAAGCAAACACCCAATAGGTTCCAAATCAAATGGAAAATCGCTCATTAGCCCCACATAAATATATTTTCCATCTGGCGAAACGTTCCCATCATTAGATCTCAACTTATATGCTCTGTCTGTACTCAATTCAGGACATTCGGAATACAAAATGACATATTCCCATTCACTTTTGCTAAAATCCAATTTTCCTATGCCAAATTTACTACCAAATAATACTTGTTTGATTTCATTTTGTGAAGCCCCATCAAGTATTGGAAATATACAGCCCACGGATTCTTTTAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-18.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-18.txt
new file mode 100644
index 0000000..b3aea45
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-18.txt
@@ -0,0 +1 @@
+TCAAATTCATTAAGATACTCTAGGAATTTCACCACGTAAATTTGAGCAAAGCCACCTTCTGCTAAATAATTTACCACTTCAACTTTATGTGTGCCTACGCAGACAATATGGCCTGGAGTGTACCTCTCTACCGCCGGATGGCCCATTGCCGAAACGCTTCTAGAAGTACCATTCGTGATCGACATGATCAGTGCTAATTAGAAAGAAAATAACAGGCGACAGGGACGAAAACAAGAGCTTGGGAAAACGTAATAGCAAAAAATGAATGGCAACTAAAATTATCTCTATGATGTCCTGTGTTGCTTATGCTCCCGAGGCACCGATTCTGATTCAACAATGCTATTGAATTTACTCATTCAAGAAAGGGGGTTCATAGGATTATGGCTTTTATTCAGTATTTTTAAGTTTTTTCGGTTATTGAGCTCTGAATAGGGCATTTTCAGAATGGCGGGGTTCACCTAAAAAAAATGAAAAACAAATATAGGTCATAAC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-19.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-19.txt
new file mode 100644
index 0000000..889e6e9
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-19.txt
@@ -0,0 +1 @@
+CTTGTACATGAAATATTAGGATGGCTTCGCTAAGAACATTTTTGCTTCTGGACAGACAGAACACGTAAAAAAAGAAACACGAAATTACGACTAACTTTCGCGTAGGGGTAAAATTACCTAATGCGCGTAATTAAAAAAAAAAGCACAGCAGTGTAAATTGGAGCAATAGGAGAAAATGACCTGTAAAGGAAGTATTCGAATTCACTAGTGCGGATATTAATAGAATTCATAATTTCGCGCTTGGCTGCTCATTATATTCTTGACTATTTGCCAAACTGTTATAAAGAAGCCTCAATATTCGTTTGGTTTATTGTTAAAGAAGGCGCAAGCTCGGCACGAGAAATATACGACGATAAAGAACATACCAACTATATATTGTGACGGCACAAAAAGACAAAATACGTAAAATGCTAAGACGCTCTAAAAATTCCAGCACTAACACAAACGCAGACACCAAGAAAAGACAATCCATGCATTTAGGATCCAAAAGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-2.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-2.txt
new file mode 100644
index 0000000..356858e
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-2.txt
@@ -0,0 +1 @@
+TCACTTATCCAAAGGAAAGTCTTGTCTGTGTAAACTGTAAGCCATACGATGTACCAGGCGGAAAGGCCATCCTACTCGGCGACGCTGCCCATGCAATGGTTCCATTTTACGGCCAAGGTATGAATTGCGGATTTGAAGATGTGAGAATTCTTATGGCGCTATTGAAAAAGCATTCAGGAGATCGTTCAAGAGCCTTTACTGAGTACACTCAAACAAGACATAAGGACCTAGTTTCTATTACTGAGCTGGCAAAAAGGAACTATAAAGAAATGTCACATGACGTTACATCCAAGCGGTTTTTATTAAGGAAAAAGCTAGATGCTCTCTTTAGTATTATAATGAAGGATAAGTGGATACCTTTGTATACAATGATATCTTTCAGATCCGATATCTCGTATTCTAGAGCTTTAGAAAGGGCTGGAAAGCAAACACGTATCTTGAAATTCTTAGAATCTCTGACACTCGGTATGTTATCTATTGGCGGTTACAAGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-20.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-20.txt
new file mode 100644
index 0000000..6af4f9c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-20.txt
@@ -0,0 +1 @@
+CCAATCTTTCTTGCAATTAAGGTCTTACCAGTACCTGGAGGACCGTACAATAGCAAACCTTTAACATGAGAAATACCCAGTTTTTCTATAACTGAAGGAGGAAAGATTCGACTTGCAAACGCTCTTCTGAAAATTTTAGTAAACTCTTTATCCAAACCACCGACACCCAAATCTTCAAACTTGAAATCCGGTCTGATCACAGCATTTGATCTTGGTCTTAATGAATTTGATGATTTCAAATTAACTAAACCATCTCTTCCTTTGAAAAAATTAATTTGTGTTTGTTTTGTCAAAATTCCCTTTGTCTCTATCCCAGTTGCAACAGCGGAGGTTGGTTCAATATCACCCAAATCGATTGCTTGGACATTTCTAATTTTTAAGTCAAAGAAATGGCCTTGGAACTCCATGATAAGGTACTGGGTGGGAGAAAATATTTGAGATTCGTAGCAACGAACAAATTGTTTGGCTAACTCATCTTGATCGAATACCGTG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-21.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-21.txt
new file mode 100644
index 0000000..ae41382
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-21.txt
@@ -0,0 +1 @@
+GAAGAAAGAGGCAATGCATGCGTAGAAACTAAATCTTGAGATTTCTGTTTGATGAAAACACTGCACAAGGTAAAATGAATTATTCTTTTAAAATTAGAGATACATAAAACATACACTTGATAGAAAGAAGAATACATATTAATTTAATGTCACCATATTATAAAAATAAAATTTGTTATCGTCCTAGCTTTTTTTTAAAGCTTCTTTTTCAGCTTATGCAATAGCCCCTTTTTATGTTTTGGTTTTTCCTTTGCCGCCGATGAAGAGGAAGAGGATTCGGTTGTCTTAGAATTGTTCGTGACATTCTTCACTCCAGCATCACGATGGCGGTGGTGGTGATGATGATGGTGGGGCTGAGTAGCATCTGTAGTAGTAGTAGTAGTAGTATTGTTATTAGATGGTCTCGCAGAATCCTTAGTTTCCTTCGTGGAAATAGATCTTGATGGGCCATTTTTCGTGCTTACTTCAGTCTTTTTTGTTTGTTCAGTAGCC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-22.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-22.txt
new file mode 100644
index 0000000..ffc51d8
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-22.txt
@@ -0,0 +1 @@
+GGTAGTCGTCAGCAACAGCTATGAAGGCGACGTTGAAAGCATAGAAAAATTCCTATCGACTTTCAAAATTTTACCTCCTCTGAGAGATTATAAGGAGTTTGGGCCTATTCAAGAGATTGTACGGAGTCCAAACATGGGTAATTTGAGGGGCAAGTTGATAGCTACTTTGATGGAAAACGAACCCAATTCTATTACGTCTTCTGCTGTTTCTCCAGGAGAAACACCCTATTTAATAACAGGTTCAGATCAAGGTGTAATCAAGATTTGGAACCTGAAAGAGATTATCGTGGGCGAGGTTTACTCTTCTTCTTTAACTTATGACTGCTCCTCTACCGTAACTCAGATAACCATGATTCCTAACTTTGACGCGTTTGCCGTTTCCAGTAAAGATGGACAAATAATTGTATTAAAGGTTAATCATTACCAACAAGAAAGTGAAGTCAAATTTTTGAATTGCGAATGCATCAGGAAAATTAACTTGAAGAATTTTGG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-23.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-23.txt
new file mode 100644
index 0000000..33aa573
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-23.txt
@@ -0,0 +1 @@
+TTGCAATGAACTATCCAGTGCTTGACGAATTAGTACAACACAATGTAAATGGGTTAAAATTTGTTGATAGAAGGGAGCTTCATGAATCTCTGATTTTTGCTATGAAAGATGCTGATTTATACCAAAAATTGAAGAAAAATGTAACGCAGGAAGCTGAGAACAGATGGCAATCAAATTGGGAACGAACAATGAGAGATTTGAAGCTAATTCATTGAGTCAATGGTAACTCAGCCTTTCTTTTTTGAAAATTACTATTTTCGACTCTTTTTTTATACAGTTACATAGTACTACCTCTAATACACATTCATGATTAACAATGTTTCAAACAATATAAAGTCCCGATAACGACCTTTTGAAGTGGTGACGTTACCGCTCTTCGTTGACAAGATTCAAGAGGGCTGTCAGAATAACAGCTATCATGGTGGAAAGTAGGTGACCCTTTGAACAAGGCAGCATATATCCATCGACTATCATGTTCCTAAAATGTTATCA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-24.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-24.txt
new file mode 100644
index 0000000..030133a
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-24.txt
@@ -0,0 +1 @@
+GATTGGAAAACATGAAGTCTCAGCAAGAAAATCTAAAACAATCGCAGAAACCTCTTAAGCGGGCTAAAGTGTCCAATACAATGGAAAATCCACCGAACAAAGTCCTTCTTATACAAAATTTGCCAAGCGGCACTACCGAGCAATTATTGTCGCAAATACTTGGCAATGAGGCTTTAGTTGAAATCAGATTAGTTAGCGTTCGTAACCTAGCTTTCGTGGAATACGAGACCGTTGCTGATGCTACGAAAATCAAGAATCAGTTAGGCTCCACTTACAAGCTACAAAACAATGACGTTACCATAGGATTTGCTAAGTAGAATTTCCTTTGCGGAAGTATACCTCGAGTAAAGAAATTCACAGATAAATTTGAATAACGTTCTCCATTATGAATAAATATGTATTTACAAGCTTAGAAAATAATGTGCTCTTTATTTACCAAATAATACAGTTCTCATTGCAGTAAATAAGCTTTCCTTTTTCTTACCGTCAACG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-25.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-25.txt
new file mode 100644
index 0000000..0e29eac
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-25.txt
@@ -0,0 +1 @@
+AAATAGACGATACAAAGGACGACAAAATTAATCAATTGAGAGGCAGTAATCAAGTTAATTAACCAATTCAATACGACCGCACTATTAGAATTCAGTTGCAAAAGGCTCACTAAAGCCCATACCAAAGATATGGCCACAGAGTAAATGGGCACACCATGCCTATTGCATCTAGTGAAGATTTTTGGCGCGTAGCCATCTAATGCCATACCATAAAATGTTCTGGATGAGCAATAAGTGTAAGCGTTACCAGCAGAAAAGGCGGCTGTAATCAAAGCTATATTAACAATGTCAGGTAATATTCTAATTTTCAGATTATTCATTGCAATGACATAAGGTGAAGACCCGGCGCCAGGCCTTGCTTCATTAATTGCTGCTGTCAAGTCAGGATCATTTGGCGAACAAACAATCCCAACACACAGACAACTCCCTAAAAACAAAAATGTTAATCTCACAAACACCTGCTTAAACGCCTTGGGTAATACTTTTCTTGGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-26.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-26.txt
new file mode 100644
index 0000000..d64786b
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-26.txt
@@ -0,0 +1 @@
+GACAAAACTTCTTTTTTTCCTTGTCATAAAGGGTAATATCATGCAAAGTAACTCTTACATCTTTCAAAACTTCCAAACTCCTACCACTCAATCCTAAGGACTTTATCAAACTTTGGTCAGTATTGCTGTTGATGAAACAACGGGGAATTGTAGTGACCAAGTACGGATTTTCTACAGTATATGATGAGGCCCATTGGTCCATAAATGATTTGTTGACGTTGAAGTAATAACATCCCATACAATTACTTGATGCTTGCTCTGGTATCAAACTGAAAAATAGAGTAGTCAATTTTTGGAAATTAGCCTTACCCCCATAAAAGGTGGTACAATCGATAACTACATAATGTTTAGAGCACCACATTCTTGCATATATCTGCAGAACTTTATAGACCAGTGAGTCCATTACAAAATTATTGTACTCGCAATTGGTAAAAGTTACGACAATGATTTGTATGCCATCCAATGTCATTGCTTCATGTACAAATGGCGCAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-27.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-27.txt
new file mode 100644
index 0000000..12fd008
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-27.txt
@@ -0,0 +1 @@
+AACCGAAAACATAGTCGAAAATATGTTGCATCCAAAGACTACAGAAATATACTTTTCACTCAACAATGGTGTTCGTATCCCAGCACTGGGTTTGGGGACAGCAAATCCTCACGAAAAGTTAGCTGAAACAAAACAAGCCGTAAAAGCTGCAATCAAAGCTGGATACAGGCACATTGATACTGCTTGGGCCTACGAGACAGAGCCATTCGTAGGTGAAGCCATCAAGGAGTTATTAGAAGATGGATCTATCAAAAGGGAGGATCTTTTCATAACCACAAAAGTGTGGCCGGTTCTATGGGACGAAGTGGACAGATCATTGAATGAATCTTTGAAAGCTTTAGGCTTGGAATACGTCGACTTGCTCTTGCAACATTGGCCGCTATGTTTTGAAAAGATTAAGGACCCTAAGGGGATCAGCGGACTGGTGAAGACTCCGGTTGATGATTCTGGAAAAACAATGTATGCTGCCGACGGTGACTATTTAGAAACTTA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-28.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-28.txt
new file mode 100644
index 0000000..fa814eb
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-28.txt
@@ -0,0 +1 @@
+AAAAAATATTCAAGCCAGCACATCAGCTACAGTGGAAAATAGCCCAGATCAAATAGAACTATCCTTCGAACATGAGCGGTGAATTAGCAAATTACAAAAGACTTGAGAAAGTCGGTGAAGGTACATACGGTGTTGTTTATAAAGCGTTAGACTTAAGACCTGGCCAAGGTCAAAGAGTAGTCGCATTGAAGAAAATAAGACTAGAGAGTGAAGACGAGGGTGTTCCCAGTACAGCCATCAGAGAAATCTCATTATTGAAGGAATTAAAAGACGATAATATTGTCAGATTATACGATATTGTTCACTCTGATGCACACAAGCTATATCTTGTTTTTGAGTTCCTCGATTTGGACCTGAAAAGATATATGGAGGGTATTCCAAAGGACCAACCGTTAGGAGCTGATATTGTTAAGAAGTTTATGATGCAACTTTGTAAGGGTATTGCATACTGCCACTCACACCGTATTCTGCATCGTGATTTAAAACCGCAGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-29.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-29.txt
new file mode 100644
index 0000000..ddf4ce3
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-29.txt
@@ -0,0 +1 @@
+TTTAATGAAGGCAAATCTTTTGATGTCTTAAAGCCTTCTGTTTGTGTTTGTGTGTCCTTTTTTGTCTTTTCTGATTTTTTTTGCTTTTCTGCTTCCAAAGCTTCTTTTTCCTTTTGTTCCTTTTGTTCCTTTTGTTTCTTCAATAAATCATCCTCTTTTTGCTTTTGTTTCTTCGATAAGCGAGCTTCTTCCTTCCTTTTGAGTTCTCTGTTCCGTTTCTCTTGTTCATCCAATAACGCTTTTGCTACAGATTCGGATTTTTCCTTCTTCCCTTTTTCCAGCATAGTGGTCGATTTGTATTCTTGCTTGTGAGATTCATCTGTTTGACTAATGGTGGTTGCTGAGGCTAATTTAATATCGACTCCCTTGGAGGTGGTACCTCCATCCATATCCCAGATATTGTCATGTGTCAATGGCTTGAAAATATCGTTTTCAGTAGCTTTAATAGTGCCAGTAGCGGCAACACCGCTTGCGTAAGGAGCTAGATTCAAG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-3.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-3.txt
new file mode 100644
index 0000000..699e5b1
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-3.txt
@@ -0,0 +1 @@
+AATAGCTGGCATAACCTTCTTTCTCAATTCTGGCTTACCCTTCTTAACGGTGGCCATAACCATATCACCTAGAGAGGCGGCTGGCAATCTGTTCAATCTGGAACCAGAGCCTTTGACGGCGATAATGTACAAGTTTCTGGCACCACTGTTGTCAGCACAGTTCATGATGGCACCGACTGGTAGACCTAACTAGTTAAAAAAGATCTCAATATCGGGGAGTGAGAATAATTTTGTTAGTAAAAGCATAGGGAGAAAGTTAGCAGATGTGGTTAATCTTGAAAAATAGAGTTAAGCGTGATTTTTGTAGAAATAACATTTCGATGGTTATCCATTAGTATGGGCAAAAGAGGGAGTTAGGATATGCTTGCCGTGTATGATTTTGAAACCAGAAATTTTTGAGTCCATGAATATTGACAGGTATCACTTCATCTGCAAATACAGCCCGCTTAACCATTTAAAAACTGAATTTATGTGTGTAATTGCATCTTTGGC [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-30.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-30.txt
new file mode 100644
index 0000000..c38ef1c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-30.txt
@@ -0,0 +1 @@
+TGCCTTGCTTTTCCAACTATTCCATTACTCAGGTTTTATTTTTTTATTTTGTAATATGGGGAGAAGGCCGGCAGAATATTTACGGACAAATGAATAAATTGGATTGGATTGACTAGTGGAACGTGTAAAGATCGCGATACTCCGTACCAATCACCGAAAGATTGCCCGTAACCGAAATGACTCCATTCTCTGAATTTTTTGTGAAACCAATATCTGAGACTCTTCCTTCATCTTATCAACGTATTGTTCAGTCAATTAAGTAAGAAGTATATTTGAGCGCAGCCTTAATCATATATAGCACCAGTTATATGTTTGCCCCTCTCTTGAGTTGAAAAACACATAATACATAGTACTGTACTTTTCTCTTTTTCATCGTTGGCGAAAATATAATCTTTCTCAAAAATATATATATATGTATATATATCCTTAGATTTGCCGTTGACAATAAGGTGGGCGGCAAATCTACGAAATGCGAGGCGGTTAAAAGAGAGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-31.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-31.txt
new file mode 100644
index 0000000..734ce88
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-31.txt
@@ -0,0 +1 @@
+ATATATGCAATATCTTGAAGAAAAGGGAGGATTCTACTATGAAAGGTGGGGTGATGCCCCTGTAAGAAGTTTGGCACTTGCTTTATTTGCGGATAAGTCAAGCATCCATTGGTTCAGGGATATAGGTTATCACCATACTCCGTACACGAACTGTCCAACTTGTCCAGCAGATTCAGATAGGTGTAATGGAAATTGTGTACCAGGCAAATTCACTCCTTGGAGTGATCTAGACAACCAAAATTGCCAGGCAACCTGGATAAGACACTCTATGTCTGAGGAAGAGTTAGAAATGTATTGAAGACATTAGCTTTGAGAATTTGTGCCGCTGCTTTCGAGAACGGCATCACATCTGGGGTTTTATGATTTAATTTAGGACGCACATAGATAGATATATATTATGGAATTAAGAAAAGTTTATAATGTAGCAACCTAAAAATGTTACAGGTGAACATTCATGGAAAATGAAATGTTTCTCAGCCAGTAGATAGTATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-32.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-32.txt
new file mode 100644
index 0000000..ea703d6
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-32.txt
@@ -0,0 +1 @@
+ACCAACAAAGGTAATGCCAGCACTGGTGCACGCATCAGAAAAATCCGCATTTTCCGACAAGAAACCATAACCAGGAATAATGGCCTGTGCATTAGTTTGCTTAGCGGCATCTATGATCTTATTCATGTCTAAATAAGTTTGGGCTGCGGTTGTGCCATGAAGGGGTACAGAAACATCTGCATCAGTAACGTGTTGAGAATATTTATCAGGGTCGGAATAAACTGCAACTGATCTAATACCCAATTTTTTTAATGTCTTGATAATACGAACGGCAATTTCACCTCTATTGGCAATTAACACGGTATCAAATAACTTCTTCTTTTGGGACTCTTTTTTCTTCAACATTTCAAAATATGCTCTAAATCCACCATACTTTGTGATATCAACTGTACCTTTGGCTTTGTAACCAGATTCTTCACAAATAAAGGATTTGATCCATTCACCAGATTCTAACTCCACTGAACCTATTCCTAATGGTTCAGGAACCATGGA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-33.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-33.txt
new file mode 100644
index 0000000..450669b
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-33.txt
@@ -0,0 +1 @@
+TAATTTGGCAGCGGCCGGTTTCATAGTACCTGCCATATCCTTAATACCCAAGATATGTGTACCCATTTGAACTATTTTTTCAACAACTTCTAGGTAGTAGTCTAAGTTGTATTTCTTACCTGGCTGAAGCATGTCACCAGAGTAACAAACAGTAGCTTCGACAACACCACCGGCCTTCTTGACAGCATTCACACCAACTTTTAATTGTTCTAAATCATTCAAGGCATCAAAAACTCTAAATATATCAACACCATTATCCTTGGCTTGCTTGACAAAATGGTCAATAGCATTGTCAGGTAATGAAGAGTAAGCCACACCGTTGGCACCACGTAATAACATTTGGAATGGAATATTAGGCACCAGAGATCTTAATTTTCTCAGACGTTCCCATGGATCCTCATGCAAGAATCTCATTGCAACGTCGAATGTAGCACCACCCCAACATTCTAAAGCGAAAGCACCTGCAAGGGCATGTGCGGTTGTTGGAGCGAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-34.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-34.txt
new file mode 100644
index 0000000..44126c6
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-34.txt
@@ -0,0 +1 @@
+TTAGGCGATACGTTTCAGATATTAGTGTACATACGTTGCAGACATATTATGAGATGCTGGAGGTAGATGTTATGTTTCTAATTGTTGGAATAGGGTTACGTCGGCGGTTTCGGTTGAAGTGGGCACATTGATCGACACTTTATATACGCAGGGACGATTAACGGGAGAGGAGGTAGCGGCGGCGTCATTGGCCGCTGGCCCGGGGTAGTAGGGCAGGGAAAGGGGACGGTGAAAAAATAAAACGTAAGTGGGACAGCAGAGATTTGAGGAGTTGAGGCAAGAACAGAGGAGAGAACGAGAAAGCGGATAAGCAACAAAGGAAAAGGAAACAAGTAAAAAAAAAAAAAGAGACTAGCATGAGAAACGAATTATACCAGTTATGGTGTGTGGCGAGTGCAGCAAGAGGGGTAGCCAAGAGCTCATTCGTAAGGGCCAATTCGGCCATGTGTGAGTACGTGCGCACGTCCAACGTCTTGAGTCGCTGGACTCGGG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-35.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-35.txt
new file mode 100644
index 0000000..76a8672
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-35.txt
@@ -0,0 +1 @@
+ATGATAAAAATAGCTCAAGAGTATCCTGACTTTATATAACTAGCAATTTTATCTAAGAGAGTAATGCCGTTGCACCGGCTCTTGTTTAGTTTTTTTATGTGGTTACTCTTTTACAATATACTAAAATTGCAACTCATTTGCTTTTTTCATTTCTTTTCCTTCAATTTTTTTTTTTTTTTAGTCTACGGCTAACAAAAAGCATAGGCTCAGAATGATAGATAGATATCCGATAAATGTCAGTAAAGACAAAGAAATCAGTAAAATAAACGTTTCACCCTGGCAGATAGGAAACCCTATCTCCCAGTGATAAACTTTAATTTTTTTTCGTGCATATATATATATACATATATAATATAAAACAAAATATCATGAATTTCTATCTAAAGTAGTGGGAAAAAATATGTCGTTAAAAATGGGTATAATATCCAAGCAAAGCTTTTGGCTTTTTTTTTCTTGAAATGAGTGAAGGGAAGGCTCAATAAGCCTAGTCCT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-36.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-36.txt
new file mode 100644
index 0000000..50960de
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-36.txt
@@ -0,0 +1 @@
+GTGAGATCGTAGTATCGGTCATTGGAAACTCCAAGTTGATTGAAATTGGTATGCCTTTCCAAATGATATTATCAATTACCAAAACAGATTCTTCCATTGAATTACAGGAAGCTTCTTTGGCAGTTGCACAGAGAATGGCAATACCAAGTATTGATTTGAAGACCAAGAAAATACTACGAGAGCCATACATTAAAAAGTCCGAGTATTTACTAAGAACTGTAGAAAGCCAAAGTTTCGATTCCGACAAGACCATTTTCGGATTTTGCTTTGATGACGTTGTCATCCCAACCTATGCGGATGGTCTACCGAGCTGGTTTAAAACATTCTACTGTGAACCAAGTTCATTTTATCCCAATCATGCTGCCCTCAAAGTAACTCATTTGCTTTTGTTCAGAATAACCTATAGCAGGAATGAATTGGTGGAAGGGCTTGAAATGAAAAAAAATTATCGTATTACGGTGAATTTTCCAATTTTAGTTGGCGATAGTGATA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-37.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-37.txt
new file mode 100644
index 0000000..8a56e23
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-37.txt
@@ -0,0 +1 @@
+GCAGCTGCTGCCAGTACTAGTAAGTCTGCTGAAAAGGAGAAACCTGTTACCAAAAAGGAGTTGAAAAGAAGGGAAAAGCAAGCATTGCTCGAAAAGAAAAAGAAACTGTTGGAAATTGCCAGGGCTAATATGCTTGAAAACATGCAAAAGAGCCAAGAGGGAAATACTCCCGACCTAAGCAAGCTCTCTTTGCAAGAAAATGAGGAGAACAAGGAAAAGGAAGAACCTAAGAAGGAGGAGCCTGAACAGTTGACCGAGGAAGAAATGGCGGAAAGAGTAATGCAAGAAAACGTACGCAACAGAGTCGATATTCCACTGGAACAATGTCTATTTTGTGAGCACAATAAGCACTTCAAAGATGTTGAAGAAAACCTGGAACACATGTTTAGGACCCACGGGTTTTATATCCCAGAACAGAAATATCTAGTCGACAAGATCGGCTTGGTAAAATACATGTCGGAGAAGATTGGTCTTGGGAACATTTGTATTGTT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-38.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-38.txt
new file mode 100644
index 0000000..1135578
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-38.txt
@@ -0,0 +1 @@
+CAGTTCACCAGGTGTGATTGAGGGCACTTCCAAAACCATGGTTGTTGCTCTAGTTTTCTATACCCGCCCCTTCTACCTCAATGACAATCTCTCTTTTATGGTTGGTGTTTCTTTTCACTTTTTTACTTCTTCTTGTACTTTTCTTTTTGAACCTTAAAAACTCTTGCAATTATGTTTCCATGATGCTTTTGAAGTAATAATTTATTTTTATTACGCGAAATTAATGGCCTTAACAACTGCCTGCCCCAACAGATAAAAACAAGCAAGGGTCAACCGTGTTGCAAAAAAAATGTCCAACTTAGTTAAAGAAAAAGCACCTGTCTTTCCTATATCTAAAGTAAAGAAGATTGCCAAATGCGACCCCGAATACGTAATTACATCTAATGTAGCTATATCAGCGACCGCATTCGCTGCTGAGTTATTTGTACAGAATCTCGTCGAAGAATCGCTGGTCTTAGCACAACTGAATTCGAAAGGAAAGACAAGCCTACG [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-39.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-39.txt
new file mode 100644
index 0000000..bbe5ace
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-39.txt
@@ -0,0 +1 @@
+TCAAGTGCCCTTAGCTCCAGCGCCTATTAATTTGCCTCCACAAATTGCTCAGTTACCTTTGGCTACACAGCAACAAGTTTTGAACAAGTTGAGGCAGCAGGCCATAGCAAAAAATAATCCACAGGTTGTGAATGCAATTACTGTTGCACAACAACAAGTGCAACGCCAAATTGAGCAGCAAAAGGGACAGCAAACGGCACAAACTCAGCTAGAACAGCAGAGGCAATTGCTGGTTCAGCAGCAACAGCAGCAGCAACTTAGAAACCAAATACAGCGACAACAGCAACAACAGTTTAGGCATCATGTGCAAATACAACAGCAGCAACAAAAGCAACAACAACAGCAGCAGCAGCATCAGCAACAACAACAACAACAACAGCAACAGCAGCAACAGCAACAGCAACAGCAGCAGCAACAACAACAGCAACAACAACAACAACAGCAGCAGCAGCAGCAGCAGCAGCAAGGACAAATACCGCAATCTCAGCAAGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-4.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-4.txt
new file mode 100644
index 0000000..67b9114
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-4.txt
@@ -0,0 +1 @@
+TGGTTAGCAAGAATACATTTTGGAGAAGAAAGACATTAACTCCACCTTACTGTATCATCTTATTTGCTTTTTCACTCCTTCCTAATATTTTTTTTATTTTATTTTGAATTTCTTCCTATTTCTGATGCATTGAACAGATCGTAATCTGTAAGTAAATACCATAACGTGCTCACATTTGTCTCCAAATACTGGTGAAATCTTGGCTCCGTTGTGTACAAAACTTCTTAATGAATATATATATATTTTTCCCTTATTTTATCTTTTTTTTTCGAATTTTTTATGTAAACATTCTTATACTGGAACAATAGATGGCTAATGAGTCCCTATAATTTCGATTTTAGATGTTAACGCTTCATTTCTTTTCATATAAAAGACTACCTGCCAAATGTATTTTCTCCTGAGTAAGTGACATACAAAAACCCGTCCTTATCCTTGTGTTCTTGATATATGGCAGACATCAACGCCGCAGTAGGTGGCAAAGTATCATTGACA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-40.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-40.txt
new file mode 100644
index 0000000..693562a
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-40.txt
@@ -0,0 +1 @@
+GCTCAGTTGTCAAGATTTAGTCATTAAGAAGGGCCGCAGCAGCTTTTTGTATAATAGAGCGTCTTTTTTGTTTGTGAAAAAAATTTTATGGTGAGATATTGTTCGATTCTACGAAGTCATTTTACTAGTTTATGGACTCTGATATAAGACAGAGTTGACAAGGAAATGGTGCCGTGATTGTTTCCGTGTACAGCTTTTGAGAACTTCCTTGAAAACCAATCATCTAGCACTTTCATTTCTGGGGAAAAACCTGGAACCAAATCTTGAAAAATAAATTCCCCAGAAGTTTTCCTTATTCCGTGTTCTAATCTTCTCGTTCACTTTGCAGTGACATTCCACGGCCATGCGCAATTTACCCCGCCCCCGGATTTTATTGTCCGTACCGCCATTTTTCAATAGATTAAAAAGGAACAAAAAATCATTTCAGAAGGTTTCTTTCTCGGGAAAACACTAGAGTGTAAATATTGAATATCAAACATCGAACGAGAGCAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-5.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-5.txt
new file mode 100644
index 0000000..bbacc59
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-5.txt
@@ -0,0 +1 @@
+TCGTTGTTTCTTACTTGGGAACTGGTGCTGTTGCAACTGTTGCTGCTCCGGTTGTCTTTTCATGTGAGCAGAACTAAAATCTTGGTCTTGGTCAGAATCTCGATTATCCTTCACCATGACGAGAATGCGTATACGCAGAGCCAATATAATCTGTATACGGGACCTTTGACATTAAGGTTTCTGCAGAGAGTTTATTACATGCATTTTCATATATATATTTTTAACGCCATTCCTTTACGGTATGTAAAGAAAAATGATCCAATGAGCGGCCCTTCGTACGAGATGAGATATAATAAGAATGAAAGATAGATAAAAGTTTTAGTTAGAGATACTTCATATACCTGTATATAGTAAAGTCGTTTATTTCAAAGCTTATTTCGACTTGGTGAATCTTAAATAGGGTTTAATTTCATTAAATTGACCAAATTTAGCCTTCGCCTCATCATTGGAGACAGAGGGAGGAATAATGACATCGTCAGCTGGCTGCCAATT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-6.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-6.txt
new file mode 100644
index 0000000..19fc036
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-6.txt
@@ -0,0 +1 @@
+CTGCGTTTATTATGTGCTCTACTGTTGTTATAGTTTTCTATATGTGTGTTATTCGAAATTACCGGAGCCATAGTTTCATTTGTAAGAGCTTCGTCCGCTAATGCGGTGGGTTCTAAGTTCATCATCCCATTTATACCAAATGACGGCCCTAATATAATAGGTTCCCACAATAGTTTGTCATATTTGACGCGAGGATAGTTTTTTGAGCTCCAAGTTTTGTAAATATTTTCAATCCTGTTCCAAGAATCGATTTTAATAATATAGTTGAAATCATCCAAACTGGATAATGAGCGTGTTTTATGGCGATACAAAACTTGAAGTTGTTCCAATCCAAACACAACGTCTGTTGGTATCATTCCTGTTAGTTTAGCGATATCGTTTAGGCTAACCTGCTGAAAAGTATCTTCATTTTTATTATTTGATCGACGTCTAGCACTGTCTCTTAATTTTAATAGCACTTCAGCACATTTTATCTTCCAAAACGTTCTGTAA [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-7.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-7.txt
new file mode 100644
index 0000000..8ea75fb
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-7.txt
@@ -0,0 +1 @@
+AAATCGGTAGAGTCATTCTTAACCTCAAAAAACTCGACAACTCTTTGCAGCCAAGACACCTTTTCGTCATCCTGAGCAATGGTTATATTGTTTTCTGAATCGGTATCTTTTTTTTCAATGACATTAGCATCAATCTTAGAAGTAATTTTAGTCGCATCACCCATCTCGGACTCTTTCGTGATTTCTTCGTAATCCTCCGTCGGTATAGTGTCGTCCTTCATTGTTTTTGAAGAATTATCGAATCCAGAATCAGATACCGGCATTTTCTCTTATATAGAATTAATGAATGAGAGGACTAGTTACCGCCTTTTATTTCCGCAATGTGCTGCAAAGTAAAGCAAAGCTGTCCAATACTATGTTTTAAATTTTACAAGAAGGTCGAAGCAATACTACTGTTATATAAAAGATAGGATAGCATGTCATCGAGTAATTGAGCAAGAGAGGAAATGAAAAACTTTTCCGTTGGAGAATTGATCAAAAATTTTCAAAAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-8.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-8.txt
new file mode 100644
index 0000000..8d4d681
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-8.txt
@@ -0,0 +1 @@
+TGTGTCCGGCCGCTCAGCCAAACAACAAACAAGACTATAATCTGGAGCATAAGTACCCGAGCATATATAACTACTTGGCATACGAAGTAAAAGCATATTGCTCCCCACTCCTATCGCGCGTCCCCTGTACAACAAAGCATCAACTTATCGGGTAACTTAGAGACAGCATTAGTATATATACCAGCCATGTCACAGTTCTTCGAAGCTGCTACTCCCGTTGCAATTCCCACAAACAATACCAACGGCGGCTCCAGTGATGCCGGCAGCGCCGCCACTGGCGGCGCCCCCGTTGTTGGCACCACCGCTCAACCCACCATCAATCACAGGCTTTTGCTGTCATTGAAAGAGGCTGCCAAGATCATTGGCACTAAGGGCTCCACCATCTCACGCATAAGAGCTGCAAACGCCGTCAAGATCGGTATTTCTGAAAAGGTGCCCGGTTGCTCTGACAGGATCCTGTCCTGTGCTGGGAACGTAATCAATGTGGCCAAT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-9.txt b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-9.txt
new file mode 100644
index 0000000..3697dba
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/9fe/c94/2a/chrII-9.txt
@@ -0,0 +1 @@
+TGAAGCAGAAAAAGTTGCAATTTGTTCTCTGAACATCGTGTTCAATTGAGAAATTTCTTTAAATACCTTCAATATTTCTGATGTTAGCGCATTAATAACGGGTGACTTTCTATCAAACGGTTCATCTTCTAAATTCAACACATTGACAAGGGAAACATTATAATTTTTTAAGAATTCTGTGGGGTTAATGTCCTCACCTTCTTGAATATCGTCTAATTCTTCAGACTCTTCTTCAGTGGCTTCTACTCTCTTATGATGTTGCAATTCTGAGTCTGGTATCCTCTCCACCACAATATCCTCCAACTTAGGCGTAGCAGGGCTGGTGCTTTCTTGGTCTTCTGGGTTGTTAGCGTCTTCAACGACGGTCGTTTCAGTATCCGTATCCTTTGCTTGTTCTTTTGATTTTTCCTTTTCCTCGTTTGGTGGAAATAATTCATCAATTTTTATTCTCCTGTGAGGATATAAAAGAGCAGTCATTGTTTCAGTGCCCGT [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/seq/refSeqs.json b/test/integration/resources/sequences/yeast/seq/refSeqs.json
new file mode 100644
index 0000000..f8a3de9
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/seq/refSeqs.json
@@ -0,0 +1 @@
+[{"end":230208,"name":"chrI","start":0,"seqChunkSize":20000,"length":230208},{"length":813178,"end":813178,"start":0,"name":"chrII","seqChunkSize":20000}]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/trackList.json b/test/integration/resources/sequences/yeast/trackList.json
new file mode 100644
index 0000000..9616054
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/trackList.json
@@ -0,0 +1,71 @@
+{
+   "dataset_id" : "yeast",
+   "plugins" : [
+      "NeatHTMLFeatures",
+      "NeatCanvasFeatures",
+      "HideTrackLabels"
+   ],
+   "tracks" : [
+      {
+         "category" : "Reference sequence",
+         "seqType" : "dna",
+         "chunkSize" : 20000,
+         "label" : "DNA",
+         "urlTemplate" : "seq/{refseq_dirpath}/{refseq}-",
+         "key" : "Reference sequence",
+         "type" : "SequenceTrack",
+         "storeClass" : "JBrowse/Store/Sequence/StaticChunked"
+      },
+      {
+         "key" : "Protein-coding genes",
+         "autocomplete" : "all",
+         "style" : {
+            "arrowheadClass" : "transcript-arrowhead",
+            "subfeatureClasses" : {
+               "CDS" : "transcript-CDS"
+            },
+            "className" : "feature5"
+         },
+         "track" : "Genes",
+         "compress" : 0,
+         "storeClass" : "JBrowse/Store/SeqFeature/NCList",
+         "type" : "FeatureTrack",
+         "subfeatures" : true,
+         "feature" : [
+            "gene"
+         ],
+         "category" : "Genes",
+         "urlTemplate" : "tracks/Genes/{refseq}/trackData.json",
+         "label" : "Genes"
+      },
+      {
+         "storeClass" : "JBrowse/Store/SeqFeature/NCList",
+         "compress" : 0,
+         "description" : 1,
+         "type" : "FeatureTrack",
+         "key" : "Exonerate predictions (misconfigured for test, and with a long description)",
+         "autocomplete" : "all",
+         "style" : {
+            "className" : "transcript",
+            "subfeatureClasses" : {
+               "CDS" : "transcript-CDS",
+               "UTR" : "transcript-UTR"
+            },
+            "arrowheadClass" : "transcript-arrowhead"
+         },
+         "track" : "transcript_with_no_features",
+         "feature" : [
+            "mRNA"
+         ],
+         "category" : "Genes",
+         "urlTemplate" : "tracks/transcript_with_no_features/{refseq}/trackData.json",
+         "label" : "transcript_with_no_features",
+         "subfeatures" : true
+      }
+   ],
+   "names" : {
+      "type" : "Hash",
+      "url" : "names/"
+   },
+   "formatVersion" : 1
+}
diff --git a/test/integration/resources/sequences/yeast/tracks.conf b/test/integration/resources/sequences/yeast/tracks.conf
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrI/hist-5000-0.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/hist-5000-0.json
new file mode 100644
index 0000000..803ae56
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/hist-5000-0.json
@@ -0,0 +1 @@
+[4,1,4,0,3,1,2,6,3,3,5,6,4,3,4,4,6,3,3,3,2,3,5,2,4,5,4,5,4,2,3,3,0,2,4,4,2,5,3,1,2,2,0,1,5,3]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrI/names.txt b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/names.txt
new file mode 100644
index 0000000..165027c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/names.txt
@@ -0,0 +1,117 @@
+[["YAL069W"],"Genes","YAL069W","chrI",334,649]
+[["YAL068W-A"],"Genes","YAL068W-A","chrI",537,792]
+[["YAL068C","PAU8"],"Genes","YAL068C","chrI",1806,2169]
+[["YAL067W-A"],"Genes","YAL067W-A","chrI",2479,2707]
+[["YAL067C","SEO1"],"Genes","YAL067C","chrI",7235,9017]
+[["YAL066W"],"Genes","YAL066W","chrI",10091,10400]
+[["YAL065C"],"Genes","YAL065C","chrI",11565,11952]
+[["YAL064W-B"],"Genes","YAL064W-B","chrI",12046,12427]
+[["YAL064C-A","YAL065C-A"],"Genes","YAL064C-A","chrI",13363,13744]
+[["YAL064W"],"Genes","YAL064W","chrI",21525,21852]
+[["YAL063C-A"],"Genes","YAL063C-A","chrI",22396,22687]
+[["YAL063C","FLO9"],"Genes","YAL063C","chrI",24000,27969]
+[["YAL062W",["GDH3","FUN51"]],"Genes","YAL062W","chrI",31567,32941]
+[["YAL061W","BDH2"],"Genes","YAL061W","chrI",33448,34702]
+[["YAL060W","BDH1"],"Genes","YAL060W","chrI",35155,36304]
+[["YAL059C-A"],"Genes","YAL059C-A","chrI",36496,36919]
+[["YAL059W","ECM1"],"Genes","YAL059W","chrI",36509,37148]
+[["YAL058W",["CNE1","FUN48"]],"Genes","YAL058W","chrI",37464,38973]
+[["YAL056C-A","YAL058C-A"],"Genes","YAL056C-A","chrI",38696,39047]
+[["YAL056W",["GPB2","KRH1"]],"Genes","YAL056W","chrI",39259,41902]
+[["YAL055W",["PEX22","YAF5"]],"Genes","YAL055W","chrI",42177,42720]
+[["YAL054C",["ACS1","FUN44"]],"Genes","YAL054C","chrI",42881,45023]
+[["YAL053W",["FLC2","HUF2"]],"Genes","YAL053W","chrI",45899,48251]
+[["YAL051W",["OAF1","YAF1"]],"Genes","YAL051W","chrI",48564,51708]
+[["YAL049C","AIM2"],"Genes","YAL049C","chrI",51855,52596]
+[["YAL048C",["GEM1","GON1"]],"Genes","YAL048C","chrI",52801,54790]
+[["YAL047W-A"],"Genes","YAL047W-A","chrI",54584,54914]
+[["YAL047C",["SPC72","LDB4"]],"Genes","YAL047C","chrI",54989,56858]
+[["YAL046C","AIM1"],"Genes","YAL046C","chrI",57029,57386]
+[["YAL045C"],"Genes","YAL045C","chrI",57488,57797]
+[["YAL044W-A"],"Genes","YAL044W-A","chrI",57518,57851]
+[["YAL044C","GCV3"],"Genes","YAL044C","chrI",57950,58463]
+[["YAL043C",["PTA1","FUN39"]],"Genes","YAL043C","chrI",58695,61053]
+[["YAL042C-A","YAL043C-A"],"Genes","YAL042C-A","chrI",61231,61609]
+[["YAL042W",["ERV46","FUN9"]],"Genes","YAL042W","chrI",61316,62564]
+[["YAL041W",["CDC24","CLS4"]],"Genes","YAL041W","chrI",62840,65405]
+[["YAL040C",["CLN3","WHI1","FUN10","DAF1"]],"Genes","YAL040C","chrI",65778,67521]
+[["YAL039C","CYC3"],"Genes","YAL039C","chrI",68716,69526]
+[["YAL038W",["CDC19","PYK1"]],"Genes","YAL038W","chrI",71786,73289]
+[["YAL037C-B"],"Genes","YAL037C-B","chrI",72326,73301]
+[["YAL037C-A"],"Genes","YAL037C-A","chrI",73426,73519]
+[["YAL037W"],"Genes","YAL037W","chrI",74020,74824]
+[["YAL036C",["RBG1","FUN11"]],"Genes","YAL036C","chrI",75043,76153]
+[["YAL035W",["FUN12","yIF2"]],"Genes","YAL035W","chrI",76427,79436]
+[["YAL034C-B","YAL035C-A"],"Genes","YAL034C-B","chrI",79489,79843]
+[["YAL034W-A",["MTW1","NSL2","DSN3"]],"Genes","YAL034W-A","chrI",79718,80588]
+[["YAL034C","FUN19"],"Genes","YAL034C","chrI",80710,81952]
+[["YAL033W",["POP5","FUN53"]],"Genes","YAL033W","chrI",82706,83228]
+[["YAL032C",["PRP45","FUN20"]],"Genes","YAL032C","chrI",83335,84475]
+[["YAL031W-A"],"Genes","YAL031W-A","chrI",84669,84978]
+[["YAL031C",["GIP4","FUN21"]],"Genes","YAL031C","chrI",84749,87032]
+[["YAL030W","SNC1"],"Genes","YAL030W","chrI",87286,87753]
+[["YAL029C",["MYO4","SHE1","FUN22"]],"Genes","YAL029C","chrI",87855,92271]
+[["YAL028W",["FRT2","HPH2"]],"Genes","YAL028W","chrI",92900,94487]
+[["YAL027W","SAW1"],"Genes","YAL027W","chrI",94687,95473]
+[["YAL026C-A"],"Genes","YAL026C-A","chrI",95386,95824]
+[["YAL026C",["DRS2","SWA3","FUN38"]],"Genes","YAL026C","chrI",95630,99698]
+[["YAL025C","MAK16"],"Genes","YAL025C","chrI",100225,101146]
+[["YAL024C",["LTE1","MSI2"]],"Genes","YAL024C","chrI",101565,105873]
+[["YAL023C",["PMT2","FUN25"]],"Genes","YAL023C","chrI",106272,108552]
+[["YAL022C","FUN26"],"Genes","YAL022C","chrI",108877,110431]
+[["YAL021C",["CCR4","NUT21","FUN27"]],"Genes","YAL021C","chrI",110846,113360]
+[["YAL020C",["ATS1","KTI13","FUN28"]],"Genes","YAL020C","chrI",113614,114616]
+[["YAL019W-A"],"Genes","YAL019W-A","chrI",114250,114820]
+[["YAL019W","FUN30"],"Genes","YAL019W","chrI",114919,118315]
+[["YAL018C"],"Genes","YAL018C","chrI",118564,119542]
+[["YAL017W",["PSK1","FUN31"]],"Genes","YAL017W","chrI",120225,124296]
+[["YAL016C-B"],"Genes","YAL016C-B","chrI",124307,124493]
+[["YAL016C-A"],"Genes","YAL016C-A","chrI",124755,125070]
+[["YAL016W",["TPD3","FUN32"]],"Genes","YAL016W","chrI",124879,126787]
+[["YAL015C",["NTG1","SCR1","FUN33"]],"Genes","YAL015C","chrI",126903,128103]
+[["YAL014C",["SYN8","SLT2","UIP2"]],"Genes","YAL014C","chrI",128252,129020]
+[["YAL013W",["DEP1","FUN54"]],"Genes","YAL013W","chrI",129270,130533]
+[["YAL012W",["CYS3","STR1","FUN35","CYI1"]],"Genes","YAL012W","chrI",130801,131986]
+[["YAL011W",["SWC3","SWC1"]],"Genes","YAL011W","chrI",132201,134079]
+[["YAL010C",["MDM10","FUN37"]],"Genes","YAL010C","chrI",134185,135667]
+[["YAL009W","SPO7"],"Genes","YAL009W","chrI",135855,136635]
+[["YAL008W","FUN14"],"Genes","YAL008W","chrI",136915,137512]
+[["YAL007C","ERP2"],"Genes","YAL007C","chrI",137699,138347]
+[["YAL005C",["SSA1","YG100"]],"Genes","YAL005C","chrI",139504,141433]
+[["YAL004W"],"Genes","YAL004W","chrI",140761,141409]
+[["YAL003W",["EFB1","TEF5"]],"Genes","YAL003W","chrI",142175,143162]
+[["YAL002W",["VPS8","VPT8","FUN15"]],"Genes","YAL002W","chrI",143708,147533]
+[["YAL001C",["TFC3","TSV115","FUN24"]],"Genes","YAL001C","chrI",147595,151168]
+[["YAR002W","NUP60"],"Genes","YAR002W","chrI",152258,153878]
+[["YAR002C-A","ERP1"],"Genes","YAR002C-A","chrI",154066,154726]
+[["YAR003W",["SWD1","SAF49","CPS50","FUN16"]],"Genes","YAR003W","chrI",155006,156287]
+[["YAR007C",["RFA1","RPA1","FUN3","BUF2"]],"Genes","YAR007C","chrI",156755,158621]
+[["YAR008W",["SEN34","FUN4"]],"Genes","YAR008W","chrI",158966,159794]
+[["YAR014C","BUD14"],"Genes","YAR014C","chrI",166742,168866]
+[["YAR015W","ADE1"],"Genes","YAR015W","chrI",169369,170290]
+[["YAR018C",["KIN3","NPK1","FUN52"]],"Genes","YAR018C","chrI",170390,171698]
+[["YAR019C",["CDC15","LYT1"]],"Genes","YAR019C","chrI",172208,175133]
+[["YAR019W-A"],"Genes","YAR019W-A","chrI",174995,175340]
+[["YAR020C","PAU7"],"Genes","YAR020C","chrI",176853,177021]
+[["YAR023C"],"Genes","YAR023C","chrI",179278,179818]
+[["YAR027W","UIP3"],"Genes","YAR027W","chrI",183763,184471]
+[["YAR028W"],"Genes","YAR028W","chrI",184885,185590]
+[["YAR029W"],"Genes","YAR029W","chrI",186314,186539]
+[["YAR030C"],"Genes","YAR030C","chrI",186505,186847]
+[["YAR031W","PRM9"],"Genes","YAR031W","chrI",186829,187726]
+[["YAR033W","MST28"],"Genes","YAR033W","chrI",188100,188805]
+[["YAR035W","YAT1"],"Genes","YAR035W","chrI",190186,192250]
+[["YAR035C-A"],"Genes","YAR035C-A","chrI",192330,192411]
+[["YAR042W",["SWH1","OSH1","YAR044W"]],"Genes","YAR042W","chrI",192612,196179]
+[["YAR047C"],"Genes","YAR047C","chrI",201459,201780]
+[["YAR050W",["FLO1","FLO4","FLO2"]],"Genes","YAR050W","chrI",203393,208007]
+[["YAR053W"],"Genes","YAR053W","chrI",208357,208654]
+[["YAR060C"],"Genes","YAR060C","chrI",217147,217483]
+[["YAR064W"],"Genes","YAR064W","chrI",220188,220488]
+[["YAR066W"],"Genes","YAR066W","chrI",221039,221651]
+[["YAR068W"],"Genes","YAR068W","chrI",222396,222882]
+[["YAR069C"],"Genes","YAR069C","chrI",224001,224295]
+[["YAR070C"],"Genes","YAR070C","chrI",224553,224853]
+[["YAR071W","PHO11"],"Genes","YAR071W","chrI",225450,226854]
+[["YAR073W","IMD1"],"Genes","YAR073W","chrI",227732,228944]
+[["YAR075W"],"Genes","YAR075W","chrI",228834,229308]
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrI/trackData.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/trackData.json
new file mode 100644
index 0000000..88d653c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrI/trackData.json
@@ -0,0 +1 @@
+{"histograms":{"meta":[{"arrayParams":{"chunkSize":10000,"length":46,"urlTemplate":"hist-5000-{Chunk}.json"},"basesPerBin":"5000"}],"stats":[{"basesPerBin":"5000","mean":3.1304347826087,"max":6}]},"intervals":{"classes":[{"isArrayAttr":{"Ontology_term":1,"Subfeatures":1},"attributes":["Start","End","Strand","Name","Source","Subfeatures","Ontology_term","Type","Load_id","Dbxref","Seq_id","Orf_classification","Note"]},{"attributes":["Start","End","Strand","Seq_id","Type","Phase","Name","Pa [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-10000-0.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-10000-0.json
new file mode 100644
index 0000000..7cecfec
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-10000-0.json
@@ -0,0 +1 @@
+[9,4,5,3,11,3,6,5,9,9,7,9,6,6,8,5,7,5,6,4,8,5,2,5,7,6,3,5,5,7,6,6,5,9,4,6,7,5,5,6,4,5,9,5,8,8,7,5,9,10,5,5,4,8,7,5,8,7,9,7,9,8,7,6,5,6,8,8,8,5,6,6,10,10,9,3,8,7,6,3,7,1]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-5000-0.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-5000-0.json
new file mode 100644
index 0000000..89bdfde
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/hist-5000-0.json
@@ -0,0 +1 @@
+[4,6,3,2,4,2,0,3,6,5,2,2,3,4,4,1,4,6,7,3,4,4,5,5,2,4,3,4,6,4,4,2,4,3,4,2,4,3,3,2,4,5,2,4,1,1,1,5,4,4,2,4,0,3,3,3,4,2,5,3,4,3,3,3,4,2,5,5,3,2,4,3,3,5,3,3,4,2,4,2,2,3,3,3,3,7,3,3,4,4,5,4,4,4,3,3,6,4,6,5,4,2,3,3,1,4,4,4,2,5,3,2,3,5,4,4,6,4,3,4,4,6,5,3,5,2,4,2,2,3,3,4,3,5,5,4,6,3,3,2,3,4,4,3,6,5,4,7,6,4,2,2,6,3,4,4,4,3,2,2,3,4,1]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-1.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-1.json
new file mode 100644
index 0000000..52719d0
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-1.json
@@ -0,0 +1 @@
+[[96,279,2658,-1,"gene",["GO:0008150","GO:0005575","GO:0004386"],"YBL113C",[[217,279,2658,-1,"chrII","0","CDS","SGD","YBL113C","YBL113C"]],"YBL113C","SGD","Helicase-like protein encoded within the telomeric Y' element","Uncharacterized","SGD:S000002153","chrII",{"Sublist":[[98,645,1128,1,"Dubious","Dubious open reading frame unlikely to encode a protein; identified by gene-trapping, microarray-based expression analysis, and genome-wide homology searching","SGD:S000028599","chrII",[[11,64 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-2.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-2.json
new file mode 100644
index 0000000..9689d4c
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-2.json
@@ -0,0 +1 @@
+[[188,236494,236890,1,"HTB2","SGD","YBL002W","YBL002W","gene",["GO:0006333","GO:0003677","GO:0000788"],[[176,236494,236890,1,"CDS","0","YBL002W","YBL002W","SGD","chrII","HTB2"]],"SGD:S000000098","chrII","HTB2","One of two nearly identical (see HTB1) histone H2B subtypes required for chromatin assembly and chromosome function; Rad6p-Bre1p-Lge1p mediated ubiquitination regulates transcriptional activation, meiotic DSB formation and H3 methylation","Verified"],[120,237154,237469,-1,[[119,23 [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-3.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-3.json
new file mode 100644
index 0000000..3841157
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-3.json
@@ -0,0 +1 @@
+[[213,458865,460215,1,"Verified","Mannosyltransferase, involved in asparagine-linked glycosylation in the endoplasmic reticulum (ER); essential for viability, mutation is functionally complemented by human ortholog","ALG1","SGD:S000000314","chrII",[[214,458865,460215,1,"chrII","ALG1","SGD","YBR110W","YBR110W","0","CDS"]],"YBR110W",["GO:0016021","GO:0019187","GO:0006487","GO:0005783","GO:0006490"],"gene","SGD","YBR110W","ALG1"],[53,461171,461867,-1,"chrII","YSA1","SGD:S000000315","Verifie [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-4.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-4.json
new file mode 100644
index 0000000..c636cbc
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/lf-4.json
@@ -0,0 +1 @@
+[[140,653350,655312,1,"YBR215W",["GO:0005634","GO:0006368","GO:0000083","GO:0006336","GO:0000417","GO:0031491","GO:0003677"],"gene",[[169,653447,655312,1,"chrII","HPC2","YBR215W","YBR215W","SGD","2","CDS"],[90,653350,653363,1,"CDS","0","YBR215W","YBR215W","SGD","HPC2","chrII"]],"HPC2","SGD","YBR215W","Subunit of the HIR complex, a nucleosome assembly complex involved in regulation of histone gene transcription; mutants display synthetic defects with subunits of FACT, a complex that allow [...]
\ No newline at end of file
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/names.txt b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/names.txt
new file mode 100644
index 0000000..021d655
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/names.txt
@@ -0,0 +1,456 @@
+[["YBL113C"],"Genes","YBL113C","chrII",279,2658]
+[["YBL113W-A"],"Genes","YBL113W-A","chrII",645,1128]
+[["YBL112C"],"Genes","YBL112C","chrII",2581,2899]
+[["YBL111C"],"Genes","YBL111C","chrII",2906,5009]
+[["YBL109W"],"Genes","YBL109W","chrII",5789,6125]
+[["YBL108C-A","PAU9"],"Genes","YBL108C-A","chrII",7604,7733]
+[["YBL108W"],"Genes","YBL108W","chrII",8176,8482]
+[["YBL107W-A"],"Genes","YBL107W-A","chrII",9267,9372]
+[["YBL107C"],"Genes","YBL107C","chrII",9960,10551]
+[["YBL106C",["SRO77","SNI2","SOP2"]],"Genes","YBL106C","chrII",10846,13879]
+[["YBL105C",["PKC1","STT1","HPO2","CLY15"]],"Genes","YBL105C","chrII",14240,17696]
+[["YBL104C","YBL103C-A"],"Genes","YBL104C","chrII",18176,21293]
+[["YBL103C","RTG3"],"Genes","YBL103C","chrII",22074,23535]
+[["YBL102W","SFT2"],"Genes","YBL102W","chrII",24097,24745]
+[["YBL101C","ECM21"],"Genes","YBL101C","chrII",24945,28299]
+[["YBL100W-C","YBL101W-C"],"Genes","YBL100W-C","chrII",28426,28546]
+[["YBL100C"],"Genes","YBL100C","chrII",36985,37300]
+[["YBL099W","ATP1"],"Genes","YBL099W","chrII",37049,38687]
+[["YBL098W","BNA4"],"Genes","YBL098W","chrII",39141,40524]
+[["YBL097W","BRN1"],"Genes","YBL097W","chrII",40827,43092]
+[["YBL096C"],"Genes","YBL096C","chrII",43170,43479]
+[["YBL095W"],"Genes","YBL095W","chrII",43273,44086]
+[["YBL094C"],"Genes","YBL094C","chrII",43761,44094]
+[["YBL093C",["ROX3","MED19","SSN7","NUT3"]],"Genes","YBL093C","chrII",44252,44915]
+[["YBL092W","RPL32"],"Genes","YBL092W","chrII",45641,46367]
+[["YBL091C-A","SCS22"],"Genes","YBL091C-A","chrII",46561,47177]
+[["YBL091C","MAP2"],"Genes","YBL091C","chrII",47359,48625]
+[["YBL090W",["MRP21","MRP50"]],"Genes","YBL090W","chrII",48821,49355]
+[["YBL089W","AVT5"],"Genes","YBL089W","chrII",49570,50950]
+[["YBL088C","TEL1"],"Genes","YBL088C","chrII",51015,59379]
+[["YBL087C","RPL23A"],"Genes","YBL087C","chrII",59817,60735]
+[["YBL086C"],"Genes","YBL086C","chrII",61198,62599]
+[["YBL085W",["BOI1","BOB1","GIN7"]],"Genes","YBL085W","chrII",63872,66815]
+[["YBL084C",["CDC27","SNB1","APC3"]],"Genes","YBL084C","chrII",67165,69442]
+[["YBL083C"],"Genes","YBL083C","chrII",69709,70135]
+[["YBL082C",["ALG3","RHK1"]],"Genes","YBL082C","chrII",69747,71124]
+[["YBL081W"],"Genes","YBL081W","chrII",71862,72969]
+[["YBL080C","PET112"],"Genes","YBL080C","chrII",73066,74692]
+[["YBL079W",["NUP170","NLE3"]],"Genes","YBL079W","chrII",75255,79764]
+[["YBL078C",["ATG8","AUT7","CVT5","APG8"]],"Genes","YBL078C","chrII",80374,80728]
+[["YBL077W"],"Genes","YBL077W","chrII",80894,81326]
+[["YBL076C","ILS1"],"Genes","YBL076C","chrII",81040,84259]
+[["YBL075C",["SSA3","YG106"]],"Genes","YBL075C","chrII",84496,86446]
+[["YBL074C","AAR2"],"Genes","YBL074C","chrII",86719,87787]
+[["YBL073W"],"Genes","YBL073W","chrII",87643,87955]
+[["YBL072C","RPS8A"],"Genes","YBL072C","chrII",88520,89438]
+[["YBL071C-B"],"Genes","YBL071C-B","chrII",89455,89554]
+[["YBL071W-A",["KTI11","DPH3"]],"Genes","YBL071W-A","chrII",89975,90224]
+[["YBL071C"],"Genes","YBL071C","chrII",90220,90529]
+[["YBL070C"],"Genes","YBL070C","chrII",90602,90923]
+[["YBL069W","AST1"],"Genes","YBL069W","chrII",90738,92028]
+[["YBL068W-A"],"Genes","YBL068W-A","chrII",91791,92028]
+[["YBL068W","PRS4"],"Genes","YBL068W","chrII",92411,93395]
+[["YBL067C","UBP13"],"Genes","YBL067C","chrII",93640,95884]
+[["YBL066C","SEF1"],"Genes","YBL066C","chrII",96670,100117]
+[["YBL065W"],"Genes","YBL065W","chrII",99964,100309]
+[["YBL064C","PRX1"],"Genes","YBL064C","chrII",100372,101158]
+[["YBL063W",["KIP1","CIN9"]],"Genes","YBL063W","chrII",101887,105223]
+[["YBL062W"],"Genes","YBL062W","chrII",105309,105690]
+[["YBL061C",["SKT5","CSD4","CHS4","CAL2"]],"Genes","YBL061C","chrII",105317,107408]
+[["YBL060W","YEL1"],"Genes","YBL060W","chrII",107933,109997]
+[["YBL059C-A"],"Genes","YBL059C-A","chrII",110126,110541]
+[["YBL059W"],"Genes","YBL059W","chrII",110595,111246]
+[["YBL058W",["SHP1","UBX1"]],"Genes","YBL058W","chrII",111438,112710]
+[["YBL057C","PTH2"],"Genes","YBL057C","chrII",112802,113447]
+[["YBL056W","PTC3"],"Genes","YBL056W","chrII",113764,115171]
+[["YBL055C","Tat-D"],"Genes","YBL055C","chrII",115575,116832]
+[["YBL054W"],"Genes","YBL054W","chrII",117591,119169]
+[["YBL053W"],"Genes","YBL053W","chrII",119337,119712]
+[["YBL052C","SAS3"],"Genes","YBL052C","chrII",119381,121877]
+[["YBL051C",["PIN4","MDT1"]],"Genes","YBL051C","chrII",122755,124762]
+[["YBL050W",["SEC17","RNS3"]],"Genes","YBL050W","chrII",125127,126122]
+[["YBL049W","MOH1"],"Genes","YBL049W","chrII",126830,127247]
+[["YBL048W"],"Genes","YBL048W","chrII",127301,127613]
+[["YBL047C",["EDE1","BUD15"]],"Genes","YBL047C","chrII",127897,132043]
+[["YBL046W","PSY4"],"Genes","YBL046W","chrII",132426,133752]
+[["YBL045C",["COR1","QCR1"]],"Genes","YBL045C","chrII",134145,135519]
+[["YBL044W"],"Genes","YBL044W","chrII",136000,136369]
+[["YBL043W","ECM13"],"Genes","YBL043W","chrII",136690,137464]
+[["YBL042C","FUI1"],"Genes","YBL042C","chrII",138343,140263]
+[["YBL041W",["PRE7","PRS3"]],"Genes","YBL041W","chrII",141249,141975]
+[["YBL040C","ERD2"],"Genes","YBL040C","chrII",142114,142871]
+[["YBL039W-B","YBL039W-A"],"Genes","YBL039W-B","chrII",143395,143575]
+[["YBL039C","URA7"],"Genes","YBL039C","chrII",143991,145731]
+[["YBL039C-A"],"Genes","YBL039C-A","chrII",144950,145034]
+[["YBL038W","MRPL16"],"Genes","YBL038W","chrII",146189,146888]
+[["YBL037W","APL3"],"Genes","YBL037W","chrII",147211,150289]
+[["YBL036C"],"Genes","YBL036C","chrII",150449,151223]
+[["YBL035C","POL12"],"Genes","YBL035C","chrII",151495,153613]
+[["YBL034C","STU1"],"Genes","YBL034C","chrII",153850,158392]
+[["YBL033C","RIB1"],"Genes","YBL033C","chrII",158658,159696]
+[["YBL032W",["HEK2","KHD1"]],"Genes","YBL032W","chrII",160186,161332]
+[["YBL031W","SHE1"],"Genes","YBL031W","chrII",161701,162718]
+[["YBL030C",["PET9","OP1","ANC2","AAC2"]],"Genes","YBL030C","chrII",163043,164000]
+[["YBL029C-A"],"Genes","YBL029C-A","chrII",164490,164775]
+[["YBL029W"],"Genes","YBL029W","chrII",166136,167267]
+[["YBL028C"],"Genes","YBL028C","chrII",167520,167841]
+[["YBL027W","RPL19B"],"Genes","YBL027W","chrII",168425,169379]
+[["YBL026W",["LSM2","SNP3","SMX5"]],"Genes","YBL026W","chrII",170625,171041]
+[["YBL025W","RRN10"],"Genes","YBL025W","chrII",171483,171921]
+[["YBL024W",["NCL1","TRM4"]],"Genes","YBL024W","chrII",172536,174591]
+[["YBL023C","MCM2"],"Genes","YBL023C","chrII",174922,177529]
+[["YBL022C",["PIM1","LON1"]],"Genes","YBL022C","chrII",177876,181278]
+[["YBL021C","HAP3"],"Genes","YBL021C","chrII",181662,182097]
+[["YBL020W","RFT1"],"Genes","YBL020W","chrII",182403,184128]
+[["YBL019W",["APN2","ETH1"]],"Genes","YBL019W","chrII",184355,185918]
+[["YBL018C","POP8"],"Genes","YBL018C","chrII",186000,186477]
+[["YBL017C",["PEP1","VPT1","VPS10"]],"Genes","YBL017C","chrII",186846,191586]
+[["YBL016W",["FUS3","DAC2"]],"Genes","YBL016W","chrII",192453,193515]
+[["YBL015W","ACH1"],"Genes","YBL015W","chrII",194124,195705]
+[["YBL014C","RRN6"],"Genes","YBL014C","chrII",199066,201751]
+[["YBL013W","FMT1"],"Genes","YBL013W","chrII",202058,203264]
+[["YBL012C"],"Genes","YBL012C","chrII",203408,203810]
+[["YBL011W",["SCT1","GAT2"]],"Genes","YBL011W","chrII",203540,205820]
+[["YBL010C"],"Genes","YBL010C","chrII",206109,206952]
+[["YBL009W","ALK2"],"Genes","YBL009W","chrII",207196,209227]
+[["YBL008W-A"],"Genes","YBL008W-A","chrII",209411,209651]
+[["YBL008W","HIR1"],"Genes","YBL008W","chrII",209655,212178]
+[["YBL007C","SLA1"],"Genes","YBL007C","chrII",212634,216369]
+[["YBL006C",["LDB7","RSC14"]],"Genes","YBL006C","chrII",216589,217132]
+[["YBL006W-A"],"Genes","YBL006W-A","chrII",216713,216863]
+[["YBL005W",["PDR3","TPE2","AMY2"]],"Genes","YBL005W","chrII",217472,220403]
+[["YBL004W","UTP20"],"Genes","YBL004W","chrII",227638,235120]
+[["YBL003C",["HTA2","H2A2"]],"Genes","YBL003C","chrII",235396,235795]
+[["YBL002W","HTB2"],"Genes","YBL002W","chrII",236494,236890]
+[["YBL001C","ECM15"],"Genes","YBL001C","chrII",237154,237469]
+[["YBR001C","NTH2"],"Genes","YBR001C","chrII",238942,241285]
+[["YBR002C","RER2"],"Genes","YBR002C","chrII",241709,242570]
+[["YBR003W","COQ1"],"Genes","YBR003W","chrII",242810,244232]
+[["YBR004C",["GPI18","FMP44"]],"Genes","YBR004C","chrII",244367,245669]
+[["YBR005W","RCR1"],"Genes","YBR005W","chrII",245907,246549]
+[["YBR006W",["UGA2","UGA5"]],"Genes","YBR006W","chrII",247011,248505]
+[["YBR007C","DSF2"],"Genes","YBR007C","chrII",248806,251017]
+[["YBR008C","FLR1"],"Genes","YBR008C","chrII",252563,254210]
+[["YBR009C","HHF1"],"Genes","YBR009C","chrII",255370,255682]
+[["YBR010W",["HHT1","SIN2","BUR5"]],"Genes","YBR010W","chrII",256328,256739]
+[["YBR011C",["IPP1","PPA1"]],"Genes","YBR011C","chrII",257109,257973]
+[["YBR012C"],"Genes","YBR012C","chrII",259144,259564]
+[["YBR013C"],"Genes","YBR013C","chrII",265489,265879]
+[["YBR014C","GRX7"],"Genes","YBR014C","chrII",266724,267336]
+[["YBR015C",["MNN2","LDB8","CRV4","TTP1"]],"Genes","YBR015C","chrII",267709,269503]
+[["YBR016W"],"Genes","YBR016W","chrII",270246,270633]
+[["YBR017C","KAP104"],"Genes","YBR017C","chrII",270946,273703]
+[["YBR018C","GAL7"],"Genes","YBR018C","chrII",274426,275527]
+[["YBR019C","GAL10"],"Genes","YBR019C","chrII",276252,278352]
+[["YBR020W","GAL1"],"Genes","YBR020W","chrII",279020,280607]
+[["YBR021W","FUR4"],"Genes","YBR021W","chrII",281442,283344]
+[["YBR022W","POA1"],"Genes","YBR022W","chrII",283737,284271]
+[["YBR023C",["CHS3","KTI2","DIT101","CSD2","CAL1"]],"Genes","YBR023C","chrII",284427,287925]
+[["YBR024W","SCO2"],"Genes","YBR024W","chrII",289444,290350]
+[["YBR025C","OLA1"],"Genes","YBR025C","chrII",290680,291865]
+[["YBR026C",["ETR1","MRF1'","MRF1"]],"Genes","YBR026C","chrII",292876,294019]
+[["YBR027C"],"Genes","YBR027C","chrII",294023,294356]
+[["YBR028C"],"Genes","YBR028C","chrII",294424,296002]
+[["YBR029C",["CDS1","CDG1"]],"Genes","YBR029C","chrII",296368,297742]
+[["YBR030W"],"Genes","YBR030W","chrII",298291,299950]
+[["YBR031W","RPL4A"],"Genes","YBR031W","chrII",300165,301254]
+[["YBR032W"],"Genes","YBR032W","chrII",301518,301821]
+[["YBR033W","EDS1"],"Genes","YBR033W","chrII",301943,304703]
+[["YBR034C",["HMT1","RMT1","ODP1","HCP1"]],"Genes","YBR034C","chrII",304929,305976]
+[["YBR035C","PDX3"],"Genes","YBR035C","chrII",306268,306955]
+[["YBR036C",["CSG2","CLS2"]],"Genes","YBR036C","chrII",309080,310313]
+[["YBR037C",["SCO1","PET161"]],"Genes","YBR037C","chrII",310563,311451]
+[["YBR038W","CHS2"],"Genes","YBR038W","chrII",311896,314788]
+[["YBR039W","ATP3"],"Genes","YBR039W","chrII",315574,316510]
+[["YBR040W","FIG1"],"Genes","YBR040W","chrII",316967,317864]
+[["YBR041W","FAT1"],"Genes","YBR041W","chrII",318265,320275]
+[["YBR042C","CST26"],"Genes","YBR042C","chrII",320415,321609]
+[["YBR043C",["QDR3","AQR2"]],"Genes","YBR043C","chrII",321875,323945]
+[["YBR044C","TCM62"],"Genes","YBR044C","chrII",324336,326058]
+[["YBR045C","GIP1"],"Genes","YBR045C","chrII",328368,330090]
+[["YBR046C","ZTA1"],"Genes","YBR046C","chrII",330504,331509]
+[["YBR047W","FMP23"],"Genes","YBR047W","chrII",331830,332358]
+[["YBR048W","RPS11B"],"Genes","YBR048W","chrII",332828,333810]
+[["YBR049C",["REB1","GRF2"]],"Genes","YBR049C","chrII",334383,336816]
+[["YBR050C","REG2"],"Genes","YBR050C","chrII",337180,338197]
+[["YBR051W"],"Genes","YBR051W","chrII",337985,338336]
+[["YBR052C","RFS1"],"Genes","YBR052C","chrII",338717,339350]
+[["YBR053C"],"Genes","YBR053C","chrII",339672,340749]
+[["YBR054W","YRO2"],"Genes","YBR054W","chrII",343098,344133]
+[["YBR055C",["PRP6","TSM7269","RNA6"]],"Genes","YBR055C","chrII",344599,347299]
+[["YBR056W"],"Genes","YBR056W","chrII",347876,349382]
+[["YBR056W-A"],"Genes","YBR056W-A","chrII",351252,351453]
+[["YBR056C-B"],"Genes","YBR056C-B","chrII",351290,351449]
+[["YBR057C",["MUM2","SPOT8"]],"Genes","YBR057C","chrII",352190,353291]
+[["YBR058C",["UBP14","GID6"]],"Genes","YBR058C","chrII",353669,356015]
+[["YBR058C-A","TSC3"],"Genes","YBR058C-A","chrII",356321,356564]
+[["YBR059C","AKL1"],"Genes","YBR059C","chrII",356858,360185]
+[["YBR060C",["ORC2","SIR5","RRR1"]],"Genes","YBR060C","chrII",360649,362512]
+[["YBR061C","TRM7"],"Genes","YBR061C","chrII",364784,365717]
+[["YBR062C"],"Genes","YBR062C","chrII",365973,366598]
+[["YBR063C"],"Genes","YBR063C","chrII",366967,368182]
+[["YBR064W"],"Genes","YBR064W","chrII",367760,368189]
+[["YBR065C",["ECM2","SLT11"]],"Genes","YBR065C","chrII",368581,369676]
+[["YBR066C","NRG2"],"Genes","YBR066C","chrII",370034,370697]
+[["YBR067C","TIP1"],"Genes","YBR067C","chrII",372099,372732]
+[["YBR068C","BAP2"],"Genes","YBR068C","chrII",373857,375687]
+[["YBR069C",["TAT1","VAP1","TAP1"]],"Genes","YBR069C","chrII",376570,378430]
+[["YBR070C","ALG14"],"Genes","YBR070C","chrII",379217,379931]
+[["YBR071W"],"Genes","YBR071W","chrII",380407,381043]
+[["YBR072W","HSP26"],"Genes","YBR072W","chrII",382026,382671]
+[["YBR072C-A"],"Genes","YBR072C-A","chrII",382854,383016]
+[["YBR073W",["RDH54","TID1"]],"Genes","YBR073W","chrII",383208,385983]
+[["YBR074W","YBR075W"],"Genes","YBR074W","chrII",386280,389211]
+[["YBR076W","ECM8"],"Genes","YBR076W","chrII",390368,391388]
+[["YBR076C-A"],"Genes","YBR076C-A","chrII",391344,391611]
+[["YBR077C",["SLM4","GSE1","NIR1","EGO3"]],"Genes","YBR077C","chrII",391798,392287]
+[["YBR078W","ECM33"],"Genes","YBR078W","chrII",393117,394854]
+[["YBR079C",["RPG1","TIF32"]],"Genes","YBR079C","chrII",395376,398271]
+[["YBR080C",["SEC18","ANU4"]],"Genes","YBR080C","chrII",398607,400884]
+[["YBR081C",["SPT7","GIT2"]],"Genes","YBR081C","chrII",401246,405245]
+[["YBR082C","UBC4"],"Genes","YBR082C","chrII",406621,407163]
+[["YBR083W",["TEC1","ROC1"]],"Genes","YBR083W","chrII",409162,410623]
+[["YBR084W","MIS1"],"Genes","YBR084W","chrII",411047,413975]
+[["YBR084C-A","RPL19A"],"Genes","YBR084C-A","chrII",414179,415255]
+[["YBR085W",["AAC3","ANC3"]],"Genes","YBR085W","chrII",415976,416900]
+[["YBR085C-A"],"Genes","YBR085C-A","chrII",418900,419158]
+[["YBR086C","IST2"],"Genes","YBR086C","chrII",420194,423035]
+[["YBR087W","RFC5"],"Genes","YBR087W","chrII",423758,424823]
+[["YBR088C","POL30"],"Genes","YBR088C","chrII",424983,425760]
+[["YBR089W"],"Genes","YBR089W","chrII",425176,425776]
+[["YBR089C-A",["NHP6B","YBR090C-A"]],"Genes","YBR089C-A","chrII",426183,426867]
+[["YBR090C"],"Genes","YBR090C","chrII",426326,427052]
+[["YBR091C",["MRS5","TIM12"]],"Genes","YBR091C","chrII",427148,427478]
+[["YBR092C",["PHO3","phoC"]],"Genes","YBR092C","chrII",427691,429095]
+[["YBR093C",["PHO5","phoE"]],"Genes","YBR093C","chrII",429541,430945]
+[["YBR094W","PBY1"],"Genes","YBR094W","chrII",432029,434291]
+[["YBR095C",["RXT2","RAF60"]],"Genes","YBR095C","chrII",434399,435692]
+[["YBR096W"],"Genes","YBR096W","chrII",436014,436707]
+[["YBR097W",["VPS15","VPL19","VAC4","GRD8"]],"Genes","YBR097W","chrII",436944,441309]
+[["YBR098W",["MMS4","YBR100W","SLX2"]],"Genes","YBR098W","chrII",441508,443584]
+[["YBR099C"],"Genes","YBR099C","chrII",442917,443301]
+[["YBR101C","FES1"],"Genes","YBR101C","chrII",443814,444687]
+[["YBR102C",["EXO84","USA3"]],"Genes","YBR102C","chrII",445055,447317]
+[["YBR103W",["SIF2","EMB1"]],"Genes","YBR103W","chrII",447702,449310]
+[["YBR103C-A"],"Genes","YBR103C-A","chrII",449313,449457]
+[["YBR104W","YMC2"],"Genes","YBR104W","chrII",449660,450650]
+[["YBR105C",["VID24","GID4"]],"Genes","YBR105C","chrII",450874,451963]
+[["YBR106W","PHO88"],"Genes","YBR106W","chrII",452651,453218]
+[["YBR107C",["IML3","MCM19"]],"Genes","YBR107C","chrII",453786,454524]
+[["YBR108W","AIM3"],"Genes","YBR108W","chrII",454815,457659]
+[["YBR109C",["CMD1","CaM"]],"Genes","YBR109C","chrII",457912,458356]
+[["YBR109W-A"],"Genes","YBR109W-A","chrII",458601,458826]
+[["YBR110W","ALG1"],"Genes","YBR110W","chrII",458865,460215]
+[["YBR111C",["YSA1","RMA2"]],"Genes","YBR111C","chrII",461171,461867]
+[["YBR111W-A","SUS1"],"Genes","YBR111W-A","chrII",462132,462573]
+[["YBR112C",["CYC8","SSN6","CRT8"]],"Genes","YBR112C","chrII",462863,465764]
+[["YBR113W"],"Genes","YBR113W","chrII",465559,466042]
+[["YBR114W",["RAD16","PSO5"]],"Genes","YBR114W","chrII",467241,469614]
+[["YBR115C","LYS2"],"Genes","YBR115C","chrII",469741,473920]
+[["YBR116C"],"Genes","YBR116C","chrII",474192,474720]
+[["YBR117C","TKL2"],"Genes","YBR117C","chrII",474385,476431]
+[["YBR118W",["TEF2","EF-1 alpha"]],"Genes","YBR118W","chrII",477664,479041]
+[["YBR119W","MUD1"],"Genes","YBR119W","chrII",479331,480317]
+[["YBR120C","CBP6"],"Genes","YBR120C","chrII",480428,480917]
+[["YBR121C","GRS1"],"Genes","YBR121C","chrII",481357,483361]
+[["YBR121C-A"],"Genes","YBR121C-A","chrII",482319,482478]
+[["YBR122C","MRPL36"],"Genes","YBR122C","chrII",483963,484497]
+[["YBR123C","TFC1"],"Genes","YBR123C","chrII",484735,486685]
+[["YBR124W"],"Genes","YBR124W","chrII",486500,486860]
+[["YBR125C",["PTC4","GCT1"]],"Genes","YBR125C","chrII",487192,488374]
+[["YBR126C",["TPS1","TSS1","GLC6","GGS1","FDP1","CIF1","BYP1"]],"Genes","YBR126C","chrII",488898,490386]
+[["YBR126W-A"],"Genes","YBR126W-A","chrII",490843,491050]
+[["YBR126W-B"],"Genes","YBR126W-B","chrII",490920,491064]
+[["YBR127C",["VMA2","ATPSV","VAT2"]],"Genes","YBR127C","chrII",491262,492816]
+[["YBR128C",["ATG14","APG14","CVT12"]],"Genes","YBR128C","chrII",493074,494109]
+[["YBR129C","OPY1"],"Genes","YBR129C","chrII",494346,495333]
+[["YBR130C","SHE3"],"Genes","YBR130C","chrII",495585,496863]
+[["YBR131W",["CCZ1","CVT16"]],"Genes","YBR131W","chrII",497156,499271]
+[["YBR131C-A"],"Genes","YBR131C-A","chrII",497643,497733]
+[["YBR132C","AGP2"],"Genes","YBR132C","chrII",499645,501436]
+[["YBR133C","HSL7"],"Genes","YBR133C","chrII",501797,504281]
+[["YBR134W"],"Genes","YBR134W","chrII",504236,504638]
+[["YBR135W","CKS1"],"Genes","YBR135W","chrII",504847,505300]
+[["YBR136W",["MEC1","SAD3","ESR1"]],"Genes","YBR136W","chrII",505661,512768]
+[["YBR137W"],"Genes","YBR137W","chrII",513037,513577]
+[["YBR138C","HDR1"],"Genes","YBR138C","chrII",513755,515330]
+[["YBR139W"],"Genes","YBR139W","chrII",515657,517184]
+[["YBR140C",["IRA1","PPD1","GLC1"]],"Genes","YBR140C","chrII",517343,526622]
+[["YBR141C"],"Genes","YBR141C","chrII",527018,528032]
+[["YBR141W-A"],"Genes","YBR141W-A","chrII",527160,527256]
+[["YBR142W","MAK5"],"Genes","YBR142W","chrII",528310,530632]
+[["YBR143C",["SUP45","SUP47","SUP1","SAL4"]],"Genes","YBR143C","chrII",530862,532176]
+[["YBR144C"],"Genes","YBR144C","chrII",533228,533543]
+[["YBR145W","ADH5"],"Genes","YBR145W","chrII",533755,534811]
+[["YBR146W","MRPS9"],"Genes","YBR146W","chrII",535253,536090]
+[["YBR147W"],"Genes","YBR147W","chrII",536568,537459]
+[["YBR148W","YSW1"],"Genes","YBR148W","chrII",537869,539699]
+[["YBR149W","ARA1"],"Genes","YBR149W","chrII",539980,541015]
+[["YBR150C","TBS1"],"Genes","YBR150C","chrII",541202,544487]
+[["YBR151W","APD1"],"Genes","YBR151W","chrII",545021,545972]
+[["YBR152W","SPP381"],"Genes","YBR152W","chrII",546369,547245]
+[["YBR153W","RIB7"],"Genes","YBR153W","chrII",547453,548188]
+[["YBR154C","RPB5"],"Genes","YBR154C","chrII",548355,549003]
+[["YBR155W","CNS1"],"Genes","YBR155W","chrII",549764,550922]
+[["YBR156C","SLI15"],"Genes","YBR156C","chrII",551097,553194]
+[["YBR157C","ICS2"],"Genes","YBR157C","chrII",553536,554304]
+[["YBR158W",["AMN1","CST13","ICS4"]],"Genes","YBR158W","chrII",556542,558192]
+[["YBR159W","IFA38"],"Genes","YBR159W","chrII",558678,559722]
+[["YBR160W",["CDC28","SRM5","HSL5","CDK1"]],"Genes","YBR160W","chrII",560071,560968]
+[["YBR161W","CSH1"],"Genes","YBR161W","chrII",561628,562759]
+[["YBR162C","TOS1"],"Genes","YBR162C","chrII",563197,564565]
+[["YBR162W-A","YSY6"],"Genes","YBR162W-A","chrII",565225,565423]
+[["YBR163W","DEM1"],"Genes","YBR163W","chrII",565717,567475]
+[["YBR164C",["ARL1","DLP2"]],"Genes","YBR164C","chrII",567869,568421]
+[["YBR165W","UBS1"],"Genes","YBR165W","chrII",568846,569680]
+[["YBR166C","TYR1"],"Genes","YBR166C","chrII",569836,571195]
+[["YBR167C",["POP7","RPP2"]],"Genes","YBR167C","chrII",571462,571885]
+[["YBR168W","PEX32"],"Genes","YBR168W","chrII",572365,573607]
+[["YBR169C","SSE2"],"Genes","YBR169C","chrII",573909,575991]
+[["YBR170C",["NPL4","HRD4"]],"Genes","YBR170C","chrII",576338,578081]
+[["YBR171W",["SEC66","SEC71","HSS1"]],"Genes","YBR171W","chrII",578358,578979]
+[["YBR172C","SMY2"],"Genes","YBR172C","chrII",579144,581367]
+[["YBR173C",["UMP1","RNS2"]],"Genes","YBR173C","chrII",581720,582167]
+[["YBR174C"],"Genes","YBR174C","chrII",582332,582647]
+[["YBR175W",["SWD3","SAF35","CPS30"]],"Genes","YBR175W","chrII",582402,583350]
+[["YBR176W","ECM31"],"Genes","YBR176W","chrII",583714,584653]
+[["YBR177C","EHT1"],"Genes","YBR177C","chrII",584801,586157]
+[["YBR178W"],"Genes","YBR178W","chrII",586065,586440]
+[["YBR179C","FZO1"],"Genes","YBR179C","chrII",586541,589109]
+[["YBR180W","DTR1"],"Genes","YBR180W","chrII",589735,591454]
+[["YBR181C",["RPS6B","RPS102","RPS101","LPG18"]],"Genes","YBR181C","chrII",591706,592769]
+[["YBR182C","SMP1"],"Genes","YBR182C","chrII",593500,594859]
+[["YBR182C-A"],"Genes","YBR182C-A","chrII",595355,595550]
+[["YBR183W","YPC1"],"Genes","YBR183W","chrII",596109,597060]
+[["YBR184W"],"Genes","YBR184W","chrII",597357,598929]
+[["YBR185C","MBA1"],"Genes","YBR185C","chrII",599117,599954]
+[["YBR186W","PCH2"],"Genes","YBR186W","chrII",600547,602355]
+[["YBR187W","GDT1"],"Genes","YBR187W","chrII",602628,603471]
+[["YBR188C","NTC20"],"Genes","YBR188C","chrII",603679,604102]
+[["YBR189W",["RPS9B","SUP46","RPS13A"]],"Genes","YBR189W","chrII",604502,605503]
+[["YBR190W"],"Genes","YBR190W","chrII",605960,606272]
+[["YBR191W",["RPL21A","URP1"]],"Genes","YBR191W","chrII",606264,607135]
+[["YBR191W-A"],"Genes","YBR191W-A","chrII",607143,607218]
+[["YBR192W",["RIM2","PYT1"]],"Genes","YBR192W","chrII",607646,608780]
+[["YBR193C","MED8"],"Genes","YBR193C","chrII",609076,609748]
+[["YBR194W",["AIM4","SOY1"]],"Genes","YBR194W","chrII",610032,610404]
+[["YBR195C",["MSI1","CAC3"]],"Genes","YBR195C","chrII",610608,611877]
+[["YBR196C",["PGI1","CDC30"]],"Genes","YBR196C","chrII",612230,613895]
+[["YBR196C-A"],"Genes","YBR196C-A","chrII",614018,614168]
+[["YBR196C-B"],"Genes","YBR196C-B","chrII",614520,614625]
+[["YBR197C"],"Genes","YBR197C","chrII",615197,615851]
+[["YBR198C",["TAF5","TAF90"]],"Genes","YBR198C","chrII",616121,618518]
+[["YBR199W","KTR4"],"Genes","YBR199W","chrII",618903,620298]
+[["YBR200W",["BEM1","SRO1"]],"Genes","YBR200W","chrII",620866,622522]
+[["YBR200W-A"],"Genes","YBR200W-A","chrII",622977,623142]
+[["YBR201W","DER1"],"Genes","YBR201W","chrII",623571,624207]
+[["YBR201C-A"],"Genes","YBR201C-A","chrII",624488,624692]
+[["YBR202W",["MCM7","CDC47"]],"Genes","YBR202W","chrII",625766,628304]
+[["YBR203W","COS111"],"Genes","YBR203W","chrII",629162,631937]
+[["YBR204C"],"Genes","YBR204C","chrII",632248,633376]
+[["YBR205W","KTR3"],"Genes","YBR205W","chrII",633616,634831]
+[["YBR206W"],"Genes","YBR206W","chrII",634595,634919]
+[["YBR207W","FTH1"],"Genes","YBR207W","chrII",635140,636538]
+[["YBR208C",["DUR1","2","DUR80"]],"Genes","YBR208C","chrII",636697,642205]
+[["YBR209W"],"Genes","YBR209W","chrII",642577,642895]
+[["YBR210W","ERV15"],"Genes","YBR210W","chrII",645544,645973]
+[["YBR211C",["AME1","ARP100"]],"Genes","YBR211C","chrII",646152,647127]
+[["YBR212W",["NGR1","RBP1"]],"Genes","YBR212W","chrII",647880,649899]
+[["YBR213W","MET8"],"Genes","YBR213W","chrII",650362,651187]
+[["YBR214W","SDS24"],"Genes","YBR214W","chrII",651409,652993]
+[["YBR215W","HPC2"],"Genes","YBR215W","chrII",653350,655312]
+[["YBR216C","YBP1"],"Genes","YBR216C","chrII",655570,657595]
+[["YBR217W",["ATG12","APG12"]],"Genes","YBR217W","chrII",657826,658387]
+[["YBR218C","PYC2"],"Genes","YBR218C","chrII",658701,662244]
+[["YBR219C"],"Genes","YBR219C","chrII",662493,663298]
+[["YBR220C"],"Genes","YBR220C","chrII",662989,664672]
+[["YBR221C","PDB1"],"Genes","YBR221C","chrII",665147,666248]
+[["YBR221W-A"],"Genes","YBR221W-A","chrII",666532,666637]
+[["YBR222C",["PCS60","FAT2"]],"Genes","YBR222C","chrII",666714,668346]
+[["YBR223C","TDP1"],"Genes","YBR223C","chrII",668657,670292]
+[["YBR223W-A"],"Genes","YBR223W-A","chrII",668715,668835]
+[["YBR224W"],"Genes","YBR224W","chrII",670119,670635]
+[["YBR225W"],"Genes","YBR225W","chrII",670621,673324]
+[["YBR226C"],"Genes","YBR226C","chrII",673149,673560]
+[["YBR227C","MCX1"],"Genes","YBR227C","chrII",673566,675129]
+[["YBR228W","SLX1"],"Genes","YBR228W","chrII",675307,676222]
+[["YBR229C",["ROT2","GLS2"]],"Genes","YBR229C","chrII",676351,679216]
+[["YBR230C","OM14"],"Genes","YBR230C","chrII",679543,680045]
+[["YBR230W-A"],"Genes","YBR230W-A","chrII",680356,680557]
+[["YBR231C",["SWC5","AOR1"]],"Genes","YBR231C","chrII",682173,683085]
+[["YBR232C"],"Genes","YBR232C","chrII",683367,683727]
+[["YBR233W",["PBP2","HEK1"]],"Genes","YBR233W","chrII",683422,684664]
+[["YBR233W-A","DAD3"],"Genes","YBR233W-A","chrII",684971,685256]
+[["YBR234C","ARC40"],"Genes","YBR234C","chrII",685432,686587]
+[["YBR235W"],"Genes","YBR235W","chrII",686895,690258]
+[["YBR236C","ABD1"],"Genes","YBR236C","chrII",690377,691688]
+[["YBR237W",["PRP5","RNA5"]],"Genes","YBR237W","chrII",691963,694513]
+[["YBR238C"],"Genes","YBR238C","chrII",695101,697297]
+[["YBR239C","ERT1"],"Genes","YBR239C","chrII",698348,699938]
+[["YBR240C",["THI2","phoF","PHO6"]],"Genes","YBR240C","chrII",700484,701837]
+[["YBR241C"],"Genes","YBR241C","chrII",702583,704050]
+[["YBR242W"],"Genes","YBR242W","chrII",704664,705381]
+[["YBR243C",["ALG7","TUR1"]],"Genes","YBR243C","chrII",705441,706788]
+[["YBR244W",["GPX2","AMI1"]],"Genes","YBR244W","chrII",707522,708011]
+[["YBR245C",["ISW1","SGN2"]],"Genes","YBR245C","chrII",708144,711534]
+[["YBR246W"],"Genes","YBR246W","chrII",711585,712749]
+[["YBR247C",["ENP1","MEG1"]],"Genes","YBR247C","chrII",712998,714450]
+[["YBR248C","HIS7"],"Genes","YBR248C","chrII",714801,716460]
+[["YBR249C","ARO4"],"Genes","YBR249C","chrII",716876,717989]
+[["YBR250W","SPO23"],"Genes","YBR250W","chrII",719027,720599]
+[["YBR251W","MRPS5"],"Genes","YBR251W","chrII",721384,722308]
+[["YBR252W","DUT1"],"Genes","YBR252W","chrII",722605,723049]
+[["YBR253W",["SRB6","MED22"]],"Genes","YBR253W","chrII",723264,723630]
+[["YBR254C","TRS20"],"Genes","YBR254C","chrII",723730,724258]
+[["YBR255W"],"Genes","YBR255W","chrII",724450,726535]
+[["YBR255C-A"],"Genes","YBR255C-A","chrII",726612,727069]
+[["YBR256C","RIB5"],"Genes","YBR256C","chrII",727380,728097]
+[["YBR257W","POP4"],"Genes","YBR257W","chrII",728879,729719]
+[["YBR258C",["SHG1","CPS15"]],"Genes","YBR258C","chrII",729728,730157]
+[["YBR259W"],"Genes","YBR259W","chrII",730381,732448]
+[["YBR260C","RGD1"],"Genes","YBR260C","chrII",732633,734634]
+[["YBR261C"],"Genes","YBR261C","chrII",734826,735525]
+[["YBR262C",["AIM5","FMP51"]],"Genes","YBR262C","chrII",735714,736035]
+[["YBR263W",["SHM1","TMP3","SHMT1"]],"Genes","YBR263W","chrII",736258,737731]
+[["YBR264C","YPT10"],"Genes","YBR264C","chrII",737764,738364]
+[["YBR265W","TSC10"],"Genes","YBR265W","chrII",738576,739539]
+[["YBR267W","REI1"],"Genes","YBR267W","chrII",739835,741017]
+[["YBR266C","SLM6"],"Genes","YBR266C","chrII",739930,740383]
+[["YBR268W","MRPL37"],"Genes","YBR268W","chrII",741293,741611]
+[["YBR269C","FMP21"],"Genes","YBR269C","chrII",742154,742571]
+[["YBR270C","BIT2"],"Genes","YBR270C","chrII",742755,744393]
+[["YBR271W"],"Genes","YBR271W","chrII",744846,746106]
+[["YBR272C","HSM3"],"Genes","YBR272C","chrII",746355,747798]
+[["YBR273C",["UBX7","CUI3"]],"Genes","YBR273C","chrII",748055,749366]
+[["YBR274W","CHK1"],"Genes","YBR274W","chrII",749588,751172]
+[["YBR275C","RIF1"],"Genes","YBR275C","chrII",751350,757101]
+[["YBR276C","PPS1"],"Genes","YBR276C","chrII",757615,760039]
+[["YBR277C"],"Genes","YBR277C","chrII",760210,760612]
+[["YBR278W","DPB3"],"Genes","YBR278W","chrII",760289,760895]
+[["YBR279W","PAF1"],"Genes","YBR279W","chrII",761252,762590]
+[["YBR280C","SAF1"],"Genes","YBR280C","chrII",762779,764693]
+[["YBR281C","DUG2"],"Genes","YBR281C","chrII",764965,767602]
+[["YBR282W","MRPL27"],"Genes","YBR282W","chrII",768235,768676]
+[["YBR283C","SSH1"],"Genes","YBR283C","chrII",768938,770411]
+[["YBR284W"],"Genes","YBR284W","chrII",771234,773628]
+[["YBR285W"],"Genes","YBR285W","chrII",773917,774352]
+[["YBR286W",["APE3","APY1"]],"Genes","YBR286W","chrII",774695,776309]
+[["YBR287W","ZSP1"],"Genes","YBR287W","chrII",776566,777850]
+[["YBR288C",["APM3","YKS6"]],"Genes","YBR288C","chrII",778007,779459]
+[["YBR289W",["SNF5","TYE4","SWI10","HAF4"]],"Genes","YBR289W","chrII",779662,782380]
+[["YBR290W","BSD2"],"Genes","YBR290W","chrII",782586,783552]
+[["YBR291C","CTP1"],"Genes","YBR291C","chrII",783668,784568]
+[["YBR292C"],"Genes","YBR292C","chrII",784697,785069]
+[["YBR293W","VBA2"],"Genes","YBR293W","chrII",787000,788425]
+[["YBR294W",["SUL1","SFP2"]],"Genes","YBR294W","chrII",789229,791809]
+[["YBR295W",["PCA1","PAY2","CAD2"]],"Genes","YBR295W","chrII",792842,796493]
+[["YBR296C",["PHO89","ITN1"]],"Genes","YBR296C","chrII",796791,798516]
+[["YBR296C-A"],"Genes","YBR296C-A","chrII",800116,800236]
+[["YBR297W",["MAL33","MALR","MAL3R"]],"Genes","YBR297W","chrII",800516,801923]
+[["YBR298C",["MAL31","MALT","MAL3T"]],"Genes","YBR298C","chrII",802624,804469]
+[["YBR298C-A"],"Genes","YBR298C-A","chrII",805028,805250]
+[["YBR299W",["MAL32","MALS","MAL3S"]],"Genes","YBR299W","chrII",805344,807099]
+[["YBR300C"],"Genes","YBR300C","chrII",808593,809091]
+[["YBR301W","DAN3"],"Genes","YBR301W","chrII",809050,809413]
+[["YBR302C","COS2"],"Genes","YBR302C","chrII",810333,811473]
diff --git a/test/integration/resources/sequences/yeast/tracks/Genes/chrII/trackData.json b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/trackData.json
new file mode 100644
index 0000000..3ee0b48
--- /dev/null
+++ b/test/integration/resources/sequences/yeast/tracks/Genes/chrII/trackData.json
@@ -0,0 +1 @@
+{"featureCount":456,"histograms":{"meta":[{"arrayParams":{"urlTemplate":"hist-5000-{Chunk}.json","length":163,"chunkSize":10000},"basesPerBin":"5000"},{"basesPerBin":"10000","arrayParams":{"chunkSize":10000,"urlTemplate":"hist-10000-{Chunk}.json","length":82}}],"stats":[{"mean":3.5521472392638,"basesPerBin":"5000","max":7},{"mean":6.34146341463415,"basesPerBin":"10000","max":11}]},"formatVersion":1,"intervals":{"nclist":[[395,279,235795,1],[395,236494,458826,2],[395,458865,652993,3],[395 [...]
\ No newline at end of file
diff --git a/test/integration/resources/track-data/inputNcList.json b/test/integration/resources/track-data/inputNcList.json
new file mode 100644
index 0000000..1488e5d
--- /dev/null
+++ b/test/integration/resources/track-data/inputNcList.json
@@ -0,0 +1,357 @@
+[
+  [
+    3,
+    53461316,
+    53478244,
+    1,
+    "MGI",
+    "MGI:5592922",
+    "13",
+    "Gm33763",
+    "predicted gene, 33763",
+    "NCBI_Gene:102636789",
+    "lncRNA_gene",
+    "gene",
+    "MGI:5592922",
+    "MGI:5592922",
+    [
+      [
+        20,
+        53461316,
+        53478244,
+        1,
+        "MGI:5592922",
+        "NCBI",
+        "13",
+        "XR_873404.1",
+        "lncRNA",
+        "ncRNA",
+        "MGI:5592922.t3",
+        "XR_873404.1",
+        [
+          [
+            34,
+            53461316,
+            53462181,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e10",
+            "exon",
+            "XR_873404.1"
+          ],
+          [
+            34,
+            53473974,
+            53476598,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e9",
+            "exon",
+            "XR_873404.1"
+          ],
+          [
+            34,
+            53477052,
+            53478244,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e8",
+            "exon",
+            "XR_873404.1"
+          ]
+        ]
+      ],
+      [
+        20,
+        53461316,
+        53478244,
+        1,
+        "MGI:5592922",
+        "NCBI",
+        "13",
+        "XR_873405.1",
+        "lncRNA",
+        "ncRNA",
+        "MGI:5592922.t2",
+        "XR_873405.1",
+        [
+          [
+            34,
+            53461316,
+            53462181,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e7",
+            "exon",
+            "XR_873405.1"
+          ],
+          [
+            34,
+            53473974,
+            53475926,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e6",
+            "exon",
+            "XR_873405.1"
+          ],
+          [
+            34,
+            53477052,
+            53478244,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e5",
+            "exon",
+            "XR_873405.1"
+          ]
+        ]
+      ],
+      [
+        20,
+        53461316,
+        53478244,
+        1,
+        "MGI:5592922",
+        "NCBI",
+        "13",
+        "XR_873406.1",
+        "lncRNA",
+        "ncRNA",
+        "MGI:5592922.t1",
+        "XR_873406.1",
+        [
+          [
+            34,
+            53461316,
+            53462181,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e4",
+            "exon",
+            "XR_873406.1"
+          ],
+          [
+            34,
+            53473974,
+            53475798,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e3",
+            "exon",
+            "XR_873406.1"
+          ],
+          [
+            34,
+            53475875,
+            53475926,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e2",
+            "exon",
+            "XR_873406.1"
+          ],
+          [
+            34,
+            53477052,
+            53478244,
+            1,
+            "MGI:5592922",
+            "NCBI",
+            "13",
+            "MGI:5592922.e1",
+            "exon",
+            "XR_873406.1"
+          ]
+        ]
+      ]
+    ],
+    {
+      "Sublist": [
+        [
+          3,
+          53466880,
+          53473074,
+          -1,
+          "MGI",
+          "MGI:97169",
+          "13",
+          "Msx2",
+          "msh homeobox 2",
+          [
+            "ENSEMBL:ENSMUSG00000021469",
+            "NCBI_Gene:17702"
+          ],
+          "protein_coding_gene",
+          "gene",
+          "MGI:97169",
+          "MGI:97169",
+          [
+            [
+              17,
+              53466880,
+              53472780,
+              -1,
+              "MGI:97169",
+              "NCBI",
+              "13",
+              "NM_013601.2",
+              "mRNA",
+              "MGI:97169.t3",
+              "NM_013601.2",
+              [
+                [
+                  34,
+                  53472331,
+                  53472780,
+                  -1,
+                  "MGI:97169",
+                  "NCBI",
+                  "13",
+                  "MGI:97169.e5",
+                  "exon",
+                  "NM_013601.2"
+                ],
+                [
+                  34,
+                  53466880,
+                  53468593,
+                  -1,
+                  "MGI:97169",
+                  "NCBI",
+                  "13",
+                  "MGI:97169.e4",
+                  "exon",
+                  "NM_013601.2"
+                ],
+                [
+                  36,
+                  53472331,
+                  53472710,
+                  -1,
+                  "MGI:97169",
+                  "NCBI",
+                  "13",
+                  "NP_038629.2",
+                  "NP_038629.2",
+                  0,
+                  "CDS",
+                  "MGI:97169.c2",
+                  "NM_013601.2"
+                ],
+                [
+                  36,
+                  53468168,
+                  53468593,
+                  -1,
+                  "MGI:97169",
+                  "NCBI",
+                  "13",
+                  "NP_038629.2",
+                  "NP_038629.2",
+                  2,
+                  "CDS",
+                  "MGI:97169.c2",
+                  "NM_013601.2"
+                ]
+              ]
+            ],
+            [
+              18,
+              53466883,
+              53473074,
+              -1,
+              "MGI:97169",
+              "ENSEMBL",
+              "13",
+              "ENSMUST00000021922",
+              "CCDS26522.1",
+              "mRNA",
+              "basic",
+              "MGI:97169.t2",
+              "ENSMUST00000021922",
+              [
+                [
+                  35,
+                  53466883,
+                  53468593,
+                  -1,
+                  "MGI:97169",
+                  "ENSEMBL",
+                  "13",
+                  "ENSMUSE00000641992",
+                  "exon",
+                  "ENSMUSE00000641992",
+                  "MGI:97169.e3",
+                  "ENSMUST00000021922"
+                ],
+                [
+                  36,
+                  53468168,
+                  53468593,
+                  -1,
+                  "MGI:97169",
+                  "ENSEMBL",
+                  "13",
+                  "ENSMUSP00000021922",
+                  "ENSMUSP00000021922",
+                  2,
+                  "CDS",
+                  "MGI:97169.c1",
+                  "ENSMUST00000021922"
+                ],
+                [
+                  36,
+                  53472331,
+                  53472710,
+                  -1,
+                  "MGI:97169",
+                  "ENSEMBL",
+                  "13",
+                  "ENSMUSP00000021922",
+                  "ENSMUSP00000021922",
+                  0,
+                  "CDS",
+                  "MGI:97169.c1",
+                  "ENSMUST00000021922"
+                ],
+                [
+                  35,
+                  53472331,
+                  53473074,
+                  -1,
+                  "MGI:97169",
+                  "ENSEMBL",
+                  "13",
+                  "ENSMUSE00000118134",
+                  "exon",
+                  "ENSMUSE00000118134",
+                  "MGI:97169.e2",
+                  "ENSMUST00000021922"
+                ]
+              ]
+            ]
+          ]
+        ]
+      ]
+    }
+  ]
+]
diff --git a/test/integration/resources/track-data/mouseMsx2.json b/test/integration/resources/track-data/mouseMsx2.json
new file mode 100644
index 0000000..3078773
--- /dev/null
+++ b/test/integration/resources/track-data/mouseMsx2.json
@@ -0,0 +1,156 @@
+[
+  {
+    "strand": -1,
+    "children": [
+      {
+        "strand": -1,
+        "children": [
+          {
+            "strand": -1,
+            "name": "ENSMUSE00000376402",
+            "source": "ENSEMBL",
+            "id": "MGI:97168.e5",
+            "fmin": 37820484,
+            "type": "exon",
+            "fmax": 37821700,
+            "seqId": "5"
+          },
+          {
+            "phase": 2,
+            "strand": -1,
+            "name": "ENSMUSP00000058354",
+            "source": "ENSEMBL",
+            "id": "MGI:97168.c2",
+            "fmin": 37821257,
+            "type": "CDS",
+            "fmax": 37821700,
+            "seqId": "5"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "name": "ENSMUSP00000058354",
+            "source": "ENSEMBL",
+            "id": "MGI:97168.c2",
+            "fmin": 37823864,
+            "type": "CDS",
+            "fmax": 37824333,
+            "seqId": "5"
+          },
+          {
+            "strand": -1,
+            "name": "ENSMUSE00000355418",
+            "source": "ENSEMBL",
+            "id": "MGI:97168.e4",
+            "fmin": 37823864,
+            "type": "exon",
+            "fmax": 37824583,
+            "seqId": "5"
+          }
+        ],
+        "name": "ENSMUST00000063116",
+        "source": "ENSEMBL",
+        "id": "MGI:97168.t3",
+        "fmin": 37820484,
+        "type": "mRNA",
+        "fmax": 37824583,
+        "seqId": "5"
+      },
+      {
+        "strand": -1,
+        "children": [
+          {
+            "strand": -1,
+            "source": "NCBI",
+            "id": "MGI:97168.e2",
+            "fmin": 37823864,
+            "type": "exon",
+            "fmax": 37824585,
+            "seqId": "5"
+          },
+          {
+            "strand": -1,
+            "source": "NCBI",
+            "id": "MGI:97168.e1",
+            "fmin": 37820490,
+            "type": "exon",
+            "fmax": 37821700,
+            "seqId": "5"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "name": "NP_034965.2",
+            "source": "NCBI",
+            "id": "MGI:97168.c1",
+            "fmin": 37823864,
+            "type": "CDS",
+            "fmax": 37824333,
+            "seqId": "5"
+          },
+          {
+            "phase": 2,
+            "strand": -1,
+            "name": "NP_034965.2",
+            "source": "NCBI",
+            "id": "MGI:97168.c1",
+            "fmin": 37821257,
+            "type": "CDS",
+            "fmax": 37821700,
+            "seqId": "5"
+          }
+        ],
+        "name": "NM_010835.2",
+        "source": "NCBI",
+        "id": "MGI:97168.t1",
+        "fmin": 37820490,
+        "type": "mRNA",
+        "fmax": 37824585,
+        "seqId": "5"
+      },
+      [
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "NCBI",
+                  "id": "MGI:2136762.e1",
+                  "fmin": 37820563,
+                  "type": "exon",
+                  "fmax": 37822751,
+                  "seqId": "5"
+                }
+              ],
+              "name": "NR_027920.1",
+              "source": "NCBI",
+              "id": "MGI:2136762.t1",
+              "fmin": 37820563,
+              "type": "ncRNA",
+              "fmax": 37822751,
+              "seqId": "5"
+            }
+          ],
+          "name": "Msx1os",
+          "source": "MGI",
+          "id": "MGI:2136762",
+          "fmin": 37820563,
+          "type": "gene",
+          "fmax": 37822751,
+          "seqId": "5"
+        }
+      ]
+    ],
+    "name": "Msx1",
+    "source": "MGI",
+    "id": "http://localhost:8080/apollo/track/Mus musculus/All Genes/5/Msx1.json",
+    "fmin": 37820484,
+    "type": "gene",
+    "fmax": 37824585,
+    "seqId": "5",
+    "selected": true
+  }
+]
\ No newline at end of file
diff --git a/test/integration/resources/track-data/trackClasses.json b/test/integration/resources/track-data/trackClasses.json
new file mode 100644
index 0000000..330acf5
--- /dev/null
+++ b/test/integration/resources/track-data/trackClasses.json
@@ -0,0 +1 @@
+[{"isArrayAttr":{"Subfeatures":1},"attributes":["Start","End","Strand","Source","Gene_id","Seq_id","Name","Description","Dbxref","So_term_name","Type","Id","Curie","Subfeatures"]},{"isArrayAttr":{"Subfeatures":1},"attributes":["Start","End","Strand","Source","Gene_id","Seq_id","Name","Id","Tag","Type","Transcript_id","Subfeatures"]},{"isArrayAttr":{},"attributes":["Start","End","Strand","Source","Gene_id","Seq_id","Name","Id","Exon_id","Type","Transcript_id"]},{"isArrayAttr":{"Subfeature [...]
diff --git a/test/integration/resources/track-data/wormGeneflp-1.json b/test/integration/resources/track-data/wormGeneflp-1.json
new file mode 100644
index 0000000..3329422
--- /dev/null
+++ b/test/integration/resources/track-data/wormGeneflp-1.json
@@ -0,0 +1,1258 @@
+[
+  {
+    "strand": -1,
+    "children": [
+      {
+        "strand": -1,
+        "children": [
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9136932,
+            "type": "exon",
+            "fmax": 9137144,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9136932,
+            "type": "three_prime_UTR",
+            "fmax": 9136982,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "name": "F23B2.4",
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9136982,
+            "type": "CDS",
+            "fmax": 9137144,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9137198,
+            "type": "CDS",
+            "fmax": 9137559,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9137720,
+            "type": "CDS",
+            "fmax": 9137915,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9138008,
+            "type": "CDS",
+            "fmax": 9138100,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9138147,
+            "type": "CDS",
+            "fmax": 9138286,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9138329,
+            "type": "CDS",
+            "fmax": 9138526,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9138575,
+            "type": "CDS",
+            "fmax": 9138747,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9138803,
+            "type": "CDS",
+            "fmax": 9138970,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9140662,
+            "type": "CDS",
+            "fmax": 9140821,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9140888,
+            "type": "CDS",
+            "fmax": 9141042,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9141098,
+            "type": "CDS",
+            "fmax": 9141559,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9141861,
+            "type": "CDS",
+            "fmax": 9141975,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9142278,
+            "type": "CDS",
+            "fmax": 9142513,
+            "seqId": "IV"
+          },
+          {
+            "phase": 2,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9142592,
+            "type": "CDS",
+            "fmax": 9142758,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9143197,
+            "type": "CDS",
+            "fmax": 9143489,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9143594,
+            "type": "CDS",
+            "fmax": 9143814,
+            "seqId": "IV"
+          },
+          {
+            "phase": 2,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9143946,
+            "type": "CDS",
+            "fmax": 9144067,
+            "seqId": "IV"
+          },
+          {
+            "phase": 1,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9144142,
+            "type": "CDS",
+            "fmax": 9144258,
+            "seqId": "IV"
+          },
+          {
+            "phase": 0,
+            "strand": -1,
+            "source": "WormBase",
+            "id": "CDS:F23B2.4",
+            "fmin": 9146140,
+            "type": "CDS",
+            "fmax": 9146196,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9137144,
+            "type": "intron",
+            "fmax": 9137198,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9137198,
+            "type": "exon",
+            "fmax": 9137559,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9137559,
+            "type": "intron",
+            "fmax": 9137720,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9137720,
+            "type": "exon",
+            "fmax": 9137915,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9137915,
+            "type": "intron",
+            "fmax": 9138008,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138008,
+            "type": "exon",
+            "fmax": 9138100,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138100,
+            "type": "intron",
+            "fmax": 9138147,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138147,
+            "type": "exon",
+            "fmax": 9138286,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138286,
+            "type": "intron",
+            "fmax": 9138329,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138329,
+            "type": "exon",
+            "fmax": 9138526,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138526,
+            "type": "intron",
+            "fmax": 9138575,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138575,
+            "type": "exon",
+            "fmax": 9138747,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138747,
+            "type": "intron",
+            "fmax": 9138803,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138803,
+            "type": "exon",
+            "fmax": 9138970,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9138970,
+            "type": "intron",
+            "fmax": 9140662,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9140662,
+            "type": "exon",
+            "fmax": 9140821,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9140821,
+            "type": "intron",
+            "fmax": 9140888,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9140888,
+            "type": "exon",
+            "fmax": 9141042,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9141042,
+            "type": "intron",
+            "fmax": 9141098,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9141098,
+            "type": "exon",
+            "fmax": 9141559,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9141559,
+            "type": "intron",
+            "fmax": 9141861,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9141861,
+            "type": "exon",
+            "fmax": 9141975,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9141975,
+            "type": "intron",
+            "fmax": 9142278,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9142278,
+            "type": "exon",
+            "fmax": 9142513,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9142513,
+            "type": "intron",
+            "fmax": 9142592,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9142592,
+            "type": "exon",
+            "fmax": 9142758,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9142758,
+            "type": "intron",
+            "fmax": 9143197,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9143197,
+            "type": "exon",
+            "fmax": 9143489,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9143489,
+            "type": "intron",
+            "fmax": 9143594,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9143594,
+            "type": "exon",
+            "fmax": 9143814,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9143814,
+            "type": "intron",
+            "fmax": 9143946,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9143946,
+            "type": "exon",
+            "fmax": 9144067,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9144067,
+            "type": "intron",
+            "fmax": 9144142,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9144142,
+            "type": "exon",
+            "fmax": 9144258,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9144258,
+            "type": "intron",
+            "fmax": 9146140,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9146140,
+            "type": "exon",
+            "fmax": 9146210,
+            "seqId": "IV"
+          },
+          {
+            "strand": -1,
+            "source": "WormBase",
+            "fmin": 9146196,
+            "type": "five_prime_UTR",
+            "fmax": 9146210,
+            "seqId": "IV"
+          }
+        ],
+        "name": "F23B2.4",
+        "source": "WormBase",
+        "id": "Transcript:F23B2.4",
+        "fmin": 9136932,
+        "type": "mRNA",
+        "fmax": 9146210,
+        "seqId": "IV"
+      },
+      [
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9137203,
+                  "type": "exon",
+                  "fmax": 9137224,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.21",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.21",
+              "fmin": 9137203,
+              "type": "piRNA",
+              "fmax": 9137224,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00165937",
+          "source": "WormBase",
+          "id": "Gene:WBGene00165937",
+          "fmin": 9137203,
+          "type": "gene",
+          "fmax": 9137224,
+          "seqId": "IV"
+        },
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9139759,
+                  "type": "exon",
+                  "fmax": 9139780,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.29",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.29",
+              "fmin": 9139759,
+              "type": "piRNA",
+              "fmax": 9139780,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00172179",
+          "source": "WormBase",
+          "id": "Gene:WBGene00172179",
+          "fmin": 9139759,
+          "type": "gene",
+          "fmax": 9139780,
+          "seqId": "IV"
+        },
+        {
+          "strand": -1,
+          "children": [
+            {
+              "strand": -1,
+              "children": [
+                {
+                  "strand": -1,
+                  "source": "WormBase",
+                  "fmin": 9139940,
+                  "type": "exon",
+                  "fmax": 9139961,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.22",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.22",
+              "fmin": 9139940,
+              "type": "piRNA",
+              "fmax": 9139961,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00166012",
+          "source": "WormBase",
+          "id": "Gene:WBGene00166012",
+          "fmin": 9139940,
+          "type": "gene",
+          "fmax": 9139961,
+          "seqId": "IV"
+        },
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9140164,
+                  "type": "exon",
+                  "fmax": 9140185,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.24",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.24",
+              "fmin": 9140164,
+              "type": "piRNA",
+              "fmax": 9140185,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00166781",
+          "source": "WormBase",
+          "id": "Gene:WBGene00166781",
+          "fmin": 9140164,
+          "type": "gene",
+          "fmax": 9140185,
+          "seqId": "IV"
+        },
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9140414,
+                  "type": "exon",
+                  "fmax": 9140523,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.15",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.15",
+              "fmin": 9140414,
+              "type": "snRNA",
+              "fmax": 9140523,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00004843",
+          "source": "WormBase",
+          "id": "Gene:WBGene00004843",
+          "fmin": 9140414,
+          "type": "gene",
+          "fmax": 9140523,
+          "seqId": "IV"
+        },
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9141400,
+                  "type": "exon",
+                  "fmax": 9141421,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.26",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.26",
+              "fmin": 9141400,
+              "type": "piRNA",
+              "fmax": 9141421,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00167260",
+          "source": "WormBase",
+          "id": "Gene:WBGene00167260",
+          "fmin": 9141400,
+          "type": "gene",
+          "fmax": 9141421,
+          "seqId": "IV"
+        },
+        {
+          "strand": 1,
+          "children": [
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144691,
+                  "type": "five_prime_UTR",
+                  "fmax": 9144712,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144691,
+                  "type": "exon",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "name": "F23B2.5a",
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9144712,
+                  "type": "CDS",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9144812,
+                  "type": "CDS",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9145348,
+                  "type": "CDS",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9145487,
+                  "type": "CDS",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9145764,
+                  "type": "CDS",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 1,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5a",
+                  "fmin": 9145907,
+                  "type": "CDS",
+                  "fmax": 9145977,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144766,
+                  "type": "intron",
+                  "fmax": 9144812,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144812,
+                  "type": "exon",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144912,
+                  "type": "intron",
+                  "fmax": 9145348,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145348,
+                  "type": "exon",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145435,
+                  "type": "intron",
+                  "fmax": 9145487,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145487,
+                  "type": "exon",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145607,
+                  "type": "intron",
+                  "fmax": 9145764,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145764,
+                  "type": "exon",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145861,
+                  "type": "intron",
+                  "fmax": 9145907,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145907,
+                  "type": "exon",
+                  "fmax": 9146122,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145977,
+                  "type": "three_prime_UTR",
+                  "fmax": 9146122,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.5a",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.5a",
+              "fmin": 9144691,
+              "type": "mRNA",
+              "fmax": 9146122,
+              "seqId": "IV"
+            },
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144704,
+                  "type": "five_prime_UTR",
+                  "fmax": 9144712,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144704,
+                  "type": "exon",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "name": "F23B2.5b",
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9144712,
+                  "type": "CDS",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9144812,
+                  "type": "CDS",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9145348,
+                  "type": "CDS",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9145520,
+                  "type": "CDS",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9145764,
+                  "type": "CDS",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 1,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5b",
+                  "fmin": 9145907,
+                  "type": "CDS",
+                  "fmax": 9145977,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144766,
+                  "type": "intron",
+                  "fmax": 9144812,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144812,
+                  "type": "exon",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144912,
+                  "type": "intron",
+                  "fmax": 9145348,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145348,
+                  "type": "exon",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145435,
+                  "type": "intron",
+                  "fmax": 9145520,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145520,
+                  "type": "exon",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145607,
+                  "type": "intron",
+                  "fmax": 9145764,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145764,
+                  "type": "exon",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145861,
+                  "type": "intron",
+                  "fmax": 9145907,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145907,
+                  "type": "exon",
+                  "fmax": 9146119,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145977,
+                  "type": "three_prime_UTR",
+                  "fmax": 9146119,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.5b",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.5b",
+              "fmin": 9144704,
+              "type": "mRNA",
+              "fmax": 9146119,
+              "seqId": "IV"
+            },
+            {
+              "strand": 1,
+              "children": [
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "name": "F23B2.5c",
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9144712,
+                  "type": "CDS",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 0,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9144812,
+                  "type": "CDS",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9145348,
+                  "type": "CDS",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9145586,
+                  "type": "CDS",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 2,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9145764,
+                  "type": "CDS",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "phase": 1,
+                  "strand": 1,
+                  "source": "WormBase",
+                  "id": "CDS:F23B2.5c",
+                  "fmin": 9145907,
+                  "type": "CDS",
+                  "fmax": 9145977,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144712,
+                  "type": "exon",
+                  "fmax": 9144766,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144766,
+                  "type": "intron",
+                  "fmax": 9144812,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144812,
+                  "type": "exon",
+                  "fmax": 9144912,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9144912,
+                  "type": "intron",
+                  "fmax": 9145348,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145348,
+                  "type": "exon",
+                  "fmax": 9145435,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145435,
+                  "type": "intron",
+                  "fmax": 9145586,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145586,
+                  "type": "exon",
+                  "fmax": 9145607,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145607,
+                  "type": "intron",
+                  "fmax": 9145764,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145764,
+                  "type": "exon",
+                  "fmax": 9145861,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145861,
+                  "type": "intron",
+                  "fmax": 9145907,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145907,
+                  "type": "exon",
+                  "fmax": 9146119,
+                  "seqId": "IV"
+                },
+                {
+                  "strand": 1,
+                  "source": "WormBase",
+                  "fmin": 9145977,
+                  "type": "three_prime_UTR",
+                  "fmax": 9146119,
+                  "seqId": "IV"
+                }
+              ],
+              "name": "F23B2.5c",
+              "source": "WormBase",
+              "id": "Transcript:F23B2.5c",
+              "fmin": 9144712,
+              "type": "mRNA",
+              "fmax": 9146119,
+              "seqId": "IV"
+            }
+          ],
+          "name": "WBGene00001444",
+          "source": "WormBase",
+          "id": "Gene:WBGene00001444",
+          "fmin": 9144691,
+          "type": "gene",
+          "fmax": 9146122,
+          "seqId": "IV"
+        }
+      ]
+    ],
+    "name": "WBGene00000906",
+    "source": "WormBase",
+    "id": "http://localhost:8080/apollo/track/Caenorhabditis elegans/All Genes/IV/WBGene00000906.json",
+    "fmin": 9136932,
+    "type": "gene",
+    "fmax": 9146210,
+    "seqId": "IV"
+  }
+]
\ No newline at end of file
diff --git a/test/unit/org/bbop/apollo/AbstractApolloControllerSpec.groovy b/test/unit/org/bbop/apollo/AbstractApolloControllerSpec.groovy
new file mode 100644
index 0000000..197377c
--- /dev/null
+++ b/test/unit/org/bbop/apollo/AbstractApolloControllerSpec.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions
+ */
+ at TestFor(AbstractApolloController)
+class AbstractApolloControllerSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "test something"() {
+    }
+}
diff --git a/test/unit/org/bbop/apollo/AnnotationEditorServiceSpec.groovy b/test/unit/org/bbop/apollo/AnnotationEditorServiceSpec.groovy
new file mode 100644
index 0000000..cffdd77
--- /dev/null
+++ b/test/unit/org/bbop/apollo/AnnotationEditorServiceSpec.groovy
@@ -0,0 +1,30 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(AnnotationEditorService)
+class AnnotationEditorServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    def "clean json"() {
+        given: "a valid sequence to translate"
+        String inputJSON = "\"{\\\"track\\\":\\\"chrI\\\",\\\"features\\\":[{\\\"location\\\":{\\\"fmin\\\":62840,\\\"fmax\\\":65405,\\\"strand\\\":1},\\\"type\\\":{\\\"cv\\\":{\\\"name\\\":\\\"sequence\\\"},\\\"name\\\":\\\"mRNA\\\"},\\\"name\\\":\\\"YAL041W\\\",\\\"children\\\":[{\\\"location\\\":{\\\"fmin\\\":62840,\\\"fmax\\\":65405,\\\"strand\\\":1},\\\"type\\\":{\\\"cv\\\":{\\\"name\\\":\\\"sequence\\\"},\\\"name\\\":\\\"exon\\\"}}]}],\\\"operation\\\":\\\"add_transcript\\\",\\\"cl [...]
+        String validJSON = "{\"track\":\"chrI\",\"features\":[{\"location\":{\"fmin\":62840,\"fmax\":65405,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"mRNA\"},\"name\":\"YAL041W\",\"children\":[{\"location\":{\"fmin\":62840,\"fmax\":65405,\"strand\":1},\"type\":{\"cv\":{\"name\":\"sequence\"},\"name\":\"exon\"}}]}],\"operation\":\"add_transcript\",\"clientToken\":\"9137872062024864552562677444\"}"
+
+        when: "the sequence string gets here"
+        String outputJSON = service.cleanJSONString(inputJSON)
+        println "OUTOUT: "+outputJSON
+
+        then: "it should be good"
+        assert validJSON == outputJSON
+    }
+}
diff --git a/test/unit/org/bbop/apollo/AvailableStatusControllerSpec.groovy b/test/unit/org/bbop/apollo/AvailableStatusControllerSpec.groovy
new file mode 100644
index 0000000..aefb199
--- /dev/null
+++ b/test/unit/org/bbop/apollo/AvailableStatusControllerSpec.groovy
@@ -0,0 +1,151 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(AvailableStatusController)
+ at Mock([AvailableStatus,AvailableStatusOrganismFilter])
+class AvailableStatusControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        params["value"] = 'Needs Review'
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.availableStatusInstanceList
+            model.availableStatusInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.availableStatusInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def availableStatus = new AvailableStatus()
+            availableStatus.validate()
+            controller.save(availableStatus)
+
+        then:"The create view is rendered again with the correct model"
+            model.availableStatusInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            availableStatus = new AvailableStatus(params)
+
+            controller.save(availableStatus)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/availableStatus/show/1'
+            controller.flash.message != null
+            AvailableStatus.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def availableStatus = new AvailableStatus(params)
+            controller.show(availableStatus)
+
+        then:"A model is populated containing the domain instance"
+            model.availableStatusInstance == availableStatus
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def availableStatus = new AvailableStatus(params)
+            controller.edit(availableStatus)
+
+        then:"A model is populated containing the domain instance"
+            model.availableStatusInstance == availableStatus
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/availableStatus/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def availableStatus = new AvailableStatus()
+            availableStatus.validate()
+            controller.update(availableStatus)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.availableStatusInstance == availableStatus
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            availableStatus = new AvailableStatus(params).save(flush: true)
+            controller.update(availableStatus)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/availableStatus/show/$availableStatus.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/availableStatus/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def availableStatus = new AvailableStatus(params).save(flush: true)
+
+        then:"It exists"
+            AvailableStatus.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(availableStatus)
+
+        then:"The instance is deleted"
+            AvailableStatus.count() == 0
+            response.redirectedUrl == '/availableStatus/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/CannedCommentControllerSpec.groovy b/test/unit/org/bbop/apollo/CannedCommentControllerSpec.groovy
new file mode 100644
index 0000000..35fe084
--- /dev/null
+++ b/test/unit/org/bbop/apollo/CannedCommentControllerSpec.groovy
@@ -0,0 +1,151 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(CannedCommentController)
+ at Mock([CannedComment,CannedCommentOrganismFilter])
+class CannedCommentControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        params["comment"] = 'something genetically interesting'
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.cannedCommentInstanceList
+            model.cannedCommentInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.cannedCommentInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def cannedComment = new CannedComment()
+            cannedComment.validate()
+            controller.save(cannedComment)
+
+        then:"The create view is rendered again with the correct model"
+            model.cannedCommentInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            cannedComment = new CannedComment(params)
+
+            controller.save(cannedComment)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/cannedComment/show/1'
+            controller.flash.message != null
+            CannedComment.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def cannedComment = new CannedComment(params)
+            controller.show(cannedComment)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedCommentInstance == cannedComment
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def cannedComment = new CannedComment(params)
+            controller.edit(cannedComment)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedCommentInstance == cannedComment
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/cannedComment/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def cannedComment = new CannedComment()
+            cannedComment.validate()
+            controller.update(cannedComment)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.cannedCommentInstance == cannedComment
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            cannedComment = new CannedComment(params).save(flush: true)
+            controller.update(cannedComment)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/cannedComment/show/$cannedComment.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/cannedComment/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def cannedComment = new CannedComment(params).save(flush: true)
+
+        then:"It exists"
+            CannedComment.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(cannedComment)
+
+        then:"The instance is deleted"
+            CannedComment.count() == 0
+            response.redirectedUrl == '/cannedComment/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/CannedKeyControllerSpec.groovy b/test/unit/org/bbop/apollo/CannedKeyControllerSpec.groovy
new file mode 100644
index 0000000..4c5b807
--- /dev/null
+++ b/test/unit/org/bbop/apollo/CannedKeyControllerSpec.groovy
@@ -0,0 +1,152 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(CannedKeyController)
+ at Mock([CannedKey,CannedKeyOrganismFilter])
+class CannedKeyControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        // TODO: Populate valid properties like...
+        params["label"] = 'someValidName'
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.cannedKeyInstanceList
+            model.cannedKeyInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.cannedKeyInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def cannedKey = new CannedKey()
+            cannedKey.validate()
+            controller.save(cannedKey)
+
+        then:"The create view is rendered again with the correct model"
+            model.cannedKeyInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            cannedKey = new CannedKey(params)
+
+            controller.save(cannedKey)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/cannedKey/show/1'
+            controller.flash.message != null
+            CannedKey.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def cannedKey = new CannedKey(params)
+            controller.show(cannedKey)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedKeyInstance == cannedKey
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def cannedKey = new CannedKey(params)
+            controller.edit(cannedKey)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedKeyInstance == cannedKey
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/cannedKey/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def cannedKey = new CannedKey()
+            cannedKey.validate()
+            controller.update(cannedKey)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.cannedKeyInstance == cannedKey
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            cannedKey = new CannedKey(params).save(flush: true)
+            controller.update(cannedKey)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/cannedKey/show/$cannedKey.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/cannedKey/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def cannedKey = new CannedKey(params).save(flush: true)
+
+        then:"It exists"
+            CannedKey.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(cannedKey)
+
+        then:"The instance is deleted"
+            CannedKey.count() == 0
+            response.redirectedUrl == '/cannedKey/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/CannedValueControllerSpec.groovy b/test/unit/org/bbop/apollo/CannedValueControllerSpec.groovy
new file mode 100644
index 0000000..68a974d
--- /dev/null
+++ b/test/unit/org/bbop/apollo/CannedValueControllerSpec.groovy
@@ -0,0 +1,152 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(CannedValueController)
+ at Mock([CannedValue,CannedValueOrganismFilter])
+class CannedValueControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        // TODO: Populate valid properties like...
+        params["label"] = 'someValidName'
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.cannedValueInstanceList
+            model.cannedValueInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.cannedValueInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def cannedValue = new CannedValue()
+            cannedValue.validate()
+            controller.save(cannedValue)
+
+        then:"The create view is rendered again with the correct model"
+            model.cannedValueInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            cannedValue = new CannedValue(params)
+
+            controller.save(cannedValue)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/cannedValue/show/1'
+            controller.flash.message != null
+            CannedValue.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def cannedValue = new CannedValue(params)
+            controller.show(cannedValue)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedValueInstance == cannedValue
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def cannedValue = new CannedValue(params)
+            controller.edit(cannedValue)
+
+        then:"A model is populated containing the domain instance"
+            model.cannedValueInstance == cannedValue
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/cannedValue/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def cannedValue = new CannedValue()
+            cannedValue.validate()
+            controller.update(cannedValue)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.cannedValueInstance == cannedValue
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            cannedValue = new CannedValue(params).save(flush: true)
+            controller.update(cannedValue)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/cannedValue/show/$cannedValue.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/cannedValue/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def cannedValue = new CannedValue(params).save(flush: true)
+
+        then:"It exists"
+            CannedValue.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(cannedValue)
+
+        then:"The instance is deleted"
+            CannedValue.count() == 0
+            response.redirectedUrl == '/cannedValue/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/ChadoHandlerServiceSpec.groovy b/test/unit/org/bbop/apollo/ChadoHandlerServiceSpec.groovy
new file mode 100644
index 0000000..0b66fa0
--- /dev/null
+++ b/test/unit/org/bbop/apollo/ChadoHandlerServiceSpec.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(ChadoHandlerService)
+class ChadoHandlerServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "test something"() {
+    }
+}
diff --git a/test/unit/org/bbop/apollo/FeatureEventControllerSpec.groovy b/test/unit/org/bbop/apollo/FeatureEventControllerSpec.groovy
new file mode 100644
index 0000000..68110be
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureEventControllerSpec.groovy
@@ -0,0 +1,156 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import org.bbop.apollo.history.FeatureOperation
+import spock.lang.*
+
+ at TestFor(FeatureEventController)
+ at Mock(FeatureEvent)
+class FeatureEventControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        // TODO: Populate valid properties like...
+        params["name"] = 'someValidName'
+        params["uniqueName"] = 'someValidNameUniqueName'
+        params["operation"] = FeatureOperation.ADD_EXON
+        params["current"] = true
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.featureEventInstanceList
+            model.featureEventInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.featureEventInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def featureEvent = new FeatureEvent()
+            featureEvent.validate()
+            controller.save(featureEvent)
+
+        then:"The create view is rendered again with the correct model"
+            model.featureEventInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            featureEvent = new FeatureEvent(params)
+
+            controller.save(featureEvent)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/featureEvent/show/1'
+            controller.flash.message != null
+            FeatureEvent.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def featureEvent = new FeatureEvent(params)
+            controller.show(featureEvent)
+
+        then:"A model is populated containing the domain instance"
+            model.featureEventInstance == featureEvent
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def featureEvent = new FeatureEvent(params)
+            controller.edit(featureEvent)
+
+        then:"A model is populated containing the domain instance"
+            model.featureEventInstance == featureEvent
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/featureEvent/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def featureEvent = new FeatureEvent()
+            featureEvent.validate()
+            controller.update(featureEvent)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.featureEventInstance == featureEvent
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            featureEvent = new FeatureEvent(params).save(flush: true)
+            controller.update(featureEvent)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/featureEvent/show/$featureEvent.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/featureEvent/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def featureEvent = new FeatureEvent(params).save(flush: true)
+
+        then:"It exists"
+            FeatureEvent.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(featureEvent)
+
+        then:"The instance is deleted"
+            FeatureEvent.count() == 0
+            response.redirectedUrl == '/featureEvent/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/FeatureEventServiceSpec.groovy b/test/unit/org/bbop/apollo/FeatureEventServiceSpec.groovy
new file mode 100644
index 0000000..81c12fe
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureEventServiceSpec.groovy
@@ -0,0 +1,1201 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import org.bbop.apollo.history.FeatureOperation
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Ignore
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(FeatureEventService)
+ at Mock([FeatureEvent])
+class FeatureEventServiceSpec extends Specification {
+
+    Date today = new Date()
+    String classUniqueName = "uniqueName"
+
+    // create 5 FeatureEvents
+    def setup() {
+        FeatureEvent f1 = new FeatureEvent(operation: FeatureOperation.ADD_FEATURE, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 7, current: false).save(failOnError: true)
+        FeatureEvent f2 = new FeatureEvent(operation: FeatureOperation.SPLIT_TRANSCRIPT, parentId: f1.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 6, current: false).save(failOnError: true)
+        f1.childId = f2.id
+        FeatureEvent f3 = new FeatureEvent(operation: FeatureOperation.SET_TRANSLATION_END, parentId: f2.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 5, current: false).save(failOnError: true)
+        f2.childId = f3.id
+        FeatureEvent f4 = new FeatureEvent(operation: FeatureOperation.SET_READTHROUGH_STOP_CODON, parentId: f3.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 4, current: false).save(failOnError: true)
+        f3.childId = f4.id
+        FeatureEvent f5 = new FeatureEvent(operation: FeatureOperation.SET_BOUNDARIES, parentId: f4.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 3, current: true).save(failOnError: true)
+        f4.childId = f5.id
+        FeatureEvent f6 = new FeatureEvent(operation: FeatureOperation.ADD_EXON, parentId: f5.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 2, current: false).save(failOnError: true)
+        f5.childId = f6.id
+        FeatureEvent f7 = new FeatureEvent(operation: FeatureOperation.MERGE_TRANSCRIPTS, parentId: f6.id, name: "Gene123", uniqueName: classUniqueName, dateCreated: today - 1, current: false).save(failOnError: true)
+        f1.save()
+        f2.save()
+        f3.save()
+        f4.save()
+        f5.save()
+        f6.save()
+        f7.save()
+    }
+
+    def cleanup() {
+        FeatureEvent.deleteAll(FeatureEvent.all)
+    }
+
+    @Ignore
+    void "make sure we sort okay for previous events from most current"() {
+        when: "we query the past events"
+        FeatureEvent featureEvent = FeatureEvent.findByUniqueName(classUniqueName, [sort: "dateCreated", order: "desc", max: 1, offset: 1])
+        then: "we should see an add_exon event"
+        assert FeatureEvent.count == 7
+        assert featureEvent.operation == FeatureOperation.ADD_EXON
+        when: "we query the last event"
+        featureEvent = FeatureEvent.findByUniqueName(classUniqueName, [sort: "dateCreated", order: "desc", max: 1, offset: 0])
+        then: "we should see merge transct"
+        assert featureEvent.operation == FeatureOperation.MERGE_TRANSCRIPTS
+    }
+
+    void "make sure we sort okay for future events from the last "() {
+        when: "we query the past events"
+        FeatureEvent featureEvent = FeatureEvent.findByUniqueName(classUniqueName, [sort: "dateCreated", order: "asc", max: 1, offset: 1])
+        then: "we should see split transcript event"
+        assert FeatureEvent.count == 7
+        assert featureEvent.operation == FeatureOperation.SPLIT_TRANSCRIPT
+        when: "we query the first event"
+        featureEvent = FeatureEvent.findByUniqueName(classUniqueName, [sort: "dateCreated", order: "asc", max: 1, offset: 0])
+        then: "we should see add feature"
+        assert featureEvent.operation == FeatureOperation.ADD_FEATURE
+    }
+
+    void "lets get the current index"() {
+        when: "we have multiple feature events"
+        FeatureEvent f4 = new FeatureEvent(
+                operation: FeatureOperation.ADD_FEATURE
+                , name: "Gene123"
+                , uniqueName: "AAAA"
+                , current: false
+                , dateCreated: new Date() - 1
+        ).save()
+        FeatureEvent f3 = new FeatureEvent(
+                operation: FeatureOperation.ADD_TRANSCRIPT
+                , name: "Gene123"
+                , uniqueName: "AAAA"
+                , childId: f4.id
+                , current: false
+                , dateCreated: new Date() - 2
+        ).save()
+        FeatureEvent f2 = new FeatureEvent(
+                operation: FeatureOperation.SPLIT_TRANSCRIPT
+                , name: "Gene123"
+                , uniqueName: "AAAA"
+                , childId: f3.id
+                , current: true
+                , dateCreated: new Date() - 3
+        ).save()
+        // this is the first one!
+        FeatureEvent f1 = new FeatureEvent(
+                operation: FeatureOperation.MERGE_TRANSCRIPTS
+                , name: "Gene123"
+                , childId: f2.id
+                , uniqueName: "AAAA"
+                , current: false
+                , dateCreated: new Date() - 4
+        ).save()
+        f4.parentId = f3.id
+        f4.save()
+        f3.parentId = f2.id
+        f3.save()
+        f2.parentId = f1.id
+        f2.save()
+
+        List<FeatureEvent> mostRecentFeatureEventList = FeatureEvent.findAllByUniqueName("AAAA", [sort: "dateCreated", order: "asc"])
+        List<FeatureEvent> currentFeatureEventList = FeatureEvent.findAllByUniqueNameAndCurrent("AAAA", true, [sort: "dateCreated", order: "asc"])
+
+
+        then: "we should have 4 valid events"
+        assert FeatureEvent.countByUniqueName("AAAA") == 4
+        assert mostRecentFeatureEventList.size() == 4
+        assert mostRecentFeatureEventList.get(0).operation == FeatureOperation.ADD_FEATURE
+        assert currentFeatureEventList.size() == 1
+        assert currentFeatureEventList.get(0).operation == FeatureOperation.SPLIT_TRANSCRIPT
+
+        when: "we find the current index"
+        int currentIndex = service.getCurrentFeatureEventIndex("AAAA")
+
+        then: "it should match the current index"
+        assert currentIndex == 1
+
+    }
+
+    void "adding feature events using tree-style feature-events"() {
+
+        given: "a transcript with a unique name"
+        String name = "sox9a-0001"
+        String uniqueName = "abc123"
+
+        when: "we add a feature event"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name, uniqueName, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList = service.getHistory(uniqueName)
+
+        then: "we should see a feature event"
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName)
+        assert featureEventList.size() == 1
+
+
+        when: "we add another feature event"
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name, uniqueName, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList = service.getHistory(uniqueName)
+
+        then: "we should see two feature events, with the second one current and the prior one before"
+        assert featureEventList.size() == 2
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName)
+        assert featureEventList[1][0].current
+        assert featureEventList[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert !featureEventList[0][0].current
+        assert featureEventList[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+        when: "we add a third feature event"
+        service.addNewFeatureEvent(FeatureOperation.SET_TRANSLATION_START, name, uniqueName, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList = service.getHistory(uniqueName)
+
+        then: "we should see three feature events, with the third one current and the prior two before"
+        assert featureEventList.size() == 3
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName)
+
+        assert featureEventList[2][0].operation == FeatureOperation.SET_TRANSLATION_START
+        assert featureEventList[2][0].current
+        assert featureEventList[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert !featureEventList[1][0].current
+        assert featureEventList[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert !featureEventList[0][0].current
+
+        when: "if we make the second one current"
+        service.setTransactionForFeature(uniqueName, 1)
+        featureEventList = service.getHistory(uniqueName)
+
+        then: "we should see one in front and one behind"
+        assert featureEventList.size() == 3
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName)
+        assert featureEventList[2][0].operation == FeatureOperation.SET_TRANSLATION_START
+        assert !featureEventList[2][0].current
+        assert featureEventList[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert featureEventList[1][0].current
+        assert featureEventList[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert !featureEventList[0][0].current
+
+        when: "we add another feature event"
+        service.addNewFeatureEvent(FeatureOperation.SPLIT_EXON, name, uniqueName, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList = service.getHistory(uniqueName)
+
+
+        then: "the last one disappears"
+        assert featureEventList.size() == 3
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName)
+        assert featureEventList[2][0].operation == FeatureOperation.SPLIT_EXON
+        assert featureEventList[2][0].current
+        assert featureEventList[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert !featureEventList[1][0].current
+        assert featureEventList[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert !featureEventList[0][0].current
+
+        when: "we set the first one current"
+        service.setTransactionForFeature(uniqueName, 0)
+        assert 1 == FeatureEvent.countByUniqueNameAndCurrent(uniqueName, true)
+
+        featureEventList = service.getHistory(uniqueName)
+
+        then: "the first one will be current"
+        assert featureEventList.size() == 3
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName)
+        assert featureEventList[2][0].operation == FeatureOperation.SPLIT_EXON
+        assert !featureEventList[2][0].current
+        assert featureEventList[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert !featureEventList[1][0].current
+        assert featureEventList[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList[0][0].current
+
+    }
+
+    void "feature events with splits can be undone"() {
+
+        given: "add 1 transcripts"
+        String name1 = "sox9a-0001"
+        String name2 = "sox9b-0001"
+        String uniqueName1 = "aaaaaa"
+        String uniqueName2 = "bbbbbb"
+
+        when: "we add a feature event"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList1 = service.getHistory(uniqueName1)
+
+        then: "we should see a feature event"
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 1
+
+        when: "we do an operation"
+        service.addNewFeatureEvent(FeatureOperation.SET_TRANSLATION_ENDS, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList1 = service.getHistory(uniqueName1)
+
+        then: "we should see an extra operation"
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 2
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[1][0].current
+
+        when: "let's split this feature event!"
+        JSONArray newJsonArray = new JSONArray()
+        newJsonArray.add(new JSONObject())
+        newJsonArray.add(new JSONObject())
+        service.addSplitFeatureEvent(name1, uniqueName1, name2, uniqueName2, new JSONObject(), new JSONObject(), newJsonArray, null)
+        featureEventList1 = service.getHistory(uniqueName1)
+        List<List<FeatureEvent>> featureEventList2 = service.getHistory(uniqueName2)
+        FeatureEvent currentFeature = service.findCurrentFeatureEvent(uniqueName2)[0]
+        List<List<FeatureEvent>> previousEvents = service.findPreviousFeatureEvents(currentFeature)
+        List<List<FeatureEvent>> futureEvents = service.findFutureFeatureEvents(currentFeature)
+
+        then: "we should see two feature events, with the second one current and the prior one before"
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert featureEventList1.size() == 3
+        assert featureEventList2.size() == 3
+
+        assert 0 == futureEvents.size()
+        assert 2 == previousEvents.size()
+        assert previousEvents.get(0).first().operation == FeatureOperation.ADD_TRANSCRIPT
+        assert previousEvents.get(1).first().operation == FeatureOperation.SET_TRANSLATION_ENDS
+
+        assert featureEventList2.size() == 3
+        assert 3 == service.getHistory(uniqueName1).size()
+        assert 3 == service.getHistory(uniqueName2).size()
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert !featureEventList1[1][0].current
+        assert featureEventList1[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+        assert featureEventList2[2][0].current
+        assert featureEventList2[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert !featureEventList2[1][0].current
+        assert featureEventList2[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert !featureEventList2[0][0].current
+        assert featureEventList2[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+        when: "we add another event to 2"
+        service.addNewFeatureEvent(FeatureOperation.FLIP_STRAND, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        futureEvents = service.findFutureFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0])
+        previousEvents = service.findPreviousFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0])
+
+        then: "we have 2 on 2"
+        assert 0 == futureEvents.size()
+        assert 2 == previousEvents.size()
+
+        when: "we test the other sizde"
+        def currentFeatureEvent2 = service.findCurrentFeatureEvent(uniqueName2)
+        def currentFeatureEvent1 = service.findCurrentFeatureEvent(uniqueName1)
+        futureEvents = service.findFutureFeatureEvents(currentFeatureEvent2[0])
+        previousEvents = service.findPreviousFeatureEvents(currentFeatureEvent2[0])
+
+        then: "we have 3 on 1"
+        assert currentFeatureEvent1 == currentFeatureEvent2
+        assert currentFeatureEvent2[0].uniqueName == uniqueName1
+        assert currentFeatureEvent2[1].uniqueName == uniqueName2
+        assert 0 == futureEvents.size()
+        assert 2 == previousEvents.size()
+        assert 4 == featureEventList1.size()
+        assert 4 == featureEventList2.size()
+
+        when: "we test the second index"
+        futureEvents = service.findFutureFeatureEvents(currentFeatureEvent2[1])
+        previousEvents = service.findPreviousFeatureEvents(currentFeatureEvent2[1])
+
+        then: "we have 3 on 1"
+        assert currentFeatureEvent1 == currentFeatureEvent2
+        assert 0 == futureEvents.size()
+        assert 3 == previousEvents.size()
+
+        assert featureEventList1 == featureEventList2
+
+        assert featureEventList2[0].size() == 1
+        assert !featureEventList2[0][0].current
+        assert featureEventList2[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+        assert featureEventList2[1].size() == 1
+        assert !featureEventList2[1][0].current
+        assert featureEventList2[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+
+        assert featureEventList2[2].size() == 2
+        assert featureEventList2[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert featureEventList2[2][1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert ((featureEventList2[2][0].current && !featureEventList2[2][1].current) || (!featureEventList2[2][0].current && featureEventList2[2][1].current))
+
+        assert featureEventList2[3].size() == 1
+        assert featureEventList2[3][0].current
+        assert featureEventList2[3][0].operation == FeatureOperation.FLIP_STRAND
+
+        // note: if we revert to 0 . . it disappears!
+        when: "when we revert 2 back on transcript 2 to setting exon boundaries"
+        FeatureEvent newActiveFeatureEvent = service.setTransactionForFeature(uniqueName2, 1)[0]
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        List<FeatureEvent> thisCurrentFeatureEvents = service.findCurrentFeatureEvent(uniqueName1)
+        previousEvents = service.findPreviousFeatureEvents(thisCurrentFeatureEvents)
+        futureEvents = service.findFutureFeatureEvents(thisCurrentFeatureEvents)
+
+        then: "we have 3 on 1"
+        assert thisCurrentFeatureEvents.size() == 1
+        assert thisCurrentFeatureEvents[0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert service.findCurrentFeatureEvent(uniqueName2)[0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert 2 == futureEvents.size()
+        assert 1 == previousEvents.size()
+
+        assert previousEvents[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert futureEvents[0][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert futureEvents[0][1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert futureEvents[1][0].operation == FeatureOperation.FLIP_STRAND
+
+        assert 4 == featureEventList1.size()  // we can fast-forward all the way up through 2 and the split
+        assert 4 == featureEventList2.size()
+
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList1[0][0].uniqueName == uniqueName1
+
+        assert featureEventList1[1][0].current
+        assert featureEventList1[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert featureEventList1[1][0].uniqueName == uniqueName1
+
+        assert !featureEventList1[2][0].current
+        assert !featureEventList1[2][1].current
+        assert featureEventList1[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert featureEventList1[2][1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+
+        assert !featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+
+
+        when: "we go forward on 1 (2 does not exist anymore unless we go forward)"
+        List<FeatureEvent> currentFeatureEvents = service.setTransactionForFeature(uniqueName1, 2)
+        assert currentFeatureEvents.size() == 2
+        assert currentFeatureEvents[0].current
+        assert currentFeatureEvents[1].current
+        assert currentFeatureEvents[0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert currentFeatureEvents[1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        def currentFeatureEvents1 = service.findCurrentFeatureEvent(uniqueName1)
+        def currentFeatureEvents2 = service.findCurrentFeatureEvent(uniqueName2)
+        def currentFeatureEventUniqueName1 = currentFeatureEvents1.find(){ it.uniqueName == uniqueName1}
+        def currentFeatureEventUniqueName2 = currentFeatureEvents2.find(){ it.uniqueName == uniqueName2}
+//        currentFeatureEvents = service.findCurrentFeatureEvent(uniqueName2)
+//        int uniqueName1Index = currentFeatureEvents[0].uniqueName == uniqueName1 ? 0 : 1
+
+
+        then: "since 2 is further then 1, it should stop on the most recent for both"
+        assert 2 == currentFeatureEvents1.size()
+        assert 2 == currentFeatureEvents2.size()
+        assert currentFeatureEvents1==currentFeatureEvents2
+        assert uniqueName1 == currentFeatureEventUniqueName1.uniqueName
+        assert uniqueName2 == currentFeatureEventUniqueName2.uniqueName
+
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList1[0][0].uniqueName == uniqueName1
+
+        assert !featureEventList1[1][0].current
+        assert featureEventList1[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+        assert featureEventList1[1][0].uniqueName == uniqueName1
+
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][1].current
+        assert featureEventList1[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert featureEventList1[2][1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+
+        assert !featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+
+        assert 0 == service.findFutureFeatureEvents(currentFeatureEventUniqueName1).size()
+        assert 2 == service.findPreviousFeatureEvents(currentFeatureEventUniqueName1).size()
+        assert 1 == service.findFutureFeatureEvents(currentFeatureEventUniqueName2).size()
+        assert 2 == service.findPreviousFeatureEvents(currentFeatureEventUniqueName2).size()
+
+        assert 4 == featureEventList1.size()
+        assert 4 == featureEventList2.size()
+        // note: I am uniqueName2 split explicitly (by default everywhere else I'm grabbing uniqueName1)
+        assert featureEventList1 == featureEventList2
+
+        when: "we go all the way forward on 2"
+        def current = service.setTransactionForFeature(uniqueName2, 3)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        currentFeatureEvents = service.findCurrentFeatureEvent(uniqueName2)
+        currentFeatureEvent1 = null
+        currentFeatureEvent2 = null
+        currentFeatureEvent1 = currentFeatureEvents.find(){ it.uniqueName==uniqueName1}
+        currentFeatureEvent2 = currentFeatureEvents.find(){ it.uniqueName==uniqueName2}
+
+
+        then: "no change on 1, 2 goes to flip strand"
+        assert currentFeatureEvents.size()==2
+        assert currentFeatureEvent1.uniqueName == uniqueName1
+        assert currentFeatureEvent2.uniqueName == uniqueName2
+        assert 0 == service.findFutureFeatureEvents(currentFeatureEvent1).size()
+        assert 2 == service.findPreviousFeatureEvents(currentFeatureEvent1).size()
+        assert 0 == service.findFutureFeatureEvents(currentFeatureEvent2).size()
+        assert 3 == service.findPreviousFeatureEvents(currentFeatureEvent2).size()
+
+        assert 4 == featureEventList1.size()
+        assert 4 == featureEventList2.size()
+
+        assert featureEventList1 == featureEventList2
+        assert !featureEventList2[0][0].current
+        assert featureEventList2[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert !featureEventList2[1][0].current
+        assert featureEventList2[1][0].operation == FeatureOperation.SET_TRANSLATION_ENDS
+
+        assert featureEventList2[2][0].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert featureEventList2[2][1].operation == FeatureOperation.SPLIT_TRANSCRIPT
+        assert ((featureEventList2[2][0].current && !featureEventList2[2][1].current) || (!featureEventList2[2][0].current && featureEventList2[2][1].current))
+
+        assert featureEventList2[3][0].current
+        assert featureEventList2[3][0].operation == FeatureOperation.FLIP_STRAND
+
+    }
+
+    void "feature events with merges can be undone"() {
+
+        given: "add 2 transcripts"
+        String name1 = "sox9a-0001"
+        String name2 = "sox9b-0001"
+        String uniqueName1 = "aaaaaa"
+        String uniqueName2 = "bbbbbb"
+
+        when: "we add 2 feature events"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList1 = service.getHistory(uniqueName1)
+        List<List<FeatureEvent>> featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "we should see a feature event"
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 1
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert featureEventList2.size() == 1
+
+        when: "we do an operation"
+        service.addNewFeatureEvent(FeatureOperation.SET_TRANSLATION_ENDS, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList1 = service.getHistory(uniqueName1)
+
+        then: "we should see an extra operation"
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 2
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[1][0].current
+
+        when: "let's merge feature events"
+        JSONArray oldJsonArray = new JSONArray()
+//        newJsonArray.add(new JSONObject())
+        oldJsonArray.add(new JSONObject())
+        service.addMergeFeatureEvent(name1, uniqueName1, name2, uniqueName2, new JSONObject(), oldJsonArray, new JSONObject(), null)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        // TODO: not sure if this is accurate, or possible
+//        FeatureEvent currentFeature = service.findCurrentFeatureEvent(uniqueName2)[0]
+        List<FeatureEvent> currentFeatureEvents = service.findCurrentFeatureEvent(uniqueName1)
+        FeatureEvent currentFeature = currentFeatureEvents[0]
+
+        then: "we should see one feature events, with the second one current and the prior one before"
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert featureEventList1.size() == 3
+        assert featureEventList1 == featureEventList2
+
+        assert 0 == service.findFutureFeatureEvents(currentFeature).size()
+        assert 2 == service.findPreviousFeatureEvents(currentFeature).size()
+
+        assert featureEventList1==featureEventList2
+        assert 3 == service.getHistory(uniqueName1).size()
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_TRANSLATION_ENDS
+        }
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        when: "we add another event to 1 (2 no longer is accessible)"
+        service.addNewFeatureEvent(FeatureOperation.FLIP_STRAND, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "we have 3 on 1 and 2 on 2"
+        assert 0 == service.findFutureFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 3 == service.findPreviousFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 4 == featureEventList1.size()
+
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_TRANSLATION_ENDS
+        }
+        assert !featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+
+        // note: if we revert to 0 . . it disappears!
+        when: "when we revert 2 back on transcript 2 to setting exon boundaries"
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "it should be active on the split transcript event for both"
+        assert 4 == featureEventList1.size()  // we can fast-forward all the way up through 2 and the split
+        assert featureEventList1==featureEventList2
+
+        assert featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+        assert !featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_TRANSLATION_ENDS
+        }
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert featureEventList1[0][0].uniqueName == uniqueName1
+
+
+        when: "we go forward on 1 we should re-merge, goes beyond 2, so just stops at end"
+        currentFeatureEvents = service.setTransactionForFeature(uniqueName1, 2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+
+
+        then: "since 2 is further then 1, it should stop on the most recent for both, but one disappears"
+        assert featureEventList1.size() == 4
+        assert featureEventList1==featureEventList2
+        assert currentFeatureEvents.size() == 1
+        assert currentFeatureEvents[0].current
+        assert 1 == service.findFutureFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 2 == service.findPreviousFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 4 == featureEventList1.size()
+
+
+        assert !featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_TRANSLATION_ENDS
+        }
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+
+        when: "we go all the way forward on 2"
+        currentFeatureEvents = service.setTransactionForFeature(uniqueName1, 3)
+        featureEventList1 = service.getHistory(uniqueName1)
+
+
+        then: "no change on 1, 2 goes to flip strand"
+        assert featureEventList1.size() == 4
+        assert featureEventList1==featureEventList2
+        assert currentFeatureEvents.size() == 1
+        assert currentFeatureEvents[0].current
+        assert 0 == service.findFutureFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 3 == service.findPreviousFeatureEvents(service.findCurrentFeatureEvent(uniqueName1)[0]).size()
+        assert 4 == featureEventList1.size()
+
+
+        assert featureEventList1[3][0].current
+        assert featureEventList1[3][0].operation == FeatureOperation.FLIP_STRAND
+        assert !featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_TRANSLATION_ENDS
+        }
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+    }
+
+    void "undo merge after an action on the other side"() {
+
+        given: "add 2 transcripts"
+        String name1 = "sox9a-0001"
+        String name2 = "sox9b-0001"
+        String uniqueName1 = "aaaaaa"
+        String uniqueName2 = "bbbbbb"
+
+        when: "we add 2 feature events"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList1 = service.getHistory(uniqueName1)
+        List<List<FeatureEvent>> featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "we should see a feature event"
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 1
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert featureEventList2.size() == 1
+
+        when: "we do an operation"
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "we should see an extra operation"
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 1
+        assert featureEventList1[0][0].current
+        assert featureEventList2.size() == 2
+        assert !featureEventList2[0][0].current
+        assert featureEventList2[1][0].current
+
+        when: "let's merge feature events"
+        JSONArray oldJsonArray = new JSONArray()
+        oldJsonArray.add(new JSONObject())
+        service.addMergeFeatureEvent(name1, uniqueName1, name2, uniqueName2, new JSONObject(), oldJsonArray, new JSONObject(), null)
+        Integer featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        Integer featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = service.extractFeatureEventGroup(uniqueName1)
+        List<FeatureEvent> featureEventList = FeatureEvent.findAllByUniqueNameAndCurrent(uniqueName1, true)
+        FeatureEvent currentFeature = featureEventList.first()
+
+        List<List<FeatureEvent>> previousFeatureEvents = service.findPreviousFeatureEvents(currentFeature)
+
+        then: "check our data structures"
+        assert featureIndex1 == 2
+        assert featureIndex2 == 2
+        assert featureEventList.size() == 1
+        assert featureEventMap.size() == 2
+        assert featureEventMap.values().first().size() == 2
+        assert featureEventMap.values().last().size() == 2
+        assert previousFeatureEvents.size() == 2
+
+        when: "we get the current feature event"
+        List<FeatureEvent> currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+
+        then: "it should not be null"
+        // this hsould return
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 1
+        assert currentFeatureEventArray.first().operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+        when: "we get the histories"
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        List<List<FeatureEvent>> futureEvents = service.findFutureFeatureEvents(currentFeatureEventArray)
+        List<List<FeatureEvent>> previousEvents = service.findPreviousFeatureEvents(currentFeatureEventArray)
+
+        then: "we should see one feature events, with the second one current and the prior one before"
+        assert 2 == featureIndex1
+        assert 2 == featureIndex2
+        assert featureEventList1.size() == 3
+        assert featureEventList1==featureEventList2
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 0 == futureEvents.size()
+        assert 2 == previousEvents.size()
+        assert previousEvents[0].size() == 1
+        assert previousEvents[1].size() == 2
+
+        assert featureEventList1[2].size() == 1
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert !fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_EXON_BOUNDARIES
+        }
+
+
+        assert featureEventList1[0].size() == 1
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+        when: "when we undo one (current index is 2)"
+        service.setTransactionForFeature(uniqueName1, 1)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+
+        then: "we should get both back as current, with set exon boundary"
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        assert currentFeatureEventArray.last().operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert currentFeatureEventArray.first().operation == FeatureOperation.ADD_TRANSCRIPT
+
+        when: "we get the histories"
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        FeatureEvent addTranscriptFeatureEvent = currentFeatureEventArray.first()
+        FeatureEvent exonBoundariesFeatureEvent = currentFeatureEventArray.last()
+        futureEvents = service.findFutureFeatureEvents(exonBoundariesFeatureEvent)
+        previousEvents = service.findPreviousFeatureEvents(exonBoundariesFeatureEvent)
+
+        then: "we verify that we are at the right place"
+        assert addTranscriptFeatureEvent.operation == FeatureOperation.ADD_TRANSCRIPT
+        assert addTranscriptFeatureEvent.uniqueName == uniqueName1
+        assert exonBoundariesFeatureEvent.operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert exonBoundariesFeatureEvent.uniqueName == uniqueName2
+        assert 1 == featureIndex1
+        assert 1 == featureIndex2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 1
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert featureEventList2.size() == 3 // this captures only the parts in its own history
+        assert featureEventList2[0].size() == 1
+        assert featureEventList2[1].size() == 2
+        assert featureEventList2[2].size() == 1
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 1 == futureEvents.size()
+        assert 1 == previousEvents.size()
+        assert previousEvents[0].size() == 1
+        assert futureEvents[0].size() == 1
+
+        assert 3 == service.getHistory(uniqueName2).size()
+        assert 3 == service.getHistory(uniqueName1).size()
+
+        assert featureEventList1[2].size() == 1
+        assert !featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            assert fe.current
+            assert fe.operation == FeatureOperation.ADD_TRANSCRIPT || fe.operation == FeatureOperation.SET_EXON_BOUNDARIES
+        }
+
+
+        assert featureEventList1[0].size() == 1
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+
+
+
+
+        when: "when we undo again (current index is 1)"
+        service.setTransactionForFeature(uniqueName2, 0)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName2)
+        FeatureEvent featureEvent1 = currentFeatureEventArray.find(){ it.uniqueName==uniqueName1}
+        FeatureEvent featureEvent2 = currentFeatureEventArray.find(){ it.uniqueName==uniqueName2}
+
+        then: "we should get both back as current, with set exon boundary"
+        assert featureEvent1.uniqueName==uniqueName1
+        assert featureEvent2.uniqueName==uniqueName2
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        for(fe in currentFeatureEventArray){
+            assert fe.operation==FeatureOperation.ADD_TRANSCRIPT
+            assert fe.current
+//            fe.operation==FeatureOperation.SET_EXON_BOUNDARIES
+        }
+//        assert featureEvent2.operation == FeatureOperation.ADD_TRANSCRIPT
+//        assert featureEvent2.uniqueName == uniqueName2
+
+
+        when: "we get the histories"
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        futureEvents = service.findFutureFeatureEvents(featureEvent2)
+        previousEvents = service.findPreviousFeatureEvents(featureEvent2)
+
+        then: "we verify that we are at the right place"
+//        assert currentFeatureEventArray.first().operation == FeatureOperation.ADD_TRANSCRIPT
+//        assert currentFeatureEventArray.first().uniqueName == uniqueName2
+        assert 1 == featureIndex1
+        assert 0 == featureIndex2
+        assert featureEventList1==featureEventList2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 1
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 2 == futureEvents.size()
+        assert 0 == previousEvents.size()
+        assert futureEvents[0].size() == 1 // this ignores the history of the other until we remerge
+        assert futureEvents[1].size() == 1
+
+
+        assert featureEventList1[2].size() == 1
+        assert !featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        assert featureEventList1[1].size() == 2
+        for (fe in featureEventList1[1]) {
+            if(!fe.current){
+                assert fe.operation==FeatureOperation.SET_EXON_BOUNDARIES
+            }
+            else{
+                assert fe.operation==FeatureOperation.ADD_TRANSCRIPT
+            }
+        }
+
+
+        assert featureEventList1[0].size() == 1
+        assert featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+    }
+
+    void "undo merges independently after merge on symmetric tree"() {
+
+        given: "add 2 transcripts"
+        String name1 = "sox9a-0001"
+        String name2 = "sox9b-0001"
+        String uniqueName1 = "aaaaaa"
+        String uniqueName2 = "bbbbbb"
+
+        when: "we add 2 feature events"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList1 = service.getHistory(uniqueName1)
+        List<List<FeatureEvent>> featureEventList2 = service.getHistory(uniqueName2)
+
+        then: "we should see a feature event"
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert featureEventList1.size() == 1
+        assert 1 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert featureEventList2.size() == 1
+
+        when: "we we add exons to each and merge them"
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        JSONArray oldJsonArray = new JSONArray()
+        oldJsonArray.add(new JSONObject())
+        service.addMergeFeatureEvent(name2, uniqueName2, name1, uniqueName1, new JSONObject(), oldJsonArray, new JSONObject(), null)
+        Integer featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        Integer featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+
+        Map<String, Map<Long, FeatureEvent>> featureEventMap = service.extractFeatureEventGroup(uniqueName1)
+        List<FeatureEvent> featureEventList = FeatureEvent.findAllByUniqueNameAndCurrent(uniqueName2, true)
+        FeatureEvent currentFeature = featureEventList.first()
+
+        then: "check our data structures"
+        assert featureIndex1 == 2
+        assert featureIndex2 == 2
+        assert featureEventList.size() == 1
+        assert featureEventMap.size() == 2
+
+        when: "we get the current feature event"
+        List<FeatureEvent> currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+
+        then: "it should not be null"
+        // this should return
+        assert currentFeatureEventArray.size() == 1
+        assert currentFeatureEventArray.first().operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+        when: "we get the histories"
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        List<List<FeatureEvent>> futureEvents = service.findFutureFeatureEvents(currentFeatureEventArray.first())
+        List<List<FeatureEvent>> previousEvents = service.findPreviousFeatureEvents(currentFeatureEventArray.first())
+
+        then: "we should see one feature events, with the second one current and the prior one before"
+        assert currentFeatureEventArray.first()
+        assert 2 == featureIndex1
+        assert 2 == featureIndex2
+        assert featureEventList1.size() == 3
+        assert featureEventList1==featureEventList2
+
+        assert featureEventList1[2].size() == 1
+        assert featureEventList1[2][0].current
+        assert featureEventList1[2][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert featureEventList1[1][1].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert !featureEventList1[1][0].current
+        assert !featureEventList1[1][1].current
+
+        assert featureEventList1[0].size() == 2
+        assert !featureEventList1[0][0].current
+        assert featureEventList1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert !featureEventList1[0][1].current
+        assert featureEventList1[0][1].operation == FeatureOperation.ADD_TRANSCRIPT
+
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName2)
+
+        assert 0 == futureEvents.size()
+        assert 2 == previousEvents.size()
+        assert previousEvents[0].size() == 2
+        assert previousEvents[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert previousEvents[0][1].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert previousEvents[1].size() == 2
+        assert previousEvents[1][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert previousEvents[1][1].operation == FeatureOperation.SET_EXON_BOUNDARIES
+
+
+        when: "when we undo merge on A2B2 (current index is 2)"
+        service.setTransactionForFeature(uniqueName1, 1)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        // just evaluate one side of this
+        futureEvents = service.findFutureFeatureEvents(currentFeatureEventArray.first())
+        previousEvents = service.findPreviousFeatureEvents(currentFeatureEventArray.first())
+
+        then: "we should get both back as current, with set exon boundary (A2B2)"
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        assert currentFeatureEventArray.last().operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert currentFeatureEventArray.first().operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert featureEventList1==featureEventList2
+
+        then: "we verify that we are at the right place"
+        assert 1 == featureIndex1
+        assert 1 == featureIndex2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 2
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert featureEventList1.size() == 3 // this captures only the parts in its own history
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 1 == futureEvents.size()
+        assert 1 == previousEvents.size()
+        assert previousEvents[0].size() == 1
+        assert futureEvents[0].size() == 1
+        assert previousEvents[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert futureEvents[0][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+
+        when: "when we undo again (current index is 1) (A2B1)"
+        service.setTransactionForFeature(uniqueName2, 0)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        // just evaluate one side of this
+        FeatureEvent fe1 = currentFeatureEventArray.find(){ it.uniqueName == uniqueName1}
+        FeatureEvent fe2 = currentFeatureEventArray.find(){ it.uniqueName == uniqueName2}
+        List<List<FeatureEvent>> futureEvents2 = service.findFutureFeatureEvents(fe2)
+        List<List<FeatureEvent>> previousEvents2 = service.findPreviousFeatureEvents(fe2)
+        List<List<FeatureEvent>> futureEvents1 = service.findFutureFeatureEvents(fe1)
+        List<List<FeatureEvent>> previousEvents1 = service.findPreviousFeatureEvents(fe1)
+
+        then: "we should get both back as current, with set exon boundary (A2B2)"
+        assert featureEventList1==featureEventList2
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        assert fe2.operation == FeatureOperation.ADD_TRANSCRIPT
+        assert fe1.operation == FeatureOperation.SET_EXON_BOUNDARIES
+
+        then: "we verify that we are at the right place WRT 2"
+        assert 1 == featureIndex1
+        assert 0 == featureIndex2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 2
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert featureEventList1.size() == 3 // this captures only the parts in its own history
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == futureEvents2.size()
+        assert 0 == previousEvents2.size()
+        assert futureEvents2[0].size() == 1
+        assert futureEvents2[1].size() == 1
+        assert futureEvents2[0][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert futureEvents2[1][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert 1 == futureEvents1.size()
+        assert 1 == previousEvents1.size()
+        assert futureEvents1[0].size() == 1
+        assert previousEvents1[0].size() == 1
+        assert previousEvents1[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert futureEvents1[0][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+        when: "when we undo again (current index is 1) (A1B1)"
+        service.setTransactionForFeature(uniqueName1, 0)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        // just evaluate one side of this
+        fe1 = currentFeatureEventArray.find(){ it.uniqueName == uniqueName1}
+        fe2 = currentFeatureEventArray.find(){ it.uniqueName == uniqueName2}
+        futureEvents2 = service.findFutureFeatureEvents(fe2)
+        previousEvents2 = service.findPreviousFeatureEvents(fe2)
+        futureEvents1 = service.findFutureFeatureEvents(fe1)
+        previousEvents1 = service.findPreviousFeatureEvents(fe1)
+
+        then: "we should get both back as current, with set exon boundary (A2B2)"
+        assert featureEventList1==featureEventList2
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        assert fe2.operation == FeatureOperation.ADD_TRANSCRIPT
+        assert fe1.operation == FeatureOperation.ADD_TRANSCRIPT
+
+        then: "we verify that we are at the right place"
+        assert 0 == featureIndex1
+        assert 0 == featureIndex2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 2
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert featureEventList1.size() == 3 // this captures only the parts in its own history
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == futureEvents1.size()
+        assert 0 == previousEvents1.size()
+        assert 2 == futureEvents2.size()
+        assert 0 == previousEvents2.size()
+        assert futureEvents1[0].size() == 1
+        assert futureEvents1[1].size() == 1
+        assert futureEvents1[0][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert futureEvents1[1][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+        assert futureEvents2[0].size() == 1
+        assert futureEvents2[1].size() == 1
+        assert futureEvents2[0][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert futureEvents2[1][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+    }
+
+    void "undo merges independently after merge on symmetric tree with different current indices"() {
+
+        given: "add 2 transcripts"
+        String name1 = "sox9a-0001"
+        String name2 = "sox9b-0001"
+        String uniqueName1 = "aaaaaa"
+        String uniqueName2 = "bbbbbb"
+
+        when: "we add 2 feature events"
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.ADD_TRANSCRIPT, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        List<List<FeatureEvent>> featureEventList1 = service.getHistory(uniqueName1)
+        List<List<FeatureEvent>> featureEventList2 = service.getHistory(uniqueName2)
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name2, uniqueName2, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        service.addNewFeatureEvent(FeatureOperation.SET_EXON_BOUNDARIES, name1, uniqueName1, new JSONObject(), new JSONObject(), new JSONObject(), null)
+        JSONArray oldJsonArray = new JSONArray()
+        oldJsonArray.add(new JSONObject())
+        service.addMergeFeatureEvent(name2, uniqueName2, name1, uniqueName1, new JSONObject(), oldJsonArray, new JSONObject(), null)
+        Integer featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        Integer featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        List<FeatureEvent> currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+
+        then: "it should not be null"
+        // this should return
+        assert currentFeatureEventArray.size() == 1
+        assert currentFeatureEventArray.first().operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+
+        when: "when we undo merge to get to A1, B2 so that they will have separate indices"
+        service.setTransactionForFeature(uniqueName1, 0)
+        service.setTransactionForFeature(uniqueName2, 1)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName1)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        FeatureEvent currentFeatureEvent1 = currentFeatureEventArray.find(){it.uniqueName==uniqueName1}
+        FeatureEvent currentFeatureEvent2 = currentFeatureEventArray.find(){it.uniqueName==uniqueName2}
+        // just evaluate one side of this
+        List<List<FeatureEvent>> futureEvents1 = service.findFutureFeatureEvents(currentFeatureEvent1)
+        List<List<FeatureEvent>> previousEvents1 = service.findPreviousFeatureEvents(currentFeatureEvent1)
+        List<List<FeatureEvent>> futureEvents2 = service.findFutureFeatureEvents(currentFeatureEvent2)
+        List<List<FeatureEvent>> previousEvents2 = service.findPreviousFeatureEvents(currentFeatureEvent2)
+
+        then: "we should get both back as current, with set exon boundary (A2B2)"
+        assert currentFeatureEventArray != null
+        assert currentFeatureEventArray.size() == 2
+        assert currentFeatureEvent1.operation == FeatureOperation.ADD_TRANSCRIPT
+        assert currentFeatureEvent2.operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert featureEventList1==featureEventList2
+
+        then: "we verify that we are at the right place"
+        assert 0 == featureIndex1
+        assert 1 == featureIndex2
+        assert featureEventList1.size() == 3 // this captures everything
+        assert featureEventList1[0].size() == 2
+        assert featureEventList1[1].size() == 2
+        assert featureEventList1[2].size() == 1
+        assert 2 == FeatureEvent.countByUniqueName(uniqueName1)
+        assert 3 == FeatureEvent.countByUniqueName(uniqueName2)
+        assert 2 == futureEvents1.size()
+        assert 0 == previousEvents1.size()
+        assert 1 == futureEvents2.size()
+        assert 1 == previousEvents2.size()
+
+        assert futureEvents1[0].size() == 1
+        assert futureEvents1[1].size() == 1
+        assert futureEvents1[0][0].operation == FeatureOperation.SET_EXON_BOUNDARIES
+        assert futureEvents1[1][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+        assert futureEvents2[0].size() == 1
+        assert previousEvents2[0].size() == 1
+        assert previousEvents2[0][0].operation == FeatureOperation.ADD_TRANSCRIPT
+        assert futureEvents2[0][0].operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+
+        when: "when we redo on A1 so A1 B2 -> AB"
+        service.setTransactionForFeature(uniqueName1, 2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        currentFeatureEventArray = service.findCurrentFeatureEvent(uniqueName2)
+        featureEventList1 = service.getHistory(uniqueName1)
+        featureEventList2 = service.getHistory(uniqueName2)
+        featureIndex1 = service.getCurrentFeatureEventIndex(uniqueName1)
+        featureIndex2 = service.getCurrentFeatureEventIndex(uniqueName2)
+        // just evaluate one side of this
+//        currentFeatureEvent1 =currentFeatureEventArray.find(){ it.uniqueName == uniqueName1}
+        currentFeatureEvent2= currentFeatureEventArray.find(){ it.uniqueName == uniqueName2}
+        futureEvents2 = service.findFutureFeatureEvents(currentFeatureEvent2)
+        previousEvents2 = service.findPreviousFeatureEvents(currentFeatureEvent2)
+//        futureEvents1 = service.findFutureFeatureEvents(currentFeatureEvent1)
+//        previousEvents1 = service.findPreviousFeatureEvents(currentFeatureEvent1)
+
+        then: "this should force a merge (AB)"
+        assert featureIndex1==2
+        assert featureIndex2==2
+        assert currentFeatureEventArray.size() == 1
+        assert currentFeatureEventArray.first().uniqueName==uniqueName2
+//        assert 0 == futureEvents1.size()
+//        assert 2 == previousEvents1.size()
+        assert 0 == futureEvents2.size()
+        assert 2 == previousEvents2.size()
+        assert featureEventList1==featureEventList2
+        assert currentFeatureEvent2.operation == FeatureOperation.MERGE_TRANSCRIPTS
+
+    }
+
+}
diff --git a/test/unit/org/bbop/apollo/FeatureRelationshipServiceSpec.groovy b/test/unit/org/bbop/apollo/FeatureRelationshipServiceSpec.groovy
new file mode 100644
index 0000000..a4eebe7
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureRelationshipServiceSpec.groovy
@@ -0,0 +1,71 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(FeatureRelationshipService)
+ at Mock([FeatureRelationship,Feature,Gene,MRNA])
+class FeatureRelationshipServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "parents for feature"() {
+        when: "A feature has parents"
+        Gene gene = new Gene(
+                name: "Gene1"
+                ,uniqueName: "Gene1"
+        ).save(failOnError: true)
+        MRNA mrna = new MRNA(
+                name: "MRNA"
+                ,uniqueName: "MRNA"
+        ).save(failOnError: true)
+        FeatureRelationship fr=new FeatureRelationship(
+                parentFeature: gene
+                , childFeature: mrna
+        ).save(failOnError: true)
+        mrna.addToChildFeatureRelationships(fr)
+        gene.addToParentFeatureRelationships(fr)
+        then: "it should have parents"
+        assert FeatureRelationship.count==1
+        List<Feature> parents = service.getParentsForFeature(mrna,Gene.ontologyId)
+        assert parents.size() ==1
+        Feature gene2 = parents.get(0)
+        assert gene == gene2
+
+        List<Feature> children = service.getChildrenForFeatureAndTypes(gene,MRNA.ontologyId)
+        assert children.size() ==1
+        Feature mrna2= children.get(0)
+        assert mrna == mrna2
+
+        when: "we get a single parent for an ontology id"
+        Feature parent = service.getParentForFeature(mrna,Gene.ontologyId)
+        
+        then: "we should find a valid parent"
+        assert parent !=null
+
+        when: "we get a single parent for NO ontology id"
+        Feature parent2 = service.getParentForFeature(mrna)
+
+        then: "we should *STILL* find a valid parent"
+        assert parent2 !=null
+
+        // NOTE: can not test hql queries
+//        when: "we delete a relationshp"
+//        service.removeFeatureRelationship(gene,mrna)
+//        parents = service.getParentsForFeature(mrna,Gene.ontologyId)
+//        children = service.getChildrenForFeatureAndTypes(gene,MRNA.ontologyId)
+//
+//        then: "they should both exist, but not be related"
+//        assert parents.size() ==0
+//        assert children.size() == 0
+//        assert FeatureRelationship.count==0
+    }
+}
diff --git a/test/unit/org/bbop/apollo/FeatureServiceSpec.groovy b/test/unit/org/bbop/apollo/FeatureServiceSpec.groovy
new file mode 100644
index 0000000..ba0e047
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureServiceSpec.groovy
@@ -0,0 +1,64 @@
+package org.bbop.apollo
+
+import grails.converters.JSON
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import org.bbop.apollo.gwt.shared.FeatureStringEnum
+import org.bbop.apollo.sequence.Strand
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(FeatureService)
+ at Mock([Sequence, FeatureLocation, Feature,MRNA])
+class FeatureServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "convert JSON to Feature Location"() {
+
+        when: "We have a valid json object"
+        JSONObject jsonObject = new JSONObject()
+        Sequence sequence = new Sequence(
+                name: "Chr3",
+                seqChunkSize: 20,
+                start: 1,
+                end: 100,
+                length: 99,
+        ).save(failOnError: true)
+        jsonObject.put(FeatureStringEnum.FMIN.value, 73)
+        jsonObject.put(FeatureStringEnum.FMAX.value, 113)
+        jsonObject.put(FeatureStringEnum.STRAND.value, Strand.POSITIVE.value)
+
+
+        then: "We should return a valid FeatureLocation"
+        FeatureLocation featureLocation = service.convertJSONToFeatureLocation(jsonObject, sequence)
+        assert featureLocation.sequence.name == "Chr3"
+        assert featureLocation.fmin == 73
+        assert featureLocation.fmax == 113
+        assert featureLocation.strand == Strand.POSITIVE.value
+
+
+    }
+
+    void "convert JSON to Ontology ID"() {
+        when: "We hav a json object of type"
+        JSONObject json = JSON.parse("{name:exon, cv:{name:sequence}}")
+
+        then: "We should be able to infer the ontology ID"
+        String ontologyId = service.convertJSONToOntologyId(json)
+        assert ontologyId != null
+        assert ontologyId == Exon.ontologyId
+    }
+
+
+    
+    
+    
+}
diff --git a/test/unit/org/bbop/apollo/FeatureSpec.groovy b/test/unit/org/bbop/apollo/FeatureSpec.groovy
new file mode 100644
index 0000000..516d605
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureSpec.groovy
@@ -0,0 +1,130 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
+ */
+ at TestFor(Feature)
+ at Mock([Feature,FeatureLocation,Sequence])
+class FeatureSpec extends Specification {
+
+    def setup() {
+        Sequence sequence = new Sequence(
+                name: "Chr1"
+                ,start: 1
+                ,end: 1013
+                ,length: 1013
+                ,seqChunkSize: 50
+        ).save(failOnError: true)
+
+
+        Feature feature1 = new Feature(
+                name: "Sox9a"
+                ,uniqueName: "ABC123"
+                ,sequenceLength: 17
+        ).save(failOnError: true)
+
+        FeatureLocation featureLocation = new FeatureLocation(
+                fmin: 13
+                ,fmax: 77
+                ,feature: feature1
+                ,sequence: sequence
+        ).save()
+
+        feature1.addToFeatureLocations(featureLocation)
+        feature1.save()
+    }
+
+    def cleanup() {
+    }
+
+    void "test feature manual copy"() {
+        
+        when: "If I clone a feature"
+        Feature feature1 = Feature.first()
+        Feature feature2 = feature1.properties
+        feature2.save()
+       
+        then: "It should be identical in all properties but the id and uniquename and relationships"
+        assert Feature.count == 2
+        assert FeatureLocation.count == 1
+        assert Sequence.count == 1
+        
+
+        assert feature1.name==feature2.name
+        assert feature1.uniqueName ==feature2.uniqueName
+        assert feature1.featureLocations.size() == feature2.featureLocations.size()
+        assert feature1.featureLocations.size() == 1
+       
+        FeatureLocation featureLocation1 = feature1.featureLocation
+        FeatureLocation featureLocation2 = feature2.featureLocation
+
+        assert featureLocation1.fmin == featureLocation2.fmin
+        assert featureLocation1.sequence == featureLocation2.sequence
+        assert featureLocation1.fmax == featureLocation2.fmax
+    }
+
+    void "test feature clone copy"() {
+
+        when: "If I clone a feature"
+        Feature feature1 = Feature.first()
+        Feature feature2 = feature1.generateClone()
+        feature2.save()
+
+        then: "It should be identical in all properties but the id and uniquename and relationships"
+        assert Feature.count == 2
+        assert FeatureLocation.count == 1
+        assert Sequence.count == 1
+
+
+        assert feature1.name==feature2.name
+        assert feature1.uniqueName ==feature2.uniqueName
+        assert feature1.featureLocations.size() == feature2.featureLocations.size()
+        assert feature1.featureLocations.size() == 1
+
+        FeatureLocation featureLocation1 = feature1.featureLocation
+        FeatureLocation featureLocation2 = feature2.featureLocation
+
+        assert featureLocation1.fmin == featureLocation2.fmin
+        assert featureLocation1.sequence == featureLocation2.sequence
+        assert featureLocation1.fmax == featureLocation2.fmax
+    }
+
+    void "can I insert a feature with the same id?"(){
+        when: "I create a feature"
+        Feature feature = Feature.first()
+        Long id = feature.id
+
+        then: "should be a total of one valid feature"
+        assert id!=null
+        assert Feature.count == 1
+        assert feature != null
+        assert feature.name == "Sox9a"
+
+        when: "we delete that feature "
+        feature.delete()
+
+        then: "we have no feaures"
+        assert Feature.count == 0
+
+        when: "we create a feature when the same id"
+        Feature feature1 = new Feature(
+                name: "Sox9a"
+                ,uniqueName: "ABC123"
+                ,sequenceLength: 17
+        )
+        // NOTE: this has to be out here
+        feature1.id = id
+        feature1.save(failOnError: true)
+
+        then: "should all be the same"
+        assert feature1.id == id
+        assert Feature.count == 1
+        assert feature1.name == "Sox9a"
+        assert feature != null
+
+    }
+}
diff --git a/test/unit/org/bbop/apollo/FeatureTypeControllerSpec.groovy b/test/unit/org/bbop/apollo/FeatureTypeControllerSpec.groovy
new file mode 100644
index 0000000..1bead32
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureTypeControllerSpec.groovy
@@ -0,0 +1,155 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(FeatureTypeController)
+ at Mock([FeatureType,MRNA])
+class FeatureTypeControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        // TODO: Populate valid properties like...
+        params["name"] = 'sequence'
+        params["display"] = MRNA.alternateCvTerm
+        params["type"] = MRNA.cvTerm
+        params["ontologyId"] = MRNA.ontologyId
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.featureTypeInstanceList
+            model.featureTypeInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.featureTypeInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def featureType = new FeatureType()
+            featureType.validate()
+            controller.save(featureType)
+
+        then:"The create view is rendered again with the correct model"
+            model.featureTypeInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            featureType = new FeatureType(params)
+
+            controller.save(featureType)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/featureType/show/1'
+            controller.flash.message != null
+            FeatureType.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def featureType = new FeatureType(params)
+            controller.show(featureType)
+
+        then:"A model is populated containing the domain instance"
+            model.featureTypeInstance == featureType
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def featureType = new FeatureType(params)
+            controller.edit(featureType)
+
+        then:"A model is populated containing the domain instance"
+            model.featureTypeInstance == featureType
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/featureType/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def featureType = new FeatureType()
+            featureType.validate()
+            controller.update(featureType)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.featureTypeInstance == featureType
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            featureType = new FeatureType(params).save(flush: true)
+            controller.update(featureType)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/featureType/show/$featureType.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/featureType/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def featureType = new FeatureType(params).save(flush: true)
+
+        then:"It exists"
+            FeatureType.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(featureType)
+
+        then:"The instance is deleted"
+            FeatureType.count() == 0
+            response.redirectedUrl == '/featureType/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/FeatureTypeServiceSpec.groovy b/test/unit/org/bbop/apollo/FeatureTypeServiceSpec.groovy
new file mode 100644
index 0000000..a9bf258
--- /dev/null
+++ b/test/unit/org/bbop/apollo/FeatureTypeServiceSpec.groovy
@@ -0,0 +1,36 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import groovy.mock.interceptor.MockFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(FeatureTypeService)
+ at Mock([Feature,FeatureType])
+class FeatureTypeServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "can add a feature type"() {
+
+        given: "no feature types"
+        assert FeatureType.count==0
+
+        when: "we add a Feature Type"
+        service.createFeatureTypeForFeature(Gene.class,Gene.alternateCvTerm)
+        FeatureType featureType = FeatureType.first()
+
+        then: "we should have one"
+        assert FeatureType.count==1
+        assert featureType.ontologyId == Gene.ontologyId
+        assert featureType.name == Gene.cvTerm
+        assert featureType.type == "sequence"
+    }
+}
diff --git a/test/unit/org/bbop/apollo/Gff3HandlerServiceSpec.groovy b/test/unit/org/bbop/apollo/Gff3HandlerServiceSpec.groovy
new file mode 100644
index 0000000..2eca865
--- /dev/null
+++ b/test/unit/org/bbop/apollo/Gff3HandlerServiceSpec.groovy
@@ -0,0 +1,48 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.support.GrailsUnitTestMixin} for usage instructions
+ */
+ at TestFor(Gff3HandlerService)
+ at Mock([Sequence,Gene,MRNA,Exon,CDS,Feature,FeatureLocation,FeatureRelationship,FeatureRelationshipService ])
+class Gff3HandlerServiceSpec extends Specification {
+    
+
+    def setup() {
+        new Sequence(
+                length: 3
+                ,seqChunkSize: 3
+                ,start: 5
+                ,end: 8
+                ,name: "Group-1.10"
+        ).save()
+    }
+
+    def cleanup() {
+        Sequence.deleteAll(Sequence.all)
+        FeatureRelationship.deleteAll(FeatureRelationship.all)
+        FeatureLocation.deleteAll(FeatureLocation.all)
+        Feature.deleteAll(Feature.all)
+    }
+ 
+
+    void "test new date format"(){
+        given: "a date and expected result"
+        String expectedOutput1 = "2001-02-03"
+
+        when: "we format the string"
+        Calendar calendar = new GregorianCalendar()
+        calendar.set(Calendar.YEAR,2001)
+        calendar.set(Calendar.MONTH,1) // for February
+        calendar.set(Calendar.DAY_OF_MONTH,3)
+
+        String outputDateString = service.formatDate(calendar.time)
+
+        then: "we should be able to"
+        assert expectedOutput1 == outputDateString
+    }
+}
diff --git a/test/unit/org/bbop/apollo/JbrowseServiceSpec.groovy b/test/unit/org/bbop/apollo/JbrowseServiceSpec.groovy
new file mode 100644
index 0000000..fea69cb
--- /dev/null
+++ b/test/unit/org/bbop/apollo/JbrowseServiceSpec.groovy
@@ -0,0 +1,44 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(JbrowseService)
+class JbrowseServiceSpec extends Specification {
+
+    String path1 = "yeast/include/myTrackMetaData.csv"
+    String path2 = "/opt/apollo/yeast/"
+    String path2a = "/opt/apollo/yeast"
+    String path3 = "/opt/apollo/"
+    String path4 = "/opt/apollo/critter"
+    String finalPath = "/opt/apollo/yeast/include/myTrackMetaData.csv"
+
+    def setup() {
+
+    }
+
+    def cleanup() {
+    }
+
+    void "test overlapping paths"() {
+
+        when: "we try to do overlapping"
+
+        then: "assert behaviors"
+        assert service.hasOverlappingDirectory(path2,path1)
+        assert service.hasOverlappingDirectory(path2a,path1)
+        assert !service.hasOverlappingDirectory(path3,path1)
+        assert !service.hasOverlappingDirectory(path4,path1)
+    }
+
+    void "test fixing paths"() {
+        when: "we try to do overlapping"
+
+        then: "assert behaviors"
+        assert finalPath == service.fixOverlappingPath(path2,path1)
+        assert finalPath == service.fixOverlappingPath(path2a,path1)
+    }
+}
diff --git a/test/unit/org/bbop/apollo/NameServiceSpec.groovy b/test/unit/org/bbop/apollo/NameServiceSpec.groovy
new file mode 100644
index 0000000..cee62cd
--- /dev/null
+++ b/test/unit/org/bbop/apollo/NameServiceSpec.groovy
@@ -0,0 +1,29 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(NameService)
+class NameServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "letter padding strategy should work"() {
+
+        when: "we have 1"
+        LetterPaddingStrategy letterPaddingStrategy = new LetterPaddingStrategy()
+
+        then: "assert a"
+        assert "a" == letterPaddingStrategy.pad(0)
+        assert "b" == letterPaddingStrategy.pad(1)
+        assert "c" == letterPaddingStrategy.pad(2)
+
+    }
+}
diff --git a/test/unit/org/bbop/apollo/PermissionServiceSpec.groovy b/test/unit/org/bbop/apollo/PermissionServiceSpec.groovy
new file mode 100644
index 0000000..aa3730b
--- /dev/null
+++ b/test/unit/org/bbop/apollo/PermissionServiceSpec.groovy
@@ -0,0 +1,139 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import org.bbop.apollo.gwt.shared.PermissionEnum
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(PermissionService)
+ at Mock([User,Organism,UserGroup,GroupOrganismPermission,UserOrganismPermission,UserTrackPermission,UserOrganismPermission,GroupTrackPermission])
+class PermissionServiceSpec extends Specification {
+
+    def setup() {
+        
+        User bobUser = new User(
+                username: 'bob at bob.com'
+                ,firstName: "Bob"
+                ,lastName: "Jones"
+                ,passwordHash: "asdfasdf"
+        ).save()
+        User user2 = new User(
+                username: 'test at test.com'
+                ,firstName: "Test"
+                ,lastName: "Case"
+                ,passwordHash: "asdfasdf"
+        ).save()
+
+        UserGroup userGroup = new UserGroup(
+                name: 'WorkGroup'
+        ).save()
+        userGroup.addToUsers(bobUser)
+        bobUser.addToUserGroups(userGroup)
+
+        userGroup.addToUsers(user2)
+        user2.addToUserGroups(userGroup)
+        
+        Organism organism = new Organism(
+                commonName: "Honeybee"
+                ,directory:  "test/integration/resources/sequences/honeybee-Group1.10/"
+        ).save()
+    }
+
+    def cleanup() {
+    }
+
+    void "merge organism permissions"() {
+        
+        given: "a list of permissions"
+        List<PermissionEnum> permissionEnums1 = new ArrayList<>()
+        List<PermissionEnum> permissionEnums2 = new ArrayList<>()
+        List<PermissionEnum> permissionEnums3 = new ArrayList<>()
+        
+        when: "we populate them"
+        permissionEnums1.add(PermissionEnum.READ)
+        permissionEnums1.add(PermissionEnum.ADMINISTRATE)
+        permissionEnums1.add(PermissionEnum.WRITE)
+        permissionEnums2.add(PermissionEnum.READ)
+        permissionEnums2.add(PermissionEnum.EXPORT)
+        permissionEnums3.add(PermissionEnum.READ)
+        
+        then: "we should get the right stuff back"
+        assert permissionEnums1.size()==3
+        assert permissionEnums2.size()==2
+        assert permissionEnums3.size()==1
+
+        when: "we merge 1 and 2"
+        Collection<PermissionEnum> combo1 = service.mergeOrganismPermissions(permissionEnums1,permissionEnums2)
+        
+        
+        then: "we should merged appropriate"
+        assert combo1.size()==4
+        assert combo1.contains(PermissionEnum.READ)
+        assert combo1.contains(PermissionEnum.ADMINISTRATE)
+        assert combo1.contains(PermissionEnum.EXPORT)
+        assert combo1.contains(PermissionEnum.WRITE)
+
+        when: "we add the simple 1"
+        Collection<PermissionEnum> combo2 = service.mergeOrganismPermissions(permissionEnums2,permissionEnums3)
+        
+        then: "should merge properly"
+        assert combo2.size()==2
+        assert combo2.contains(PermissionEnum.READ)
+        assert combo2.contains(PermissionEnum.EXPORT)
+    }
+
+    
+    void "set organism permissions "(){
+
+        given: "a user, organism, and group"
+        User user = User.first()
+        Organism organism = Organism.first()
+        UserGroup group = UserGroup.first()
+
+
+        when: "we add permissions to a user"
+        List<PermissionEnum> permissionEnumList2 = new ArrayList<>()
+        permissionEnumList2.add(PermissionEnum.ADMINISTRATE)
+        permissionEnumList2.add(PermissionEnum.READ)
+        permissionEnumList2.add(PermissionEnum.WRITE)
+        service.setOrganismPermissionsForUser(permissionEnumList2,organism,user,"123123")
+        List<PermissionEnum> userPermissionEnumsReceived2 = service.getOrganismPermissionsForUser(organism,user)
+
+        then: "we should see the same come back "
+        assert 3==userPermissionEnumsReceived2.size()
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.ADMINISTRATE)
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.WRITE)
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.READ)
+
+
+        when: "we add permission to a group"
+        List<PermissionEnum> permissionEnumList1 = new ArrayList<>()
+        permissionEnumList1.add(PermissionEnum.READ)
+        permissionEnumList1.add(PermissionEnum.EXPORT)
+        service.setOrganismPermissionsForUserGroup(permissionEnumList1,organism,group,"123123")
+        List<PermissionEnum> userPermissionEnumsReceived1 = service.getOrganismPermissionsForUserGroup(organism,group)
+        userPermissionEnumsReceived2 = service.getOrganismPermissionsForUser(organism,user)
+        List<PermissionEnum> userPermissionEnumsReceived3 = service.getOrganismPermissionsForUser(organism,User.all.get(1))
+
+        then: "we should get back the same for group, and combined for user"
+        assert 2==userPermissionEnumsReceived1.size()
+        assert userPermissionEnumsReceived1.contains(PermissionEnum.READ)
+        assert userPermissionEnumsReceived1.contains(PermissionEnum.EXPORT)
+
+        assert 4==userPermissionEnumsReceived2.size()
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.ADMINISTRATE)
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.WRITE)
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.READ)
+        assert userPermissionEnumsReceived2.contains(PermissionEnum.EXPORT)
+
+        assert 2==userPermissionEnumsReceived3.size()
+        assert userPermissionEnumsReceived3.contains(PermissionEnum.READ)
+        assert userPermissionEnumsReceived3.contains(PermissionEnum.EXPORT)
+
+
+
+    }
+}
diff --git a/test/unit/org/bbop/apollo/ProjectionSpec.groovy b/test/unit/org/bbop/apollo/ProjectionSpec.groovy
new file mode 100644
index 0000000..4d3722c
--- /dev/null
+++ b/test/unit/org/bbop/apollo/ProjectionSpec.groovy
@@ -0,0 +1,187 @@
+package org.bbop.apollo
+
+import org.bbop.apollo.projection.AbstractProjection
+import org.bbop.apollo.projection.DiscontinuousProjection
+import org.bbop.apollo.projection.DiscontinuousProjectionFactory
+import org.bbop.apollo.projection.DuplicateTrackProjection
+
+import org.bbop.apollo.projection.Projection
+import org.bbop.apollo.projection.ReverseProjection
+import org.bbop.apollo.projection.Track
+import spock.lang.Specification
+
+/**
+ * Created by nathandunn on 8/14/15.
+ */
+class ProjectionSpec extends Specification{
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "confirm that we can generate a duplicate projection"() {
+
+        given:
+        Track track1 = new Track()
+
+        when: "we generate a duplicate projection"
+        track1.addCoordinate(4,12)
+        track1.addCoordinate(70,80)
+        Projection projectionTrack1To2 = new DuplicateTrackProjection()
+        Track track2 = projectionTrack1To2.projectTrack(track1)
+
+        then: "it should generate forward "
+        assert track1.equals(track2)
+
+    }
+
+    void "confirm that we can generate a reverse projection"(){
+        given:
+        Track track1 = new Track(length: 100)
+
+        when: "we generate a duplicate projection"
+        track1.addCoordinate(4,12)
+        track1.addCoordinate(70,80)
+        Projection projectionTrack1To2 = new ReverseProjection(track1)
+        Track track2 = projectionTrack1To2.projectTrack(track1)
+
+        then: "it should generate forward "
+        assert 99==projectionTrack1To2.projectValue(0)
+        assert 9==projectionTrack1To2.projectValue(90)
+        assert !track1.equals(track2)
+    }
+
+    void "create a discontinuous projection capable of appropriately "(){
+
+        given:
+        Track track1 = new Track(length: 11)
+        DiscontinuousProjection discontinuousProjection = new DiscontinuousProjection()
+
+        when: "we generate a duplicate projection"
+        track1.addCoordinate(2,4)
+        track1.addCoordinate(7,8)
+        track1.addCoordinate(9,10)
+        Track nullTrack = discontinuousProjection.projectTrack(track1)
+
+        then: "with no map in the discontinuous projection, should return same"
+        for(i in 0..track1.length){
+            assert discontinuousProjection.projectValue(i)==i
+        }
+        assert nullTrack==track1
+
+        when: "we add some intervals"
+        discontinuousProjection.addInterval(2,4)
+        discontinuousProjection.addInterval(7,8)
+        discontinuousProjection.addInterval(9,10)
+
+        then: "values should be mapped appropriately"
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(0)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(1)
+        assert 0 == discontinuousProjection.projectValue(2)
+        assert 1 == discontinuousProjection.projectValue(3)
+        assert 2 == discontinuousProjection.projectValue(4)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(5)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(6)
+        assert 3 == discontinuousProjection.projectValue(7)
+        assert 4 == discontinuousProjection.projectValue(8)
+        assert 5 == discontinuousProjection.projectValue(9)
+        assert 6 == discontinuousProjection.projectValue(10)
+
+        // test reverse values
+        assert 2 == discontinuousProjection.reverseProjectValue(0)
+        assert 3 == discontinuousProjection.reverseProjectValue(1)
+        assert 4 == discontinuousProjection.reverseProjectValue(2)
+        assert 7 == discontinuousProjection.reverseProjectValue(3)
+        assert 8 == discontinuousProjection.reverseProjectValue(4)
+        assert 9 == discontinuousProjection.reverseProjectValue(5)
+        assert 10 == discontinuousProjection.reverseProjectValue(6)
+
+        when: "we project a track"
+        Track trackOut = discontinuousProjection.projectTrack(track1)
+
+        then: "it should properly projecto out the proper coordinates"
+        assert track1.coordinateList.size()==trackOut.coordinateList.size()
+        assert 0==trackOut.coordinateList.get(0).min  // 2
+        assert 2==trackOut.coordinateList.get(0).max  // 4
+        assert 3==trackOut.coordinateList.get(1).min  // 7
+        assert 4==trackOut.coordinateList.get(1).max  // 8
+        assert 5==trackOut.coordinateList.get(2).min  // 9
+        assert 6==trackOut.coordinateList.get(2).max  // 10
+    }
+
+    void "try a difference discontinuous projection capable of reverse projection"(){
+
+        given:
+        Track track1 = new Track(length: 10)
+        DiscontinuousProjection discontinuousProjection = new DiscontinuousProjection()
+
+        when: "we generate a duplicate projection"
+        track1.addCoordinate(2,4)
+        track1.addCoordinate(7,8)
+        track1.addCoordinate(9,10)
+        discontinuousProjection.addInterval(0,2)
+        discontinuousProjection.addInterval(4,6)
+        discontinuousProjection.addInterval(8,9)
+
+        then: "values should be mapped appropriately"
+        assert 0 == discontinuousProjection.projectValue(0)
+        assert 1 == discontinuousProjection.projectValue(1)
+        assert 2 == discontinuousProjection.projectValue(2)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(3)
+        assert 3 == discontinuousProjection.projectValue(4)
+        assert 4 == discontinuousProjection.projectValue(5)
+        assert 5 == discontinuousProjection.projectValue(6)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(7)
+        assert 6 == discontinuousProjection.projectValue(8)
+        assert 7 == discontinuousProjection.projectValue(9)
+        assert AbstractProjection.UNMAPPED_VALUE == discontinuousProjection.projectValue(10)
+
+        // test reverse values
+        assert 0 == discontinuousProjection.reverseProjectValue(0)
+        assert 1 == discontinuousProjection.reverseProjectValue(1)
+        assert 2 == discontinuousProjection.reverseProjectValue(2)
+        assert 4 == discontinuousProjection.reverseProjectValue(3)
+        assert 5 == discontinuousProjection.reverseProjectValue(4)
+        assert 6 == discontinuousProjection.reverseProjectValue(5)
+        assert 8 == discontinuousProjection.reverseProjectValue(6)
+        assert 9 == discontinuousProjection.reverseProjectValue(7)
+
+        when: "we project a track"
+        Track trackOut = discontinuousProjection.projectTrack(track1)
+
+        then: "it should properly projecto out the proper coordinates"
+        assert track1.coordinateList.size()==trackOut.coordinateList.size()
+        assert 2==trackOut.coordinateList.get(0).min  // 2
+        assert 3==trackOut.coordinateList.get(0).max  // 4
+        assert -1==trackOut.coordinateList.get(1).min  // 7
+        assert 6==trackOut.coordinateList.get(1).max  // 8
+        assert 7==trackOut.coordinateList.get(2).min  // 9
+        assert -1==trackOut.coordinateList.get(2).max  // 10
+
+    }
+
+    void "create discontinuous projection"(){
+
+        given: "if we have two sets of tracks"
+        Track track1 = new Track(length: 11)
+        track1.addCoordinate(2,4)
+        track1.addCoordinate(7,8)
+        track1.addCoordinate(9,10)
+        Track track2 = new Track(length: 7)
+        track2.addCoordinate(0,2)
+        track2.addCoordinate(3,4)
+        track2.addCoordinate(5,6)
+
+
+        when: "we create a projection from them"
+        DiscontinuousProjection projection = DiscontinuousProjectionFactory.getInstance().createProjection(track1)
+        Track track3 = projection.projectTrack(track1)
+
+
+        then: "if we create a track from that projection it should be an equivalent track"
+        assert track2==track3
+    }
+
+}
diff --git a/test/unit/org/bbop/apollo/ProxyControllerSpec.groovy b/test/unit/org/bbop/apollo/ProxyControllerSpec.groovy
new file mode 100644
index 0000000..f8876c5
--- /dev/null
+++ b/test/unit/org/bbop/apollo/ProxyControllerSpec.groovy
@@ -0,0 +1,154 @@
+package org.bbop.apollo
+
+
+
+import grails.test.mixin.*
+import spock.lang.*
+
+ at TestFor(ProxyController)
+ at Mock(Proxy)
+class ProxyControllerSpec extends Specification {
+
+    def populateValidParams(params) {
+        assert params != null
+        // TODO: Populate valid properties like...
+        params["referenceUrl"] = 'http://someValidName.com'
+        params["targetUrl"] = 'http://someOtherValidName.com'
+        params["active"] = true
+    }
+
+    void "Test the index action returns the correct model"() {
+
+        when:"The index action is executed"
+            controller.index()
+
+        then:"The model is correct"
+            !model.proxyInstanceList
+            model.proxyInstanceCount == 0
+    }
+
+    void "Test the create action returns the correct model"() {
+        when:"The create action is executed"
+            controller.create()
+
+        then:"The model is correctly created"
+            model.proxyInstance!= null
+    }
+
+    void "Test the save action correctly persists an instance"() {
+
+        when:"The save action is executed with an invalid instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'POST'
+            def proxy = new Proxy()
+            proxy.validate()
+            controller.save(proxy)
+
+        then:"The create view is rendered again with the correct model"
+            model.proxyInstance!= null
+            view == 'create'
+
+        when:"The save action is executed with a valid instance"
+            response.reset()
+            populateValidParams(params)
+            proxy = new Proxy(params)
+
+            controller.save(proxy)
+
+        then:"A redirect is issued to the show action"
+            response.redirectedUrl == '/proxy/show/1'
+            controller.flash.message != null
+            Proxy.count() == 1
+    }
+
+    void "Test that the show action returns the correct model"() {
+        when:"The show action is executed with a null domain"
+            controller.show(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the show action"
+            populateValidParams(params)
+            def proxy = new Proxy(params)
+            controller.show(proxy)
+
+        then:"A model is populated containing the domain instance"
+            model.proxyInstance == proxy
+    }
+
+    void "Test that the edit action returns the correct model"() {
+        when:"The edit action is executed with a null domain"
+            controller.edit(null)
+
+        then:"A 404 error is returned"
+            response.status == 404
+
+        when:"A domain instance is passed to the edit action"
+            populateValidParams(params)
+            def proxy = new Proxy(params)
+            controller.edit(proxy)
+
+        then:"A model is populated containing the domain instance"
+            model.proxyInstance == proxy
+    }
+
+    void "Test the update action performs an update on a valid domain instance"() {
+        when:"Update is called for a domain instance that doesn't exist"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'PUT'
+            controller.update(null)
+
+        then:"A 404 error is returned"
+            response.redirectedUrl == '/proxy/index'
+            flash.message != null
+
+
+        when:"An invalid domain instance is passed to the update action"
+            response.reset()
+            def proxy = new Proxy()
+            proxy.validate()
+            controller.update(proxy)
+
+        then:"The edit view is rendered again with the invalid instance"
+            view == 'edit'
+            model.proxyInstance == proxy
+
+        when:"A valid domain instance is passed to the update action"
+            response.reset()
+            populateValidParams(params)
+            proxy = new Proxy(params).save(flush: true)
+            controller.update(proxy)
+
+        then:"A redirect is issues to the show action"
+            response.redirectedUrl == "/proxy/show/$proxy.id"
+            flash.message != null
+    }
+
+    void "Test that the delete action deletes an instance if it exists"() {
+        when:"The delete action is called for a null instance"
+            request.contentType = FORM_CONTENT_TYPE
+            request.method = 'DELETE'
+            controller.delete(null)
+
+        then:"A 404 is returned"
+            response.redirectedUrl == '/proxy/index'
+            flash.message != null
+
+        when:"A domain instance is created"
+            response.reset()
+            populateValidParams(params)
+            def proxy = new Proxy(params).save(flush: true)
+
+        then:"It exists"
+            Proxy.count() == 1
+
+        when:"The domain instance is passed to the delete action"
+            controller.delete(proxy)
+
+        then:"The instance is deleted"
+            Proxy.count() == 0
+            response.redirectedUrl == '/proxy/index'
+            flash.message != null
+    }
+}
diff --git a/test/unit/org/bbop/apollo/RequestHandlingServiceSpec.groovy b/test/unit/org/bbop/apollo/RequestHandlingServiceSpec.groovy
new file mode 100644
index 0000000..4aeaaa1
--- /dev/null
+++ b/test/unit/org/bbop/apollo/RequestHandlingServiceSpec.groovy
@@ -0,0 +1,30 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(RequestHandlingService)
+ at Mock([Sequence, FeatureLocation, Feature, MRNA])
+class RequestHandlingServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    def "clean json"() {
+
+        given: "a valid sequence to translate"
+        String inputJSON = "\"{\\\"track\\\":\\\"chrI\\\",\\\"features\\\":[{\\\"location\\\":{\\\"fmin\\\":62840,\\\"fmax\\\":65405,\\\"strand\\\":1},\\\"type\\\":{\\\"cv\\\":{\\\"name\\\":\\\"sequence\\\"},\\\"name\\\":\\\"mRNA\\\"},\\\"name\\\":\\\"YAL041W\\\",\\\"children\\\":[{\\\"location\\\":{\\\"fmin\\\":62840,\\\"fmax\\\":65405,\\\"strand\\\":1},\\\"type\\\":{\\\"cv\\\":{\\\"name\\\":\\\"sequence\\\"},\\\"name\\\":\\\"exon\\\"}}]}],\\\"operation\\\":\\\"add_transcript\\\",\\\"cl [...]
+
+        when: "the sequence string gets here"
+
+        then: "it should be good"
+
+    }
+}
diff --git a/test/unit/org/bbop/apollo/SvgServiceSpec.groovy b/test/unit/org/bbop/apollo/SvgServiceSpec.groovy
new file mode 100644
index 0000000..2372a76
--- /dev/null
+++ b/test/unit/org/bbop/apollo/SvgServiceSpec.groovy
@@ -0,0 +1,20 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
+ */
+ at TestFor(SvgService)
+class SvgServiceSpec extends Specification {
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    void "test something"() {
+    }
+}
diff --git a/test/unit/org/bbop/apollo/TrackServiceSpec.groovy b/test/unit/org/bbop/apollo/TrackServiceSpec.groovy
new file mode 100644
index 0000000..e4e15d9
--- /dev/null
+++ b/test/unit/org/bbop/apollo/TrackServiceSpec.groovy
@@ -0,0 +1,129 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import org.bbop.apollo.sequence.SequenceDTO
+import org.codehaus.groovy.grails.web.json.JSONArray
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Ignore
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions
+ */
+ at TestFor(TrackService)
+class TrackServiceSpec extends Specification {
+
+    def setup() {
+        service.trackMapperService = new TrackMapperService()
+    }
+
+    def cleanup() {
+    }
+
+    void "return embedded JSON from NCList"() {
+
+        given: "an NCList object for the C. elegans gene flp-1"
+        // http://jbrowse.alliancegenome.org/jbrowse/index.html?data=data%2FMus%20musculus&tracks=All%20Genes&highlight=&lookupSymbol=Msx2&loc=13%3A53463164..53476782
+//        http://localhost:8080/apollo/track/Mus%20musculus/All%20Genes/13:53466884..53473074.json?name=Msx2&ignoreCache=true
+        String ncListString = new File("test/integration/resources/track-data/inputNcList.json").text
+        String classesForTrackString = new File("test/integration/resources/track-data/trackClasses.json").text
+
+
+        JSONArray ncListArray = new JSONArray(ncListString)
+        JSONArray classesForTrackArray = new JSONArray(classesForTrackString)
+        SequenceDTO sequenceDTO = new SequenceDTO(
+                organismCommonName: 'Caenorhabditis elegans'
+                , trackName: 'All Genes'
+                , sequenceName: 'IV'
+        )
+
+        when: "we filter it to an object"
+        service.storeTrackData(sequenceDTO, classesForTrackArray)
+        JSONArray renderedArray = service.convertAllNCListToObject(ncListArray, sequenceDTO)
+
+
+        then: "we should get all of the surrounding data and the data itself"
+        // we should see 3 encompassing non-coding transcripts:  Gm33763 (MGI:5592922), XR_873404.1, XR_873405.1, XR_873406.1
+        // we should see 2 coding transcripts with select=true, Msx2 (MGI:97169), ENSMUST00000021922, NM_013601.2
+        assert renderedArray.size() == 1
+        assert renderedArray.getJSONObject(0).type == 'gene'
+
+        when: 'we get the very first gene'
+        JSONObject firstGeneObject = renderedArray.getJSONObject(0)
+        JSONArray firstGeneChildren = firstGeneObject.children
+
+        then: "we should see the next level"
+        assert firstGeneChildren.size() == 4
+        assert firstGeneChildren.getJSONObject(0).type == 'ncRNA'
+        assert firstGeneChildren.getJSONObject(1).type == 'ncRNA'
+        assert firstGeneChildren.getJSONObject(2).type == 'ncRNA'
+        assert firstGeneChildren.getJSONArray(3)
+
+        when: "we get the coding gene"
+        JSONArray subArray = firstGeneChildren.getJSONArray(3)
+        println "subarray siez ${subArray.size()}"
+        JSONObject codingGene = subArray.getJSONObject(0)
+        JSONArray codingGeneChildren = codingGene.children
+
+        then: "we should see 2 mrnas"
+        assert codingGeneChildren.size() == 2
+        assert codingGeneChildren.getJSONObject(0).type == 'mRNA'
+        assert codingGeneChildren.getJSONObject(0).children.size() > 0
+        assert codingGeneChildren.getJSONObject(1).type == 'mRNA'
+        assert codingGeneChildren.getJSONObject(1).children.size() > 0
+
+
+    }
+
+    void "flatten nested mouse"() {
+
+        given: "a nested JSON object"
+        JSONArray inputArray = new JSONArray(new File("test/integration/resources/track-data/mouseMsx2.json").text)
+
+
+        when: "we flatten it"
+        JSONArray renderedArray = service.flattenArray(inputArray, 'gene')
+
+        then: "we should see an expected result"
+        assert renderedArray.size() == 2
+
+        when: "we get the two genes out"
+        JSONObject gene1 = renderedArray.getJSONObject(0)
+        JSONObject gene2 = renderedArray.getJSONObject(1)
+
+        then: "we should see the appropriate structure"
+        assert gene1.type == 'gene'
+        assert gene1.name == 'Msx1os'
+        assert gene1.children.size() == 1
+        assert gene1.children[0].children.size() == 1
+        assert gene1.children[0].type == 'ncRNA'
+
+        assert gene2.type == 'gene'
+        assert gene2.name == 'Msx1'
+        assert gene2.children.size() == 3
+        assert gene2.children[0].children.size() == 4
+        assert gene2.children[0].type == 'mRNA'
+
+
+    }
+
+    void "flatten nested worm"() {
+
+        given: "a nested JSON object"
+        JSONArray inputArray = new JSONArray(new File("test/integration/resources/track-data/wormGeneflp-1.json").text)
+
+
+        when: "we flatten it"
+        JSONArray renderedArray = service.flattenArray(inputArray, 'gene')
+        println renderedArray.name
+
+        then: "we should see an expected result"
+        assert renderedArray.size() == 8
+        for (obj in renderedArray) {
+            assert obj.type == 'gene'
+        }
+
+
+    }
+
+}
diff --git a/test/unit/org/bbop/apollo/TranslationTableSpec.groovy b/test/unit/org/bbop/apollo/TranslationTableSpec.groovy
new file mode 100644
index 0000000..b1676e9
--- /dev/null
+++ b/test/unit/org/bbop/apollo/TranslationTableSpec.groovy
@@ -0,0 +1,73 @@
+package org.bbop.apollo
+
+import grails.test.mixin.TestFor
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import org.bbop.apollo.sequence.TranslationTable
+import org.bbop.apollo.sequence.SequenceTranslationHandler
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
+ */
+class TranslationTableSpec extends Specification {
+
+
+    def setup() {
+    }
+
+    def cleanup() {
+    }
+
+    // if we init with "default" does that work?
+    void "is the default behavior correct?"() {
+
+        given:
+        SequenceTranslationHandler handler = new SequenceTranslationHandler()
+
+        when: "we read a translation table"
+        // be something with STOPS, etc.
+        TranslationTable translationTable = handler.getDefaultTranslationTable()
+
+        then: "we should get the correct results"
+        assert translationTable!=null
+        assert translationTable.startCodons.size()==1
+        assert translationTable.stopCodons.size()==3
+        assert translationTable.alternateTranslationTable.size()==1
+        assert translationTable.translationTable.size()==64
+        assert translationTable.alternateTranslationTable.size()==1
+    }
+
+
+    void "can I read in translation tables"() {
+
+        given:
+        File file = new File("web-app/translation_tables/ncbi_11_translation_table.txt")
+
+        when: "we read a translation table"
+        TranslationTable translationTable = SequenceTranslationHandler.readTable(file)
+
+        then: "we should get the correct results"
+        assert translationTable.startCodons.size()==1+6
+        assert translationTable.stopCodons.size()==3
+        assert translationTable.translationTable.size()==64 // start codons are existing translations
+        assert translationTable.alternateTranslationTable.size()==1
+    }
+
+    void "is the init behavior correct?"() {
+
+        given:
+        SequenceTranslationHandler handler = new SequenceTranslationHandler()
+
+        when: "we read a translation table"
+        // be something with STOPS, etc.
+        TranslationTable translationTable = handler.getTranslationTableForGeneticCode("2")
+
+        then: "we should get the correct results"
+        assert translationTable!=null
+        assert translationTable.startCodons.size()==1+1-1
+        assert translationTable.stopCodons.size()==3+2-1
+        assert translationTable.translationTable.size()==64
+        assert translationTable.alternateTranslationTable.size()==1-1
+    }
+
+}
diff --git a/test/unit/org/bbop/apollo/UserSpec.groovy b/test/unit/org/bbop/apollo/UserSpec.groovy
new file mode 100644
index 0000000..8671515
--- /dev/null
+++ b/test/unit/org/bbop/apollo/UserSpec.groovy
@@ -0,0 +1,63 @@
+package org.bbop.apollo
+
+import grails.test.mixin.Mock
+import grails.test.mixin.TestFor
+import org.bbop.apollo.authenticator.RemoteUserAuthenticatorService
+import org.codehaus.groovy.grails.web.json.JSONObject
+import spock.lang.Specification
+
+/**
+ * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
+ */
+ at TestFor(User)
+ at Mock([User])
+class UserSpec extends Specification {
+
+
+    void "when I add metadata I can retrieve it again"() {
+
+        given: "A user"
+        String randomPassword1 = "RandomPassword1"
+        String randomPassword2 = "RandomPassword2"
+        User user = new User(
+                username: "bobjones",
+                passwordHash: "abc123",
+                firstName: "Bob",
+                lastName: "Jones",
+        ).save(flush: true, failOnError: true, insert: true)
+
+
+        when: "I add metadata to the user"
+        user.addMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD, randomPassword1)
+        User otherUser = User.first()
+
+        then: "I should see it"
+        assert User.count == 1
+        assert otherUser.getMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD) == randomPassword1
+
+        when: "we change it"
+        user.addMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD, randomPassword2)
+        JSONObject returnedObject = user.getMetaDataObject()
+        otherUser = User.first()
+
+        then: "we should see the change"
+        assert User.count == 1
+        assert otherUser.getMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD) == randomPassword2
+        assert returnedObject.keySet().size() == 1
+        assert returnedObject.get(RemoteUserAuthenticatorService.INTERNAL_PASSWORD) == randomPassword2
+
+        when: "We remove it, it should not be there anymore"
+        String value = user.removeMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD)
+        otherUser = User.first()
+        returnedObject = user.getMetaDataObject()
+
+        then: "should reflect it"
+        assert User.count == 1
+        assert returnedObject.keySet().size() == 0
+        assert otherUser.getMetaData(RemoteUserAuthenticatorService.INTERNAL_PASSWORD) == null
+
+
+
+    }
+
+}
diff --git a/tools/cleanup/remove_temporary_files.sh b/tools/cleanup/remove_temporary_files.sh
new file mode 100755
index 0000000..d8db8b8
--- /dev/null
+++ b/tools/cleanup/remove_temporary_files.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+function print_usage {
+	cat << END
+usage: $(basename $0) -d <base_directory>
+	-m <file_age_in_minutes>
+        [-h]
+
+       d: base directory for temporary files
+       m: number of minutes where any file that is older will be deleted
+       h: this help
+END
+	exit 1;
+}
+
+while getopts "d:m:h" opt;
+do
+	case $opt in
+		h) print_usage;;
+		d) dir=$OPTARG;;
+		m) min=$OPTARG;;
+	esac
+done
+
+for i in $(find $dir -mmin +$min -type f);
+do
+	rm $i && rmdir $(dirname $i)
+done
+
+find $dir -mindepth 1 -maxdepth 1 -empty -type d -exec rmdir {} \;
diff --git a/tools/data/add_features_from_gff3_to_annotations.pl b/tools/data/add_features_from_gff3_to_annotations.pl
new file mode 100755
index 0000000..1ac3edf
--- /dev/null
+++ b/tools/data/add_features_from_gff3_to_annotations.pl
@@ -0,0 +1,920 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../jbrowse-download/src/perl5";
+use JBlibs;
+
+use File::Basename;
+use lib dirname($0);
+
+use Bio::GFF3::LowLevel::Parser;
+use Getopt::Long qw(:config no_ignore_case bundling);
+use IO::File;
+use LWP::UserAgent;
+use JSON;
+use Data::Dumper;
+
+my $gene_types_in = "gene";
+my $pseudogene_types_in = "pseudogene";
+my $transcript_types_in = "transcript|mRNA";
+my $noncoding_rna_types = "ncRNA|rRNA|tRNA|snRNA|snoRNA|miRNA";
+my $exon_types_in = "exon";
+my $cds_types_in = "CDS";
+my $ontology = "sequence";
+my $gene_type_out = "gene";
+my $pseudogene_type_out = "pseudogene";
+my $mrna_type_out = "mRNA";
+my $transcript_type_out = "transcript";
+my $exon_type_out = "exon";
+my $cds_type_out = "CDS";
+my $sequence_alteration_types = "insertion|deletion|substitution";
+my $annotation_track_prefix = "";
+my $property_ontology = "feature_property";
+my $comment_type_out = "comment";
+my $property_type_out = "feature_property";
+my $disable_cds_recalculation = 0;
+my $organism;
+my $in = \*STDIN;
+my $username;
+my $password;
+my $url;
+my $session_id;
+my $name_attributes="Name";
+my $test = 0;
+my $use_name_for_feature = 0;
+
+my $success_log;
+my $error_log;
+
+my $chunk_size = 100;
+
+my $json = new JSON();
+my $ua = new LWP::UserAgent();
+$ua->timeout(3000); # 5 minutes
+
+my %source_features = ();
+my %skip_ids = ();
+
+my %reserved_properties = (     symbol      => 1,
+                                description => 1,
+                                status      => 1
+                          );
+my %ignored_properties = (  owner               => 1,
+                            date_creation       => 1,
+                            date_last_modified  => 1
+                         );
+
+$| = 1;
+
+parse_options();
+process_gff();
+
+sub parse_options {
+    my $help;
+    my $input_file;
+    my $success_log_file;
+    my $error_log_file;
+    my $skip_file;
+    GetOptions("input|i=s"          => \$input_file,
+           "username|u=s"       => \$username,
+           "password|p=s"       => \$password,
+           "url|U=s"            => \$url,
+           "gene_types_in|g=s"      => \$gene_types_in,
+           "pseudogene_type_in|n=s" => \$pseudogene_types_in,
+           "transcript_types_in|t=s"    => \$transcript_types_in,
+           "exon_types_in|e=s"      => \$exon_types_in,
+           "organism|o=s"      => \$organism,
+           "cds_types_in|d=s"       => \$cds_types_in,
+           "ontology|O=s"       => \$ontology,
+           "gene_type_out|G=s"      => \$gene_type_out,
+           "pseudogene_type_out|N=s" => \$pseudogene_type_out,
+           "mrna_type_out|M=s"    => \$mrna_type_out,
+           "transcript_type_out|T=s" => \$transcript_type_out,
+           "exon_type_out|E=s"      => \$exon_type_out,
+           "cds_type_out|D=s"       => \$cds_type_out,
+           "property_ontolgy|R=s"   => \$property_ontology,
+           "comment_type_out|C=s"   => \$comment_type_out,
+           "property_type_out|S=s"  => \$property_type_out,
+           "track_prefix|P=s"       => \$annotation_track_prefix,
+           "disable_cds_recalculation|X"   => \$disable_cds_recalculation,
+           "success_log|l=s"        => \$success_log_file,
+           "error_log|L=s"      => \$error_log_file,
+           "skip|s=s"           => \$skip_file,
+           "test|x"           => \$test,
+           "help|h"         => \$help,
+           "name_attributes=s"   => \$name_attributes,
+           "use_name_for_feature|a" => \$use_name_for_feature);
+
+    print_usage() if $help;
+    $in = new IO::File($input_file) or die "Error reading $input_file: $!\n"
+        if $input_file;
+    $success_log = new IO::File($success_log_file, "w") or die "Error writing success log $success_log_file: $!\n" if $success_log_file;
+    $error_log = new IO::File($error_log_file, "w") or die "Error writing error log $error_log_file: $!\n" if $error_log_file;
+    if ($skip_file) {
+        my $fh = new IO::File($skip_file) or die "Error reading $skip_file: $!\n";
+        while (my $id = <$fh>) {
+            chomp $id;
+            ++$skip_ids{$id};
+        }
+    }
+	print_usage() if(!$username && !$password && !$url);
+    die "Missing required parameter: username\n" if !$username;
+    die "Missing required parameter: password\n" if !$password;
+    die "Missing required parameter: url\n" if !$url;
+    $url = "http://$url" if $url !~ m%https?://%;
+}
+
+sub print_usage {
+    my $progname = basename($0);
+    die << "END";
+
+usage: $progname
+    --url|-U <URL to Apollo instance>
+    --username|-u <username>
+    --password|-p <password>
+    [--genes_types_in|-g <gene types for input>]
+    [--pseudogene_type_in|-n <pseudogene type for input>]
+    [--transcript_types_in|-t <transcript types for input>]
+    [--exon_types_in|-e <exon types for input>]
+    [--cds_types_in|-d <CDS types for input>]
+    [--ontology|-O <ontology name used in server>]
+    [--gene_type_out|-G <gene type used in server>]
+    [--pseudogene_type_out|-N <pseudogene type used in server>]
+    [--mrna_type_out|-T <mRNA type used in server>]
+    [--transcript_type_out|-T <transcript type used in server>]
+    [--exon_type_out|-E <exon type used in server>]
+    [--cds_type_out|-D <CDS type used in server>]
+    [--property_ontology|-R <property ontology name used in server>]
+    [--comment_type_out|-C <comment type used in server>]
+    [--property_type_out|-S <feature property type used in server>]
+    [--track_prefix|-P <annotation track prefix>]
+    [--disable_cds_recalculation|-X]
+    [--input|-i <GFF3 file>]
+    [--organism|-o <the name field used for your organism in WA2>]
+    [--success_log|-l <success log file>]
+    [--error_log|-L <error log file>]
+    [--skip|-s <skip id file>]
+    [--test|-x]
+    [--help|-h]
+    [--name_attributes <feature attribute to be used as name, first found used>]
+    [--use_name_for_feature|-a]
+
+    U: URL to Apollo instance
+    u: username to access Apollo
+    p: password to access Apollo
+    g: string/regex to define the GFF3 types to treat as genes
+       [default: "$gene_types_in"]
+    n: string/regex to define the GFF3 types to treat as pseudogenes
+       [default: "$pseudogene_types_in"]
+    t: string/regex to define the GFF3 types to treat as transcripts
+       [default: "$transcript_types_in"]
+    e: string/regex to define the GFF3 types to treat as exons
+       [default: "$exon_types_in"]
+    d: string/regex to define the GFF3 types to treat as CDS
+       [default: "$cds_types_in"]
+    O: ontology name used in Apollo instance
+       [default: "$ontology"]
+    G: gene type used in Apollo instance
+       [default: "$gene_type_out"]
+    N: pseudogene type used in Apollo instance
+       [default: "$pseudogene_type_out"]
+    M: mRNA type used in Apollo instance
+       [default: "$mrna_type_out"]
+    T: transcript type used in Apollo instance
+       [default: "$transcript_type_out"]
+    E: exon type used in Apollo instance
+       [default: "$exon_type_out"]
+    D: CDS type used in Apollo instance
+       [default: "$cds_type_out"]
+    R: property ontology name used in Apollo instance
+       [default: "$property_ontology"]
+    C: comment type used in Apollo instance
+       [default: "$comment_type_out"]
+    S: feature property type used in Apollo instance
+       [default: "$property_type_out"]
+    P: annotation track prefix
+       [default: "$annotation_track_prefix"]
+    X: disable the recalculation of CDS by Apollo for protein coding features
+    i: input GFF3 file
+       [default: STDIN]
+    o: organism common name in Apollo instance
+    a: preserve feature names, as-is, in Apollo
+    l: log file for ids that were successfully processed
+    L: log file for ids that were erroneously processed
+    s: file with ids to skip
+    x: test mode
+    h: this help screen
+END
+}
+
+sub process_gff {
+    my $gffio = Bio::GFF3::LowLevel::Parser->open($in);
+    my $seq_ids_to_transcripts = {};
+    my $seq_ids_to_genes = {};
+    my $seq_ids_to_sequence_alterations = {};
+
+    while (my $features = $gffio->next_item()) {
+        next if ref $features ne "ARRAY";
+        process_gff_entry($features, $seq_ids_to_genes, $seq_ids_to_transcripts, $seq_ids_to_sequence_alterations);
+    }
+    if ($test) {
+        print "[";
+        if (keys %{$seq_ids_to_genes} > 0) {
+            print_features($seq_ids_to_genes, "addFeature");
+        }
+        if (keys %{$seq_ids_to_transcripts}) {
+            if (keys %{$seq_ids_to_genes} > 0) {
+                print ",";
+            }
+            print_features($seq_ids_to_transcripts, "addTranscript");
+        }
+        if (keys %{$seq_ids_to_sequence_alterations}) {
+            if (keys %{$seq_ids_to_sequence_alterations} > 0) {
+                print ",";
+            }
+            print_features($seq_ids_to_sequence_alterations, "addSequenceAlteration");
+        }
+        print "]";
+    }
+    else {
+        write_features("addFeature", $seq_ids_to_genes);
+        write_features("addTranscript", $seq_ids_to_transcripts);
+        write_features("addSequenceAlteration", $seq_ids_to_sequence_alterations);
+        if (!scalar(keys(%{$seq_ids_to_genes})) && !scalar(keys(%{$seq_ids_to_transcripts})) && !scalar(keys(%{$seq_ids_to_sequence_alterations}))) {
+            print "No genes of type \"$gene_types_in\" or transcripts of type \"$transcript_types_in\" or sequence alterations of type \"$sequence_alteration_types\" found\n";
+        }
+    }
+
+}
+
+sub write_features {
+    my $operation = shift;
+    my $data = shift;
+    while (my ($seq_id, $features) = each(%{$data})) {
+        print "Processing $seq_id\n";
+        my @to_be_written = ();
+        my @ids = ();
+        my @error = ();
+        my $chunk = 0;
+        for (my $i = 0; $i < scalar(@{$features}); ++$i) {
+            my $feature = $features->[$i];
+            push @to_be_written, $feature->[0];
+            push @ids, $feature->[1];
+            if (($i + 1) % $chunk_size == 0 || $i == scalar(@{$features}) - 1) {
+                print "Processing chunk " . ++$chunk . ": ";
+                my $res = send_request($seq_id, $operation, \@to_be_written);
+                if ($res->{error}) {
+                    print "error: $res->{error}\n";
+                    for (my $j = 0; $j < scalar(@to_be_written); ++$j) {
+                        push @error, [$to_be_written[$j], $ids[$j]];
+                    }
+                }
+                else {
+                    print "success\n";
+                    foreach my $id (@ids) {
+                        print $success_log "$id\n" if $success_log;
+                    }
+                }
+                @to_be_written = ();
+                @ids = ();
+            }
+        }
+        if (scalar(@error)) {
+            print "Processing individual features for failed chunks\n";
+            foreach my $feature (@error) {
+                print "Processing $feature->[1]: ";
+                my $res = send_request($seq_id, $operation, [$feature->[0]]);
+                if ($res->{error}) {
+                    print "error: $res->{error}\n";
+                    print $error_log "$feature->[1]\n" if $error_log;
+                }
+                else {
+                    print "success\n";
+                    print $success_log "$feature->[1]\n" if $success_log;
+                }
+            }
+        }
+    }
+}
+
+sub convert_feature {
+    my $features = shift;
+    my $type = shift;
+    my $name_attributes = shift;
+    my $start = get_start($features);
+    my $end = get_end($features);
+    my $strand = get_strand($features);
+    my $name = get_name($features,$name_attributes);
+    my $comments = get_comments($features);
+    my $dbxrefs = get_dbxrefs($features);
+    my $ontology_terms = get_ontology_terms($features);
+    my $json_feature = {
+        location => {
+            fmin => $start - 1,
+            fmax => $end,
+            strand => $strand
+        },
+        type => {
+            cv => {
+                name => $ontology
+            },
+            name => $type
+        }
+    };
+    if ($name) {
+        $json_feature->{name} = $name;
+    }
+    my $json_properties = [];
+    foreach my $comment (@{$comments}) {
+        my $json_comment = {
+            value => $comment,
+            type => {
+                cv => {
+                    name => $property_ontology
+                },
+                name => $comment_type_out
+            }
+        };
+        push @{$json_properties}, $json_comment;
+    }
+    foreach my $feature (@{$features}) {
+        while (my ($tag, $values) = each(%{$feature->{attributes}})) {
+            next if $tag =~ /^[A-Z]/;
+            next if $ignored_properties{$tag};
+            if ($tag =~ /symbol/) {
+                $json_feature->{symbol} = $values->[0];
+                next;
+            }
+            if ($tag =~ /description/) {
+                $json_feature->{description} = $values->[0];
+                next;
+            }
+
+            my $type = $tag;
+            foreach my $value (@{$values}) {
+                my $json_property = {
+                    value => $value,
+                    type => {
+                        cv => {
+                            name => $property_ontology
+                        },
+                        name => $type
+                    }
+                };
+                push @{$json_properties}, $json_property;
+            }
+        }
+        if (scalar(@{$json_properties})) {
+            $json_feature->{properties} = $json_properties;
+        }
+    }
+    my $json_dbxrefs = [];
+    foreach my $dbxref (@{$dbxrefs}) {
+        my ($db, $accession) = split ":", $dbxref;
+        my $json_dbxref = {
+            accession => $accession,
+            db => {
+                name => $db
+            }
+        };
+        push @{$json_dbxrefs}, $json_dbxref;
+    }
+    foreach my $ontology_term (@{$ontology_terms}) {
+        my @ontology_term_parts = split /:/, $ontology_term;
+        my $json_ontology_term = {
+            accession => $ontology_term_parts[1],
+            db => {
+                name => $ontology_term_parts[0]
+            }
+        };
+        push @{$json_dbxrefs}, $json_ontology_term;
+    }
+    if (scalar(@{$dbxrefs})) {
+        $json_feature->{dbxrefs} = $json_dbxrefs;
+    }
+    return $json_feature;
+}
+
+sub convert_sequence_alteration {
+    my $features = shift;
+    my $type = shift;
+    my $name_attributes = shift;
+    my $start = get_start($features);
+    my $end = get_end($features);
+    my $strand = get_strand($features);
+    #my $name = get_name($features,$name_attributes);
+    my $comments = get_comments($features);
+
+    if ($type =~ /insertion/) {
+        $start--;
+        $end--;
+    }
+    else {
+        $start--;
+    }
+
+    my $json_feature = {
+        location => {
+            fmin => $start,
+            fmax => $end,
+            strand => $strand
+        },
+        type => {
+            cv => {
+                name => $ontology
+            },
+            name => $type
+        }
+    };
+
+    #if ($name) {
+    #    $json_feature->{name} = $name;
+    #}
+
+    my $json_properties = [];
+    foreach my $comment (@{$comments}) {
+        my $json_comment = {
+            value => $comment,
+            type => {
+                cv => {
+                    name => $property_ontology
+                },
+                name => $comment_type_out
+            }
+        };
+        push @{$json_properties}, $json_comment;
+    }
+
+
+    foreach my $feature(@{$features}) {
+        while (my ($tag, $values) = each(%{$feature->{attributes}})) {
+            next if $tag =~ /^[A-Z]/;
+            next if $ignored_properties{$tag};
+            if ($tag =~ /residues/) {
+                $json_feature->{residues} = $values->[0];
+                next;
+            }
+
+            my $type = $tag;
+            foreach my $value (@{$values}) {
+                my $json_property = {
+                    value => $value,
+                    type => {
+                        cv => {
+                            name => $property_ontology
+                        },
+                        name => $type
+                    }
+                };
+                push @{$json_properties}, $json_property;
+            }
+        }
+        if (scalar(@{$json_properties})) {
+            $json_feature->{properties} = $json_properties;
+        }
+    }
+    return $json_feature;
+}
+
+sub get_editor_url {
+    return "$url/annotationEditor";
+}
+
+
+sub send_request {
+    my $seq_id = shift;
+    my $operation = shift;
+    my $features = shift;
+    my $request = {
+            track => "$annotation_track_prefix$seq_id",
+            operation => $operation,
+            username => $username,
+            password => $password
+    };
+    if($organism) {
+        $request->{organism}= $organism;
+    }
+
+    if ($features) {
+        foreach my $feature (@{$features}) {
+            push(@{$request->{features}}, $feature);
+        }
+    }
+    my $req = new HTTP::Request();
+    $req->method("POST");
+
+    my $newurl = get_editor_url();
+    $newurl = "$newurl/$operation";
+    print "$newurl \n";
+    $req->uri($newurl);
+    $req->content($json->encode($request));
+    my $res = $ua->request($req);
+    if ($res->is_success()) {
+        return $json->decode($res->content());
+    }
+    else {
+        my $content;
+        my $message;
+        eval {
+            $content = $json->decode($res->content());
+        };
+        if ($@) {
+            $message = $res->status_line;
+        }
+        else {
+            $message = $content->{error} ? $content->{error} : $res->status_line;
+        }
+        return { error => $message };
+    }
+}
+
+sub get_name {
+    my $features = shift;
+    my $name_attributes = shift;
+    my @attrs = split /\|/, $name_attributes;
+    my $name;
+    ATTR:
+    foreach my $attr (@attrs) {
+        $name = $features->[0]->{attributes}->{$attr}->[0];
+        last ATTR unless !$name;
+    }
+    return $name;
+}
+
+sub get_comments {
+    my $features = shift;
+    my @comments;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Note};
+        push @comments, @{$values} if $values;
+    }
+    return \@comments;
+}
+
+sub get_dbxrefs {
+    my $features = shift;
+    my @dbxrefs;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Dbxref};
+        push @dbxrefs, @{$values} if $values;
+    }
+    return \@dbxrefs;
+}
+
+sub get_ontology_terms {
+    my $features = shift;
+    my @ontology_terms;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Ontology_term};
+        push @ontology_terms, @{$values} if $values;
+    }
+    return \@ontology_terms;
+}
+
+sub get_seq_id {
+    my $features = shift;
+    return $features->[0]->{seq_id};
+}
+
+sub get_start {
+    my $features = shift;
+    my $start = undef;
+    foreach my $feature (@{$features}) {
+        if (!defined $start || $feature->{start} < $start) {
+            $start = $feature->{start};
+        }
+    }
+    return int($start);
+}
+
+sub get_end {
+    my $features = shift;
+    my $end = undef;
+    foreach my $feature (@{$features}) {
+        if (!defined $end || $feature->{end} > $end) {
+            $end = $feature->{end};
+        }
+    }
+    return int($end);
+}
+
+sub get_strand {
+    my $features = shift;
+    my $strand = $features->[0]->{strand};
+    if (!defined $strand) {
+        $strand = "."
+    }
+    if ($strand eq "+") {
+        return 1;
+    }
+    elsif ($strand eq "-") {
+        return -1;
+    }
+    return 0;
+}
+
+sub get_id {
+    my $features = shift;
+    return $features->[0]->{attributes}->{ID}->[0];
+}
+
+sub get_type {
+    my $features = shift;
+    return $features->[0]->{type};
+}
+
+sub process_gene {
+    my $features = shift;
+    my $gene_type = get_type($features);
+    my $gene;
+    if ($gene_type =~ $pseudogene_types_in) {
+        $gene = convert_feature($features, $pseudogene_type_out, $name_attributes);
+    }
+    else {
+        $gene = convert_feature($features, $gene_type_out, $name_attributes);
+    }
+    if ($use_name_for_feature) {
+        $gene->{use_name} = "true";
+    }
+    my $subfeatures = get_subfeatures($features);
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$transcript_types_in/) {
+            my $transcript = process_transcript($subfeature);
+            push(@{$gene->{children}}, $transcript) if $transcript;
+        }
+        elsif ($type =~ /$noncoding_rna_types/) {
+            my $rna = process_rna($subfeature);
+            push(@{$gene->{children}}, $rna) if $rna;
+        }
+    }
+    return $gene;
+}
+
+sub process_transcript {
+    my $features = shift;
+    my $original_type = get_type($features);
+    my $transcript;
+    if ($original_type =~ /transcript/) {
+        $transcript = convert_feature($features, $transcript_type_out, $name_attributes);
+    }
+    else {
+        $transcript = convert_feature($features, $mrna_type_out, $name_attributes);
+    }
+
+    if ($use_name_for_feature) {
+        $transcript->{use_name} = "true";
+    }
+    my $cds_feature = undef;
+    my $subfeatures = get_subfeatures($features);
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$exon_types_in/) {
+            my $exon = convert_feature($subfeature, $exon_type_out,$name_attributes);
+            push(@{$transcript->{children}}, $exon);
+        }
+        elsif ($type =~ /$cds_types_in/) {
+            my $start = get_start($subfeature);
+            my $end = get_end($subfeature);
+            if (!defined $cds_feature) {
+                $cds_feature = $subfeature;
+            }
+            else {
+                my $cds_start = get_start($cds_feature);
+                my $cds_end = get_end($cds_feature);
+                if ($start < $cds_start) {
+                    $cds_feature->[0]->{start} = $start;
+                }
+                if ($end > $cds_end) {
+                    $cds_feature->[0]->{end} = $end;
+                }
+            }
+        }
+    }
+    if ($cds_feature) {
+        my $cds = convert_feature($cds_feature, $cds_type_out, $name_attributes);
+        push(@{$transcript->{children}}, $cds);
+    }
+    return $transcript;
+}
+
+sub process_rna {
+    my $features = shift;
+    my $type = get_type($features);
+    my $rna = convert_feature($features, $type, $name_attributes);
+    if ($use_name_for_feature) {
+        $rna->{use_name} = "true";
+    }
+    my $subfeatures = get_subfeatures($features);
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$exon_types_in/) {
+            my $exon = convert_feature($subfeature, $exon_type_out, $name_attributes);
+            push(@{$rna->{children}}, $exon);
+        }
+    }
+    return $rna;
+}
+
+sub get_subfeatures {
+    my @subfeatures;
+    my $features = shift;
+    foreach my $feature (@{$features}) {
+        push @subfeatures, @{$feature->{child_features}};
+    }
+    return \@subfeatures;
+}
+
+sub process_gff_entry {
+    my $features = shift;
+    my $seq_ids_to_genes = shift;
+    my $seq_ids_to_transcripts = shift;
+    my $seq_ids_to_sequence_alterations = shift;
+    my $type = get_type($features);
+    if ($type !~ /$gene_types_in/ && $type !~ /$transcript_types_in/) {
+        my @subfeatures_array = get_subfeatures($features);
+        if ($#subfeatures_array) {
+            foreach my $subfeature (@subfeatures_array) {
+                process_gff_entry($subfeature, $seq_ids_to_genes, $seq_ids_to_transcripts);
+            }
+        }
+        else {
+            my @json_features = process_singleton_feature($features);
+            if (scalar(@json_features) != 0) {
+                foreach my $json_feature (@json_features) {
+                    my $id = get_id($features);
+                    if ($skip_ids{$id}) {
+                        print "Skipping $id\n";
+                        next;
+                    }
+                    my $seq_id = get_seq_id($features);
+                    if ($json_feature->{type}->{name} =~ /$sequence_alteration_types/) {
+                        push(@{$seq_ids_to_sequence_alterations->{$seq_id}}, [$json_feature, $id]);
+                    }
+                    else {
+                        push(@{$seq_ids_to_genes->{$seq_id}}, [$json_feature, $id]);
+                    }
+                }
+            }
+        }
+
+    }
+    else {
+        my @json_features = @{process_feature($features)};
+        if (scalar(@json_features) != 0) {
+            foreach my $json_feature (@json_features) {
+                my $id = get_id($features);
+                if ($skip_ids{$id}) {
+                    print "Skipping $id\n";
+                    next;
+                }
+                my $seq_id = get_seq_id($features);
+                if ($json_feature->{type}->{name} =~ /$gene_types_in/) {
+                    push(@{$seq_ids_to_genes->{$seq_id}}, [$json_feature, $id]);
+                }
+                else {
+                    push(@{$seq_ids_to_transcripts->{$seq_id}}, [$json_feature, $id]);
+                }
+            }
+        }
+    }
+}
+
+sub process_feature {
+    my $features = shift;
+    my $type = get_type($features);
+    my $transcript_type = get_subfeature_type($features);
+
+    if ($type =~ /$gene_types_in/) {
+        if ($transcript_type =~ /mRNA/) {
+            # process with mRNA at top-level and gene information as 'parent'
+            return process_mrna($features);
+        }
+        elsif ($transcript_type =~ /$noncoding_rna_types/) {
+            # process noncoding RNAs
+            return [process_gene($features)];
+        }
+        else {
+            # process top-level
+            return [process_gene($features)];
+        }
+    }
+    elsif ($type =~ /$transcript_types_in/) {
+        return [process_transcript($features)];
+    }
+    return undef;
+}
+
+sub process_singleton_feature {
+    my $features = shift;
+    my $type = get_type($features);
+    my $feature;
+    if ($type =~ /$sequence_alteration_types/) {
+        # process sequence alterations
+        $feature = convert_sequence_alteration($features, $type, $name_attributes);
+    }
+    else {
+        # process other singletons
+        $feature = convert_feature($features, $type, $name_attributes);
+        if ($use_name_for_feature) {
+            $feature->{use_name} = "true";
+        }
+    }
+    return $feature;
+}
+
+sub get_subfeature_type {
+    my $features = shift;
+    return ${features}->[0]->{child_features}->[0]->[0]->{type};
+}
+
+sub process_mrna {
+    my $features = shift;
+    my $gene = convert_feature($features, $gene_type_out, $name_attributes);
+    my $subfeatures = get_subfeatures($features);
+    my @processed_mrnas;
+
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /mRNA/) {
+            my $mrna_json_feature = convert_mrna_feature($subfeature, $gene);
+            push(@processed_mrnas, $mrna_json_feature);
+        }
+    }
+    return \@processed_mrnas;
+}
+
+sub convert_mrna_feature {
+    my $features = shift;
+    my $gene_json_feature = shift;
+    my $cds_feature = undef;
+    my $rtsc_json_feature = undef;
+    my $mrna_json_feature = convert_feature($features, $mrna_type_out, $name_attributes);
+    if ($disable_cds_recalculation) {
+        $mrna_json_feature->{use_cds} = "true";
+    }
+    if ($use_name_for_feature) {
+        $mrna_json_feature->{use_name} = "true";
+    }
+    $mrna_json_feature->{parent} = $gene_json_feature;
+    my $subfeatures = get_subfeatures($features);
+
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$exon_types_in/) {
+            my $exon = convert_feature($subfeature, $exon_type_out, $name_attributes);
+            push(@{$mrna_json_feature->{children}}, $exon);
+        }
+        elsif ($type =~ /$cds_types_in/) {
+            my $start = get_start($subfeature);
+            my $end = get_end($subfeature);
+            if (!defined $cds_feature) {
+                $cds_feature = $subfeature;
+            }
+            else {
+                my $cds_start = get_start($cds_feature);
+                my $cds_end = get_end($cds_feature);
+                if ($start < $cds_start) {
+                    $cds_feature->[0]->{start} = $start;
+                }
+                if ($end > $cds_end) {
+                    $cds_feature->[0]->{end} = $end;
+                }
+            }
+            my $cds_subfeatures = get_subfeatures($cds_feature);
+            if (scalar(@{$cds_subfeatures}) > 0) {
+                $rtsc_json_feature = convert_feature(${$cds_subfeatures}[0], "stop_codon_read_through", $name_attributes);
+            }
+        }
+        else {
+            print "Ignoring unsupported sub-feature type: $type\n";
+        }
+    }
+
+    if ($cds_feature) {
+        my $cds_json_feature = convert_feature($cds_feature, $cds_type_out, $name_attributes);
+        push(@{$cds_json_feature->{children}}, $rtsc_json_feature) if ($rtsc_json_feature);
+        push(@{$mrna_json_feature->{children}}, $cds_json_feature);
+    }
+
+    return $mrna_json_feature;
+}
+
+sub print_features {
+    my $data = shift;
+    my $operation = shift;
+    while (my ($seq_id, $features) = each(%{$data})) {
+        my @to_be_written = ();
+        my @ids = ();
+        print "{\"$operation\": [";
+        for (my $i = 0; $i < scalar(@${features}); ++$i) {
+            my $feature = $features->[$i];
+            print to_json(@$feature[0]);
+            print "," if ($i != scalar(@$features) - 1);
+        }
+        print "]}";
+    }
+}
diff --git a/tools/data/add_transcripts_from_gff3_to_annotations.pl b/tools/data/add_transcripts_from_gff3_to_annotations.pl
new file mode 100755
index 0000000..3a28d8c
--- /dev/null
+++ b/tools/data/add_transcripts_from_gff3_to_annotations.pl
@@ -0,0 +1,749 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib "$RealBin/../../jbrowse-download/src/perl5";
+use JBlibs;
+
+use File::Basename;
+use lib dirname($0);
+
+use Bio::GFF3::LowLevel::Parser;
+use Getopt::Long qw(:config no_ignore_case bundling);
+use IO::File;
+use LWP::UserAgent;
+use JSON;
+
+my $gene_types_in = "gene";
+my $pseudogene_types_in = "pseudogene";
+my $transcript_types_in = "transcript|mRNA";
+my $exon_types_in = "exon";
+my $cds_types_in = "CDS";
+my $ontology = "sequence";
+my $gene_type_out = "gene";
+my $pseudogene_type_out = "pseudogene";
+my $mrna_type_out = "mRNA";
+my $transcript_type_out = "transcript";
+my $exon_type_out = "exon";
+my $cds_type_out = "CDS";
+my $annotation_track_prefix = "";
+my $property_ontology = "feature_property";
+my $comment_type_out = "comment";
+my $property_type_out = "feature_property";
+my $disable_cds_recalculation = 0;
+my $organism;
+my $in = \*STDIN;
+my $username;
+my $password;
+my $url;
+my $session_id;
+my $name_attributes="Name";
+my $test = 0;
+my $use_name_for_feature = 0;
+
+my $success_log;
+my $error_log;
+
+my $chunk_size = 100;
+
+my $json = new JSON();
+my $ua = new LWP::UserAgent();
+$ua->timeout(3000); # 5 minutes
+
+my %source_features = ();
+my %skip_ids = ();
+
+my %reserved_properties = (     symbol      => 1,
+                                description => 1,
+                                status      => 1
+                          );
+my %ignored_properties = (  owner               => 1,
+                            date_creation       => 1,
+                            date_last_modified  => 1
+                         );
+
+$| = 1;
+
+parse_options();
+print_deprecation_note();
+exit();
+
+sub parse_options {
+    my $help;
+    my $input_file;
+    my $success_log_file;
+    my $error_log_file;
+    my $skip_file;
+    GetOptions("input|i=s"          => \$input_file,
+           "username|u=s"       => \$username,
+           "password|p=s"       => \$password,
+           "url|U=s"            => \$url,
+           "gene_types_in|g=s"      => \$gene_types_in,
+           "pseudogene_type_in|n=s" => \$pseudogene_types_in,
+           "transcript_types_in|t=s"    => \$transcript_types_in,
+           "exon_types_in|e=s"      => \$exon_types_in,
+           "organism|o=s"      => \$organism,
+           "cds_types_in|d=s"       => \$cds_types_in,
+           "ontology|O=s"       => \$ontology,
+           "gene_type_out|G=s"      => \$gene_type_out,
+           "pseudogene_type_out|N=s" => \$pseudogene_type_out,
+           "mrna_type_out|M=s"    => \$mrna_type_out,
+           "transcript_type_out|T=s" => \$transcript_type_out,
+           "exon_type_out|E=s"      => \$exon_type_out,
+           "cds_type_out|D=s"       => \$cds_type_out,
+           "property_ontolgy|R=s"   => \$property_ontology,
+           "comment_type_out|C=s"   => \$comment_type_out,
+           "property_type_out|S=s"  => \$property_type_out,
+           "track_prefix|P=s"       => \$annotation_track_prefix,
+           "disable_cds_recalculation|X"   => \$disable_cds_recalculation,
+           "success_log|l=s"        => \$success_log_file,
+           "error_log|L=s"      => \$error_log_file,
+           "skip|s=s"           => \$skip_file,
+           "test|x"           => \$test,
+           "help|h"         => \$help,
+           "name_attributes=s"   => \$name_attributes,
+           "use_name_for_feature|a" => \$use_name_for_feature);
+
+    print_usage() if $help;
+    $in = new IO::File($input_file) or die "Error reading $input_file: $!\n"
+        if $input_file;
+    $success_log = new IO::File($success_log_file, "w") or die "Error writing success log $success_log_file: $!\n" if $success_log_file;
+    $error_log = new IO::File($error_log_file, "w") or die "Error writing error log $error_log_file: $!\n" if $error_log_file;
+    if ($skip_file) {
+        my $fh = new IO::File($skip_file) or die "Error reading $skip_file: $!\n";
+        while (my $id = <$fh>) {
+            chomp $id;
+            ++$skip_ids{$id};
+        }
+    }
+	print_usage() if(!$username && !$password && !$url);
+    die "Missing required parameter: username\n" if !$username;
+    die "Missing required parameter: password\n" if !$password;
+    die "Missing required parameter: url\n" if !$url;
+    $url = "http://$url" if $url !~ m%https?://%;
+}
+
+sub print_usage {
+    print_deprecation_note();
+    my $progname = basename($0);
+    die << "END";
+
+usage: $progname
+    --url|-U <URL to Apollo instance>
+    --username|-u <username>
+    --password|-p <password>
+    [--genes_types_in|-g <gene types for input>]
+    [--pseudogene_type_in|-n <pseudogene type for input>]
+    [--transcript_types_in|-t <transcript types for input>]
+    [--exon_types_in|-e <exon types for input>]
+    [--cds_types_in|-d <CDS types for input>]
+    [--ontology|-O <ontology name used in server>]
+    [--gene_type_out|-G <gene type used in server>]
+    [--pseudogene_type_out|-N <pseudogene type used in server>]
+    [--mrna_type_out|-T <mRNA type used in server>]
+    [--transcript_type_out|-T <transcript type used in server>]
+    [--exon_type_out|-E <exon type used in server>]
+    [--cds_type_out|-D <CDS type used in server>]
+    [--property_ontology|-R <property ontology name used in server>]
+    [--comment_type_out|-C <comment type used in server>]
+    [--property_type_out|-S <feature property type used in server>]
+    [--track_prefix|-P <annotation track prefix>]
+    [--disable_cds_recalculation|-X]
+    [--input|-i <GFF3 file>]
+    [--organism|-o <the name field used for your organism in WA2>]
+    [--success_log|-l <success log file>]
+    [--error_log|-L <error log file>]
+    [--skip|-s <skip id file>]
+    [--test|-x]
+    [--help|-h]
+    [--name_attributes <feature attribute to be used as name, first found used>]
+    [--use_name_for_feature|-a]
+
+    U: URL to Apollo instance
+    u: username to access Apollo
+    p: password to access Apollo
+    g: string/regex to define the GFF3 types to treat as genes
+       [default: "$gene_types_in"]
+    n: string/regex to define the GFF3 types to treat as pseudogenes
+       [default: "$pseudogene_types_in"]
+    t: string/regex to define the GFF3 types to treat as transcripts
+       [default: "$transcript_types_in"]
+    e: string/regex to define the GFF3 types to treat as exons
+       [default: "$exon_types_in"]
+    d: string/regex to define the GFF3 types to treat as CDS
+       [default: "$cds_types_in"]
+    O: ontology name used in Apollo instance
+       [default: "$ontology"]
+    G: gene type used in Apollo instance
+       [default: "$gene_type_out"]
+    N: pseudogene type used in Apollo instance
+       [default: "$pseudogene_type_out"]
+    M: mRNA type used in Apollo instance
+       [default: "$mrna_type_out"]
+    T: transcript type used in Apollo instance
+       [default: "$transcript_type_out"]
+    E: exon type used in Apollo instance
+       [default: "$exon_type_out"]
+    D: CDS type used in Apollo instance
+       [default: "$cds_type_out"]
+    R: property ontology name used in Apollo instance
+       [default: "$property_ontology"]
+    C: comment type used in Apollo instance
+       [default: "$comment_type_out"]
+    S: feature property type used in Apollo instance
+       [default: "$property_type_out"]
+    P: annotation track prefix
+       [default: "$annotation_track_prefix"]
+    X: disable the recalculation of CDS by Apollo for protein coding features
+    i: input GFF3 file
+       [default: STDIN]
+    o: organism common name in Apollo instance
+    a: preserve feature names, as-is, in Apollo
+    l: log file for ids that were successfully processed
+    L: log file for ids that were erroneously processed
+    s: file with ids to skip
+    x: test mode
+    h: this help screen
+END
+}
+
+sub print_deprecation_note {
+    print "\n\n";
+    print "*********************************************************************************************\n";
+    print "* This script has been deprecated and replaced by add_features_from_gff3_to_annotations.pl, *\n";
+    print "* which supports importing all types of annotations that are currently supported by Apollo. *\n";
+    print "*********************************************************************************************\n";
+    print "\n\n";
+}
+
+sub process_gff {
+    my $gffio = Bio::GFF3::LowLevel::Parser->open($in);
+    my $seq_ids_to_transcripts = {};
+    my $seq_ids_to_genes = {};
+    while (my $features = $gffio->next_item()) {
+        next if ref $features ne "ARRAY";
+        process_gff_entry($features, $seq_ids_to_genes, $seq_ids_to_transcripts);
+    }
+    if ($test) {
+        print_features($seq_ids_to_genes);
+        print_features($seq_ids_to_transcripts);
+    }
+    else {
+        write_features("addFeature", $seq_ids_to_genes);
+        write_features("addTranscript", $seq_ids_to_transcripts);
+        if (!scalar(keys(%{$seq_ids_to_genes})) && !scalar(keys(%{$seq_ids_to_transcripts}))) {
+            print "No genes of type \"$gene_types_in\" or transcripts of type \"$transcript_types_in\" found\n";
+        }
+    }
+
+}
+
+sub write_features {
+    my $operation = shift;
+    my $data = shift;
+    while (my ($seq_id, $features) = each(%{$data})) {
+        print "Processing $seq_id\n";
+        my @to_be_written = ();
+        my @ids = ();
+        my @error = ();
+        my $chunk = 0;
+        for (my $i = 0; $i < scalar(@{$features}); ++$i) {
+            my $feature = $features->[$i];
+            push @to_be_written, $feature->[0];
+            push @ids, $feature->[1];
+            if (($i + 1) % $chunk_size == 0 || $i == scalar(@{$features}) - 1) {
+                print "Processing chunk " . ++$chunk . ": ";
+                my $res = send_request($seq_id, $operation, \@to_be_written);
+                if ($res->{error}) {
+                    print "error: $res->{error}\n";
+                    for (my $j = 0; $j < scalar(@to_be_written); ++$j) {
+                        push @error, [$to_be_written[$j], $ids[$j]];
+                    }
+                }
+                else {
+                    print "success\n";
+                    foreach my $id (@ids) {
+                        print $success_log "$id\n" if $success_log;
+                    }
+                }
+                @to_be_written = ();
+                @ids = ();
+            }
+        }
+        if (scalar(@error)) {
+            print "Processing individual features for failed chunks\n";
+            foreach my $feature (@error) {
+                print "Processing $feature->[1]: ";
+                my $res = send_request($seq_id, $operation, [$feature->[0]]);
+                if ($res->{error}) {
+                    print "error: $res->{error}\n";
+                    print $error_log "$feature->[1]\n" if $error_log;
+                }
+                else {
+                    print "success\n";
+                    print $success_log "$feature->[1]\n" if $success_log;
+                }
+            }
+        }
+    }
+}
+
+sub convert_feature {
+    my $features = shift;
+    my $type = shift;
+    my $name_attributes = shift;
+    my $start = get_start($features);
+    my $end = get_end($features);
+    my $strand = get_strand($features);
+    my $name = get_name($features,$name_attributes);
+    my $comments = get_comments($features);
+    my $dbxrefs = get_dbxrefs($features);
+    my $ontology_terms = get_ontology_terms($features);
+    my $json_feature = {
+        location => {
+            fmin => $start - 1,
+            fmax => $end,
+            strand => $strand
+        },
+        type => {
+            cv => {
+                name => $ontology
+            },
+            name => $type
+        }
+    };
+    if ($name) {
+        $json_feature->{name} = $name;
+    }
+    my $json_properties = [];
+    foreach my $comment (@{$comments}) {
+        my $json_comment = {
+            value => $comment,
+            type => {
+                cv => {
+                    name => $property_ontology
+                },
+                name => $comment_type_out
+            }
+        };
+        push @{$json_properties}, $json_comment;
+    }
+    foreach my $feature (@{$features}) {
+        while (my ($tag, $values) = each(%{$feature->{attributes}})) {
+            next if $tag =~ /^[A-Z]/;
+            next if $ignored_properties{$tag};
+            if ($tag =~ /symbol/) {
+                $json_feature->{symbol} = $values->[0];
+                next;
+            }
+            if ($tag =~ /description/) {
+                $json_feature->{description} = $values->[0];
+                next;
+            }
+
+            my $type = $tag;
+            foreach my $value (@{$values}) {
+                my $json_property = {
+                    value => $value,
+                    type => {
+                        cv => {
+                            name => $property_ontology
+                        },
+                        name => $type
+                    }
+                };
+                push @{$json_properties}, $json_property;
+            }
+        }
+        if (scalar(@{$json_properties})) {
+            $json_feature->{properties} = $json_properties;
+        }
+    }
+    my $json_dbxrefs = [];
+    foreach my $dbxref (@{$dbxrefs}) {
+        my ($db, $accession) = split ":", $dbxref;
+        my $json_dbxref = {
+            accession => $accession,
+            db => {
+                name => $db
+            }
+        };
+        push @{$json_dbxrefs}, $json_dbxref;
+    }
+    foreach my $ontology_term (@{$ontology_terms}) {
+        my @ontology_term_parts = split /:/, $ontology_term;
+        my $json_ontology_term = {
+            accession => $ontology_term_parts[1],
+            db => {
+                name => $ontology_term_parts[0]
+            }
+        };
+        push @{$json_dbxrefs}, $json_ontology_term;
+    }
+    if (scalar(@{$dbxrefs})) {
+        $json_feature->{dbxrefs} = $json_dbxrefs;
+    }
+    return $json_feature;
+}
+
+sub get_editor_url {
+    return "$url/annotationEditor";
+}
+
+
+sub send_request {
+    my $seq_id = shift;
+    my $operation = shift;
+    my $features = shift;
+    my $request = {
+            track => "$annotation_track_prefix$seq_id",
+            operation => $operation,
+            username => $username,
+            password => $password
+    };
+    if($organism) {
+        $request->{organism}= $organism;
+    }
+
+    if ($features) {
+        foreach my $feature (@{$features}) {
+            push(@{$request->{features}}, $feature);
+        }
+    }
+    my $req = new HTTP::Request();
+    $req->method("POST");
+
+    my $newurl = get_editor_url();
+    $newurl = "$newurl/$operation";
+    print "$newurl \n";
+    $req->uri($newurl);
+    $req->content($json->encode($request));
+    my $res = $ua->request($req);
+    if ($res->is_success()) {
+        return $json->decode($res->content());
+    }
+    else {
+        my $content;
+        my $message;
+        eval {
+            $content = $json->decode($res->content());
+        };
+        if ($@) {
+            $message = $res->status_line;
+        }
+        else {
+            $message = $content->{error} ? $content->{error} : $res->status_line;
+        }
+        return { error => $message };
+    }
+}
+
+sub get_name {
+    my $features = shift;
+    my $name_attributes = shift;
+    my @attrs = split /\|/, $name_attributes;
+    my $name;
+    ATTR:
+    foreach my $attr (@attrs) {
+        $name = $features->[0]->{attributes}->{$attr}->[0];
+        last ATTR unless !$name;
+    }
+    return $name;
+}
+
+sub get_comments {
+    my $features = shift;
+    my @comments;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Note};
+        push @comments, @{$values} if $values;
+    }
+    return \@comments;
+}
+
+sub get_dbxrefs {
+    my $features = shift;
+    my @dbxrefs;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Dbxref};
+        push @dbxrefs, @{$values} if $values;
+    }
+    return \@dbxrefs;
+}
+
+sub get_ontology_terms {
+    my $features = shift;
+    my @ontology_terms;
+    foreach my $feature (@{$features}) {
+        my $values = $feature->{attributes}->{Ontology_term};
+        push @ontology_terms, @{$values} if $values;
+    }
+    return \@ontology_terms;
+}
+
+sub get_seq_id {
+    my $features = shift;
+    return $features->[0]->{seq_id};
+}
+
+sub get_start {
+    my $features = shift;
+    my $start = undef;
+    foreach my $feature (@{$features}) {
+        if (!defined $start || $feature->{start} < $start) {
+            $start = $feature->{start};
+        }
+    }
+    return int($start);
+}
+
+sub get_end {
+    my $features = shift;
+    my $end = undef;
+    foreach my $feature (@{$features}) {
+        if (!defined $end || $feature->{end} > $end) {
+            $end = $feature->{end};
+        }
+    }
+    return int($end);
+}
+
+sub get_strand {
+    my $features = shift;
+    my $strand = $features->[0]->{strand};
+    if ($strand eq "+") {
+        return 1;
+    }
+    elsif ($strand eq "-") {
+        return -1;
+    }
+    return 0;
+}
+
+sub get_id {
+    my $features = shift;
+    return $features->[0]->{attributes}->{ID}->[0];
+}
+
+sub get_type {
+    my $features = shift;
+    return $features->[0]->{type};
+}
+
+sub process_gene {
+    my $features = shift;
+    my $gene_type = get_type($features);
+    my $gene;
+    if ($gene_type =~ $pseudogene_types_in) {
+        $gene = convert_feature($features, $pseudogene_type_out, $name_attributes);
+    }
+    else {
+        $gene = convert_feature($features, $gene_type_out, $name_attributes);
+    }
+    if ($use_name_for_feature) {
+        $gene->{use_name} = "true";
+    }
+    my $subfeatures = get_subfeatures($features);
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$transcript_types_in/) {
+            my $transcript = process_transcript($subfeature);
+            push(@{$gene->{children}}, $transcript) if $transcript;
+        }
+    }
+    return $gene;
+}
+
+sub process_transcript {
+    my $features = shift;
+    my $original_type = get_type($features);
+    my $transcript;
+    if ($original_type =~ /transcript/) {
+        $transcript = convert_feature($features, $transcript_type_out, $name_attributes);
+    }
+    else {
+        $transcript = convert_feature($features, $mrna_type_out, $name_attributes);
+    }
+
+    if ($use_name_for_feature) {
+        $transcript->{use_name} = "true";
+    }
+    my $cds_feature = undef;
+    my $subfeatures = get_subfeatures($features);
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$exon_types_in/) {
+            my $exon = convert_feature($subfeature, $exon_type_out,$name_attributes);
+            push(@{$transcript->{children}}, $exon);
+        }
+        elsif ($type =~ /$cds_types_in/) {
+            my $start = get_start($subfeature);
+            my $end = get_end($subfeature);
+            if (!defined $cds_feature) {
+                $cds_feature = $subfeature;
+            }
+            else {
+                my $cds_start = get_start($cds_feature);
+                my $cds_end = get_end($cds_feature);
+                if ($start < $cds_start) {
+                    $cds_feature->[0]->{start} = $start;
+                }
+                if ($end > $cds_end) {
+                    $cds_feature->[0]->{end} = $end;
+                }
+            }
+        }
+    }
+    if ($cds_feature) {
+        my $cds = convert_feature($cds_feature, $cds_type_out, $name_attributes);
+        push(@{$transcript->{children}}, $cds);
+    }
+    return $transcript;
+}
+
+sub get_subfeatures {
+    my @subfeatures;
+    my $features = shift;
+    foreach my $feature (@{$features}) {
+        push @subfeatures, @{$feature->{child_features}};
+    }
+    return \@subfeatures;
+}
+
+sub process_gff_entry {
+    my $features = shift;
+    my $seq_ids_to_genes = shift;
+    my $seq_ids_to_transcripts = shift;
+    my $type = get_type($features);
+    if ($type !~ /$gene_types_in/ && $type !~ /$transcript_types_in/) {
+        foreach my $subfeature (@{get_subfeatures($features)}) {
+            process_gff_entry($subfeature, $seq_ids_to_genes, $seq_ids_to_transcripts);
+        }
+    }
+    else {
+        my @json_features = @{process_feature($features)};
+        if (scalar(@json_features) != 0) {
+            foreach my $json_feature (@json_features) {
+                my $id = get_id($features);
+                if ($skip_ids{$id}) {
+                    print "Skipping $id\n";
+                    next;
+                }
+                my $seq_id = get_seq_id($features);
+                if ($json_feature->{type}->{name} =~ /$gene_types_in/) {
+                    push(@{$seq_ids_to_genes->{$seq_id}}, [$json_feature, $id]);
+                }
+                else {
+                    push(@{$seq_ids_to_transcripts->{$seq_id}}, [$json_feature, $id]);
+                }
+            }
+        }
+    }
+}
+
+sub process_feature {
+    my $features = shift;
+    my $type = get_type($features);
+    my $transcript_type = get_subfeature_type($features);
+
+    if ($type =~ /$gene_types_in/) {
+        if ($transcript_type =~ /mRNA/) {
+            # process with mRNA at top-level and gene information as 'parent'
+            return process_mrna($features);
+        }
+        else {
+            # process gene
+            return [process_gene($features)];
+        }
+    }
+    elsif ($type =~ /$transcript_types_in/) {
+        return [process_transcript($features)];
+    }
+    return undef;
+}
+
+sub get_subfeature_type {
+    my $features = shift;
+    return ${features}->[0]->{child_features}->[0]->[0]->{type};
+}
+
+sub process_mrna {
+    my $features = shift;
+    my $gene = convert_feature($features, $gene_type_out, $name_attributes);
+    my $subfeatures = get_subfeatures($features);
+    my @processed_mrnas;
+
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /mRNA/) {
+            my $mrna_json_feature = convert_mrna_feature($subfeature, $gene);
+            push(@processed_mrnas, $mrna_json_feature);
+        }
+    }
+    return \@processed_mrnas;
+}
+
+sub convert_mrna_feature {
+    my $features = shift;
+    my $gene_json_feature = shift;
+    my $cds_feature = undef;
+    my $mrna_json_feature = convert_feature($features, $mrna_type_out, $name_attributes);
+    if ($disable_cds_recalculation) {
+        $mrna_json_feature->{use_cds} = "true";
+    }
+    if ($use_name_for_feature) {
+        $mrna_json_feature->{use_name} = "true";
+    }
+    $mrna_json_feature->{parent} = $gene_json_feature;
+    my $subfeatures = get_subfeatures($features);
+
+    foreach my $subfeature (@{$subfeatures}) {
+        my $type = get_type($subfeature);
+        if ($type =~ /$exon_types_in/) {
+            my $exon = convert_feature($subfeature, $exon_type_out, $name_attributes);
+            push(@{$mrna_json_feature->{children}}, $exon);
+        }
+        elsif ($type =~ /$cds_types_in/) {
+            my $start = get_start($subfeature);
+            my $end = get_end($subfeature);
+            if (!defined $cds_feature) {
+                $cds_feature = $subfeature;
+            }
+            else {
+                my $cds_start = get_start($cds_feature);
+                my $cds_end = get_end($cds_feature);
+                if ($start < $cds_start) {
+                    $cds_feature->[0]->{start} = $start;
+                }
+                if ($end > $cds_end) {
+                    $cds_feature->[0]->{end} = $end;
+                }
+            }
+        }
+        else {
+            print "Ignoring unsupported sub-feature type: $type\n";
+        }
+    }
+
+    if ($cds_feature) {
+        my $cds = convert_feature($cds_feature, $cds_type_out, $name_attributes);
+        push(@{$mrna_json_feature->{children}}, $cds);
+    }
+
+    return $mrna_json_feature;
+}
+
+sub print_features {
+    my $data = shift;
+    while (my ($seq_id, $features) = each(%{$data})) {
+        my @to_be_written = ();
+        my @ids = ();
+        for (my $i = 0; $i < scalar(@${features}); ++$i) {
+            my $feature = $features->[$i];
+            print to_json(@$feature[0]);
+            print "\n";
+        }
+    }
+}
diff --git a/web-app/WEB-INF/applicationContext.xml b/web-app/WEB-INF/applicationContext.xml
new file mode 100644
index 0000000..130e70d
--- /dev/null
+++ b/web-app/WEB-INF/applicationContext.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<bean id="grailsApplication" class="org.codehaus.groovy.grails.commons.GrailsApplicationFactoryBean">
+		<description>Grails application factory bean</description>
+		<property name="grailsDescriptor" value="/WEB-INF/grails.xml" />
+	</bean>
+
+	<bean id="pluginManager" class="org.codehaus.groovy.grails.plugins.GrailsPluginManagerFactoryBean">
+		<description>A bean that manages Grails plugins</description>
+		<property name="grailsDescriptor" value="/WEB-INF/grails.xml" />
+		<property name="application" ref="grailsApplication" />
+	</bean>
+
+	<bean id="grailsConfigurator" class="org.codehaus.groovy.grails.commons.spring.GrailsRuntimeConfigurator">
+		<constructor-arg>
+			<ref bean="grailsApplication" />
+		</constructor-arg>
+		<property name="pluginManager" ref="pluginManager" />
+	</bean>
+
+	<bean id="characterEncodingFilter" class="org.springframework.web.filter.CharacterEncodingFilter">
+		<property name="encoding">
+			<value>utf-8</value>
+		</property>
+	</bean>
+
+	<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" />
+</beans>
\ No newline at end of file
diff --git a/web-app/WEB-INF/sitemesh.xml b/web-app/WEB-INF/sitemesh.xml
new file mode 100644
index 0000000..72399ce
--- /dev/null
+++ b/web-app/WEB-INF/sitemesh.xml
@@ -0,0 +1,14 @@
+<sitemesh>
+    <page-parsers>
+        <parser content-type="text/html"
+            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+        <parser content-type="text/html;charset=ISO-8859-1"
+            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+        <parser content-type="text/html;charset=UTF-8"
+            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+    </page-parsers>
+
+    <decorator-mappers>
+        <mapper class="org.codehaus.groovy.grails.web.sitemesh.GrailsLayoutDecoratorMapper" />
+    </decorator-mappers>
+</sitemesh>
\ No newline at end of file
diff --git a/web-app/WEB-INF/tld/c.tld b/web-app/WEB-INF/tld/c.tld
new file mode 100644
index 0000000..5e18236
--- /dev/null
+++ b/web-app/WEB-INF/tld/c.tld
@@ -0,0 +1,572 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<taglib xmlns="http://java.sun.com/xml/ns/javaee"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
+    version="2.1">
+
+  <description>JSTL 1.2 core library</description>
+  <display-name>JSTL core</display-name>
+  <tlib-version>1.2</tlib-version>
+  <short-name>c</short-name>
+  <uri>http://java.sun.com/jsp/jstl/core</uri>
+
+  <validator>
+    <description>
+        Provides core validation features for JSTL tags.
+    </description>
+    <validator-class>
+        org.apache.taglibs.standard.tlv.JstlCoreTLV
+    </validator-class>
+  </validator>
+
+  <tag>
+    <description>
+        Catches any Throwable that occurs in its body and optionally
+        exposes it.
+    </description>
+    <name>catch</name>
+    <tag-class>org.apache.taglibs.standard.tag.common.core.CatchTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+exception thrown from a nested action. The type of the
+scoped variable is the type of the exception thrown.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+	Simple conditional tag that establishes a context for
+	mutually exclusive conditional operations, marked by
+	<when> and <otherwise>
+    </description>
+    <name>choose</name>
+    <tag-class>org.apache.taglibs.standard.tag.common.core.ChooseTag</tag-class>
+    <body-content>JSP</body-content>
+  </tag>
+
+  <tag>
+    <description>
+	Simple conditional tag, which evalutes its body if the
+	supplied condition is true and optionally exposes a Boolean
+	scripting variable representing the evaluation of this condition
+    </description>
+    <name>if</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.IfTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+The test condition that determines whether or
+not the body content should be processed.
+        </description>
+        <name>test</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+	<type>boolean</type>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+resulting value of the test condition. The type
+of the scoped variable is Boolean.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope for var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Retrieves an absolute or relative URL and exposes its contents
+        to either the page, a String in 'var', or a Reader in 'varReader'.
+    </description>
+    <name>import</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.ImportTag</tag-class>
+    <tei-class>org.apache.taglibs.standard.tei.ImportTEI</tei-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+The URL of the resource to import.
+        </description>
+        <name>url</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+resource's content. The type of the scoped
+variable is String.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope for var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+resource's content. The type of the scoped
+variable is Reader.
+        </description>
+        <name>varReader</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the context when accessing a relative
+URL resource that belongs to a foreign
+context.
+        </description>
+        <name>context</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Character encoding of the content at the input
+resource.
+        </description>
+        <name>charEncoding</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+	The basic iteration tag, accepting many different
+        collection types and supporting subsetting and other
+        functionality
+    </description>
+    <name>forEach</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.ForEachTag</tag-class>
+    <tei-class>org.apache.taglibs.standard.tei.ForEachTEI</tei-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Collection of items to iterate over.
+        </description>
+	<name>items</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>java.lang.Object</type>
+        <deferred-value>
+	    <type>java.lang.Object</type>
+        </deferred-value>
+    </attribute>
+    <attribute>
+        <description>
+If items specified:
+Iteration begins at the item located at the
+specified index. First item of the collection has
+index 0.
+If items not specified:
+Iteration begins with index set at the value
+specified.
+        </description>
+	<name>begin</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+If items specified:
+Iteration ends at the item located at the
+specified index (inclusive).
+If items not specified:
+Iteration ends when index reaches the value
+specified.
+        </description>
+	<name>end</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+Iteration will only process every step items of
+the collection, starting with the first one.
+        </description>
+	<name>step</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+current item of the iteration. This scoped
+variable has nested visibility. Its type depends
+on the object of the underlying collection.
+        </description>
+	<name>var</name>
+	<required>false</required>
+	<rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+status of the iteration. Object exported is of type
+javax.servlet.jsp.jstl.core.LoopTagStatus. This scoped variable has nested
+visibility.
+        </description>
+	<name>varStatus</name>
+	<required>false</required>
+	<rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+	Iterates over tokens, separated by the supplied delimeters
+    </description>
+    <name>forTokens</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.ForTokensTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+String of tokens to iterate over.
+        </description>
+	<name>items</name>
+	<required>true</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>java.lang.String</type>
+        <deferred-value>
+	    <type>java.lang.String</type>
+        </deferred-value>
+    </attribute>
+    <attribute>
+        <description>
+The set of delimiters (the characters that
+separate the tokens in the string).
+        </description>
+	<name>delims</name>
+	<required>true</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>java.lang.String</type>
+    </attribute>
+    <attribute>
+        <description>
+Iteration begins at the token located at the
+specified index. First token has index 0.
+        </description>
+	<name>begin</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+Iteration ends at the token located at the
+specified index (inclusive).
+        </description>
+	<name>end</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+Iteration will only process every step tokens
+of the string, starting with the first one.
+        </description>
+	<name>step</name>
+	<required>false</required>
+	<rtexprvalue>true</rtexprvalue>
+	<type>int</type>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+current item of the iteration. This scoped
+variable has nested visibility.
+        </description>
+	<name>var</name>
+	<required>false</required>
+	<rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+status of the iteration. Object exported is of
+type
+javax.servlet.jsp.jstl.core.LoopTag
+Status. This scoped variable has nested
+visibility.
+        </description>
+	<name>varStatus</name>
+	<required>false</required>
+	<rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Like <%= ... >, but for expressions.
+    </description>
+    <name>out</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.OutTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Expression to be evaluated.
+        </description>
+        <name>value</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Default value if the resulting value is null.
+        </description>
+        <name>default</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Determines whether characters <,>,&,'," in the
+resulting string should be converted to their
+corresponding character entity codes. Default value is
+true.
+        </description>
+        <name>escapeXml</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+
+  <tag>
+    <description>
+        Subtag of <choose> that follows <when> tags
+        and runs only if all of the prior conditions evaluated to
+        'false'
+    </description>
+    <name>otherwise</name>
+    <tag-class>org.apache.taglibs.standard.tag.common.core.OtherwiseTag</tag-class>
+    <body-content>JSP</body-content>
+  </tag>
+
+  <tag>
+    <description>
+        Adds a parameter to a containing 'import' tag's URL.
+    </description>
+    <name>param</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.ParamTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Name of the query string parameter.
+        </description>
+        <name>name</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Value of the parameter.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Redirects to a new URL.
+    </description>
+    <name>redirect</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.RedirectTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+The URL of the resource to redirect to.
+        </description>
+        <name>url</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the context when redirecting to a relative URL
+resource that belongs to a foreign context.
+        </description>
+        <name>context</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Removes a scoped variable (from a particular scope, if specified).
+    </description>
+    <name>remove</name>
+    <tag-class>org.apache.taglibs.standard.tag.common.core.RemoveTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+Name of the scoped variable to be removed.
+        </description>
+        <name>var</name>
+        <required>true</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope for var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+ <tag>
+    <description>
+        Sets the result of an expression evaluation in a 'scope'
+    </description>
+    <name>set</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.SetTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Name of the exported scoped variable to hold the value
+specified in the action. The type of the scoped variable is
+whatever type the value expression evaluates to.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Expression to be evaluated.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+        <deferred-value>
+	    <type>java.lang.Object</type>
+        </deferred-value>
+    </attribute>
+    <attribute>
+        <description>
+Target object whose property will be set. Must evaluate to
+a JavaBeans object with setter property property, or to a
+java.util.Map object.
+        </description>
+        <name>target</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the property to be set in the target object.
+        </description>
+        <name>property</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope for var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Creates a URL with optional query parameters.
+    </description>
+    <name>url</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.UrlTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Name of the exported scoped variable for the
+processed url. The type of the scoped variable is
+String.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope for var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+URL to be processed.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the context when specifying a relative URL
+resource that belongs to a foreign context.
+        </description>
+        <name>context</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+	Subtag of <choose> that includes its body if its
+	condition evalutes to 'true'
+    </description>
+    <name>when</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.core.WhenTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+The test condition that determines whether or not the
+body content should be processed.
+        </description>
+        <name>test</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+	<type>boolean</type>
+    </attribute>
+  </tag>
+
+</taglib>
diff --git a/web-app/WEB-INF/tld/fmt.tld b/web-app/WEB-INF/tld/fmt.tld
new file mode 100644
index 0000000..2ae4776
--- /dev/null
+++ b/web-app/WEB-INF/tld/fmt.tld
@@ -0,0 +1,671 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<taglib xmlns="http://java.sun.com/xml/ns/javaee"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
+    version="2.1">
+
+  <description>JSTL 1.2 i18n-capable formatting library</description>
+  <display-name>JSTL fmt</display-name>
+  <tlib-version>1.2</tlib-version>
+  <short-name>fmt</short-name>
+  <uri>http://java.sun.com/jsp/jstl/fmt</uri>
+
+  <validator>
+    <description>
+        Provides core validation features for JSTL tags.
+    </description>
+    <validator-class>
+        org.apache.taglibs.standard.tlv.JstlFmtTLV
+    </validator-class>
+  </validator>
+
+  <tag>
+    <description>
+        Sets the request character encoding
+    </description>
+    <name>requestEncoding</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.RequestEncodingTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+Name of character encoding to be applied when
+decoding request parameters.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Stores the given locale in the locale configuration variable
+    </description>
+    <name>setLocale</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.SetLocaleTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+A String value is interpreted as the
+printable representation of a locale, which
+must contain a two-letter (lower-case)
+language code (as defined by ISO-639),
+and may contain a two-letter (upper-case)
+country code (as defined by ISO-3166).
+Language and country codes must be
+separated by hyphen (-) or underscore
+(_).
+	</description>
+        <name>value</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Vendor- or browser-specific variant.
+See the java.util.Locale javadocs for
+more information on variants.
+        </description>
+        <name>variant</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of the locale configuration variable.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Specifies the time zone for any time formatting or parsing actions
+        nested in its body
+    </description>
+    <name>timeZone</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.TimeZoneTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+The time zone. A String value is interpreted as
+a time zone ID. This may be one of the time zone
+IDs supported by the Java platform (such as
+"America/Los_Angeles") or a custom time zone
+ID (such as "GMT-8"). See
+java.util.TimeZone for more information on
+supported time zone formats.
+        </description>
+        <name>value</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Stores the given time zone in the time zone configuration variable
+    </description>
+    <name>setTimeZone</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.SetTimeZoneTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+The time zone. A String value is interpreted as
+a time zone ID. This may be one of the time zone
+IDs supported by the Java platform (such as
+"America/Los_Angeles") or a custom time zone
+ID (such as "GMT-8"). See java.util.TimeZone for
+more information on supported time zone
+formats.
+        </description>
+        <name>value</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable which
+stores the time zone of type
+java.util.TimeZone.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var or the time zone configuration
+variable.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Loads a resource bundle to be used by its tag body
+    </description>
+    <name>bundle</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.BundleTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Resource bundle base name. This is the bundle's
+fully-qualified resource name, which has the same
+form as a fully-qualified class name, that is, it uses
+"." as the package component separator and does not
+have any file type (such as ".class" or ".properties")
+suffix.
+        </description>
+        <name>basename</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Prefix to be prepended to the value of the message
+key of any nested <fmt:message> action.
+        </description>
+        <name>prefix</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Loads a resource bundle and stores it in the named scoped variable or
+        the bundle configuration variable
+    </description>
+    <name>setBundle</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.SetBundleTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+Resource bundle base name. This is the bundle's
+fully-qualified resource name, which has the same
+form as a fully-qualified class name, that is, it uses
+"." as the package component separator and does not
+have any file type (such as ".class" or ".properties")
+suffix.
+        </description>
+        <name>basename</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable which stores
+the i18n localization context of type
+javax.servlet.jsp.jstl.fmt.LocalizationC
+ontext.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var or the localization context
+configuration variable.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Maps key to localized message and performs parametric replacement
+    </description>
+    <name>message</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.MessageTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Message key to be looked up.
+        </description>
+        <name>key</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Localization context in whose resource
+bundle the message key is looked up.
+        </description>
+        <name>bundle</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable
+which stores the localized message.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Supplies an argument for parametric replacement to a containing
+        <message> tag
+    </description>
+    <name>param</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.ParamTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Argument used for parametric replacement.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Formats a numeric value as a number, currency, or percentage
+    </description>
+    <name>formatNumber</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.FormatNumberTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Numeric value to be formatted.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether the value is to be
+formatted as number, currency, or
+percentage.
+        </description>
+        <name>type</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Custom formatting pattern.
+        </description>
+        <name>pattern</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+ISO 4217 currency code. Applied only
+when formatting currencies (i.e. if type is
+equal to "currency"); ignored otherwise.
+        </description>
+        <name>currencyCode</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Currency symbol. Applied only when
+formatting currencies (i.e. if type is equal
+to "currency"); ignored otherwise.
+        </description>
+        <name>currencySymbol</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether the formatted output
+will contain any grouping separators.
+        </description>
+        <name>groupingUsed</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Maximum number of digits in the integer
+portion of the formatted output.
+        </description>
+        <name>maxIntegerDigits</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Minimum number of digits in the integer
+portion of the formatted output.
+        </description>
+        <name>minIntegerDigits</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Maximum number of digits in the
+fractional portion of the formatted output.
+        </description>
+        <name>maxFractionDigits</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Minimum number of digits in the
+fractional portion of the formatted output.
+        </description>
+        <name>minFractionDigits</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable
+which stores the formatted result as a
+String.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Parses the string representation of a number, currency, or percentage
+    </description>
+    <name>parseNumber</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.ParseNumberTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+String to be parsed.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether the string in the value
+attribute should be parsed as a number,
+currency, or percentage.
+        </description>
+        <name>type</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Custom formatting pattern that determines
+how the string in the value attribute is to be
+parsed.
+        </description>
+        <name>pattern</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Locale whose default formatting pattern (for
+numbers, currencies, or percentages,
+respectively) is to be used during the parse
+operation, or to which the pattern specified
+via the pattern attribute (if present) is
+applied.
+        </description>
+        <name>parseLocale</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether just the integer portion of
+the given value should be parsed.
+        </description>
+        <name>integerOnly</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable which
+stores the parsed result (of type
+java.lang.Number).
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Formats a date and/or time using the supplied styles and pattern
+    </description>
+    <name>formatDate</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.FormatDateTag</tag-class>
+    <body-content>empty</body-content>
+    <attribute>
+        <description>
+Date and/or time to be formatted.
+        </description>
+        <name>value</name>
+        <required>true</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether the time, the date, or both
+the time and date components of the given
+date are to be formatted.
+        </description>
+        <name>type</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Predefined formatting style for dates. Follows
+the semantics defined in class
+java.text.DateFormat. Applied only
+when formatting a date or both a date and
+time (i.e. if type is missing or is equal to
+"date" or "both"); ignored otherwise.
+        </description>
+        <name>dateStyle</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Predefined formatting style for times. Follows
+the semantics defined in class
+java.text.DateFormat. Applied only
+when formatting a time or both a date and
+time (i.e. if type is equal to "time" or "both");
+ignored otherwise.
+        </description>
+        <name>timeStyle</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Custom formatting style for dates and times.
+        </description>
+        <name>pattern</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Time zone in which to represent the formatted
+time.
+        </description>
+        <name>timeZone</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable which
+stores the formatted result as a String.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+  <tag>
+    <description>
+        Parses the string representation of a date and/or time
+    </description>
+    <name>parseDate</name>
+    <tag-class>org.apache.taglibs.standard.tag.rt.fmt.ParseDateTag</tag-class>
+    <body-content>JSP</body-content>
+    <attribute>
+        <description>
+Date string to be parsed.
+        </description>
+        <name>value</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Specifies whether the date string in the
+value attribute is supposed to contain a
+time, a date, or both.
+        </description>
+        <name>type</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Predefined formatting style for days
+which determines how the date
+component of the date string is to be
+parsed. Applied only when formatting a
+date or both a date and time (i.e. if type
+is missing or is equal to "date" or "both");
+ignored otherwise.
+        </description>
+        <name>dateStyle</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Predefined formatting styles for times
+which determines how the time
+component in the date string is to be
+parsed. Applied only when formatting a
+time or both a date and time (i.e. if type
+is equal to "time" or "both"); ignored
+otherwise.
+        </description>
+        <name>timeStyle</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Custom formatting pattern which
+determines how the date string is to be
+parsed.
+        </description>
+        <name>pattern</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Time zone in which to interpret any time
+information in the date string.
+        </description>
+        <name>timeZone</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Locale whose predefined formatting styles
+for dates and times are to be used during
+the parse operation, or to which the
+pattern specified via the pattern
+attribute (if present) is applied.
+        </description>
+        <name>parseLocale</name>
+        <required>false</required>
+        <rtexprvalue>true</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Name of the exported scoped variable in
+which the parsing result (of type
+java.util.Date) is stored.
+        </description>
+        <name>var</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+    <attribute>
+        <description>
+Scope of var.
+        </description>
+        <name>scope</name>
+        <required>false</required>
+        <rtexprvalue>false</rtexprvalue>
+    </attribute>
+  </tag>
+
+</taglib>
diff --git a/web-app/WEB-INF/tld/grails.tld b/web-app/WEB-INF/tld/grails.tld
new file mode 100644
index 0000000..9bd036b
--- /dev/null
+++ b/web-app/WEB-INF/tld/grails.tld
@@ -0,0 +1,550 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
+            http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
+        version="2.0">
+    <description>The Grails custom tag library</description>
+    <tlib-version>0.2</tlib-version>
+    <short-name>grails</short-name>
+    <uri>http://grails.codehaus.org/tags</uri>
+
+    <tag>
+        <name>link</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspLinkTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>action</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>controller</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>params</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>form</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspFormTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>action</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>controller</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>method</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>select</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspSelectTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>optionKey</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>optionValue</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>datePicker</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspDatePickerTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>precision</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>false</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>currencySelect</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspCurrencySelectTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>localeSelect</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspLocaleSelectTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>timeZoneSelect</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspTimeZoneSelectTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>checkBox</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspCheckboxTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>name</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>value</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>hasErrors</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspHasErrorsTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>model</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>bean</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>field</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>false</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>eachError</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>model</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>bean</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>field</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>false</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>renderErrors</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>model</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>bean</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>field</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>as</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>false</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>message</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspMessageTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>code</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>error</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>default</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>false</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>remoteFunction</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteFunctionTag</tag-class>
+        <body-content>empty</body-content>
+        <attribute>
+            <name>before</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>after</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>action</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>controller</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>params</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>asynchronous</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>method</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>update</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onSuccess</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onFailure</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onComplete</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoading</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoaded</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onInteractive</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>remoteLink</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteLinkTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>before</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>after</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>action</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>controller</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>params</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>asynchronous</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>method</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>update</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onSuccess</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onFailure</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onComplete</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoading</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoaded</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onInteractive</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>formRemote</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspFormRemoteTag</tag-class>
+        <body-content>JSP</body-content>
+        <attribute>
+            <name>before</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>after</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>action</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>controller</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>url</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>params</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>asynchronous</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>method</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>update</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onSuccess</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onFailure</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onComplete</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoading</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onLoaded</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <name>onInteractive</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+    <tag>
+        <name>invokeTag</name>
+        <tag-class>org.codehaus.groovy.grails.web.taglib.jsp.JspInvokeGrailsTagLibTag</tag-class>
+        <body-content>JSP</body-content>
+        <variable>
+            <name-given>it</name-given>
+            <variable-class>java.lang.Object</variable-class>
+            <declare>true</declare>
+            <scope>NESTED</scope>
+        </variable>
+        <attribute>
+            <name>tagName</name>
+            <required>true</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <dynamic-attributes>true</dynamic-attributes>
+    </tag>
+</taglib>
+
diff --git a/web-app/WEB-INF/tld/spring-form.tld b/web-app/WEB-INF/tld/spring-form.tld
new file mode 100644
index 0000000..1520a68
--- /dev/null
+++ b/web-app/WEB-INF/tld/spring-form.tld
@@ -0,0 +1,2411 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
+		version="2.0">
+
+	<description>Spring Framework JSP Form Tag Library</description>
+	<tlib-version>3.0</tlib-version>
+	<short-name>form</short-name>
+	<uri>http://www.springframework.org/tags/form</uri>
+
+	<tag>
+		<description>Renders an HTML 'form' tag and exposes a binding path to inner tags for binding.</description>
+		<name>form</name>
+		<tag-class>org.springframework.web.servlet.tags.form.FormTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute - added for backwards compatibility cases</description>
+			<name>name</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the model attribute under which the form object is exposed.
+				Defaults to 'command'.</description>
+			<name>modelAttribute</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the model attribute under which the form object is exposed.
+				Defaults to 'command'.</description>
+			<name>commandName</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Required Attribute</description>
+			<name>action</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>method</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>target</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>enctype</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Specifies the list of character encodings for input data that is accepted by the server processing this form. The value is a space- and/or comma-delimited list of charset values. The client must interpret this list as an exclusive-or list, i.e., the server is able to accept any single character encoding per entity received.</description>
+			<name>acceptCharset</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onsubmit</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onreset</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Common Optional Attribute</description>
+			<name>autocomplete</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The parameter name used for HTTP methods other then GET and POST. Default is '_method'</description>
+			<name>methodParam</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'input' tag with type 'text' using the bound value.</description>
+		<name>input</name>
+		<tag-class>org.springframework.web.servlet.tags.form.InputTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>size</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>maxlength</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>alt</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onselect</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will make the HTML element readonly.</description>
+			<name>readonly</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Common Optional Attribute</description>
+			<name>autocomplete</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'input' tag with type 'password' using the bound value.</description>
+		<name>password</name>
+		<tag-class>org.springframework.web.servlet.tags.form.PasswordInputTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>size</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>maxlength</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>alt</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onselect</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will make the HTML element readonly.</description>
+			<name>readonly</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Common Optional Attribute</description>
+			<name>autocomplete</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Is the password value to be shown? Defaults to false.</description>
+			<name>showPassword</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'input' tag with type 'hidden' using the bound value.</description>
+		<name>hidden</name>
+		<tag-class>org.springframework.web.servlet.tags.form.HiddenInputTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+		<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+			</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'select' element. Supports databinding to the selected option.</description>
+		<name>select</name>
+		<tag-class>org.springframework.web.servlet.tags.form.SelectTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The Collection, Map or array of objects used to generate the inner 'option' tags</description>
+			<name>items</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to 'value' attribute of the 'option' tag</description>
+			<name>itemValue</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to the inner text of the 'option' tag</description>
+			<name>itemLabel</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>size</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>multiple</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders a single HTML 'option'. Sets 'selected' as appropriate based on bound value.</description>
+		<name>option</name>
+		<tag-class>org.springframework.web.servlet.tags.form.OptionTag</tag-class>
+		<body-content>JSP</body-content>
+		<variable>
+			<description>The actual value bound to the 'value' attribute</description>
+			<name-given>value</name-given>
+			<variable-class>java.lang.Object</variable-class>
+		</variable>
+		<variable>
+			<description>The String representation of thr value bound to the 'value' attribute, taking into consideration
+				any PropertyEditor associated with the enclosing 'select' tag.</description>
+			<name-given>displayValue</name-given>
+			<variable-class>java.lang.String</variable-class>
+		</variable>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>value</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>label</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders a list of HTML 'option' tags. Sets 'selected' as appropriate based on bound value.</description>
+		<name>options</name>
+		<tag-class>org.springframework.web.servlet.tags.form.OptionsTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The Collection, Map or array of objects used to generate the inner 'option' tags. This attribute is required unless the containing select's property for data binding is an Enum, in which case the enum's values are used.</description>
+			<name>items</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to 'value' attribute of the 'option' tag</description>
+			<name>itemValue</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to the inner text of the 'option' tag</description>
+			<name>itemLabel</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'input' tag with type 'radio'.</description>
+		<name>radiobutton</name>
+		<tag-class>org.springframework.web.servlet.tags.form.RadioButtonTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>value</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Value to be displayed as part of the tag</description>
+			<name>label</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders multiple HTML 'input' tags with type 'radio'.</description>
+		<name>radiobuttons</name>
+		<tag-class>org.springframework.web.servlet.tags.form.RadioButtonsTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The Collection, Map or array of objects used to generate the 'input' tags with type 'radio'. This attribute is required unless the property for data binding is an Enum, in which case the enum's values are used.</description>
+			<name>items</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to 'value' attribute of the 'input' tags with type 'radio'</description>
+			<name>itemValue</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Value to be displayed as part of the 'input' tags with type 'radio'</description>
+			<name>itemLabel</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Delimiter to use between each 'input' tag with type 'radio'. There is no delimiter by default.</description>
+			<name>delimiter</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Specifies the HTML element that is used to enclose each 'input' tag with type 'radio'. Defaults to 'span'.</description>
+			<name>element</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'input' tag with type 'checkbox'.</description>
+		<name>checkbox</name>
+		<tag-class>org.springframework.web.servlet.tags.form.CheckboxTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute</description>
+			<name>value</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Value to be displayed as part of the tag</description>
+			<name>label</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders multiple HTML 'input' tags with type 'checkbox'.</description>
+		<name>checkboxes</name>
+		<tag-class>org.springframework.web.servlet.tags.form.CheckboxesTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The Collection, Map or array of objects used to generate the 'input' tags with type 'checkbox'</description>
+			<name>items</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Name of the property mapped to 'value' attribute of the 'input' tags with type 'checkbox'</description>
+			<name>itemValue</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Value to be displayed as part of the 'input' tags with type 'checkbox'</description>
+			<name>itemLabel</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Delimiter to use between each 'input' tag with type 'checkbox'. There is no delimiter by default.</description>
+			<name>delimiter</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Specifies the HTML element that is used to enclose each 'input' tag with type 'checkbox'. Defaults to 'span'.</description>
+			<name>element</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'textarea'.</description>
+		<name>textarea</name>
+		<tag-class>org.springframework.web.servlet.tags.form.TextareaTag</tag-class>
+		<body-content>empty</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used when the bound field has errors.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onfocus</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onblur</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onchange</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>accesskey</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Required Attribute</description>
+			<name>rows</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Required Attribute</description>
+			<name>cols</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onselect</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will make the HTML element readonly.</description>
+			<name>readonly</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders field errors in an HTML 'span' tag.</description>
+		<name>errors</name>
+		<tag-class>org.springframework.web.servlet.tags.form.ErrorsTag</tag-class>
+		<body-content>JSP</body-content>
+		<variable>
+			<name-given>messages</name-given>
+			<variable-class>java.util.List</variable-class>
+		</variable>
+		<attribute>
+			<description>Path to errors object for data binding</description>
+			<name>path</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Delimiter for displaying multiple error messages. Defaults to the br tag.</description>
+			<name>delimiter</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Specifies the HTML element that is used to render the enclosing errors.</description>
+			<name>element</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders a form field label in an HTML 'label' tag.</description>
+		<name>label</name>
+		<tag-class>org.springframework.web.servlet.tags.form.LabelTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>Path to property for data binding</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>id</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Enable/disable HTML escaping of rendered values.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>for</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute.</description>
+			<name>cssClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "class" - HTML Optional Attribute. Used only when errors are present.</description>
+			<name>cssErrorClass</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Equivalent to "style" - HTML Optional Attribute</description>
+			<name>cssStyle</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>lang</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>title</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>dir</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Standard Attribute</description>
+			<name>tabindex</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>ondblclick</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousedown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseover</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmousemove</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onmouseout</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeypress</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeyup</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Event Attribute</description>
+			<name>onkeydown</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+
+	<tag>
+		<description>Renders an HTML 'button' tag.</description>
+		<name>button</name>
+		<tag-class>org.springframework.web.servlet.tags.form.ButtonTag</tag-class>
+		<body-content>JSP</body-content>
+        <attribute>
+            <description>HTML Standard Attribute</description>
+            <name>id</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+        <attribute>
+            <description>The name attribute for the HTML button tag</description>
+            <name>name</name>
+            <required>false</required>
+            <rtexprvalue>true</rtexprvalue>
+        </attribute>
+		<attribute>
+			<description>The value attribute for the HTML button tag</description>
+			<name>value</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>HTML Optional Attribute. Setting the value of this attribute to 'true' (without the quotes) will disable the HTML element.</description>
+			<name>disabled</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<dynamic-attributes>true</dynamic-attributes>
+	</tag>
+	
+</taglib>
diff --git a/web-app/WEB-INF/tld/spring.tld b/web-app/WEB-INF/tld/spring.tld
new file mode 100644
index 0000000..a0a8c6f
--- /dev/null
+++ b/web-app/WEB-INF/tld/spring.tld
@@ -0,0 +1,457 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
+		version="2.0">
+
+	<description>Spring Framework JSP Tag Library</description>
+	<tlib-version>3.0</tlib-version>
+	<short-name>spring</short-name>
+	<uri>http://www.springframework.org/tags</uri>
+
+	<tag>
+		<description>
+			Sets default HTML escape value for the current page.
+			Overrides a "defaultHtmlEscape" context-param in web.xml, if any.
+		</description>
+		<name>htmlEscape</name>
+		<tag-class>org.springframework.web.servlet.tags.HtmlEscapeTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>Set the default value for HTML escaping, to be put
+				into the current PageContext.</description>
+			<name>defaultHtmlEscape</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Escapes its enclosed body content, applying HTML escaping and/or JavaScript escaping.
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml).
+		</description>
+		<name>escapeBody</name>
+		<tag-class>org.springframework.web.servlet.tags.EscapeBodyTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value. Overrides the
+			default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set JavaScript escaping for this tag, as boolean value.
+			Default is false.</description>
+			<name>javaScriptEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Retrieves the message with the given code, or text if code isn't resolvable.
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml).
+		</description>
+		<name>message</name>
+		<tag-class>org.springframework.web.servlet.tags.MessageTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>A MessageSourceResolvable argument (direct or through JSP EL).
+				Fits nicely when used in conjunction with Spring's own validation error
+				classes which all implement the MessageSourceResolvable interface. For
+				example, this allows you to iterate over all of the errors in a form,
+				passing each error (using a runtime expression) as the value of this
+				'message' attribute, thus effecting the easy display of such error
+				messages.</description>
+			<name>message</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The code (key) to use when looking up the message.
+			If code is not provided, the text attribute will be used.</description>
+			<name>code</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set optional message arguments for this tag, as a
+			(comma-)delimited String (each String argument can contain JSP EL),
+			an Object array (used as argument array), or a single Object (used
+			as single argument).</description>
+			<name>arguments</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The separator character to be used for splitting the
+			arguments string value; defaults to a 'comma' (',').</description>
+			<name>argumentSeparator</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Default text to output when a message for the given code
+			could not be found. If both text and code are not set, the tag will
+			output null.</description>
+			<name>text</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The string to use when binding the result to the page,
+			request, session or application scope. If not specified, the result
+			gets outputted to the writer (i.e. typically directly to the JSP).</description>
+			<name>var</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The scope to use when exporting the result to a variable.
+			This attribute is only used when var is also set. Possible values are
+			page, request, session and application.</description>
+			<name>scope</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value.
+			Overrides the default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set JavaScript escaping for this tag, as boolean value. Default is false.</description>
+			<name>javaScriptEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Retrieves the theme message with the given code, or text if code isn't resolvable.
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml).
+		</description>
+		<name>theme</name>
+		<tag-class>org.springframework.web.servlet.tags.ThemeTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>A MessageSourceResolvable argument (direct or through JSP EL).</description>
+			<name>message</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The code (key) to use when looking up the message.
+			If code is not provided, the text attribute will be used.</description>
+			<name>code</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set optional message arguments for this tag, as a
+			(comma-)delimited String (each String argument can contain JSP EL),
+			an Object array (used as argument array), or a single Object (used
+			as single argument).</description>
+			<name>arguments</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The separator character to be used for splitting the
+			arguments string value; defaults to a 'comma' (',').</description>
+			<name>argumentSeparator</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Default text to output when a message for the given code
+			could not be found. If both text and code are not set, the tag will
+			output null.</description>
+			<name>text</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The string to use when binding the result to the page,
+			request, session or application scope. If not specified, the result
+			gets outputted to the writer (i.e. typically directly to the JSP).</description>
+			<name>var</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The scope to use when exporting the result to a variable.
+			This attribute is only used when var is also set. Possible values are
+			page, request, session and application.</description>
+			<name>scope</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value.
+			Overrides the default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set JavaScript escaping for this tag, as boolean value. Default is false.</description>
+			<name>javaScriptEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Provides Errors instance in case of bind errors.
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml).
+		</description>
+		<name>hasBindErrors</name>
+		<tag-class>org.springframework.web.servlet.tags.BindErrorsTag</tag-class>
+		<body-content>JSP</body-content>
+		<variable>
+			<name-given>errors</name-given>
+			<variable-class>org.springframework.validation.Errors</variable-class>
+		</variable>
+		<attribute>
+			<description>The name of the bean in the request, that needs to be
+			inspected for errors. If errors are available for this bean, they
+			will be bound under the 'errors' key.</description>
+			<name>name</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value.
+			Overrides the default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Sets a nested path to be used by the bind tag's path.
+		</description>
+		<name>nestedPath</name>
+		<tag-class>org.springframework.web.servlet.tags.NestedPathTag</tag-class>
+		<body-content>JSP</body-content>
+		<variable>
+			<name-given>nestedPath</name-given>
+			<variable-class>java.lang.String</variable-class>
+		</variable>
+		<attribute>
+			<description>Set the path that this tag should apply. E.g. 'customer'
+			to allow bind paths like 'address.street' rather than
+			'customer.address.street'.</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Provides BindStatus object for the given bind path.
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml).
+		</description>
+		<name>bind</name>
+		<tag-class>org.springframework.web.servlet.tags.BindTag</tag-class>
+		<body-content>JSP</body-content>
+		<variable>
+			<name-given>status</name-given>
+			<variable-class>org.springframework.web.servlet.support.BindStatus</variable-class>
+		</variable>
+		<attribute>
+			<description>The path to the bean or bean property to bind status
+			information for. For instance account.name, company.address.zipCode
+			or just employee. The status object will exported to the page scope,
+			specifically for this bean or bean property</description>
+			<name>path</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set whether to ignore a nested path, if any. Default is to not ignore.</description>
+			<name>ignoreNestedPath</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value. Overrides
+			the default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>
+			Provides transformation of variables to Strings, using an appropriate
+			custom PropertyEditor from BindTag (can only be used inside BindTag).
+			The HTML escaping flag participates in a page-wide or application-wide setting
+			(i.e. by HtmlEscapeTag or a 'defaultHtmlEscape' context-param in web.xml).
+		</description>
+		<name>transform</name>
+		<tag-class>org.springframework.web.servlet.tags.TransformTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>The value to transform. This is the actual object you want
+			to have transformed (for instance a Date). Using the PropertyEditor that
+			is currently in use by the 'spring:bind' tag.</description>
+			<name>value</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The string to use when binding the result to the page,
+			request, session or application scope. If not specified, the result gets
+			outputted to the writer (i.e. typically directly to the JSP).</description>
+			<name>var</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The scope to use when exported the result to a variable.
+			This attribute is only used when var is also set. Possible values are
+			page, request, session and application.</description>
+			<name>scope</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as boolean value. Overrides
+			the default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>URL tag based on the JSTL c:url tag.  This variant is fully 
+		backwards compatible with the standard tag.  Enhancements include support 
+		for URL template parameters.</description>
+		<name>url</name>
+		<tag-class>org.springframework.web.servlet.tags.UrlTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>The URL to build.  This value can include template place holders 
+			that are replaced with the URL encoded value of the named parameter.  Parameters 
+			must be defined using the param tag inside the body of this tag.</description>
+			<name>value</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Specifies a remote application context path.  The default is the 
+			current application context path.</description>
+			<name>context</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The name of the variable to export the URL value to.</description>
+			<name>var</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The scope for the var.  'application', 'session', 'request' and 
+			'page' scopes are supported.  Defaults to page scope.  This attribute has no 
+			effect unless the var attribute is also defined.</description>
+			<name>scope</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as a boolean value. Overrides the
+			default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set JavaScript escaping for this tag, as a boolean value.
+			Default is false.</description>
+			<name>javaScriptEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>Parameter tag based on the JSTL c:param tag.  The sole purpose is to 
+		support params inside the spring:url tag.</description>
+		<name>param</name>
+		<tag-class>org.springframework.web.servlet.tags.ParamTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>The name of the parameter.</description>
+			<name>name</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The value of the parameter.</description>
+			<name>value</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+	<tag>
+		<description>Evaluates a Spring expression (SpEL) and either prints the result or assigns it to a variable.</description>
+		<name>eval</name>
+		<tag-class>org.springframework.web.servlet.tags.EvalTag</tag-class>
+		<body-content>JSP</body-content>
+		<attribute>
+			<description>The expression to evaluate.</description>
+			<name>expression</name>
+			<required>true</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The name of the variable to export the evaluation result to.</description>
+			<name>var</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>The scope for the var.  'application', 'session', 'request' and 
+			'page' scopes are supported.  Defaults to page scope.  This attribute has no 
+			effect unless the var attribute is also defined.</description>
+			<name>scope</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set HTML escaping for this tag, as a boolean value. Overrides the
+			default HTML escaping setting for the current page.</description>
+			<name>htmlEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+		<attribute>
+			<description>Set JavaScript escaping for this tag, as a boolean value.  Default is false.</description>
+			<name>javaScriptEscape</name>
+			<required>false</required>
+			<rtexprvalue>true</rtexprvalue>
+		</attribute>
+	</tag>
+
+</taglib>
diff --git a/web-app/css/Annotator.css b/web-app/css/Annotator.css
new file mode 100644
index 0000000..4be9b63
--- /dev/null
+++ b/web-app/css/Annotator.css
@@ -0,0 +1,42 @@
+/** Add css rules here for your application. */
+
+
+/** Example rules used by the template application (remove for your app) */
+h1 {
+  font-size: 2em;
+  font-weight: bold;
+  color: #777777;
+  margin: 40px 0px 70px;
+  text-align: center;
+}
+
+.sendButton {
+  display: block;
+  font-size: 16pt;
+}
+
+/** Most GWT widgets already have a style name defined */
+.gwt-DialogBox {
+  width: 400px;
+}
+
+.dialogVPanel {
+  margin: 5px;
+}
+
+.serverResponseLabelError {
+  color: red;
+}
+
+/** Set ids using widget.getElement().setId("idOfElement") */
+#closeButton {
+  margin: 15px 6px 6px;
+}
+
+/*.dropdown-menu .item {*/
+  /*padding: 5px;*/
+/*}*/
+
+/*.dropdown-menu .item-selected {*/
+  /*background-color: #eee;*/
+/*}*/
diff --git a/web-app/css/chado.css b/web-app/css/chado.css
new file mode 100644
index 0000000..4baa35f
--- /dev/null
+++ b/web-app/css/chado.css
@@ -0,0 +1,20 @@
+ at CHARSET "UTF-8";
+
+table {
+	border-width: 1px;
+	border-style: double;
+	border-color: black;
+	width: 90%;
+	margin-top: 10px;
+}
+
+td {
+	padding-right: 2em;
+	border-style: solid;
+	border-width: 1px;
+	border-color: gray;
+}
+
+td.uniquename {
+	width: 50%;
+}
diff --git a/web-app/css/login.css b/web-app/css/login.css
new file mode 100644
index 0000000..f2ad1bd
--- /dev/null
+++ b/web-app/css/login.css
@@ -0,0 +1,22 @@
+ at CHARSET "UTF-8";
+
+.user_login {
+	/* width:30%; */
+	margin:5px;
+}
+
+.fieldname {
+	margin-right:5px;
+	font-size: 1.3em;
+}
+
+.input_field {
+	width: 10em;
+	float: right;
+}
+
+.button_login {
+	text-align:center;
+	margin-top:10px;
+	width: 100%;
+}
\ No newline at end of file
diff --git a/web-app/css/search_sequence.css b/web-app/css/search_sequence.css
new file mode 100644
index 0000000..d57580c
--- /dev/null
+++ b/web-app/css/search_sequence.css
@@ -0,0 +1,78 @@
+ at CHARSET "UTF-8";
+
+.search_sequence_label {
+	font-size: 1.5em;
+}
+
+.search_sequence_input {
+	width: 50em;
+	height: 3.5em;
+}
+
+.search_sequence_area {
+	margin-bottom: 10px;
+}
+
+.search_all_ref_seqs_area {
+	margin-top: 5px;
+	margin-bottom: 5px;
+}
+
+.search_all_ref_seqs_checkbox {
+	margin-right: 3px;
+}
+
+.search_all_ref_seqs_label {
+	font-size: 1em;
+}
+
+.search_sequence_matches_row:hover {
+	background-color: #000099;
+	color: white;
+}
+
+.search_sequence_matches_generic_field {
+	width: 6.3em;
+	margin-right: 0.2em;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	font-size: 1.3em;
+	background-color: inherit;
+	display: table-cell;
+}
+
+.search_sequence_matches_header {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches_field {
+	margin-top: 0px;
+	margin-bottom: 0px;
+	float: left;
+	overflow: hidden;
+	width: 6em;
+}
+
+.search_sequence_tools {
+	margin-bottom: 10px;
+}
+
+.search_sequence_matches {
+	max-height: 25em;
+	overflow: auto;
+}
+
+.search_sequence_matches_row {
+}
+
+.search_sequence_matches_row-firefox {
+	display: table-row;
+}
+
+.search_sequence_message {
+	font-size: 1.3em;
+}
+
+.search_sequence {
+	font-size: 12px;
+}
\ No newline at end of file
diff --git a/web-app/css/ui-layout.css b/web-app/css/ui-layout.css
new file mode 100644
index 0000000..a53727b
--- /dev/null
+++ b/web-app/css/ui-layout.css
@@ -0,0 +1,49 @@
+
+/**
+  UI.Layout CSS
+*************************************/
+.stretch {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  /* Can be changed by hand ;)*/
+  overflow: auto;
+}
+
+.ui-splitbar{
+  display: -webkit-box;      /* OLD - iOS 6-, Safari 3.1-6 */
+  display: -moz-box;         /* OLD - Firefox 19- (buggy but mostly works) */
+  display: -ms-flexbox;      /* TWEENER - IE 10 */
+  display: -webkit-flex;     /* NEW - Chrome */
+  display: flex;             /* NEW, Spec - Opera 12.1, Firefox 20+ */
+  -webkit-justify-content: center;
+  justify-content: center;
+
+  background-color: #ffffff;
+  right: auto;
+  position: absolute;
+  z-index: 1010;
+}
+
+.ui-layout-row > .ui-splitbar{
+  height: 8px; width: 100%;
+  cursor: row-resize;
+  text-align: center;
+  justify-content: center;
+  align-items: center;
+}
+.ui-layout-column > .ui-splitbar{
+  width: 8px; height: 100%;
+  cursor: col-resize;
+  -webkit-flex-direction: column;
+  flex-direction: column;
+}
+
+.ui-layout-column > .ui-splitbar > a,
+.ui-layout-row > .ui-splitbar > a {
+  color: #fff;
+  cursor: pointer;
+  font-size: 9px;
+}
\ No newline at end of file
diff --git a/web-app/css/web_api_stylesheet.css b/web-app/css/web_api_stylesheet.css
new file mode 100644
index 0000000..146b065
--- /dev/null
+++ b/web-app/css/web_api_stylesheet.css
@@ -0,0 +1,31 @@
+body {
+  font-size: 1em;
+}
+
+h1 {
+  font-style: bold;
+  font-size: 150%;
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+}
+
+h2 {
+  font-style: bold;
+  font-size: 110%;
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+  margin-top: 25px;
+}
+
+.code {
+  border-style: dashed;
+  border-width: 1px;
+  margin-bottom: 15px;
+  padding: 5px;
+  background: #ffffcc;
+  white-space: pre;
+}
+
+.section {
+  margin-top: 30px;
+}
diff --git a/web-app/images/ApolloLogo_100x36.png b/web-app/images/ApolloLogo_100x36.png
new file mode 100644
index 0000000..aa04f44
Binary files /dev/null and b/web-app/images/ApolloLogo_100x36.png differ
diff --git a/web-app/images/loading.gif b/web-app/images/loading.gif
new file mode 100644
index 0000000..f2a1bc0
Binary files /dev/null and b/web-app/images/loading.gif differ
diff --git a/web-app/images/qrcode.26636394.png b/web-app/images/qrcode.26636394.png
new file mode 100644
index 0000000..308f3a3
Binary files /dev/null and b/web-app/images/qrcode.26636394.png differ
diff --git a/web-app/images/sign_in_green.png b/web-app/images/sign_in_green.png
new file mode 100644
index 0000000..7e84129
Binary files /dev/null and b/web-app/images/sign_in_green.png differ
diff --git a/web-app/images/user_icon_16px.png b/web-app/images/user_icon_16px.png
new file mode 100644
index 0000000..b79692c
Binary files /dev/null and b/web-app/images/user_icon_16px.png differ
diff --git a/web-app/images/webapollo_favicon.ico b/web-app/images/webapollo_favicon.ico
new file mode 100644
index 0000000..96cd853
Binary files /dev/null and b/web-app/images/webapollo_favicon.ico differ
diff --git a/web-app/js/DataTables-plugins/dataTablesPlugins.js b/web-app/js/DataTables-plugins/dataTablesPlugins.js
new file mode 100644
index 0000000..48b8de9
--- /dev/null
+++ b/web-app/js/DataTables-plugins/dataTablesPlugins.js
@@ -0,0 +1,36 @@
+$.fn.dataTableExt.oApi.fnAddDataAndDisplay = function ( oSettings, aData )
+{
+	/* Add the data */
+	var iAdded = this.oApi._fnAddData( oSettings, aData );
+	var nAdded = oSettings.aoData[ iAdded ].nTr;
+	 
+	/* Need to re-filter and re-sort the table to get positioning correct, not perfect
+	 * as this will actually redraw the table on screen, but the update should be so fast (and
+	 * possibly not alter what is already on display) that the user will not notice
+	 */
+	this.oApi._fnReDraw( oSettings );
+	 
+	/* Find it's position in the table */
+	var iPos = -1;
+	for( var i=0, iLen=oSettings.aiDisplay.length ; i<iLen ; i++ )
+	{
+		if( oSettings.aoData[ oSettings.aiDisplay[i] ].nTr == nAdded )
+		{
+			iPos = i;
+			break;
+		}
+	}
+	 
+	/* Get starting point, taking account of paging */
+	if( iPos >= 0 )
+	{
+		oSettings._iDisplayStart = ( Math.floor(i / oSettings._iDisplayLength) ) * oSettings._iDisplayLength;
+		this.oApi._fnCalculateEnd( oSettings );
+	}
+	 
+	this.oApi._fnDraw( oSettings );
+	return {
+		"nTr": nAdded,
+		"iPos": iAdded
+	};
+};
\ No newline at end of file
diff --git a/web-app/js/DataTables/css/demo_table.css b/web-app/js/DataTables/css/demo_table.css
new file mode 100644
index 0000000..12f352d
--- /dev/null
+++ b/web-app/js/DataTables/css/demo_table.css
@@ -0,0 +1,577 @@
+/*
+ *  File:         demo_table.css
+ *  CVS:          $Id$
+ *  Description:  CSS descriptions for DataTables demo pages
+ *  Author:       Allan Jardine
+ *  Created:      Tue May 12 06:47:22 BST 2009
+ *  Modified:     $Date$ by $Author$
+ *  Language:     CSS
+ *  Project:      DataTables
+ *
+ *  Copyright 2009 Allan Jardine. All Rights Reserved.
+ *
+ * ***************************************************************************
+ * DESCRIPTION
+ *
+ * The styles given here are suitable for the demos that are used with the standard DataTables
+ * distribution (see www.datatables.net). You will most likely wish to modify these styles to
+ * meet the layout requirements of your site.
+ *
+ * Common issues:
+ *   'full_numbers' pagination - I use an extra selector on the body tag to ensure that there is
+ *     no conflict between the two pagination types. If you want to use full_numbers pagination
+ *     ensure that you either have "example_alt_pagination" as a body class name, or better yet,
+ *     modify that selector.
+ *   Note that the path used for Images is relative. All images are by default located in
+ *     ../images/ - relative to this CSS file.
+ */
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables features
+ */
+
+.dataTables_wrapper {
+	position: relative;
+	clear: both;
+	zoom: 1; /* Feeling sorry for IE */
+}
+
+.dataTables_processing {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	width: 250px;
+	height: 30px;
+	margin-left: -125px;
+	margin-top: -15px;
+	padding: 14px 0 2px 0;
+	border: 1px solid #ddd;
+	text-align: center;
+	color: #999;
+	font-size: 14px;
+	background-color: white;
+}
+
+.dataTables_length {
+	width: 40%;
+	float: left;
+}
+
+.dataTables_filter {
+	width: 50%;
+	float: right;
+	text-align: right;
+}
+
+.dataTables_info {
+	width: 60%;
+	float: left;
+}
+
+.dataTables_paginate {
+	float: right;
+	text-align: right;
+}
+
+/* Pagination nested */
+.paginate_disabled_previous, .paginate_enabled_previous,
+.paginate_disabled_next, .paginate_enabled_next {
+	height: 19px;
+	float: left;
+	cursor: pointer;
+	*cursor: hand;
+	color: #111 !important;
+}
+.paginate_disabled_previous:hover, .paginate_enabled_previous:hover,
+.paginate_disabled_next:hover, .paginate_enabled_next:hover {
+	text-decoration: none !important;
+}
+.paginate_disabled_previous:active, .paginate_enabled_previous:active,
+.paginate_disabled_next:active, .paginate_enabled_next:active {
+	outline: none;
+}
+
+.paginate_disabled_previous,
+.paginate_disabled_next {
+	color: #666 !important;
+}
+.paginate_disabled_previous, .paginate_enabled_previous {
+	padding-left: 23px;
+}
+.paginate_disabled_next, .paginate_enabled_next {
+	padding-right: 23px;
+	margin-left: 10px;
+}
+
+.paginate_disabled_previous {
+	background: url('../images/back_disabled.png') no-repeat top left;
+}
+
+.paginate_enabled_previous {
+	background: url('../images/back_enabled.png') no-repeat top left;
+}
+.paginate_enabled_previous:hover {
+	background: url('../images/back_enabled_hover.png') no-repeat top left;
+}
+
+.paginate_disabled_next {
+	background: url('../images/forward_disabled.png') no-repeat top right;
+}
+
+.paginate_enabled_next {
+	background: url('../images/forward_enabled.png') no-repeat top right;
+}
+.paginate_enabled_next:hover {
+	background: url('../images/forward_enabled_hover.png') no-repeat top right;
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables display
+ */
+table.display {
+	margin: 0 auto;
+	clear: both;
+	width: 100%;
+	
+	/* Note Firefox 3.5 and before have a bug with border-collapse
+	 * ( https://bugzilla.mozilla.org/show%5Fbug.cgi?id=155955 ) 
+	 * border-spacing: 0; is one possible option. Conditional-css.com is
+	 * useful for this kind of thing
+	 *
+	 * Further note IE 6/7 has problems when calculating widths with border width.
+	 * It subtracts one px relative to the other browsers from the first column, and
+	 * adds one to the end...
+	 *
+	 * If you want that effect I'd suggest setting a border-top/left on th/td's and 
+	 * then filling in the gaps with other borders.
+	 */
+}
+
+table.display thead th {
+	padding: 3px 18px 3px 10px;
+	border-bottom: 1px solid black;
+	font-weight: bold;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+table.display tfoot th {
+	padding: 3px 18px 3px 10px;
+	border-top: 1px solid black;
+	font-weight: bold;
+}
+
+table.display tr.heading2 td {
+	border-bottom: 1px solid #aaa;
+}
+
+table.display td {
+	padding: 3px 10px;
+}
+
+table.display td.center {
+	text-align: center;
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables sorting
+ */
+
+.sorting_asc {
+	background: url('../images/sort_asc.png') no-repeat center right;
+}
+
+.sorting_desc {
+	background: url('../images/sort_desc.png') no-repeat center right;
+}
+
+.sorting {
+	background: url('../images/sort_both.png') no-repeat center right;
+}
+
+.sorting_asc_disabled {
+	background: url('../images/sort_asc_disabled.png') no-repeat center right;
+}
+
+.sorting_desc_disabled {
+	background: url('../images/sort_desc_disabled.png') no-repeat center right;
+}
+ 
+table.display thead th:active,
+table.display thead td:active {
+	outline: none;
+}
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables row classes
+ */
+table.display tr.odd.gradeA {
+	background-color: #ddffdd;
+}
+
+table.display tr.even.gradeA {
+	background-color: #eeffee;
+}
+
+table.display tr.odd.gradeC {
+	background-color: #ddddff;
+}
+
+table.display tr.even.gradeC {
+	background-color: #eeeeff;
+}
+
+table.display tr.odd.gradeX {
+	background-color: #ffdddd;
+}
+
+table.display tr.even.gradeX {
+	background-color: #ffeeee;
+}
+
+table.display tr.odd.gradeU {
+	background-color: #ddd;
+}
+
+table.display tr.even.gradeU {
+	background-color: #eee;
+}
+
+
+tr.odd {
+	background-color: #E2E4FF;
+}
+
+tr.even {
+	background-color: white;
+}
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Misc
+ */
+.dataTables_scroll {
+	clear: both;
+}
+
+.dataTables_scrollBody {
+	*margin-top: -1px;
+	-webkit-overflow-scrolling: touch;
+}
+
+.top, .bottom {
+	padding: 15px;
+	background-color: #F5F5F5;
+	border: 1px solid #CCCCCC;
+}
+
+.top .dataTables_info {
+	float: none;
+}
+
+.clear {
+	clear: both;
+}
+
+.dataTables_empty {
+	text-align: center;
+}
+
+tfoot input {
+	margin: 0.5em 0;
+	width: 100%;
+	color: #444;
+}
+
+tfoot input.search_init {
+	color: #999;
+}
+
+td.group {
+	background-color: #d1cfd0;
+	border-bottom: 2px solid #A19B9E;
+	border-top: 2px solid #A19B9E;
+}
+
+td.details {
+	background-color: #d1cfd0;
+	border: 2px solid #A19B9E;
+}
+
+
+.example_alt_pagination div.dataTables_info {
+	width: 40%;
+}
+
+.paging_full_numbers {
+	width: 400px;
+	height: 22px;
+	line-height: 22px;
+}
+
+.paging_full_numbers a:active {
+	outline: none
+}
+
+.paging_full_numbers a:hover {
+	text-decoration: none;
+}
+
+.paging_full_numbers a.paginate_button,
+ 	.paging_full_numbers a.paginate_active {
+	border: 1px solid #aaa;
+	-webkit-border-radius: 5px;
+	-moz-border-radius: 5px;
+	padding: 2px 5px;
+	margin: 0 3px;
+	cursor: pointer;
+	*cursor: hand;
+	color: #333 !important;
+}
+
+.paging_full_numbers a.paginate_button {
+	background-color: #ddd;
+}
+
+.paging_full_numbers a.paginate_button:hover {
+	background-color: #ccc;
+	text-decoration: none !important;
+}
+
+.paging_full_numbers a.paginate_active {
+	background-color: #99B3FF;
+}
+
+table.display tr.even.row_selected td {
+	background-color: #B0BED9;
+}
+
+table.display tr.odd.row_selected td {
+	background-color: #9FAFD1;
+}
+
+
+/*
+ * Sorting classes for columns
+ */
+/* For the standard odd/even */
+tr.odd td.sorting_1 {
+	background-color: #D3D6FF;
+}
+
+tr.odd td.sorting_2 {
+	background-color: #DADCFF;
+}
+
+tr.odd td.sorting_3 {
+	background-color: #E0E2FF;
+}
+
+tr.even td.sorting_1 {
+	background-color: #EAEBFF;
+}
+
+tr.even td.sorting_2 {
+	background-color: #F2F3FF;
+}
+
+tr.even td.sorting_3 {
+	background-color: #F9F9FF;
+}
+
+
+/* For the Conditional-CSS grading rows */
+/*
+ 	Colour calculations (based off the main row colours)
+  Level 1:
+		dd > c4
+		ee > d5
+	Level 2:
+	  dd > d1
+	  ee > e2
+ */
+tr.odd.gradeA td.sorting_1 {
+	background-color: #c4ffc4;
+}
+
+tr.odd.gradeA td.sorting_2 {
+	background-color: #d1ffd1;
+}
+
+tr.odd.gradeA td.sorting_3 {
+	background-color: #d1ffd1;
+}
+
+tr.even.gradeA td.sorting_1 {
+	background-color: #d5ffd5;
+}
+
+tr.even.gradeA td.sorting_2 {
+	background-color: #e2ffe2;
+}
+
+tr.even.gradeA td.sorting_3 {
+	background-color: #e2ffe2;
+}
+
+tr.odd.gradeC td.sorting_1 {
+	background-color: #c4c4ff;
+}
+
+tr.odd.gradeC td.sorting_2 {
+	background-color: #d1d1ff;
+}
+
+tr.odd.gradeC td.sorting_3 {
+	background-color: #d1d1ff;
+}
+
+tr.even.gradeC td.sorting_1 {
+	background-color: #d5d5ff;
+}
+
+tr.even.gradeC td.sorting_2 {
+	background-color: #e2e2ff;
+}
+
+tr.even.gradeC td.sorting_3 {
+	background-color: #e2e2ff;
+}
+
+tr.odd.gradeX td.sorting_1 {
+	background-color: #ffc4c4;
+}
+
+tr.odd.gradeX td.sorting_2 {
+	background-color: #ffd1d1;
+}
+
+tr.odd.gradeX td.sorting_3 {
+	background-color: #ffd1d1;
+}
+
+tr.even.gradeX td.sorting_1 {
+	background-color: #ffd5d5;
+}
+
+tr.even.gradeX td.sorting_2 {
+	background-color: #ffe2e2;
+}
+
+tr.even.gradeX td.sorting_3 {
+	background-color: #ffe2e2;
+}
+
+tr.odd.gradeU td.sorting_1 {
+	background-color: #c4c4c4;
+}
+
+tr.odd.gradeU td.sorting_2 {
+	background-color: #d1d1d1;
+}
+
+tr.odd.gradeU td.sorting_3 {
+	background-color: #d1d1d1;
+}
+
+tr.even.gradeU td.sorting_1 {
+	background-color: #d5d5d5;
+}
+
+tr.even.gradeU td.sorting_2 {
+	background-color: #e2e2e2;
+}
+
+tr.even.gradeU td.sorting_3 {
+	background-color: #e2e2e2;
+}
+
+
+/*
+ * Row highlighting example
+ */
+.ex_highlight #example tbody tr.even:hover, #example tbody tr.even td.highlighted {
+	background-color: #ECFFB3;
+}
+
+.ex_highlight #example tbody tr.odd:hover, #example tbody tr.odd td.highlighted {
+	background-color: #E6FF99;
+}
+
+.ex_highlight_row #example tr.even:hover {
+	background-color: #ECFFB3;
+}
+
+.ex_highlight_row #example tr.even:hover td.sorting_1 {
+	background-color: #DDFF75;
+}
+
+.ex_highlight_row #example tr.even:hover td.sorting_2 {
+	background-color: #E7FF9E;
+}
+
+.ex_highlight_row #example tr.even:hover td.sorting_3 {
+	background-color: #E2FF89;
+}
+
+.ex_highlight_row #example tr.odd:hover {
+	background-color: #E6FF99;
+}
+
+.ex_highlight_row #example tr.odd:hover td.sorting_1 {
+	background-color: #D6FF5C;
+}
+
+.ex_highlight_row #example tr.odd:hover td.sorting_2 {
+	background-color: #E0FF84;
+}
+
+.ex_highlight_row #example tr.odd:hover td.sorting_3 {
+	background-color: #DBFF70;
+}
+
+
+/*
+ * KeyTable
+ */
+table.KeyTable td {
+	border: 3px solid transparent;
+}
+
+table.KeyTable td.focus {
+	border: 3px solid #3366FF;
+}
+
+table.display tr.gradeA {
+	background-color: #eeffee;
+}
+
+table.display tr.gradeC {
+	background-color: #ddddff;
+}
+
+table.display tr.gradeX {
+	background-color: #ffdddd;
+}
+
+table.display tr.gradeU {
+	background-color: #ddd;
+}
+
+div.box {
+	height: 100px;
+	padding: 10px;
+	overflow: auto;
+	border: 1px solid #8080FF;
+	background-color: #E5E5FF;
+}
diff --git a/web-app/js/DataTables/images/Sorting icons.psd b/web-app/js/DataTables/images/Sorting icons.psd
new file mode 100644
index 0000000..53b2e06
Binary files /dev/null and b/web-app/js/DataTables/images/Sorting icons.psd differ
diff --git a/web-app/js/DataTables/images/back_disabled.png b/web-app/js/DataTables/images/back_disabled.png
new file mode 100644
index 0000000..881de79
Binary files /dev/null and b/web-app/js/DataTables/images/back_disabled.png differ
diff --git a/web-app/js/DataTables/images/back_enabled.png b/web-app/js/DataTables/images/back_enabled.png
new file mode 100644
index 0000000..c608682
Binary files /dev/null and b/web-app/js/DataTables/images/back_enabled.png differ
diff --git a/web-app/js/DataTables/images/back_enabled_hover.png b/web-app/js/DataTables/images/back_enabled_hover.png
new file mode 100644
index 0000000..d300f10
Binary files /dev/null and b/web-app/js/DataTables/images/back_enabled_hover.png differ
diff --git a/web-app/js/DataTables/images/favicon.ico b/web-app/js/DataTables/images/favicon.ico
new file mode 100644
index 0000000..6eeaa2a
Binary files /dev/null and b/web-app/js/DataTables/images/favicon.ico differ
diff --git a/web-app/js/DataTables/images/forward_disabled.png b/web-app/js/DataTables/images/forward_disabled.png
new file mode 100644
index 0000000..6a6ded7
Binary files /dev/null and b/web-app/js/DataTables/images/forward_disabled.png differ
diff --git a/web-app/js/DataTables/images/forward_enabled.png b/web-app/js/DataTables/images/forward_enabled.png
new file mode 100644
index 0000000..a4e6b53
Binary files /dev/null and b/web-app/js/DataTables/images/forward_enabled.png differ
diff --git a/web-app/js/DataTables/images/forward_enabled_hover.png b/web-app/js/DataTables/images/forward_enabled_hover.png
new file mode 100644
index 0000000..fc46c5e
Binary files /dev/null and b/web-app/js/DataTables/images/forward_enabled_hover.png differ
diff --git a/web-app/js/DataTables/images/sort_asc.png b/web-app/js/DataTables/images/sort_asc.png
new file mode 100644
index 0000000..a88d797
Binary files /dev/null and b/web-app/js/DataTables/images/sort_asc.png differ
diff --git a/web-app/js/DataTables/images/sort_asc_disabled.png b/web-app/js/DataTables/images/sort_asc_disabled.png
new file mode 100644
index 0000000..4e144cf
Binary files /dev/null and b/web-app/js/DataTables/images/sort_asc_disabled.png differ
diff --git a/web-app/js/DataTables/images/sort_both.png b/web-app/js/DataTables/images/sort_both.png
new file mode 100644
index 0000000..1867040
Binary files /dev/null and b/web-app/js/DataTables/images/sort_both.png differ
diff --git a/web-app/js/DataTables/images/sort_desc.png b/web-app/js/DataTables/images/sort_desc.png
new file mode 100644
index 0000000..def071e
Binary files /dev/null and b/web-app/js/DataTables/images/sort_desc.png differ
diff --git a/web-app/js/DataTables/images/sort_desc_disabled.png b/web-app/js/DataTables/images/sort_desc_disabled.png
new file mode 100644
index 0000000..7824973
Binary files /dev/null and b/web-app/js/DataTables/images/sort_desc_disabled.png differ
diff --git a/web-app/js/DataTables/js/jquery.dataTables.js b/web-app/js/DataTables/js/jquery.dataTables.js
new file mode 100644
index 0000000..1d8a220
--- /dev/null
+++ b/web-app/js/DataTables/js/jquery.dataTables.js
@@ -0,0 +1,12099 @@
+/**
+ * @summary     DataTables
+ * @description Paginate, search and sort HTML tables
+ * @version     1.9.4
+ * @file        jquery.dataTables.js
+ * @author      Allan Jardine (www.sprymedia.co.uk)
+ * @contact     www.sprymedia.co.uk/contact
+ *
+ * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ * 
+ * For details please refer to: http://www.datatables.net
+ */
+
+/*jslint evil: true, undef: true, browser: true */
+/*globals $, jQuery,define,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataTo [...]
+
+(/** @lends <global> */function( window, document, undefined ) {
+
+(function( factory ) {
+	"use strict";
+
+	// Define as an AMD module if possible
+	if ( typeof define === 'function' && define.amd )
+	{
+		define( ['jquery'], factory );
+	}
+	/* Define using browser globals otherwise
+	 * Prevent multiple instantiations if the script is loaded twice
+	 */
+	else if ( jQuery && !jQuery.fn.dataTable )
+	{
+		factory( jQuery );
+	}
+}
+(/** @lends <global> */function( $ ) {
+	"use strict";
+	/** 
+	 * DataTables is a plug-in for the jQuery Javascript library. It is a 
+	 * highly flexible tool, based upon the foundations of progressive 
+	 * enhancement, which will add advanced interaction controls to any 
+	 * HTML table. For a full list of features please refer to
+	 * <a href="http://datatables.net">DataTables.net</a>.
+	 *
+	 * Note that the <i>DataTable</i> object is not a global variable but is
+	 * aliased to <i>jQuery.fn.DataTable</i> and <i>jQuery.fn.dataTable</i> through which 
+	 * it may be  accessed.
+	 *
+	 *  @class
+	 *  @param {object} [oInit={}] Configuration object for DataTables. Options
+	 *    are defined by {@link DataTable.defaults}
+	 *  @requires jQuery 1.3+
+	 * 
+	 *  @example
+	 *    // Basic initialisation
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable();
+	 *    } );
+	 *  
+	 *  @example
+	 *    // Initialisation with configuration options - in this case, disable
+	 *    // pagination and sorting.
+	 *    $(document).ready( function {
+	 *      $('#example').dataTable( {
+	 *        "bPaginate": false,
+	 *        "bSort": false 
+	 *      } );
+	 *    } );
+	 */
+	var DataTable = function( oInit )
+	{
+		
+		
+		/**
+		 * Add a column to the list used for the table with default values
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nTh The th element for this column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddColumn( oSettings, nTh )
+		{
+			var oDefaults = DataTable.defaults.columns;
+			var iCol = oSettings.aoColumns.length;
+			var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
+				"sSortingClass": oSettings.oClasses.sSortable,
+				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
+				"nTh": nTh ? nTh : document.createElement('th'),
+				"sTitle":    oDefaults.sTitle    ? oDefaults.sTitle    : nTh ? nTh.innerHTML : '',
+				"aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
+				"mData": oDefaults.mData ? oDefaults.oDefaults : iCol
+			} );
+			oSettings.aoColumns.push( oCol );
+			
+			/* Add a column specific filter */
+			if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null )
+			{
+				oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch );
+			}
+			else
+			{
+				var oPre = oSettings.aoPreSearchCols[ iCol ];
+				
+				/* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */
+				if ( oPre.bRegex === undefined )
+				{
+					oPre.bRegex = true;
+				}
+				
+				if ( oPre.bSmart === undefined )
+				{
+					oPre.bSmart = true;
+				}
+				
+				if ( oPre.bCaseInsensitive === undefined )
+				{
+					oPre.bCaseInsensitive = true;
+				}
+			}
+			
+			/* Use the column options function to initialise classes etc */
+			_fnColumnOptions( oSettings, iCol, null );
+		}
+		
+		
+		/**
+		 * Apply options for a column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column index to consider
+		 *  @param {object} oOptions object with sType, bVisible and bSearchable etc
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOptions( oSettings, iCol, oOptions )
+		{
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* User specified column options */
+			if ( oOptions !== undefined && oOptions !== null )
+			{
+				/* Backwards compatibility for mDataProp */
+				if ( oOptions.mDataProp && !oOptions.mData )
+				{
+					oOptions.mData = oOptions.mDataProp;
+				}
+		
+				if ( oOptions.sType !== undefined )
+				{
+					oCol.sType = oOptions.sType;
+					oCol._bAutoType = false;
+				}
+				
+				$.extend( oCol, oOptions );
+				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+		
+				/* iDataSort to be applied (backwards compatibility), but aDataSort will take
+				 * priority if defined
+				 */
+				if ( oOptions.iDataSort !== undefined )
+				{
+					oCol.aDataSort = [ oOptions.iDataSort ];
+				}
+				_fnMap( oCol, oOptions, "aDataSort" );
+			}
+		
+			/* Cache the data get and set functions for speed */
+			var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null;
+			var mData = _fnGetObjectDataFn( oCol.mData );
+		
+			oCol.fnGetData = function (oData, sSpecific) {
+				var innerData = mData( oData, sSpecific );
+		
+				if ( oCol.mRender && (sSpecific && sSpecific !== '') )
+				{
+					return mRender( innerData, sSpecific, oData );
+				}
+				return innerData;
+			};
+			oCol.fnSetData = _fnSetObjectDataFn( oCol.mData );
+			
+			/* Feature sorting overrides column specific when off */
+			if ( !oSettings.oFeatures.bSort )
+			{
+				oCol.bSortable = false;
+			}
+			
+			/* Check that the class assignment is correct for sorting */
+			if ( !oCol.bSortable ||
+				 ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableNone;
+				oCol.sSortingClassJUI = "";
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortable;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
+			}
+			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )
+			{
+				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
+				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
+			}
+		}
+		
+		
+		/**
+		 * Adjust the table column widths for new data. Note: you would probably want to 
+		 * do a redraw after calling this function!
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAdjustColumnSizing ( oSettings )
+		{
+			/* Not interested in doing column width calculation if auto-width is disabled */
+			if ( oSettings.oFeatures.bAutoWidth === false )
+			{
+				return false;
+			}
+			
+			_fnCalculateColumnWidths( oSettings );
+			for ( var i=0 , iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;
+			}
+		}
+		
+		
+		/**
+		 * Covert the index of a visible column to the index in the data array (take account
+		 * of hidden columns)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMatch Visible column index to lookup
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisibleToColumnIndex( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+		
+			return typeof aiVis[iMatch] === 'number' ?
+				aiVis[iMatch] :
+				null;
+		}
+		
+		
+		/**
+		 * Covert the index of an index in the data array and convert it to the visible
+		 *   column index (take account of hidden columns)
+		 *  @param {int} iMatch Column index to lookup
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the data index
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnIndexToVisible( oSettings, iMatch )
+		{
+			var aiVis = _fnGetColumns( oSettings, 'bVisible' );
+			var iPos = $.inArray( iMatch, aiVis );
+		
+			return iPos !== -1 ? iPos : null;
+		}
+		
+		
+		/**
+		 * Get the number of visible columns
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {int} i the number of visible columns
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnVisbleColumns( oSettings )
+		{
+			return _fnGetColumns( oSettings, 'bVisible' ).length;
+		}
+		
+		
+		/**
+		 * Get an array of column indexes that match a given property
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sParam Parameter in aoColumns to look for - typically 
+		 *    bVisible or bSearchable
+		 *  @returns {array} Array of indexes with matched properties
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetColumns( oSettings, sParam )
+		{
+			var a = [];
+		
+			$.map( oSettings.aoColumns, function(val, i) {
+				if ( val[sParam] ) {
+					a.push( i );
+				}
+			} );
+		
+			return a;
+		}
+		
+		
+		/**
+		 * Get the sort type based on an input string
+		 *  @param {string} sData data we wish to know the type of
+		 *  @returns {string} type (defaults to 'string' if no type can be detected)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectType( sData )
+		{
+			var aTypes = DataTable.ext.aTypes;
+			var iLen = aTypes.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				var sType = aTypes[i]( sData );
+				if ( sType !== null )
+				{
+					return sType;
+				}
+			}
+			
+			return 'string';
+		}
+		
+		
+		/**
+		 * Figure out how to reorder a display list
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {int} aiReturn index list for reordering
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReOrderIndex ( oSettings, sColumns )
+		{
+			var aColumns = sColumns.split(',');
+			var aiReturn = [];
+			
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				for ( var j=0 ; j<iLen ; j++ )
+				{
+					if ( oSettings.aoColumns[i].sName == aColumns[j] )
+					{
+						aiReturn.push( j );
+						break;
+					}
+				}
+			}
+			
+			return aiReturn;
+		}
+		
+		
+		/**
+		 * Get the column ordering that DataTables expects
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {string} comma separated list of names
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnColumnOrdering ( oSettings )
+		{
+			var sNames = '';
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				sNames += oSettings.aoColumns[i].sName+',';
+			}
+			if ( sNames.length == iLen )
+			{
+				return "";
+			}
+			return sNames.slice(0, -1);
+		}
+		
+		
+		/**
+		 * Take the column definitions and static columns arrays and calculate how
+		 * they relate to column indexes. The callback function will then apply the
+		 * definition found for a column to a suitable configuration object.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aoColDefs The aoColumnDefs array that is to be applied
+		 *  @param {array} aoCols The aoColumns array that defines columns individually
+		 *  @param {function} fn Callback function - takes two parameters, the calculated
+		 *    column index and the definition for that column.
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn )
+		{
+			var i, iLen, j, jLen, k, kLen;
+		
+			// Column definitions with aTargets
+			if ( aoColDefs )
+			{
+				/* Loop over the definitions array - loop in reverse so first instance has priority */
+				for ( i=aoColDefs.length-1 ; i>=0 ; i-- )
+				{
+					/* Each definition can target multiple columns, as it is an array */
+					var aTargets = aoColDefs[i].aTargets;
+					if ( !$.isArray( aTargets ) )
+					{
+						_fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );
+					}
+		
+					for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )
+					{
+						if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 )
+						{
+							/* Add columns that we don't yet know about */
+							while( oSettings.aoColumns.length <= aTargets[j] )
+							{
+								_fnAddColumn( oSettings );
+							}
+		
+							/* Integer, basic index */
+							fn( aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
+						{
+							/* Negative integer, right to left column counting */
+							fn( oSettings.aoColumns.length+aTargets[j], aoColDefs[i] );
+						}
+						else if ( typeof aTargets[j] === 'string' )
+						{
+							/* Class name matching on TH element */
+							for ( k=0, kLen=oSettings.aoColumns.length ; k<kLen ; k++ )
+							{
+								if ( aTargets[j] == "_all" ||
+								     $(oSettings.aoColumns[k].nTh).hasClass( aTargets[j] ) )
+								{
+									fn( k, aoColDefs[i] );
+								}
+							}
+						}
+					}
+				}
+			}
+		
+			// Statically defined columns array
+			if ( aoCols )
+			{
+				for ( i=0, iLen=aoCols.length ; i<iLen ; i++ )
+				{
+					fn( i, aoCols[i] );
+				}
+			}
+		}
+		
+		/**
+		 * Add a data array to the table, creating DOM node etc. This is the parallel to 
+		 * _fnGatherData, but for adding rows from a Javascript source, rather than a
+		 * DOM source.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData data array to be added
+		 *  @returns {int} >=0 if successful (index of new aoData entry), -1 if failed
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddData ( oSettings, aDataSupplied )
+		{
+			var oCol;
+			
+			/* Take an independent copy of the data source so we can bash it about as we wish */
+			var aDataIn = ($.isArray(aDataSupplied)) ?
+				aDataSupplied.slice() :
+				$.extend( true, {}, aDataSupplied );
+			
+			/* Create the object for storing information about this new row */
+			var iRow = oSettings.aoData.length;
+			var oData = $.extend( true, {}, DataTable.models.oRow );
+			oData._aData = aDataIn;
+			oSettings.aoData.push( oData );
+		
+			/* Create the cells */
+			var nTd, sThisType;
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oCol = oSettings.aoColumns[i];
+		
+				/* Use rendered data for filtering / sorting */
+				if ( typeof oCol.fnRender === 'function' && oCol.bUseRendered && oCol.mData !== null )
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnRender(oSettings, iRow, i) );
+				}
+				else
+				{
+					_fnSetCellData( oSettings, iRow, i, _fnGetCellData( oSettings, iRow, i ) );
+				}
+				
+				/* See if we should auto-detect the column type */
+				if ( oCol._bAutoType && oCol.sType != 'string' )
+				{
+					/* Attempt to auto detect the type - same as _fnGatherData() */
+					var sVarType = _fnGetCellData( oSettings, iRow, i, 'type' );
+					if ( sVarType !== null && sVarType !== '' )
+					{
+						sThisType = _fnDetectType( sVarType );
+						if ( oCol.sType === null )
+						{
+							oCol.sType = sThisType;
+						}
+						else if ( oCol.sType != sThisType && oCol.sType != "html" )
+						{
+							/* String is always the 'fallback' option */
+							oCol.sType = 'string';
+						}
+					}
+				}
+			}
+			
+			/* Add to the display array */
+			oSettings.aiDisplayMaster.push( iRow );
+		
+			/* Create the DOM information */
+			if ( !oSettings.oFeatures.bDeferRender )
+			{
+				_fnCreateTr( oSettings, iRow );
+			}
+		
+			return iRow;
+		}
+		
+		
+		/**
+		 * Read in the data from the target table from the DOM
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGatherData( oSettings )
+		{
+			var iLoop, i, iLen, j, jLen, jInner,
+			 	nTds, nTrs, nTd, nTr, aLocalData, iThisIndex,
+				iRow, iRows, iColumn, iColumns, sNodeName,
+				oCol, oData;
+			
+			/*
+			 * Process by row first
+			 * Add the data object for the whole table - storing the tr node. Note - no point in getting
+			 * DOM based data if we are going to go and replace it with Ajax source data.
+			 */
+			if ( oSettings.bDeferLoading || oSettings.sAjaxSource === null )
+			{
+				nTr = oSettings.nTBody.firstChild;
+				while ( nTr )
+				{
+					if ( nTr.nodeName.toUpperCase() == "TR" )
+					{
+						iThisIndex = oSettings.aoData.length;
+						nTr._DT_RowIndex = iThisIndex;
+						oSettings.aoData.push( $.extend( true, {}, DataTable.models.oRow, {
+							"nTr": nTr
+						} ) );
+		
+						oSettings.aiDisplayMaster.push( iThisIndex );
+						nTd = nTr.firstChild;
+						jInner = 0;
+						while ( nTd )
+						{
+							sNodeName = nTd.nodeName.toUpperCase();
+							if ( sNodeName == "TD" || sNodeName == "TH" )
+							{
+								_fnSetCellData( oSettings, iThisIndex, jInner, $.trim(nTd.innerHTML) );
+								jInner++;
+							}
+							nTd = nTd.nextSibling;
+						}
+					}
+					nTr = nTr.nextSibling;
+				}
+			}
+			
+			/* Gather in the TD elements of the Table - note that this is basically the same as
+			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet
+			 * setup!
+			 */
+			nTrs = _fnGetTrNodes( oSettings );
+			nTds = [];
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTd = nTrs[i].firstChild;
+				while ( nTd )
+				{
+					sNodeName = nTd.nodeName.toUpperCase();
+					if ( sNodeName == "TD" || sNodeName == "TH" )
+					{
+						nTds.push( nTd );
+					}
+					nTd = nTd.nextSibling;
+				}
+			}
+			
+			/* Now process by column */
+			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+			{
+				oCol = oSettings.aoColumns[iColumn];
+		
+				/* Get the title of the column - unless there is a user set one */
+				if ( oCol.sTitle === null )
+				{
+					oCol.sTitle = oCol.nTh.innerHTML;
+				}
+				
+				var
+					bAutoType = oCol._bAutoType,
+					bRender = typeof oCol.fnRender === 'function',
+					bClass = oCol.sClass !== null,
+					bVisible = oCol.bVisible,
+					nCell, sThisType, sRendered, sValType;
+				
+				/* A single loop to rule them all (and be more efficient) */
+				if ( bAutoType || bRender || bClass || !bVisible )
+				{
+					for ( iRow=0, iRows=oSettings.aoData.length ; iRow<iRows ; iRow++ )
+					{
+						oData = oSettings.aoData[iRow];
+						nCell = nTds[ (iRow*iColumns) + iColumn ];
+						
+						/* Type detection */
+						if ( bAutoType && oCol.sType != 'string' )
+						{
+							sValType = _fnGetCellData( oSettings, iRow, iColumn, 'type' );
+							if ( sValType !== '' )
+							{
+								sThisType = _fnDetectType( sValType );
+								if ( oCol.sType === null )
+								{
+									oCol.sType = sThisType;
+								}
+								else if ( oCol.sType != sThisType && 
+								          oCol.sType != "html" )
+								{
+									/* String is always the 'fallback' option */
+									oCol.sType = 'string';
+								}
+							}
+						}
+		
+						if ( oCol.mRender )
+						{
+							// mRender has been defined, so we need to get the value and set it
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						else if ( oCol.mData !== iColumn )
+						{
+							// If mData is not the same as the column number, then we need to
+							// get the dev set value. If it is the column, no point in wasting
+							// time setting the value that is already there!
+							nCell.innerHTML = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+						}
+						
+						/* Rendering */
+						if ( bRender )
+						{
+							sRendered = _fnRender( oSettings, iRow, iColumn );
+							nCell.innerHTML = sRendered;
+							if ( oCol.bUseRendered )
+							{
+								/* Use the rendered data for filtering / sorting */
+								_fnSetCellData( oSettings, iRow, iColumn, sRendered );
+							}
+						}
+						
+						/* Classes */
+						if ( bClass )
+						{
+							nCell.className += ' '+oCol.sClass;
+						}
+						
+						/* Column visibility */
+						if ( !bVisible )
+						{
+							oData._anHidden[iColumn] = nCell;
+							nCell.parentNode.removeChild( nCell );
+						}
+						else
+						{
+							oData._anHidden[iColumn] = null;
+						}
+		
+						if ( oCol.fnCreatedCell )
+						{
+							oCol.fnCreatedCell.call( oSettings.oInstance,
+								nCell, _fnGetCellData( oSettings, iRow, iColumn, 'display' ), oData._aData, iRow, iColumn
+							);
+						}
+					}
+				}
+			}
+		
+			/* Row created callbacks */
+			if ( oSettings.aoRowCreatedCallback.length !== 0 )
+			{
+				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+				{
+					oData = oSettings.aoData[i];
+					_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, i] );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Take a TR element and convert it to an index in aoData
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n the TR element to find
+		 *  @returns {int} index if the node is found, null if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToDataIndex( oSettings, n )
+		{
+			return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null;
+		}
+		
+		
+		/**
+		 * Take a TD element and convert it into a column data index (not the visible index)
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow The row number the TD/TH can be found in
+		 *  @param {node} n The TD/TH element to find
+		 *  @returns {int} index if the node is found, -1 if not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnNodeToColumnIndex( oSettings, iRow, n )
+		{
+			var anCells = _fnGetTdNodes( oSettings, iRow );
+		
+			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( anCells[i] === n )
+				{
+					return i;
+				}
+			}
+			return -1;
+		}
+		
+		
+		/**
+		 * Get an array of data for a given row from the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {string} sSpecific data get type ('type' 'filter' 'sort')
+		 *  @param {array} aiColumns Array of column indexes to get data from
+		 *  @returns {array} Data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetRowData( oSettings, iRow, sSpecific, aiColumns )
+		{
+			var out = [];
+			for ( var i=0, iLen=aiColumns.length ; i<iLen ; i++ )
+			{
+				out.push( _fnGetCellData( oSettings, iRow, aiColumns[i], sSpecific ) );
+			}
+			return out;
+		}
+		
+		
+		/**
+		 * Get the data for a given cell from the internal cache, taking into account data mapping
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {string} sSpecific data get type ('display', 'type' 'filter' 'sort')
+		 *  @returns {*} Cell data
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetCellData( oSettings, iRow, iCol, sSpecific )
+		{
+			var sData;
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			if ( (sData=oCol.fnGetData( oData, sSpecific )) === undefined )
+			{
+				if ( oSettings.iDrawError != oSettings.iDraw && oCol.sDefaultContent === null )
+				{
+					_fnLog( oSettings, 0, "Requested unknown parameter "+
+						(typeof oCol.mData=='function' ? '{mData function}' : "'"+oCol.mData+"'")+
+						" from the data source for row "+iRow );
+					oSettings.iDrawError = oSettings.iDraw;
+				}
+				return oCol.sDefaultContent;
+			}
+		
+			/* When the data source is null, we can use default column data */
+			if ( sData === null && oCol.sDefaultContent !== null )
+			{
+				sData = oCol.sDefaultContent;
+			}
+			else if ( typeof sData === 'function' )
+			{
+				/* If the data source is a function, then we run it and use the return */
+				return sData();
+			}
+		
+			if ( sSpecific == 'display' && sData === null )
+			{
+				return '';
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * Set the value for a specific cell, into the internal data cache
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData row id
+		 *  @param {int} iCol Column index
+		 *  @param {*} val Value to set
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetCellData( oSettings, iRow, iCol, val )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+			var oData = oSettings.aoData[iRow]._aData;
+		
+			oCol.fnSetData( oData, val );
+		}
+		
+		
+		// Private variable that is used to match array syntax in the data property object
+		var __reArray = /\[.*?\]$/;
+		
+		/**
+		 * Return a function that can be used to get data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data get function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Give an empty string for rendering / sorting etc */
+				return function (data, type) {
+					return null;
+				};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, type, extra) {
+					return mSource( data, type, extra );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* If there is a . in the source string then the data source is in a 
+				 * nested object so we loop over the data for each level to get the next
+				 * level down. On each loop we test for undefined, and if found immediately
+				 * return. This allows entire objects to be missing and sDefaultContent to
+				 * be used if defined, rather than throwing an error
+				 */
+				var fetchData = function (data, type, src) {
+					var a = src.split('.');
+					var arrayNotation, out, innerSrc;
+		
+					if ( src !== "" )
+					{
+						for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+						{
+							// Check if we are dealing with an array notation request
+							arrayNotation = a[i].match(__reArray);
+		
+							if ( arrayNotation ) {
+								a[i] = a[i].replace(__reArray, '');
+		
+								// Condition allows simply [] to be passed in
+								if ( a[i] !== "" ) {
+									data = data[ a[i] ];
+								}
+								out = [];
+								
+								// Get the remainder of the nested object to get
+								a.splice( 0, i+1 );
+								innerSrc = a.join('.');
+		
+								// Traverse each entry in the array getting the properties requested
+								for ( var j=0, jLen=data.length ; j<jLen ; j++ ) {
+									out.push( fetchData( data[j], type, innerSrc ) );
+								}
+		
+								// If a string is given in between the array notation indicators, that
+								// is used to join the strings together, otherwise an array is returned
+								var join = arrayNotation[0].substring(1, arrayNotation[0].length-1);
+								data = (join==="") ? out : out.join(join);
+		
+								// The inner call to fetchData has already traversed through the remainder
+								// of the source requested, so we exit from the loop
+								break;
+							}
+		
+							if ( data === null || data[ a[i] ] === undefined )
+							{
+								return undefined;
+							}
+							data = data[ a[i] ];
+						}
+					}
+		
+					return data;
+				};
+		
+				return function (data, type) {
+					return fetchData( data, type, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, type) {
+					return data[mSource];	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return a function that can be used to set data from a source object, taking
+		 * into account the ability to use nested objects as a source
+		 *  @param {string|int|function} mSource The data source for the object
+		 *  @returns {function} Data set function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSetObjectDataFn( mSource )
+		{
+			if ( mSource === null )
+			{
+				/* Nothing to do when the data source is null */
+				return function (data, val) {};
+			}
+			else if ( typeof mSource === 'function' )
+			{
+				return function (data, val) {
+					mSource( data, 'set', val );
+				};
+			}
+			else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || mSource.indexOf('[') !== -1) )
+			{
+				/* Like the get, we need to get data from a nested object */
+				var setData = function (data, val, src) {
+					var a = src.split('.'), b;
+					var arrayNotation, o, innerSrc;
+		
+					for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ )
+					{
+						// Check if we are dealing with an array notation request
+						arrayNotation = a[i].match(__reArray);
+		
+						if ( arrayNotation )
+						{
+							a[i] = a[i].replace(__reArray, '');
+							data[ a[i] ] = [];
+							
+							// Get the remainder of the nested object to set so we can recurse
+							b = a.slice();
+							b.splice( 0, i+1 );
+							innerSrc = b.join('.');
+		
+							// Traverse each entry in the array setting the properties requested
+							for ( var j=0, jLen=val.length ; j<jLen ; j++ )
+							{
+								o = {};
+								setData( o, val[j], innerSrc );
+								data[ a[i] ].push( o );
+							}
+		
+							// The inner call to setData has already traversed through the remainder
+							// of the source and has set the data, thus we can exit here
+							return;
+						}
+		
+						// If the nested object doesn't currently exist - since we are
+						// trying to set the value - create it
+						if ( data[ a[i] ] === null || data[ a[i] ] === undefined )
+						{
+							data[ a[i] ] = {};
+						}
+						data = data[ a[i] ];
+					}
+		
+					// If array notation is used, we just want to strip it and use the property name
+					// and assign the value. If it isn't used, then we get the result we want anyway
+					data[ a[a.length-1].replace(__reArray, '') ] = val;
+				};
+		
+				return function (data, val) {
+					return setData( data, val, mSource );
+				};
+			}
+			else
+			{
+				/* Array or flat object mapping */
+				return function (data, val) {
+					data[mSource] = val;	
+				};
+			}
+		}
+		
+		
+		/**
+		 * Return an array with the full table data
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns array {array} aData Master data array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetDataMaster ( oSettings )
+		{
+			var aData = [];
+			var iLen = oSettings.aoData.length;
+			for ( var i=0 ; i<iLen; i++ )
+			{
+				aData.push( oSettings.aoData[i]._aData );
+			}
+			return aData;
+		}
+		
+		
+		/**
+		 * Nuke the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnClearTable( oSettings )
+		{
+			oSettings.aoData.splice( 0, oSettings.aoData.length );
+			oSettings.aiDisplayMaster.splice( 0, oSettings.aiDisplayMaster.length );
+			oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length );
+			_fnCalculateEnd( oSettings );
+		}
+		
+		
+		 /**
+		 * Take an array of integers (index array) and remove a target integer (value - not 
+		 * the key!)
+		 *  @param {array} a Index array to target
+		 *  @param {int} iTarget value to find
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDeleteIndex( a, iTarget )
+		{
+			var iTargetIndex = -1;
+			
+			for ( var i=0, iLen=a.length ; i<iLen ; i++ )
+			{
+				if ( a[i] == iTarget )
+				{
+					iTargetIndex = i;
+				}
+				else if ( a[i] > iTarget )
+				{
+					a[i]--;
+				}
+			}
+			
+			if ( iTargetIndex != -1 )
+			{
+				a.splice( iTargetIndex, 1 );
+			}
+		}
+		
+		
+		 /**
+		 * Call the developer defined fnRender function for a given cell (row/column) with
+		 * the required parameters and return the result.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow aoData index for the row
+		 *  @param {int} iCol aoColumns index for the column
+		 *  @returns {*} Return of the developer's fnRender function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnRender( oSettings, iRow, iCol )
+		{
+			var oCol = oSettings.aoColumns[iCol];
+		
+			return oCol.fnRender( {
+				"iDataRow":    iRow,
+				"iDataColumn": iCol,
+				"oSettings":   oSettings,
+				"aData":       oSettings.aoData[iRow]._aData,
+				"mDataProp":   oCol.mData
+			}, _fnGetCellData(oSettings, iRow, iCol, 'display') );
+		}
+		/**
+		 * Create a new TR element (and it's TD children) for a row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iRow Row to consider
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateTr ( oSettings, iRow )
+		{
+			var oData = oSettings.aoData[iRow];
+			var nTd;
+		
+			if ( oData.nTr === null )
+			{
+				oData.nTr = document.createElement('tr');
+		
+				/* Use a private property on the node to allow reserve mapping from the node
+				 * to the aoData array for fast look up
+				 */
+				oData.nTr._DT_RowIndex = iRow;
+		
+				/* Special parameters can be given by the data source to be used on the row */
+				if ( oData._aData.DT_RowId )
+				{
+					oData.nTr.id = oData._aData.DT_RowId;
+				}
+		
+				if ( oData._aData.DT_RowClass )
+				{
+					oData.nTr.className = oData._aData.DT_RowClass;
+				}
+		
+				/* Process each column */
+				for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					var oCol = oSettings.aoColumns[i];
+					nTd = document.createElement( oCol.sCellType );
+		
+					/* Render if needed - if bUseRendered is true then we already have the rendered
+					 * value in the data source - so can just use that
+					 */
+					nTd.innerHTML = (typeof oCol.fnRender === 'function' && (!oCol.bUseRendered || oCol.mData === null)) ?
+						_fnRender( oSettings, iRow, i ) :
+						_fnGetCellData( oSettings, iRow, i, 'display' );
+				
+					/* Add user defined class */
+					if ( oCol.sClass !== null )
+					{
+						nTd.className = oCol.sClass;
+					}
+					
+					if ( oCol.bVisible )
+					{
+						oData.nTr.appendChild( nTd );
+						oData._anHidden[i] = null;
+					}
+					else
+					{
+						oData._anHidden[i] = nTd;
+					}
+		
+					if ( oCol.fnCreatedCell )
+					{
+						oCol.fnCreatedCell.call( oSettings.oInstance,
+							nTd, _fnGetCellData( oSettings, iRow, i, 'display' ), oData._aData, iRow, i
+						);
+					}
+				}
+		
+				_fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [oData.nTr, oData._aData, iRow] );
+			}
+		}
+		
+		
+		/**
+		 * Create the HTML header for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildHead( oSettings )
+		{
+			var i, nTh, iLen, j, jLen;
+			var iThs = $('th, td', oSettings.nTHead).length;
+			var iCorrector = 0;
+			var jqChildren;
+			
+			/* If there is a header in place - then use it - otherwise it's going to get nuked... */
+			if ( iThs !== 0 )
+			{
+				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.setAttribute('role', 'columnheader');
+					if ( oSettings.aoColumns[i].bSortable )
+					{
+						nTh.setAttribute('tabindex', oSettings.iTabIndex);
+						nTh.setAttribute('aria-controls', oSettings.sTableId);
+					}
+		
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					/* Set the title of the column if it is user defined (not what was auto detected) */
+					if ( oSettings.aoColumns[i].sTitle != nTh.innerHTML )
+					{
+						nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					}
+				}
+			}
+			else
+			{
+				/* We don't have a header in the DOM - so we are going to have to create one */
+				var nTr = document.createElement( "tr" );
+				
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					nTh.innerHTML = oSettings.aoColumns[i].sTitle;
+					nTh.setAttribute('tabindex', '0');
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					nTr.appendChild( nTh );
+				}
+				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );
+				_fnDetectHeader( oSettings.aoHeader, oSettings.nTHead );
+			}
+			
+			/* ARIA role for the rows */	
+			$(oSettings.nTHead).children('tr').attr('role', 'row');
+			
+			/* Add the extra markup needed by jQuery UI's themes */
+			if ( oSettings.bJUI )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					nTh = oSettings.aoColumns[i].nTh;
+					
+					var nDiv = document.createElement('div');
+					nDiv.className = oSettings.oClasses.sSortJUIWrapper;
+					$(nTh).contents().appendTo(nDiv);
+					
+					var nSpan = document.createElement('span');
+					nSpan.className = oSettings.oClasses.sSortIcon;
+					nDiv.appendChild( nSpan );
+					nTh.appendChild( nDiv );
+				}
+			}
+			
+			if ( oSettings.oFeatures.bSort )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bSortable !== false )
+					{
+						_fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );
+					}
+					else
+					{
+						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );
+					}
+				}
+			}
+			
+			/* Deal with the footer - add classes if required */
+			if ( oSettings.oClasses.sFooterTH !== "" )
+			{
+				$(oSettings.nTFoot).children('tr').children('th').addClass( oSettings.oClasses.sFooterTH );
+			}
+			
+			/* Cache the footer elements */
+			if ( oSettings.nTFoot !== null )
+			{
+				var anCells = _fnGetUniqueThs( oSettings, null, oSettings.aoFooter );
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( anCells[i] )
+					{
+						oSettings.aoColumns[i].nTf = anCells[i];
+						if ( oSettings.aoColumns[i].sClass )
+						{
+							$(anCells[i]).addClass( oSettings.aoColumns[i].sClass );
+						}
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Draw the header (or footer) element based on the column visibility states. The
+		 * methodology here is to use the layout array from _fnDetectHeader, modified for
+		 * the instantaneous column visibility, to construct the new layout. The grid is
+		 * traversed over cell at a time in a rows x columns grid fashion, although each 
+		 * cell insert can cover multiple elements in the grid - which is tracks using the
+		 * aApplied array. Cell inserts in the grid will only occur where there isn't
+		 * already a cell in that position.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoSource Layout array from _fnDetectHeader
+		 *  @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, 
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDrawHead( oSettings, aoSource, bIncludeHidden )
+		{
+			var i, iLen, j, jLen, k, kLen, n, nLocalTr;
+			var aoLocal = [];
+			var aApplied = [];
+			var iColumns = oSettings.aoColumns.length;
+			var iRowspan, iColspan;
+		
+			if (  bIncludeHidden === undefined )
+			{
+				bIncludeHidden = false;
+			}
+		
+			/* Make a copy of the master layout array, but without the visible columns in it */
+			for ( i=0, iLen=aoSource.length ; i<iLen ; i++ )
+			{
+				aoLocal[i] = aoSource[i].slice();
+				aoLocal[i].nTr = aoSource[i].nTr;
+		
+				/* Remove any columns which are currently hidden */
+				for ( j=iColumns-1 ; j>=0 ; j-- )
+				{
+					if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
+					{
+						aoLocal[i].splice( j, 1 );
+					}
+				}
+		
+				/* Prep the applied array - it needs an element for each row */
+				aApplied.push( [] );
+			}
+		
+			for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ )
+			{
+				nLocalTr = aoLocal[i].nTr;
+				
+				/* All cells are going to be replaced, so empty out the row */
+				if ( nLocalTr )
+				{
+					while( (n = nLocalTr.firstChild) )
+					{
+						nLocalTr.removeChild( n );
+					}
+				}
+		
+				for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ )
+				{
+					iRowspan = 1;
+					iColspan = 1;
+		
+					/* Check to see if there is already a cell (row/colspan) covering our target
+					 * insert point. If there is, then there is nothing to do.
+					 */
+					if ( aApplied[i][j] === undefined )
+					{
+						nLocalTr.appendChild( aoLocal[i][j].cell );
+						aApplied[i][j] = 1;
+		
+						/* Expand the cell to cover as many rows as needed */
+						while ( aoLocal[i+iRowspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell )
+						{
+							aApplied[i+iRowspan][j] = 1;
+							iRowspan++;
+						}
+		
+						/* Expand the cell to cover as many columns as needed */
+						while ( aoLocal[i][j+iColspan] !== undefined &&
+						        aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell )
+						{
+							/* Must update the applied array over the rows for the columns */
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aApplied[i+k][j+iColspan] = 1;
+							}
+							iColspan++;
+						}
+		
+						/* Do the actual expansion in the DOM */
+						aoLocal[i][j].cell.rowSpan = iRowspan;
+						aoLocal[i][j].cell.colSpan = iColspan;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Insert the required TR nodes into the table for display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDraw( oSettings )
+		{
+			/* Provide a pre-callback function which can be used to cancel the draw is false is returned */
+			var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] );
+			if ( $.inArray( false, aPreDraw ) !== -1 )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				return;
+			}
+			
+			var i, iLen, n;
+			var anRows = [];
+			var iRowCount = 0;
+			var iStripes = oSettings.asStripeClasses.length;
+			var iOpenRows = oSettings.aoOpenRows.length;
+			
+			oSettings.bDrawing = true;
+			
+			/* Check and see if we have an initial draw position from state saving */
+			if ( oSettings.iInitDisplayStart !== undefined && oSettings.iInitDisplayStart != -1 )
+			{
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					oSettings._iDisplayStart = oSettings.iInitDisplayStart;
+				}
+				else
+				{
+					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?
+						0 : oSettings.iInitDisplayStart;
+				}
+				oSettings.iInitDisplayStart = -1;
+				_fnCalculateEnd( oSettings );
+			}
+			
+			/* Server-side processing draw intercept */
+			if ( oSettings.bDeferLoading )
+			{
+				oSettings.bDeferLoading = false;
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.oFeatures.bServerSide )
+			{
+				oSettings.iDraw++;
+			}
+			else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
+			{
+				return;
+			}
+			
+			if ( oSettings.aiDisplay.length !== 0 )
+			{
+				var iStart = oSettings._iDisplayStart;
+				var iEnd = oSettings._iDisplayEnd;
+				
+				if ( oSettings.oFeatures.bServerSide )
+				{
+					iStart = 0;
+					iEnd = oSettings.aoData.length;
+				}
+				
+				for ( var j=iStart ; j<iEnd ; j++ )
+				{
+					var aoData = oSettings.aoData[ oSettings.aiDisplay[j] ];
+					if ( aoData.nTr === null )
+					{
+						_fnCreateTr( oSettings, oSettings.aiDisplay[j] );
+					}
+		
+					var nRow = aoData.nTr;
+					
+					/* Remove the old striping classes and then add the new one */
+					if ( iStripes !== 0 )
+					{
+						var sStripe = oSettings.asStripeClasses[ iRowCount % iStripes ];
+						if ( aoData._sRowStripe != sStripe )
+						{
+							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe );
+							aoData._sRowStripe = sStripe;
+						}
+					}
+					
+					/* Row callback functions - might want to manipulate the row */
+					_fnCallbackFire( oSettings, 'aoRowCallback', null, 
+						[nRow, oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j] );
+					
+					anRows.push( nRow );
+					iRowCount++;
+					
+					/* If there is an open row - and it is attached to this parent - attach it on redraw */
+					if ( iOpenRows !== 0 )
+					{
+						for ( var k=0 ; k<iOpenRows ; k++ )
+						{
+							if ( nRow == oSettings.aoOpenRows[k].nParent )
+							{
+								anRows.push( oSettings.aoOpenRows[k].nTr );
+								break;
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Table is empty - create a row with an empty message in it */
+				anRows[ 0 ] = document.createElement( 'tr' );
+				
+				if ( oSettings.asStripeClasses[0] )
+				{
+					anRows[ 0 ].className = oSettings.asStripeClasses[0];
+				}
+		
+				var oLang = oSettings.oLanguage;
+				var sZero = oLang.sZeroRecords;
+				if ( oSettings.iDraw == 1 && oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+				{
+					sZero = oLang.sLoadingRecords;
+				}
+				else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 )
+				{
+					sZero = oLang.sEmptyTable;
+				}
+		
+				var nTd = document.createElement( 'td' );
+				nTd.setAttribute( 'valign', "top" );
+				nTd.colSpan = _fnVisbleColumns( oSettings );
+				nTd.className = oSettings.oClasses.sRowEmpty;
+				nTd.innerHTML = _fnInfoMacros( oSettings, sZero );
+				
+				anRows[ iRowCount ].appendChild( nTd );
+			}
+			
+			/* Header and footer callbacks */
+			_fnCallbackFire( oSettings, 'aoHeaderCallback', 'header', [ $(oSettings.nTHead).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			_fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], 
+				_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(), oSettings.aiDisplay ] );
+			
+			/* 
+			 * Need to remove any old row from the display - note we can't just empty the tbody using
+			 * $().html('') since this will unbind the jQuery event handlers (even although the node 
+			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.
+			 */
+			var
+				nAddFrag = document.createDocumentFragment(),
+				nRemoveFrag = document.createDocumentFragment(),
+				nBodyPar, nTrs;
+			
+			if ( oSettings.nTBody )
+			{
+				nBodyPar = oSettings.nTBody.parentNode;
+				nRemoveFrag.appendChild( oSettings.nTBody );
+				
+				/* When doing infinite scrolling, only remove child rows when sorting, filtering or start
+				 * up. When not infinite scroll, always do it.
+				 */
+				if ( !oSettings.oScroll.bInfinite || !oSettings._bInitComplete ||
+				 	oSettings.bSorted || oSettings.bFiltered )
+				{
+					while( (n = oSettings.nTBody.firstChild) )
+					{
+						oSettings.nTBody.removeChild( n );
+					}
+				}
+				
+				/* Put the draw table into the dom */
+				for ( i=0, iLen=anRows.length ; i<iLen ; i++ )
+				{
+					nAddFrag.appendChild( anRows[i] );
+				}
+				
+				oSettings.nTBody.appendChild( nAddFrag );
+				if ( nBodyPar !== null )
+				{
+					nBodyPar.appendChild( oSettings.nTBody );
+				}
+			}
+			
+			/* Call all required callback functions for the end of a draw */
+			_fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] );
+			
+			/* Draw is complete, sorting and filtering must be as well */
+			oSettings.bSorted = false;
+			oSettings.bFiltered = false;
+			oSettings.bDrawing = false;
+			
+			if ( oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				if ( !oSettings._bInitComplete )
+				{
+					_fnInitComplete( oSettings );
+				}
+			}
+		}
+		
+		
+		/**
+		 * Redraw the table - taking account of the various features which are enabled
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReDraw( oSettings )
+		{
+			if ( oSettings.oFeatures.bSort )
+			{
+				/* Sorting will refilter and draw for us */
+				_fnSort( oSettings, oSettings.oPreviousSearch );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				/* Filtering will redraw for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Add the options to the page HTML for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAddOptionsHtml ( oSettings )
+		{
+			/*
+			 * Create a temporary, empty, div which we can later on replace with what we have generated
+			 * we do it this way to rendering the 'options' html offline - speed :-)
+			 */
+			var nHolding = $('<div></div>')[0];
+			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
+			
+			/* 
+			 * All DataTables are wrapped in a div
+			 */
+			oSettings.nTableWrapper = $('<div id="'+oSettings.sTableId+'_wrapper" class="'+oSettings.oClasses.sWrapper+'" role="grid"></div>')[0];
+			oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
+		
+			/* Track where we want to insert the option */
+			var nInsertNode = oSettings.nTableWrapper;
+			
+			/* Loop over the user set positioning and place the elements as needed */
+			var aDom = oSettings.sDom.split('');
+			var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;
+			for ( var i=0 ; i<aDom.length ; i++ )
+			{
+				iPushFeature = 0;
+				cOption = aDom[i];
+				
+				if ( cOption == '<' )
+				{
+					/* New container div */
+					nNewNode = $('<div></div>')[0];
+					
+					/* Check to see if we should append an id and/or a class name to the container */
+					cNext = aDom[i+1];
+					if ( cNext == "'" || cNext == '"' )
+					{
+						sAttr = "";
+						j = 2;
+						while ( aDom[i+j] != cNext )
+						{
+							sAttr += aDom[i+j];
+							j++;
+						}
+						
+						/* Replace jQuery UI constants */
+						if ( sAttr == "H" )
+						{
+							sAttr = oSettings.oClasses.sJUIHeader;
+						}
+						else if ( sAttr == "F" )
+						{
+							sAttr = oSettings.oClasses.sJUIFooter;
+						}
+						
+						/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
+						 * breaks the string into parts and applies them as needed
+						 */
+						if ( sAttr.indexOf('.') != -1 )
+						{
+							var aSplit = sAttr.split('.');
+							nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
+							nNewNode.className = aSplit[1];
+						}
+						else if ( sAttr.charAt(0) == "#" )
+						{
+							nNewNode.id = sAttr.substr(1, sAttr.length-1);
+						}
+						else
+						{
+							nNewNode.className = sAttr;
+						}
+						
+						i += j; /* Move along the position array */
+					}
+					
+					nInsertNode.appendChild( nNewNode );
+					nInsertNode = nNewNode;
+				}
+				else if ( cOption == '>' )
+				{
+					/* End container div */
+					nInsertNode = nInsertNode.parentNode;
+				}
+				else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )
+				{
+					/* Length */
+					nTmp = _fnFeatureHtmlLength( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'f' && oSettings.oFeatures.bFilter )
+				{
+					/* Filter */
+					nTmp = _fnFeatureHtmlFilter( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )
+				{
+					/* pRocessing */
+					nTmp = _fnFeatureHtmlProcessing( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 't' )
+				{
+					/* Table */
+					nTmp = _fnFeatureHtmlTable( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption ==  'i' && oSettings.oFeatures.bInfo )
+				{
+					/* Info */
+					nTmp = _fnFeatureHtmlInfo( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )
+				{
+					/* Pagination */
+					nTmp = _fnFeatureHtmlPaginate( oSettings );
+					iPushFeature = 1;
+				}
+				else if ( DataTable.ext.aoFeatures.length !== 0 )
+				{
+					/* Plug-in features */
+					var aoFeatures = DataTable.ext.aoFeatures;
+					for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
+					{
+						if ( cOption == aoFeatures[k].cFeature )
+						{
+							nTmp = aoFeatures[k].fnInit( oSettings );
+							if ( nTmp )
+							{
+								iPushFeature = 1;
+							}
+							break;
+						}
+					}
+				}
+				
+				/* Add to the 2D features array */
+				if ( iPushFeature == 1 && nTmp !== null )
+				{
+					if ( typeof oSettings.aanFeatures[cOption] !== 'object' )
+					{
+						oSettings.aanFeatures[cOption] = [];
+					}
+					oSettings.aanFeatures[cOption].push( nTmp );
+					nInsertNode.appendChild( nTmp );
+				}
+			}
+			
+			/* Built our DOM structure - replace the holding div with what we want */
+			nHolding.parentNode.replaceChild( oSettings.nTableWrapper, nHolding );
+		}
+		
+		
+		/**
+		 * Use the DOM source to create up an array of header cells. The idea here is to
+		 * create a layout grid (array) of rows x columns, which contains a reference
+		 * to the cell that that point in the grid (regardless of col/rowspan), such that
+		 * any column / row could be removed and the new grid constructed
+		 *  @param array {object} aLayout Array to store the calculated layout in
+		 *  @param {node} nThead The header/footer element for the table
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDetectHeader ( aLayout, nThead )
+		{
+			var nTrs = $(nThead).children('tr');
+			var nTr, nCell;
+			var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan;
+			var bUnique;
+			var fnShiftCol = function ( a, i, j ) {
+				var k = a[i];
+		                while ( k[j] ) {
+					j++;
+				}
+				return j;
+			};
+		
+			aLayout.splice( 0, aLayout.length );
+			
+			/* We know how many rows there are in the layout - so prep it */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				aLayout.push( [] );
+			}
+			
+			/* Calculate a layout array */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				nTr = nTrs[i];
+				iColumn = 0;
+				
+				/* For every cell in the row... */
+				nCell = nTr.firstChild;
+				while ( nCell ) {
+					if ( nCell.nodeName.toUpperCase() == "TD" ||
+					     nCell.nodeName.toUpperCase() == "TH" )
+					{
+						/* Get the col and rowspan attributes from the DOM and sanitise them */
+						iColspan = nCell.getAttribute('colspan') * 1;
+						iRowspan = nCell.getAttribute('rowspan') * 1;
+						iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan;
+						iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan;
+		
+						/* There might be colspan cells already in this row, so shift our target 
+						 * accordingly
+						 */
+						iColShifted = fnShiftCol( aLayout, i, iColumn );
+						
+						/* Cache calculation for unique columns */
+						bUnique = iColspan === 1 ? true : false;
+						
+						/* If there is col / rowspan, copy the information into the layout grid */
+						for ( l=0 ; l<iColspan ; l++ )
+						{
+							for ( k=0 ; k<iRowspan ; k++ )
+							{
+								aLayout[i+k][iColShifted+l] = {
+									"cell": nCell,
+									"unique": bUnique
+								};
+								aLayout[i+k].nTr = nTr;
+							}
+						}
+					}
+					nCell = nCell.nextSibling;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Get an array of unique th elements, one for each column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nHeader automatically detect the layout from this node - optional
+		 *  @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional
+		 *  @returns array {node} aReturn list of unique th's
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetUniqueThs ( oSettings, nHeader, aLayout )
+		{
+			var aReturn = [];
+			if ( !aLayout )
+			{
+				aLayout = oSettings.aoHeader;
+				if ( nHeader )
+				{
+					aLayout = [];
+					_fnDetectHeader( aLayout, nHeader );
+				}
+			}
+		
+			for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ )
+			{
+				for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+				{
+					if ( aLayout[i][j].unique && 
+						 (!aReturn[j] || !oSettings.bSortCellsTop) )
+					{
+						aReturn[j] = aLayout[i][j].cell;
+					}
+				}
+			}
+			
+			return aReturn;
+		}
+		
+		
+		
+		/**
+		 * Update the table using an Ajax call
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {boolean} Block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdate( oSettings )
+		{
+			if ( oSettings.bAjaxDataGet )
+			{
+				oSettings.iDraw++;
+				_fnProcessingDisplay( oSettings, true );
+				var iColumns = oSettings.aoColumns.length;
+				var aoData = _fnAjaxParameters( oSettings );
+				_fnServerParams( oSettings, aoData );
+				
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,
+					function(json) {
+						_fnAjaxUpdateDraw( oSettings, json );
+					}, oSettings );
+				return false;
+			}
+			else
+			{
+				return true;
+			}
+		}
+		
+		
+		/**
+		 * Build up the parameters in an object needed for a server-side processing request
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {bool} block the table drawing or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxParameters( oSettings )
+		{
+			var iColumns = oSettings.aoColumns.length;
+			var aoData = [], mDataProp, aaSort, aDataSort;
+			var i, j;
+			
+			aoData.push( { "name": "sEcho",          "value": oSettings.iDraw } );
+			aoData.push( { "name": "iColumns",       "value": iColumns } );
+			aoData.push( { "name": "sColumns",       "value": _fnColumnOrdering(oSettings) } );
+			aoData.push( { "name": "iDisplayStart",  "value": oSettings._iDisplayStart } );
+			aoData.push( { "name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?
+				oSettings._iDisplayLength : -1 } );
+				
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+			  mDataProp = oSettings.aoColumns[i].mData;
+				aoData.push( { "name": "mDataProp_"+i, "value": typeof(mDataProp)==="function" ? 'function' : mDataProp } );
+			}
+			
+			/* Filtering */
+			if ( oSettings.oFeatures.bFilter !== false )
+			{
+				aoData.push( { "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch } );
+				aoData.push( { "name": "bRegex",  "value": oSettings.oPreviousSearch.bRegex } );
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "sSearch_"+i,     "value": oSettings.aoPreSearchCols[i].sSearch } );
+					aoData.push( { "name": "bRegex_"+i,      "value": oSettings.aoPreSearchCols[i].bRegex } );
+					aoData.push( { "name": "bSearchable_"+i, "value": oSettings.aoColumns[i].bSearchable } );
+				}
+			}
+			
+			/* Sorting */
+			if ( oSettings.oFeatures.bSort !== false )
+			{
+				var iCounter = 0;
+		
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					aDataSort = oSettings.aoColumns[ aaSort[i][0] ].aDataSort;
+					
+					for ( j=0 ; j<aDataSort.length ; j++ )
+					{
+						aoData.push( { "name": "iSortCol_"+iCounter,  "value": aDataSort[j] } );
+						aoData.push( { "name": "sSortDir_"+iCounter,  "value": aaSort[i][1] } );
+						iCounter++;
+					}
+				}
+				aoData.push( { "name": "iSortingCols",   "value": iCounter } );
+				
+				for ( i=0 ; i<iColumns ; i++ )
+				{
+					aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );
+				}
+			}
+			
+			return aoData;
+		}
+		
+		
+		/**
+		 * Add Ajax parameters from plug-ins
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param array {objects} aoData name/value pairs to send to the server
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnServerParams( oSettings, aoData )
+		{
+			_fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [aoData] );
+		}
+		
+		
+		/**
+		 * Data the data from the server (nuking the old) and redraw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} json json data return from the server.
+		 *  @param {string} json.sEcho Tracking flag for DataTables to match requests
+		 *  @param {int} json.iTotalRecords Number of records in the data set, not accounting for filtering
+		 *  @param {int} json.iTotalDisplayRecords Number of records in the data set, accounting for filtering
+		 *  @param {array} json.aaData The data to display on this page
+		 *  @param {string} [json.sColumns] Column ordering (sName, comma separated)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnAjaxUpdateDraw ( oSettings, json )
+		{
+			if ( json.sEcho !== undefined )
+			{
+				/* Protect against old returns over-writing a new one. Possible when you get
+				 * very fast interaction, and later queries are completed much faster
+				 */
+				if ( json.sEcho*1 < oSettings.iDraw )
+				{
+					return;
+				}
+				else
+				{
+					oSettings.iDraw = json.sEcho * 1;
+				}
+			}
+			
+			if ( !oSettings.oScroll.bInfinite ||
+				   (oSettings.oScroll.bInfinite && (oSettings.bSorted || oSettings.bFiltered)) )
+			{
+				_fnClearTable( oSettings );
+			}
+			oSettings._iRecordsTotal = parseInt(json.iTotalRecords, 10);
+			oSettings._iRecordsDisplay = parseInt(json.iTotalDisplayRecords, 10);
+			
+			/* Determine if reordering is required */
+			var sOrdering = _fnColumnOrdering(oSettings);
+			var bReOrder = (json.sColumns !== undefined && sOrdering !== "" && json.sColumns != sOrdering );
+			var aiIndex;
+			if ( bReOrder )
+			{
+				aiIndex = _fnReOrderIndex( oSettings, json.sColumns );
+			}
+			
+			var aData = _fnGetObjectDataFn( oSettings.sAjaxDataProp )( json );
+			for ( var i=0, iLen=aData.length ; i<iLen ; i++ )
+			{
+				if ( bReOrder )
+				{
+					/* If we need to re-order, then create a new array with the correct order and add it */
+					var aDataSorted = [];
+					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+					{
+						aDataSorted.push( aData[i][ aiIndex[j] ] );
+					}
+					_fnAddData( oSettings, aDataSorted );
+				}
+				else
+				{
+					/* No re-order required, sever got it "right" - just straight add */
+					_fnAddData( oSettings, aData[i] );
+				}
+			}
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			oSettings.bAjaxDataGet = false;
+			_fnDraw( oSettings );
+			oSettings.bAjaxDataGet = true;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for filtering text
+		 *  @returns {node} Filter control element
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlFilter ( oSettings )
+		{
+			var oPreviousSearch = oSettings.oPreviousSearch;
+			
+			var sSearchStr = oSettings.oLanguage.sSearch;
+			sSearchStr = (sSearchStr.indexOf('_INPUT_') !== -1) ?
+			  sSearchStr.replace('_INPUT_', '<input type="text" />') :
+			  sSearchStr==="" ? '<input type="text" />' : sSearchStr+' <input type="text" />';
+			
+			var nFilter = document.createElement( 'div' );
+			nFilter.className = oSettings.oClasses.sFilter;
+			nFilter.innerHTML = '<label>'+sSearchStr+'</label>';
+			if ( !oSettings.aanFeatures.f )
+			{
+				nFilter.id = oSettings.sTableId+'_filter';
+			}
+			
+			var jqFilter = $('input[type="text"]', nFilter);
+		
+			// Store a reference to the input element, so other input elements could be
+			// added to the filter wrapper if needed (submit button for example)
+			nFilter._DT_Input = jqFilter[0];
+		
+			jqFilter.val( oPreviousSearch.sSearch.replace('"','"') );
+			jqFilter.bind( 'keyup.DT', function(e) {
+				/* Update all other filter input elements for the new display */
+				var n = oSettings.aanFeatures.f;
+				var val = this.value==="" ? "" : this.value; // mental IE8 fix :-(
+		
+				for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != $(this).parents('div.dataTables_filter')[0] )
+					{
+						$(n[i]._DT_Input).val( val );
+					}
+				}
+				
+				/* Now do the filter */
+				if ( val != oPreviousSearch.sSearch )
+				{
+					_fnFilterComplete( oSettings, { 
+						"sSearch": val, 
+						"bRegex": oPreviousSearch.bRegex,
+						"bSmart": oPreviousSearch.bSmart ,
+						"bCaseInsensitive": oPreviousSearch.bCaseInsensitive 
+					} );
+				}
+			} );
+		
+			jqFilter
+				.attr('aria-controls', oSettings.sTableId)
+				.bind( 'keypress.DT', function(e) {
+					/* Prevent form submission */
+					if ( e.keyCode == 13 )
+					{
+						return false;
+					}
+				}
+			);
+			
+			return nFilter;
+		}
+		
+		
+		/**
+		 * Filter the table using both the global filter and column based filtering
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oSearch search information
+		 *  @param {int} [iForce] force a research of the master array (1) or not (undefined or 0)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterComplete ( oSettings, oInput, iForce )
+		{
+			var oPrevSearch = oSettings.oPreviousSearch;
+			var aoPrevSearch = oSettings.aoPreSearchCols;
+			var fnSaveFilter = function ( oFilter ) {
+				/* Save the filtering values */
+				oPrevSearch.sSearch = oFilter.sSearch;
+				oPrevSearch.bRegex = oFilter.bRegex;
+				oPrevSearch.bSmart = oFilter.bSmart;
+				oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive;
+			};
+		
+			/* In server-side processing all filtering is done by the server, so no point hanging around here */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Global filter */
+				_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart, oInput.bCaseInsensitive );
+				fnSaveFilter( oInput );
+		
+				/* Now do the individual column filter */
+				for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+				{
+					_fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, aoPrevSearch[i].bRegex, 
+						aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive );
+				}
+				
+				/* Custom filtering */
+				_fnFilterCustom( oSettings );
+			}
+			else
+			{
+				fnSaveFilter( oInput );
+			}
+			
+			/* Tell the draw function we have been filtering */
+			oSettings.bFiltered = true;
+			$(oSettings.oInstance).trigger('filter', oSettings);
+			
+			/* Redraw the table */
+			oSettings._iDisplayStart = 0;
+			_fnCalculateEnd( oSettings );
+			_fnDraw( oSettings );
+			
+			/* Rebuild search array 'offline' */
+			_fnBuildSearchArray( oSettings, 0 );
+		}
+		
+		
+		/**
+		 * Apply custom filtering functions
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCustom( oSettings )
+		{
+			var afnFilters = DataTable.ext.afnFiltering;
+			var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+		
+			for ( var i=0, iLen=afnFilters.length ; i<iLen ; i++ )
+			{
+				var iCorrector = 0;
+				for ( var j=0, jLen=oSettings.aiDisplay.length ; j<jLen ; j++ )
+				{
+					var iDisIndex = oSettings.aiDisplay[j-iCorrector];
+					var bTest = afnFilters[i](
+						oSettings,
+						_fnGetRowData( oSettings, iDisIndex, 'filter', aiFilterColumns ),
+						iDisIndex
+					);
+					
+					/* Check if we should use this row based on the filtering function */
+					if ( !bTest )
+					{
+						oSettings.aiDisplay.splice( j-iCorrector, 1 );
+						iCorrector++;
+					}
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the table on a per-column basis
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iColumn column to filter
+		 *  @param {bool} bRegex treat search string as a regular expression or not
+		 *  @param {bool} bSmart use smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart, bCaseInsensitive )
+		{
+			if ( sInput === "" )
+			{
+				return;
+			}
+			
+			var iIndexCorrector = 0;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			
+			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )
+			{
+				var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ),
+					oSettings.aoColumns[iColumn].sType );
+				if ( ! rpSearch.test( sData ) )
+				{
+					oSettings.aiDisplay.splice( i, 1 );
+					iIndexCorrector++;
+				}
+			}
+		}
+		
+		
+		/**
+		 * Filter the data table based on user input and draw the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sInput string to filter on
+		 *  @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0)
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insenstive matching or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive )
+		{
+			var i;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
+			var oPrevSearch = oSettings.oPreviousSearch;
+			
+			/* Check if we are forcing or not - optional parameter */
+			if ( !iForce )
+			{
+				iForce = 0;
+			}
+			
+			/* Need to take account of custom filtering functions - always filter */
+			if ( DataTable.ext.afnFiltering.length !== 0 )
+			{
+				iForce = 1;
+			}
+			
+			/*
+			 * If the input is blank - we want the full data set
+			 */
+			if ( sInput.length <= 0 )
+			{
+				oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			}
+			else
+			{
+				/*
+				 * We are starting a new search or the new search string is smaller 
+				 * then the old one (i.e. delete). Search from the master array
+			 	 */
+				if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
+					   oPrevSearch.sSearch.length > sInput.length || iForce == 1 ||
+					   sInput.indexOf(oPrevSearch.sSearch) !== 0 )
+				{
+					/* Nuke the old display array - we are going to rebuild it */
+					oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
+					
+					/* Force a rebuild of the search array */
+					_fnBuildSearchArray( oSettings, 1 );
+					
+					/* Search through all records to populate the search array
+					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 
+					 * mapping
+					 */
+					for ( i=0 ; i<oSettings.aiDisplayMaster.length ; i++ )
+					{
+						if ( rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+							oSettings.aiDisplay.push( oSettings.aiDisplayMaster[i] );
+						}
+					}
+			  }
+			  else
+				{
+			  	/* Using old search array - refine it - do it this way for speed
+			  	 * Don't have to search the whole master array again
+					 */
+			  	var iIndexCorrector = 0;
+			  	
+			  	/* Search the current results */
+			  	for ( i=0 ; i<oSettings.asDataSearch.length ; i++ )
+					{
+			  		if ( ! rpSearch.test(oSettings.asDataSearch[i]) )
+						{
+			  			oSettings.aiDisplay.splice( i-iIndexCorrector, 1 );
+			  			iIndexCorrector++;
+			  		}
+			  	}
+			  }
+			}
+		}
+		
+		
+		/**
+		 * Create an array which can be quickly search through
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iMaster use the master data array - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchArray ( oSettings, iMaster )
+		{
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				/* Clear out the old data */
+				oSettings.asDataSearch = [];
+		
+				var aiFilterColumns = _fnGetColumns( oSettings, 'bSearchable' );
+				var aiIndex = (iMaster===1) ?
+				 	oSettings.aiDisplayMaster :
+				 	oSettings.aiDisplay;
+				
+				for ( var i=0, iLen=aiIndex.length ; i<iLen ; i++ )
+				{
+					oSettings.asDataSearch[i] = _fnBuildSearchRow(
+						oSettings,
+						_fnGetRowData( oSettings, aiIndex[i], 'filter', aiFilterColumns )
+					);
+				}
+			}
+		}
+		
+		
+		/**
+		 * Create a searchable string from a single data row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {array} aData Row data array to use for the data to search
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBuildSearchRow( oSettings, aData )
+		{
+			var sSearch = aData.join('  ');
+			
+			/* If it looks like there is an HTML entity in the string, attempt to decode it */
+			if ( sSearch.indexOf('&') !== -1 )
+			{
+				sSearch = $('<div>').html(sSearch).text();
+			}
+			
+			// Strip newline characters
+			return sSearch.replace( /[\n\r]/g, " " );
+		}
+		
+		/**
+		 * Build a regular expression object suitable for searching a table
+		 *  @param {string} sSearch string to search for
+		 *  @param {bool} bRegex treat as a regular expression or not
+		 *  @param {bool} bSmart perform smart filtering or not
+		 *  @param {bool} bCaseInsensitive Do case insensitive matching or not
+		 *  @returns {RegExp} constructed object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive )
+		{
+			var asSearch, sRegExpString;
+			
+			if ( bSmart )
+			{
+				/* Generate the regular expression to use. Something along the lines of:
+				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
+				 */
+				asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );
+				sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';
+				return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" );
+			}
+			else
+			{
+				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
+				return new RegExp( sSearch, bCaseInsensitive ? "i" : "" );
+			}
+		}
+		
+		
+		/**
+		 * Convert raw data into something that the user can search on
+		 *  @param {string} sData data to be modified
+		 *  @param {string} sType data type
+		 *  @returns {string} search string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnDataToSearch ( sData, sType )
+		{
+			if ( typeof DataTable.ext.ofnSearch[sType] === "function" )
+			{
+				return DataTable.ext.ofnSearch[sType]( sData );
+			}
+			else if ( sData === null )
+			{
+				return '';
+			}
+			else if ( sType == "html" )
+			{
+				return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" );
+			}
+			else if ( typeof sData === "string" )
+			{
+				return sData.replace(/[\r\n]/g," ");
+			}
+			return sData;
+		}
+		
+		
+		/**
+		 * scape a string such that it can be used in a regular expression
+		 *  @param {string} sVal string to escape
+		 *  @returns {string} escaped string
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnEscapeRegex ( sVal )
+		{
+			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ];
+			var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
+			return sVal.replace(reReplace, '\\$1');
+		}
+		
+		
+		/**
+		 * Generate the node required for the info display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Information element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlInfo ( oSettings )
+		{
+			var nInfo = document.createElement( 'div' );
+			nInfo.className = oSettings.oClasses.sInfo;
+			
+			/* Actions that are to be taken once only for this feature */
+			if ( !oSettings.aanFeatures.i )
+			{
+				/* Add draw callback */
+				oSettings.aoDrawCallback.push( {
+					"fn": _fnUpdateInfo,
+					"sName": "information"
+				} );
+				
+				/* Add id */
+				nInfo.id = oSettings.sTableId+'_info';
+			}
+			oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' );
+			
+			return nInfo;
+		}
+		
+		
+		/**
+		 * Update the information elements in the display
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnUpdateInfo ( oSettings )
+		{
+			/* Show information about the table */
+			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
+			{
+				return;
+			}
+			
+			var
+				oLang = oSettings.oLanguage,
+				iStart = oSettings._iDisplayStart+1,
+				iEnd = oSettings.fnDisplayEnd(),
+				iMax = oSettings.fnRecordsTotal(),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sOut;
+			
+			if ( iTotal === 0 )
+			{
+				/* Empty record set */
+				sOut = oLang.sInfoEmpty;
+			}
+			else {
+				/* Normal record set */
+				sOut = oLang.sInfo;
+			}
+		
+			if ( iTotal != iMax )
+			{
+				/* Record set after filtering */
+				sOut += ' ' + oLang.sInfoFiltered;
+			}
+		
+			// Convert the macros
+			sOut += oLang.sInfoPostFix;
+			sOut = _fnInfoMacros( oSettings, sOut );
+			
+			if ( oLang.fnInfoCallback !== null )
+			{
+				sOut = oLang.fnInfoCallback.call( oSettings.oInstance, 
+					oSettings, iStart, iEnd, iMax, iTotal, sOut );
+			}
+			
+			var n = oSettings.aanFeatures.i;
+			for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+			{
+				$(n[i]).html( sOut );
+			}
+		}
+		
+		
+		function _fnInfoMacros ( oSettings, str )
+		{
+			var
+				iStart = oSettings._iDisplayStart+1,
+				sStart = oSettings.fnFormatNumber( iStart ),
+				iEnd = oSettings.fnDisplayEnd(),
+				sEnd = oSettings.fnFormatNumber( iEnd ),
+				iTotal = oSettings.fnRecordsDisplay(),
+				sTotal = oSettings.fnFormatNumber( iTotal ),
+				iMax = oSettings.fnRecordsTotal(),
+				sMax = oSettings.fnFormatNumber( iMax );
+		
+			// When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+			// internally
+			if ( oSettings.oScroll.bInfinite )
+			{
+				sStart = oSettings.fnFormatNumber( 1 );
+			}
+		
+			return str.
+				replace(/_START_/g, sStart).
+				replace(/_END_/g,   sEnd).
+				replace(/_TOTAL_/g, sTotal).
+				replace(/_MAX_/g,   sMax);
+		}
+		
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitialise ( oSettings )
+		{
+			var i, iLen, iAjaxStart=oSettings.iInitDisplayStart;
+			
+			/* Ensure that the table data is fully initialised */
+			if ( oSettings.bInitialised === false )
+			{
+				setTimeout( function(){ _fnInitialise( oSettings ); }, 200 );
+				return;
+			}
+			
+			/* Show the display HTML options */
+			_fnAddOptionsHtml( oSettings );
+			
+			/* Build and draw the header / footer for the table */
+			_fnBuildHead( oSettings );
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+		
+			/* Okay to show that something is going on now */
+			_fnProcessingDisplay( oSettings, true );
+			
+			/* Calculate sizes for columns */
+			if ( oSettings.oFeatures.bAutoWidth )
+			{
+				_fnCalculateColumnWidths( oSettings );
+			}
+			
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoColumns[i].sWidth !== null )
+				{
+					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );
+				}
+			}
+			
+			/* If there is default sorting required - let's do it. The sort function will do the
+			 * drawing for us. Otherwise we draw the table regardless of the Ajax source - this allows
+			 * the table to look initialised for Ajax sourcing data (show 'loading' message possibly)
+			 */
+			if ( oSettings.oFeatures.bSort )
+			{
+				_fnSort( oSettings );
+			}
+			else if ( oSettings.oFeatures.bFilter )
+			{
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			/* if there is an ajax source load the data */
+			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )
+			{
+				var aoData = [];
+				_fnServerParams( oSettings, aoData );
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData, function(json) {
+					var aData = (oSettings.sAjaxDataProp !== "") ?
+					 	_fnGetObjectDataFn( oSettings.sAjaxDataProp )(json) : json;
+		
+					/* Got the data - add it to the table */
+					for ( i=0 ; i<aData.length ; i++ )
+					{
+						_fnAddData( oSettings, aData[i] );
+					}
+					
+					/* Reset the init display for cookie saving. We've already done a filter, and
+					 * therefore cleared it before. So we need to make it appear 'fresh'
+					 */
+					oSettings.iInitDisplayStart = iAjaxStart;
+					
+					if ( oSettings.oFeatures.bSort )
+					{
+						_fnSort( oSettings );
+					}
+					else
+					{
+						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+						_fnCalculateEnd( oSettings );
+						_fnDraw( oSettings );
+					}
+					
+					_fnProcessingDisplay( oSettings, false );
+					_fnInitComplete( oSettings, json );
+				}, oSettings );
+				return;
+			}
+			
+			/* Server-side processing initialisation complete is done at the end of _fnDraw */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				_fnInitComplete( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Draw the table for the first time, adding all required features
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} [json] JSON from the server that completed the table, if using Ajax source
+		 *    with client-side processing (optional)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnInitComplete ( oSettings, json )
+		{
+			oSettings._bInitComplete = true;
+			_fnCallbackFire( oSettings, 'aoInitComplete', 'init', [oSettings, json] );
+		}
+		
+		
+		/**
+		 * Language compatibility - when certain options are given, and others aren't, we
+		 * need to duplicate the values over, in order to provide backwards compatibility
+		 * with older language files.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLanguageCompat( oLanguage )
+		{
+			var oDefaults = DataTable.defaults.oLanguage;
+		
+			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+			 * sZeroRecords - assuming that is given.
+			 */
+			if ( !oLanguage.sEmptyTable && oLanguage.sZeroRecords &&
+				oDefaults.sEmptyTable === "No data available in table" )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
+			}
+		
+			/* Likewise with loading records */
+			if ( !oLanguage.sLoadingRecords && oLanguage.sZeroRecords &&
+				oDefaults.sLoadingRecords === "Loading..." )
+			{
+				_fnMap( oLanguage, oLanguage, 'sZeroRecords', 'sLoadingRecords' );
+			}
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for user display length changing
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Display length feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlLength ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			/* This can be overruled by not using the _MENU_ var/macro in the language variable */
+			var sName = 'name="'+oSettings.sTableId+'_length"';
+			var sStdMenu = '<select size="1" '+sName+'>';
+			var i, iLen;
+			var aLengthMenu = oSettings.aLengthMenu;
+			
+			if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' && 
+					typeof aLengthMenu[1] === 'object' )
+			{
+				for ( i=0, iLen=aLengthMenu[0].length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[0][i]+'">'+aLengthMenu[1][i]+'</option>';
+				}
+			}
+			else
+			{
+				for ( i=0, iLen=aLengthMenu.length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+aLengthMenu[i]+'">'+aLengthMenu[i]+'</option>';
+				}
+			}
+			sStdMenu += '</select>';
+			
+			var nLength = document.createElement( 'div' );
+			if ( !oSettings.aanFeatures.l )
+			{
+				nLength.id = oSettings.sTableId+'_length';
+			}
+			nLength.className = oSettings.oClasses.sLength;
+			nLength.innerHTML = '<label>'+oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu )+'</label>';
+			
+			/*
+			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
+			 * and Stefan Skopnik for fixing the fix!
+			 */
+			$('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true);
+			
+			$('select', nLength).bind( 'change.DT', function(e) {
+				var iVal = $(this).val();
+				
+				/* Update all other length options for the new display */
+				var n = oSettings.aanFeatures.l;
+				for ( i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != this.parentNode )
+					{
+						$('select', n[i]).val( iVal );
+					}
+				}
+				
+				/* Redraw the table */
+				oSettings._iDisplayLength = parseInt(iVal, 10);
+				_fnCalculateEnd( oSettings );
+				
+				/* If we have space to show extra rows (backing up from the end point - then do so */
+				if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;
+					if ( oSettings._iDisplayStart < 0 )
+					{
+						oSettings._iDisplayStart = 0;
+					}
+				}
+				
+				if ( oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+				
+				_fnDraw( oSettings );
+			} );
+		
+		
+			$('select', nLength).attr('aria-controls', oSettings.sTableId);
+			
+			return nLength;
+		}
+		
+		
+		/**
+		 * Recalculate the end point based on the start point
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateEnd( oSettings )
+		{
+			if ( oSettings.oFeatures.bPaginate === false )
+			{
+				oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+			}
+			else
+			{
+				/* Set the end point of the display - based on how many elements there are
+				 * still to display
+				 */
+				if ( oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||
+					   oSettings._iDisplayLength == -1 )
+				{
+					oSettings._iDisplayEnd = oSettings.aiDisplay.length;
+				}
+				else
+				{
+					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
+				}
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Note that most of the paging logic is done in 
+		 * DataTable.ext.oPagination
+		 */
+		
+		/**
+		 * Generate the node required for default pagination
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Pagination feature node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlPaginate ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			var nPaginate = document.createElement( 'div' );
+			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
+			
+			DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, 
+				function( oSettings ) {
+					_fnCalculateEnd( oSettings );
+					_fnDraw( oSettings );
+				}
+			);
+			
+			/* Add a draw callback for the pagination on first instance, to update the paging display */
+			if ( !oSettings.aanFeatures.p )
+			{
+				oSettings.aoDrawCallback.push( {
+					"fn": function( oSettings ) {
+						DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
+							_fnCalculateEnd( oSettings );
+							_fnDraw( oSettings );
+						} );
+					},
+					"sName": "pagination"
+				} );
+			}
+			return nPaginate;
+		}
+		
+		
+		/**
+		 * Alter the display settings to change the page
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer)
+		 *  @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnPageChange ( oSettings, mAction )
+		{
+			var iOldStart = oSettings._iDisplayStart;
+			
+			if ( typeof mAction === "number" )
+			{
+				oSettings._iDisplayStart = mAction * oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "first" )
+			{
+				oSettings._iDisplayStart = 0;
+			}
+			else if ( mAction == "previous" )
+			{
+				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
+					oSettings._iDisplayStart - oSettings._iDisplayLength :
+					0;
+				
+				/* Correct for under-run */
+				if ( oSettings._iDisplayStart < 0 )
+				{
+				  oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "next" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					/* Make sure we are not over running the display array */
+					if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )
+					{
+						oSettings._iDisplayStart += oSettings._iDisplayLength;
+					}
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( mAction == "last" )
+			{
+				if ( oSettings._iDisplayLength >= 0 )
+				{
+					var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;
+					oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;
+				}
+				else
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 0, "Unknown paging action: "+mAction );
+			}
+			$(oSettings.oInstance).trigger('page', oSettings);
+			
+			return iOldStart != oSettings._iDisplayStart;
+		}
+		
+		
+		
+		/**
+		 * Generate the node required for the processing node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Processing element
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlProcessing ( oSettings )
+		{
+			var nProcessing = document.createElement( 'div' );
+			
+			if ( !oSettings.aanFeatures.r )
+			{
+				nProcessing.id = oSettings.sTableId+'_processing';
+			}
+			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
+			nProcessing.className = oSettings.oClasses.sProcessing;
+			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
+			
+			return nProcessing;
+		}
+		
+		
+		/**
+		 * Display or hide the processing indicator
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bShow Show the processing indicator (true) or not (false)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnProcessingDisplay ( oSettings, bShow )
+		{
+			if ( oSettings.oFeatures.bProcessing )
+			{
+				var an = oSettings.aanFeatures.r;
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					an[i].style.visibility = bShow ? "visible" : "hidden";
+				}
+			}
+		
+			$(oSettings.oInstance).trigger('processing', [oSettings, bShow]);
+		}
+		
+		/**
+		 * Add any control elements for the table - specifically scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnFeatureHtmlTable ( oSettings )
+		{
+			/* Check if scrolling is enabled or not - if not then leave the DOM unaltered */
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				return oSettings.nTable;
+			}
+			
+			/*
+			 * The HTML structure that we want to generate in this function is:
+			 *  div - nScroller
+			 *    div - nScrollHead
+			 *      div - nScrollHeadInner
+			 *        table - nScrollHeadTable
+			 *          thead - nThead
+			 *    div - nScrollBody
+			 *      table - oSettings.nTable
+			 *        thead - nTheadSize
+			 *        tbody - nTbody
+			 *    div - nScrollFoot
+			 *      div - nScrollFootInner
+			 *        table - nScrollFootTable
+			 *          tfoot - nTfoot
+			 */
+			var
+			 	nScroller = document.createElement('div'),
+			 	nScrollHead = document.createElement('div'),
+			 	nScrollHeadInner = document.createElement('div'),
+			 	nScrollBody = document.createElement('div'),
+			 	nScrollFoot = document.createElement('div'),
+			 	nScrollFootInner = document.createElement('div'),
+			 	nScrollHeadTable = oSettings.nTable.cloneNode(false),
+			 	nScrollFootTable = oSettings.nTable.cloneNode(false),
+				nThead = oSettings.nTable.getElementsByTagName('thead')[0],
+			 	nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null : 
+					oSettings.nTable.getElementsByTagName('tfoot')[0],
+				oClasses = oSettings.oClasses;
+			
+			nScrollHead.appendChild( nScrollHeadInner );
+			nScrollFoot.appendChild( nScrollFootInner );
+			nScrollBody.appendChild( oSettings.nTable );
+			nScroller.appendChild( nScrollHead );
+			nScroller.appendChild( nScrollBody );
+			nScrollHeadInner.appendChild( nScrollHeadTable );
+			nScrollHeadTable.appendChild( nThead );
+			if ( nTfoot !== null )
+			{
+				nScroller.appendChild( nScrollFoot );
+				nScrollFootInner.appendChild( nScrollFootTable );
+				nScrollFootTable.appendChild( nTfoot );
+			}
+			
+			nScroller.className = oClasses.sScrollWrapper;
+			nScrollHead.className = oClasses.sScrollHead;
+			nScrollHeadInner.className = oClasses.sScrollHeadInner;
+			nScrollBody.className = oClasses.sScrollBody;
+			nScrollFoot.className = oClasses.sScrollFoot;
+			nScrollFootInner.className = oClasses.sScrollFootInner;
+			
+			if ( oSettings.oScroll.bAutoCss )
+			{
+				nScrollHead.style.overflow = "hidden";
+				nScrollHead.style.position = "relative";
+				nScrollFoot.style.overflow = "hidden";
+				nScrollBody.style.overflow = "auto";
+			}
+			
+			nScrollHead.style.border = "0";
+			nScrollHead.style.width = "100%";
+			nScrollFoot.style.border = "0";
+			nScrollHeadInner.style.width = oSettings.oScroll.sXInner !== "" ?
+				oSettings.oScroll.sXInner : "100%"; /* will be overwritten */
+			
+			/* Modify attributes to respect the clones */
+			nScrollHeadTable.removeAttribute('id');
+			nScrollHeadTable.style.marginLeft = "0";
+			oSettings.nTable.style.marginLeft = "0";
+			if ( nTfoot !== null )
+			{
+				nScrollFootTable.removeAttribute('id');
+				nScrollFootTable.style.marginLeft = "0";
+			}
+			
+			/* Move caption elements from the body to the header, footer or leave where it is
+			 * depending on the configuration. Note that the DTD says there can be only one caption */
+			var nCaption = $(oSettings.nTable).children('caption');
+			if ( nCaption.length > 0 )
+			{
+				nCaption = nCaption[0];
+				if ( nCaption._captionSide === "top" )
+				{
+					nScrollHeadTable.appendChild( nCaption );
+				}
+				else if ( nCaption._captionSide === "bottom" && nTfoot )
+				{
+					nScrollFootTable.appendChild( nCaption );
+				}
+			}
+			
+			/*
+			 * Sizing
+			 */
+			/* When x-scrolling add the width and a scroller to move the header with the body */
+			if ( oSettings.oScroll.sX !== "" )
+			{
+				nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );
+				
+				if ( nTfoot !== null )
+				{
+					nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );	
+				}
+				
+				/* When the body is scrolled, then we also want to scroll the headers */
+				$(nScrollBody).scroll( function (e) {
+					nScrollHead.scrollLeft = this.scrollLeft;
+					
+					if ( nTfoot !== null )
+					{
+						nScrollFoot.scrollLeft = this.scrollLeft;
+					}
+				} );
+			}
+			
+			/* When yscrolling, add the height */
+			if ( oSettings.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );
+			}
+			
+			/* Redraw - align columns across the tables */
+			oSettings.aoDrawCallback.push( {
+				"fn": _fnScrollDraw,
+				"sName": "scrolling"
+			} );
+			
+			/* Infinite scrolling event handlers */
+			if ( oSettings.oScroll.bInfinite )
+			{
+				$(nScrollBody).scroll( function() {
+					/* Use a blocker to stop scrolling from loading more data while other data is still loading */
+					if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 )
+					{
+						/* Check if we should load the next data set */
+						if ( $(this).scrollTop() + $(this).height() > 
+							$(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )
+						{
+							/* Only do the redraw if we have to - we might be at the end of the data */
+							if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )
+							{
+								_fnPageChange( oSettings, 'next' );
+								_fnCalculateEnd( oSettings );
+								_fnDraw( oSettings );
+							}
+						}
+					}
+				} );
+			}
+			
+			oSettings.nScrollHead = nScrollHead;
+			oSettings.nScrollFoot = nScrollFoot;
+			
+			return nScroller;
+		}
+		
+		
+		/**
+		 * Update the various tables for resizing. It's a bit of a pig this function, but
+		 * basically the idea to:
+		 *   1. Re-create the table inside the scrolling div
+		 *   2. Take live measurements from the DOM
+		 *   3. Apply the measurements
+		 *   4. Clean up
+		 *  @param {object} o dataTables settings object
+		 *  @returns {node} Node to add to the DOM
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollDraw ( o )
+		{
+			var
+				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
+				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
+				nScrollBody = o.nTable.parentNode,
+				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
+				nTheadSize, nTfootSize,
+				iWidth, aApplied=[], aAppliedFooter=[], iSanityWidth,
+				nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null,
+				nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null,
+				ie67 = o.oBrowser.bScrollOversize,
+				zeroOut = function(nSizer) {
+					oStyle = nSizer.style;
+					oStyle.paddingTop = "0";
+					oStyle.paddingBottom = "0";
+					oStyle.borderTopWidth = "0";
+					oStyle.borderBottomWidth = "0";
+					oStyle.height = 0;
+				};
+			
+			/*
+			 * 1. Re-create the table inside the scrolling div
+			 */
+			
+			/* Remove the old minimised thead and tfoot elements in the inner table */
+			$(o.nTable).children('thead, tfoot').remove();
+		
+			/* Clone the current header and footer elements and then place it into the inner table */
+			nTheadSize = $(o.nTHead).clone()[0];
+			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
+			anHeadToSize = o.nTHead.getElementsByTagName('tr');
+			anHeadSizers = nTheadSize.getElementsByTagName('tr');
+			
+			if ( o.nTFoot !== null )
+			{
+				nTfootSize = $(o.nTFoot).clone()[0];
+				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
+				anFootToSize = o.nTFoot.getElementsByTagName('tr');
+				anFootSizers = nTfootSize.getElementsByTagName('tr');
+			}
+			
+			/*
+			 * 2. Take live measurements from the DOM - do not alter the DOM itself!
+			 */
+			
+			/* Remove old sizing and apply the calculated column widths
+			 * Get the unique column headers in the newly created (cloned) header. We want to apply the
+			 * calculated sizes to this header
+			 */
+			if ( o.oScroll.sX === "" )
+			{
+				nScrollBody.style.width = '100%';
+				nScrollHeadInner.parentNode.style.width = '100%';
+			}
+			
+			var nThs = _fnGetUniqueThs( o, nTheadSize );
+			for ( i=0, iLen=nThs.length ; i<iLen ; i++ )
+			{
+				iVis = _fnVisibleToColumnIndex( o, i );
+				nThs[i].style.width = o.aoColumns[iVis].sWidth;
+			}
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(n) {
+					n.style.width = "";
+				}, anFootSizers );
+			}
+		
+			// If scroll collapse is enabled, when we put the headers back into the body for sizing, we
+			// will end up forcing the scrollbar to appear, making our measurements wrong for when we
+			// then hide it (end of this function), so add the header height to the body scroller.
+			if ( o.oScroll.bCollapse && o.oScroll.sY !== "" )
+			{
+				nScrollBody.style.height = (nScrollBody.offsetHeight + o.nTHead.offsetHeight)+"px";
+			}
+			
+			/* Size the table as a whole */
+			iSanityWidth = $(o.nTable).outerWidth();
+			if ( o.oScroll.sX === "" )
+			{
+				/* No x scrolling */
+				o.nTable.style.width = "100%";
+				
+				/* I know this is rubbish - but IE7 will make the width of the table when 100% include
+				 * the scrollbar - which is shouldn't. When there is a scrollbar we need to take this
+				 * into account.
+				 */
+				if ( ie67 && ($('tbody', nScrollBody).height() > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth);
+				}
+			}
+			else
+			{
+				if ( o.oScroll.sXInner !== "" )
+				{
+					/* x scroll inner has been given - use it */
+					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
+				}
+				else if ( iSanityWidth == $(nScrollBody).width() &&
+				   $(nScrollBody).height() < $(o.nTable).height() )
+				{
+					/* There is y-scrolling - try to take account of the y scroll bar */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );
+					if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )
+					{
+						/* Not possible to take account of it */
+						o.nTable.style.width = _fnStringToCss( iSanityWidth );
+					}
+				}
+				else
+				{
+					/* All else fails */
+					o.nTable.style.width = _fnStringToCss( iSanityWidth );
+				}
+			}
+			
+			/* Recalculate the sanity width - now that we've applied the required width, before it was
+			 * a temporary variable. This is required because the column width calculation is done
+			 * before this table DOM is created.
+			 */
+			iSanityWidth = $(o.nTable).outerWidth();
+			
+			/* We want the hidden header to have zero height, so remove padding and borders. Then
+			 * set the width based on the real headers
+			 */
+			
+			// Apply all styles in one pass. Invalidates layout only once because we don't read any 
+			// DOM properties.
+			_fnApplyToChildren( zeroOut, anHeadSizers );
+			 
+			// Read all widths in next pass. Forces layout only once because we do not change 
+			// any DOM properties.
+			_fnApplyToChildren( function(nSizer) {
+				aApplied.push( _fnStringToCss( $(nSizer).width() ) );
+			}, anHeadSizers );
+			 
+			// Apply all widths in final pass. Invalidates layout only once because we do not
+			// read any DOM properties.
+			_fnApplyToChildren( function(nToSize, i) {
+				nToSize.style.width = aApplied[i];
+			}, anHeadToSize );
+		
+			$(anHeadSizers).height(0);
+			
+			/* Same again with the footer if we have one */
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( zeroOut, anFootSizers );
+				 
+				_fnApplyToChildren( function(nSizer) {
+					aAppliedFooter.push( _fnStringToCss( $(nSizer).width() ) );
+				}, anFootSizers );
+				 
+				_fnApplyToChildren( function(nToSize, i) {
+					nToSize.style.width = aAppliedFooter[i];
+				}, anFootToSize );
+		
+				$(anFootSizers).height(0);
+			}
+			
+			/*
+			 * 3. Apply the measurements
+			 */
+			
+			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width
+			 * to what they currently are
+			 */
+			_fnApplyToChildren( function(nSizer, i) {
+				nSizer.innerHTML = "";
+				nSizer.style.width = aApplied[i];
+			}, anHeadSizers );
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(nSizer, i) {
+					nSizer.innerHTML = "";
+					nSizer.style.width = aAppliedFooter[i];
+				}, anFootSizers );
+			}
+			
+			/* Sanity check that the table is of a sensible width. If not then we are going to get
+			 * misalignment - try to prevent this by not allowing the table to shrink below its min width
+			 */
+			if ( $(o.nTable).outerWidth() < iSanityWidth )
+			{
+				/* The min width depends upon if we have a vertical scrollbar visible or not */
+				var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight || 
+					$(nScrollBody).css('overflow-y') == "scroll")) ?
+						iSanityWidth+o.oScroll.iBarWidth : iSanityWidth;
+				
+				/* IE6/7 are a law unto themselves... */
+				if ( ie67 && (nScrollBody.scrollHeight > 
+					nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll")  )
+				{
+					o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth );
+				}
+				
+				/* Apply the calculated minimum width to the table wrappers */
+				nScrollBody.style.width = _fnStringToCss( iCorrection );
+				o.nScrollHead.style.width = _fnStringToCss( iCorrection );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( iCorrection );
+				}
+				
+				/* And give the user a warning that we've stopped the table getting too small */
+				if ( o.oScroll.sX === "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. The table has been drawn at its minimum possible width." );
+				}
+				else if ( o.oScroll.sXInner !== "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. Increase the sScrollXInner value or remove it to allow automatic"+
+						" calculation" );
+				}
+			}
+			else
+			{
+				nScrollBody.style.width = _fnStringToCss( '100%' );
+				o.nScrollHead.style.width = _fnStringToCss( '100%' );
+				
+				if ( o.nTFoot !== null )
+				{
+					o.nScrollFoot.style.width = _fnStringToCss( '100%' );
+				}
+			}
+			
+			
+			/*
+			 * 4. Clean up
+			 */
+			if ( o.oScroll.sY === "" )
+			{
+				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
+				 * the scrollbar height from the visible display, rather than adding it on. We need to
+				 * set the height in order to sort this. Don't want to do it in any other browsers.
+				 */
+				if ( ie67 )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );
+				}
+			}
+			
+			if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )
+			{
+				nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );
+				
+				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
+				 	o.oScroll.iBarWidth : 0;
+				if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )
+				{
+					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra );
+				}
+			}
+			
+			/* Finally set the width's of the header and footer tables */
+			var iOuterWidth = $(o.nTable).outerWidth();
+			nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );
+			nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth );
+		
+			// Figure out if there are scrollbar present - if so then we need a the header and footer to
+			// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
+			var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll";
+			nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			
+			if ( o.nTFoot !== null )
+			{
+				nScrollFootTable.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.width = _fnStringToCss( iOuterWidth );
+				nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
+			}
+		
+			/* Adjust the position of the header in case we loose the y-scrollbar */
+			$(nScrollBody).scroll();
+			
+			/* If sorting or filtering has occurred, jump the scrolling back to the top */
+			if ( o.bSorted || o.bFiltered )
+			{
+				nScrollBody.scrollTop = 0;
+			}
+		}
+		
+		
+		/**
+		 * Apply a given function to the display child nodes of an element array (typically
+		 * TD children of TR rows
+		 *  @param {function} fn Method to apply to the objects
+		 *  @param array {nodes} an1 List of elements to look through for display children
+		 *  @param array {nodes} an2 Another list (identical structure to the first) - optional
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnApplyToChildren( fn, an1, an2 )
+		{
+			var index=0, i=0, iLen=an1.length;
+			var nNode1, nNode2;
+		
+			while ( i < iLen )
+			{
+				nNode1 = an1[i].firstChild;
+				nNode2 = an2 ? an2[i].firstChild : null;
+				while ( nNode1 )
+				{
+					if ( nNode1.nodeType === 1 )
+					{
+						if ( an2 )
+						{
+							fn( nNode1, nNode2, index );
+						}
+						else
+						{
+							fn( nNode1, index );
+						}
+						index++;
+					}
+					nNode1 = nNode1.nextSibling;
+					nNode2 = an2 ? nNode2.nextSibling : null;
+				}
+				i++;
+			}
+		}
+		
+		/**
+		 * Convert a CSS unit width to pixels (e.g. 2em)
+		 *  @param {string} sWidth width to be converted
+		 *  @param {node} nParent parent to get the with for (required for relative widths) - optional
+		 *  @returns {int} iWidth width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnConvertToWidth ( sWidth, nParent )
+		{
+			if ( !sWidth || sWidth === null || sWidth === '' )
+			{
+				return 0;
+			}
+			
+			if ( !nParent )
+			{
+				nParent = document.body;
+			}
+			
+			var iWidth;
+			var nTmp = document.createElement( "div" );
+			nTmp.style.width = _fnStringToCss( sWidth );
+			
+			nParent.appendChild( nTmp );
+			iWidth = nTmp.offsetWidth;
+			nParent.removeChild( nTmp );
+			
+			return ( iWidth );
+		}
+		
+		
+		/**
+		 * Calculate the width of columns for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCalculateColumnWidths ( oSettings )
+		{
+			var iTableWidth = oSettings.nTable.offsetWidth;
+			var iUserInputs = 0;
+			var iTmpWidth;
+			var iVisibleColumns = 0;
+			var iColums = oSettings.aoColumns.length;
+			var i, iIndex, iCorrector, iWidth;
+			var oHeaders = $('th', oSettings.nTHead);
+			var widthAttr = oSettings.nTable.getAttribute('width');
+			var nWrapper = oSettings.nTable.parentNode;
+			
+			/* Convert any user input sizes into pixel sizes */
+			for ( i=0 ; i<iColums ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible )
+				{
+					iVisibleColumns++;
+					
+					if ( oSettings.aoColumns[i].sWidth !== null )
+					{
+						iTmpWidth = _fnConvertToWidth( oSettings.aoColumns[i].sWidthOrig, 
+							nWrapper );
+						if ( iTmpWidth !== null )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+						}
+							
+						iUserInputs++;
+					}
+				}
+			}
+			
+			/* If the number of columns in the DOM equals the number that we have to process in 
+			 * DataTables, then we can use the offsets that are created by the web-browser. No custom 
+			 * sizes can be set in order for this to happen, nor scrolling used
+			 */
+			if ( iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums &&
+				oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )
+			{
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					iTmpWidth = $(oHeaders[i]).width();
+					if ( iTmpWidth !== null )
+					{
+						oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );
+					}
+				}
+			}
+			else
+			{
+				/* Otherwise we are going to have to do some calculations to get the width of each column.
+				 * Construct a 1 row table with the widest node in the data, and any user defined widths,
+				 * then insert it into the DOM and allow the browser to do all the hard work of
+				 * calculating table widths.
+				 */
+				var
+					nCalcTmp = oSettings.nTable.cloneNode( false ),
+					nTheadClone = oSettings.nTHead.cloneNode(true),
+					nBody = document.createElement( 'tbody' ),
+					nTr = document.createElement( 'tr' ),
+					nDivSizing;
+				
+				nCalcTmp.removeAttribute( "id" );
+				nCalcTmp.appendChild( nTheadClone );
+				if ( oSettings.nTFoot !== null )
+				{
+					nCalcTmp.appendChild( oSettings.nTFoot.cloneNode(true) );
+					_fnApplyToChildren( function(n) {
+						n.style.width = "";
+					}, nCalcTmp.getElementsByTagName('tr') );
+				}
+				
+				nCalcTmp.appendChild( nBody );
+				nBody.appendChild( nTr );
+				
+				/* Remove any sizing that was previously applied by the styles */
+				var jqColSizing = $('thead th', nCalcTmp);
+				if ( jqColSizing.length === 0 )
+				{
+					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);
+				}
+		
+				/* Apply custom sizing to the cloned header */
+				var nThs = _fnGetUniqueThs( oSettings, nTheadClone );
+				iCorrector = 0;
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					var oColumn = oSettings.aoColumns[i];
+					if ( oColumn.bVisible && oColumn.sWidthOrig !== null && oColumn.sWidthOrig !== "" )
+					{
+						nThs[i-iCorrector].style.width = _fnStringToCss( oColumn.sWidthOrig );
+					}
+					else if ( oColumn.bVisible )
+					{
+						nThs[i-iCorrector].style.width = "";
+					}
+					else
+					{
+						iCorrector++;
+					}
+				}
+		
+				/* Find the biggest td for each column and put it into the table */
+				for ( i=0 ; i<iColums ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						var nTd = _fnGetWidestNode( oSettings, i );
+						if ( nTd !== null )
+						{
+							nTd = nTd.cloneNode(true);
+							if ( oSettings.aoColumns[i].sContentPadding !== "" )
+							{
+								nTd.innerHTML += oSettings.aoColumns[i].sContentPadding;
+							}
+							nTr.appendChild( nTd );
+						}
+					}
+				}
+				
+				/* Build the table and 'display' it */
+				nWrapper.appendChild( nCalcTmp );
+				
+				/* When scrolling (X or Y) we want to set the width of the table as appropriate. However,
+				 * when not scrolling leave the table width as it is. This results in slightly different,
+				 * but I think correct behaviour
+				 */
+				if ( oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);
+				}
+				else if ( oSettings.oScroll.sX !== "" )
+				{
+					nCalcTmp.style.width = "";
+					if ( $(nCalcTmp).width() < nWrapper.offsetWidth )
+					{
+						nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+					}
+				}
+				else if ( oSettings.oScroll.sY !== "" )
+				{
+					nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );
+				}
+				else if ( widthAttr )
+				{
+					nCalcTmp.style.width = _fnStringToCss( widthAttr );
+				}
+				nCalcTmp.style.visibility = "hidden";
+				
+				/* Scrolling considerations */
+				_fnScrollingWidthAdjust( oSettings, nCalcTmp );
+				
+				/* Read the width's calculated by the browser and store them for use by the caller. We
+				 * first of all try to use the elements in the body, but it is possible that there are
+				 * no elements there, under which circumstances we use the header elements
+				 */
+				var oNodes = $("tbody tr:eq(0)", nCalcTmp).children();
+				if ( oNodes.length === 0 )
+				{
+					oNodes = _fnGetUniqueThs( oSettings, $('thead', nCalcTmp)[0] );
+				}
+		
+				/* Browsers need a bit of a hand when a width is assigned to any columns when 
+				 * x-scrolling as they tend to collapse the table to the min-width, even if
+				 * we sent the column widths. So we need to keep track of what the table width
+				 * should be by summing the user given values, and the automatic values
+				 */
+				if ( oSettings.oScroll.sX !== "" )
+				{
+					var iTotal = 0;
+					iCorrector = 0;
+					for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+					{
+						if ( oSettings.aoColumns[i].bVisible )
+						{
+							if ( oSettings.aoColumns[i].sWidthOrig === null )
+							{
+								iTotal += $(oNodes[iCorrector]).outerWidth();
+							}
+							else
+							{
+								iTotal += parseInt(oSettings.aoColumns[i].sWidth.replace('px',''), 10) +
+									($(oNodes[iCorrector]).outerWidth() - $(oNodes[iCorrector]).width());
+							}
+							iCorrector++;
+						}
+					}
+					
+					nCalcTmp.style.width = _fnStringToCss( iTotal );
+					oSettings.nTable.style.width = _fnStringToCss( iTotal );
+				}
+		
+				iCorrector = 0;
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iWidth = $(oNodes[iCorrector]).width();
+						if ( iWidth !== null && iWidth > 0 )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
+						}
+						iCorrector++;
+					}
+				}
+		
+				var cssWidth = $(nCalcTmp).css('width');
+				oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ?
+				    cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() );
+				nCalcTmp.parentNode.removeChild( nCalcTmp );
+			}
+		
+			if ( widthAttr )
+			{
+				oSettings.nTable.style.width = _fnStringToCss( widthAttr );
+			}
+		}
+		
+		
+		/**
+		 * Adjust a table's width to take account of scrolling
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} n table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollingWidthAdjust ( oSettings, n )
+		{
+			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )
+			{
+				/* When y-scrolling only, we want to remove the width of the scroll bar so the table
+				 * + scroll bar will fit into the area avaialble.
+				 */
+				var iOrigWidth = $(n).width();
+				n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );
+			}
+			else if ( oSettings.oScroll.sX !== "" )
+			{
+				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */
+				n.style.width = _fnStringToCss( $(n).outerWidth() );
+			}
+		}
+		
+		
+		/**
+		 * Get the widest node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {node} widest table node
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetWidestNode( oSettings, iCol )
+		{
+			var iMaxIndex = _fnGetMaxLenString( oSettings, iCol );
+			if ( iMaxIndex < 0 )
+			{
+				return null;
+			}
+		
+			if ( oSettings.aoData[iMaxIndex].nTr === null )
+			{
+				var n = document.createElement('td');
+				n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' );
+				return n;
+			}
+			return _fnGetTdNodes(oSettings, iMaxIndex)[iCol];
+		}
+		
+		
+		/**
+		 * Get the maximum strlen for each data column
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iCol column of interest
+		 *  @returns {string} max string length for each column
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetMaxLenString( oSettings, iCol )
+		{
+			var iMax = -1;
+			var iMaxIndex = -1;
+			
+			for ( var i=0 ; i<oSettings.aoData.length ; i++ )
+			{
+				var s = _fnGetCellData( oSettings, i, iCol, 'display' )+"";
+				s = s.replace( /<.*?>/g, "" );
+				if ( s.length > iMax )
+				{
+					iMax = s.length;
+					iMaxIndex = i;
+				}
+			}
+			
+			return iMaxIndex;
+		}
+		
+		
+		/**
+		 * Append a CSS unit (only if required) to a string
+		 *  @param {array} aArray1 first array
+		 *  @param {array} aArray2 second array
+		 *  @returns {int} 0 if match, 1 if length is different, 2 if no match
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnStringToCss( s )
+		{
+			if ( s === null )
+			{
+				return "0px";
+			}
+			
+			if ( typeof s == 'number' )
+			{
+				if ( s < 0 )
+				{
+					return "0px";
+				}
+				return s+"px";
+			}
+			
+			/* Check if the last character is not 0-9 */
+			var c = s.charCodeAt( s.length-1 );
+			if (c < 0x30 || c > 0x39)
+			{
+				return s;
+			}
+			return s+"px";
+		}
+		
+		
+		/**
+		 * Get the width of a scroll bar in this browser being used
+		 *  @returns {int} width in pixels
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnScrollBarWidth ()
+		{  
+			var inner = document.createElement('p');
+			var style = inner.style;
+			style.width = "100%";
+			style.height = "200px";
+			style.padding = "0px";
+			
+			var outer = document.createElement('div');
+			style = outer.style;
+			style.position = "absolute";
+			style.top = "0px";
+			style.left = "0px";
+			style.visibility = "hidden";
+			style.width = "200px";
+			style.height = "150px";
+			style.padding = "0px";
+			style.overflow = "hidden";
+			outer.appendChild(inner);
+			
+			document.body.appendChild(outer);
+			var w1 = inner.offsetWidth;
+			outer.style.overflow = 'scroll';
+			var w2 = inner.offsetWidth;
+			if ( w1 == w2 )
+			{
+				w2 = outer.clientWidth;
+			}
+			
+			document.body.removeChild(outer);
+			return (w1 - w2);  
+		}
+		
+		/**
+		 * Change the order of the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {bool} bApplyClasses optional - should we apply classes or not
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSort ( oSettings, bApplyClasses )
+		{
+			var
+				i, iLen, j, jLen, k, kLen,
+				sDataType, nTh,
+				aaSort = [],
+			 	aiOrig = [],
+				oSort = DataTable.ext.oSort,
+				aoData = oSettings.aoData,
+				aoColumns = oSettings.aoColumns,
+				oAria = oSettings.oLanguage.oAria;
+			
+			/* No sorting required if server-side or no sorting array */
+			if ( !oSettings.oFeatures.bServerSide && 
+				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
+			{
+				aaSort = ( oSettings.aaSortingFixed !== null ) ?
+					oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
+					oSettings.aaSorting.slice();
+				
+				/* If there is a sorting data type, and a function belonging to it, then we need to
+				 * get the data from the developer's function and apply it for this column
+				 */
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					var iColumn = aaSort[i][0];
+					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );
+					sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
+					if ( DataTable.ext.afnSortData[sDataType] )
+					{
+						var aData = DataTable.ext.afnSortData[sDataType].call( 
+							oSettings.oInstance, oSettings, iColumn, iVisColumn
+						);
+						if ( aData.length === aoData.length )
+						{
+							for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
+							{
+								_fnSetCellData( oSettings, j, iColumn, aData[j] );
+							}
+						}
+						else
+						{
+							_fnLog( oSettings, 0, "Returned data sort array (col "+iColumn+") is the wrong length" );
+						}
+					}
+				}
+				
+				/* Create a value - key array of the current row positions such that we can use their
+				 * current position during the sort, if values match, in order to perform stable sorting
+				 */
+				for ( i=0, iLen=oSettings.aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					aiOrig[ oSettings.aiDisplayMaster[i] ] = i;
+				}
+		
+				/* Build an internal data array which is specific to the sort, so we can get and prep
+				 * the data to be sorted only once, rather than needing to do it every time the sorting
+				 * function runs. This make the sorting function a very simple comparison
+				 */
+				var iSortLen = aaSort.length;
+				var fnSortFormat, aDataSort;
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					for ( j=0 ; j<iSortLen ; j++ )
+					{
+						aDataSort = aoColumns[ aaSort[j][0] ].aDataSort;
+		
+						for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ )
+						{
+							sDataType = aoColumns[ aDataSort[k] ].sType;
+							fnSortFormat = oSort[ (sDataType ? sDataType : 'string')+"-pre" ];
+							
+							aoData[i]._aSortData[ aDataSort[k] ] = fnSortFormat ?
+								fnSortFormat( _fnGetCellData( oSettings, i, aDataSort[k], 'sort' ) ) :
+								_fnGetCellData( oSettings, i, aDataSort[k], 'sort' );
+						}
+					}
+				}
+				
+				/* Do the sort - here we want multi-column sorting based on a given data source (column)
+				 * and sorting function (from oSort) in a certain direction. It's reasonably complex to
+				 * follow on it's own, but this is what we want (example two column sorting):
+				 *  fnLocalSorting = function(a,b){
+				 *  	var iTest;
+				 *  	iTest = oSort['string-asc']('data11', 'data12');
+				 *  	if (iTest !== 0)
+				 *  		return iTest;
+				 *    iTest = oSort['numeric-desc']('data21', 'data22');
+				 *    if (iTest !== 0)
+				 *  		return iTest;
+				 *  	return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				 *  }
+				 * Basically we have a test for each sorting column, if the data in that column is equal,
+				 * test the next column. If all columns match, then we use a numeric sort on the row 
+				 * positions in the original data array to provide a stable sort.
+				 */
+				oSettings.aiDisplayMaster.sort( function ( a, b ) {
+					var k, l, lLen, iTest, aDataSort, sDataType;
+					for ( k=0 ; k<iSortLen ; k++ )
+					{
+						aDataSort = aoColumns[ aaSort[k][0] ].aDataSort;
+		
+						for ( l=0, lLen=aDataSort.length ; l<lLen ; l++ )
+						{
+							sDataType = aoColumns[ aDataSort[l] ].sType;
+							
+							iTest = oSort[ (sDataType ? sDataType : 'string')+"-"+aaSort[k][1] ](
+								aoData[a]._aSortData[ aDataSort[l] ],
+								aoData[b]._aSortData[ aDataSort[l] ]
+							);
+						
+							if ( iTest !== 0 )
+							{
+								return iTest;
+							}
+						}
+					}
+					
+					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				} );
+			}
+			
+			/* Alter the sorting classes to take account of the changes */
+			if ( (bApplyClasses === undefined || bApplyClasses) && !oSettings.oFeatures.bDeferRender )
+			{
+				_fnSortingClasses( oSettings );
+			}
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				var sTitle = aoColumns[i].sTitle.replace( /<.*?>/g, "" );
+				nTh = aoColumns[i].nTh;
+				nTh.removeAttribute('aria-sort');
+				nTh.removeAttribute('aria-label');
+				
+				/* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
+				if ( aoColumns[i].bSortable )
+				{
+					if ( aaSort.length > 0 && aaSort[0][0] == i )
+					{
+						nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
+						
+						var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? 
+							aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
+						nTh.setAttribute('aria-label', sTitle+
+							(nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+					else
+					{
+						nTh.setAttribute('aria-label', sTitle+
+							(aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
+					}
+				}
+				else
+				{
+					nTh.setAttribute('aria-label', sTitle);
+				}
+			}
+			
+			/* Tell the draw function that we have sorted the data */
+			oSettings.bSorted = true;
+			$(oSettings.oInstance).trigger('sort', oSettings);
+			
+			/* Copy the master data into the draw array and re-draw */
+			if ( oSettings.oFeatures.bFilter )
+			{
+				/* _fnFilter() will redraw the table for us */
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+			else
+			{
+				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+		}
+		
+		
+		/**
+		 * Attach a sort handler (click) to a node
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {node} nNode node to attach the handler to
+		 *  @param {int} iDataIndex column sorting index
+		 *  @param {function} [fnCallback] callback function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
+		{
+			_fnBindAction( nNode, {}, function (e) {
+				/* If the column is not sortable - don't to anything */
+				if ( oSettings.aoColumns[iDataIndex].bSortable === false )
+				{
+					return;
+				}
+				
+				/*
+				 * This is a little bit odd I admit... I declare a temporary function inside the scope of
+				 * _fnBuildHead and the click handler in order that the code presented here can be used 
+				 * twice - once for when bProcessing is enabled, and another time for when it is 
+				 * disabled, as we need to perform slightly different actions.
+				 *   Basically the issue here is that the Javascript engine in modern browsers don't 
+				 * appear to allow the rendering engine to update the display while it is still executing
+				 * it's thread (well - it does but only after long intervals). This means that the 
+				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
+				 * I force an execution break by using setTimeout - but this breaks the expected 
+				 * thread continuation for the end-developer's point of view (their code would execute
+				 * too early), so we only do it when we absolutely have to.
+				 */
+				var fnInnerSorting = function () {
+					var iColumn, iNextSort;
+					
+					/* If the shift key is pressed then we are multiple column sorting */
+					if ( e.shiftKey )
+					{
+						/* Are we already doing some kind of sort on this column? */
+						var bFound = false;
+						for ( var i=0 ; i<oSettings.aaSorting.length ; i++ )
+						{
+							if ( oSettings.aaSorting[i][0] == iDataIndex )
+							{
+								bFound = true;
+								iColumn = oSettings.aaSorting[i][0];
+								iNextSort = oSettings.aaSorting[i][2]+1;
+								
+								if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+								{
+									/* Reached the end of the sorting options, remove from multi-col sort */
+									oSettings.aaSorting.splice( i, 1 );
+								}
+								else
+								{
+									/* Move onto next sorting direction */
+									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+									oSettings.aaSorting[i][2] = iNextSort;
+								}
+								break;
+							}
+						}
+						
+						/* No sort yet - add it in */
+						if ( bFound === false )
+						{
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					else
+					{
+						/* If no shift key then single column sort */
+						if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex )
+						{
+							iColumn = oSettings.aaSorting[0][0];
+							iNextSort = oSettings.aaSorting[0][2]+1;
+							if ( !oSettings.aoColumns[iColumn].asSorting[iNextSort] )
+							{
+								iNextSort = 0;
+							}
+							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
+							oSettings.aaSorting[0][2] = iNextSort;
+						}
+						else
+						{
+							oSettings.aaSorting.splice( 0, oSettings.aaSorting.length );
+							oSettings.aaSorting.push( [ iDataIndex, 
+								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );
+						}
+					}
+					
+					/* Run the sort */
+					_fnSort( oSettings );
+				}; /* /fnInnerSorting */
+				
+				if ( !oSettings.oFeatures.bProcessing )
+				{
+					fnInnerSorting();
+				}
+				else
+				{
+					_fnProcessingDisplay( oSettings, true );
+					setTimeout( function() {
+						fnInnerSorting();
+						if ( !oSettings.oFeatures.bServerSide )
+						{
+							_fnProcessingDisplay( oSettings, false );
+						}
+					}, 0 );
+				}
+				
+				/* Call the user specified callback function - used for async user interaction */
+				if ( typeof fnCallback == 'function' )
+				{
+					fnCallback( oSettings );
+				}
+			} );
+		}
+		
+		
+		/**
+		 * Set the sorting classes on the header, Note: it is safe to call this function 
+		 * when bSort and bSortClasses are false
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSortingClasses( oSettings )
+		{
+			var i, iLen, j, jLen, iFound;
+			var aaSort, sClass;
+			var iColumns = oSettings.aoColumns.length;
+			var oClasses = oSettings.oClasses;
+			
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc +
+						" "+ oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			if ( oSettings.aaSortingFixed !== null )
+			{
+				aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );
+			}
+			else
+			{
+				aaSort = oSettings.aaSorting.slice();
+			}
+			
+			/* Apply the required classes to the header */
+			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bSortable )
+				{
+					sClass = oSettings.aoColumns[i].sSortingClass;
+					iFound = -1;
+					for ( j=0 ; j<aaSort.length ; j++ )
+					{
+						if ( aaSort[j][0] == i )
+						{
+							sClass = ( aaSort[j][1] == "asc" ) ?
+								oClasses.sSortAsc : oClasses.sSortDesc;
+							iFound = j;
+							break;
+						}
+					}
+					$(oSettings.aoColumns[i].nTh).addClass( sClass );
+					
+					if ( oSettings.bJUI )
+					{
+						/* jQuery UI uses extra markup */
+						var jqSpan = $("span."+oClasses.sSortIcon,  oSettings.aoColumns[i].nTh);
+						jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ 
+							oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed );
+						
+						var sSpanClass;
+						if ( iFound == -1 )
+						{
+						 	sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;
+						}
+						else if ( aaSort[iFound][1] == "asc" )
+						{
+							sSpanClass = oClasses.sSortJUIAsc;
+						}
+						else
+						{
+							sSpanClass = oClasses.sSortJUIDesc;
+						}
+						
+						jqSpan.addClass( sSpanClass );
+					}
+				}
+				else
+				{
+					/* No sorting on this column, so add the base class. This will have been assigned by
+					 * _fnAddColumn
+					 */
+					$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass );
+				}
+			}
+			
+			/* 
+			 * Apply the required classes to the table body
+			 * Note that this is given as a feature switch since it can significantly slow down a sort
+			 * on large data sets (adding and removing of classes is always slow at the best of times..)
+			 * Further to this, note that this code is admittedly fairly ugly. It could be made a lot 
+			 * simpler using jQuery selectors and add/removeClass, but that is significantly slower
+			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
+			 * Note that for deferred drawing we do use jQuery - the reason being that taking the first
+			 * row found to see if the whole column needs processed can miss classes since the first
+			 * column might be new.
+			 */
+			sClass = oClasses.sSortColumn;
+			
+			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )
+			{
+				var nTds = _fnGetTdNodes( oSettings );
+				
+				/* Determine what the sorting class for each column should be */
+				var iClass, iTargetCol;
+				var asClasses = [];
+				for (i = 0; i < iColumns; i++)
+				{
+					asClasses.push("");
+				}
+				for (i = 0, iClass = 1; i < aaSort.length; i++)
+				{
+					iTargetCol = parseInt( aaSort[i][0], 10 );
+					asClasses[iTargetCol] = sClass + iClass;
+					
+					if ( iClass < 3 )
+					{
+						iClass++;
+					}
+				}
+				
+				/* Make changes to the classes for each cell as needed */
+				var reClass = new RegExp(sClass + "[123]");
+				var sTmpClass, sCurrentClass, sNewClass;
+				for ( i=0, iLen=nTds.length; i<iLen; i++ )
+				{
+					/* Determine which column we're looking at */
+					iTargetCol = i % iColumns;
+					
+					/* What is the full list of classes now */
+					sCurrentClass = nTds[i].className;
+					/* What sorting class should be applied? */
+					sNewClass = asClasses[iTargetCol];
+					/* What would the new full list be if we did a replacement? */
+					sTmpClass = sCurrentClass.replace(reClass, sNewClass);
+					
+					if ( sTmpClass != sCurrentClass )
+					{
+						/* We changed something */
+						nTds[i].className = $.trim( sTmpClass );
+					}
+					else if ( sNewClass.length > 0 && sCurrentClass.indexOf(sNewClass) == -1 )
+					{
+						/* We need to add a class */
+						nTds[i].className = sCurrentClass + " " + sNewClass;
+					}
+				}
+			}
+		}
+		
+		
+		
+		/**
+		 * Save the state of a table in a cookie such that the page can be reloaded
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSaveState ( oSettings )
+		{
+			if ( !oSettings.oFeatures.bStateSave || oSettings.bDestroying )
+			{
+				return;
+			}
+		
+			/* Store the interesting variables */
+			var i, iLen, bInfinite=oSettings.oScroll.bInfinite;
+			var oState = {
+				"iCreate":      new Date().getTime(),
+				"iStart":       (bInfinite ? 0 : oSettings._iDisplayStart),
+				"iEnd":         (bInfinite ? oSettings._iDisplayLength : oSettings._iDisplayEnd),
+				"iLength":      oSettings._iDisplayLength,
+				"aaSorting":    $.extend( true, [], oSettings.aaSorting ),
+				"oSearch":      $.extend( true, {}, oSettings.oPreviousSearch ),
+				"aoSearchCols": $.extend( true, [], oSettings.aoPreSearchCols ),
+				"abVisCols":    []
+			};
+		
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				oState.abVisCols.push( oSettings.aoColumns[i].bVisible );
+			}
+		
+			_fnCallbackFire( oSettings, "aoStateSaveParams", 'stateSaveParams', [oSettings, oState] );
+			
+			oSettings.fnStateSave.call( oSettings.oInstance, oSettings, oState );
+		}
+		
+		
+		/**
+		 * Attempt to load a saved table state from a cookie
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {object} oInit DataTables init object so we can override settings
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLoadState ( oSettings, oInit )
+		{
+			if ( !oSettings.oFeatures.bStateSave )
+			{
+				return;
+			}
+		
+			var oData = oSettings.fnStateLoad.call( oSettings.oInstance, oSettings );
+			if ( !oData )
+			{
+				return;
+			}
+			
+			/* Allow custom and plug-in manipulation functions to alter the saved data set and
+			 * cancelling of loading by returning false
+			 */
+			var abStateLoad = _fnCallbackFire( oSettings, 'aoStateLoadParams', 'stateLoadParams', [oSettings, oData] );
+			if ( $.inArray( false, abStateLoad ) !== -1 )
+			{
+				return;
+			}
+			
+			/* Store the saved state so it might be accessed at any time */
+			oSettings.oLoadedState = $.extend( true, {}, oData );
+			
+			/* Restore key features */
+			oSettings._iDisplayStart    = oData.iStart;
+			oSettings.iInitDisplayStart = oData.iStart;
+			oSettings._iDisplayEnd      = oData.iEnd;
+			oSettings._iDisplayLength   = oData.iLength;
+			oSettings.aaSorting         = oData.aaSorting.slice();
+			oSettings.saved_aaSorting   = oData.aaSorting.slice();
+			
+			/* Search filtering  */
+			$.extend( oSettings.oPreviousSearch, oData.oSearch );
+			$.extend( true, oSettings.aoPreSearchCols, oData.aoSearchCols );
+			
+			/* Column visibility state
+			 * Pass back visibility settings to the init handler, but to do not here override
+			 * the init object that the user might have passed in
+			 */
+			oInit.saved_aoColumns = [];
+			for ( var i=0 ; i<oData.abVisCols.length ; i++ )
+			{
+				oInit.saved_aoColumns[i] = {};
+				oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
+			}
+		
+			_fnCallbackFire( oSettings, 'aoStateLoaded', 'stateLoaded', [oSettings, oData] );
+		}
+		
+		
+		/**
+		 * Create a new cookie with a value to store the state of a table
+		 *  @param {string} sName name of the cookie to create
+		 *  @param {string} sValue the value the cookie should take
+		 *  @param {int} iSecs duration of the cookie
+		 *  @param {string} sBaseName sName is made up of the base + file name - this is the base
+		 *  @param {function} fnCallback User definable function to modify the cookie
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )
+		{
+			var date = new Date();
+			date.setTime( date.getTime()+(iSecs*1000) );
+			
+			/* 
+			 * Shocking but true - it would appear IE has major issues with having the path not having
+			 * a trailing slash on it. We need the cookie to be available based on the path, so we
+			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the
+			 * patch to use at least some of the path
+			 */
+			var aParts = window.location.pathname.split('/');
+			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g,"").toLowerCase();
+			var sFullCookie, oData;
+			
+			if ( fnCallback !== null )
+			{
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON( sValue ) : eval( '('+sValue+')' );
+				sFullCookie = fnCallback( sNameFile, oData, date.toGMTString(),
+					aParts.join('/')+"/" );
+			}
+			else
+			{
+				sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +
+					"; expires=" + date.toGMTString() +"; path=" + aParts.join('/')+"/";
+			}
+			
+			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies
+			 * belonging to DataTables.
+			 */
+			var
+				aCookies =document.cookie.split(';'),
+				iNewCookieLen = sFullCookie.split(';')[0].length,
+				aOldCookies = [];
+			
+			if ( iNewCookieLen+document.cookie.length+10 > 4096 ) /* Magic 10 for padding */
+			{
+				for ( var i=0, iLen=aCookies.length ; i<iLen ; i++ )
+				{
+					if ( aCookies[i].indexOf( sBaseName ) != -1 )
+					{
+						/* It's a DataTables cookie, so eval it and check the time stamp */
+						var aSplitCookie = aCookies[i].split('=');
+						try {
+							oData = eval( '('+decodeURIComponent(aSplitCookie[1])+')' );
+		
+							if ( oData && oData.iCreate )
+							{
+								aOldCookies.push( {
+									"name": aSplitCookie[0],
+									"time": oData.iCreate
+								} );
+							}
+						}
+						catch( e ) {}
+					}
+				}
+		
+				// Make sure we delete the oldest ones first
+				aOldCookies.sort( function (a, b) {
+					return b.time - a.time;
+				} );
+		
+				// Eliminate as many old DataTables cookies as we need to
+				while ( iNewCookieLen + document.cookie.length + 10 > 4096 ) {
+					if ( aOldCookies.length === 0 ) {
+						// Deleted all DT cookies and still not enough space. Can't state save
+						return;
+					}
+					
+					var old = aOldCookies.pop();
+					document.cookie = old.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+
+						aParts.join('/') + "/";
+				}
+			}
+			
+			document.cookie = sFullCookie;
+		}
+		
+		
+		/**
+		 * Read an old cookie to get a cookie with an old table state
+		 *  @param {string} sName name of the cookie to read
+		 *  @returns {string} contents of the cookie - or null if no cookie with that name found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnReadCookie ( sName )
+		{
+			var
+				aParts = window.location.pathname.split('/'),
+				sNameEQ = sName + '_' + aParts[aParts.length-1].replace(/[\/:]/g,"").toLowerCase() + '=',
+			 	sCookieContents = document.cookie.split(';');
+			
+			for( var i=0 ; i<sCookieContents.length ; i++ )
+			{
+				var c = sCookieContents[i];
+				
+				while (c.charAt(0)==' ')
+				{
+					c = c.substring(1,c.length);
+				}
+				
+				if (c.indexOf(sNameEQ) === 0)
+				{
+					return decodeURIComponent( c.substring(sNameEQ.length,c.length) );
+				}
+			}
+			return null;
+		}
+		
+		
+		/**
+		 * Return the settings object for a particular table
+		 *  @param {node} nTable table we are using as a dataTable
+		 *  @returns {object} Settings object - or null if not found
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnSettingsFromNode ( nTable )
+		{
+			for ( var i=0 ; i<DataTable.settings.length ; i++ )
+			{
+				if ( DataTable.settings[i].nTable === nTable )
+				{
+					return DataTable.settings[i];
+				}
+			}
+			
+			return null;
+		}
+		
+		
+		/**
+		 * Return an array with the TR nodes for the table
+		 *  @param {object} oSettings dataTables settings object
+		 *  @returns {array} TR array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTrNodes ( oSettings )
+		{
+			var aNodes = [];
+			var aoData = oSettings.aoData;
+			for ( var i=0, iLen=aoData.length ; i<iLen ; i++ )
+			{
+				if ( aoData[i].nTr !== null )
+				{
+					aNodes.push( aoData[i].nTr );
+				}
+			}
+			return aNodes;
+		}
+		
+		
+		/**
+		 * Return an flat array with all TD nodes for the table, or row
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} [iIndividualRow] aoData index to get the nodes for - optional 
+		 *    if not given then the return array will contain all nodes for the table
+		 *  @returns {array} TD array
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnGetTdNodes ( oSettings, iIndividualRow )
+		{
+			var anReturn = [];
+			var iCorrector;
+			var anTds, nTd;
+			var iRow, iRows=oSettings.aoData.length,
+				iColumn, iColumns, oData, sNodeName, iStart=0, iEnd=iRows;
+			
+			/* Allow the collection to be limited to just one row */
+			if ( iIndividualRow !== undefined )
+			{
+				iStart = iIndividualRow;
+				iEnd = iIndividualRow+1;
+			}
+		
+			for ( iRow=iStart ; iRow<iEnd ; iRow++ )
+			{
+				oData = oSettings.aoData[iRow];
+				if ( oData.nTr !== null )
+				{
+					/* get the TD child nodes - taking into account text etc nodes */
+					anTds = [];
+					nTd = oData.nTr.firstChild;
+					while ( nTd )
+					{
+						sNodeName = nTd.nodeName.toLowerCase();
+						if ( sNodeName == 'td' || sNodeName == 'th' )
+						{
+							anTds.push( nTd );
+						}
+						nTd = nTd.nextSibling;
+					}
+		
+					iCorrector = 0;
+					for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+					{
+						if ( oSettings.aoColumns[iColumn].bVisible )
+						{
+							anReturn.push( anTds[iColumn-iCorrector] );
+						}
+						else
+						{
+							anReturn.push( oData._anHidden[iColumn] );
+							iCorrector++;
+						}
+					}
+				}
+			}
+		
+			return anReturn;
+		}
+		
+		
+		/**
+		 * Log an error message
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {int} iLevel log error messages, or display them to the user
+		 *  @param {string} sMesg error message
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnLog( oSettings, iLevel, sMesg )
+		{
+			var sAlert = (oSettings===null) ?
+				"DataTables warning: "+sMesg :
+				"DataTables warning (table id = '"+oSettings.sTableId+"'): "+sMesg;
+			
+			if ( iLevel === 0 )
+			{
+				if ( DataTable.ext.sErrMode == 'alert' )
+				{
+					alert( sAlert );
+				}
+				else
+				{
+					throw new Error(sAlert);
+				}
+				return;
+			}
+			else if ( window.console && console.log )
+			{
+				console.log( sAlert );
+			}
+		}
+		
+		
+		/**
+		 * See if a property is defined on one object, if so assign it to the other object
+		 *  @param {object} oRet target object
+		 *  @param {object} oSrc source object
+		 *  @param {string} sName property
+		 *  @param {string} [sMappedName] name to map too - optional, sName used if not given
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnMap( oRet, oSrc, sName, sMappedName )
+		{
+			if ( sMappedName === undefined )
+			{
+				sMappedName = sName;
+			}
+			if ( oSrc[sName] !== undefined )
+			{
+				oRet[sMappedName] = oSrc[sName];
+			}
+		}
+		
+		
+		/**
+		 * Extend objects - very similar to jQuery.extend, but deep copy objects, and shallow
+		 * copy arrays. The reason we need to do this, is that we don't want to deep copy array
+		 * init values (such as aaSorting) since the dev wouldn't be able to override them, but
+		 * we do want to deep copy arrays.
+		 *  @param {object} oOut Object to extend
+		 *  @param {object} oExtender Object from which the properties will be applied to oOut
+		 *  @returns {object} oOut Reference, just for convenience - oOut === the return.
+		 *  @memberof DataTable#oApi
+		 *  @todo This doesn't take account of arrays inside the deep copied objects.
+		 */
+		function _fnExtend( oOut, oExtender )
+		{
+			var val;
+			
+			for ( var prop in oExtender )
+			{
+				if ( oExtender.hasOwnProperty(prop) )
+				{
+					val = oExtender[prop];
+		
+					if ( typeof oInit[prop] === 'object' && val !== null && $.isArray(val) === false )
+					{
+						$.extend( true, oOut[prop], val );
+					}
+					else
+					{
+						oOut[prop] = val;
+					}
+				}
+			}
+		
+			return oOut;
+		}
+		
+		
+		/**
+		 * Bind an event handers to allow a click or return key to activate the callback.
+		 * This is good for accessibility since a return on the keyboard will have the
+		 * same effect as a click, if the element has focus.
+		 *  @param {element} n Element to bind the action to
+		 *  @param {object} oData Data object to pass to the triggered function
+		 *  @param {function} fn Callback function for when the event is triggered
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBindAction( n, oData, fn )
+		{
+			$(n)
+				.bind( 'click.DT', oData, function (e) {
+						n.blur(); // Remove focus outline for mouse users
+						fn(e);
+					} )
+				.bind( 'keypress.DT', oData, function (e){
+					if ( e.which === 13 ) {
+						fn(e);
+					} } )
+				.bind( 'selectstart.DT', function () {
+					/* Take the brutal approach to cancelling text selection */
+					return false;
+					} );
+		}
+		
+		
+		/**
+		 * Register a callback function. Easily allows a callback function to be added to
+		 * an array store of callback functions that can then all be called together.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {function} fn Function to be called back
+		 *  @param {string} sName Identifying name for the callback (i.e. a label)
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackReg( oSettings, sStore, fn, sName )
+		{
+			if ( fn )
+			{
+				oSettings[sStore].push( {
+					"fn": fn,
+					"sName": sName
+				} );
+			}
+		}
+		
+		
+		/**
+		 * Fire callback functions and trigger events. Note that the loop over the callback
+		 * array store is done backwards! Further note that you do not want to fire off triggers
+		 * in time sensitive applications (for example cell creation) as its slow.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @param {string} sStore Name of the array storage for the callbacks in oSettings
+		 *  @param {string} sTrigger Name of the jQuery custom event to trigger. If null no trigger
+		 *    is fired
+		 *  @param {array} aArgs Array of arguments to pass to the callback function / trigger
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnCallbackFire( oSettings, sStore, sTrigger, aArgs )
+		{
+			var aoStore = oSettings[sStore];
+			var aRet =[];
+		
+			for ( var i=aoStore.length-1 ; i>=0 ; i-- )
+			{
+				aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) );
+			}
+		
+			if ( sTrigger !== null )
+			{
+				$(oSettings.oInstance).trigger(sTrigger, aArgs);
+			}
+		
+			return aRet;
+		}
+		
+		
+		/**
+		 * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other
+		 * library, then we use that as it is fast, safe and accurate. If the function isn't 
+		 * available then we need to built it ourselves - the inspiration for this function comes
+		 * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is
+		 * not perfect and absolutely should not be used as a replacement to json2.js - but it does
+		 * do what we need, without requiring a dependency for DataTables.
+		 *  @param {object} o JSON object to be converted
+		 *  @returns {string} JSON string
+		 *  @memberof DataTable#oApi
+		 */
+		var _fnJsonString = (window.JSON) ? JSON.stringify : function( o )
+		{
+			/* Not an object or array */
+			var sType = typeof o;
+			if (sType !== "object" || o === null)
+			{
+				// simple data type
+				if (sType === "string")
+				{
+					o = '"'+o+'"';
+				}
+				return o+"";
+			}
+		
+			/* If object or array, need to recurse over it */
+			var
+				sProp, mValue,
+				json = [],
+				bArr = $.isArray(o);
+			
+			for (sProp in o)
+			{
+				mValue = o[sProp];
+				sType = typeof mValue;
+		
+				if (sType === "string")
+				{
+					mValue = '"'+mValue+'"';
+				}
+				else if (sType === "object" && mValue !== null)
+				{
+					mValue = _fnJsonString(mValue);
+				}
+		
+				json.push((bArr ? "" : '"'+sProp+'":') + mValue);
+			}
+		
+			return (bArr ? "[" : "{") + json + (bArr ? "]" : "}");
+		};
+		
+		
+		/**
+		 * From some browsers (specifically IE6/7) we need special handling to work around browser
+		 * bugs - this function is used to detect when these workarounds are needed.
+		 *  @param {object} oSettings dataTables settings object
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnBrowserDetect( oSettings )
+		{
+			/* IE6/7 will oversize a width 100% element inside a scrolling element, to include the
+			 * width of the scrollbar, while other browsers ensure the inner element is contained
+			 * without forcing scrolling
+			 */
+			var n = $(
+				'<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden">'+
+					'<div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;">'+
+						'<div id="DT_BrowserTest" style="width:100%; height:10px;"></div>'+
+					'</div>'+
+				'</div>')[0];
+		
+			document.body.appendChild( n );
+			oSettings.oBrowser.bScrollOversize = $('#DT_BrowserTest', n)[0].offsetWidth === 100 ? true : false;
+			document.body.removeChild( n );
+		}
+		
+
+		/**
+		 * Perform a jQuery selector action on the table's TR elements (from the tbody) and
+		 * return the resulting jQuery object.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
+		 *    criterion ("applied") or all TR elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {object} jQuery object, filtered by the given selector.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Highlight every second row
+		 *      oTable.$('tr:odd').css('backgroundColor', 'blue');
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to rows with 'Webkit' in them, add a background colour and then
+		 *      // remove the filter, thus highlighting the 'Webkit' rows only.
+		 *      oTable.fnFilter('Webkit');
+		 *      oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue');
+		 *      oTable.fnFilter('');
+		 *    } );
+		 */
+		this.$ = function ( sSelector, oOpts )
+		{
+			var i, iLen, a = [], tr;
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoData = oSettings.aoData;
+			var aiDisplay = oSettings.aiDisplay;
+			var aiDisplayMaster = oSettings.aiDisplayMaster;
+		
+			if ( !oOpts )
+			{
+				oOpts = {};
+			}
+		
+			oOpts = $.extend( {}, {
+				"filter": "none", // applied
+				"order": "current", // "original"
+				"page": "all" // current
+			}, oOpts );
+		
+			// Current page implies that order=current and fitler=applied, since it is fairly
+			// senseless otherwise
+			if ( oOpts.page == 'current' )
+			{
+				for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aiDisplayMaster.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplayMaster[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "current" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aiDisplay.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ aiDisplay[i] ].nTr;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "none" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr ;
+					if ( tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else if ( oOpts.order == "original" && oOpts.filter == "applied" )
+			{
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					tr = aoData[ i ].nTr;
+					if ( $.inArray( i, aiDisplay ) !== -1 && tr )
+					{
+						a.push( tr );
+					}
+				}
+			}
+			else
+			{
+				_fnLog( oSettings, 1, "Unknown selection options" );
+			}
+		
+			/* We need to filter on the TR elements and also 'find' in their descendants
+			 * to make the selector act like it would in a full table - so we need
+			 * to build both results and then combine them together
+			 */
+			var jqA = $(a);
+			var jqTRs = jqA.filter( sSelector );
+			var jqDescendants = jqA.find( sSelector );
+		
+			return $( [].concat($.makeArray(jqTRs), $.makeArray(jqDescendants)) );
+		};
+		
+		
+		/**
+		 * Almost identical to $ in operation, but in this case returns the data for the matched
+		 * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes
+		 * rather than any descendants, so the data can be obtained for the row/cell. If matching
+		 * rows are found, the data returned is the original data array/object that was used to  
+		 * create the row (or a generated array if from a DOM source).
+		 *
+		 * This method is often useful in-combination with $ where both functions are given the
+		 * same parameters and the array indexes will match identically.
+		 *  @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
+		 *  @param {object} [oOpts] Optional parameters for modifying the rows to be included
+		 *  @param {string} [oOpts.filter=none] Select elements that meet the current filter
+		 *    criterion ("applied") or all elements (i.e. no filter).
+		 *  @param {string} [oOpts.order=current] Order of the data in the processed array.
+		 *    Can be either 'current', whereby the current sorting of the table is used, or
+		 *    'original' whereby the original order the data was read into the table is used.
+		 *  @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
+		 *    ("current") or not ("all"). If 'current' is given, then order is assumed to be 
+		 *    'current' and filter is 'applied', regardless of what they might be given as.
+		 *  @returns {array} Data for the matched elements. If any elements, as a result of the
+		 *    selector, were not TR, TD or TH elements in the DataTable, they will have a null 
+		 *    entry in the array.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Get the data from the first row in the table
+		 *      var data = oTable._('tr:first');
+		 *
+		 *      // Do something useful with the data
+		 *      alert( "First cell is: "+data[0] );
+		 *    } );
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *
+		 *      // Filter to 'Webkit' and get all data for 
+		 *      oTable.fnFilter('Webkit');
+		 *      var data = oTable._('tr', {"filter": "applied"});
+		 *      
+		 *      // Do something with the data
+		 *      alert( data.length+" rows matched the filter" );
+		 *    } );
+		 */
+		this._ = function ( sSelector, oOpts )
+		{
+			var aOut = [];
+			var i, iLen, iIndex;
+			var aTrs = this.$( sSelector, oOpts );
+		
+			for ( i=0, iLen=aTrs.length ; i<iLen ; i++ )
+			{
+				aOut.push( this.fnGetData(aTrs[i]) );
+			}
+		
+			return aOut;
+		};
+		
+		
+		/**
+		 * Add a single new row or multiple rows of data to the table. Please note
+		 * that this is suitable for client-side processing only - if you are using 
+		 * server-side processing (i.e. "bServerSide": true), then to add data, you
+		 * must add it to the data source, i.e. the server-side, through an Ajax call.
+		 *  @param {array|object} mData The data to be added to the table. This can be:
+		 *    <ul>
+		 *      <li>1D array of data - add a single row with the data provided</li>
+		 *      <li>2D array of arrays - add multiple rows in a single call</li>
+		 *      <li>object - data object when using <i>mData</i></li>
+		 *      <li>array of objects - multiple data objects when using <i>mData</i></li>
+		 *    </ul>
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @returns {array} An array of integers, representing the list of indexes in 
+		 *    <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to 
+		 *    the table.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Global var for counter
+		 *    var giCount = 2;
+		 *    
+		 *    $(document).ready(function() {
+		 *      $('#example').dataTable();
+		 *    } );
+		 *    
+		 *    function fnClickAddRow() {
+		 *      $('#example').dataTable().fnAddData( [
+		 *        giCount+".1",
+		 *        giCount+".2",
+		 *        giCount+".3",
+		 *        giCount+".4" ]
+		 *      );
+		 *        
+		 *      giCount++;
+		 *    }
+		 */
+		this.fnAddData = function( mData, bRedraw )
+		{
+			if ( mData.length === 0 )
+			{
+				return [];
+			}
+			
+			var aiReturn = [];
+			var iTest;
+			
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			/* Check if we want to add multiple rows or not */
+			if ( typeof mData[0] === "object" && mData[0] !== null )
+			{
+				for ( var i=0 ; i<mData.length ; i++ )
+				{
+					iTest = _fnAddData( oSettings, mData[i] );
+					if ( iTest == -1 )
+					{
+						return aiReturn;
+					}
+					aiReturn.push( iTest );
+				}
+			}
+			else
+			{
+				iTest = _fnAddData( oSettings, mData );
+				if ( iTest == -1 )
+				{
+					return aiReturn;
+				}
+				aiReturn.push( iTest );
+			}
+			
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return aiReturn;
+		};
+		
+		
+		/**
+		 * This function will make DataTables recalculate the column sizes, based on the data 
+		 * contained in the table and the sizes applied to the columns (in the DOM, CSS or 
+		 * through the sWidth parameter). This can be useful when the width of the table's 
+		 * parent element changes (for example a window resize).
+		 *  @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      $(window).bind('resize', function () {
+		 *        oTable.fnAdjustColumnSizing();
+		 *      } );
+		 *    } );
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode(this[DataTable.ext.iApiIndex]);
+			_fnAdjustColumnSizing( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				this.fnDraw( false );
+			}
+			else if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				/* If not redrawing, but scrolling, we want to apply the new column sizes anyway */
+				this.oApi._fnScrollDraw(oSettings);
+			}
+		};
+		
+		
+		/**
+		 * Quickly and simply clear a table
+		 *  @param {bool} [bRedraw=true] redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...)
+		 *      oTable.fnClearTable();
+		 *    } );
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnClearTable( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * The exact opposite of 'opening' a row, this function will close any rows which 
+		 * are currently 'open'.
+		 *  @param {node} nTr the table row to 'close'
+		 *  @returns {int} 0 on success, or 1 if failed (can't find the row)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnClose = function( nTr )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
+					if ( nTrParent )
+					{
+						/* Remove it if it is currently on display */
+						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );
+					}
+					oSettings.aoOpenRows.splice( i, 1 );
+					return 0;
+				}
+			}
+			return 1;
+		};
+		
+		
+		/**
+		 * Remove a row for the table
+		 *  @param {mixed} mTarget The index of the row from aoData to be deleted, or
+		 *    the TR element you want to delete
+		 *  @param {function|null} [fnCallBack] Callback function
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @returns {array} The row that was deleted
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Immediately remove the first row
+		 *      oTable.fnDeleteRow( 0 );
+		 *    } );
+		 */
+		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, iAODataIndex;
+			
+			iAODataIndex = (typeof mTarget === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
+			
+			/* Return the data array from this row */
+			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
+		
+			/* Update the _DT_RowIndex parameter */
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					oSettings.aoData[i].nTr._DT_RowIndex = i;
+				}
+			}
+			
+			/* Remove the target row from the search array */
+			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );
+			oSettings.asDataSearch.splice( iDisplayIndex, 1 );
+			
+			/* Delete from the display arrays */
+			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );
+			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );
+			
+			/* If there is a user callback function - call it */
+			if ( typeof fnCallBack === "function" )
+			{
+				fnCallBack.call( this, oSettings, oData );
+			}
+			
+			/* Check for an 'overflow' they case for displaying the table */
+			if ( oSettings._iDisplayStart >= oSettings.fnRecordsDisplay() )
+			{
+				oSettings._iDisplayStart -= oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart < 0 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			return oData;
+		};
+		
+		
+		/**
+		 * Restore the table to it's original state in the DOM by removing all of DataTables 
+		 * enhancements, alterations to the DOM structure of the table and event listeners.
+		 *  @param {boolean} [bRemove=false] Completely remove the table from the DOM
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      // This example is fairly pointless in reality, but shows how fnDestroy can be used
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnDestroy();
+		 *    } );
+		 */
+		this.fnDestroy = function ( bRemove )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var nOrig = oSettings.nTableWrapper.parentNode;
+			var nBody = oSettings.nTBody;
+			var i, iLen;
+		
+			bRemove = (bRemove===undefined) ? false : bRemove;
+			
+			/* Flag to note that the table is currently being destroyed - no action should be taken */
+			oSettings.bDestroying = true;
+			
+			/* Fire off the destroy callbacks for plug-ins etc */
+			_fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] );
+		
+			/* If the table is not being removed, restore the hidden columns */
+			if ( !bRemove )
+			{
+				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible === false )
+					{
+						this.fnSetColumnVis( i, true );
+					}
+				}
+			}
+			
+			/* Blitz all DT events */
+			$(oSettings.nTableWrapper).find('*').andSelf().unbind('.DT');
+			
+			/* If there is an 'empty' indicator row, remove it */
+			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
+			
+			/* When scrolling we had to break the table up - restore it */
+			if ( oSettings.nTable != oSettings.nTHead.parentNode )
+			{
+				$(oSettings.nTable).children('thead').remove();
+				oSettings.nTable.appendChild( oSettings.nTHead );
+			}
+			
+			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
+			{
+				$(oSettings.nTable).children('tfoot').remove();
+				oSettings.nTable.appendChild( oSettings.nTFoot );
+			}
+			
+			/* Remove the DataTables generated nodes, events and classes */
+			oSettings.nTable.parentNode.removeChild( oSettings.nTable );
+			$(oSettings.nTableWrapper).remove();
+			
+			oSettings.aaSorting = [];
+			oSettings.aaSortingFixed = [];
+			_fnSortingClasses( oSettings );
+			
+			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') );
+			
+			$('th, td', oSettings.nTHead).removeClass( [
+				oSettings.oClasses.sSortable,
+				oSettings.oClasses.sSortableAsc,
+				oSettings.oClasses.sSortableDesc,
+				oSettings.oClasses.sSortableNone ].join(' ')
+			);
+			if ( oSettings.bJUI )
+			{
+				$('th span.'+oSettings.oClasses.sSortIcon
+					+ ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove();
+		
+				$('th, td', oSettings.nTHead).each( function () {
+					var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this);
+					var kids = jqWrapper.contents();
+					$(this).append( kids );
+					jqWrapper.remove();
+				} );
+			}
+			
+			/* Add the TR elements back into the table in their original order */
+			if ( !bRemove && oSettings.nTableReinsertBefore )
+			{
+				nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore );
+			}
+			else if ( !bRemove )
+			{
+				nOrig.appendChild( oSettings.nTable );
+			}
+		
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoData[i].nTr !== null )
+				{
+					nBody.appendChild( oSettings.aoData[i].nTr );
+				}
+			}
+			
+			/* Restore the width of the original table */
+			if ( oSettings.oFeatures.bAutoWidth === true )
+			{
+			  oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
+			}
+			
+			/* If the were originally stripe classes - then we add them back here. Note
+			 * this is not fool proof (for example if not all rows had stripe classes - but
+			 * it's a good effort without getting carried away
+			 */
+			iLen = oSettings.asDestroyStripes.length;
+			if (iLen)
+			{
+				var anRows = $(nBody).children('tr');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					anRows.filter(':nth-child(' + iLen + 'n + ' + i + ')').addClass( oSettings.asDestroyStripes[i] );
+				}
+			}
+			
+			/* Remove the settings object from the settings array */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				if ( DataTable.settings[i] == oSettings )
+				{
+					DataTable.settings.splice( i, 1 );
+				}
+			}
+			
+			/* End it all */
+			oSettings = null;
+			oInit = null;
+		};
+		
+		
+		/**
+		 * Redraw the table
+		 *  @param {bool} [bComplete=true] Re-filter and resort (if enabled) the table before the draw.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Re-draw the table - you wouldn't want to do it here, but it's an example :-)
+		 *      oTable.fnDraw();
+		 *    } );
+		 */
+		this.fnDraw = function( bComplete )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			if ( bComplete === false )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			else
+			{
+				_fnReDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Filter the input based on data
+		 *  @param {string} sInput String to filter the table on
+		 *  @param {int|null} [iColumn] Column to limit filtering to
+		 *  @param {bool} [bRegex=false] Treat as regular expression or not
+		 *  @param {bool} [bSmart=true] Perform smart filtering or not
+		 *  @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es)
+		 *  @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false)
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sometime later - filter...
+		 *      oTable.fnFilter( 'test string' );
+		 *    } );
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( !oSettings.oFeatures.bFilter )
+			{
+				return;
+			}
+			
+			if ( bRegex === undefined || bRegex === null )
+			{
+				bRegex = false;
+			}
+			
+			if ( bSmart === undefined || bSmart === null )
+			{
+				bSmart = true;
+			}
+			
+			if ( bShowGlobal === undefined || bShowGlobal === null )
+			{
+				bShowGlobal = true;
+			}
+			
+			if ( bCaseInsensitive === undefined || bCaseInsensitive === null )
+			{
+				bCaseInsensitive = true;
+			}
+			
+			if ( iColumn === undefined || iColumn === null )
+			{
+				/* Global filter */
+				_fnFilterComplete( oSettings, {
+					"sSearch":sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				}, 1 );
+				
+				if ( bShowGlobal && oSettings.aanFeatures.f )
+				{
+					var n = oSettings.aanFeatures.f;
+					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+					{
+						// IE9 throws an 'unknown error' if document.activeElement is used
+						// inside an iframe or frame...
+						try {
+							if ( n[i]._DT_Input != document.activeElement )
+							{
+								$(n[i]._DT_Input).val( sInput );
+							}
+						}
+						catch ( e ) {
+							$(n[i]._DT_Input).val( sInput );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Single column filter */
+				$.extend( oSettings.aoPreSearchCols[ iColumn ], {
+					"sSearch": sInput+"",
+					"bRegex": bRegex,
+					"bSmart": bSmart,
+					"bCaseInsensitive": bCaseInsensitive
+				} );
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+		};
+		
+		
+		/**
+		 * Get the data for the whole table, an individual row or an individual cell based on the 
+		 * provided parameters.
+		 *  @param {int|node} [mRow] A TR row node, TD/TH cell node or an integer. If given as
+		 *    a TR node then the data source for the whole row will be returned. If given as a
+		 *    TD/TH cell node then iCol will be automatically calculated and the data for the
+		 *    cell returned. If given as an integer, then this is treated as the aoData internal
+		 *    data index for the row (see fnGetPosition) and the data for that row used.
+		 *  @param {int} [iCol] Optional column index that you want the data of.
+		 *  @returns {array|object|string} If mRow is undefined, then the data for all rows is
+		 *    returned. If mRow is defined, just data for that row, and is iCol is
+		 *    defined, only data for the designated cell is returned.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    // Row data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('tr').click( function () {
+		 *        var data = oTable.fnGetData( this );
+		 *        // ... do something with the array / object of data for the row
+		 *      } );
+		 *    } );
+		 *
+		 *  @example
+		 *    // Individual cell data
+		 *    $(document).ready(function() {
+		 *      oTable = $('#example').dataTable();
+		 *
+		 *      oTable.$('td').click( function () {
+		 *        var sData = oTable.fnGetData( this );
+		 *        alert( 'The cell clicked on had the value of '+sData );
+		 *      } );
+		 *    } );
+		 */
+		this.fnGetData = function( mRow, iCol )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( mRow !== undefined )
+			{
+				var iRow = mRow;
+				if ( typeof mRow === 'object' )
+				{
+					var sNode = mRow.nodeName.toLowerCase();
+					if (sNode === "tr" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow);
+					}
+					else if ( sNode === "td" )
+					{
+						iRow = _fnNodeToDataIndex(oSettings, mRow.parentNode);
+						iCol = _fnNodeToColumnIndex( oSettings, iRow, mRow );
+					}
+				}
+		
+				if ( iCol !== undefined )
+				{
+					return _fnGetCellData( oSettings, iRow, iCol, '' );
+				}
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow]._aData : null;
+			}
+			return _fnGetDataMaster( oSettings );
+		};
+		
+		
+		/**
+		 * Get an array of the TR nodes that are used in the table's body. Note that you will 
+		 * typically want to use the '$' API method in preference to this as it is more 
+		 * flexible.
+		 *  @param {int} [iRow] Optional row index for the TR element you want
+		 *  @returns {array|node} If iRow is undefined, returns an array of all TR elements
+		 *    in the table's body, or iRow is defined, just the TR element requested.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Get the nodes from the table
+		 *      var nNodes = oTable.fnGetNodes( );
+		 *    } );
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			
+			if ( iRow !== undefined ) {
+				return (oSettings.aoData[iRow]!==undefined) ?
+					oSettings.aoData[iRow].nTr : null;
+			}
+			return _fnGetTrNodes( oSettings );
+		};
+		
+		
+		/**
+		 * Get the array indexes of a particular cell from it's DOM element
+		 * and column index including hidden columns
+		 *  @param {node} nNode this can either be a TR, TD or TH in the table's body
+		 *  @returns {int} If nNode is given as a TR, then a single index is returned, or
+		 *    if given as a cell, an array of [row index, column index (visible), 
+		 *    column index (all)] is given.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      $('#example tbody td').click( function () {
+		 *        // Get the position of the current data from the node
+		 *        var aPos = oTable.fnGetPosition( this );
+		 *        
+		 *        // Get the data array for this row
+		 *        var aData = oTable.fnGetData( aPos[0] );
+		 *        
+		 *        // Update the data array and return the value
+		 *        aData[ aPos[1] ] = 'clicked';
+		 *        this.innerHTML = 'clicked';
+		 *      } );
+		 *      
+		 *      // Init DataTables
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnGetPosition = function( nNode )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var sNodeName = nNode.nodeName.toUpperCase();
+			
+			if ( sNodeName == "TR" )
+			{
+				return _fnNodeToDataIndex(oSettings, nNode);
+			}
+			else if ( sNodeName == "TD" || sNodeName == "TH" )
+			{
+				var iDataIndex = _fnNodeToDataIndex( oSettings, nNode.parentNode );
+				var iColumnIndex = _fnNodeToColumnIndex( oSettings, iDataIndex, nNode );
+				return [ iDataIndex, _fnColumnIndexToVisible(oSettings, iColumnIndex ), iColumnIndex ];
+			}
+			return null;
+		};
+		
+		
+		/**
+		 * Check to see if a row is 'open' or not.
+		 *  @param {node} nTr the table row to check
+		 *  @returns {boolean} true if the row is currently open, false otherwise
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnIsOpen = function( nTr )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var aoOpenRows = oSettings.aoOpenRows;
+			
+			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )
+			{
+				if ( oSettings.aoOpenRows[i].nParent == nTr )
+				{
+					return true;
+				}
+			}
+			return false;
+		};
+		
+		
+		/**
+		 * This function will place a new row directly after a row which is currently 
+		 * on display on the page, with the HTML contents that is passed into the 
+		 * function. This can be used, for example, to ask for confirmation that a 
+		 * particular record should be deleted.
+		 *  @param {node} nTr The table row to 'open'
+		 *  @param {string|node|jQuery} mHtml The HTML to put into the row
+		 *  @param {string} sClass Class to give the new TD cell
+		 *  @returns {node} The row opened. Note that if the table row passed in as the
+		 *    first parameter, is not found in the table, this method will silently
+		 *    return.
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable;
+		 *      
+		 *      // 'open' an information row when a row is clicked on
+		 *      $('#example tbody tr').click( function () {
+		 *        if ( oTable.fnIsOpen(this) ) {
+		 *          oTable.fnClose( this );
+		 *        } else {
+		 *          oTable.fnOpen( this, "Temporary row opened", "info_row" );
+		 *        }
+		 *      } );
+		 *      
+		 *      oTable = $('#example').dataTable();
+		 *    } );
+		 */
+		this.fnOpen = function( nTr, mHtml, sClass )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		
+			/* Check that the row given is in the table */
+			var nTableRows = _fnGetTrNodes( oSettings );
+			if ( $.inArray(nTr, nTableRows) === -1 )
+			{
+				return;
+			}
+			
+			/* the old open one if there is one */
+			this.fnClose( nTr );
+			
+			var nNewRow = document.createElement("tr");
+			var nNewCell = document.createElement("td");
+			nNewRow.appendChild( nNewCell );
+			nNewCell.className = sClass;
+			nNewCell.colSpan = _fnVisbleColumns( oSettings );
+		
+			if (typeof mHtml === "string")
+			{
+				nNewCell.innerHTML = mHtml;
+			}
+			else
+			{
+				$(nNewCell).html( mHtml );
+			}
+		
+			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
+			var nTrs = $('tr', oSettings.nTBody);
+			if ( $.inArray(nTr, nTrs) != -1  )
+			{
+				$(nNewRow).insertAfter(nTr);
+			}
+			
+			oSettings.aoOpenRows.push( {
+				"nTr": nNewRow,
+				"nParent": nTr
+			} );
+			
+			return nNewRow;
+		};
+		
+		
+		/**
+		 * Change the pagination - provides the internal logic for pagination in a simple API 
+		 * function. With this function you can have a DataTables table go to the next, 
+		 * previous, first or last pages.
+		 *  @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
+		 *    or page number to jump to (integer), note that page 0 is the first page.
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnPageChange( 'next' );
+		 *    } );
+		 */
+		this.fnPageChange = function ( mAction, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			_fnPageChange( oSettings, mAction );
+			_fnCalculateEnd( oSettings );
+			
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		
+		/**
+		 * Show a particular column
+		 *  @param {int} iCol The column whose display should be changed
+		 *  @param {bool} bShow Show (true) or hide (false) the column
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Hide the second column after initialisation
+		 *      oTable.fnSetColumnVis( 1, false );
+		 *    } );
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen;
+			var aoColumns = oSettings.aoColumns;
+			var aoData = oSettings.aoData;
+			var nTd, bAppend, iBefore;
+			
+			/* No point in doing anything if we are requesting what is already true */
+			if ( aoColumns[iCol].bVisible == bShow )
+			{
+				return;
+			}
+			
+			/* Show the column */
+			if ( bShow )
+			{
+				var iInsert = 0;
+				for ( i=0 ; i<iCol ; i++ )
+				{
+					if ( aoColumns[i].bVisible )
+					{
+						iInsert++;
+					}
+				}
+				
+				/* Need to decide if we should use appendChild or insertBefore */
+				bAppend = (iInsert >= _fnVisbleColumns( oSettings ));
+		
+				/* Which coloumn should we be inserting before? */
+				if ( !bAppend )
+				{
+					for ( i=iCol ; i<aoColumns.length ; i++ )
+					{
+						if ( aoColumns[i].bVisible )
+						{
+							iBefore = i;
+							break;
+						}
+					}
+				}
+		
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						if ( bAppend )
+						{
+							aoData[i].nTr.appendChild( 
+								aoData[i]._anHidden[iCol]
+							);
+						}
+						else
+						{
+							aoData[i].nTr.insertBefore(
+								aoData[i]._anHidden[iCol], 
+								_fnGetTdNodes( oSettings, i )[iBefore] );
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Remove a column from display */
+				for ( i=0, iLen=aoData.length ; i<iLen ; i++ )
+				{
+					if ( aoData[i].nTr !== null )
+					{
+						nTd = _fnGetTdNodes( oSettings, i )[iCol];
+						aoData[i]._anHidden[iCol] = nTd;
+						nTd.parentNode.removeChild( nTd );
+					}
+				}
+			}
+		
+			/* Clear to set the visible flag */
+			aoColumns[iCol].bVisible = bShow;
+		
+			/* Redraw the header and footer based on the new column visibility */
+			_fnDrawHead( oSettings, oSettings.aoHeader );
+			if ( oSettings.nTFoot )
+			{
+				_fnDrawHead( oSettings, oSettings.aoFooter );
+			}
+			
+			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
+			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )
+			{
+				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );
+			}
+			
+			/* Do a redraw incase anything depending on the table columns needs it 
+			 * (built-in: scrolling) 
+			 */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnAdjustColumnSizing( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			_fnSaveState( oSettings );
+		};
+		
+		
+		/**
+		 * Get the settings for a particular table for external manipulation
+		 *  @returns {object} DataTables settings object. See 
+		 *    {@link DataTable.models.oSettings}
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      var oSettings = oTable.fnSettings();
+		 *      
+		 *      // Show an example parameter from the settings
+		 *      alert( oSettings._iDisplayStart );
+		 *    } );
+		 */
+		this.fnSettings = function()
+		{
+			return _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+		};
+		
+		
+		/**
+		 * Sort the table by a particular column
+		 *  @param {int} iCol the data index to sort on. Note that this will not match the 
+		 *    'display index' if you have hidden data entries
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort immediately with columns 0 and 1
+		 *      oTable.fnSort( [ [0,'asc'], [1,'asc'] ] );
+		 *    } );
+		 */
+		this.fnSort = function( aaSort )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			oSettings.aaSorting = aaSort;
+			_fnSort( oSettings );
+		};
+		
+		
+		/**
+		 * Attach a sort listener to an element for a given column
+		 *  @param {node} nNode the element to attach the sort listener to
+		 *  @param {int} iColumn the column that a click on this node will sort on
+		 *  @param {function} [fnCallback] callback function when sort is run
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      
+		 *      // Sort on column 1, when 'sorter' is clicked on
+		 *      oTable.fnSortListener( document.getElementById('sorter'), 1 );
+		 *    } );
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			_fnSortAttachListener( _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ), nNode, iColumn,
+			 	fnCallback );
+		};
+		
+		
+		/**
+		 * Update a table cell or row - this method will accept either a single value to
+		 * update the cell with, an array of values with one element for each column or
+		 * an object in the same format as the original data source. The function is
+		 * self-referencing in order to make the multi column updates easier.
+		 *  @param {object|array|string} mData Data to update the cell/row with
+		 *  @param {node|int} mRow TR element you want to update or the aoData index
+		 *  @param {int} [iColumn] The column to update (not used of mData is an array or object)
+		 *  @param {bool} [bRedraw=true] Redraw the table or not
+		 *  @param {bool} [bAction=true] Perform pre-draw actions or not
+		 *  @returns {int} 0 on success, 1 on error
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell
+		 *      oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], 1, 0 ); // Row
+		 *    } );
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
+			var i, iLen, sDisplay;
+			var iRow = (typeof mRow === 'object') ? 
+				_fnNodeToDataIndex(oSettings, mRow) : mRow;
+			
+			if ( $.isArray(mData) && iColumn === undefined )
+			{
+				/* Array update - update the whole row */
+				oSettings.aoData[iRow]._aData = mData.slice();
+				
+				/* Flag to the function that we are recursing */
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else if ( $.isPlainObject(mData) && iColumn === undefined )
+			{
+				/* Object update - update the whole row - assume the developer gets the object right */
+				oSettings.aoData[iRow]._aData = $.extend( true, {}, mData );
+		
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					this.fnUpdate( _fnGetCellData( oSettings, iRow, i ), iRow, i, false, false );
+				}
+			}
+			else
+			{
+				/* Individual cell update */
+				_fnSetCellData( oSettings, iRow, iColumn, mData );
+				sDisplay = _fnGetCellData( oSettings, iRow, iColumn, 'display' );
+				
+				var oCol = oSettings.aoColumns[iColumn];
+				if ( oCol.fnRender !== null )
+				{
+					sDisplay = _fnRender( oSettings, iRow, iColumn );
+					if ( oCol.bUseRendered )
+					{
+						_fnSetCellData( oSettings, iRow, iColumn, sDisplay );
+					}
+				}
+				
+				if ( oSettings.aoData[iRow].nTr !== null )
+				{
+					/* Do the actual HTML update */
+					_fnGetTdNodes( oSettings, iRow )[iColumn].innerHTML = sDisplay;
+				}
+			}
+			
+			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw
+			 * will rebuild the search array - however, the redraw might be disabled by the user)
+			 */
+			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );
+			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow(
+				oSettings, 
+				_fnGetRowData( oSettings, iRow, 'filter', _fnGetColumns( oSettings, 'bSearchable' ) )
+			);
+			
+			/* Perform pre-draw actions */
+			if ( bAction === undefined || bAction )
+			{
+				_fnAdjustColumnSizing( oSettings );
+			}
+			
+			/* Redraw the table */
+			if ( bRedraw === undefined || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return 0;
+		};
+		
+		
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+		 * to ensure compatibility.
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+		 *    formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+		 *    version, or false if this version of DataTales is not suitable
+		 *  @method
+		 *  @dtopt API
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		this.fnVersionCheck = DataTable.ext.fnVersionCheck;
+		
+		
+		/*
+		 * This is really a good bit rubbish this method of exposing the internal methods
+		 * publicly... - To be fixed in 2.0 using methods on the prototype
+		 */
+		
+		
+		/**
+		 * Create a wrapper function for exporting an internal functions to an external API.
+		 *  @param {string} sFunc API function name
+		 *  @returns {function} wrapped function
+		 *  @memberof DataTable#oApi
+		 */
+		function _fnExternApiFunc (sFunc)
+		{
+			return function() {
+				var aArgs = [_fnSettingsFromNode(this[DataTable.ext.iApiIndex])].concat( 
+					Array.prototype.slice.call(arguments) );
+				return DataTable.ext.oApi[sFunc].apply( this, aArgs );
+			};
+		}
+		
+		
+		/**
+		 * Reference to internal functions for use by plug-in developers. Note that these
+		 * methods are references to internal functions and are considered to be private.
+		 * If you use these methods, be aware that they are liable to change between versions
+		 * (check the upgrade notes).
+		 *  @namespace
+		 */
+		this.oApi = {
+			"_fnExternApiFunc": _fnExternApiFunc,
+			"_fnInitialise": _fnInitialise,
+			"_fnInitComplete": _fnInitComplete,
+			"_fnLanguageCompat": _fnLanguageCompat,
+			"_fnAddColumn": _fnAddColumn,
+			"_fnColumnOptions": _fnColumnOptions,
+			"_fnAddData": _fnAddData,
+			"_fnCreateTr": _fnCreateTr,
+			"_fnGatherData": _fnGatherData,
+			"_fnBuildHead": _fnBuildHead,
+			"_fnDrawHead": _fnDrawHead,
+			"_fnDraw": _fnDraw,
+			"_fnReDraw": _fnReDraw,
+			"_fnAjaxUpdate": _fnAjaxUpdate,
+			"_fnAjaxParameters": _fnAjaxParameters,
+			"_fnAjaxUpdateDraw": _fnAjaxUpdateDraw,
+			"_fnServerParams": _fnServerParams,
+			"_fnAddOptionsHtml": _fnAddOptionsHtml,
+			"_fnFeatureHtmlTable": _fnFeatureHtmlTable,
+			"_fnScrollDraw": _fnScrollDraw,
+			"_fnAdjustColumnSizing": _fnAdjustColumnSizing,
+			"_fnFeatureHtmlFilter": _fnFeatureHtmlFilter,
+			"_fnFilterComplete": _fnFilterComplete,
+			"_fnFilterCustom": _fnFilterCustom,
+			"_fnFilterColumn": _fnFilterColumn,
+			"_fnFilter": _fnFilter,
+			"_fnBuildSearchArray": _fnBuildSearchArray,
+			"_fnBuildSearchRow": _fnBuildSearchRow,
+			"_fnFilterCreateSearch": _fnFilterCreateSearch,
+			"_fnDataToSearch": _fnDataToSearch,
+			"_fnSort": _fnSort,
+			"_fnSortAttachListener": _fnSortAttachListener,
+			"_fnSortingClasses": _fnSortingClasses,
+			"_fnFeatureHtmlPaginate": _fnFeatureHtmlPaginate,
+			"_fnPageChange": _fnPageChange,
+			"_fnFeatureHtmlInfo": _fnFeatureHtmlInfo,
+			"_fnUpdateInfo": _fnUpdateInfo,
+			"_fnFeatureHtmlLength": _fnFeatureHtmlLength,
+			"_fnFeatureHtmlProcessing": _fnFeatureHtmlProcessing,
+			"_fnProcessingDisplay": _fnProcessingDisplay,
+			"_fnVisibleToColumnIndex": _fnVisibleToColumnIndex,
+			"_fnColumnIndexToVisible": _fnColumnIndexToVisible,
+			"_fnNodeToDataIndex": _fnNodeToDataIndex,
+			"_fnVisbleColumns": _fnVisbleColumns,
+			"_fnCalculateEnd": _fnCalculateEnd,
+			"_fnConvertToWidth": _fnConvertToWidth,
+			"_fnCalculateColumnWidths": _fnCalculateColumnWidths,
+			"_fnScrollingWidthAdjust": _fnScrollingWidthAdjust,
+			"_fnGetWidestNode": _fnGetWidestNode,
+			"_fnGetMaxLenString": _fnGetMaxLenString,
+			"_fnStringToCss": _fnStringToCss,
+			"_fnDetectType": _fnDetectType,
+			"_fnSettingsFromNode": _fnSettingsFromNode,
+			"_fnGetDataMaster": _fnGetDataMaster,
+			"_fnGetTrNodes": _fnGetTrNodes,
+			"_fnGetTdNodes": _fnGetTdNodes,
+			"_fnEscapeRegex": _fnEscapeRegex,
+			"_fnDeleteIndex": _fnDeleteIndex,
+			"_fnReOrderIndex": _fnReOrderIndex,
+			"_fnColumnOrdering": _fnColumnOrdering,
+			"_fnLog": _fnLog,
+			"_fnClearTable": _fnClearTable,
+			"_fnSaveState": _fnSaveState,
+			"_fnLoadState": _fnLoadState,
+			"_fnCreateCookie": _fnCreateCookie,
+			"_fnReadCookie": _fnReadCookie,
+			"_fnDetectHeader": _fnDetectHeader,
+			"_fnGetUniqueThs": _fnGetUniqueThs,
+			"_fnScrollBarWidth": _fnScrollBarWidth,
+			"_fnApplyToChildren": _fnApplyToChildren,
+			"_fnMap": _fnMap,
+			"_fnGetRowData": _fnGetRowData,
+			"_fnGetCellData": _fnGetCellData,
+			"_fnSetCellData": _fnSetCellData,
+			"_fnGetObjectDataFn": _fnGetObjectDataFn,
+			"_fnSetObjectDataFn": _fnSetObjectDataFn,
+			"_fnApplyColumnDefs": _fnApplyColumnDefs,
+			"_fnBindAction": _fnBindAction,
+			"_fnExtend": _fnExtend,
+			"_fnCallbackReg": _fnCallbackReg,
+			"_fnCallbackFire": _fnCallbackFire,
+			"_fnJsonString": _fnJsonString,
+			"_fnRender": _fnRender,
+			"_fnNodeToColumnIndex": _fnNodeToColumnIndex,
+			"_fnInfoMacros": _fnInfoMacros,
+			"_fnBrowserDetect": _fnBrowserDetect,
+			"_fnGetColumns": _fnGetColumns
+		};
+		
+		$.extend( DataTable.ext.oApi, this.oApi );
+		
+		for ( var sFunc in DataTable.ext.oApi )
+		{
+			if ( sFunc )
+			{
+				this[sFunc] = _fnExternApiFunc(sFunc);
+			}
+		}
+		
+		
+		var _that = this;
+		this.each(function() {
+			var i=0, iLen, j, jLen, k, kLen;
+			var sId = this.getAttribute( 'id' );
+			var bInitHandedOff = false;
+			var bUsePassedData = false;
+			
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( null, 0, "Attempted to initialise DataTables on a node which is not a "+
+					"table: "+this.nodeName );
+				return;
+			}
+			
+			/* Check to see if we are re-initialising a table */
+			for ( i=0, iLen=DataTable.settings.length ; i<iLen ; i++ )
+			{
+				/* Base check on table node */
+				if ( DataTable.settings[i].nTable == this )
+				{
+					if ( oInit === undefined || oInit.bRetrieve )
+					{
+						return DataTable.settings[i].oInstance;
+					}
+					else if ( oInit.bDestroy )
+					{
+						DataTable.settings[i].oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( DataTable.settings[i], 0, "Cannot reinitialise DataTable.\n\n"+
+							"To retrieve the DataTables object for this table, pass no arguments or see "+
+							"the docs for bRetrieve and bDestroy" );
+						return;
+					}
+				}
+				
+				/* If the element we are initialising has the same ID as a table which was previously
+				 * initialised, but the table nodes don't match (from before) then we destroy the old
+				 * instance by simply deleting it. This is under the assumption that the table has been
+				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
+				 */
+				if ( DataTable.settings[i].sTableId == this.id )
+				{
+					DataTable.settings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Ensure the table has an ID - required for accessibility */
+			if ( sId === null || sId === "" )
+			{
+				sId = "DataTables_Table_"+(DataTable.ext._oExternConfig.iNextUnique++);
+				this.id = sId;
+			}
+			
+			/* Create the settings object for this table and set some of the default parameters */
+			var oSettings = $.extend( true, {}, DataTable.models.oSettings, {
+				"nTable":        this,
+				"oApi":          _that.oApi,
+				"oInit":         oInit,
+				"sDestroyWidth": $(this).width(),
+				"sInstance":     sId,
+				"sTableId":      sId
+			} );
+			DataTable.settings.push( oSettings );
+			
+			// Need to add the instance after the instance after the settings object has been added
+			// to the settings array, so we can self reference the table instance if more than one
+			oSettings.oInstance = (_that.length===1) ? _that : $(this).dataTable();
+			
+			/* Setting up the initialisation object */
+			if ( !oInit )
+			{
+				oInit = {};
+			}
+			
+			// Backwards compatibility, before we apply all the defaults
+			if ( oInit.oLanguage )
+			{
+				_fnLanguageCompat( oInit.oLanguage );
+			}
+			
+			oInit = _fnExtend( $.extend(true, {}, DataTable.defaults), oInit );
+			
+			// Map the initialisation options onto the settings object
+			_fnMap( oSettings.oFeatures, oInit, "bPaginate" );
+			_fnMap( oSettings.oFeatures, oInit, "bLengthChange" );
+			_fnMap( oSettings.oFeatures, oInit, "bFilter" );
+			_fnMap( oSettings.oFeatures, oInit, "bSort" );
+			_fnMap( oSettings.oFeatures, oInit, "bInfo" );
+			_fnMap( oSettings.oFeatures, oInit, "bProcessing" );
+			_fnMap( oSettings.oFeatures, oInit, "bAutoWidth" );
+			_fnMap( oSettings.oFeatures, oInit, "bSortClasses" );
+			_fnMap( oSettings.oFeatures, oInit, "bServerSide" );
+			_fnMap( oSettings.oFeatures, oInit, "bDeferRender" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollX", "sX" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollXInner", "sXInner" );
+			_fnMap( oSettings.oScroll, oInit, "sScrollY", "sY" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollInfinite", "bInfinite" );
+			_fnMap( oSettings.oScroll, oInit, "iScrollLoadGap", "iLoadGap" );
+			_fnMap( oSettings.oScroll, oInit, "bScrollAutoCss", "bAutoCss" );
+			_fnMap( oSettings, oInit, "asStripeClasses" );
+			_fnMap( oSettings, oInit, "asStripClasses", "asStripeClasses" ); // legacy
+			_fnMap( oSettings, oInit, "fnServerData" );
+			_fnMap( oSettings, oInit, "fnFormatNumber" );
+			_fnMap( oSettings, oInit, "sServerMethod" );
+			_fnMap( oSettings, oInit, "aaSorting" );
+			_fnMap( oSettings, oInit, "aaSortingFixed" );
+			_fnMap( oSettings, oInit, "aLengthMenu" );
+			_fnMap( oSettings, oInit, "sPaginationType" );
+			_fnMap( oSettings, oInit, "sAjaxSource" );
+			_fnMap( oSettings, oInit, "sAjaxDataProp" );
+			_fnMap( oSettings, oInit, "iCookieDuration" );
+			_fnMap( oSettings, oInit, "sCookiePrefix" );
+			_fnMap( oSettings, oInit, "sDom" );
+			_fnMap( oSettings, oInit, "bSortCellsTop" );
+			_fnMap( oSettings, oInit, "iTabIndex" );
+			_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );
+			_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );
+			_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );
+			_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );
+			_fnMap( oSettings, oInit, "fnCookieCallback" );
+			_fnMap( oSettings, oInit, "fnStateLoad" );
+			_fnMap( oSettings, oInit, "fnStateSave" );
+			_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+			
+			/* Callback functions which are array driven */
+			_fnCallbackReg( oSettings, 'aoDrawCallback',       oInit.fnDrawCallback,      'user' );
+			_fnCallbackReg( oSettings, 'aoServerParams',       oInit.fnServerParams,      'user' );
+			_fnCallbackReg( oSettings, 'aoStateSaveParams',    oInit.fnStateSaveParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoadParams',    oInit.fnStateLoadParams,   'user' );
+			_fnCallbackReg( oSettings, 'aoStateLoaded',        oInit.fnStateLoaded,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCallback',        oInit.fnRowCallback,       'user' );
+			_fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow,        'user' );
+			_fnCallbackReg( oSettings, 'aoHeaderCallback',     oInit.fnHeaderCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoFooterCallback',     oInit.fnFooterCallback,    'user' );
+			_fnCallbackReg( oSettings, 'aoInitComplete',       oInit.fnInitComplete,      'user' );
+			_fnCallbackReg( oSettings, 'aoPreDrawCallback',    oInit.fnPreDrawCallback,   'user' );
+			
+			if ( oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&
+				   oSettings.oFeatures.bSortClasses )
+			{
+				/* Enable sort classes for server-side processing. Safe to do it here, since server-side
+				 * processing must be enabled by the developer
+				 */
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'server_side_sort_classes' );
+			}
+			else if ( oSettings.oFeatures.bDeferRender )
+			{
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSortingClasses, 'defer_sort_classes' );
+			}
+			
+			if ( oInit.bJQueryUI )
+			{
+				/* Use the JUI classes object for display. You could clone the oStdClasses object if 
+				 * you want to have multiple tables with multiple independent classes 
+				 */
+				$.extend( oSettings.oClasses, DataTable.ext.oJUIClasses );
+				
+				if ( oInit.sDom === DataTable.defaults.sDom && DataTable.defaults.sDom === "lfrtip" )
+				{
+					/* Set the DOM to use a layout suitable for jQuery UI's theming */
+					oSettings.sDom = '<"H"lfr>t<"F"ip>';
+				}
+			}
+			else
+			{
+				$.extend( oSettings.oClasses, DataTable.ext.oStdClasses );
+			}
+			$(this).addClass( oSettings.oClasses.sTable );
+			
+			/* Calculate the scroll bar width and cache it for use later on */
+			if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+			{
+				oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
+			}
+			
+			if ( oSettings.iInitDisplayStart === undefined )
+			{
+				/* Display start point, taking into account the save saving */
+				oSettings.iInitDisplayStart = oInit.iDisplayStart;
+				oSettings._iDisplayStart = oInit.iDisplayStart;
+			}
+			
+			/* Must be done after everything which can be overridden by a cookie! */
+			if ( oInit.bStateSave )
+			{
+				oSettings.oFeatures.bStateSave = true;
+				_fnLoadState( oSettings, oInit );
+				_fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' );
+			}
+			
+			if ( oInit.iDeferLoading !== null )
+			{
+				oSettings.bDeferLoading = true;
+				var tmp = $.isArray( oInit.iDeferLoading );
+				oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading;
+				oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading;
+			}
+			
+			if ( oInit.aaData !== null )
+			{
+				bUsePassedData = true;
+			}
+			
+			/* Language definitions */
+			if ( oInit.oLanguage.sUrl !== "" )
+			{
+				/* Get the language definitions from a file - because this Ajax call makes the language
+				 * get async to the remainder of this function we use bInitHandedOff to indicate that 
+				 * _fnInitialise will be fired by the returned Ajax handler, rather than the constructor
+				 */
+				oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
+				$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) {
+					_fnLanguageCompat( json );
+					$.extend( true, oSettings.oLanguage, oInit.oLanguage, json );
+					_fnInitialise( oSettings );
+				} );
+				bInitHandedOff = true;
+			}
+			else
+			{
+				$.extend( true, oSettings.oLanguage, oInit.oLanguage );
+			}
+			
+			
+			/*
+			 * Stripes
+			 */
+			if ( oInit.asStripeClasses === null )
+			{
+				oSettings.asStripeClasses =[
+					oSettings.oClasses.sStripeOdd,
+					oSettings.oClasses.sStripeEven
+				];
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			iLen=oSettings.asStripeClasses.length;
+			oSettings.asDestroyStripes = [];
+			if (iLen)
+			{
+				var bStripeRemove = false;
+				var anRows = $(this).children('tbody').children('tr:lt(' + iLen + ')');
+				for ( i=0 ; i<iLen ; i++ )
+				{
+					if ( anRows.hasClass( oSettings.asStripeClasses[i] ) )
+					{
+						bStripeRemove = true;
+						
+						/* Store the classes which we are about to remove so they can be re-added on destroy */
+						oSettings.asDestroyStripes.push( oSettings.asStripeClasses[i] );
+					}
+				}
+				
+				if ( bStripeRemove )
+				{
+					anRows.removeClass( oSettings.asStripeClasses.join(' ') );
+				}
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var anThs = [];
+			var aoColumnsInit;
+			var nThead = this.getElementsByTagName('thead');
+			if ( nThead.length !== 0 )
+			{
+				_fnDetectHeader( oSettings.aoHeader, nThead[0] );
+				anThs = _fnGetUniqueThs( oSettings );
+			}
+			
+			/* If not given a column array, generate one with nulls */
+			if ( oInit.aoColumns === null )
+			{
+				aoColumnsInit = [];
+				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )
+				{
+					aoColumnsInit.push( null );
+				}
+			}
+			else
+			{
+				aoColumnsInit = oInit.aoColumns;
+			}
+			
+			/* Add the columns */
+			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+			{
+				/* Short cut - use the loop to check if we have column visibility state to restore */
+				if ( oInit.saved_aoColumns !== undefined && oInit.saved_aoColumns.length == iLen )
+				{
+					if ( aoColumnsInit[i] === null )
+					{
+						aoColumnsInit[i] = {};
+					}
+					aoColumnsInit[i].bVisible = oInit.saved_aoColumns[i].bVisible;
+				}
+				
+				_fnAddColumn( oSettings, anThs ? anThs[i] : null );
+			}
+			
+			/* Apply the column definitions */
+			_fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) {
+				_fnColumnOptions( oSettings, iCol, oDef );
+			} );
+			
+			
+			/*
+			 * Sorting
+			 * Check the aaSorting array
+			 */
+			for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aaSorting[i][0] >= oSettings.aoColumns.length )
+				{
+					oSettings.aaSorting[i][0] = 0;
+				}
+				var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];
+				
+				/* Add a default sorting index */
+				if ( oSettings.aaSorting[i][2] === undefined )
+				{
+					oSettings.aaSorting[i][2] = 0;
+				}
+				
+				/* If aaSorting is not defined, then we use the first indicator in asSorting */
+				if ( oInit.aaSorting === undefined && oSettings.saved_aaSorting === undefined )
+				{
+					oSettings.aaSorting[i][1] = oColumn.asSorting[0];
+				}
+				
+				/* Set the current sorting index based on aoColumns.asSorting */
+				for ( j=0, jLen=oColumn.asSorting.length ; j<jLen ; j++ )
+				{
+					if ( oSettings.aaSorting[i][1] == oColumn.asSorting[j] )
+					{
+						oSettings.aaSorting[i][2] = j;
+						break;
+					}
+				}
+			}
+				
+			/* Do a first pass on the sorting classes (allows any size changes to be taken into
+			 * account, and also will apply sorting disabled classes if disabled
+			 */
+			_fnSortingClasses( oSettings );
+			
+			
+			/*
+			 * Final init
+			 * Cache the header, body and footer as required, creating them if needed
+			 */
+			
+			/* Browser support detection */
+			_fnBrowserDetect( oSettings );
+			
+			// Work around for Webkit bug 83867 - store the caption-side before removing from doc
+			var captions = $(this).children('caption').each( function () {
+				this._captionSide = $(this).css('caption-side');
+			} );
+			
+			var thead = $(this).children('thead');
+			if ( thead.length === 0 )
+			{
+				thead = [ document.createElement( 'thead' ) ];
+				this.appendChild( thead[0] );
+			}
+			oSettings.nTHead = thead[0];
+			
+			var tbody = $(this).children('tbody');
+			if ( tbody.length === 0 )
+			{
+				tbody = [ document.createElement( 'tbody' ) ];
+				this.appendChild( tbody[0] );
+			}
+			oSettings.nTBody = tbody[0];
+			oSettings.nTBody.setAttribute( "role", "alert" );
+			oSettings.nTBody.setAttribute( "aria-live", "polite" );
+			oSettings.nTBody.setAttribute( "aria-relevant", "all" );
+			
+			var tfoot = $(this).children('tfoot');
+			if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") )
+			{
+				// If we are a scrolling table, and no footer has been given, then we need to create
+				// a tfoot element for the caption element to be appended to
+				tfoot = [ document.createElement( 'tfoot' ) ];
+				this.appendChild( tfoot[0] );
+			}
+			
+			if ( tfoot.length > 0 )
+			{
+				oSettings.nTFoot = tfoot[0];
+				_fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
+			}
+			
+			/* Check if there is data passing into the constructor */
+			if ( bUsePassedData )
+			{
+				for ( i=0 ; i<oInit.aaData.length ; i++ )
+				{
+					_fnAddData( oSettings, oInit.aaData[ i ] );
+				}
+			}
+			else
+			{
+				/* Grab the data from the page */
+				_fnGatherData( oSettings );
+			}
+			
+			/* Copy the data index array */
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			/* Initialisation complete - table can be drawn */
+			oSettings.bInitialised = true;
+			
+			/* Check if we need to initialise the table (it might not have been handed off to the
+			 * language processor)
+			 */
+			if ( bInitHandedOff === false )
+			{
+				_fnInitialise( oSettings );
+			}
+		} );
+		_that = null;
+		return this;
+	};
+
+	
+	
+	/**
+	 * Provide a common method for plug-ins to check the version of DataTables being used, in order
+	 * to ensure compatibility.
+	 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the
+	 *    formats "X" and "X.Y" are also acceptable.
+	 *  @returns {boolean} true if this version of DataTables is greater or equal to the required
+	 *    version, or false if this version of DataTales is not suitable
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    alert( $.fn.dataTable.fnVersionCheck( '1.9.0' ) );
+	 */
+	DataTable.fnVersionCheck = function( sVersion )
+	{
+		/* This is cheap, but effective */
+		var fnZPad = function (Zpad, count)
+		{
+			while(Zpad.length < count) {
+				Zpad += '0';
+			}
+			return Zpad;
+		};
+		var aThis = DataTable.ext.sVersion.split('.');
+		var aThat = sVersion.split('.');
+		var sThis = '', sThat = '';
+		
+		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ )
+		{
+			sThis += fnZPad( aThis[i], 3 );
+			sThat += fnZPad( aThat[i], 3 );
+		}
+		
+		return parseInt(sThis, 10) >= parseInt(sThat, 10);
+	};
+	
+	
+	/**
+	 * Check if a TABLE node is a DataTable table already or not.
+	 *  @param {node} nTable The TABLE node to check if it is a DataTable or not (note that other
+	 *    node types can be passed in, but will always return false).
+	 *  @returns {boolean} true the table given is a DataTable, or false otherwise
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var ex = document.getElementById('example');
+	 *    if ( ! $.fn.DataTable.fnIsDataTable( ex ) ) {
+	 *      $(ex).dataTable();
+	 *    }
+	 */
+	DataTable.fnIsDataTable = function ( nTable )
+	{
+		var o = DataTable.settings;
+	
+		for ( var i=0 ; i<o.length ; i++ )
+		{
+			if ( o[i].nTable === nTable || o[i].nScrollHead === nTable || o[i].nScrollFoot === nTable )
+			{
+				return true;
+			}
+		}
+	
+		return false;
+	};
+	
+	
+	/**
+	 * Get all DataTable tables that have been initialised - optionally you can select to
+	 * get only currently visible tables.
+	 *  @param {boolean} [bVisible=false] Flag to indicate if you want all (default) or 
+	 *    visible tables only.
+	 *  @returns {array} Array of TABLE nodes (not DataTable instances) which are DataTables
+	 *  @static
+	 *  @dtopt API-Static
+	 *
+	 *  @example
+	 *    var table = $.fn.dataTable.fnTables(true);
+	 *    if ( table.length > 0 ) {
+	 *      $(table).dataTable().fnAdjustColumnSizing();
+	 *    }
+	 */
+	DataTable.fnTables = function ( bVisible )
+	{
+		var out = [];
+	
+		jQuery.each( DataTable.settings, function (i, o) {
+			if ( !bVisible || (bVisible === true && $(o.nTable).is(':visible')) )
+			{
+				out.push( o.nTable );
+			}
+		} );
+	
+		return out;
+	};
+	
+
+	/**
+	 * Version string for plug-ins to check compatibility. Allowed format is
+	 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+	 * e are optional
+	 *  @member
+	 *  @type string
+	 *  @default Version number
+	 */
+	DataTable.version = "1.9.4";
+
+	/**
+	 * Private data store, containing all of the settings objects that are created for the
+	 * tables on a given page.
+	 * 
+	 * Note that the <i>DataTable.settings</i> object is aliased to <i>jQuery.fn.dataTableExt</i> 
+	 * through which it may be accessed and manipulated, or <i>jQuery.fn.dataTable.settings</i>.
+	 *  @member
+	 *  @type array
+	 *  @default []
+	 *  @private
+	 */
+	DataTable.settings = [];
+
+	/**
+	 * Object models container, for the various models that DataTables has available
+	 * to it. These models define the objects that are used to hold the active state 
+	 * and configuration of the table.
+	 *  @namespace
+	 */
+	DataTable.models = {};
+	
+	
+	/**
+	 * DataTables extension options and plug-ins. This namespace acts as a collection "area"
+	 * for plug-ins that can be used to extend the default DataTables behaviour - indeed many
+	 * of the build in methods use this method to provide their own capabilities (sorting methods
+	 * for example).
+	 * 
+	 * Note that this namespace is aliased to jQuery.fn.dataTableExt so it can be readily accessed
+	 * and modified by plug-ins.
+	 *  @namespace
+	 */
+	DataTable.models.ext = {
+		/**
+		 * Plug-in filtering functions - this method of filtering is complimentary to the default
+		 * type based filtering, and a lot more comprehensive as it allows you complete control
+		 * over the filtering logic. Each element in this array is a function (parameters
+		 * described below) that is called for every row in the table, and your logic decides if
+		 * it should be included in the filtered data set or not.
+		 *   <ul>
+		 *     <li>
+		 *       Function input parameters:
+		 *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *         <li>{array|object} Data for the row to be processed (same as the original format
+		 *           that was passed in as the data source, or an array from a DOM data source</li>
+		 *         <li>{int} Row index in aoData ({@link DataTable.models.oSettings.aoData}), which can
+		 *           be useful to retrieve the TR element if you need DOM interaction.</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{boolean} Include the row in the filtered result set (true) or not (false)</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *
+		 *  @example
+		 *    // The following example shows custom filtering being applied to the fourth column (i.e.
+		 *    // the aData[3] index) based on two input values from the end-user, matching the data in 
+		 *    // a certain range.
+		 *    $.fn.dataTableExt.afnFiltering.push(
+		 *      function( oSettings, aData, iDataIndex ) {
+		 *        var iMin = document.getElementById('min').value * 1;
+		 *        var iMax = document.getElementById('max').value * 1;
+		 *        var iVersion = aData[3] == "-" ? 0 : aData[3]*1;
+		 *        if ( iMin == "" && iMax == "" ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin == "" && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && "" == iMax ) {
+		 *          return true;
+		 *        }
+		 *        else if ( iMin < iVersion && iVersion < iMax ) {
+		 *          return true;
+		 *        }
+		 *        return false;
+		 *      }
+		 *    );
+		 */
+		"afnFiltering": [],
+	
+	
+		/**
+		 * Plug-in sorting functions - this method of sorting is complimentary to the default type
+		 * based sorting that DataTables does automatically, allowing much greater control over the
+		 * the data that is being used to sort a column. This is useful if you want to do sorting
+		 * based on live data (for example the contents of an 'input' element) rather than just the
+		 * static string that DataTables knows of. The way these plug-ins work is that you create
+		 * an array of the values you wish to be sorted for the column in question and then return
+		 * that array. Which pre-sorting function is run here depends on the sSortDataType parameter
+		 * that is used for the column (if any). This is the corollary of <i>ofnSearch</i> for sort 
+		 * data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *         <li>{int} Target column index</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{array} Data for the column to be sorted upon</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type array
+		 *  @default []
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    // Updating the cached sorting information with user entered values in HTML input elements
+		 *    jQuery.fn.dataTableExt.afnSortData['dom-text'] = function ( oSettings, iColumn )
+		 *    {
+		 *      var aData = [];
+		 *      $( 'td:eq('+iColumn+') input', oSettings.oApi._fnGetTrNodes(oSettings) ).each( function () {
+		 *        aData.push( this.value );
+		 *      } );
+		 *      return aData;
+		 *    }
+		 */
+		"afnSortData": [],
+	
+	
+		/**
+		 * Feature plug-ins - This is an array of objects which describe the feature plug-ins that are
+		 * available to DataTables. These feature plug-ins are accessible through the sDom initialisation
+		 * option. As such, each feature plug-in must describe a function that is used to initialise
+		 * itself (fnInit), a character so the feature can be enabled by sDom (cFeature) and the name
+		 * of the feature (sFeature). Thus the objects attached to this method must provide:
+		 *   <ul>
+		 *     <li>{function} fnInit Initialisation of the plug-in
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>{node|null} The element which contains your feature. Note that the return
+		 *                may also be void if your plug-in does not require to inject any DOM elements 
+		 *                into DataTables control (sDom) - for example this might be useful when 
+		 *                developing a plug-in which allows table control via keyboard entry.</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </li>
+		 *     <li>{character} cFeature Character that will be matched in sDom - case sensitive</li>
+		 *     <li>{string} sFeature Feature name</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 * 
+		 *  @example
+		 *    // How TableTools initialises itself.
+		 *    $.fn.dataTableExt.aoFeatures.push( {
+		 *      "fnInit": function( oSettings ) {
+		 *        return new TableTools( { "oDTSettings": oSettings } );
+		 *      },
+		 *      "cFeature": "T",
+		 *      "sFeature": "TableTools"
+		 *    } );
+		 */
+		"aoFeatures": [],
+	
+	
+		/**
+		 * Type detection plug-in functions - DataTables utilises types to define how sorting and
+		 * filtering behave, and types can be either  be defined by the developer (sType for the
+		 * column) or they can be automatically detected by the methods in this array. The functions
+		 * defined in the array are quite simple, taking a single parameter (the data to analyse) 
+		 * and returning the type if it is a known type, or null otherwise.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be analysed</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Data type detected, or null if unknown (and thus pass it
+		 *           on to the other type detection functions.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 *  
+		 *  @example
+		 *    // Currency type detection plug-in:
+		 *    jQuery.fn.dataTableExt.aTypes.push(
+		 *      function ( sData ) {
+		 *        var sValidChars = "0123456789.-";
+		 *        var Char;
+		 *        
+		 *        // Check the numeric part
+		 *        for ( i=1 ; i<sData.length ; i++ ) {
+		 *          Char = sData.charAt(i); 
+		 *          if (sValidChars.indexOf(Char) == -1) {
+		 *            return null;
+		 *          }
+		 *        }
+		 *        
+		 *        // Check prefixed by currency
+		 *        if ( sData.charAt(0) == '$' || sData.charAt(0) == '£' ) {
+		 *          return 'currency';
+		 *        }
+		 *        return null;
+		 *      }
+		 *    );
+		 */
+		"aTypes": [],
+	
+	
+		/**
+		 * Provide a common method for plug-ins to check the version of DataTables being used, 
+		 * in order to ensure compatibility.
+		 *  @type function
+		 *  @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note 
+		 *    that the formats "X" and "X.Y" are also acceptable.
+		 *  @returns {boolean} true if this version of DataTables is greater or equal to the 
+		 *    required version, or false if this version of DataTales is not suitable
+		 *
+		 *  @example
+		 *    $(document).ready(function() {
+		 *      var oTable = $('#example').dataTable();
+		 *      alert( oTable.fnVersionCheck( '1.9.0' ) );
+		 *    } );
+		 */
+		"fnVersionCheck": DataTable.fnVersionCheck,
+	
+	
+		/**
+		 * Index for what 'this' index API functions should use
+		 *  @type int
+		 *  @default 0
+		 */
+		"iApiIndex": 0,
+	
+	
+		/**
+		 * Pre-processing of filtering data plug-ins - When you assign the sType for a column
+		 * (or have it automatically detected for you by DataTables or a type detection plug-in), 
+		 * you will typically be using this for custom sorting, but it can also be used to provide 
+		 * custom filtering by allowing you to pre-processing the data and returning the data in
+		 * the format that should be filtered upon. This is done by adding functions this object 
+		 * with a parameter name which matches the sType for that target column. This is the
+		 * corollary of <i>afnSortData</i> for filtering data.
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data from the column cell to be prepared for filtering</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{string|null} Formatted string that will be used for the filtering.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 * 
+		 * Note that as of v1.9, it is typically preferable to use <i>mData</i> to prepare data for
+		 * the different uses that DataTables can put the data to. Specifically <i>mData</i> when
+		 * used as a function will give you a 'type' (sorting, filtering etc) that you can use to 
+		 * prepare the data as required for the different types. As such, this method is deprecated.
+		 *  @type object
+		 *  @default {}
+		 *  @deprecated
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.ofnSearch['title-numeric'] = function ( sData ) {
+		 *      return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
+		 *    }
+		 */
+		"ofnSearch": {},
+	
+	
+		/**
+		 * Container for all private functions in DataTables so they can be exposed externally
+		 *  @type object
+		 *  @default {}
+		 */
+		"oApi": {},
+	
+	
+		/**
+		 * Storage for the various classes that DataTables uses
+		 *  @type object
+		 *  @default {}
+		 */
+		"oStdClasses": {},
+		
+	
+		/**
+		 * Storage for the various classes that DataTables uses - jQuery UI suitable
+		 *  @type object
+		 *  @default {}
+		 */
+		"oJUIClasses": {},
+	
+	
+		/**
+		 * Pagination plug-in methods - The style and controls of the pagination can significantly 
+		 * impact on how the end user interacts with the data in your table, and DataTables allows 
+		 * the addition of pagination controls by extending this object, which can then be enabled
+		 * through the <i>sPaginationType</i> initialisation parameter. Each pagination type that
+		 * is added is an object (the property name of which is what <i>sPaginationType</i> refers
+		 * to) that has two properties, both methods that are used by DataTables to update the
+		 * control's state.
+		 *   <ul>
+		 *     <li>
+		 *       fnInit -  Initialisation of the paging controls. Called only during initialisation 
+		 *         of the table. It is expected that this function will add the required DOM elements 
+		 *         to the page for the paging controls to work. The element pointer 
+		 *         'oSettings.aanFeatures.p' array is provided by DataTables to contain the paging 
+		 *         controls (note that this is a 2D array to allow for multiple instances of each 
+		 *         DataTables DOM element). It is suggested that you add the controls to this element 
+		 *         as children
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{node} Container into which the pagination controls must be inserted</li>
+		 *             <li>{function} Draw callback function - whenever the controls cause a page
+		 *               change, this method must be called to redraw the table.</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *     <li>
+		 *       fnInit -  This function is called whenever the paging status of the table changes and is
+		 *         typically used to update classes and/or text of the paging controls to reflex the new 
+		 *         status.
+		 *       <ul>
+	     *         <li>
+	     *           Function input parameters:
+	     *           <ul>
+		 *             <li>{object} DataTables settings object: see {@link DataTable.models.oSettings}.</li>
+		 *             <li>{function} Draw callback function - in case you need to redraw the table again
+		 *               or attach new event listeners</li>
+	     *           </ul>
+	     *         </li>
+		 *         <li>
+		 *           Function return:
+		 *           <ul>
+		 *             <li>No return required</li>
+		 *           </ul>
+		 *         </il>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    $.fn.dataTableExt.oPagination.four_button = {
+		 *      "fnInit": function ( oSettings, nPaging, fnCallbackDraw ) {
+		 *        nFirst = document.createElement( 'span' );
+		 *        nPrevious = document.createElement( 'span' );
+		 *        nNext = document.createElement( 'span' );
+		 *        nLast = document.createElement( 'span' );
+		 *        
+		 *        nFirst.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sFirst ) );
+		 *        nPrevious.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sPrevious ) );
+		 *        nNext.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sNext ) );
+		 *        nLast.appendChild( document.createTextNode( oSettings.oLanguage.oPaginate.sLast ) );
+		 *        
+		 *        nFirst.className = "paginate_button first";
+		 *        nPrevious.className = "paginate_button previous";
+		 *        nNext.className="paginate_button next";
+		 *        nLast.className = "paginate_button last";
+		 *        
+		 *        nPaging.appendChild( nFirst );
+		 *        nPaging.appendChild( nPrevious );
+		 *        nPaging.appendChild( nNext );
+		 *        nPaging.appendChild( nLast );
+		 *        
+		 *        $(nFirst).click( function () {
+		 *          oSettings.oApi._fnPageChange( oSettings, "first" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nPrevious).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "previous" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nNext).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "next" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nLast).click( function() {
+		 *          oSettings.oApi._fnPageChange( oSettings, "last" );
+		 *          fnCallbackDraw( oSettings );
+		 *        } );
+		 *        
+		 *        $(nFirst).bind( 'selectstart', function () { return false; } );
+		 *        $(nPrevious).bind( 'selectstart', function () { return false; } );
+		 *        $(nNext).bind( 'selectstart', function () { return false; } );
+		 *        $(nLast).bind( 'selectstart', function () { return false; } );
+		 *      },
+		 *      
+		 *      "fnUpdate": function ( oSettings, fnCallbackDraw ) {
+		 *        if ( !oSettings.aanFeatures.p ) {
+		 *          return;
+		 *        }
+		 *        
+		 *        // Loop over each instance of the pager
+		 *        var an = oSettings.aanFeatures.p;
+		 *        for ( var i=0, iLen=an.length ; i<iLen ; i++ ) {
+		 *          var buttons = an[i].getElementsByTagName('span');
+		 *          if ( oSettings._iDisplayStart === 0 ) {
+		 *            buttons[0].className = "paginate_disabled_previous";
+		 *            buttons[1].className = "paginate_disabled_previous";
+		 *          }
+		 *          else {
+		 *            buttons[0].className = "paginate_enabled_previous";
+		 *            buttons[1].className = "paginate_enabled_previous";
+		 *          }
+		 *          
+		 *          if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) {
+		 *            buttons[2].className = "paginate_disabled_next";
+		 *            buttons[3].className = "paginate_disabled_next";
+		 *          }
+		 *          else {
+		 *            buttons[2].className = "paginate_enabled_next";
+		 *            buttons[3].className = "paginate_enabled_next";
+		 *          }
+		 *        }
+		 *      }
+		 *    };
+		 */
+		"oPagination": {},
+	
+	
+		/**
+		 * Sorting plug-in methods - Sorting in DataTables is based on the detected type of the
+		 * data column (you can add your own type detection functions, or override automatic 
+		 * detection using sType). With this specific type given to the column, DataTables will 
+		 * apply the required sort from the functions in the object. Each sort type must provide
+		 * two mandatory methods, one each for ascending and descending sorting, and can optionally
+		 * provide a pre-formatting method that will help speed up sorting by allowing DataTables
+		 * to pre-format the sort data only once (rather than every time the actual sort functions
+		 * are run). The two sorting functions are typical Javascript sort methods:
+		 *   <ul>
+	     *     <li>
+	     *       Function input parameters:
+	     *       <ul>
+		 *         <li>{*} Data to compare to the second parameter</li>
+		 *         <li>{*} Data to compare to the first parameter</li>
+	     *       </ul>
+	     *     </li>
+		 *     <li>
+		 *       Function return:
+		 *       <ul>
+		 *         <li>{int} Sorting match: <0 if first parameter should be sorted lower than
+		 *           the second parameter, ===0 if the two parameters are equal and >0 if
+		 *           the first parameter should be sorted height than the second parameter.</li>
+		 *       </ul>
+		 *     </il>
+		 *   </ul>
+		 *  @type object
+		 *  @default {}
+		 *
+		 *  @example
+		 *    // Case-sensitive string sorting, with no pre-formatting method
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-case-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-case-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 *
+		 *  @example
+		 *    // Case-insensitive string sorting, with pre-formatting
+		 *    $.extend( $.fn.dataTableExt.oSort, {
+		 *      "string-pre": function(x) {
+		 *        return x.toLowerCase();
+		 *      },
+		 *      "string-asc": function(x,y) {
+		 *        return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		 *      },
+		 *      "string-desc": function(x,y) {
+		 *        return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		 *      }
+		 *    } );
+		 */
+		"oSort": {},
+	
+	
+		/**
+		 * Version string for plug-ins to check compatibility. Allowed format is
+		 * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
+		 * e are optional
+		 *  @type string
+		 *  @default Version number
+		 */
+		"sVersion": DataTable.version,
+	
+	
+		/**
+		 * How should DataTables report an error. Can take the value 'alert' or 'throw'
+		 *  @type string
+		 *  @default alert
+		 */
+		"sErrMode": "alert",
+	
+	
+		/**
+		 * Store information for DataTables to access globally about other instances
+		 *  @namespace
+		 *  @private
+		 */
+		"_oExternConfig": {
+			/* int:iNextUnique - next unique number for an instance */
+			"iNextUnique": 0
+		}
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * search information for the global filter and individual column filters.
+	 *  @namespace
+	 */
+	DataTable.models.oSearch = {
+		/**
+		 * Flag to indicate if the filtering should be case insensitive or not
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bCaseInsensitive": true,
+	
+		/**
+		 * Applied search term
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sSearch": "",
+	
+		/**
+		 * Flag to indicate if the search term should be interpreted as a
+		 * regular expression (true) or not (false) and therefore and special
+		 * regex characters escaped.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bRegex": false,
+	
+		/**
+		 * Flag to indicate if DataTables is to use its smart filtering or not.
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bSmart": true
+	};
+	
+	
+	
+	
+	/**
+	 * Template object for the way in which DataTables holds information about
+	 * each individual row. This is the object format used for the settings 
+	 * aoData array.
+	 *  @namespace
+	 */
+	DataTable.models.oRow = {
+		/**
+		 * TR element for the row
+		 *  @type node
+		 *  @default null
+		 */
+		"nTr": null,
+	
+		/**
+		 * Data object from the original data source for the row. This is either
+		 * an array if using the traditional form of DataTables, or an object if
+		 * using mData options. The exact type will depend on the passed in
+		 * data from the data source, or will be an array if using DOM a data 
+		 * source.
+		 *  @type array|object
+		 *  @default []
+		 */
+		"_aData": [],
+	
+		/**
+		 * Sorting data cache - this array is ostensibly the same length as the
+		 * number of columns (although each index is generated only as it is 
+		 * needed), and holds the data that is used for sorting each column in the
+		 * row. We do this cache generation at the start of the sort in order that
+		 * the formatting of the sort data need be done only once for each cell
+		 * per sort. This array should not be read from or written to by anything
+		 * other than the master sorting methods.
+		 *  @type array
+		 *  @default []
+		 *  @private
+		 */
+		"_aSortData": [],
+	
+		/**
+		 * Array of TD elements that are cached for hidden rows, so they can be
+		 * reinserted into the table if a column is made visible again (or to act
+		 * as a store if a column is made hidden). Only hidden columns have a 
+		 * reference in the array. For non-hidden columns the value is either
+		 * undefined or null.
+		 *  @type array nodes
+		 *  @default []
+		 *  @private
+		 */
+		"_anHidden": [],
+	
+		/**
+		 * Cache of the class name that DataTables has applied to the row, so we
+		 * can quickly look at this variable rather than needing to do a DOM check
+		 * on className for the nTr property.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @private
+		 */
+		"_sRowStripe": ""
+	};
+	
+	
+	
+	/**
+	 * Template object for the column information object in DataTables. This object
+	 * is held in the settings aoColumns array and contains all the information that
+	 * DataTables needs about each individual column.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults.columns} 
+	 * but this one is the internal data store for DataTables's cache of columns.
+	 * It should NOT be manipulated outside of DataTables. Any configuration should
+	 * be done through the initialisation options.
+	 *  @namespace
+	 */
+	DataTable.models.oColumn = {
+		/**
+		 * A list of the columns that sorting should occur on when this column
+		 * is sorted. That this property is an array allows multi-column sorting
+		 * to be defined for a column (for example first name / last name columns
+		 * would benefit from this). The values are integers pointing to the
+		 * columns to be sorted on (typically it will be a single integer pointing
+		 * at itself, but that doesn't need to be the case).
+		 *  @type array
+		 */
+		"aDataSort": null,
+	
+		/**
+		 * Define the sorting directions that are applied to the column, in sequence
+		 * as the column is repeatedly sorted upon - i.e. the first value is used
+		 * as the sorting direction when the column if first sorted (clicked on).
+		 * Sort it again (click again) and it will move on to the next index.
+		 * Repeat until loop.
+		 *  @type array
+		 */
+		"asSorting": null,
+		
+		/**
+		 * Flag to indicate if the column is searchable, and thus should be included
+		 * in the filtering or not.
+		 *  @type boolean
+		 */
+		"bSearchable": null,
+		
+		/**
+		 * Flag to indicate if the column is sortable or not.
+		 *  @type boolean
+		 */
+		"bSortable": null,
+		
+		/**
+		 * <code>Deprecated</code> When using fnRender, you have two options for what 
+		 * to do with the data, and this property serves as the switch. Firstly, you 
+		 * can have the sorting and filtering use the rendered value (true - default), 
+		 * or you can have the sorting and filtering us the original value (false).
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @deprecated
+		 */
+		"bUseRendered": null,
+		
+		/**
+		 * Flag to indicate if the column is currently visible in the table or not
+		 *  @type boolean
+		 */
+		"bVisible": null,
+		
+		/**
+		 * Flag to indicate to the type detection method if the automatic type
+		 * detection should be used, or if a column type (sType) has been specified
+		 *  @type boolean
+		 *  @default true
+		 *  @private
+		 */
+		"_bAutoType": true,
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @default null
+		 */
+		"fnCreatedCell": null,
+		
+		/**
+		 * Function to get data from a cell in a column. You should <b>never</b>
+		 * access data directly through _aData internally in DataTables - always use
+		 * the method attached to this property. It allows mData to function as
+		 * required. This function is automatically assigned by the column 
+		 * initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {string} sSpecific The specific data type you want to get - 
+		 *    'display', 'type' 'filter' 'sort'
+		 *  @returns {*} The data for the cell from the given row's data
+		 *  @default null
+		 */
+		"fnGetData": null,
+		
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @returns {string} The string you which to use in the display
+		 *  @default null
+		 *  @deprecated
+		 */
+		"fnRender": null,
+		
+		/**
+		 * Function to set data for a cell in the column. You should <b>never</b> 
+		 * set the data directly to _aData internally in DataTables - always use
+		 * this method. It allows mData to function as required. This function
+		 * is automatically assigned by the column initialisation method
+		 *  @type function
+		 *  @param {array|object} oData The data array/object for the array 
+		 *    (i.e. aoData[]._aData)
+		 *  @param {*} sValue Value to set
+		 *  @default null
+		 */
+		"fnSetData": null,
+		
+		/**
+		 * Property to read the value for the cells in the column from the data 
+		 * source array / object. If null, then the default content is used, if a
+		 * function is given then the return from the function is used.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mData": null,
+		
+		/**
+		 * Partner property to mData which is used (only when defined) to get
+		 * the data - i.e. it is basically the same as mData, but without the
+		 * 'set' option, and also the data fed to it is the result from mData.
+		 * This is the rendering method to match the data method of mData.
+		 *  @type function|int|string|null
+		 *  @default null
+		 */
+		"mRender": null,
+		
+		/**
+		 * Unique header TH/TD element for this column - this is what the sorting
+		 * listener is attached to (if sorting is enabled.)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTh": null,
+		
+		/**
+		 * Unique footer TH/TD element for this column (if there is one). Not used 
+		 * in DataTables as such, but can be used for plug-ins to reference the 
+		 * footer for each column.
+		 *  @type node
+		 *  @default null
+		 */
+		"nTf": null,
+		
+		/**
+		 * The class to apply to all TD elements in the table's TBODY for the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sClass": null,
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 *  @type string
+		 */
+		"sContentPadding": null,
+		
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 */
+		"sDefaultContent": null,
+		
+		/**
+		 * Name for the column, allowing reference to the column by name as well as
+		 * by index (needs a lookup to work by name).
+		 *  @type string
+		 */
+		"sName": null,
+		
+		/**
+		 * Custom sorting data type - defines which of the available plug-ins in
+		 * afnSortData the custom sorting will use - if any is defined.
+		 *  @type string
+		 *  @default std
+		 */
+		"sSortDataType": 'std',
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClass": null,
+		
+		/**
+		 * Class to be applied to the header element when sorting on this column -
+		 * when jQuery UI theming is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sSortingClassJUI": null,
+		
+		/**
+		 * Title of the column - what is seen in the TH element (nTh).
+		 *  @type string
+		 */
+		"sTitle": null,
+		
+		/**
+		 * Column sorting and filtering type
+		 *  @type string
+		 *  @default null
+		 */
+		"sType": null,
+		
+		/**
+		 * Width of the column
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidth": null,
+		
+		/**
+		 * Width of the column when it was first "encountered"
+		 *  @type string
+		 *  @default null
+		 */
+		"sWidthOrig": null
+	};
+	
+	
+	
+	/**
+	 * Initialisation options that can be given to DataTables at initialisation 
+	 * time.
+	 *  @namespace
+	 */
+	DataTable.defaults = {
+		/**
+		 * An array of data to use for the table, passed in at initialisation which 
+		 * will be used in preference to any data which is already in the DOM. This is
+		 * particularly useful for constructing tables purely in Javascript, for
+		 * example with a custom Ajax call.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Using a 2D array data source
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
+		 *          ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine" },
+		 *          { "sTitle": "Browser" },
+		 *          { "sTitle": "Platform" },
+		 *          { "sTitle": "Version" },
+		 *          { "sTitle": "Grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using an array of objects as a data source (mData)
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "aaData": [
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 4.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  4,
+		 *            "grade":    "X"
+		 *          },
+		 *          {
+		 *            "engine":   "Trident",
+		 *            "browser":  "Internet Explorer 5.0",
+		 *            "platform": "Win 95+",
+		 *            "version":  5,
+		 *            "grade":    "C"
+		 *          }
+		 *        ],
+		 *        "aoColumns": [
+		 *          { "sTitle": "Engine",   "mData": "engine" },
+		 *          { "sTitle": "Browser",  "mData": "browser" },
+		 *          { "sTitle": "Platform", "mData": "platform" },
+		 *          { "sTitle": "Version",  "mData": "version" },
+		 *          { "sTitle": "Grade",    "mData": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aaData": null,
+	
+	
+		/**
+		 * If sorting is enabled, then DataTables will perform a first pass sort on 
+		 * initialisation. You can define which column(s) the sort is performed upon, 
+		 * and the sorting direction, with this variable. The aaSorting array should 
+		 * contain an array for each column to be sorted initially containing the 
+		 * column's index and a direction string ('asc' or 'desc').
+		 *  @type array
+		 *  @default [[0,'asc']]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    // Sort by 3rd column first, and then 4th column
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": [[2,'asc'], [3,'desc']]
+		 *      } );
+		 *    } );
+		 *    
+		 *    // No initial sorting
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSorting": []
+		 *      } );
+		 *    } );
+		 */
+		"aaSorting": [[0,'asc']],
+	
+	
+		/**
+		 * This parameter is basically identical to the aaSorting parameter, but 
+		 * cannot be overridden by user interaction with the table. What this means 
+		 * is that you could have a column (visible or hidden) which the sorting will 
+		 * always be forced on first - any sorting after that (from the user) will 
+		 * then be performed as required. This can be useful for grouping rows 
+		 * together.
+		 *  @type array
+		 *  @default null
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aaSortingFixed": [[0,'asc']]
+		 *      } );
+		 *    } )
+		 */
+		"aaSortingFixed": null,
+	
+	
+		/**
+		 * This parameter allows you to readily specify the entries in the length drop
+		 * down menu that DataTables shows when pagination is enabled. It can be 
+		 * either a 1D array of options which will be used for both the displayed 
+		 * option and the value, or a 2D array which will use the array in the first 
+		 * position as the value, and the array in the second position as the 
+		 * displayed options (useful for language strings such as 'All').
+		 *  @type array
+		 *  @default [ 10, 25, 50, 100 ]
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aLengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
+		 *      } );
+		 *    } );
+		 *  
+		 *  @example
+		 *    // Setting the default display length as well as length menu
+		 *    // This is likely to be wanted if you remove the '10' option which
+		 *    // is the iDisplayLength default.
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 25,
+		 *        "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
+		 *      } );
+		 *    } );
+		 */
+		"aLengthMenu": [ 10, 25, 50, 100 ],
+	
+	
+		/**
+		 * The aoColumns option in the initialisation parameter allows you to define
+		 * details about the way individual columns behave. For a full list of
+		 * column options that can be set, please see 
+		 * {@link DataTable.defaults.columns}. Note that if you use aoColumns to
+		 * define your columns, you must have an entry in the array for every single
+		 * column that you have in your table (these can be null if you don't which
+		 * to specify any options).
+		 *  @member
+		 */
+		"aoColumns": null,
+	
+		/**
+		 * Very similar to aoColumns, aoColumnDefs allows you to target a specific 
+		 * column, multiple columns, or all columns, using the aTargets property of 
+		 * each object in the array. This allows great flexibility when creating 
+		 * tables, as the aoColumnDefs arrays can be of any length, targeting the 
+		 * columns you specifically want. aoColumnDefs may use any of the column 
+		 * options available: {@link DataTable.defaults.columns}, but it _must_
+		 * have aTargets defined in each object in the array. Values in the aTargets
+		 * array may be:
+		 *   <ul>
+		 *     <li>a string - class name will be matched on the TH for the column</li>
+		 *     <li>0 or a positive integer - column index counting from the left</li>
+		 *     <li>a negative integer - column index counting from the right</li>
+		 *     <li>the string "_all" - all columns (i.e. assign a default)</li>
+		 *   </ul>
+		 *  @member
+		 */
+		"aoColumnDefs": null,
+	
+	
+		/**
+		 * Basically the same as oSearch, this parameter defines the individual column
+		 * filtering state at initialisation time. The array must be of the same size 
+		 * as the number of columns, and each element be an object with the parameters
+		 * "sSearch" and "bEscapeRegex" (the latter is optional). 'null' is also
+		 * accepted and the default will be used.
+		 *  @type array
+		 *  @default []
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoSearchCols": [
+		 *          null,
+		 *          { "sSearch": "My filter" },
+		 *          null,
+		 *          { "sSearch": "^[0-9]", "bEscapeRegex": false }
+		 *        ]
+		 *      } );
+		 *    } )
+		 */
+		"aoSearchCols": [],
+	
+	
+		/**
+		 * An array of CSS classes that should be applied to displayed rows. This 
+		 * array may be of any length, and DataTables will apply each class 
+		 * sequentially, looping when required.
+		 *  @type array
+		 *  @default null <i>Will take the values determined by the oClasses.sStripe*
+		 *    options</i>
+		 *  @dtopt Option
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "asStripeClasses": [ 'strip1', 'strip2', 'strip3' ]
+		 *      } );
+		 *    } )
+		 */
+		"asStripeClasses": null,
+	
+	
+		/**
+		 * Enable or disable automatic column width calculation. This can be disabled
+		 * as an optimisation (it takes some time to calculate the widths) if the
+		 * tables widths are passed in using aoColumns.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bAutoWidth": false
+		 *      } );
+		 *    } );
+		 */
+		"bAutoWidth": true,
+	
+	
+		/**
+		 * Deferred rendering can provide DataTables with a huge speed boost when you
+		 * are using an Ajax or JS data source for the table. This option, when set to
+		 * true, will cause DataTables to defer the creation of the table elements for
+		 * each row until they are needed for a draw - saving a significant amount of
+		 * time.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/arrays.txt",
+		 *        "bDeferRender": true
+		 *      } );
+		 *    } );
+		 */
+		"bDeferRender": false,
+	
+	
+		/**
+		 * Replace a DataTable which matches the given selector and replace it with 
+		 * one which has the properties of the new initialisation object passed. If no
+		 * table matches the selector, then the new DataTable will be constructed as
+		 * per normal.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *      
+		 *      // Some time later....
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false,
+		 *        "bDestroy": true
+		 *      } );
+		 *    } );
+		 */
+		"bDestroy": false,
+	
+	
+		/**
+		 * Enable or disable filtering of data. Filtering in DataTables is "smart" in
+		 * that it allows the end user to input multiple words (space separated) and
+		 * will match a row containing those words, even if not in the order that was
+		 * specified (this allow matching across multiple columns). Note that if you
+		 * wish to use filtering in DataTables this must remain 'true' - to remove the
+		 * default filtering input box and retain filtering abilities, please use
+		 * {@link DataTable.defaults.sDom}.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bFilter": false
+		 *      } );
+		 *    } );
+		 */
+		"bFilter": true,
+	
+	
+		/**
+		 * Enable or disable the table information display. This shows information 
+		 * about the data that is currently visible on the page, including information
+		 * about filtered data if that action is being performed.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bInfo": false
+		 *      } );
+		 *    } );
+		 */
+		"bInfo": true,
+	
+	
+		/**
+		 * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
+		 * slightly different and additional mark-up from what DataTables has
+		 * traditionally used).
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bJQueryUI": true
+		 *      } );
+		 *    } );
+		 */
+		"bJQueryUI": false,
+	
+	
+		/**
+		 * Allows the end user to select the size of a formatted page from a select
+		 * menu (sizes are 10, 25, 50 and 100). Requires pagination (bPaginate).
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bLengthChange": false
+		 *      } );
+		 *    } );
+		 */
+		"bLengthChange": true,
+	
+	
+		/**
+		 * Enable or disable pagination.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"bPaginate": true,
+	
+	
+		/**
+		 * Enable or disable the display of a 'processing' indicator when the table is
+		 * being processed (e.g. a sort). This is particularly useful for tables with
+		 * large amounts of data where it can take a noticeable amount of time to sort
+		 * the entries.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true
+		 *      } );
+		 *    } );
+		 */
+		"bProcessing": false,
+	
+	
+		/**
+		 * Retrieve the DataTables object for the given selector. Note that if the
+		 * table has already been initialised, this parameter will cause DataTables
+		 * to simply return the object that has already been set up - it will not take
+		 * account of any changes you might have made to the initialisation object
+		 * passed to DataTables (setting this parameter to true is an acknowledgement
+		 * that you understand this). bDestroy can be used to reinitialise a table if
+		 * you need.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      initTable();
+		 *      tableActions();
+		 *    } );
+		 *    
+		 *    function initTable ()
+		 *    {
+		 *      return $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false,
+		 *        "bRetrieve": true
+		 *      } );
+		 *    }
+		 *    
+		 *    function tableActions ()
+		 *    {
+		 *      var oTable = initTable();
+		 *      // perform API operations with oTable 
+		 *    }
+		 */
+		"bRetrieve": false,
+	
+	
+		/**
+		 * Indicate if DataTables should be allowed to set the padding / margin
+		 * etc for the scrolling header elements or not. Typically you will want
+		 * this.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollAutoCss": false,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollAutoCss": true,
+	
+	
+		/**
+		 * When vertical (y) scrolling is enabled, DataTables will force the height of
+		 * the table's viewport to the given height at all times (useful for layout).
+		 * However, this can look odd when filtering data down to a small data set,
+		 * and the footer is left "floating" further down. This parameter (when
+		 * enabled) will cause DataTables to collapse the table's viewport down when
+		 * the result set will fit within the given Y height.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"bScrollCollapse": false,
+	
+	
+		/**
+		 * Enable infinite scrolling for DataTables (to be used in combination with
+		 * sScrollY). Infinite scrolling means that DataTables will continually load
+		 * data as a user scrolls through a table, which is very useful for large
+		 * dataset. This cannot be used with pagination, which is automatically
+		 * disabled. Note - the Scroller extra for DataTables is recommended in
+		 * in preference to this option.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px"
+		 *      } );
+		 *    } );
+		 */
+		"bScrollInfinite": false,
+	
+	
+		/**
+		 * Configure DataTables to use server-side processing. Note that the
+		 * sAjaxSource parameter must also be given in order to give DataTables a
+		 * source to obtain the required data for each draw.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php"
+		 *      } );
+		 *    } );
+		 */
+		"bServerSide": false,
+	
+	
+		/**
+		 * Enable or disable sorting of columns. Sorting of individual columns can be
+		 * disabled by the "bSortable" option for each column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSort": false
+		 *      } );
+		 *    } );
+		 */
+		"bSort": true,
+	
+	
+		/**
+		 * Allows control over whether DataTables should use the top (true) unique
+		 * cell that is found for a single column, or the bottom (false - default).
+		 * This is useful when using complex headers.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bSortCellsTop": true
+		 *      } );
+		 *    } );
+		 */
+		"bSortCellsTop": false,
+	
+	
+		/**
+		 * Enable or disable the addition of the classes 'sorting_1', 'sorting_2' and
+		 * 'sorting_3' to the columns which are currently being sorted on. This is
+		 * presented as a feature switch as it can increase processing time (while
+		 * classes are removed and added) so for large data sets you might want to
+		 * turn this off.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bSortClasses": false
+		 *      } );
+		 *    } );
+		 */
+		"bSortClasses": true,
+	
+	
+		/**
+		 * Enable or disable state saving. When enabled a cookie will be used to save
+		 * table display information such as pagination information, display length,
+		 * filtering and sorting. As such when the end user reloads the page the
+		 * display display will match what thy had previously set up.
+		 *  @type boolean
+		 *  @default false
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true
+		 *      } );
+		 *    } );
+		 */
+		"bStateSave": false,
+	
+	
+		/**
+		 * Customise the cookie and / or the parameters being stored when using
+		 * DataTables with state saving enabled. This function is called whenever
+		 * the cookie is modified, and it expects a fully formed cookie string to be
+		 * returned. Note that the data object passed in is a Javascript object which
+		 * must be converted to a string (JSON.stringify for example).
+		 *  @type function
+		 *  @param {string} sName Name of the cookie defined by DataTables
+		 *  @param {object} oData Data to be stored in the cookie
+		 *  @param {string} sExpires Cookie expires string
+		 *  @param {string} sPath Path of the cookie to set
+		 *  @returns {string} Cookie formatted string (which should be encoded by
+		 *    using encodeURIComponent())
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function () {
+		 *      $('#example').dataTable( {
+		 *        "fnCookieCallback": function (sName, oData, sExpires, sPath) {
+		 *          // Customise oData or sName or whatever else here
+		 *          return sName + "="+JSON.stringify(oData)+"; expires=" + sExpires +"; path=" + sPath;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCookieCallback": null,
+	
+	
+		/**
+		 * This function is called when a TR element is created (and all TD child
+		 * elements have been inserted), or registered if using a DOM source, allowing
+		 * manipulation of the TR element (adding classes etc).
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDataIndex The index of this row in aoData
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnCreatedRow": function( nRow, aData, iDataIndex ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnCreatedRow": null,
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify any aspect you want about the created DOM.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnDrawCallback": function( oSettings ) {
+		 *          alert( 'DataTables has redrawn the table' );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnDrawCallback": null,
+	
+	
+		/**
+		 * Identical to fnHeaderCallback() but for the table footer this function
+		 * allows you to modify the table footer on every 'draw' even.
+		 *  @type function
+		 *  @param {node} nFoot "TR" element for the footer
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the 
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the 
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFooterCallback": function( nFoot, aData, iStart, iEnd, aiDisplay ) {
+		 *          nFoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+iStart;
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnFooterCallback": null,
+	
+	
+		/**
+		 * When rendering large numbers in the information element for the table
+		 * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
+		 * to have a comma separator for the 'thousands' units (e.g. 1 million is
+		 * rendered as "1,000,000") to help readability for the end user. This
+		 * function will override the default method DataTables uses.
+		 *  @type function
+		 *  @member
+		 *  @param {int} iIn number to be formatted
+		 *  @returns {string} formatted string for DataTables to show the number
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnFormatNumber": function ( iIn ) {
+		 *          if ( iIn < 1000 ) {
+		 *            return iIn;
+		 *          } else {
+		 *            var 
+		 *              s=(iIn+""), 
+		 *              a=s.split(""), out="", 
+		 *              iLen=s.length;
+		 *            
+		 *            for ( var i=0 ; i<iLen ; i++ ) {
+		 *              if ( i%3 === 0 && i !== 0 ) {
+		 *                out = "'"+out;
+		 *              }
+		 *              out = a[iLen-i-1]+out;
+		 *            }
+		 *          }
+		 *          return out;
+		 *        };
+		 *      } );
+		 *    } );
+		 */
+		"fnFormatNumber": function ( iIn ) {
+			if ( iIn < 1000 )
+			{
+				// A small optimisation for what is likely to be the majority of use cases
+				return iIn;
+			}
+	
+			var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				if ( i%3 === 0 && i !== 0 )
+				{
+					out = this.oLanguage.sInfoThousands+out;
+				}
+				out = a[iLen-i-1]+out;
+			}
+			return out;
+		},
+	
+	
+		/**
+		 * This function is called on every 'draw' event, and allows you to
+		 * dynamically modify the header row. This can be used to calculate and
+		 * display useful information about the table.
+		 *  @type function
+		 *  @param {node} nHead "TR" element for the header
+		 *  @param {array} aData Full table data (as derived from the original HTML)
+		 *  @param {int} iStart Index for the current display starting point in the
+		 *    display array
+		 *  @param {int} iEnd Index for the current display ending point in the
+		 *    display array
+		 *  @param {array int} aiDisplay Index array to translate the visual position
+		 *    to the full data array
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnHeaderCallback": function( nHead, aData, iStart, iEnd, aiDisplay ) {
+		 *          nHead.getElementsByTagName('th')[0].innerHTML = "Displaying "+(iEnd-iStart)+" records";
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnHeaderCallback": null,
+	
+	
+		/**
+		 * The information element can be used to convey information about the current
+		 * state of the table. Although the internationalisation options presented by
+		 * DataTables are quite capable of dealing with most customisations, there may
+		 * be times where you wish to customise the string further. This callback
+		 * allows you to do exactly that.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {int} iStart Starting position in data for the draw
+		 *  @param {int} iEnd End position in data for the draw
+		 *  @param {int} iMax Total number of rows in the table (regardless of
+		 *    filtering)
+		 *  @param {int} iTotal Total number of rows in the data set, after filtering
+		 *  @param {string} sPre The string that DataTables has formatted using it's
+		 *    own rules
+		 *  @returns {string} The string to be displayed in the information element.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $('#example').dataTable( {
+		 *      "fnInfoCallback": function( oSettings, iStart, iEnd, iMax, iTotal, sPre ) {
+		 *        return iStart +" to "+ iEnd;
+		 *      }
+		 *    } );
+		 */
+		"fnInfoCallback": null,
+	
+	
+		/**
+		 * Called when the table has been initialised. Normally DataTables will
+		 * initialise sequentially and there will be no need for this function,
+		 * however, this does not hold true when using external language information
+		 * since that is obtained using an async XHR call.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} json The JSON object request from the server - only
+		 *    present if client-side Ajax sourced data is used
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnInitComplete": function(oSettings, json) {
+		 *          alert( 'DataTables has finished its initialisation.' );
+		 *        }
+		 *      } );
+		 *    } )
+		 */
+		"fnInitComplete": null,
+	
+	
+		/**
+		 * Called at the very start of each table draw and can be used to cancel the
+		 * draw by returning false, any other return (including undefined) results in
+		 * the full draw occurring).
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @returns {boolean} False will cancel the draw, anything else (including no
+		 *    return) will allow it to complete.
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnPreDrawCallback": function( oSettings ) {
+		 *          if ( $('#test').val() == 1 ) {
+		 *            return false;
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnPreDrawCallback": null,
+	
+	
+		/**
+		 * This function allows you to 'post process' each row after it have been
+		 * generated for each table draw, but before it is rendered on screen. This
+		 * function might be used for setting the row class name etc.
+		 *  @type function
+		 *  @param {node} nRow "TR" element for the current row
+		 *  @param {array} aData Raw data array for this row
+		 *  @param {int} iDisplayIndex The display index for the current table draw
+		 *  @param {int} iDisplayIndexFull The index of the data in the full list of
+		 *    rows (after filtering)
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) {
+		 *          // Bold the grade for all 'A' grade browsers
+		 *          if ( aData[4] == "A" )
+		 *          {
+		 *            $('td:eq(4)', nRow).html( '<b>A</b>' );
+		 *          }
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnRowCallback": null,
+	
+	
+		/**
+		 * This parameter allows you to override the default function which obtains
+		 * the data from the server ($.getJSON) so something more suitable for your
+		 * application. For example you could use POST data, or pull information from
+		 * a Gears or AIR database.
+		 *  @type function
+		 *  @member
+		 *  @param {string} sSource HTTP source to obtain the data from (sAjaxSource)
+		 *  @param {array} aoData A key/value pair object containing the data to send
+		 *    to the server
+		 *  @param {function} fnCallback to be called on completion of the data get
+		 *    process that will draw the data on the page.
+		 *  @param {object} oSettings DataTables settings object
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // POST data to server
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "xhr.php",
+		 *        "fnServerData": function ( sSource, aoData, fnCallback, oSettings ) {
+		 *          oSettings.jqXHR = $.ajax( {
+		 *            "dataType": 'json', 
+		 *            "type": "POST", 
+		 *            "url": sSource, 
+		 *            "data": aoData, 
+		 *            "success": fnCallback
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerData": function ( sUrl, aoData, fnCallback, oSettings ) {
+			oSettings.jqXHR = $.ajax( {
+				"url":  sUrl,
+				"data": aoData,
+				"success": function (json) {
+					if ( json.sError ) {
+						oSettings.oApi._fnLog( oSettings, 0, json.sError );
+					}
+					
+					$(oSettings.oInstance).trigger('xhr', [oSettings, json]);
+					fnCallback( json );
+				},
+				"dataType": "json",
+				"cache": false,
+				"type": oSettings.sServerMethod,
+				"error": function (xhr, error, thrown) {
+					if ( error == "parsererror" ) {
+						oSettings.oApi._fnLog( oSettings, 0, "DataTables warning: JSON data from "+
+							"server could not be parsed. This is caused by a JSON formatting error." );
+					}
+				}
+			} );
+		},
+	
+	
+		/**
+		 * It is often useful to send extra data to the server when making an Ajax
+		 * request - for example custom filtering information, and this callback
+		 * function makes it trivial to send extra information to the server. The
+		 * passed in parameter is the data set that has been constructed by
+		 * DataTables, and you can add to this or modify it as you require.
+		 *  @type function
+		 *  @param {array} aoData Data array (array of objects which are name/value
+		 *    pairs) that has been constructed by DataTables and will be sent to the
+		 *    server. In the case of Ajax sourced data with server-side processing
+		 *    this will be an empty array, for server-side processing there will be a
+		 *    significant number of parameters!
+		 *  @returns {undefined} Ensure that you modify the aoData array passed in,
+		 *    as this is passed by reference.
+		 *  @dtopt Callbacks
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bProcessing": true,
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "fnServerParams": function ( aoData ) {
+		 *          aoData.push( { "name": "more_data", "value": "my_value" } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnServerParams": null,
+	
+	
+		/**
+		 * Load the table state. With this function you can define from where, and how, the
+		 * state of a table is loaded. By default DataTables will load from its state saving
+		 * cookie, but you might wish to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @return {object} The DataTables state object to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoad": function (oSettings) {
+		 *          var o;
+		 *          
+		 *          // Send an Ajax request to the server to get the data. Note that
+		 *          // this is a synchronous request.
+		 *          $.ajax( {
+		 *            "url": "/state_load",
+		 *            "async": false,
+		 *            "dataType": "json",
+		 *            "success": function (json) {
+		 *              o = json;
+		 *            }
+		 *          } );
+		 *          
+		 *          return o;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoad": function ( oSettings ) {
+			var sData = this.oApi._fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
+			var oData;
+	
+			try {
+				oData = (typeof $.parseJSON === 'function') ? 
+					$.parseJSON(sData) : eval( '('+sData+')' );
+			} catch (e) {
+				oData = null;
+			}
+	
+			return oData;
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the saved state prior to loading that state.
+		 * This callback is called when the table is loading state from the stored data, but
+		 * prior to the settings object being modified by the saved state. Note that for 
+		 * plug-in authors, you should use the 'stateLoadParams' event to load parameters for 
+		 * a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that is to be loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never loaded
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Disallow state loading by returning false
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoadParams": function (oSettings, oData) {
+		 *          return false;
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoadParams": null,
+	
+	
+		/**
+		 * Callback that is called when the state has been loaded from the state saving method
+		 * and the DataTables settings object has been modified as a result of the loaded state.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object that was loaded
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Show an alert with the filtering value that was saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateLoaded": function (oSettings, oData) {
+		 *          alert( 'Saved filter was: '+oData.oSearch.sSearch );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateLoaded": null,
+	
+	
+		/**
+		 * Save the table state. This function allows you to define where and how the state
+		 * information for the table is stored - by default it will use a cookie, but you
+		 * might want to use local storage (HTML5) or a server-side database.
+		 *  @type function
+		 *  @member
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSave": function (oSettings, oData) {
+		 *          // Send an Ajax request to the server with the state object
+		 *          $.ajax( {
+		 *            "url": "/state_save",
+		 *            "data": oData,
+		 *            "dataType": "json",
+		 *            "method": "POST"
+		 *            "success": function () {}
+		 *          } );
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSave": function ( oSettings, oData ) {
+			this.oApi._fnCreateCookie( 
+				oSettings.sCookiePrefix+oSettings.sInstance, 
+				this.oApi._fnJsonString(oData), 
+				oSettings.iCookieDuration, 
+				oSettings.sCookiePrefix, 
+				oSettings.fnCookieCallback
+			);
+		},
+	
+	
+		/**
+		 * Callback which allows modification of the state to be saved. Called when the table 
+		 * has changed state a new state save is required. This method allows modification of
+		 * the state saving object prior to actually doing the save, including addition or 
+		 * other state properties or modification. Note that for plug-in authors, you should 
+		 * use the 'stateSaveParams' event to save parameters for a plug-in.
+		 *  @type function
+		 *  @param {object} oSettings DataTables settings object
+		 *  @param {object} oData The state object to be saved
+		 *  @dtopt Callbacks
+		 * 
+		 *  @example
+		 *    // Remove a saved filter, so filtering is never saved
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bStateSave": true,
+		 *        "fnStateSaveParams": function (oSettings, oData) {
+		 *          oData.oSearch.sSearch = "";
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"fnStateSaveParams": null,
+	
+	
+		/**
+		 * Duration of the cookie which is used for storing session information. This
+		 * value is given in seconds.
+		 *  @type int
+		 *  @default 7200 <i>(2 hours)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iCookieDuration": 60*60*24; // 1 day
+		 *      } );
+		 *    } )
+		 */
+		"iCookieDuration": 7200,
+	
+	
+		/**
+		 * When enabled DataTables will not make a request to the server for the first
+		 * page draw - rather it will use the data already on the page (no sorting etc
+		 * will be applied to it), thus saving on an XHR at load time. iDeferLoading
+		 * is used to indicate that deferred loading is required, but it is also used
+		 * to tell DataTables how many records there are in the full table (allowing
+		 * the information element and pagination to be displayed correctly). In the case
+		 * where a filtering is applied to the table on initial load, this can be
+		 * indicated by giving the parameter as an array, where the first element is
+		 * the number of records available after filtering and the second element is the
+		 * number of records without filtering (allowing the table information element
+		 * to be shown correctly).
+		 *  @type int | array
+		 *  @default null
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    // 57 records available in the table, no filtering applied
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": 57
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // 57 records after filtering, 100 without filtering (an initial filter applied)
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/server_processing.php",
+		 *        "iDeferLoading": [ 57, 100 ],
+		 *        "oSearch": {
+		 *          "sSearch": "my_filter"
+		 *        }
+		 *      } );
+		 *    } );
+		 */
+		"iDeferLoading": null,
+	
+	
+		/**
+		 * Number of rows to display on a single page when using pagination. If
+		 * feature enabled (bLengthChange) then the end user will be able to override
+		 * this to a custom setting using a pop-up menu.
+		 *  @type int
+		 *  @default 10
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayLength": 50
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayLength": 10,
+	
+	
+		/**
+		 * Define the starting point for data display when using DataTables with
+		 * pagination. Note that this parameter is the number of records, rather than
+		 * the page number, so if you have 10 records per page and want to start on
+		 * the third page, it should be "20".
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iDisplayStart": 20
+		 *      } );
+		 *    } )
+		 */
+		"iDisplayStart": 0,
+	
+	
+		/**
+		 * The scroll gap is the amount of scrolling that is left to go before
+		 * DataTables will load the next 'page' of data automatically. You typically
+		 * want a gap which is big enough that the scrolling will be smooth for the
+		 * user, while not so large that it will load more data than need.
+		 *  @type int
+		 *  @default 100
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bScrollInfinite": true,
+		 *        "bScrollCollapse": true,
+		 *        "sScrollY": "200px",
+		 *        "iScrollLoadGap": 50
+		 *      } );
+		 *    } );
+		 */
+		"iScrollLoadGap": 100,
+	
+	
+		/**
+		 * By default DataTables allows keyboard navigation of the table (sorting, paging,
+		 * and filtering) by adding a tabindex attribute to the required elements. This
+		 * allows you to tab through the controls and press the enter key to activate them.
+		 * The tabindex is default 0, meaning that the tab follows the flow of the document.
+		 * You can overrule this using this parameter if you wish. Use a value of -1 to
+		 * disable built-in keyboard navigation.
+		 *  @type int
+		 *  @default 0
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "iTabIndex": 1
+		 *      } );
+		 *    } );
+		 */
+		"iTabIndex": 0,
+	
+	
+		/**
+		 * All strings that DataTables uses in the user interface that it creates
+		 * are defined in this object, allowing you to modified them individually or
+		 * completely replace them all as required.
+		 *  @namespace
+		 */
+		"oLanguage": {
+			/**
+			 * Strings that are used for WAI-ARIA labels and controls only (these are not
+			 * actually visible on the page, but will be read by screenreaders, and thus
+			 * must be internationalised as well).
+			 *  @namespace
+			 */
+			"oAria": {
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted ascending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortAscending": " - click/return to sort ascending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortAscending": ": activate to sort column ascending",
+	
+				/**
+				 * ARIA label that is added to the table headers when the column may be
+				 * sorted descending by activing the column (click or return when focused).
+				 * Note that the column header is prefixed to this string.
+				 *  @type string
+				 *  @default : activate to sort column ascending
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oAria": {
+				 *            "sSortDescending": " - click/return to sort descending"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sSortDescending": ": activate to sort column descending"
+			},
+	
+			/**
+			 * Pagination string used by DataTables for the two built-in pagination
+			 * control types ("two_button" and "full_numbers")
+			 *  @namespace
+			 */
+			"oPaginate": {
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the first page.
+				 *  @type string
+				 *  @default First
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sFirst": "First page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sFirst": "First",
+			
+			
+				/**
+				 * Text to use when using the 'full_numbers' type of pagination for the
+				 * button to take the user to the last page.
+				 *  @type string
+				 *  @default Last
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sLast": "Last page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sLast": "Last",
+			
+			
+				/**
+				 * Text to use for the 'next' pagination button (to take the user to the 
+				 * next page).
+				 *  @type string
+				 *  @default Next
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sNext": "Next page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sNext": "Next",
+			
+			
+				/**
+				 * Text to use for the 'previous' pagination button (to take the user to  
+				 * the previous page).
+				 *  @type string
+				 *  @default Previous
+				 *  @dtopt Language
+				 * 
+				 *  @example
+				 *    $(document).ready( function() {
+				 *      $('#example').dataTable( {
+				 *        "oLanguage": {
+				 *          "oPaginate": {
+				 *            "sPrevious": "Previous page"
+				 *          }
+				 *        }
+				 *      } );
+				 *    } );
+				 */
+				"sPrevious": "Previous"
+			},
+		
+			/**
+			 * This string is shown in preference to sZeroRecords when the table is
+			 * empty of data (regardless of filtering). Note that this is an optional
+			 * parameter - if it is not given, the value of sZeroRecords will be used
+			 * instead (either the default or given value).
+			 *  @type string
+			 *  @default No data available in table
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sEmptyTable": "No data available in table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sEmptyTable": "No data available in table",
+		
+		
+			/**
+			 * This string gives information to the end user about the information that 
+			 * is current on display on the page. The _START_, _END_ and _TOTAL_ 
+			 * variables are all dynamically replaced as the table display updates, and 
+			 * can be freely moved or removed as the language requirements change.
+			 *  @type string
+			 *  @default Showing _START_ to _END_ of _TOTAL_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfo": "Got a total of _TOTAL_ entries to show (_START_ to _END_)"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
+		
+		
+			/**
+			 * Display information string for when the table is empty. Typically the 
+			 * format of this string should match sInfo.
+			 *  @type string
+			 *  @default Showing 0 to 0 of 0 entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoEmpty": "No entries to show"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoEmpty": "Showing 0 to 0 of 0 entries",
+		
+		
+			/**
+			 * When a user filters the information in a table, this string is appended 
+			 * to the information (sInfo) to give an idea of how strong the filtering 
+			 * is. The variable _MAX_ is dynamically updated.
+			 *  @type string
+			 *  @default (filtered from _MAX_ total entries)
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoFiltered": " - filtering from _MAX_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoFiltered": "(filtered from _MAX_ total entries)",
+		
+		
+			/**
+			 * If can be useful to append extra information to the info string at times,
+			 * and this variable does exactly that. This information will be appended to
+			 * the sInfo (sInfoEmpty and sInfoFiltered in whatever combination they are
+			 * being used) at all times.
+			 *  @type string
+			 *  @default <i>Empty string</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoPostFix": "All records shown are derived from real information."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoPostFix": "",
+		
+		
+			/**
+			 * DataTables has a build in number formatter (fnFormatNumber) which is used
+			 * to format large numbers that are used in the table information. By
+			 * default a comma is used, but this can be trivially changed to any
+			 * character you wish with this parameter.
+			 *  @type string
+			 *  @default ,
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sInfoThousands": "'"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sInfoThousands": ",",
+		
+		
+			/**
+			 * Detail the action that will be taken when the drop down menu for the
+			 * pagination length option is changed. The '_MENU_' variable is replaced
+			 * with a default select list of 10, 25, 50 and 100, and can be replaced
+			 * with a custom select box if required.
+			 *  @type string
+			 *  @default Show _MENU_ entries
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Language change only
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": "Display _MENU_ records"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Language and options change
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLengthMenu": 'Display <select>'+
+			 *            '<option value="10">10</option>'+
+			 *            '<option value="20">20</option>'+
+			 *            '<option value="30">30</option>'+
+			 *            '<option value="40">40</option>'+
+			 *            '<option value="50">50</option>'+
+			 *            '<option value="-1">All</option>'+
+			 *            '</select> records'
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLengthMenu": "Show _MENU_ entries",
+		
+		
+			/**
+			 * When using Ajax sourced data and during the first draw when DataTables is
+			 * gathering the data, this message is shown in an empty row in the table to
+			 * indicate to the end user the the data is being loaded. Note that this
+			 * parameter is not used when loading data by server-side processing, just
+			 * Ajax sourced data with client-side processing.
+			 *  @type string
+			 *  @default Loading...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sLoadingRecords": "Please wait - loading..."
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sLoadingRecords": "Loading...",
+		
+		
+			/**
+			 * Text which is displayed when the table is processing a user action
+			 * (usually a sort command or similar).
+			 *  @type string
+			 *  @default Processing...
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sProcessing": "DataTables is currently busy"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sProcessing": "Processing...",
+		
+		
+			/**
+			 * Details the actions that will be taken when the user types into the
+			 * filtering input text box. The variable "_INPUT_", if used in the string,
+			 * is replaced with the HTML text box for the filtering input allowing
+			 * control over where it appears in the string. If "_INPUT_" is not given
+			 * then the input box is appended to the string automatically.
+			 *  @type string
+			 *  @default Search:
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    // Input text box will be appended at the end automatically
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Filter records:"
+			 *        }
+			 *      } );
+			 *    } );
+			 *    
+			 *  @example
+			 *    // Specify where the filter should appear
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sSearch": "Apply filter _INPUT_ to table"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sSearch": "Search:",
+		
+		
+			/**
+			 * All of the language information can be stored in a file on the
+			 * server-side, which DataTables will look up if this parameter is passed.
+			 * It must store the URL of the language file, which is in a JSON format,
+			 * and the object has the same properties as the oLanguage object in the
+			 * initialiser object (i.e. the above parameters). Please refer to one of
+			 * the example language files to see how this works in action.
+			 *  @type string
+			 *  @default <i>Empty string - i.e. disabled</i>
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sUrl": "http://www.sprymedia.co.uk/dataTables/lang.txt"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sUrl": "",
+		
+		
+			/**
+			 * Text shown inside the table records when the is no information to be
+			 * displayed after filtering. sEmptyTable is shown when there is simply no
+			 * information in the table at all (regardless of filtering).
+			 *  @type string
+			 *  @default No matching records found
+			 *  @dtopt Language
+			 * 
+			 *  @example
+			 *    $(document).ready( function() {
+			 *      $('#example').dataTable( {
+			 *        "oLanguage": {
+			 *          "sZeroRecords": "No records to display"
+			 *        }
+			 *      } );
+			 *    } );
+			 */
+			"sZeroRecords": "No matching records found"
+		},
+	
+	
+		/**
+		 * This parameter allows you to have define the global filtering state at
+		 * initialisation time. As an object the "sSearch" parameter must be
+		 * defined, but all other parameters are optional. When "bRegex" is true,
+		 * the search string will be treated as a regular expression, when false
+		 * (default) it will be treated as a straight string. When "bSmart"
+		 * DataTables will use it's smart filtering methods (to word match at
+		 * any point in the data), when false this will not be done.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "oSearch": {"sSearch": "Initial search"}
+		 *      } );
+		 *    } )
+		 */
+		"oSearch": $.extend( {}, DataTable.models.oSearch ),
+	
+	
+		/**
+		 * By default DataTables will look for the property 'aaData' when obtaining
+		 * data from an Ajax source or for server-side processing - this parameter
+		 * allows that property to be changed. You can use Javascript dotted object
+		 * notation to get a data source for multiple levels of nesting.
+		 *  @type string
+		 *  @default aaData
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    // Get data from { "data": [...] }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data"
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Get data from { "data": { "inner": [...] } }
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/data.txt",
+		 *        "sAjaxDataProp": "data.inner"
+		 *      } );
+		 *    } );
+		 */
+		"sAjaxDataProp": "aaData",
+	
+	
+		/**
+		 * You can instruct DataTables to load data from an external source using this
+		 * parameter (use aData if you want to pass data in you already have). Simply
+		 * provide a url a JSON object can be obtained from. This object must include
+		 * the parameter 'aaData' which is the data source for the table.
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sAjaxSource": "http://www.sprymedia.co.uk/dataTables/json.php"
+		 *      } );
+		 *    } )
+		 */
+		"sAjaxSource": null,
+	
+	
+		/**
+		 * This parameter can be used to override the default prefix that DataTables
+		 * assigns to a cookie when state saving is enabled.
+		 *  @type string
+		 *  @default SpryMedia_DataTables_
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sCookiePrefix": "my_datatable_",
+		 *      } );
+		 *    } );
+		 */
+		"sCookiePrefix": "SpryMedia_DataTables_",
+	
+	
+		/**
+		 * This initialisation variable allows you to specify exactly where in the
+		 * DOM you want DataTables to inject the various controls it adds to the page
+		 * (for example you might want the pagination controls at the top of the
+		 * table). DIV elements (with or without a custom class) can also be added to
+		 * aid styling. The follow syntax is used:
+		 *   <ul>
+		 *     <li>The following options are allowed:	
+		 *       <ul>
+		 *         <li>'l' - Length changing</li
+		 *         <li>'f' - Filtering input</li>
+		 *         <li>'t' - The table!</li>
+		 *         <li>'i' - Information</li>
+		 *         <li>'p' - Pagination</li>
+		 *         <li>'r' - pRocessing</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following constants are allowed:
+		 *       <ul>
+		 *         <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li>
+		 *         <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>The following syntax is expected:
+		 *       <ul>
+		 *         <li>'<' and '>' - div elements</li>
+		 *         <li>'<"class" and '>' - div with a class</li>
+		 *         <li>'<"#id" and '>' - div with an ID</li>
+		 *       </ul>
+		 *     </li>
+		 *     <li>Examples:
+		 *       <ul>
+		 *         <li>'<"wrapper"flipt>'</li>
+		 *         <li>'<lf<t>ip>'</li>
+		 *       </ul>
+		 *     </li>
+		 *   </ul>
+		 *  @type string
+		 *  @default lfrtip <i>(when bJQueryUI is false)</i> <b>or</b> 
+		 *    <"H"lfr>t<"F"ip> <i>(when bJQueryUI is true)</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sDom": '<"top"i>rt<"bottom"flp><"clear">'
+		 *      } );
+		 *    } );
+		 */
+		"sDom": "lfrtip",
+	
+	
+		/**
+		 * DataTables features two different built-in pagination interaction methods
+		 * ('two_button' or 'full_numbers') which present different page controls to
+		 * the end user. Further methods can be added using the API (see below).
+		 *  @type string
+		 *  @default two_button
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sPaginationType": "full_numbers"
+		 *      } );
+		 *    } )
+		 */
+		"sPaginationType": "two_button",
+	
+	
+		/**
+		 * Enable horizontal scrolling. When a table is too wide to fit into a certain
+		 * layout, or you have a large number of columns in the table, you can enable
+		 * x-scrolling to show the table in a viewport, which can be scrolled. This
+		 * property can be any CSS unit, or a number (in which case it will be treated
+		 * as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "bScrollCollapse": true
+		 *      } );
+		 *    } );
+		 */
+		"sScrollX": "",
+	
+	
+		/**
+		 * This property can be used to force a DataTable to use more width than it
+		 * might otherwise do when x-scrolling is enabled. For example if you have a
+		 * table which requires to be well spaced, this parameter is useful for
+		 * "over-sizing" the table, and thus forcing scrolling. This property can by
+		 * any CSS unit, or a number (in which case it will be treated as a pixel
+		 * measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Options
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollX": "100%",
+		 *        "sScrollXInner": "110%"
+		 *      } );
+		 *    } );
+		 */
+		"sScrollXInner": "",
+	
+	
+		/**
+		 * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
+		 * to the given height, and enable scrolling for any data which overflows the
+		 * current viewport. This can be used as an alternative to paging to display
+		 * a lot of data in a small area (although paging and scrolling can both be
+		 * enabled at the same time). This property can be any CSS unit, or a number
+		 * (in which case it will be treated as a pixel measurement).
+		 *  @type string
+		 *  @default <i>blank string - i.e. disabled</i>
+		 *  @dtopt Features
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "sScrollY": "200px",
+		 *        "bPaginate": false
+		 *      } );
+		 *    } );
+		 */
+		"sScrollY": "",
+	
+	
+		/**
+		 * Set the HTTP method that is used to make the Ajax call for server-side
+		 * processing or Ajax sourced data.
+		 *  @type string
+		 *  @default GET
+		 *  @dtopt Options
+		 *  @dtopt Server-side
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "bServerSide": true,
+		 *        "sAjaxSource": "scripts/post.php",
+		 *        "sServerMethod": "POST"
+		 *      } );
+		 *    } );
+		 */
+		"sServerMethod": "GET"
+	};
+	
+	
+	
+	/**
+	 * Column options that can be given to DataTables at initialisation time.
+	 *  @namespace
+	 */
+	DataTable.defaults.columns = {
+		/**
+		 * Allows a column's sorting to take multiple columns into account when 
+		 * doing a sort. For example first name / last name columns make sense to 
+		 * do a multi-column sort over the two columns.
+		 *  @type array
+		 *  @default null <i>Takes the value of the column index automatically</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "aDataSort": [ 0, 1 ], "aTargets": [ 0 ] },
+		 *          { "aDataSort": [ 1, 0 ], "aTargets": [ 1 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ], "aTargets": [ 2 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          { "aDataSort": [ 0, 1 ] },
+		 *          { "aDataSort": [ 1, 0 ] },
+		 *          { "aDataSort": [ 2, 3, 4 ] },
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"aDataSort": null,
+	
+	
+		/**
+		 * You can control the default sorting direction, and even alter the behaviour
+		 * of the sort handler (i.e. only allow ascending sorting etc) using this
+		 * parameter.
+		 *  @type array
+		 *  @default [ 'asc', 'desc' ]
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "asSorting": [ "asc" ], "aTargets": [ 1 ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ], "aTargets": [ 2 ] },
+		 *          { "asSorting": [ "desc" ], "aTargets": [ 3 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          { "asSorting": [ "asc" ] },
+		 *          { "asSorting": [ "desc", "asc", "asc" ] },
+		 *          { "asSorting": [ "desc" ] },
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"asSorting": [ 'asc', 'desc' ],
+	
+	
+		/**
+		 * Enable or disable filtering on the data in this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSearchable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSearchable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSearchable": true,
+	
+	
+		/**
+		 * Enable or disable sorting on this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bSortable": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bSortable": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bSortable": true,
+	
+	
+		/**
+		 * <code>Deprecated</code> When using fnRender() for a column, you may wish 
+		 * to use the original data (before rendering) for sorting and filtering 
+		 * (the default is to used the rendered data that the user can see). This 
+		 * may be useful for dates etc.
+		 * 
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"bUseRendered": true,
+	
+	
+		/**
+		 * Enable or disable the display of this column.
+		 *  @type boolean
+		 *  @default true
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "bVisible": false, "aTargets": [ 0 ] }
+		 *        ] } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "bVisible": false },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ] } );
+		 *    } );
+		 */
+		"bVisible": true,
+		
+		
+		/**
+		 * Developer definable function that is called whenever a cell is created (Ajax source,
+		 * etc) or processed for input (DOM source). This can be used as a compliment to mRender
+		 * allowing you to modify the DOM element (add background colour for example) when the
+		 * element is available.
+		 *  @type function
+		 *  @param {element} nTd The TD node that has been created
+		 *  @param {*} sData The Data for the cell
+		 *  @param {array|object} oData The data for the whole row
+		 *  @param {int} iRow The row index for the aoData data store
+		 *  @param {int} iCol The column index for aoColumns
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [3],
+		 *          "fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
+		 *            if ( sData == "1.7" ) {
+		 *              $(nTd).css('color', 'blue')
+		 *            }
+		 *          }
+		 *        } ]
+		 *      });
+		 *    } );
+		 */
+		"fnCreatedCell": null,
+	
+	
+		/**
+		 * <code>Deprecated</code> Custom display function that will be called for the 
+		 * display of each cell in this column.
+		 *
+		 * Please note that this option has now been deprecated and will be removed
+		 * in the next version of DataTables. Please use mRender / mData rather than
+		 * fnRender.
+		 *  @type function
+		 *  @param {object} o Object with the following parameters:
+		 *  @param {int}    o.iDataRow The row in aoData
+		 *  @param {int}    o.iDataColumn The column in question
+		 *  @param {array}  o.aData The data for the row in question
+		 *  @param {object} o.oSettings The settings object for this DataTables instance
+		 *  @param {object} o.mDataProp The data property used for this column
+		 *  @param {*}      val The current cell value
+		 *  @returns {string} The string you which to use in the display
+		 *  @dtopt Columns
+		 *  @deprecated
+		 */
+		"fnRender": null,
+	
+	
+		/**
+		 * The column index (starting from 0!) that you wish a sort to be performed
+		 * upon when this column is selected for sorting. This can be used for sorting
+		 * on hidden columns for example.
+		 *  @type int
+		 *  @default -1 <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "iDataSort": 1, "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "iDataSort": 1 },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"iDataSort": -1,
+	
+	
+		/**
+		 * This parameter has been replaced by mData in DataTables to ensure naming
+		 * consistency. mDataProp can still be used, as there is backwards compatibility
+		 * in DataTables for this option, but it is strongly recommended that you use
+		 * mData in preference to mDataProp.
+		 *  @name DataTable.defaults.columns.mDataProp
+		 */
+	
+	
+		/**
+		 * This property can be used to read data from any JSON data source property,
+		 * including deeply nested objects / properties. mData can be given in a
+		 * number of different ways which effect its behaviour:
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source.</li>
+		 *     <li>null - the sDefaultContent option will be used for the cell (null
+		 *       by default, so you will need to specify the default content you want -
+		 *       typically an empty string). This can be useful on generated columns such 
+		 *       as edit / delete action columns.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row</li>
+		 *         <li>{string} The type call data requested - this will be 'set' when
+		 *           setting data or 'filter', 'display', 'type', 'sort' or undefined when 
+		 *           gathering data. Note that when <i>undefined</i> is given for the type
+		 *           DataTables expects to get the raw data for the object back</li>
+		 *         <li>{*} Data to set when the second parameter is 'set'.</li>
+		 *       </ul>
+		 *       The return value from the function is not required when 'set' is the type
+		 *       of call, but otherwise the return is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *
+		 * Note that prior to DataTables 1.9.2 mData was called mDataProp. The name change
+		 * reflects the flexibility of this property and is consistent with the naming of
+		 * mRender. If 'mDataProp' is given, then it will still be used by DataTables, as
+		 * it automatically maps the old name to the new if required.
+		 *  @type string|int|function|null
+		 *  @default null <i>Use automatically calculated column index</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Read table data from objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          { "mData": "platform.inner" },
+		 *          { "mData": "platform.details.0" },
+		 *          { "mData": "platform.details.1" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Using mData as a function to provide different information for
+		 *    // sorting, filtering and display. In this case, currency (price)
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": function ( source, type, val ) {
+		 *            if (type === 'set') {
+		 *              source.price = val;
+		 *              // Store the computed dislay and filter values for efficiency
+		 *              source.price_display = val=="" ? "" : "$"+numberFormat(val);
+		 *              source.price_filter  = val=="" ? "" : "$"+numberFormat(val)+" "+val;
+		 *              return;
+		 *            }
+		 *            else if (type === 'display') {
+		 *              return source.price_display;
+		 *            }
+		 *            else if (type === 'filter') {
+		 *              return source.price_filter;
+		 *            }
+		 *            // 'sort', 'type' and undefined all just use the integer
+		 *            return source.price;
+		 *          }
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"mData": null,
+	
+	
+		/**
+		 * This property is the rendering partner to mData and it is suggested that
+		 * when you want to manipulate data for display (including filtering, sorting etc)
+		 * but not altering the underlying data for the table, use this property. mData
+		 * can actually do everything this property can and more, but this parameter is
+		 * easier to use since there is no 'set' option. Like mData is can be given
+		 * in a number of different ways to effect its behaviour, with the addition of 
+		 * supporting array syntax for easy outputting of arrays (including arrays of
+		 * objects):
+		 *   <ul>
+		 *     <li>integer - treated as an array index for the data source. This is the
+		 *       default that DataTables uses (incrementally increased for each column).</li>
+		 *     <li>string - read an object property from the data source. Note that you can
+		 *       use Javascript dotted notation to read deep properties / arrays from the
+		 *       data source and also array brackets to indicate that the data reader should
+		 *       loop over the data source array. When characters are given between the array
+		 *       brackets, these characters are used to join the data source array together.
+		 *       For example: "accounts[, ].name" would result in a comma separated list with
+		 *       the 'name' value from the 'accounts' array of objects.</li>
+		 *     <li>function - the function given will be executed whenever DataTables 
+		 *       needs to set or get the data for a cell in the column. The function 
+		 *       takes three parameters:
+		 *       <ul>
+		 *         <li>{array|object} The data source for the row (based on mData)</li>
+		 *         <li>{string} The type call data requested - this will be 'filter', 'display', 
+		 *           'type' or 'sort'.</li>
+		 *         <li>{array|object} The full data source for the row (not based on mData)</li>
+		 *       </ul>
+		 *       The return value from the function is what will be used for the data
+		 *       requested.</li>
+		 *    </ul>
+		 *  @type string|int|function|null
+		 *  @default null <i>Use mData</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Create a comma separated list from an array of objects
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "sAjaxSource": "sources/deep.txt",
+		 *        "aoColumns": [
+		 *          { "mData": "engine" },
+		 *          { "mData": "browser" },
+		 *          {
+		 *            "mData": "platform",
+		 *            "mRender": "[, ].name"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 * 
+		 *  @example
+		 *    // Use as a function to create a link from the data source
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *        {
+		 *          "aTargets": [ 0 ],
+		 *          "mData": "download_link",
+		 *          "mRender": function ( data, type, full ) {
+		 *            return '<a href="'+data+'">Download</a>';
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"mRender": null,
+	
+	
+		/**
+		 * Change the cell type created for the column - either TD cells or TH cells. This
+		 * can be useful as TH cells have semantic meaning in the table body, allowing them
+		 * to act as a header for a row (you may wish to add scope='row' to the TH elements).
+		 *  @type string
+		 *  @default td
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Make the first column use TH cells
+		 *    $(document).ready( function() {
+		 *      var oTable = $('#example').dataTable( {
+		 *        "aoColumnDefs": [ {
+		 *          "aTargets": [ 0 ],
+		 *          "sCellType": "th"
+		 *        } ]
+		 *      } );
+		 *    } );
+		 */
+		"sCellType": "td",
+	
+	
+		/**
+		 * Class to give to each cell in this column.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sClass": "my_class", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sClass": "my_class" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sClass": "",
+		
+		/**
+		 * When DataTables calculates the column widths to assign to each column,
+		 * it finds the longest string in each column and then constructs a
+		 * temporary table and reads the widths from that. The problem with this
+		 * is that "mmm" is much wider then "iiii", but the latter is a longer 
+		 * string - thus the calculation can go wrong (doing it properly and putting
+		 * it into an DOM object and measuring that is horribly(!) slow). Thus as
+		 * a "work around" we provide this option. It will append its value to the
+		 * text that is found to be the longest string for the column - i.e. padding.
+		 * Generally you shouldn't need this, and it is not documented on the 
+		 * general DataTables.net documentation
+		 *  @type string
+		 *  @default <i>Empty string<i>
+		 *  @dtopt Columns
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "sContentPadding": "mmm"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sContentPadding": "",
+	
+	
+		/**
+		 * Allows a default value to be given for a column's data, and will be used
+		 * whenever a null data source is encountered (this can be because mData
+		 * is set to null, or because the data source itself is null).
+		 *  @type string
+		 *  @default null
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit",
+		 *            "aTargets": [ -1 ]
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          {
+		 *            "mData": null,
+		 *            "sDefaultContent": "Edit"
+		 *          }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sDefaultContent": null,
+	
+	
+		/**
+		 * This parameter is only used in DataTables' server-side processing. It can
+		 * be exceptionally useful to know what columns are being displayed on the
+		 * client side, and to map these to database fields. When defined, the names
+		 * also allow DataTables to reorder information from the server if it comes
+		 * back in an unexpected order (i.e. if you switch your columns around on the
+		 * client-side, your server-side code does not also need updating).
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sName": "engine", "aTargets": [ 0 ] },
+		 *          { "sName": "browser", "aTargets": [ 1 ] },
+		 *          { "sName": "platform", "aTargets": [ 2 ] },
+		 *          { "sName": "version", "aTargets": [ 3 ] },
+		 *          { "sName": "grade", "aTargets": [ 4 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sName": "engine" },
+		 *          { "sName": "browser" },
+		 *          { "sName": "platform" },
+		 *          { "sName": "version" },
+		 *          { "sName": "grade" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sName": "",
+	
+	
+		/**
+		 * Defines a data source type for the sorting which can be used to read
+		 * real-time information from the table (updating the internally cached
+		 * version) prior to sorting. This allows sorting to occur on user editable
+		 * elements such as form inputs.
+		 *  @type string
+		 *  @default std
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [
+		 *          { "sSortDataType": "dom-text", "aTargets": [ 2, 3 ] },
+		 *          { "sType": "numeric", "aTargets": [ 3 ] },
+		 *          { "sSortDataType": "dom-select", "aTargets": [ 4 ] },
+		 *          { "sSortDataType": "dom-checkbox", "aTargets": [ 5 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [
+		 *          null,
+		 *          null,
+		 *          { "sSortDataType": "dom-text" },
+		 *          { "sSortDataType": "dom-text", "sType": "numeric" },
+		 *          { "sSortDataType": "dom-select" },
+		 *          { "sSortDataType": "dom-checkbox" }
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sSortDataType": "std",
+	
+	
+		/**
+		 * The title of this column.
+		 *  @type string
+		 *  @default null <i>Derived from the 'TH' value for this column in the 
+		 *    original HTML table.</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sTitle": "My column title", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sTitle": "My column title" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sTitle": null,
+	
+	
+		/**
+		 * The type allows you to specify how the data for this column will be sorted.
+		 * Four types (string, numeric, date and html (which will strip HTML tags
+		 * before sorting)) are currently available. Note that only date formats
+		 * understood by Javascript's Date() object will be accepted as type date. For
+		 * example: "Mar 26, 2008 5:03 PM". May take the values: 'string', 'numeric',
+		 * 'date' or 'html' (by default). Further types can be adding through
+		 * plug-ins.
+		 *  @type string
+		 *  @default null <i>Auto-detected from raw data</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sType": "html", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sType": "html" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sType": null,
+	
+	
+		/**
+		 * Defining the width of the column, this parameter may take any CSS value
+		 * (3em, 20px etc). DataTables apples 'smart' widths to columns which have not
+		 * been given a specific width through this interface ensuring that the table
+		 * remains readable.
+		 *  @type string
+		 *  @default null <i>Automatic</i>
+		 *  @dtopt Columns
+		 * 
+		 *  @example
+		 *    // Using aoColumnDefs
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumnDefs": [ 
+		 *          { "sWidth": "20%", "aTargets": [ 0 ] }
+		 *        ]
+		 *      } );
+		 *    } );
+		 *    
+		 *  @example
+		 *    // Using aoColumns
+		 *    $(document).ready( function() {
+		 *      $('#example').dataTable( {
+		 *        "aoColumns": [ 
+		 *          { "sWidth": "20%" },
+		 *          null,
+		 *          null,
+		 *          null,
+		 *          null
+		 *        ]
+		 *      } );
+		 *    } );
+		 */
+		"sWidth": null
+	};
+	
+	
+	
+	/**
+	 * DataTables settings object - this holds all the information needed for a
+	 * given table, including configuration, data and current application of the
+	 * table options. DataTables does not have a single instance for each DataTable
+	 * with the settings attached to that instance, but rather instances of the
+	 * DataTable "class" are created on-the-fly as needed (typically by a 
+	 * $().dataTable() call) and the settings object is then applied to that
+	 * instance.
+	 * 
+	 * Note that this object is related to {@link DataTable.defaults} but this 
+	 * one is the internal data store for DataTables's cache of columns. It should
+	 * NOT be manipulated outside of DataTables. Any configuration should be done
+	 * through the initialisation options.
+	 *  @namespace
+	 *  @todo Really should attach the settings object to individual instances so we
+	 *    don't need to create new instances on each $().dataTable() call (if the
+	 *    table already exists). It would also save passing oSettings around and
+	 *    into every single function. However, this is a very significant 
+	 *    architecture change for DataTables and will almost certainly break
+	 *    backwards compatibility with older installations. This is something that
+	 *    will be done in 2.0.
+	 */
+	DataTable.models.oSettings = {
+		/**
+		 * Primary features of DataTables and their enablement state.
+		 *  @namespace
+		 */
+		"oFeatures": {
+			
+			/**
+			 * Flag to say if DataTables should automatically try to calculate the
+			 * optimum table and columns widths (true) or not (false).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoWidth": null,
+	
+			/**
+			 * Delay the creation of TR and TD elements until they are actually
+			 * needed by a driven page draw. This can give a significant speed
+			 * increase for Ajax source and Javascript source data, but makes no
+			 * difference at all fro DOM and server-side processing tables.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bDeferRender": null,
+			
+			/**
+			 * Enable filtering on the table or not. Note that if this is disabled
+			 * then there is no filtering at all on the table, including fnFilter.
+			 * To just remove the filtering input use sDom and remove the 'f' option.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bFilter": null,
+			
+			/**
+			 * Table information element (the 'Showing x of y records' div) enable
+			 * flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfo": null,
+			
+			/**
+			 * Present a user control allowing the end user to change the page size
+			 * when pagination is enabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bLengthChange": null,
+	
+			/**
+			 * Pagination enabled or not. Note that if this is disabled then length
+			 * changing must also be disabled.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bPaginate": null,
+			
+			/**
+			 * Processing indicator enable flag whenever DataTables is enacting a
+			 * user request - typically an Ajax request for server-side processing.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bProcessing": null,
+			
+			/**
+			 * Server-side processing enabled flag - when enabled DataTables will
+			 * get all data from the server for every draw - there is no filtering,
+			 * sorting or paging done on the client-side.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bServerSide": null,
+			
+			/**
+			 * Sorting enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSort": null,
+			
+			/**
+			 * Apply a class to the columns which are being sorted to provide a
+			 * visual highlight or not. This can slow things down when enabled since
+			 * there is a lot of DOM interaction.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bSortClasses": null,
+			
+			/**
+			 * State saving enablement flag.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bStateSave": null
+		},
+		
+	
+		/**
+		 * Scrolling settings for a table.
+		 *  @namespace
+		 */
+		"oScroll": {
+			/**
+			 * Indicate if DataTables should be allowed to set the padding / margin
+			 * etc for the scrolling header elements or not. Typically you will want
+			 * this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bAutoCss": null,
+			
+			/**
+			 * When the table is shorter in height than sScrollY, collapse the
+			 * table container down to the height of the table (when true).
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bCollapse": null,
+			
+			/**
+			 * Infinite scrolling enablement flag. Now deprecated in favour of
+			 * using the Scroller plug-in.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type boolean
+			 */
+			"bInfinite": null,
+			
+			/**
+			 * Width of the scrollbar for the web-browser's platform. Calculated
+			 * during table initialisation.
+			 *  @type int
+			 *  @default 0
+			 */
+			"iBarWidth": 0,
+			
+			/**
+			 * Space (in pixels) between the bottom of the scrolling container and 
+			 * the bottom of the scrolling viewport before the next page is loaded
+			 * when using infinite scrolling.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type int
+			 */
+			"iLoadGap": null,
+			
+			/**
+			 * Viewport width for horizontal scrolling. Horizontal scrolling is 
+			 * disabled if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sX": null,
+			
+			/**
+			 * Width to expand the table to when using x-scrolling. Typically you
+			 * should not need to use this.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 *  @deprecated
+			 */
+			"sXInner": null,
+			
+			/**
+			 * Viewport height for vertical scrolling. Vertical scrolling is disabled
+			 * if an empty string.
+			 * Note that this parameter will be set by the initialisation routine. To
+			 * set a default use {@link DataTable.defaults}.
+			 *  @type string
+			 */
+			"sY": null
+		},
+		
+		/**
+		 * Language information for the table.
+		 *  @namespace
+		 *  @extends DataTable.defaults.oLanguage
+		 */
+		"oLanguage": {
+			/**
+			 * Information callback function. See 
+			 * {@link DataTable.defaults.fnInfoCallback}
+			 *  @type function
+			 *  @default null
+			 */
+			"fnInfoCallback": null
+		},
+		
+		/**
+		 * Browser support parameters
+		 *  @namespace
+		 */
+		"oBrowser": {
+			/**
+			 * Indicate if the browser incorrectly calculates width:100% inside a
+			 * scrolling element (IE6/7)
+			 *  @type boolean
+			 *  @default false
+			 */
+			"bScrollOversize": false
+		},
+		
+		/**
+		 * Array referencing the nodes which are used for the features. The 
+		 * parameters of this object match what is allowed by sDom - i.e.
+		 *   <ul>
+		 *     <li>'l' - Length changing</li>
+		 *     <li>'f' - Filtering input</li>
+		 *     <li>'t' - The table!</li>
+		 *     <li>'i' - Information</li>
+		 *     <li>'p' - Pagination</li>
+		 *     <li>'r' - pRocessing</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aanFeatures": [],
+		
+		/**
+		 * Store data information - see {@link DataTable.models.oRow} for detailed
+		 * information.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoData": [],
+		
+		/**
+		 * Array of indexes which are in the current display (after filtering etc)
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplay": [],
+		
+		/**
+		 * Array of indexes for display - no filtering
+		 *  @type array
+		 *  @default []
+		 */
+		"aiDisplayMaster": [],
+		
+		/**
+		 * Store information about each column that is in use
+		 *  @type array
+		 *  @default []
+		 */
+		"aoColumns": [],
+		
+		/**
+		 * Store information about the table's header
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeader": [],
+		
+		/**
+		 * Store information about the table's footer
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooter": [],
+		
+		/**
+		 * Search data array for regular expression searching
+		 *  @type array
+		 *  @default []
+		 */
+		"asDataSearch": [],
+		
+		/**
+		 * Store the applied global search information in case we want to force a 
+		 * research or compare the old search to a new one.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @namespace
+		 *  @extends DataTable.models.oSearch
+		 */
+		"oPreviousSearch": {},
+		
+		/**
+		 * Store the applied search for each column - see 
+		 * {@link DataTable.models.oSearch} for the format that is used for the
+		 * filtering information for each column.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreSearchCols": [],
+		
+		/**
+		 * Sorting that is applied to the table. Note that the inner arrays are
+		 * used in the following manner:
+		 * <ul>
+		 *   <li>Index 0 - column number</li>
+		 *   <li>Index 1 - current sorting direction</li>
+		 *   <li>Index 2 - index of asSorting for this column</li>
+		 * </ul>
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @todo These inner arrays should really be objects
+		 */
+		"aaSorting": null,
+		
+		/**
+		 * Sorting that is always applied to the table (i.e. prefixed in front of
+		 * aaSorting).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array|null
+		 *  @default null
+		 */
+		"aaSortingFixed": null,
+		
+		/**
+		 * Classes to use for the striping of a table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"asStripeClasses": null,
+		
+		/**
+		 * If restoring a table - we should restore its striping classes as well
+		 *  @type array
+		 *  @default []
+		 */
+		"asDestroyStripes": [],
+		
+		/**
+		 * If restoring a table - we should restore its width 
+		 *  @type int
+		 *  @default 0
+		 */
+		"sDestroyWidth": 0,
+		
+		/**
+		 * Callback functions array for every time a row is inserted (i.e. on a draw).
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCallback": [],
+		
+		/**
+		 * Callback functions for the header on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoHeaderCallback": [],
+		
+		/**
+		 * Callback function for the footer on each draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoFooterCallback": [],
+		
+		/**
+		 * Array of callback functions for draw callback functions
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDrawCallback": [],
+		
+		/**
+		 * Array of callback functions for row created function
+		 *  @type array
+		 *  @default []
+		 */
+		"aoRowCreatedCallback": [],
+		
+		/**
+		 * Callback functions for just before the table is redrawn. A return of 
+		 * false will be used to cancel the draw.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoPreDrawCallback": [],
+		
+		/**
+		 * Callback functions for when the table has been initialised.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoInitComplete": [],
+	
+		
+		/**
+		 * Callbacks for modifying the settings to be stored for state saving, prior to
+		 * saving state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSaveParams": [],
+		
+		/**
+		 * Callbacks for modifying the settings that have been stored for state saving
+		 * prior to using the stored values to restore the state.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoadParams": [],
+		
+		/**
+		 * Callbacks for operating on the settings object once the saved state has been
+		 * loaded
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoaded": [],
+		
+		/**
+		 * Cache the table ID for quick access
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sTableId": "",
+		
+		/**
+		 * The TABLE node for the main table
+		 *  @type node
+		 *  @default null
+		 */
+		"nTable": null,
+		
+		/**
+		 * Permanent ref to the thead element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTHead": null,
+		
+		/**
+		 * Permanent ref to the tfoot element - if it exists
+		 *  @type node
+		 *  @default null
+		 */
+		"nTFoot": null,
+		
+		/**
+		 * Permanent ref to the tbody element
+		 *  @type node
+		 *  @default null
+		 */
+		"nTBody": null,
+		
+		/**
+		 * Cache the wrapper node (contains all DataTables controlled elements)
+		 *  @type node
+		 *  @default null
+		 */
+		"nTableWrapper": null,
+		
+		/**
+		 * Indicate if when using server-side processing the loading of data 
+		 * should be deferred until the second draw.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDeferLoading": false,
+		
+		/**
+		 * Indicate if all required information has been read in
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bInitialised": false,
+		
+		/**
+		 * Information about open rows. Each object in the array has the parameters
+		 * 'nTr' and 'nParent'
+		 *  @type array
+		 *  @default []
+		 */
+		"aoOpenRows": [],
+		
+		/**
+		 * Dictate the positioning of DataTables' control elements - see
+		 * {@link DataTable.model.oInit.sDom}.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sDom": null,
+		
+		/**
+		 * Which type of pagination should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string 
+		 *  @default two_button
+		 */
+		"sPaginationType": "two_button",
+		
+		/**
+		 * The cookie duration (for bStateSave) in seconds.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type int
+		 *  @default 0
+		 */
+		"iCookieDuration": 0,
+		
+		/**
+		 * The cookie name prefix.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default <i>Empty string</i>
+		 */
+		"sCookiePrefix": "",
+		
+		/**
+		 * Callback function for cookie creation.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 *  @default null
+		 */
+		"fnCookieCallback": null,
+		
+		/**
+		 * Array of callback functions for state saving. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings
+		 *       and the JSON string to save that has been thus far created. Returns
+		 *       a JSON string to be inserted into a json object 
+		 *       (i.e. '"param": [ 0, 1, 2]')</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateSave": [],
+		
+		/**
+		 * Array of callback functions for state loading. Each array element is an 
+		 * object with the following parameters:
+		 *   <ul>
+		 *     <li>function:fn - function to call. Takes two parameters, oSettings 
+		 *       and the object stored. May return false to cancel state loading</li>
+		 *     <li>string:sName - name of callback</li>
+		 *   </ul>
+		 *  @type array
+		 *  @default []
+		 */
+		"aoStateLoad": [],
+		
+		/**
+		 * State that was loaded from the cookie. Useful for back reference
+		 *  @type object
+		 *  @default null
+		 */
+		"oLoadedState": null,
+		
+		/**
+		 * Source url for AJAX data for the table.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 *  @default null
+		 */
+		"sAjaxSource": null,
+		
+		/**
+		 * Property from a given object from which to read the table data from. This
+		 * can be an empty string (when not server-side processing), in which case 
+		 * it is  assumed an an array is given directly.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sAjaxDataProp": null,
+		
+		/**
+		 * Note if draw should be blocked while getting data
+		 *  @type boolean
+		 *  @default true
+		 */
+		"bAjaxDataGet": true,
+		
+		/**
+		 * The last jQuery XHR object that was used for server-side data gathering. 
+		 * This can be used for working with the XHR information in one of the 
+		 * callbacks
+		 *  @type object
+		 *  @default null
+		 */
+		"jqXHR": null,
+		
+		/**
+		 * Function to get the server-side data.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnServerData": null,
+		
+		/**
+		 * Functions which are called prior to sending an Ajax request so extra 
+		 * parameters can easily be sent to the server
+		 *  @type array
+		 *  @default []
+		 */
+		"aoServerParams": [],
+		
+		/**
+		 * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if 
+		 * required).
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type string
+		 */
+		"sServerMethod": null,
+		
+		/**
+		 * Format numbers for display.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type function
+		 */
+		"fnFormatNumber": null,
+		
+		/**
+		 * List of options that can be used for the user selectable length menu.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type array
+		 *  @default []
+		 */
+		"aLengthMenu": null,
+		
+		/**
+		 * Counter for the draws that the table does. Also used as a tracker for
+		 * server-side processing
+		 *  @type int
+		 *  @default 0
+		 */
+		"iDraw": 0,
+		
+		/**
+		 * Indicate if a redraw is being done - useful for Ajax
+		 *  @type boolean
+		 *  @default false
+		 */
+		"bDrawing": false,
+		
+		/**
+		 * Draw index (iDraw) of the last error when parsing the returned data
+		 *  @type int
+		 *  @default -1
+		 */
+		"iDrawError": -1,
+		
+		/**
+		 * Paging display length
+		 *  @type int
+		 *  @default 10
+		 */
+		"_iDisplayLength": 10,
+	
+		/**
+		 * Paging start point - aiDisplay index
+		 *  @type int
+		 *  @default 0
+		 */
+		"_iDisplayStart": 0,
+	
+		/**
+		 * Paging end point - aiDisplay index. Use fnDisplayEnd rather than
+		 * this property to get the end point
+		 *  @type int
+		 *  @default 10
+		 *  @private
+		 */
+		"_iDisplayEnd": 10,
+		
+		/**
+		 * Server-side processing - number of records in the result set
+		 * (i.e. before filtering), Use fnRecordsTotal rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type int
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsTotal": 0,
+	
+		/**
+		 * Server-side processing - number of records in the current display set
+		 * (i.e. after filtering). Use fnRecordsDisplay rather than
+		 * this property to get the value of the number of records, regardless of
+		 * the server-side processing setting.
+		 *  @type boolean
+		 *  @default 0
+		 *  @private
+		 */
+		"_iRecordsDisplay": 0,
+		
+		/**
+		 * Flag to indicate if jQuery UI marking and classes should be used.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bJUI": null,
+		
+		/**
+		 * The classes to use for the table
+		 *  @type object
+		 *  @default {}
+		 */
+		"oClasses": {},
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if filtering has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bFiltered": false,
+		
+		/**
+		 * Flag attached to the settings object so you can check in the draw 
+		 * callback if sorting has been done in the draw. Deprecated in favour of
+		 * events.
+		 *  @type boolean
+		 *  @default false
+		 *  @deprecated
+		 */
+		"bSorted": false,
+		
+		/**
+		 * Indicate that if multiple rows are in the header and there is more than 
+		 * one unique cell per column, if the top one (true) or bottom one (false) 
+		 * should be used for sorting / title by DataTables.
+		 * Note that this parameter will be set by the initialisation routine. To
+		 * set a default use {@link DataTable.defaults}.
+		 *  @type boolean
+		 */
+		"bSortCellsTop": null,
+		
+		/**
+		 * Initialisation object that is used for the table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInit": null,
+		
+		/**
+		 * Destroy callback functions - for plug-ins to attach themselves to the
+		 * destroy so they can clean up markup and events.
+		 *  @type array
+		 *  @default []
+		 */
+		"aoDestroyCallback": [],
+	
+		
+		/**
+		 * Get the number of records in the current record set, before filtering
+		 *  @type function
+		 */
+		"fnRecordsTotal": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsTotal, 10);
+			} else {
+				return this.aiDisplayMaster.length;
+			}
+		},
+		
+		/**
+		 * Get the number of records in the current record set, after filtering
+		 *  @type function
+		 */
+		"fnRecordsDisplay": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				return parseInt(this._iRecordsDisplay, 10);
+			} else {
+				return this.aiDisplay.length;
+			}
+		},
+		
+		/**
+		 * Set the display end point - aiDisplay index
+		 *  @type function
+		 *  @todo Should do away with _iDisplayEnd and calculate it on-the-fly here
+		 */
+		"fnDisplayEnd": function ()
+		{
+			if ( this.oFeatures.bServerSide ) {
+				if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {
+					return this._iDisplayStart+this.aiDisplay.length;
+				} else {
+					return Math.min( this._iDisplayStart+this._iDisplayLength, 
+						this._iRecordsDisplay );
+				}
+			} else {
+				return this._iDisplayEnd;
+			}
+		},
+		
+		/**
+		 * The DataTables object for this table
+		 *  @type object
+		 *  @default null
+		 */
+		"oInstance": null,
+		
+		/**
+		 * Unique identifier for each instance of the DataTables object. If there
+		 * is an ID on the table node, then it takes that value, otherwise an
+		 * incrementing internal counter is used.
+		 *  @type string
+		 *  @default null
+		 */
+		"sInstance": null,
+	
+		/**
+		 * tabindex attribute value that is added to DataTables control elements, allowing
+		 * keyboard navigation of the table and its controls.
+		 */
+		"iTabIndex": 0,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollHead": null,
+	
+		/**
+		 * DIV container for the footer scrolling table if scrolling
+		 */
+		"nScrollFoot": null
+	};
+
+	/**
+	 * Extension object for DataTables that is used to provide all extension options.
+	 * 
+	 * Note that the <i>DataTable.ext</i> object is available through
+	 * <i>jQuery.fn.dataTable.ext</i> where it may be accessed and manipulated. It is
+	 * also aliased to <i>jQuery.fn.dataTableExt</i> for historic reasons.
+	 *  @namespace
+	 *  @extends DataTable.models.ext
+	 */
+	DataTable.ext = $.extend( true, {}, DataTable.models.ext );
+	
+	$.extend( DataTable.ext.oStdClasses, {
+		"sTable": "dataTable",
+	
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "paginate_enabled_previous",
+		"sPagePrevDisabled": "paginate_disabled_previous",
+		"sPageNextEnabled": "paginate_enabled_next",
+		"sPageNextDisabled": "paginate_disabled_next",
+		"sPageJUINext": "",
+		"sPageJUIPrev": "",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "paginate_button",
+		"sPageButtonActive": "paginate_active",
+		"sPageButtonStaticDisabled": "paginate_button paginate_button_disabled",
+		"sPageFirst": "first",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last",
+		
+		/* Striping classes */
+		"sStripeOdd": "odd",
+		"sStripeEven": "even",
+		
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+		
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+		
+		/* Sorting */
+		"sSortAsc": "sorting_asc",
+		"sSortDesc": "sorting_desc",
+		"sSortable": "sorting", /* Sortable in both directions */
+		"sSortableAsc": "sorting_asc_disabled",
+		"sSortableDesc": "sorting_desc_disabled",
+		"sSortableNone": "sorting_disabled",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+		"sSortJUIAsc": "",
+		"sSortJUIDesc": "",
+		"sSortJUI": "",
+		"sSortJUIAscAllowed": "",
+		"sSortJUIDescAllowed": "",
+		"sSortJUIWrapper": "",
+		"sSortIcon": "",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": "",
+		"sJUIHeader": "",
+		"sJUIFooter": ""
+	} );
+	
+	
+	$.extend( DataTable.ext.oJUIClasses, DataTable.ext.oStdClasses, {
+		/* Two buttons buttons */
+		"sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",
+		"sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
+		"sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",
+		"sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
+		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
+		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",
+		
+		/* Full numbers paging buttons */
+		"sPageButton": "fg-button ui-button ui-state-default",
+		"sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",
+		"sPageFirst": "first ui-corner-tl ui-corner-bl",
+		"sPageLast": "last ui-corner-tr ui-corner-br",
+		
+		/* Features */
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+		
+		/* Sorting */
+		"sSortAsc": "ui-state-default",
+		"sSortDesc": "ui-state-default",
+		"sSortable": "ui-state-default",
+		"sSortableAsc": "ui-state-default",
+		"sSortableDesc": "ui-state-default",
+		"sSortableNone": "ui-state-default",
+		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
+		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
+		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
+		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
+		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",
+		"sSortJUIWrapper": "DataTables_sort_wrapper",
+		"sSortIcon": "DataTables_sort_icon",
+		
+		/* Scrolling */
+		"sScrollHead": "dataTables_scrollHead ui-state-default",
+		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
+		
+		/* Misc */
+		"sFooterTH": "ui-state-default",
+		"sJUIHeader": "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",
+		"sJUIFooter": "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"
+	} );
+	
+	/*
+	 * Variable: oPagination
+	 * Purpose:  
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	$.extend( DataTable.ext.oPagination, {
+		/*
+		 * Variable: two_button
+		 * Purpose:  Standard two button (forward/back) pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"two_button": {
+			/*
+			 * Function: oPagination.two_button.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with forward/back buttons only
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				var sAppend = (!oSettings.bJUI) ?
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sPrevious+'</a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button">'+oLang.sNext+'</a>'
+					:
+					'<a class="'+oSettings.oClasses.sPagePrevDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUIPrev+'"></span></a>'+
+					'<a class="'+oSettings.oClasses.sPageNextDisabled+'" tabindex="'+oSettings.iTabIndex+'" role="button"><span class="'+oSettings.oClasses.sPageJUINext+'"></span></a>';
+				$(nPaging).append( sAppend );
+				
+				var els = $('a', nPaging);
+				var nPrevious = els[0],
+					nNext = els[1];
+				
+				oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,     {action: "next"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nPrevious.id = oSettings.sTableId+'_previous';
+					nNext.id = oSettings.sTableId+'_next';
+	
+					nPrevious.setAttribute('aria-controls', oSettings.sTableId);
+					nNext.setAttribute('aria-controls', oSettings.sTableId);
+				}
+			},
+			
+			/*
+			 * Function: oPagination.two_button.fnUpdate
+			 * Purpose:  Update the two button pagination at the end of the draw
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var oClasses = oSettings.oClasses;
+				var an = oSettings.aanFeatures.p;
+				var nNode;
+	
+				/* Loop over each instance of the pager */
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i].firstChild;
+					if ( nNode )
+					{
+						/* Previous page */
+						nNode.className = ( oSettings._iDisplayStart === 0 ) ?
+						    oClasses.sPagePrevDisabled : oClasses.sPagePrevEnabled;
+						    
+						/* Next page */
+						nNode = nNode.nextSibling;
+						nNode.className = ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ?
+						    oClasses.sPageNextDisabled : oClasses.sPageNextEnabled;
+					}
+				}
+			}
+		},
+		
+		
+		/*
+		 * Variable: iFullNumbersShowPages
+		 * Purpose:  Change the number of pages which can be seen
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"iFullNumbersShowPages": 5,
+		
+		/*
+		 * Variable: full_numbers
+		 * Purpose:  Full numbers pagination
+		 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"full_numbers": {
+			/*
+			 * Function: oPagination.full_numbers.fnInit
+			 * Purpose:  Initialise dom elements required for pagination with a list of the pages
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           node:nPaging - the DIV which contains this pagination control
+			 *           function:fnCallbackDraw - draw function which must be called on update
+			 */
+			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )
+			{
+				var oLang = oSettings.oLanguage.oPaginate;
+				var oClasses = oSettings.oClasses;
+				var fnClickHandler = function ( e ) {
+					if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				};
+	
+				$(nPaging).append(
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageFirst+'">'+oLang.sFirst+'</a>'+
+					'<a  tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPagePrevious+'">'+oLang.sPrevious+'</a>'+
+					'<span></span>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageNext+'">'+oLang.sNext+'</a>'+
+					'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+" "+oClasses.sPageLast+'">'+oLang.sLast+'</a>'
+				);
+				var els = $('a', nPaging);
+				var nFirst = els[0],
+					nPrev = els[1],
+					nNext = els[2],
+					nLast = els[3];
+				
+				oSettings.oApi._fnBindAction( nFirst, {action: "first"},    fnClickHandler );
+				oSettings.oApi._fnBindAction( nPrev,  {action: "previous"}, fnClickHandler );
+				oSettings.oApi._fnBindAction( nNext,  {action: "next"},     fnClickHandler );
+				oSettings.oApi._fnBindAction( nLast,  {action: "last"},     fnClickHandler );
+				
+				/* ID the first elements only */
+				if ( !oSettings.aanFeatures.p )
+				{
+					nPaging.id = oSettings.sTableId+'_paginate';
+					nFirst.id =oSettings.sTableId+'_first';
+					nPrev.id =oSettings.sTableId+'_previous';
+					nNext.id =oSettings.sTableId+'_next';
+					nLast.id =oSettings.sTableId+'_last';
+				}
+			},
+			
+			/*
+			 * Function: oPagination.full_numbers.fnUpdate
+			 * Purpose:  Update the list of page buttons shows
+			 * Returns:  -
+			 * Inputs:   object:oSettings - dataTables settings object
+			 *           function:fnCallbackDraw - draw function to call on page change
+			 */
+			"fnUpdate": function ( oSettings, fnCallbackDraw )
+			{
+				if ( !oSettings.aanFeatures.p )
+				{
+					return;
+				}
+				
+				var iPageCount = DataTable.ext.oPagination.iFullNumbersShowPages;
+				var iPageCountHalf = Math.floor(iPageCount / 2);
+				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
+				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
+				var sList = "";
+				var iStartButton, iEndButton, i, iLen;
+				var oClasses = oSettings.oClasses;
+				var anButtons, anStatic, nPaginateList, nNode;
+				var an = oSettings.aanFeatures.p;
+				var fnBind = function (j) {
+					oSettings.oApi._fnBindAction( this, {"page": j+iStartButton-1}, function(e) {
+						/* Use the information in the element to jump to the required page */
+						oSettings.oApi._fnPageChange( oSettings, e.data.page );
+						fnCallbackDraw( oSettings );
+						e.preventDefault();
+					} );
+				};
+				
+				/* Pages calculation */
+				if ( oSettings._iDisplayLength === -1 )
+				{
+					iStartButton = 1;
+					iEndButton = 1;
+					iCurrentPage = 1;
+				}
+				else if (iPages < iPageCount)
+				{
+					iStartButton = 1;
+					iEndButton = iPages;
+				}
+				else if (iCurrentPage <= iPageCountHalf)
+				{
+					iStartButton = 1;
+					iEndButton = iPageCount;
+				}
+				else if (iCurrentPage >= (iPages - iPageCountHalf))
+				{
+					iStartButton = iPages - iPageCount + 1;
+					iEndButton = iPages;
+				}
+				else
+				{
+					iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
+					iEndButton = iStartButton + iPageCount - 1;
+				}
+	
+				
+				/* Build the dynamic list */
+				for ( i=iStartButton ; i<=iEndButton ; i++ )
+				{
+					sList += (iCurrentPage !== i) ?
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButton+'">'+oSettings.fnFormatNumber(i)+'</a>' :
+						'<a tabindex="'+oSettings.iTabIndex+'" class="'+oClasses.sPageButtonActive+'">'+oSettings.fnFormatNumber(i)+'</a>';
+				}
+				
+				/* Loop over each instance of the pager */
+				for ( i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					nNode = an[i];
+					if ( !nNode.hasChildNodes() )
+					{
+						continue;
+					}
+					
+					/* Build up the dynamic list first - html and listeners */
+					$('span:eq(0)', nNode)
+						.html( sList )
+						.children('a').each( fnBind );
+					
+					/* Update the permanent button's classes */
+					anButtons = nNode.getElementsByTagName('a');
+					anStatic = [
+						anButtons[0], anButtons[1], 
+						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
+					];
+	
+					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
+					$([anStatic[0], anStatic[1]]).addClass( 
+						(iCurrentPage==1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+					$([anStatic[2], anStatic[3]]).addClass(
+						(iPages===0 || iCurrentPage===iPages || oSettings._iDisplayLength===-1) ?
+							oClasses.sPageButtonStaticDisabled :
+							oClasses.sPageButton
+					);
+				}
+			}
+		}
+	} );
+	
+	$.extend( DataTable.ext.oSort, {
+		/*
+		 * text sorting
+		 */
+		"string-pre": function ( a )
+		{
+			if ( typeof a != 'string' ) {
+				a = (a !== null && a.toString) ? a.toString() : '';
+			}
+			return a.toLowerCase();
+		},
+	
+		"string-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"string-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * html sorting (ignore html tags)
+		 */
+		"html-pre": function ( a )
+		{
+			return a.replace( /<.*?>/g, "" ).toLowerCase();
+		},
+		
+		"html-asc": function ( x, y )
+		{
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"html-desc": function ( x, y )
+		{
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * date sorting
+		 */
+		"date-pre": function ( a )
+		{
+			var x = Date.parse( a );
+			
+			if ( isNaN(x) || x==="" )
+			{
+				x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			return x;
+		},
+	
+		"date-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"date-desc": function ( x, y )
+		{
+			return y - x;
+		},
+		
+		
+		/*
+		 * numerical sorting
+		 */
+		"numeric-pre": function ( a )
+		{
+			return (a=="-" || a==="") ? 0 : a*1;
+		},
+	
+		"numeric-asc": function ( x, y )
+		{
+			return x - y;
+		},
+		
+		"numeric-desc": function ( x, y )
+		{
+			return y - x;
+		}
+	} );
+	
+	
+	$.extend( DataTable.ext.aTypes, [
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is numeric
+		 * Returns:  string:'numeric' or null
+		 * Inputs:   mixed:sText - string to check
+		 */
+		function ( sData )
+		{
+			/* Allow zero length strings as a number */
+			if ( typeof sData === 'number' )
+			{
+				return 'numeric';
+			}
+			else if ( typeof sData !== 'string' )
+			{
+				return null;
+			}
+			
+			var sValidFirstChars = "0123456789-";
+			var sValidChars = "0123456789.";
+			var Char;
+			var bDecimal = false;
+			
+			/* Check for a valid first char (no period and allow negatives) */
+			Char = sData.charAt(0); 
+			if (sValidFirstChars.indexOf(Char) == -1) 
+			{
+				return null;
+			}
+			
+			/* Check all the other characters are valid */
+			for ( var i=1 ; i<sData.length ; i++ ) 
+			{
+				Char = sData.charAt(i); 
+				if (sValidChars.indexOf(Char) == -1) 
+				{
+					return null;
+				}
+				
+				/* Only allowed one decimal place... */
+				if ( Char == "." )
+				{
+					if ( bDecimal )
+					{
+						return null;
+					}
+					bDecimal = true;
+				}
+			}
+			
+			return 'numeric';
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is actually a formatted date
+		 * Returns:  string:'date' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			var iParse = Date.parse(sData);
+			if ( (iParse !== null && !isNaN(iParse)) || (typeof sData === 'string' && sData.length === 0) )
+			{
+				return 'date';
+			}
+			return null;
+		},
+		
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string should be treated as an HTML string
+		 * Returns:  string:'html' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			if ( typeof sData === 'string' && sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )
+			{
+				return 'html';
+			}
+			return null;
+		}
+	] );
+	
+
+	// jQuery aliases
+	$.fn.DataTable = DataTable;
+	$.fn.dataTable = DataTable;
+	$.fn.dataTableSettings = DataTable.settings;
+	$.fn.dataTableExt = DataTable.ext;
+
+
+	// Information about events fired by DataTables - for documentation.
+	/**
+	 * Draw event, fired whenever the table is redrawn on the page, at the same point as
+	 * fnDrawCallback. This may be useful for binding events or performing calculations when
+	 * the table is altered at all.
+	 *  @name DataTable#draw
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Filter event, fired when the filtering applied to the table (using the build in global
+	 * global filter, or column filters) is altered.
+	 *  @name DataTable#filter
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Page change event, fired when the paging of the table is altered.
+	 *  @name DataTable#page
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * Sort event, fired when the sorting applied to the table is altered.
+	 *  @name DataTable#sort
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+
+	/**
+	 * DataTables initialisation complete event, fired when the table is fully drawn,
+	 * including Ajax data loaded, if Ajax data is required.
+	 *  @name DataTable#init
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The JSON object request from the server - only
+	 *    present if client-side Ajax sourced data is used</li></ol>
+	 */
+
+	/**
+	 * State save event, fired when the table has changed state a new state save is required.
+	 * This method allows modification of the state saving object prior to actually doing the
+	 * save, including addition or other state properties (for plug-ins) or modification
+	 * of a DataTables core property.
+	 *  @name DataTable#stateSaveParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The state information to be saved
+	 */
+
+	/**
+	 * State load event, fired when the table is loading state from the stored data, but
+	 * prior to the settings object being modified by the saved state - allowing modification
+	 * of the saved state is required or loading of state for a plug-in.
+	 *  @name DataTable#stateLoadParams
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * State loaded event, fired when state has been loaded from stored data and the settings
+	 * object has been modified by the loaded data.
+	 *  @name DataTable#stateLoaded
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {object} json The saved state information
+	 */
+
+	/**
+	 * Processing event, fired when DataTables is doing some kind of processing (be it,
+	 * sort, filter or anything else). Can be used to indicate to the end user that
+	 * there is something happening, or that something has finished.
+	 *  @name DataTable#processing
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} oSettings DataTables settings object
+	 *  @param {boolean} bShow Flag for if DataTables is doing processing or not
+	 */
+
+	/**
+	 * Ajax (XHR) event, fired whenever an Ajax request is completed from a request to 
+	 * made to the server for new data (note that this trigger is called in fnServerData,
+	 * if you override fnServerData and which to use this event, you need to trigger it in
+	 * you success function).
+	 *  @name DataTable#xhr
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 *  @param {object} json JSON returned from the server
+	 */
+
+	/**
+	 * Destroy event, fired when the DataTable is destroyed by calling fnDestroy or passing
+	 * the bDestroy:true parameter in the initialisation object. This can be used to remove
+	 * bound events, added DOM nodes, etc.
+	 *  @name DataTable#destroy
+	 *  @event
+	 *  @param {event} e jQuery event object
+	 *  @param {object} o DataTables settings object {@link DataTable.models.oSettings}
+	 */
+}));
+
+}(window, document));
+
diff --git a/web-app/js/DataTables/js/jquery.dataTables.min.js b/web-app/js/DataTables/js/jquery.dataTables.min.js
new file mode 100644
index 0000000..02694a4
--- /dev/null
+++ b/web-app/js/DataTables/js/jquery.dataTables.min.js
@@ -0,0 +1,155 @@
+/*
+ * File:        jquery.dataTables.min.js
+ * Version:     1.9.4
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Info:        www.datatables.net
+ * 
+ * Copyright 2008-2012 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, available at:
+ *   http://datatables.net/license_gpl2
+ *   http://datatables.net/license_bsd
+ * 
+ * This source file is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+ */
+(function(X,l,n){var L=function(h){var j=function(e){function o(a,b){var c=j.defaults.columns,d=a.aoColumns.length,c=h.extend({},j.models.oColumn,c,{sSortingClass:a.oClasses.sSortable,sSortingClassJUI:a.oClasses.sSortJUI,nTh:b?b:l.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.oDefaults:d});a.aoColumns.push(c);if(a.aoPreSearchCols[d]===n||null===a.aoPreSearchCols[d])a.aoPreSearchCols[d]=h.extend({},j.models.oSearch);els [...]
+c.bRegex===n&&(c.bRegex=!0),c.bSmart===n&&(c.bSmart=!0),c.bCaseInsensitive===n)c.bCaseInsensitive=!0;m(a,d,null)}function m(a,b,c){var d=a.aoColumns[b];c!==n&&null!==c&&(c.mDataProp&&!c.mData&&(c.mData=c.mDataProp),c.sType!==n&&(d.sType=c.sType,d._bAutoType=!1),h.extend(d,c),p(d,c,"sWidth","sWidthOrig"),c.iDataSort!==n&&(d.aDataSort=[c.iDataSort]),p(d,c,"aDataSort"));var i=d.mRender?Q(d.mRender):null,f=Q(d.mData);d.fnGetData=function(a,b){var c=f(a,b);return d.mRender&&b&&""!==b?i(c,b,a) [...]
+L(d.mData);a.oFeatures.bSort||(d.bSortable=!1);!d.bSortable||-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableNone,d.sSortingClassJUI=""):-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortable,d.sSortingClassJUI=a.oClasses.sSortJUI):-1!=h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableAsc,d.sSortingClassJUI=a.oClasses.sSortJUIAscAl [...]
+h.inArray("asc",d.asSorting)&&-1!=h.inArray("desc",d.asSorting)&&(d.sSortingClass=a.oClasses.sSortableDesc,d.sSortingClassJUI=a.oClasses.sSortJUIDescAllowed)}function k(a){if(!1===a.oFeatures.bAutoWidth)return!1;da(a);for(var b=0,c=a.aoColumns.length;b<c;b++)a.aoColumns[b].nTh.style.width=a.aoColumns[b].sWidth}function G(a,b){var c=r(a,"bVisible");return"number"===typeof c[b]?c[b]:null}function R(a,b){var c=r(a,"bVisible"),c=h.inArray(b,c);return-1!==c?c:null}function t(a){return r(a,"bV [...]
+function r(a,b){var c=[];h.map(a.aoColumns,function(a,i){a[b]&&c.push(i)});return c}function B(a){for(var b=j.ext.aTypes,c=b.length,d=0;d<c;d++){var i=b[d](a);if(null!==i)return i}return"string"}function u(a,b){for(var c=b.split(","),d=[],i=0,f=a.aoColumns.length;i<f;i++)for(var g=0;g<f;g++)if(a.aoColumns[i].sName==c[g]){d.push(g);break}return d}function M(a){for(var b="",c=0,d=a.aoColumns.length;c<d;c++)b+=a.aoColumns[c].sName+",";return b.length==d?"":b.slice(0,-1)}function ta(a,b,c,d) [...]
+g,e,w;if(b)for(i=b.length-1;0<=i;i--){var j=b[i].aTargets;h.isArray(j)||D(a,1,"aTargets must be an array of targets, not a "+typeof j);f=0;for(g=j.length;f<g;f++)if("number"===typeof j[f]&&0<=j[f]){for(;a.aoColumns.length<=j[f];)o(a);d(j[f],b[i])}else if("number"===typeof j[f]&&0>j[f])d(a.aoColumns.length+j[f],b[i]);else if("string"===typeof j[f]){e=0;for(w=a.aoColumns.length;e<w;e++)("_all"==j[f]||h(a.aoColumns[e].nTh).hasClass(j[f]))&&d(e,b[i])}}if(c){i=0;for(a=c.length;i<a;i++)d(i,c[i [...]
+b){var c;c=h.isArray(b)?b.slice():h.extend(!0,{},b);var d=a.aoData.length,i=h.extend(!0,{},j.models.oRow);i._aData=c;a.aoData.push(i);for(var f,i=0,g=a.aoColumns.length;i<g;i++)c=a.aoColumns[i],"function"===typeof c.fnRender&&c.bUseRendered&&null!==c.mData?F(a,d,i,S(a,d,i)):F(a,d,i,v(a,d,i)),c._bAutoType&&"string"!=c.sType&&(f=v(a,d,i,"type"),null!==f&&""!==f&&(f=B(f),null===c.sType?c.sType=f:c.sType!=f&&"html"!=c.sType&&(c.sType="string")));a.aiDisplayMaster.push(d);a.oFeatures.bDeferRe [...]
+d);return d}function ua(a){var b,c,d,i,f,g,e;if(a.bDeferLoading||null===a.sAjaxSource)for(b=a.nTBody.firstChild;b;){if("TR"==b.nodeName.toUpperCase()){c=a.aoData.length;b._DT_RowIndex=c;a.aoData.push(h.extend(!0,{},j.models.oRow,{nTr:b}));a.aiDisplayMaster.push(c);f=b.firstChild;for(d=0;f;){g=f.nodeName.toUpperCase();if("TD"==g||"TH"==g)F(a,c,d,h.trim(f.innerHTML)),d++;f=f.nextSibling}}b=b.nextSibling}i=T(a);d=[];b=0;for(c=i.length;b<c;b++)for(f=i[b].firstChild;f;)g=f.nodeName.toUpperCas [...]
+g||"TH"==g)&&d.push(f),f=f.nextSibling;c=0;for(i=a.aoColumns.length;c<i;c++){e=a.aoColumns[c];null===e.sTitle&&(e.sTitle=e.nTh.innerHTML);var w=e._bAutoType,o="function"===typeof e.fnRender,k=null!==e.sClass,n=e.bVisible,m,p;if(w||o||k||!n){g=0;for(b=a.aoData.length;g<b;g++)f=a.aoData[g],m=d[g*i+c],w&&"string"!=e.sType&&(p=v(a,g,c,"type"),""!==p&&(p=B(p),null===e.sType?e.sType=p:e.sType!=p&&"html"!=e.sType&&(e.sType="string"))),e.mRender?m.innerHTML=v(a,g,c,"display"):e.mData!==c&&(m.inn [...]
+g,c,"display")),o&&(p=S(a,g,c),m.innerHTML=p,e.bUseRendered&&F(a,g,c,p)),k&&(m.className+=" "+e.sClass),n?f._anHidden[c]=null:(f._anHidden[c]=m,m.parentNode.removeChild(m)),e.fnCreatedCell&&e.fnCreatedCell.call(a.oInstance,m,v(a,g,c,"display"),f._aData,g,c)}}if(0!==a.aoRowCreatedCallback.length){b=0;for(c=a.aoData.length;b<c;b++)f=a.aoData[b],A(a,"aoRowCreatedCallback",null,[f.nTr,f._aData,b])}}function I(a,b){return b._DT_RowIndex!==n?b._DT_RowIndex:null}function fa(a,b,c){for(var b=J(a [...]
+a.aoColumns.length;d<a;d++)if(b[d]===c)return d;return-1}function Y(a,b,c,d){for(var i=[],f=0,g=d.length;f<g;f++)i.push(v(a,b,d[f],c));return i}function v(a,b,c,d){var i=a.aoColumns[c];if((c=i.fnGetData(a.aoData[b]._aData,d))===n)return a.iDrawError!=a.iDraw&&null===i.sDefaultContent&&(D(a,0,"Requested unknown parameter "+("function"==typeof i.mData?"{mData function}":"'"+i.mData+"'")+" from the data source for row "+b),a.iDrawError=a.iDraw),i.sDefaultContent;if(null===c&&null!==i.sDefau [...]
+i.sDefaultContent;else if("function"===typeof c)return c();return"display"==d&&null===c?"":c}function F(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d)}function Q(a){if(null===a)return function(){return null};if("function"===typeof a)return function(b,d,i){return a(b,d,i)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var f=i.split("."),g;if(""!==i){var e=0;for(g=f.length;e<g;e++){if(i=f[e].match(U)){f[e]=f[e].replace(U,"");""!==f[e]&&( [...]
+g=[];f.splice(0,e+1);for(var f=f.join("."),e=0,h=a.length;e<h;e++)g.push(b(a[e],d,f));a=i[0].substring(1,i[0].length-1);a=""===a?g:g.join(a);break}if(null===a||a[f[e]]===n)return n;a=a[f[e]]}}return a};return function(c,d){return b(c,d,a)}}return function(b){return b[a]}}function L(a){if(null===a)return function(){};if("function"===typeof a)return function(b,d){a(b,"set",d)};if("string"===typeof a&&(-1!==a.indexOf(".")||-1!==a.indexOf("["))){var b=function(a,d,i){var i=i.split("."),f,g,e [...]
+i.length-1;e<g;e++){if(f=i[e].match(U)){i[e]=i[e].replace(U,"");a[i[e]]=[];f=i.slice();f.splice(0,e+1);g=f.join(".");for(var h=0,j=d.length;h<j;h++)f={},b(f,d[h],g),a[i[e]].push(f);return}if(null===a[i[e]]||a[i[e]]===n)a[i[e]]={};a=a[i[e]]}a[i[i.length-1].replace(U,"")]=d};return function(c,d){return b(c,d,a)}}return function(b,d){b[a]=d}}function Z(a){for(var b=[],c=a.aoData.length,d=0;d<c;d++)b.push(a.aoData[d]._aData);return b}function ga(a){a.aoData.splice(0,a.aoData.length);a.aiDisp [...]
+a.aiDisplayMaster.length);a.aiDisplay.splice(0,a.aiDisplay.length);y(a)}function ha(a,b){for(var c=-1,d=0,i=a.length;d<i;d++)a[d]==b?c=d:a[d]>b&&a[d]--; -1!=c&&a.splice(c,1)}function S(a,b,c){var d=a.aoColumns[c];return d.fnRender({iDataRow:b,iDataColumn:c,oSettings:a,aData:a.aoData[b]._aData,mDataProp:d.mData},v(a,b,c,"display"))}function ea(a,b){var c=a.aoData[b],d;if(null===c.nTr){c.nTr=l.createElement("tr");c.nTr._DT_RowIndex=b;c._aData.DT_RowId&&(c.nTr.id=c._aData.DT_RowId);c._aData [...]
+(c.nTr.className=c._aData.DT_RowClass);for(var i=0,f=a.aoColumns.length;i<f;i++){var g=a.aoColumns[i];d=l.createElement(g.sCellType);d.innerHTML="function"===typeof g.fnRender&&(!g.bUseRendered||null===g.mData)?S(a,b,i):v(a,b,i,"display");null!==g.sClass&&(d.className=g.sClass);g.bVisible?(c.nTr.appendChild(d),c._anHidden[i]=null):c._anHidden[i]=d;g.fnCreatedCell&&g.fnCreatedCell.call(a.oInstance,d,v(a,b,i,"display"),c._aData,b,i)}A(a,"aoRowCreatedCallback",null,[c.nTr,c._aData,b])}}func [...]
+c,d;if(0!==h("th, td",a.nTHead).length){b=0;for(d=a.aoColumns.length;b<d;b++)if(c=a.aoColumns[b].nTh,c.setAttribute("role","columnheader"),a.aoColumns[b].bSortable&&(c.setAttribute("tabindex",a.iTabIndex),c.setAttribute("aria-controls",a.sTableId)),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),a.aoColumns[b].sTitle!=c.innerHTML)c.innerHTML=a.aoColumns[b].sTitle}else{var i=l.createElement("tr");b=0;for(d=a.aoColumns.length;b<d;b++)c=a.aoColumns[b].nTh,c.innerHTML=a.ao [...]
+c.setAttribute("tabindex","0"),null!==a.aoColumns[b].sClass&&h(c).addClass(a.aoColumns[b].sClass),i.appendChild(c);h(a.nTHead).html("")[0].appendChild(i);V(a.aoHeader,a.nTHead)}h(a.nTHead).children("tr").attr("role","row");if(a.bJUI){b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;i=l.createElement("div");i.className=a.oClasses.sSortJUIWrapper;h(c).contents().appendTo(i);var f=l.createElement("span");f.className=a.oClasses.sSortIcon;i.appendChild(f);c.appendChild(i)}}if(a.oFea [...]
+0;b<a.aoColumns.length;b++)!1!==a.aoColumns[b].bSortable?ia(a,a.aoColumns[b].nTh,b):h(a.aoColumns[b].nTh).addClass(a.oClasses.sSortableNone);""!==a.oClasses.sFooterTH&&h(a.nTFoot).children("tr").children("th").addClass(a.oClasses.sFooterTH);if(null!==a.nTFoot){c=N(a,null,a.aoFooter);b=0;for(d=a.aoColumns.length;b<d;b++)c[b]&&(a.aoColumns[b].nTf=c[b],a.aoColumns[b].sClass&&h(c[b]).addClass(a.aoColumns[b].sClass))}}function W(a,b,c){var d,i,f,g=[],e=[],h=a.aoColumns.length,j;c===n&&(c=!1); [...]
+b.length;d<i;d++){g[d]=b[d].slice();g[d].nTr=b[d].nTr;for(f=h-1;0<=f;f--)!a.aoColumns[f].bVisible&&!c&&g[d].splice(f,1);e.push([])}d=0;for(i=g.length;d<i;d++){if(a=g[d].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=g[d].length;f<b;f++)if(j=h=1,e[d][f]===n){a.appendChild(g[d][f].cell);for(e[d][f]=1;g[d+h]!==n&&g[d][f].cell==g[d+h][f].cell;)e[d+h][f]=1,h++;for(;g[d][f+j]!==n&&g[d][f].cell==g[d][f+j].cell;){for(c=0;c<h;c++)e[d+c][f+j]=1;j++}g[d][f].cell.rowSpan=h;g[d][f].cell.colSpan= [...]
+A(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==h.inArray(!1,b))E(a,!1);else{var c,d,b=[],i=0,f=a.asStripeClasses.length;c=a.aoOpenRows.length;a.bDrawing=!0;a.iInitDisplayStart!==n&&-1!=a.iInitDisplayStart&&(a._iDisplayStart=a.oFeatures.bServerSide?a.iInitDisplayStart:a.iInitDisplayStart>=a.fnRecordsDisplay()?0:a.iInitDisplayStart,a.iInitDisplayStart=-1,y(a));if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++;else if(a.oFeatures.bServerSide){if(!a.bDestroying&&!wa(a))return}else a.iDraw++;if [...]
+a._iDisplayStart;d=a._iDisplayEnd;a.oFeatures.bServerSide&&(g=0,d=a.aoData.length);for(;g<d;g++){var e=a.aoData[a.aiDisplay[g]];null===e.nTr&&ea(a,a.aiDisplay[g]);var j=e.nTr;if(0!==f){var o=a.asStripeClasses[i%f];e._sRowStripe!=o&&(h(j).removeClass(e._sRowStripe).addClass(o),e._sRowStripe=o)}A(a,"aoRowCallback",null,[j,a.aoData[a.aiDisplay[g]]._aData,i,g]);b.push(j);i++;if(0!==c)for(e=0;e<c;e++)if(j==a.aoOpenRows[e].nParent){b.push(a.aoOpenRows[e].nTr);break}}}else b[0]=l.createElement( [...]
+(b[0].className=a.asStripeClasses[0]),c=a.oLanguage,f=c.sZeroRecords,1==a.iDraw&&null!==a.sAjaxSource&&!a.oFeatures.bServerSide?f=c.sLoadingRecords:c.sEmptyTable&&0===a.fnRecordsTotal()&&(f=c.sEmptyTable),c=l.createElement("td"),c.setAttribute("valign","top"),c.colSpan=t(a),c.className=a.oClasses.sRowEmpty,c.innerHTML=ja(a,f),b[i].appendChild(c);A(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);A(a,"aoFooterCallback","foot [...]
+Z(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay]);i=l.createDocumentFragment();c=l.createDocumentFragment();if(a.nTBody){f=a.nTBody.parentNode;c.appendChild(a.nTBody);if(!a.oScroll.bInfinite||!a._bInitComplete||a.bSorted||a.bFiltered)for(;c=a.nTBody.firstChild;)a.nTBody.removeChild(c);c=0;for(d=b.length;c<d;c++)i.appendChild(b[c]);a.nTBody.appendChild(i);null!==f&&f.appendChild(a.nTBody)}A(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1;a.oFeatures.bServerSide [...]
+a._bInitComplete||$(a))}}function aa(a){a.oFeatures.bSort?O(a,a.oPreviousSearch):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(y(a),x(a))}function xa(a){var b=h("<div></div>")[0];a.nTable.parentNode.insertBefore(b,a.nTable);a.nTableWrapper=h('<div id="'+a.sTableId+'_wrapper" class="'+a.oClasses.sWrapper+'" role="grid"></div>')[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var c=a.nTableWrapper,d=a.sDom.split(""),i,f,g,e,w,o,k,m=0;m<d.length;m++){f=0;g=d[m];if("<"==g){e=h("<div></div>" [...]
+1];if("'"==w||'"'==w){o="";for(k=2;d[m+k]!=w;)o+=d[m+k],k++;"H"==o?o=a.oClasses.sJUIHeader:"F"==o&&(o=a.oClasses.sJUIFooter);-1!=o.indexOf(".")?(w=o.split("."),e.id=w[0].substr(1,w[0].length-1),e.className=w[1]):"#"==o.charAt(0)?e.id=o.substr(1,o.length-1):e.className=o;m+=k}c.appendChild(e);c=e}else if(">"==g)c=c.parentNode;else if("l"==g&&a.oFeatures.bPaginate&&a.oFeatures.bLengthChange)i=ya(a),f=1;else if("f"==g&&a.oFeatures.bFilter)i=za(a),f=1;else if("r"==g&&a.oFeatures.bProcessing) [...]
+1;else if("t"==g)i=Ba(a),f=1;else if("i"==g&&a.oFeatures.bInfo)i=Ca(a),f=1;else if("p"==g&&a.oFeatures.bPaginate)i=Da(a),f=1;else if(0!==j.ext.aoFeatures.length){e=j.ext.aoFeatures;k=0;for(w=e.length;k<w;k++)if(g==e[k].cFeature){(i=e[k].fnInit(a))&&(f=1);break}}1==f&&null!==i&&("object"!==typeof a.aanFeatures[g]&&(a.aanFeatures[g]=[]),a.aanFeatures[g].push(i),c.appendChild(i))}b.parentNode.replaceChild(a.nTableWrapper,b)}function V(a,b){var c=h(b).children("tr"),d,i,f,g,e,j,o,k,m,p;a.spl [...]
+f=0;for(j=c.length;f<j;f++)a.push([]);f=0;for(j=c.length;f<j;f++){d=c[f];for(i=d.firstChild;i;){if("TD"==i.nodeName.toUpperCase()||"TH"==i.nodeName.toUpperCase()){k=1*i.getAttribute("colspan");m=1*i.getAttribute("rowspan");k=!k||0===k||1===k?1:k;m=!m||0===m||1===m?1:m;g=0;for(e=a[f];e[g];)g++;o=g;p=1===k?!0:!1;for(e=0;e<k;e++)for(g=0;g<m;g++)a[f+g][o+e]={cell:i,unique:p},a[f+g].nTr=d}i=i.nextSibling}}}function N(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],V(c,b)));for(var b=0,i=c.length;b< [...]
+0,g=c[b].length;f<g;f++)if(c[b][f].unique&&(!d[f]||!a.bSortCellsTop))d[f]=c[b][f].cell;return d}function wa(a){if(a.bAjaxDataGet){a.iDraw++;E(a,!0);var b=Ea(a);ka(a,b);a.fnServerData.call(a.oInstance,a.sAjaxSource,b,function(b){Fa(a,b)},a);return!1}return!0}function Ea(a){var b=a.aoColumns.length,c=[],d,i,f,g;c.push({name:"sEcho",value:a.iDraw});c.push({name:"iColumns",value:b});c.push({name:"sColumns",value:M(a)});c.push({name:"iDisplayStart",value:a._iDisplayStart});c.push({name:"iDisp [...]
+value:!1!==a.oFeatures.bPaginate?a._iDisplayLength:-1});for(f=0;f<b;f++)d=a.aoColumns[f].mData,c.push({name:"mDataProp_"+f,value:"function"===typeof d?"function":d});if(!1!==a.oFeatures.bFilter){c.push({name:"sSearch",value:a.oPreviousSearch.sSearch});c.push({name:"bRegex",value:a.oPreviousSearch.bRegex});for(f=0;f<b;f++)c.push({name:"sSearch_"+f,value:a.aoPreSearchCols[f].sSearch}),c.push({name:"bRegex_"+f,value:a.aoPreSearchCols[f].bRegex}),c.push({name:"bSearchable_"+f,value:a.aoColum [...]
+a.oFeatures.bSort){var e=0;d=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(f=0;f<d.length;f++){i=a.aoColumns[d[f][0]].aDataSort;for(g=0;g<i.length;g++)c.push({name:"iSortCol_"+e,value:i[g]}),c.push({name:"sSortDir_"+e,value:d[f][1]}),e++}c.push({name:"iSortingCols",value:e});for(f=0;f<b;f++)c.push({name:"bSortable_"+f,value:a.aoColumns[f].bSortable})}return c}function ka(a,b){A(a,"aoServerParams","serverParams",[b])}function Fa(a,b){if(b.sEcho!==n){ [...]
+a.iDraw)return;a.iDraw=1*b.sEcho}(!a.oScroll.bInfinite||a.oScroll.bInfinite&&(a.bSorted||a.bFiltered))&&ga(a);a._iRecordsTotal=parseInt(b.iTotalRecords,10);a._iRecordsDisplay=parseInt(b.iTotalDisplayRecords,10);var c=M(a),c=b.sColumns!==n&&""!==c&&b.sColumns!=c,d;c&&(d=u(a,b.sColumns));for(var i=Q(a.sAjaxDataProp)(b),f=0,g=i.length;f<g;f++)if(c){for(var e=[],h=0,j=a.aoColumns.length;h<j;h++)e.push(i[f][d[h]]);H(a,e)}else H(a,i[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;x [...]
+!0;E(a,!1)}function za(a){var b=a.oPreviousSearch,c=a.oLanguage.sSearch,c=-1!==c.indexOf("_INPUT_")?c.replace("_INPUT_",'<input type="text" />'):""===c?'<input type="text" />':c+' <input type="text" />',d=l.createElement("div");d.className=a.oClasses.sFilter;d.innerHTML="<label>"+c+"</label>";a.aanFeatures.f||(d.id=a.sTableId+"_filter");c=h('input[type="text"]',d);d._DT_Input=c[0];c.val(b.sSearch.replace('"',"""));c.bind("keyup.DT",function(){for(var c=a.aanFeatures.f,d=this.value== [...]
+g=0,e=c.length;g<e;g++)c[g]!=h(this).parents("div.dataTables_filter")[0]&&h(c[g]._DT_Input).val(d);d!=b.sSearch&&K(a,{sSearch:d,bRegex:b.bRegex,bSmart:b.bSmart,bCaseInsensitive:b.bCaseInsensitive})});c.attr("aria-controls",a.sTableId).bind("keypress.DT",function(a){if(a.keyCode==13)return false});return d}function K(a,b,c){var d=a.oPreviousSearch,i=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};if(a.oFeatures [...]
+else{Ga(a,b.sSearch,c,b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b<a.aoPreSearchCols.length;b++)Ha(a,i[b].sSearch,b,i[b].bRegex,i[b].bSmart,i[b].bCaseInsensitive);Ia(a)}a.bFiltered=!0;h(a.oInstance).trigger("filter",a);a._iDisplayStart=0;y(a);x(a);la(a,0)}function Ia(a){for(var b=j.ext.afnFiltering,c=r(a,"bSearchable"),d=0,i=b.length;d<i;d++)for(var f=0,g=0,e=a.aiDisplay.length;g<e;g++){var h=a.aiDisplay[g-f];b[d](a,Y(a,h,"filter",c),h)||(a.aiDisplay.splice(g-f,1),f++)}}function  [...]
+d,i,f){if(""!==b)for(var g=0,b=ma(b,d,i,f),d=a.aiDisplay.length-1;0<=d;d--)i=Ja(v(a,a.aiDisplay[d],c,"filter"),a.aoColumns[c].sType),b.test(i)||(a.aiDisplay.splice(d,1),g++)}function Ga(a,b,c,d,i,f){d=ma(b,d,i,f);i=a.oPreviousSearch;c||(c=0);0!==j.ext.afnFiltering.length&&(c=1);if(0>=b.length)a.aiDisplay.splice(0,a.aiDisplay.length),a.aiDisplay=a.aiDisplayMaster.slice();else if(a.aiDisplay.length==a.aiDisplayMaster.length||i.sSearch.length>b.length||1==c||0!==b.indexOf(i.sSearch)){a.aiDi [...]
+a.aiDisplay.length);la(a,1);for(b=0;b<a.aiDisplayMaster.length;b++)d.test(a.asDataSearch[b])&&a.aiDisplay.push(a.aiDisplayMaster[b])}else for(b=c=0;b<a.asDataSearch.length;b++)d.test(a.asDataSearch[b])||(a.aiDisplay.splice(b-c,1),c++)}function la(a,b){if(!a.oFeatures.bServerSide){a.asDataSearch=[];for(var c=r(a,"bSearchable"),d=1===b?a.aiDisplayMaster:a.aiDisplay,i=0,f=d.length;i<f;i++)a.asDataSearch[i]=na(a,Y(a,d[i],"filter",c))}}function na(a,b){var c=b.join("  ");-1!==c.indexOf("&")&& [...]
+return c.replace(/[\n\r]/g," ")}function ma(a,b,c,d){if(c)return a=b?a.split(" "):oa(a).split(" "),a="^(?=.*?"+a.join(")(?=.*?")+").*$",RegExp(a,d?"i":"");a=b?a:oa(a);return RegExp(a,d?"i":"")}function Ja(a,b){return"function"===typeof j.ext.ofnSearch[b]?j.ext.ofnSearch[b](a):null===a?"":"html"==b?a.replace(/[\r\n]/g," ").replace(/<.*?>/g,""):"string"===typeof a?a.replace(/[\r\n]/g," "):a}function oa(a){return a.replace(RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\ [...]
+"\\$1")}function Ca(a){var b=l.createElement("div");b.className=a.oClasses.sInfo;a.aanFeatures.i||(a.aoDrawCallback.push({fn:Ka,sName:"information"}),b.id=a.sTableId+"_info");a.nTable.setAttribute("aria-describedby",a.sTableId+"_info");return b}function Ka(a){if(a.oFeatures.bInfo&&0!==a.aanFeatures.i.length){var b=a.oLanguage,c=a._iDisplayStart+1,d=a.fnDisplayEnd(),i=a.fnRecordsTotal(),f=a.fnRecordsDisplay(),g;g=0===f?b.sInfoEmpty:b.sInfo;f!=i&&(g+=" "+b.sInfoFiltered);g+=b.sInfoPostFix; [...]
+null!==b.fnInfoCallback&&(g=b.fnInfoCallback.call(a.oInstance,a,c,d,i,f,g));a=a.aanFeatures.i;b=0;for(c=a.length;b<c;b++)h(a[b]).html(g)}}function ja(a,b){var c=a.fnFormatNumber(a._iDisplayStart+1),d=a.fnDisplayEnd(),d=a.fnFormatNumber(d),i=a.fnRecordsDisplay(),i=a.fnFormatNumber(i),f=a.fnRecordsTotal(),f=a.fnFormatNumber(f);a.oScroll.bInfinite&&(c=a.fnFormatNumber(1));return b.replace(/_START_/g,c).replace(/_END_/g,d).replace(/_TOTAL_/g,i).replace(/_MAX_/g,f)}function ba(a){var b,c,d=a. [...]
+if(!1===a.bInitialised)setTimeout(function(){ba(a)},200);else{xa(a);va(a);W(a,a.aoHeader);a.nTFoot&&W(a,a.aoFooter);E(a,!0);a.oFeatures.bAutoWidth&&da(a);b=0;for(c=a.aoColumns.length;b<c;b++)null!==a.aoColumns[b].sWidth&&(a.aoColumns[b].nTh.style.width=q(a.aoColumns[b].sWidth));a.oFeatures.bSort?O(a):a.oFeatures.bFilter?K(a,a.oPreviousSearch):(a.aiDisplay=a.aiDisplayMaster.slice(),y(a),x(a));null!==a.sAjaxSource&&!a.oFeatures.bServerSide?(c=[],ka(a,c),a.fnServerData.call(a.oInstance,a.sA [...]
+c,function(c){var f=a.sAjaxDataProp!==""?Q(a.sAjaxDataProp)(c):c;for(b=0;b<f.length;b++)H(a,f[b]);a.iInitDisplayStart=d;if(a.oFeatures.bSort)O(a);else{a.aiDisplay=a.aiDisplayMaster.slice();y(a);x(a)}E(a,false);$(a,c)},a)):a.oFeatures.bServerSide||(E(a,!1),$(a))}}function $(a,b){a._bInitComplete=!0;A(a,"aoInitComplete","init",[a,b])}function pa(a){var b=j.defaults.oLanguage;!a.sEmptyTable&&(a.sZeroRecords&&"No data available in table"===b.sEmptyTable)&&p(a,a,"sZeroRecords","sEmptyTable"); [...]
+(a.sZeroRecords&&"Loading..."===b.sLoadingRecords)&&p(a,a,"sZeroRecords","sLoadingRecords")}function ya(a){if(a.oScroll.bInfinite)return null;var b='<select size="1" '+('name="'+a.sTableId+'_length"')+">",c,d,i=a.aLengthMenu;if(2==i.length&&"object"===typeof i[0]&&"object"===typeof i[1]){c=0;for(d=i[0].length;c<d;c++)b+='<option value="'+i[0][c]+'">'+i[1][c]+"</option>"}else{c=0;for(d=i.length;c<d;c++)b+='<option value="'+i[c]+'">'+i[c]+"</option>"}b+="</select>";i=l.createElement("div") [...]
+(i.id=a.sTableId+"_length");i.className=a.oClasses.sLength;i.innerHTML="<label>"+a.oLanguage.sLengthMenu.replace("_MENU_",b)+"</label>";h('select option[value="'+a._iDisplayLength+'"]',i).attr("selected",!0);h("select",i).bind("change.DT",function(){var b=h(this).val(),i=a.aanFeatures.l;c=0;for(d=i.length;c<d;c++)i[c]!=this.parentNode&&h("select",i[c]).val(b);a._iDisplayLength=parseInt(b,10);y(a);if(a.fnDisplayEnd()==a.fnRecordsDisplay()){a._iDisplayStart=a.fnDisplayEnd()-a._iDisplayLeng [...]
+0)a._iDisplayStart=0}if(a._iDisplayLength==-1)a._iDisplayStart=0;x(a)});h("select",i).attr("aria-controls",a.sTableId);return i}function y(a){a._iDisplayEnd=!1===a.oFeatures.bPaginate?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength>a.aiDisplay.length||-1==a._iDisplayLength?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength}function Da(a){if(a.oScroll.bInfinite)return null;var b=l.createElement("div");b.className=a.oClasses.sPaging+a.sPaginationType;j.ext.oPagination[a.sPaginatio [...]
+b,function(a){y(a);x(a)});a.aanFeatures.p||a.aoDrawCallback.push({fn:function(a){j.ext.oPagination[a.sPaginationType].fnUpdate(a,function(a){y(a);x(a)})},sName:"pagination"});return b}function qa(a,b){var c=a._iDisplayStart;if("number"===typeof b)a._iDisplayStart=b*a._iDisplayLength,a._iDisplayStart>a.fnRecordsDisplay()&&(a._iDisplayStart=0);else if("first"==b)a._iDisplayStart=0;else if("previous"==b)a._iDisplayStart=0<=a._iDisplayLength?a._iDisplayStart-a._iDisplayLength:0,0>a._iDisplay [...]
+0);else if("next"==b)0<=a._iDisplayLength?a._iDisplayStart+a._iDisplayLength<a.fnRecordsDisplay()&&(a._iDisplayStart+=a._iDisplayLength):a._iDisplayStart=0;else if("last"==b)if(0<=a._iDisplayLength){var d=parseInt((a.fnRecordsDisplay()-1)/a._iDisplayLength,10)+1;a._iDisplayStart=(d-1)*a._iDisplayLength}else a._iDisplayStart=0;else D(a,0,"Unknown paging action: "+b);h(a.oInstance).trigger("page",a);return c!=a._iDisplayStart}function Aa(a){var b=l.createElement("div");a.aanFeatures.r||(b. [...]
+"_processing");b.innerHTML=a.oLanguage.sProcessing;b.className=a.oClasses.sProcessing;a.nTable.parentNode.insertBefore(b,a.nTable);return b}function E(a,b){if(a.oFeatures.bProcessing)for(var c=a.aanFeatures.r,d=0,i=c.length;d<i;d++)c[d].style.visibility=b?"visible":"hidden";h(a.oInstance).trigger("processing",[a,b])}function Ba(a){if(""===a.oScroll.sX&&""===a.oScroll.sY)return a.nTable;var b=l.createElement("div"),c=l.createElement("div"),d=l.createElement("div"),i=l.createElement("div") [...]
+g=l.createElement("div"),e=a.nTable.cloneNode(!1),j=a.nTable.cloneNode(!1),o=a.nTable.getElementsByTagName("thead")[0],k=0===a.nTable.getElementsByTagName("tfoot").length?null:a.nTable.getElementsByTagName("tfoot")[0],m=a.oClasses;c.appendChild(d);f.appendChild(g);i.appendChild(a.nTable);b.appendChild(c);b.appendChild(i);d.appendChild(e);e.appendChild(o);null!==k&&(b.appendChild(f),g.appendChild(j),j.appendChild(k));b.className=m.sScrollWrapper;c.className=m.sScrollHead;d.className=m.sSc [...]
+i.className=m.sScrollBody;f.className=m.sScrollFoot;g.className=m.sScrollFootInner;a.oScroll.bAutoCss&&(c.style.overflow="hidden",c.style.position="relative",f.style.overflow="hidden",i.style.overflow="auto");c.style.border="0";c.style.width="100%";f.style.border="0";d.style.width=""!==a.oScroll.sXInner?a.oScroll.sXInner:"100%";e.removeAttribute("id");e.style.marginLeft="0";a.nTable.style.marginLeft="0";null!==k&&(j.removeAttribute("id"),j.style.marginLeft="0");d=h(a.nTable).children("ca [...]
+d.length&&(d=d[0],"top"===d._captionSide?e.appendChild(d):"bottom"===d._captionSide&&k&&j.appendChild(d));""!==a.oScroll.sX&&(c.style.width=q(a.oScroll.sX),i.style.width=q(a.oScroll.sX),null!==k&&(f.style.width=q(a.oScroll.sX)),h(i).scroll(function(){c.scrollLeft=this.scrollLeft;if(k!==null)f.scrollLeft=this.scrollLeft}));""!==a.oScroll.sY&&(i.style.height=q(a.oScroll.sY));a.aoDrawCallback.push({fn:La,sName:"scrolling"});a.oScroll.bInfinite&&h(i).scroll(function(){if(!a.bDrawing&&h(this) [...]
+0&&h(this).scrollTop()+h(this).height()>h(a.nTable).height()-a.oScroll.iLoadGap&&a.fnDisplayEnd()<a.fnRecordsDisplay()){qa(a,"next");y(a);x(a)}});a.nScrollHead=c;a.nScrollFoot=f;return b}function La(a){var b=a.nScrollHead.getElementsByTagName("div")[0],c=b.getElementsByTagName("table")[0],d=a.nTable.parentNode,i,f,g,e,j,o,k,m,p=[],n=[],l=null!==a.nTFoot?a.nScrollFoot.getElementsByTagName("div")[0]:null,R=null!==a.nTFoot?l.getElementsByTagName("table")[0]:null,r=a.oBrowser.bScrollOversize [...]
+a.style;k.paddingTop="0";k.paddingBottom="0";k.borderTopWidth="0";k.borderBottomWidth="0";k.height=0};h(a.nTable).children("thead, tfoot").remove();i=h(a.nTHead).clone()[0];a.nTable.insertBefore(i,a.nTable.childNodes[0]);g=a.nTHead.getElementsByTagName("tr");e=i.getElementsByTagName("tr");null!==a.nTFoot&&(j=h(a.nTFoot).clone()[0],a.nTable.insertBefore(j,a.nTable.childNodes[1]),o=a.nTFoot.getElementsByTagName("tr"),j=j.getElementsByTagName("tr"));""===a.oScroll.sX&&(d.style.width="100%", [...]
+"100%");var t=N(a,i);i=0;for(f=t.length;i<f;i++)m=G(a,i),t[i].style.width=a.aoColumns[m].sWidth;null!==a.nTFoot&&C(function(a){a.style.width=""},j);a.oScroll.bCollapse&&""!==a.oScroll.sY&&(d.style.height=d.offsetHeight+a.nTHead.offsetHeight+"px");i=h(a.nTable).outerWidth();if(""===a.oScroll.sX){if(a.nTable.style.width="100%",r&&(h("tbody",d).height()>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(h(a.nTable).outerWidth()-a.oScroll.iBarWidth)}else""!==a.oScroll.s [...]
+q(a.oScroll.sXInner):i==h(d).width()&&h(d).height()<h(a.nTable).height()?(a.nTable.style.width=q(i-a.oScroll.iBarWidth),h(a.nTable).outerWidth()>i-a.oScroll.iBarWidth&&(a.nTable.style.width=q(i))):a.nTable.style.width=q(i);i=h(a.nTable).outerWidth();C(s,e);C(function(a){p.push(q(h(a).width()))},e);C(function(a,b){a.style.width=p[b]},g);h(e).height(0);null!==a.nTFoot&&(C(s,j),C(function(a){n.push(q(h(a).width()))},j),C(function(a,b){a.style.width=n[b]},o),h(j).height(0));C(function(a,b){a [...]
+"";a.style.width=p[b]},e);null!==a.nTFoot&&C(function(a,b){a.innerHTML="";a.style.width=n[b]},j);if(h(a.nTable).outerWidth()<i){g=d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")?i+a.oScroll.iBarWidth:i;if(r&&(d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(g-a.oScroll.iBarWidth);d.style.width=q(g);a.nScrollHead.style.width=q(g);null!==a.nTFoot&&(a.nScrollFoot.style.width=q(g));""===a.oScroll.sX?D(a,1,"The table cannot fit into the c [...]
+""!==a.oScroll.sXInner&&D(a,1,"The table cannot fit into the current element which will cause column misalignment. Increase the sScrollXInner value or remove it to allow automatic calculation")}else d.style.width=q("100%"),a.nScrollHead.style.width=q("100%"),null!==a.nTFoot&&(a.nScrollFoot.style.width=q("100%"));""===a.oScroll.sY&&r&&(d.style.height=q(a.nTable.offsetHeight+a.oScroll.iBarWidth));""!==a.oScroll.sY&&a.oScroll.bCollapse&&(d.style.height=q(a.oScroll.sY),r=""!==a.oScroll.sX&&a [...]
+d.offsetWidth?a.oScroll.iBarWidth:0,a.nTable.offsetHeight<d.offsetHeight&&(d.style.height=q(a.nTable.offsetHeight+r)));r=h(a.nTable).outerWidth();c.style.width=q(r);b.style.width=q(r);c=h(a.nTable).height()>d.clientHeight||"scroll"==h(d).css("overflow-y");b.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px";null!==a.nTFoot&&(R.style.width=q(r),l.style.width=q(r),l.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px");h(d).scroll();if(a.bSorted||a.bFiltered)d.scrollTop=0}function C(a,b,c) [...]
+0,i=0,f=b.length,g,e;i<f;){g=b[i].firstChild;for(e=c?c[i].firstChild:null;g;)1===g.nodeType&&(c?a(g,e,d):a(g,d),d++),g=g.nextSibling,e=c?e.nextSibling:null;i++}}function Ma(a,b){if(!a||null===a||""===a)return 0;b||(b=l.body);var c,d=l.createElement("div");d.style.width=q(a);b.appendChild(d);c=d.offsetWidth;b.removeChild(d);return c}function da(a){var b=0,c,d=0,i=a.aoColumns.length,f,e,j=h("th",a.nTHead),o=a.nTable.getAttribute("width");e=a.nTable.parentNode;for(f=0;f<i;f++)a.aoColumns[f] [...]
+(d++,null!==a.aoColumns[f].sWidth&&(c=Ma(a.aoColumns[f].sWidthOrig,e),null!==c&&(a.aoColumns[f].sWidth=q(c)),b++));if(i==j.length&&0===b&&d==i&&""===a.oScroll.sX&&""===a.oScroll.sY)for(f=0;f<a.aoColumns.length;f++)c=h(j[f]).width(),null!==c&&(a.aoColumns[f].sWidth=q(c));else{b=a.nTable.cloneNode(!1);f=a.nTHead.cloneNode(!0);d=l.createElement("tbody");c=l.createElement("tr");b.removeAttribute("id");b.appendChild(f);null!==a.nTFoot&&(b.appendChild(a.nTFoot.cloneNode(!0)),C(function(a){a.st [...]
+""},b.getElementsByTagName("tr")));b.appendChild(d);d.appendChild(c);d=h("thead th",b);0===d.length&&(d=h("tbody tr:eq(0)>td",b));j=N(a,f);for(f=d=0;f<i;f++){var k=a.aoColumns[f];k.bVisible&&null!==k.sWidthOrig&&""!==k.sWidthOrig?j[f-d].style.width=q(k.sWidthOrig):k.bVisible?j[f-d].style.width="":d++}for(f=0;f<i;f++)a.aoColumns[f].bVisible&&(d=Na(a,f),null!==d&&(d=d.cloneNode(!0),""!==a.aoColumns[f].sContentPadding&&(d.innerHTML+=a.aoColumns[f].sContentPadding),c.appendChild(d)));e.appen [...]
+""!==a.oScroll.sX&&""!==a.oScroll.sXInner?b.style.width=q(a.oScroll.sXInner):""!==a.oScroll.sX?(b.style.width="",h(b).width()<e.offsetWidth&&(b.style.width=q(e.offsetWidth))):""!==a.oScroll.sY?b.style.width=q(e.offsetWidth):o&&(b.style.width=q(o));b.style.visibility="hidden";Oa(a,b);i=h("tbody tr:eq(0)",b).children();0===i.length&&(i=N(a,h("thead",b)[0]));if(""!==a.oScroll.sX){for(f=d=e=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=null===a.aoColumns[f].sWidthOrig?e+h(i[d]).oute [...]
+e+(parseInt(a.aoColumns[f].sWidth.replace("px",""),10)+(h(i[d]).outerWidth()-h(i[d]).width())),d++);b.style.width=q(e);a.nTable.style.width=q(e)}for(f=d=0;f<a.aoColumns.length;f++)a.aoColumns[f].bVisible&&(e=h(i[d]).width(),null!==e&&0<e&&(a.aoColumns[f].sWidth=q(e)),d++);i=h(b).css("width");a.nTable.style.width=-1!==i.indexOf("%")?i:q(h(b).outerWidth());b.parentNode.removeChild(b)}o&&(a.nTable.style.width=q(o))}function Oa(a,b){""===a.oScroll.sX&&""!==a.oScroll.sY?(h(b).width(),b.style. [...]
+a.oScroll.iBarWidth)):""!==a.oScroll.sX&&(b.style.width=q(h(b).outerWidth()))}function Na(a,b){var c=Pa(a,b);if(0>c)return null;if(null===a.aoData[c].nTr){var d=l.createElement("td");d.innerHTML=v(a,c,b,"");return d}return J(a,c)[b]}function Pa(a,b){for(var c=-1,d=-1,i=0;i<a.aoData.length;i++){var e=v(a,i,b,"display")+"",e=e.replace(/<.*?>/g,"");e.length>c&&(c=e.length,d=i)}return d}function q(a){if(null===a)return"0px";if("number"==typeof a)return 0>a?"0px":a+"px";var b=a.charCodeAt(a.l [...]
+return 48>b||57<b?a:a+"px"}function Qa(){var a=l.createElement("p"),b=a.style;b.width="100%";b.height="200px";b.padding="0px";var c=l.createElement("div"),b=c.style;b.position="absolute";b.top="0px";b.left="0px";b.visibility="hidden";b.width="200px";b.height="150px";b.padding="0px";b.overflow="hidden";c.appendChild(a);l.body.appendChild(c);b=a.offsetWidth;c.style.overflow="scroll";a=a.offsetWidth;b==a&&(a=c.clientWidth);l.body.removeChild(c);return b-a}function O(a,b){var c,d,i,e,g,k,o=[ [...]
+j.ext.oSort,l=a.aoData,q=a.aoColumns,G=a.oLanguage.oAria;if(!a.oFeatures.bServerSide&&(0!==a.aaSorting.length||null!==a.aaSortingFixed)){o=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(c=0;c<o.length;c++)if(d=o[c][0],i=R(a,d),e=a.aoColumns[d].sSortDataType,j.ext.afnSortData[e])if(g=j.ext.afnSortData[e].call(a.oInstance,a,d,i),g.length===l.length){i=0;for(e=l.length;i<e;i++)F(a,i,d,g[i])}else D(a,0,"Returned data sort array (col "+d+") is the wrong l [...]
+0;for(d=a.aiDisplayMaster.length;c<d;c++)m[a.aiDisplayMaster[c]]=c;var r=o.length,s;c=0;for(d=l.length;c<d;c++)for(i=0;i<r;i++){s=q[o[i][0]].aDataSort;g=0;for(k=s.length;g<k;g++)e=q[s[g]].sType,e=p[(e?e:"string")+"-pre"],l[c]._aSortData[s[g]]=e?e(v(a,c,s[g],"sort")):v(a,c,s[g],"sort")}a.aiDisplayMaster.sort(function(a,b){var c,d,e,i,f;for(c=0;c<r;c++){f=q[o[c][0]].aDataSort;d=0;for(e=f.length;d<e;d++)if(i=q[f[d]].sType,i=p[(i?i:"string")+"-"+o[c][1]](l[a]._aSortData[f[d]],l[b]._aSortData [...]
+i)return i}return p["numeric-asc"](m[a],m[b])})}(b===n||b)&&!a.oFeatures.bDeferRender&&P(a);c=0;for(d=a.aoColumns.length;c<d;c++)e=q[c].sTitle.replace(/<.*?>/g,""),i=q[c].nTh,i.removeAttribute("aria-sort"),i.removeAttribute("aria-label"),q[c].bSortable?0<o.length&&o[0][0]==c?(i.setAttribute("aria-sort","asc"==o[0][1]?"ascending":"descending"),i.setAttribute("aria-label",e+("asc"==(q[c].asSorting[o[0][2]+1]?q[c].asSorting[o[0][2]+1]:q[c].asSorting[0])?G.sSortAscending:G.sSortDescending))) [...]
+e+("asc"==q[c].asSorting[0]?G.sSortAscending:G.sSortDescending)):i.setAttribute("aria-label",e);a.bSorted=!0;h(a.oInstance).trigger("sort",a);a.oFeatures.bFilter?K(a,a.oPreviousSearch,1):(a.aiDisplay=a.aiDisplayMaster.slice(),a._iDisplayStart=0,y(a),x(a))}function ia(a,b,c,d){Ra(b,{},function(b){if(!1!==a.aoColumns[c].bSortable){var e=function(){var d,e;if(b.shiftKey){for(var f=!1,h=0;h<a.aaSorting.length;h++)if(a.aaSorting[h][0]==c){f=!0;d=a.aaSorting[h][0];e=a.aaSorting[h][2]+1;a.aoCol [...]
+(a.aaSorting[h][1]=a.aoColumns[d].asSorting[e],a.aaSorting[h][2]=e):a.aaSorting.splice(h,1);break}!1===f&&a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0])}else 1==a.aaSorting.length&&a.aaSorting[0][0]==c?(d=a.aaSorting[0][0],e=a.aaSorting[0][2]+1,a.aoColumns[d].asSorting[e]||(e=0),a.aaSorting[0][1]=a.aoColumns[d].asSorting[e],a.aaSorting[0][2]=e):(a.aaSorting.splice(0,a.aaSorting.length),a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0]));O(a)};a.oFeatures.bProcessing?(E(a,!0),setTim [...]
+a.oFeatures.bServerSide||E(a,!1)},0)):e();"function"==typeof d&&d(a)}})}function P(a){var b,c,d,e,f,g=a.aoColumns.length,j=a.oClasses;for(b=0;b<g;b++)a.aoColumns[b].bSortable&&h(a.aoColumns[b].nTh).removeClass(j.sSortAsc+" "+j.sSortDesc+" "+a.aoColumns[b].sSortingClass);c=null!==a.aaSortingFixed?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(b=0;b<a.aoColumns.length;b++)if(a.aoColumns[b].bSortable){f=a.aoColumns[b].sSortingClass;e=-1;for(d=0;d<c.length;d++)if(c[d][0]==b){f= [...]
+j.sSortAsc:j.sSortDesc;e=d;break}h(a.aoColumns[b].nTh).addClass(f);a.bJUI&&(f=h("span."+j.sSortIcon,a.aoColumns[b].nTh),f.removeClass(j.sSortJUIAsc+" "+j.sSortJUIDesc+" "+j.sSortJUI+" "+j.sSortJUIAscAllowed+" "+j.sSortJUIDescAllowed),f.addClass(-1==e?a.aoColumns[b].sSortingClassJUI:"asc"==c[e][1]?j.sSortJUIAsc:j.sSortJUIDesc))}else h(a.aoColumns[b].nTh).addClass(a.aoColumns[b].sSortingClass);f=j.sSortColumn;if(a.oFeatures.bSort&&a.oFeatures.bSortClasses){a=J(a);e=[];for(b=0;b<g;b++)e.pus [...]
+for(d=1;b<c.length;b++)j=parseInt(c[b][0],10),e[j]=f+d,3>d&&d++;f=RegExp(f+"[123]");var o;b=0;for(c=a.length;b<c;b++)j=b%g,d=a[b].className,o=e[j],j=d.replace(f,o),j!=d?a[b].className=h.trim(j):0<o.length&&-1==d.indexOf(o)&&(a[b].className=d+" "+o)}}function ra(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b,c;b=a.oScroll.bInfinite;var d={iCreate:(new Date).getTime(),iStart:b?0:a._iDisplayStart,iEnd:b?a._iDisplayLength:a._iDisplayEnd,iLength:a._iDisplayLength,aaSorting:h.extend(!0,[] [...]
+oSearch:h.extend(!0,{},a.oPreviousSearch),aoSearchCols:h.extend(!0,[],a.aoPreSearchCols),abVisCols:[]};b=0;for(c=a.aoColumns.length;b<c;b++)d.abVisCols.push(a.aoColumns[b].bVisible);A(a,"aoStateSaveParams","stateSaveParams",[a,d]);a.fnStateSave.call(a.oInstance,a,d)}}function Sa(a,b){if(a.oFeatures.bStateSave){var c=a.fnStateLoad.call(a.oInstance,a);if(c){var d=A(a,"aoStateLoadParams","stateLoadParams",[a,c]);if(-1===h.inArray(!1,d)){a.oLoadedState=h.extend(!0,{},c);a._iDisplayStart=c.iS [...]
+c.iStart;a._iDisplayEnd=c.iEnd;a._iDisplayLength=c.iLength;a.aaSorting=c.aaSorting.slice();a.saved_aaSorting=c.aaSorting.slice();h.extend(a.oPreviousSearch,c.oSearch);h.extend(!0,a.aoPreSearchCols,c.aoSearchCols);b.saved_aoColumns=[];for(d=0;d<c.abVisCols.length;d++)b.saved_aoColumns[d]={},b.saved_aoColumns[d].bVisible=c.abVisCols[d];A(a,"aoStateLoaded","stateLoaded",[a,c])}}}}function s(a){for(var b=0;b<j.settings.length;b++)if(j.settings[b].nTable===a)return j.settings[b];return null}f [...]
+[],a=a.aoData,c=0,d=a.length;c<d;c++)null!==a[c].nTr&&b.push(a[c].nTr);return b}function J(a,b){var c=[],d,e,f,g,h,j;e=0;var o=a.aoData.length;b!==n&&(e=b,o=b+1);for(f=e;f<o;f++)if(j=a.aoData[f],null!==j.nTr){e=[];for(d=j.nTr.firstChild;d;)g=d.nodeName.toLowerCase(),("td"==g||"th"==g)&&e.push(d),d=d.nextSibling;g=d=0;for(h=a.aoColumns.length;g<h;g++)a.aoColumns[g].bVisible?c.push(e[g-d]):(c.push(j._anHidden[g]),d++)}return c}function D(a,b,c){a=null===a?"DataTables warning: "+c:"DataTabl [...]
+a.sTableId+"'): "+c;if(0===b)if("alert"==j.ext.sErrMode)alert(a);else throw Error(a);else X.console&&console.log&&console.log(a)}function p(a,b,c,d){d===n&&(d=c);b[c]!==n&&(a[d]=b[c])}function Ta(a,b){var c,d;for(d in b)b.hasOwnProperty(d)&&(c=b[d],"object"===typeof e[d]&&null!==c&&!1===h.isArray(c)?h.extend(!0,a[d],c):a[d]=c);return a}function Ra(a,b,c){h(a).bind("click.DT",b,function(b){a.blur();c(b)}).bind("keypress.DT",b,function(a){13===a.which&&c(a)}).bind("selectstart.DT",function [...]
+function z(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function A(a,b,c,d){for(var b=a[b],e=[],f=b.length-1;0<=f;f--)e.push(b[f].fn.apply(a.oInstance,d));null!==c&&h(a.oInstance).trigger(c,d);return e}function Ua(a){var b=h('<div style="position:absolute; top:0; left:0; height:1px; width:1px; overflow:hidden"><div style="position:absolute; top:1px; left:1px; width:100px; overflow:scroll;"><div id="DT_BrowserTest" style="width:100%; height:10px;"></div></div></div>')[0];l.body.appendChild(b);a. [...]
+100===h("#DT_BrowserTest",b)[0].offsetWidth?!0:!1;l.body.removeChild(b)}function Va(a){return function(){var b=[s(this[j.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return j.ext.oApi[a].apply(this,b)}}var U=/\[.*?\]$/,Wa=X.JSON?JSON.stringify:function(a){var b=typeof a;if("object"!==b||null===a)return"string"===b&&(a='"'+a+'"'),a+"";var c,d,e=[],f=h.isArray(a);for(c in a)d=a[c],b=typeof d,"string"===b?d='"'+d+'"':"object"===b&&null!==d&&(d=Wa(d)),e.push((f?"":'"'+c+'": [...]
+"[":"{")+e+(f?"]":"}")};this.$=function(a,b){var c,d,e=[],f;d=s(this[j.ext.iApiIndex]);var g=d.aoData,o=d.aiDisplay,k=d.aiDisplayMaster;b||(b={});b=h.extend({},{filter:"none",order:"current",page:"all"},b);if("current"==b.page){c=d._iDisplayStart;for(d=d.fnDisplayEnd();c<d;c++)(f=g[o[c]].nTr)&&e.push(f)}else if("current"==b.order&&"none"==b.filter){c=0;for(d=k.length;c<d;c++)(f=g[k[c]].nTr)&&e.push(f)}else if("current"==b.order&&"applied"==b.filter){c=0;for(d=o.length;c<d;c++)(f=g[o[c]]. [...]
+b.order&&"none"==b.filter){c=0;for(d=g.length;c<d;c++)(f=g[c].nTr)&&e.push(f)}else if("original"==b.order&&"applied"==b.filter){c=0;for(d=g.length;c<d;c++)f=g[c].nTr,-1!==h.inArray(c,o)&&f&&e.push(f)}else D(d,1,"Unknown selection options");e=h(e);c=e.filter(a);e=e.find(a);return h([].concat(h.makeArray(c),h.makeArray(e)))};this._=function(a,b){var c=[],d,e,f=this.$(a,b);d=0;for(e=f.length;d<e;d++)c.push(this.fnGetData(f[d]));return c};this.fnAddData=function(a,b){if(0===a.length)return[] [...]
+d,e=s(this[j.ext.iApiIndex]);if("object"===typeof a[0]&&null!==a[0])for(var f=0;f<a.length;f++){d=H(e,a[f]);if(-1==d)return c;c.push(d)}else{d=H(e,a);if(-1==d)return c;c.push(d)}e.aiDisplay=e.aiDisplayMaster.slice();(b===n||b)&&aa(e);return c};this.fnAdjustColumnSizing=function(a){var b=s(this[j.ext.iApiIndex]);k(b);a===n||a?this.fnDraw(!1):(""!==b.oScroll.sX||""!==b.oScroll.sY)&&this.oApi._fnScrollDraw(b)};this.fnClearTable=function(a){var b=s(this[j.ext.iApiIndex]);ga(b);(a===n||a)&&x( [...]
+function(a){for(var b=s(this[j.ext.iApiIndex]),c=0;c<b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return(a=b.aoOpenRows[c].nTr.parentNode)&&a.removeChild(b.aoOpenRows[c].nTr),b.aoOpenRows.splice(c,1),0;return 1};this.fnDeleteRow=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,a="object"===typeof a?I(d,a):a,g=d.aoData.splice(a,1);e=0;for(f=d.aoData.length;e<f;e++)null!==d.aoData[e].nTr&&(d.aoData[e].nTr._DT_RowIndex=e);e=h.inArray(a,d.aiDisplay);d.asDataSearch.splice(e,1);ha(d [...]
+a);ha(d.aiDisplay,a);"function"===typeof b&&b.call(this,d,g);d._iDisplayStart>=d.fnRecordsDisplay()&&(d._iDisplayStart-=d._iDisplayLength,0>d._iDisplayStart&&(d._iDisplayStart=0));if(c===n||c)y(d),x(d);return g};this.fnDestroy=function(a){var b=s(this[j.ext.iApiIndex]),c=b.nTableWrapper.parentNode,d=b.nTBody,i,f,a=a===n?!1:a;b.bDestroying=!0;A(b,"aoDestroyCallback","destroy",[b]);if(!a){i=0;for(f=b.aoColumns.length;i<f;i++)!1===b.aoColumns[i].bVisible&&this.fnSetColumnVis(i,!0)}h(b.nTabl [...]
+h("tbody>tr>td."+b.oClasses.sRowEmpty,b.nTable).parent().remove();b.nTable!=b.nTHead.parentNode&&(h(b.nTable).children("thead").remove(),b.nTable.appendChild(b.nTHead));b.nTFoot&&b.nTable!=b.nTFoot.parentNode&&(h(b.nTable).children("tfoot").remove(),b.nTable.appendChild(b.nTFoot));b.nTable.parentNode.removeChild(b.nTable);h(b.nTableWrapper).remove();b.aaSorting=[];b.aaSortingFixed=[];P(b);h(T(b)).removeClass(b.asStripeClasses.join(" "));h("th, td",b.nTHead).removeClass([b.oClasses.sSorta [...]
+b.oClasses.sSortableDesc,b.oClasses.sSortableNone].join(" "));b.bJUI&&(h("th span."+b.oClasses.sSortIcon+", td span."+b.oClasses.sSortIcon,b.nTHead).remove(),h("th, td",b.nTHead).each(function(){var a=h("div."+b.oClasses.sSortJUIWrapper,this),c=a.contents();h(this).append(c);a.remove()}));!a&&b.nTableReinsertBefore?c.insertBefore(b.nTable,b.nTableReinsertBefore):a||c.appendChild(b.nTable);i=0;for(f=b.aoData.length;i<f;i++)null!==b.aoData[i].nTr&&d.appendChild(b.aoData[i].nTr);!0===b.oFea [...]
+(b.nTable.style.width=q(b.sDestroyWidth));if(f=b.asDestroyStripes.length){a=h(d).children("tr");for(i=0;i<f;i++)a.filter(":nth-child("+f+"n + "+i+")").addClass(b.asDestroyStripes[i])}i=0;for(f=j.settings.length;i<f;i++)j.settings[i]==b&&j.settings.splice(i,1);e=b=null};this.fnDraw=function(a){var b=s(this[j.ext.iApiIndex]);!1===a?(y(b),x(b)):aa(b)};this.fnFilter=function(a,b,c,d,e,f){var g=s(this[j.ext.iApiIndex]);if(g.oFeatures.bFilter){if(c===n||null===c)c=!1;if(d===n||null===d)d=!0;if [...]
+e)e=!0;if(f===n||null===f)f=!0;if(b===n||null===b){if(K(g,{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f},1),e&&g.aanFeatures.f){b=g.aanFeatures.f;c=0;for(d=b.length;c<d;c++)try{b[c]._DT_Input!=l.activeElement&&h(b[c]._DT_Input).val(a)}catch(o){h(b[c]._DT_Input).val(a)}}}else h.extend(g.aoPreSearchCols[b],{sSearch:a+"",bRegex:c,bSmart:d,bCaseInsensitive:f}),K(g,g.oPreviousSearch,1)}};this.fnGetData=function(a,b){var c=s(this[j.ext.iApiIndex]);if(a!==n){var d=a;if("object"===typeof a) [...]
+"tr"===e?d=I(c,a):"td"===e&&(d=I(c,a.parentNode),b=fa(c,d,a))}return b!==n?v(c,d,b,""):c.aoData[d]!==n?c.aoData[d]._aData:null}return Z(c)};this.fnGetNodes=function(a){var b=s(this[j.ext.iApiIndex]);return a!==n?b.aoData[a]!==n?b.aoData[a].nTr:null:T(b)};this.fnGetPosition=function(a){var b=s(this[j.ext.iApiIndex]),c=a.nodeName.toUpperCase();return"TR"==c?I(b,a):"TD"==c||"TH"==c?(c=I(b,a.parentNode),a=fa(b,c,a),[c,R(b,a),a]):null};this.fnIsOpen=function(a){for(var b=s(this[j.ext.iApiInde [...]
+b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a)return!0;return!1};this.fnOpen=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e=T(d);if(-1!==h.inArray(a,e)){this.fnClose(a);var e=l.createElement("tr"),f=l.createElement("td");e.appendChild(f);f.className=c;f.colSpan=t(d);"string"===typeof b?f.innerHTML=b:h(f).html(b);b=h("tr",d.nTBody);-1!=h.inArray(a,b)&&h(e).insertAfter(a);d.aoOpenRows.push({nTr:e,nParent:a});return e}};this.fnPageChange=function(a,b){var c=s(this[j.ext.iApiIndex] [...]
+y(c);(b===n||b)&&x(c)};this.fnSetColumnVis=function(a,b,c){var d=s(this[j.ext.iApiIndex]),e,f,g=d.aoColumns,h=d.aoData,o,m;if(g[a].bVisible!=b){if(b){for(e=f=0;e<a;e++)g[e].bVisible&&f++;m=f>=t(d);if(!m)for(e=a;e<g.length;e++)if(g[e].bVisible){o=e;break}e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(m?h[e].nTr.appendChild(h[e]._anHidden[a]):h[e].nTr.insertBefore(h[e]._anHidden[a],J(d,e)[o]))}else{e=0;for(f=h.length;e<f;e++)null!==h[e].nTr&&(o=J(d,e)[a],h[e]._anHidden[a]=o,o.parentNode.remo [...]
+b;W(d,d.aoHeader);d.nTFoot&&W(d,d.aoFooter);e=0;for(f=d.aoOpenRows.length;e<f;e++)d.aoOpenRows[e].nTr.colSpan=t(d);if(c===n||c)k(d),x(d);ra(d)}};this.fnSettings=function(){return s(this[j.ext.iApiIndex])};this.fnSort=function(a){var b=s(this[j.ext.iApiIndex]);b.aaSorting=a;O(b)};this.fnSortListener=function(a,b,c){ia(s(this[j.ext.iApiIndex]),a,b,c)};this.fnUpdate=function(a,b,c,d,e){var f=s(this[j.ext.iApiIndex]),b="object"===typeof b?I(f,b):b;if(h.isArray(a)&&c===n){f.aoData[b]._aData=a [...]
+for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else if(h.isPlainObject(a)&&c===n){f.aoData[b]._aData=h.extend(!0,{},a);for(c=0;c<f.aoColumns.length;c++)this.fnUpdate(v(f,b,c),b,c,!1,!1)}else{F(f,b,c,a);var a=v(f,b,c,"display"),g=f.aoColumns[c];null!==g.fnRender&&(a=S(f,b,c),g.bUseRendered&&F(f,b,c,a));null!==f.aoData[b].nTr&&(J(f,b)[c].innerHTML=a)}c=h.inArray(b,f.aiDisplay);f.asDataSearch[c]=na(f,Y(f,b,"filter",r(f,"bSearchable")));(e===n||e)&&k(f);(d===n||d)&&aa(f); [...]
+this.fnVersionCheck=j.ext.fnVersionCheck;this.oApi={_fnExternApiFunc:Va,_fnInitialise:ba,_fnInitComplete:$,_fnLanguageCompat:pa,_fnAddColumn:o,_fnColumnOptions:m,_fnAddData:H,_fnCreateTr:ea,_fnGatherData:ua,_fnBuildHead:va,_fnDrawHead:W,_fnDraw:x,_fnReDraw:aa,_fnAjaxUpdate:wa,_fnAjaxParameters:Ea,_fnAjaxUpdateDraw:Fa,_fnServerParams:ka,_fnAddOptionsHtml:xa,_fnFeatureHtmlTable:Ba,_fnScrollDraw:La,_fnAdjustColumnSizing:k,_fnFeatureHtmlFilter:za,_fnFilterComplete:K,_fnFilterCustom:Ia,_fnFil [...]
+_fnFilter:Ga,_fnBuildSearchArray:la,_fnBuildSearchRow:na,_fnFilterCreateSearch:ma,_fnDataToSearch:Ja,_fnSort:O,_fnSortAttachListener:ia,_fnSortingClasses:P,_fnFeatureHtmlPaginate:Da,_fnPageChange:qa,_fnFeatureHtmlInfo:Ca,_fnUpdateInfo:Ka,_fnFeatureHtmlLength:ya,_fnFeatureHtmlProcessing:Aa,_fnProcessingDisplay:E,_fnVisibleToColumnIndex:G,_fnColumnIndexToVisible:R,_fnNodeToDataIndex:I,_fnVisbleColumns:t,_fnCalculateEnd:y,_fnConvertToWidth:Ma,_fnCalculateColumnWidths:da,_fnScrollingWidthAdj [...]
+_fnGetMaxLenString:Pa,_fnStringToCss:q,_fnDetectType:B,_fnSettingsFromNode:s,_fnGetDataMaster:Z,_fnGetTrNodes:T,_fnGetTdNodes:J,_fnEscapeRegex:oa,_fnDeleteIndex:ha,_fnReOrderIndex:u,_fnColumnOrdering:M,_fnLog:D,_fnClearTable:ga,_fnSaveState:ra,_fnLoadState:Sa,_fnCreateCookie:function(a,b,c,d,e){var f=new Date;f.setTime(f.getTime()+1E3*c);var c=X.location.pathname.split("/"),a=a+"_"+c.pop().replace(/[\/:]/g,"").toLowerCase(),g;null!==e?(g="function"===typeof h.parseJSON?h.parseJSON(b):eva [...]
+b=e(a,g,f.toGMTString(),c.join("/")+"/")):b=a+"="+encodeURIComponent(b)+"; expires="+f.toGMTString()+"; path="+c.join("/")+"/";a=l.cookie.split(";");e=b.split(";")[0].length;f=[];if(4096<e+l.cookie.length+10){for(var j=0,o=a.length;j<o;j++)if(-1!=a[j].indexOf(d)){var k=a[j].split("=");try{(g=eval("("+decodeURIComponent(k[1])+")"))&&g.iCreate&&f.push({name:k[0],time:g.iCreate})}catch(m){}}for(f.sort(function(a,b){return b.time-a.time});4096<e+l.cookie.length+10;){if(0===f.length)return;d= [...]
+d.name+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+c.join("/")+"/"}}l.cookie=b},_fnReadCookie:function(a){for(var b=X.location.pathname.split("/"),a=a+"_"+b[b.length-1].replace(/[\/:]/g,"").toLowerCase()+"=",b=l.cookie.split(";"),c=0;c<b.length;c++){for(var d=b[c];" "==d.charAt(0);)d=d.substring(1,d.length);if(0===d.indexOf(a))return decodeURIComponent(d.substring(a.length,d.length))}return null},_fnDetectHeader:V,_fnGetUniqueThs:N,_fnScrollBarWidth:Qa,_fnApplyToChildren:C,_fnMap:p [...]
+_fnGetCellData:v,_fnSetCellData:F,_fnGetObjectDataFn:Q,_fnSetObjectDataFn:L,_fnApplyColumnDefs:ta,_fnBindAction:Ra,_fnExtend:Ta,_fnCallbackReg:z,_fnCallbackFire:A,_fnJsonString:Wa,_fnRender:S,_fnNodeToColumnIndex:fa,_fnInfoMacros:ja,_fnBrowserDetect:Ua,_fnGetColumns:r};h.extend(j.ext.oApi,this.oApi);for(var sa in j.ext.oApi)sa&&(this[sa]=Va(sa));var ca=this;this.each(function(){var a=0,b,c,d;c=this.getAttribute("id");var i=!1,f=!1;if("table"!=this.nodeName.toLowerCase())D(null,0,"Attempt [...]
+this.nodeName);else{a=0;for(b=j.settings.length;a<b;a++){if(j.settings[a].nTable==this){if(e===n||e.bRetrieve)return j.settings[a].oInstance;if(e.bDestroy){j.settings[a].oInstance.fnDestroy();break}else{D(j.settings[a],0,"Cannot reinitialise DataTable.\n\nTo retrieve the DataTables object for this table, pass no arguments or see the docs for bRetrieve and bDestroy");return}}if(j.settings[a].sTableId==this.id){j.settings.splice(a,1);break}}if(null===c||""===c)this.id=c="DataTables_Table_" [...]
+var g=h.extend(!0,{},j.models.oSettings,{nTable:this,oApi:ca.oApi,oInit:e,sDestroyWidth:h(this).width(),sInstance:c,sTableId:c});j.settings.push(g);g.oInstance=1===ca.length?ca:h(this).dataTable();e||(e={});e.oLanguage&&pa(e.oLanguage);e=Ta(h.extend(!0,{},j.defaults),e);p(g.oFeatures,e,"bPaginate");p(g.oFeatures,e,"bLengthChange");p(g.oFeatures,e,"bFilter");p(g.oFeatures,e,"bSort");p(g.oFeatures,e,"bInfo");p(g.oFeatures,e,"bProcessing");p(g.oFeatures,e,"bAutoWidth");p(g.oFeatures,e,"bSor [...]
+p(g.oFeatures,e,"bServerSide");p(g.oFeatures,e,"bDeferRender");p(g.oScroll,e,"sScrollX","sX");p(g.oScroll,e,"sScrollXInner","sXInner");p(g.oScroll,e,"sScrollY","sY");p(g.oScroll,e,"bScrollCollapse","bCollapse");p(g.oScroll,e,"bScrollInfinite","bInfinite");p(g.oScroll,e,"iScrollLoadGap","iLoadGap");p(g.oScroll,e,"bScrollAutoCss","bAutoCss");p(g,e,"asStripeClasses");p(g,e,"asStripClasses","asStripeClasses");p(g,e,"fnServerData");p(g,e,"fnFormatNumber");p(g,e,"sServerMethod");p(g,e,"aaSorti [...]
+e,"aaSortingFixed");p(g,e,"aLengthMenu");p(g,e,"sPaginationType");p(g,e,"sAjaxSource");p(g,e,"sAjaxDataProp");p(g,e,"iCookieDuration");p(g,e,"sCookiePrefix");p(g,e,"sDom");p(g,e,"bSortCellsTop");p(g,e,"iTabIndex");p(g,e,"oSearch","oPreviousSearch");p(g,e,"aoSearchCols","aoPreSearchCols");p(g,e,"iDisplayLength","_iDisplayLength");p(g,e,"bJQueryUI","bJUI");p(g,e,"fnCookieCallback");p(g,e,"fnStateLoad");p(g,e,"fnStateSave");p(g.oLanguage,e,"fnInfoCallback");z(g,"aoDrawCallback",e.fnDrawCall [...]
+z(g,"aoServerParams",e.fnServerParams,"user");z(g,"aoStateSaveParams",e.fnStateSaveParams,"user");z(g,"aoStateLoadParams",e.fnStateLoadParams,"user");z(g,"aoStateLoaded",e.fnStateLoaded,"user");z(g,"aoRowCallback",e.fnRowCallback,"user");z(g,"aoRowCreatedCallback",e.fnCreatedRow,"user");z(g,"aoHeaderCallback",e.fnHeaderCallback,"user");z(g,"aoFooterCallback",e.fnFooterCallback,"user");z(g,"aoInitComplete",e.fnInitComplete,"user");z(g,"aoPreDrawCallback",e.fnPreDrawCallback,"user");g.oFea [...]
+g.oFeatures.bSort&&g.oFeatures.bSortClasses?z(g,"aoDrawCallback",P,"server_side_sort_classes"):g.oFeatures.bDeferRender&&z(g,"aoDrawCallback",P,"defer_sort_classes");e.bJQueryUI?(h.extend(g.oClasses,j.ext.oJUIClasses),e.sDom===j.defaults.sDom&&"lfrtip"===j.defaults.sDom&&(g.sDom='<"H"lfr>t<"F"ip>')):h.extend(g.oClasses,j.ext.oStdClasses);h(this).addClass(g.oClasses.sTable);if(""!==g.oScroll.sX||""!==g.oScroll.sY)g.oScroll.iBarWidth=Qa();g.iInitDisplayStart===n&&(g.iInitDisplayStart=e.iDi [...]
+g._iDisplayStart=e.iDisplayStart);e.bStateSave&&(g.oFeatures.bStateSave=!0,Sa(g,e),z(g,"aoDrawCallback",ra,"state_save"));null!==e.iDeferLoading&&(g.bDeferLoading=!0,a=h.isArray(e.iDeferLoading),g._iRecordsDisplay=a?e.iDeferLoading[0]:e.iDeferLoading,g._iRecordsTotal=a?e.iDeferLoading[1]:e.iDeferLoading);null!==e.aaData&&(f=!0);""!==e.oLanguage.sUrl?(g.oLanguage.sUrl=e.oLanguage.sUrl,h.getJSON(g.oLanguage.sUrl,null,function(a){pa(a);h.extend(true,g.oLanguage,e.oLanguage,a);ba(g)}),i=!0): [...]
+g.oLanguage,e.oLanguage);null===e.asStripeClasses&&(g.asStripeClasses=[g.oClasses.sStripeOdd,g.oClasses.sStripeEven]);b=g.asStripeClasses.length;g.asDestroyStripes=[];if(b){c=!1;d=h(this).children("tbody").children("tr:lt("+b+")");for(a=0;a<b;a++)d.hasClass(g.asStripeClasses[a])&&(c=!0,g.asDestroyStripes.push(g.asStripeClasses[a]));c&&d.removeClass(g.asStripeClasses.join(" "))}c=[];a=this.getElementsByTagName("thead");0!==a.length&&(V(g.aoHeader,a[0]),c=N(g));if(null===e.aoColumns){d=[]; [...]
+c.length;a<b;a++)d.push(null)}else d=e.aoColumns;a=0;for(b=d.length;a<b;a++)e.saved_aoColumns!==n&&e.saved_aoColumns.length==b&&(null===d[a]&&(d[a]={}),d[a].bVisible=e.saved_aoColumns[a].bVisible),o(g,c?c[a]:null);ta(g,e.aoColumnDefs,d,function(a,b){m(g,a,b)});a=0;for(b=g.aaSorting.length;a<b;a++){g.aaSorting[a][0]>=g.aoColumns.length&&(g.aaSorting[a][0]=0);var k=g.aoColumns[g.aaSorting[a][0]];g.aaSorting[a][2]===n&&(g.aaSorting[a][2]=0);e.aaSorting===n&&g.saved_aaSorting===n&&(g.aaSorti [...]
+k.asSorting[0]);c=0;for(d=k.asSorting.length;c<d;c++)if(g.aaSorting[a][1]==k.asSorting[c]){g.aaSorting[a][2]=c;break}}P(g);Ua(g);a=h(this).children("caption").each(function(){this._captionSide=h(this).css("caption-side")});b=h(this).children("thead");0===b.length&&(b=[l.createElement("thead")],this.appendChild(b[0]));g.nTHead=b[0];b=h(this).children("tbody");0===b.length&&(b=[l.createElement("tbody")],this.appendChild(b[0]));g.nTBody=b[0];g.nTBody.setAttribute("role","alert");g.nTBody.se [...]
+"polite");g.nTBody.setAttribute("aria-relevant","all");b=h(this).children("tfoot");if(0===b.length&&0<a.length&&(""!==g.oScroll.sX||""!==g.oScroll.sY))b=[l.createElement("tfoot")],this.appendChild(b[0]);0<b.length&&(g.nTFoot=b[0],V(g.aoFooter,g.nTFoot));if(f)for(a=0;a<e.aaData.length;a++)H(g,e.aaData[a]);else ua(g);g.aiDisplay=g.aiDisplayMaster.slice();g.bInitialised=!0;!1===i&&ba(g)}});ca=null;return this};j.fnVersionCheck=function(e){for(var h=function(e,h){for(;e.length<h;)e+="0";retu [...]
+e=e.split("."),k="",n="",l=0,t=e.length;l<t;l++)k+=h(m[l],3),n+=h(e[l],3);return parseInt(k,10)>=parseInt(n,10)};j.fnIsDataTable=function(e){for(var h=j.settings,m=0;m<h.length;m++)if(h[m].nTable===e||h[m].nScrollHead===e||h[m].nScrollFoot===e)return!0;return!1};j.fnTables=function(e){var o=[];jQuery.each(j.settings,function(j,k){(!e||!0===e&&h(k.nTable).is(":visible"))&&o.push(k.nTable)});return o};j.version="1.9.4";j.settings=[];j.models={};j.models.ext={afnFiltering:[],afnSortData:[], [...]
+aTypes:[],fnVersionCheck:j.fnVersionCheck,iApiIndex:0,ofnSearch:{},oApi:{},oStdClasses:{},oJUIClasses:{},oPagination:{},oSort:{},sVersion:j.version,sErrMode:"alert",_oExternConfig:{iNextUnique:0}};j.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};j.models.oRow={nTr:null,_aData:[],_aSortData:[],_anHidden:[],_sRowStripe:""};j.models.oColumn={aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bUseRendered:null,bVisible:null,_bAutoType:!0,fnCreatedCell:null,fnG [...]
+fnRender:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};j.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLeng [...]
+bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollAutoCss:!0,bScrollCollapse:!1,bScrollInfinite:!1,bServerSide:!1,bSort:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCookieCallback:null,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(e){if(1E3>e)return e;for(var h=e+"",e=h.split(""),j="",h=h.length,k=0;k<h;k++)0===k%3&&0!==k&&(j=this.oLanguage.sInfoThousands+j),j=e[h-k-1]+j;return j},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnP [...]
+fnRowCallback:null,fnServerData:function(e,j,m,k){k.jqXHR=h.ajax({url:e,data:j,success:function(e){e.sError&&k.oApi._fnLog(k,0,e.sError);h(k.oInstance).trigger("xhr",[k,e]);m(e)},dataType:"json",cache:!1,type:k.sServerMethod,error:function(e,h){"parsererror"==h&&k.oApi._fnLog(k,0,"DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.")}})},fnServerParams:null,fnStateLoad:function(e){var e=this.oApi._fnReadCookie(e.sCookiePrefix+e.sInsta [...]
+"function"===typeof h.parseJSON?h.parseJSON(e):eval("("+e+")")}catch(m){j=null}return j},fnStateLoadParams:null,fnStateLoaded:null,fnStateSave:function(e,h){this.oApi._fnCreateCookie(e.sCookiePrefix+e.sInstance,this.oApi._fnJsonString(h),e.iCookieDuration,e.sCookiePrefix,e.fnCookieCallback)},fnStateSaveParams:null,iCookieDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iScrollLoadGap:100,iTabIndex:0,oLanguage:{oAria:{sSortAscending:": activate to sort column ascending", [...]
+oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sInfoThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},j.models.oSearch), [...]
+sAjaxSource:null,sCookiePrefix:"SpryMedia_DataTables_",sDom:"lfrtip",sPaginationType:"two_button",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET"};j.defaults.columns={aDataSort:null,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bUseRendered:!0,bVisible:!0,fnCreatedCell:null,fnRender:null,iDataSort:-1,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};j.models.oSettin [...]
+bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortClasses:null,bStateSave:null},oScroll:{bAutoCss:null,bCollapse:null,bInfinite:null,iBarWidth:0,iLoadGap:null,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1},aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aoColumns:[],aoHeader:[],aoFooter:[],asDataSearch:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aa [...]
+asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,sPaginationType:"two_button",iCookieDuration:0,sCookiePrefix:"",fnCookieCallback:null,aoStateSave:[],a [...]
+oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iDisplayEnd:10,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsTotal,10):this.ai [...]
+fnRecordsDisplay:function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsDisplay,10):this.aiDisplay.length},fnDisplayEnd:function(){return this.oFeatures.bServerSide?!1===this.oFeatures.bPaginate||-1==this._iDisplayLength?this._iDisplayStart+this.aiDisplay.length:Math.min(this._iDisplayStart+this._iDisplayLength,this._iRecordsDisplay):this._iDisplayEnd},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null};j.ext=h.extend(!0,{},j.models.ext);h.extend(j.e [...]
+{sTable:"dataTable",sPagePrevEnabled:"paginate_enabled_previous",sPagePrevDisabled:"paginate_disabled_previous",sPageNextEnabled:"paginate_enabled_next",sPageNextDisabled:"paginate_disabled_next",sPageJUINext:"",sPageJUIPrev:"",sPageButton:"paginate_button",sPageButtonActive:"paginate_active",sPageButtonStaticDisabled:"paginate_button paginate_button_disabled",sPageFirst:"first",sPagePrevious:"previous",sPageNext:"next",sPageLast:"last",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"data [...]
+sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSo [...]
+sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sFooterTH:"",sJUIHeader:"",sJUIFooter:""});h.extend(j.ext.oJUIClasses,j.ext.oStdClasses,{sPagePrevEnabled:"fg-button ui-button ui-state-default ui-corner-left",sPagePrevDisabled:"fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",sPageNe [...]
+sPageNextDisabled:"fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",sPageJUINext:"ui-icon ui-icon-circle-arrow-e",sPageJUIPrev:"ui-icon ui-icon-circle-arrow-w",sPageButton:"fg-button ui-button ui-state-default",sPageButtonActive:"fg-button ui-button ui-state-default ui-state-disabled",sPageButtonStaticDisabled:"fg-button ui-button ui-state-default ui-state-disabled",sPageFirst:"first ui-corner-tl ui-corner-bl",sPageLast:"last ui-corner-tr ui-corner-br",sPaging:"dat [...]
+sSortAsc:"ui-state-default",sSortDesc:"ui-state-default",sSortable:"ui-state-default",sSortableAsc:"ui-state-default",sSortableDesc:"ui-state-default",sSortableNone:"ui-state-default",sSortJUIAsc:"css_right ui-icon ui-icon-triangle-1-n",sSortJUIDesc:"css_right ui-icon ui-icon-triangle-1-s",sSortJUI:"css_right ui-icon ui-icon-carat-2-n-s",sSortJUIAscAllowed:"css_right ui-icon ui-icon-carat-1-n",sSortJUIDescAllowed:"css_right ui-icon ui-icon-carat-1-s",sSortJUIWrapper:"DataTables_sort_wrap [...]
+sScrollHead:"dataTables_scrollHead ui-state-default",sScrollFoot:"dataTables_scrollFoot ui-state-default",sFooterTH:"ui-state-default",sJUIHeader:"fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix",sJUIFooter:"fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"});h.extend(j.ext.oPagination,{two_button:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)},k=!e.bJUI?' [...]
+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sPrevious+'</a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button">'+k.sNext+"</a>":'<a class="'+e.oClasses.sPagePrevDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUIPrev+'"></span></a><a class="'+e.oClasses.sPageNextDisabled+'" tabindex="'+e.iTabIndex+'" role="button"><span class="'+e.oClasses.sPageJUINext+'"></span></a>';h(j).append(k);var  [...]
+k=l[0],l=l[1];e.oApi._fnBindAction(k,{action:"previous"},n);e.oApi._fnBindAction(l,{action:"next"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_previous",l.id=e.sTableId+"_next",k.setAttribute("aria-controls",e.sTableId),l.setAttribute("aria-controls",e.sTableId))},fnUpdate:function(e){if(e.aanFeatures.p)for(var h=e.oClasses,j=e.aanFeatures.p,k,l=0,n=j.length;l<n;l++)if(k=j[l].firstChild)k.className=0===e._iDisplayStart?h.sPagePrevDisabled:h.sPagePrevEnabled,k=k.nex [...]
+k.className=e.fnDisplayEnd()==e.fnRecordsDisplay()?h.sPageNextDisabled:h.sPageNextEnabled}},iFullNumbersShowPages:5,full_numbers:{fnInit:function(e,j,m){var k=e.oLanguage.oPaginate,l=e.oClasses,n=function(h){e.oApi._fnPageChange(e,h.data.action)&&m(e)};h(j).append('<a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageFirst+'">'+k.sFirst+'</a><a  tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPagePrevious+'">'+k.sPrevious+'</a><span></span><a tabindex="'+e.iTabIndex+ [...]
+l.sPageButton+" "+l.sPageNext+'">'+k.sNext+'</a><a tabindex="'+e.iTabIndex+'" class="'+l.sPageButton+" "+l.sPageLast+'">'+k.sLast+"</a>");var t=h("a",j),k=t[0],l=t[1],r=t[2],t=t[3];e.oApi._fnBindAction(k,{action:"first"},n);e.oApi._fnBindAction(l,{action:"previous"},n);e.oApi._fnBindAction(r,{action:"next"},n);e.oApi._fnBindAction(t,{action:"last"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_first",l.id=e.sTableId+"_previous",r.id=e.sTableId+"_next",t.id=e.sTableId [...]
+fnUpdate:function(e,o){if(e.aanFeatures.p){var m=j.ext.oPagination.iFullNumbersShowPages,k=Math.floor(m/2),l=Math.ceil(e.fnRecordsDisplay()/e._iDisplayLength),n=Math.ceil(e._iDisplayStart/e._iDisplayLength)+1,t="",r,B=e.oClasses,u,M=e.aanFeatures.p,L=function(h){e.oApi._fnBindAction(this,{page:h+r-1},function(h){e.oApi._fnPageChange(e,h.data.page);o(e);h.preventDefault()})};-1===e._iDisplayLength?n=k=r=1:l<m?(r=1,k=l):n<=k?(r=1,k=m):n>=l-k?(r=l-m+1,k=l):(r=n-Math.ceil(m/2)+1,k=r+m-1);for [...]
+n!==m?'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButton+'">'+e.fnFormatNumber(m)+"</a>":'<a tabindex="'+e.iTabIndex+'" class="'+B.sPageButtonActive+'">'+e.fnFormatNumber(m)+"</a>";m=0;for(k=M.length;m<k;m++)u=M[m],u.hasChildNodes()&&(h("span:eq(0)",u).html(t).children("a").each(L),u=u.getElementsByTagName("a"),u=[u[0],u[1],u[u.length-2],u[u.length-1]],h(u).removeClass(B.sPageButton+" "+B.sPageButtonActive+" "+B.sPageButtonStaticDisabled),h([u[0],u[1]]).addClass(1==n?B.sPageButtonStat [...]
+B.sPageButton),h([u[2],u[3]]).addClass(0===l||n===l||-1===e._iDisplayLength?B.sPageButtonStaticDisabled:B.sPageButton))}}}});h.extend(j.ext.oSort,{"string-pre":function(e){"string"!=typeof e&&(e=null!==e&&e.toString?e.toString():"");return e.toLowerCase()},"string-asc":function(e,h){return e<h?-1:e>h?1:0},"string-desc":function(e,h){return e<h?1:e>h?-1:0},"html-pre":function(e){return e.replace(/<.*?>/g,"").toLowerCase()},"html-asc":function(e,h){return e<h?-1:e>h?1:0},"html-desc":functi [...]
+h?1:e>h?-1:0},"date-pre":function(e){e=Date.parse(e);if(isNaN(e)||""===e)e=Date.parse("01/01/1970 00:00:00");return e},"date-asc":function(e,h){return e-h},"date-desc":function(e,h){return h-e},"numeric-pre":function(e){return"-"==e||""===e?0:1*e},"numeric-asc":function(e,h){return e-h},"numeric-desc":function(e,h){return h-e}});h.extend(j.ext.aTypes,[function(e){if("number"===typeof e)return"numeric";if("string"!==typeof e)return null;var h,j=!1;h=e.charAt(0);if(-1=="0123456789-".indexO [...]
+for(var k=1;k<e.length;k++){h=e.charAt(k);if(-1=="0123456789.".indexOf(h))return null;if("."==h){if(j)return null;j=!0}}return"numeric"},function(e){var h=Date.parse(e);return null!==h&&!isNaN(h)||"string"===typeof e&&0===e.length?"date":null},function(e){return"string"===typeof e&&-1!=e.indexOf("<")&&-1!=e.indexOf(">")?"html":null}]);h.fn.DataTable=j;h.fn.dataTable=j;h.fn.dataTableSettings=j.settings;h.fn.dataTableExt=j.ext};"function"===typeof define&&define.amd?define(["jquery"],L):jQ [...]
+L(jQuery)})(window,document);
diff --git a/web-app/js/DataTables/js/jquery.js b/web-app/js/DataTables/js/jquery.js
new file mode 100644
index 0000000..63174a0
--- /dev/null
+++ b/web-app/js/DataTables/js/jquery.js
@@ -0,0 +1,2 @@
+/*! jQuery v1.8.2 jquery.com | jquery.org/license */
+(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){retur [...]
\ No newline at end of file
diff --git a/web-app/js/jQueryUtils.js b/web-app/js/jQueryUtils.js
new file mode 100644
index 0000000..860b76e
--- /dev/null
+++ b/web-app/js/jQueryUtils.js
@@ -0,0 +1,11 @@
+function jQueryUtils() {
+
+};
+
+jQueryUtils.getURLParameter = function(parameter) {
+	var results = new RegExp('[\\?&]' + parameter + '=([^&#]*)').exec(window.location.href);
+	if (results && results[1]) {
+		return decodeURIComponent(results[1]);
+	}
+	return null;
+}
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png b/web-app/js/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 0000000..5b5dab2
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_flat_0_aaaaaa_40x100.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png b/web-app/js/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 0000000..ac8b229
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_flat_75_ffffff_40x100.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 0000000..ad3d634
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..42ccba2
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 0000000..5a46b47
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 0000000..86c2baa
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 0000000..4443fdc
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_glass_95_fef1ec_1x400.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/web-app/js/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 0000000..7c9fa6c
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-icons_222222_256x240.png b/web-app/js/jquery-ui-menubar/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000..ee039dc
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-icons_222222_256x240.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png b/web-app/js/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000..45e8928
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-icons_2e83ff_256x240.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-icons_454545_256x240.png b/web-app/js/jquery-ui-menubar/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..7ec70d1
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-icons_454545_256x240.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-icons_888888_256x240.png b/web-app/js/jquery-ui-menubar/images/ui-icons_888888_256x240.png
new file mode 100644
index 0000000..5ba708c
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-icons_888888_256x240.png differ
diff --git a/web-app/js/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png b/web-app/js/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000..7930a55
Binary files /dev/null and b/web-app/js/jquery-ui-menubar/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/web-app/js/jquery-ui-menubar/jquery-1.8.2.js b/web-app/js/jquery-ui-menubar/jquery-1.8.2.js
new file mode 100644
index 0000000..d4f3bb3
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery-1.8.2.js
@@ -0,0 +1,9440 @@
+/*!
+ * jQuery JavaScript Library v1.8.2
+ * http://jquery.com/
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: Thu Sep 20 2012 21:13:05 GMT-0400 (Eastern Daylight Time)
+ */
+(function( window, undefined ) {
+var
+	// A central reference to the root jQuery(document)
+	rootjQuery,
+
+	// The deferred used on DOM ready
+	readyList,
+
+	// Use the correct document accordingly with window argument (sandbox)
+	document = window.document,
+	location = window.location,
+	navigator = window.navigator,
+
+	// Map over jQuery in case of overwrite
+	_jQuery = window.jQuery,
+
+	// Map over the $ in case of overwrite
+	_$ = window.$,
+
+	// Save a reference to some core methods
+	core_push = Array.prototype.push,
+	core_slice = Array.prototype.slice,
+	core_indexOf = Array.prototype.indexOf,
+	core_toString = Object.prototype.toString,
+	core_hasOwn = Object.prototype.hasOwnProperty,
+	core_trim = String.prototype.trim,
+
+	// Define a local copy of jQuery
+	jQuery = function( selector, context ) {
+		// The jQuery object is actually just the init constructor 'enhanced'
+		return new jQuery.fn.init( selector, context, rootjQuery );
+	},
+
+	// Used for matching numbers
+	core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,
+
+	// Used for detecting and trimming whitespace
+	core_rnotwhite = /\S/,
+	core_rspace = /\s+/,
+
+	// Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE)
+	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+
+	// A simple way to check for HTML strings
+	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
+	rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
+
+	// Match a standalone tag
+	rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+
+	// JSON RegExp
+	rvalidchars = /^[\],:{}\s]*$/,
+	rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+	rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,
+	rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,
+
+	// Matches dashed string for camelizing
+	rmsPrefix = /^-ms-/,
+	rdashAlpha = /-([\da-z])/gi,
+
+	// Used by jQuery.camelCase as callback to replace()
+	fcamelCase = function( all, letter ) {
+		return ( letter + "" ).toUpperCase();
+	},
+
+	// The ready event handler and self cleanup method
+	DOMContentLoaded = function() {
+		if ( document.addEventListener ) {
+			document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+			jQuery.ready();
+		} else if ( document.readyState === "complete" ) {
+			// we're here because readyState === "complete" in oldIE
+			// which is good enough for us to call the dom ready!
+			document.detachEvent( "onreadystatechange", DOMContentLoaded );
+			jQuery.ready();
+		}
+	},
+
+	// [[Class]] -> type pairs
+	class2type = {};
+
+jQuery.fn = jQuery.prototype = {
+	constructor: jQuery,
+	init: function( selector, context, rootjQuery ) {
+		var match, elem, ret, doc;
+
+		// Handle $(""), $(null), $(undefined), $(false)
+		if ( !selector ) {
+			return this;
+		}
+
+		// Handle $(DOMElement)
+		if ( selector.nodeType ) {
+			this.context = this[0] = selector;
+			this.length = 1;
+			return this;
+		}
+
+		// Handle HTML strings
+		if ( typeof selector === "string" ) {
+			if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+				// Assume that strings that start and end with <> are HTML and skip the regex check
+				match = [ null, selector, null ];
+
+			} else {
+				match = rquickExpr.exec( selector );
+			}
+
+			// Match html or make sure no context is specified for #id
+			if ( match && (match[1] || !context) ) {
+
+				// HANDLE: $(html) -> $(array)
+				if ( match[1] ) {
+					context = context instanceof jQuery ? context[0] : context;
+					doc = ( context && context.nodeType ? context.ownerDocument || context : document );
+
+					// scripts is true for back-compat
+					selector = jQuery.parseHTML( match[1], doc, true );
+					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
+						this.attr.call( selector, context, true );
+					}
+
+					return jQuery.merge( this, selector );
+
+				// HANDLE: $(#id)
+				} else {
+					elem = document.getElementById( match[2] );
+
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE and Opera return items
+						// by name instead of ID
+						if ( elem.id !== match[2] ) {
+							return rootjQuery.find( selector );
+						}
+
+						// Otherwise, we inject the element directly into the jQuery object
+						this.length = 1;
+						this[0] = elem;
+					}
+
+					this.context = document;
+					this.selector = selector;
+					return this;
+				}
+
+			// HANDLE: $(expr, $(...))
+			} else if ( !context || context.jquery ) {
+				return ( context || rootjQuery ).find( selector );
+
+			// HANDLE: $(expr, context)
+			// (which is just equivalent to: $(context).find(expr)
+			} else {
+				return this.constructor( context ).find( selector );
+			}
+
+		// HANDLE: $(function)
+		// Shortcut for document ready
+		} else if ( jQuery.isFunction( selector ) ) {
+			return rootjQuery.ready( selector );
+		}
+
+		if ( selector.selector !== undefined ) {
+			this.selector = selector.selector;
+			this.context = selector.context;
+		}
+
+		return jQuery.makeArray( selector, this );
+	},
+
+	// Start with an empty selector
+	selector: "",
+
+	// The current version of jQuery being used
+	jquery: "1.8.2",
+
+	// The default length of a jQuery object is 0
+	length: 0,
+
+	// The number of elements contained in the matched element set
+	size: function() {
+		return this.length;
+	},
+
+	toArray: function() {
+		return core_slice.call( this );
+	},
+
+	// Get the Nth element in the matched element set OR
+	// Get the whole matched element set as a clean array
+	get: function( num ) {
+		return num == null ?
+
+			// Return a 'clean' array
+			this.toArray() :
+
+			// Return just the object
+			( num < 0 ? this[ this.length + num ] : this[ num ] );
+	},
+
+	// Take an array of elements and push it onto the stack
+	// (returning the new matched element set)
+	pushStack: function( elems, name, selector ) {
+
+		// Build a new jQuery matched element set
+		var ret = jQuery.merge( this.constructor(), elems );
+
+		// Add the old object onto the stack (as a reference)
+		ret.prevObject = this;
+
+		ret.context = this.context;
+
+		if ( name === "find" ) {
+			ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
+		} else if ( name ) {
+			ret.selector = this.selector + "." + name + "(" + selector + ")";
+		}
+
+		// Return the newly-formed element set
+		return ret;
+	},
+
+	// Execute a callback for every element in the matched set.
+	// (You can seed the arguments with an array of args, but this is
+	// only used internally.)
+	each: function( callback, args ) {
+		return jQuery.each( this, callback, args );
+	},
+
+	ready: function( fn ) {
+		// Add the callback
+		jQuery.ready.promise().done( fn );
+
+		return this;
+	},
+
+	eq: function( i ) {
+		i = +i;
+		return i === -1 ?
+			this.slice( i ) :
+			this.slice( i, i + 1 );
+	},
+
+	first: function() {
+		return this.eq( 0 );
+	},
+
+	last: function() {
+		return this.eq( -1 );
+	},
+
+	slice: function() {
+		return this.pushStack( core_slice.apply( this, arguments ),
+			"slice", core_slice.call(arguments).join(",") );
+	},
+
+	map: function( callback ) {
+		return this.pushStack( jQuery.map(this, function( elem, i ) {
+			return callback.call( elem, i, elem );
+		}));
+	},
+
+	end: function() {
+		return this.prevObject || this.constructor(null);
+	},
+
+	// For internal use only.
+	// Behaves like an Array's method, not like a jQuery method.
+	push: core_push,
+	sort: [].sort,
+	splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+	var options, name, src, copy, copyIsArray, clone,
+		target = arguments[0] || {},
+		i = 1,
+		length = arguments.length,
+		deep = false;
+
+	// Handle a deep copy situation
+	if ( typeof target === "boolean" ) {
+		deep = target;
+		target = arguments[1] || {};
+		// skip the boolean and the target
+		i = 2;
+	}
+
+	// Handle case when target is a string or something (possible in deep copy)
+	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+		target = {};
+	}
+
+	// extend jQuery itself if only one argument is passed
+	if ( length === i ) {
+		target = this;
+		--i;
+	}
+
+	for ( ; i < length; i++ ) {
+		// Only deal with non-null/undefined values
+		if ( (options = arguments[ i ]) != null ) {
+			// Extend the base object
+			for ( name in options ) {
+				src = target[ name ];
+				copy = options[ name ];
+
+				// Prevent never-ending loop
+				if ( target === copy ) {
+					continue;
+				}
+
+				// Recurse if we're merging plain objects or arrays
+				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+					if ( copyIsArray ) {
+						copyIsArray = false;
+						clone = src && jQuery.isArray(src) ? src : [];
+
+					} else {
+						clone = src && jQuery.isPlainObject(src) ? src : {};
+					}
+
+					// Never move original objects, clone them
+					target[ name ] = jQuery.extend( deep, clone, copy );
+
+				// Don't bring in undefined values
+				} else if ( copy !== undefined ) {
+					target[ name ] = copy;
+				}
+			}
+		}
+	}
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	noConflict: function( deep ) {
+		if ( window.$ === jQuery ) {
+			window.$ = _$;
+		}
+
+		if ( deep && window.jQuery === jQuery ) {
+			window.jQuery = _jQuery;
+		}
+
+		return jQuery;
+	},
+
+	// Is the DOM ready to be used? Set to true once it occurs.
+	isReady: false,
+
+	// A counter to track how many items to wait for before
+	// the ready event fires. See #6781
+	readyWait: 1,
+
+	// Hold (or release) the ready event
+	holdReady: function( hold ) {
+		if ( hold ) {
+			jQuery.readyWait++;
+		} else {
+			jQuery.ready( true );
+		}
+	},
+
+	// Handle when the DOM is ready
+	ready: function( wait ) {
+
+		// Abort if there are pending holds or we're already ready
+		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
+			return;
+		}
+
+		// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+		if ( !document.body ) {
+			return setTimeout( jQuery.ready, 1 );
+		}
+
+		// Remember that the DOM is ready
+		jQuery.isReady = true;
+
+		// If a normal DOM Ready event fired, decrement, and wait if need be
+		if ( wait !== true && --jQuery.readyWait > 0 ) {
+			return;
+		}
+
+		// If there are functions bound, to execute
+		readyList.resolveWith( document, [ jQuery ] );
+
+		// Trigger any bound ready events
+		if ( jQuery.fn.trigger ) {
+			jQuery( document ).trigger("ready").off("ready");
+		}
+	},
+
+	// See test/unit/core.js for details concerning isFunction.
+	// Since version 1.3, DOM methods and functions like alert
+	// aren't supported. They return false on IE (#2968).
+	isFunction: function( obj ) {
+		return jQuery.type(obj) === "function";
+	},
+
+	isArray: Array.isArray || function( obj ) {
+		return jQuery.type(obj) === "array";
+	},
+
+	isWindow: function( obj ) {
+		return obj != null && obj == obj.window;
+	},
+
+	isNumeric: function( obj ) {
+		return !isNaN( parseFloat(obj) ) && isFinite( obj );
+	},
+
+	type: function( obj ) {
+		return obj == null ?
+			String( obj ) :
+			class2type[ core_toString.call(obj) ] || "object";
+	},
+
+	isPlainObject: function( obj ) {
+		// Must be an Object.
+		// Because of IE, we also have to check the presence of the constructor property.
+		// Make sure that DOM nodes and window objects don't pass through, as well
+		if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+			return false;
+		}
+
+		try {
+			// Not own constructor property must be Object
+			if ( obj.constructor &&
+				!core_hasOwn.call(obj, "constructor") &&
+				!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+				return false;
+			}
+		} catch ( e ) {
+			// IE8,9 Will throw exceptions on certain host objects #9897
+			return false;
+		}
+
+		// Own properties are enumerated firstly, so to speed up,
+		// if last one is own, then all properties are own.
+
+		var key;
+		for ( key in obj ) {}
+
+		return key === undefined || core_hasOwn.call( obj, key );
+	},
+
+	isEmptyObject: function( obj ) {
+		var name;
+		for ( name in obj ) {
+			return false;
+		}
+		return true;
+	},
+
+	error: function( msg ) {
+		throw new Error( msg );
+	},
+
+	// data: string of html
+	// context (optional): If specified, the fragment will be created in this context, defaults to document
+	// scripts (optional): If true, will include scripts passed in the html string
+	parseHTML: function( data, context, scripts ) {
+		var parsed;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		if ( typeof context === "boolean" ) {
+			scripts = context;
+			context = 0;
+		}
+		context = context || document;
+
+		// Single tag
+		if ( (parsed = rsingleTag.exec( data )) ) {
+			return [ context.createElement( parsed[1] ) ];
+		}
+
+		parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] );
+		return jQuery.merge( [],
+			(parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes );
+	},
+
+	parseJSON: function( data ) {
+		if ( !data || typeof data !== "string") {
+			return null;
+		}
+
+		// Make sure leading/trailing whitespace is removed (IE can't handle it)
+		data = jQuery.trim( data );
+
+		// Attempt to parse using the native JSON parser first
+		if ( window.JSON && window.JSON.parse ) {
+			return window.JSON.parse( data );
+		}
+
+		// Make sure the incoming data is actual JSON
+		// Logic borrowed from http://json.org/json2.js
+		if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+			.replace( rvalidtokens, "]" )
+			.replace( rvalidbraces, "")) ) {
+
+			return ( new Function( "return " + data ) )();
+
+		}
+		jQuery.error( "Invalid JSON: " + data );
+	},
+
+	// Cross-browser xml parsing
+	parseXML: function( data ) {
+		var xml, tmp;
+		if ( !data || typeof data !== "string" ) {
+			return null;
+		}
+		try {
+			if ( window.DOMParser ) { // Standard
+				tmp = new DOMParser();
+				xml = tmp.parseFromString( data , "text/xml" );
+			} else { // IE
+				xml = new ActiveXObject( "Microsoft.XMLDOM" );
+				xml.async = "false";
+				xml.loadXML( data );
+			}
+		} catch( e ) {
+			xml = undefined;
+		}
+		if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
+			jQuery.error( "Invalid XML: " + data );
+		}
+		return xml;
+	},
+
+	noop: function() {},
+
+	// Evaluates a script in a global context
+	// Workarounds based on findings by Jim Driscoll
+	// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+	globalEval: function( data ) {
+		if ( data && core_rnotwhite.test( data ) ) {
+			// We use execScript on Internet Explorer
+			// We use an anonymous function so that context is window
+			// rather than jQuery in Firefox
+			( window.execScript || function( data ) {
+				window[ "eval" ].call( window, data );
+			} )( data );
+		}
+	},
+
+	// Convert dashed to camelCase; used by the css and data modules
+	// Microsoft forgot to hump their vendor prefix (#9572)
+	camelCase: function( string ) {
+		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+	},
+
+	// args is for internal usage only
+	each: function( obj, callback, args ) {
+		var name,
+			i = 0,
+			length = obj.length,
+			isObj = length === undefined || jQuery.isFunction( obj );
+
+		if ( args ) {
+			if ( isObj ) {
+				for ( name in obj ) {
+					if ( callback.apply( obj[ name ], args ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.apply( obj[ i++ ], args ) === false ) {
+						break;
+					}
+				}
+			}
+
+		// A special, fast, case for the most common use of each
+		} else {
+			if ( isObj ) {
+				for ( name in obj ) {
+					if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) {
+						break;
+					}
+				}
+			} else {
+				for ( ; i < length; ) {
+					if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) {
+						break;
+					}
+				}
+			}
+		}
+
+		return obj;
+	},
+
+	// Use native String.trim function wherever possible
+	trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
+		function( text ) {
+			return text == null ?
+				"" :
+				core_trim.call( text );
+		} :
+
+		// Otherwise use our own trimming functionality
+		function( text ) {
+			return text == null ?
+				"" :
+				( text + "" ).replace( rtrim, "" );
+		},
+
+	// results is for internal usage only
+	makeArray: function( arr, results ) {
+		var type,
+			ret = results || [];
+
+		if ( arr != null ) {
+			// The window, strings (and functions) also have 'length'
+			// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+			type = jQuery.type( arr );
+
+			if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) {
+				core_push.call( ret, arr );
+			} else {
+				jQuery.merge( ret, arr );
+			}
+		}
+
+		return ret;
+	},
+
+	inArray: function( elem, arr, i ) {
+		var len;
+
+		if ( arr ) {
+			if ( core_indexOf ) {
+				return core_indexOf.call( arr, elem, i );
+			}
+
+			len = arr.length;
+			i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
+
+			for ( ; i < len; i++ ) {
+				// Skip accessing in sparse arrays
+				if ( i in arr && arr[ i ] === elem ) {
+					return i;
+				}
+			}
+		}
+
+		return -1;
+	},
+
+	merge: function( first, second ) {
+		var l = second.length,
+			i = first.length,
+			j = 0;
+
+		if ( typeof l === "number" ) {
+			for ( ; j < l; j++ ) {
+				first[ i++ ] = second[ j ];
+			}
+
+		} else {
+			while ( second[j] !== undefined ) {
+				first[ i++ ] = second[ j++ ];
+			}
+		}
+
+		first.length = i;
+
+		return first;
+	},
+
+	grep: function( elems, callback, inv ) {
+		var retVal,
+			ret = [],
+			i = 0,
+			length = elems.length;
+		inv = !!inv;
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( ; i < length; i++ ) {
+			retVal = !!callback( elems[ i ], i );
+			if ( inv !== retVal ) {
+				ret.push( elems[ i ] );
+			}
+		}
+
+		return ret;
+	},
+
+	// arg is for internal usage only
+	map: function( elems, callback, arg ) {
+		var value, key,
+			ret = [],
+			i = 0,
+			length = elems.length,
+			// jquery objects are treated as arrays
+			isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
+
+		// Go through the array, translating each of the items to their
+		if ( isArray ) {
+			for ( ; i < length; i++ ) {
+				value = callback( elems[ i ], i, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+
+		// Go through every key on the object,
+		} else {
+			for ( key in elems ) {
+				value = callback( elems[ key ], key, arg );
+
+				if ( value != null ) {
+					ret[ ret.length ] = value;
+				}
+			}
+		}
+
+		// Flatten any nested arrays
+		return ret.concat.apply( [], ret );
+	},
+
+	// A global GUID counter for objects
+	guid: 1,
+
+	// Bind a function to a context, optionally partially applying any
+	// arguments.
+	proxy: function( fn, context ) {
+		var tmp, args, proxy;
+
+		if ( typeof context === "string" ) {
+			tmp = fn[ context ];
+			context = fn;
+			fn = tmp;
+		}
+
+		// Quick check to determine if target is callable, in the spec
+		// this throws a TypeError, but we will just return undefined.
+		if ( !jQuery.isFunction( fn ) ) {
+			return undefined;
+		}
+
+		// Simulated bind
+		args = core_slice.call( arguments, 2 );
+		proxy = function() {
+			return fn.apply( context, args.concat( core_slice.call( arguments ) ) );
+		};
+
+		// Set the guid of unique handler to the same of original handler, so it can be removed
+		proxy.guid = fn.guid = fn.guid || jQuery.guid++;
+
+		return proxy;
+	},
+
+	// Multifunctional method to get and set values of a collection
+	// The value/s can optionally be executed if it's a function
+	access: function( elems, fn, key, value, chainable, emptyGet, pass ) {
+		var exec,
+			bulk = key == null,
+			i = 0,
+			length = elems.length;
+
+		// Sets many values
+		if ( key && typeof key === "object" ) {
+			for ( i in key ) {
+				jQuery.access( elems, fn, i, key[i], 1, emptyGet, value );
+			}
+			chainable = 1;
+
+		// Sets one value
+		} else if ( value !== undefined ) {
+			// Optionally, function values get executed if exec is true
+			exec = pass === undefined && jQuery.isFunction( value );
+
+			if ( bulk ) {
+				// Bulk operations only iterate when executing function values
+				if ( exec ) {
+					exec = fn;
+					fn = function( elem, key, value ) {
+						return exec.call( jQuery( elem ), value );
+					};
+
+				// Otherwise they run against the entire set
+				} else {
+					fn.call( elems, value );
+					fn = null;
+				}
+			}
+
+			if ( fn ) {
+				for (; i < length; i++ ) {
+					fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+				}
+			}
+
+			chainable = 1;
+		}
+
+		return chainable ?
+			elems :
+
+			// Gets
+			bulk ?
+				fn.call( elems ) :
+				length ? fn( elems[0], key ) : emptyGet;
+	},
+
+	now: function() {
+		return ( new Date() ).getTime();
+	}
+});
+
+jQuery.ready.promise = function( obj ) {
+	if ( !readyList ) {
+
+		readyList = jQuery.Deferred();
+
+		// Catch cases where $(document).ready() is called after the browser event has already occurred.
+		// we once tried to use readyState "interactive" here, but it caused issues like the one
+		// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+		if ( document.readyState === "complete" ) {
+			// Handle it asynchronously to allow scripts the opportunity to delay ready
+			setTimeout( jQuery.ready, 1 );
+
+		// Standards-based browsers support DOMContentLoaded
+		} else if ( document.addEventListener ) {
+			// Use the handy event callback
+			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+			// A fallback to window.onload, that will always work
+			window.addEventListener( "load", jQuery.ready, false );
+
+		// If IE event model is used
+		} else {
+			// Ensure firing before onload, maybe late but safe also for iframes
+			document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+			// A fallback to window.onload, that will always work
+			window.attachEvent( "onload", jQuery.ready );
+
+			// If IE and not a frame
+			// continually check to see if the document is ready
+			var top = false;
+
+			try {
+				top = window.frameElement == null && document.documentElement;
+			} catch(e) {}
+
+			if ( top && top.doScroll ) {
+				(function doScrollCheck() {
+					if ( !jQuery.isReady ) {
+
+						try {
+							// Use the trick by Diego Perini
+							// http://javascript.nwbox.com/IEContentLoaded/
+							top.doScroll("left");
+						} catch(e) {
+							return setTimeout( doScrollCheck, 50 );
+						}
+
+						// and execute any waiting functions
+						jQuery.ready();
+					}
+				})();
+			}
+		}
+	}
+	return readyList.promise( obj );
+};
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+	class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+// String to Object options format cache
+var optionsCache = {};
+
+// Convert String-formatted options into Object-formatted ones and store in cache
+function createOptions( options ) {
+	var object = optionsCache[ options ] = {};
+	jQuery.each( options.split( core_rspace ), function( _, flag ) {
+		object[ flag ] = true;
+	});
+	return object;
+}
+
+/*
+ * Create a callback list using the following parameters:
+ *
+ *	options: an optional list of space-separated options that will change how
+ *			the callback list behaves or a more traditional option object
+ *
+ * By default a callback list will act like an event callback list and can be
+ * "fired" multiple times.
+ *
+ * Possible options:
+ *
+ *	once:			will ensure the callback list can only be fired once (like a Deferred)
+ *
+ *	memory:			will keep track of previous values and will call any callback added
+ *					after the list has been fired right away with the latest "memorized"
+ *					values (like a Deferred)
+ *
+ *	unique:			will ensure a callback can only be added once (no duplicate in the list)
+ *
+ *	stopOnFalse:	interrupt callings when a callback returns false
+ *
+ */
+jQuery.Callbacks = function( options ) {
+
+	// Convert options from String-formatted to Object-formatted if needed
+	// (we check in cache first)
+	options = typeof options === "string" ?
+		( optionsCache[ options ] || createOptions( options ) ) :
+		jQuery.extend( {}, options );
+
+	var // Last fire value (for non-forgettable lists)
+		memory,
+		// Flag to know if list was already fired
+		fired,
+		// Flag to know if list is currently firing
+		firing,
+		// First callback to fire (used internally by add and fireWith)
+		firingStart,
+		// End of the loop when firing
+		firingLength,
+		// Index of currently firing callback (modified by remove if needed)
+		firingIndex,
+		// Actual callback list
+		list = [],
+		// Stack of fire calls for repeatable lists
+		stack = !options.once && [],
+		// Fire callbacks
+		fire = function( data ) {
+			memory = options.memory && data;
+			fired = true;
+			firingIndex = firingStart || 0;
+			firingStart = 0;
+			firingLength = list.length;
+			firing = true;
+			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
+				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
+					memory = false; // To prevent further calls using add
+					break;
+				}
+			}
+			firing = false;
+			if ( list ) {
+				if ( stack ) {
+					if ( stack.length ) {
+						fire( stack.shift() );
+					}
+				} else if ( memory ) {
+					list = [];
+				} else {
+					self.disable();
+				}
+			}
+		},
+		// Actual Callbacks object
+		self = {
+			// Add a callback or a collection of callbacks to the list
+			add: function() {
+				if ( list ) {
+					// First, we save the current length
+					var start = list.length;
+					(function add( args ) {
+						jQuery.each( args, function( _, arg ) {
+							var type = jQuery.type( arg );
+							if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) {
+								list.push( arg );
+							} else if ( arg && arg.length && type !== "string" ) {
+								// Inspect recursively
+								add( arg );
+							}
+						});
+					})( arguments );
+					// Do we need to add the callbacks to the
+					// current firing batch?
+					if ( firing ) {
+						firingLength = list.length;
+					// With memory, if we're not firing then
+					// we should call right away
+					} else if ( memory ) {
+						firingStart = start;
+						fire( memory );
+					}
+				}
+				return this;
+			},
+			// Remove a callback from the list
+			remove: function() {
+				if ( list ) {
+					jQuery.each( arguments, function( _, arg ) {
+						var index;
+						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
+							list.splice( index, 1 );
+							// Handle firing indexes
+							if ( firing ) {
+								if ( index <= firingLength ) {
+									firingLength--;
+								}
+								if ( index <= firingIndex ) {
+									firingIndex--;
+								}
+							}
+						}
+					});
+				}
+				return this;
+			},
+			// Control if a given callback is in the list
+			has: function( fn ) {
+				return jQuery.inArray( fn, list ) > -1;
+			},
+			// Remove all callbacks from the list
+			empty: function() {
+				list = [];
+				return this;
+			},
+			// Have the list do nothing anymore
+			disable: function() {
+				list = stack = memory = undefined;
+				return this;
+			},
+			// Is it disabled?
+			disabled: function() {
+				return !list;
+			},
+			// Lock the list in its current state
+			lock: function() {
+				stack = undefined;
+				if ( !memory ) {
+					self.disable();
+				}
+				return this;
+			},
+			// Is it locked?
+			locked: function() {
+				return !stack;
+			},
+			// Call all callbacks with the given context and arguments
+			fireWith: function( context, args ) {
+				args = args || [];
+				args = [ context, args.slice ? args.slice() : args ];
+				if ( list && ( !fired || stack ) ) {
+					if ( firing ) {
+						stack.push( args );
+					} else {
+						fire( args );
+					}
+				}
+				return this;
+			},
+			// Call all the callbacks with the given arguments
+			fire: function() {
+				self.fireWith( this, arguments );
+				return this;
+			},
+			// To know if the callbacks have already been called at least once
+			fired: function() {
+				return !!fired;
+			}
+		};
+
+	return self;
+};
+jQuery.extend({
+
+	Deferred: function( func ) {
+		var tuples = [
+				// action, add listener, listener list, final state
+				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
+				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
+				[ "notify", "progress", jQuery.Callbacks("memory") ]
+			],
+			state = "pending",
+			promise = {
+				state: function() {
+					return state;
+				},
+				always: function() {
+					deferred.done( arguments ).fail( arguments );
+					return this;
+				},
+				then: function( /* fnDone, fnFail, fnProgress */ ) {
+					var fns = arguments;
+					return jQuery.Deferred(function( newDefer ) {
+						jQuery.each( tuples, function( i, tuple ) {
+							var action = tuple[ 0 ],
+								fn = fns[ i ];
+							// deferred[ done | fail | progress ] for forwarding actions to newDefer
+							deferred[ tuple[1] ]( jQuery.isFunction( fn ) ?
+								function() {
+									var returned = fn.apply( this, arguments );
+									if ( returned && jQuery.isFunction( returned.promise ) ) {
+										returned.promise()
+											.done( newDefer.resolve )
+											.fail( newDefer.reject )
+											.progress( newDefer.notify );
+									} else {
+										newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
+									}
+								} :
+								newDefer[ action ]
+							);
+						});
+						fns = null;
+					}).promise();
+				},
+				// Get a promise for this deferred
+				// If obj is provided, the promise aspect is added to the object
+				promise: function( obj ) {
+					return obj != null ? jQuery.extend( obj, promise ) : promise;
+				}
+			},
+			deferred = {};
+
+		// Keep pipe for back-compat
+		promise.pipe = promise.then;
+
+		// Add list-specific methods
+		jQuery.each( tuples, function( i, tuple ) {
+			var list = tuple[ 2 ],
+				stateString = tuple[ 3 ];
+
+			// promise[ done | fail | progress ] = list.add
+			promise[ tuple[1] ] = list.add;
+
+			// Handle state
+			if ( stateString ) {
+				list.add(function() {
+					// state = [ resolved | rejected ]
+					state = stateString;
+
+				// [ reject_list | resolve_list ].disable; progress_list.lock
+				}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
+			}
+
+			// deferred[ resolve | reject | notify ] = list.fire
+			deferred[ tuple[0] ] = list.fire;
+			deferred[ tuple[0] + "With" ] = list.fireWith;
+		});
+
+		// Make the deferred a promise
+		promise.promise( deferred );
+
+		// Call given func if any
+		if ( func ) {
+			func.call( deferred, deferred );
+		}
+
+		// All done!
+		return deferred;
+	},
+
+	// Deferred helper
+	when: function( subordinate /* , ..., subordinateN */ ) {
+		var i = 0,
+			resolveValues = core_slice.call( arguments ),
+			length = resolveValues.length,
+
+			// the count of uncompleted subordinates
+			remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
+
+			// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
+			deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
+
+			// Update function for both resolve and progress values
+			updateFunc = function( i, contexts, values ) {
+				return function( value ) {
+					contexts[ i ] = this;
+					values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;
+					if( values === progressValues ) {
+						deferred.notifyWith( contexts, values );
+					} else if ( !( --remaining ) ) {
+						deferred.resolveWith( contexts, values );
+					}
+				};
+			},
+
+			progressValues, progressContexts, resolveContexts;
+
+		// add listeners to Deferred subordinates; treat others as resolved
+		if ( length > 1 ) {
+			progressValues = new Array( length );
+			progressContexts = new Array( length );
+			resolveContexts = new Array( length );
+			for ( ; i < length; i++ ) {
+				if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
+					resolveValues[ i ].promise()
+						.done( updateFunc( i, resolveContexts, resolveValues ) )
+						.fail( deferred.reject )
+						.progress( updateFunc( i, progressContexts, progressValues ) );
+				} else {
+					--remaining;
+				}
+			}
+		}
+
+		// if we're not waiting on anything, resolve the master
+		if ( !remaining ) {
+			deferred.resolveWith( resolveContexts, resolveValues );
+		}
+
+		return deferred.promise();
+	}
+});
+jQuery.support = (function() {
+
+	var support,
+		all,
+		a,
+		select,
+		opt,
+		input,
+		fragment,
+		eventName,
+		i,
+		isSupported,
+		clickFn,
+		div = document.createElement("div");
+
+	// Preliminary tests
+	div.setAttribute( "className", "t" );
+	div.innerHTML = "  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>";
+
+	all = div.getElementsByTagName("*");
+	a = div.getElementsByTagName("a")[ 0 ];
+	a.style.cssText = "top:1px;float:left;opacity:.5";
+
+	// Can't get basic test support
+	if ( !all || !all.length ) {
+		return {};
+	}
+
+	// First batch of supports tests
+	select = document.createElement("select");
+	opt = select.appendChild( document.createElement("option") );
+	input = div.getElementsByTagName("input")[ 0 ];
+
+	support = {
+		// IE strips leading whitespace when .innerHTML is used
+		leadingWhitespace: ( div.firstChild.nodeType === 3 ),
+
+		// Make sure that tbody elements aren't automatically inserted
+		// IE will insert them into empty tables
+		tbody: !div.getElementsByTagName("tbody").length,
+
+		// Make sure that link elements get serialized correctly by innerHTML
+		// This requires a wrapper element in IE
+		htmlSerialize: !!div.getElementsByTagName("link").length,
+
+		// Get the style information from getAttribute
+		// (IE uses .cssText instead)
+		style: /top/.test( a.getAttribute("style") ),
+
+		// Make sure that URLs aren't manipulated
+		// (IE normalizes it by default)
+		hrefNormalized: ( a.getAttribute("href") === "/a" ),
+
+		// Make sure that element opacity exists
+		// (IE uses filter instead)
+		// Use a regex to work around a WebKit issue. See #5145
+		opacity: /^0.5/.test( a.style.opacity ),
+
+		// Verify style float existence
+		// (IE uses styleFloat instead of cssFloat)
+		cssFloat: !!a.style.cssFloat,
+
+		// Make sure that if no value is specified for a checkbox
+		// that it defaults to "on".
+		// (WebKit defaults to "" instead)
+		checkOn: ( input.value === "on" ),
+
+		// Make sure that a selected-by-default option has a working selected property.
+		// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+		optSelected: opt.selected,
+
+		// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+		getSetAttribute: div.className !== "t",
+
+		// Tests for enctype support on a form(#6743)
+		enctype: !!document.createElement("form").enctype,
+
+		// Makes sure cloning an html5 element does not cause problems
+		// Where outerHTML is undefined, this still works
+		html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
+
+		// jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode
+		boxModel: ( document.compatMode === "CSS1Compat" ),
+
+		// Will be defined later
+		submitBubbles: true,
+		changeBubbles: true,
+		focusinBubbles: false,
+		deleteExpando: true,
+		noCloneEvent: true,
+		inlineBlockNeedsLayout: false,
+		shrinkWrapBlocks: false,
+		reliableMarginRight: true,
+		boxSizingReliable: true,
+		pixelPosition: false
+	};
+
+	// Make sure checked status is properly cloned
+	input.checked = true;
+	support.noCloneChecked = input.cloneNode( true ).checked;
+
+	// Make sure that the options inside disabled selects aren't marked as disabled
+	// (WebKit marks them as disabled)
+	select.disabled = true;
+	support.optDisabled = !opt.disabled;
+
+	// Test to see if it's possible to delete an expando from an element
+	// Fails in Internet Explorer
+	try {
+		delete div.test;
+	} catch( e ) {
+		support.deleteExpando = false;
+	}
+
+	if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
+		div.attachEvent( "onclick", clickFn = function() {
+			// Cloning a node shouldn't copy over any
+			// bound event handlers (IE does this)
+			support.noCloneEvent = false;
+		});
+		div.cloneNode( true ).fireEvent("onclick");
+		div.detachEvent( "onclick", clickFn );
+	}
+
+	// Check if a radio maintains its value
+	// after being appended to the DOM
+	input = document.createElement("input");
+	input.value = "t";
+	input.setAttribute( "type", "radio" );
+	support.radioValue = input.value === "t";
+
+	input.setAttribute( "checked", "checked" );
+
+	// #11217 - WebKit loses check when the name is after the checked attribute
+	input.setAttribute( "name", "t" );
+
+	div.appendChild( input );
+	fragment = document.createDocumentFragment();
+	fragment.appendChild( div.lastChild );
+
+	// WebKit doesn't clone checked state correctly in fragments
+	support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+	// Check if a disconnected checkbox will retain its checked
+	// value of true after appended to the DOM (IE6/7)
+	support.appendChecked = input.checked;
+
+	fragment.removeChild( input );
+	fragment.appendChild( div );
+
+	// Technique from Juriy Zaytsev
+	// http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
+	// We only care about the case where non-standard event systems
+	// are used, namely in IE. Short-circuiting here helps us to
+	// avoid an eval call (in setAttribute) which can cause CSP
+	// to go haywire. See: https://developer.mozilla.org/en/Security/CSP
+	if ( div.attachEvent ) {
+		for ( i in {
+			submit: true,
+			change: true,
+			focusin: true
+		}) {
+			eventName = "on" + i;
+			isSupported = ( eventName in div );
+			if ( !isSupported ) {
+				div.setAttribute( eventName, "return;" );
+				isSupported = ( typeof div[ eventName ] === "function" );
+			}
+			support[ i + "Bubbles" ] = isSupported;
+		}
+	}
+
+	// Run tests that need a body at doc ready
+	jQuery(function() {
+		var container, div, tds, marginDiv,
+			divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;",
+			body = document.getElementsByTagName("body")[0];
+
+		if ( !body ) {
+			// Return for frameset docs that don't have a body
+			return;
+		}
+
+		container = document.createElement("div");
+		container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px";
+		body.insertBefore( container, body.firstChild );
+
+		// Construct the test element
+		div = document.createElement("div");
+		container.appendChild( div );
+
+		// Check if table cells still have offsetWidth/Height when they are set
+		// to display:none and there are still other visible table cells in a
+		// table row; if so, offsetWidth/Height are not reliable for use when
+		// determining if an element has been hidden directly using
+		// display:none (it is still safe to use offsets if a parent element is
+		// hidden; don safety goggles and see bug #4512 for more information).
+		// (only IE 8 fails this test)
+		div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>";
+		tds = div.getElementsByTagName("td");
+		tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none";
+		isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+		tds[ 0 ].style.display = "";
+		tds[ 1 ].style.display = "none";
+
+		// Check if empty table cells still have offsetWidth/Height
+		// (IE <= 8 fail this test)
+		support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+
+		// Check box-sizing and margin behavior
+		div.innerHTML = "";
+		div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;";
+		support.boxSizing = ( div.offsetWidth === 4 );
+		support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 );
+
+		// NOTE: To any future maintainer, we've window.getComputedStyle
+		// because jsdom on node.js will break without it.
+		if ( window.getComputedStyle ) {
+			support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%";
+			support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px";
+
+			// Check if div with explicit width and no margin-right incorrectly
+			// gets computed margin-right based on width of container. For more
+			// info see bug #3333
+			// Fails in WebKit before Feb 2011 nightlies
+			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+			marginDiv = document.createElement("div");
+			marginDiv.style.cssText = div.style.cssText = divReset;
+			marginDiv.style.marginRight = marginDiv.style.width = "0";
+			div.style.width = "1px";
+			div.appendChild( marginDiv );
+			support.reliableMarginRight =
+				!parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );
+		}
+
+		if ( typeof div.style.zoom !== "undefined" ) {
+			// Check if natively block-level elements act like inline-block
+			// elements when setting their display to 'inline' and giving
+			// them layout
+			// (IE < 8 does this)
+			div.innerHTML = "";
+			div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1";
+			support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 );
+
+			// Check if elements with layout shrink-wrap their children
+			// (IE 6 does this)
+			div.style.display = "block";
+			div.style.overflow = "visible";
+			div.innerHTML = "<div></div>";
+			div.firstChild.style.width = "5px";
+			support.shrinkWrapBlocks = ( div.offsetWidth !== 3 );
+
+			container.style.zoom = 1;
+		}
+
+		// Null elements to avoid leaks in IE
+		body.removeChild( container );
+		container = div = tds = marginDiv = null;
+	});
+
+	// Null elements to avoid leaks in IE
+	fragment.removeChild( div );
+	all = a = select = opt = input = fragment = div = null;
+
+	return support;
+})();
+var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+	rmultiDash = /([A-Z])/g;
+
+jQuery.extend({
+	cache: {},
+
+	deletedIds: [],
+
+	// Remove at next major release (1.9/2.0)
+	uuid: 0,
+
+	// Unique for each copy of jQuery on the page
+	// Non-digits removed to match rinlinejQuery
+	expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
+
+	// The following elements throw uncatchable exceptions if you
+	// attempt to add expando properties to them.
+	noData: {
+		"embed": true,
+		// Ban all objects except for Flash (which handle expandos)
+		"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+		"applet": true
+	},
+
+	hasData: function( elem ) {
+		elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+		return !!elem && !isEmptyDataObject( elem );
+	},
+
+	data: function( elem, name, data, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var thisCache, ret,
+			internalKey = jQuery.expando,
+			getByName = typeof name === "string",
+
+			// We have to handle DOM nodes and JS objects differently because IE6-7
+			// can't GC object references properly across the DOM-JS boundary
+			isNode = elem.nodeType,
+
+			// Only DOM nodes need the global jQuery cache; JS object data is
+			// attached directly to the object so GC can occur automatically
+			cache = isNode ? jQuery.cache : elem,
+
+			// Only defining an ID for JS objects if its cache already exists allows
+			// the code to shortcut on the same path as a DOM node with no cache
+			id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
+
+		// Avoid doing any more work than we need to when trying to get data on an
+		// object that has no data at all
+		if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
+			return;
+		}
+
+		if ( !id ) {
+			// Only DOM nodes need a new unique ID for each element since their data
+			// ends up in the global cache
+			if ( isNode ) {
+				elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++;
+			} else {
+				id = internalKey;
+			}
+		}
+
+		if ( !cache[ id ] ) {
+			cache[ id ] = {};
+
+			// Avoids exposing jQuery metadata on plain JS objects when the object
+			// is serialized using JSON.stringify
+			if ( !isNode ) {
+				cache[ id ].toJSON = jQuery.noop;
+			}
+		}
+
+		// An object can be passed to jQuery.data instead of a key/value pair; this gets
+		// shallow copied over onto the existing cache
+		if ( typeof name === "object" || typeof name === "function" ) {
+			if ( pvt ) {
+				cache[ id ] = jQuery.extend( cache[ id ], name );
+			} else {
+				cache[ id ].data = jQuery.extend( cache[ id ].data, name );
+			}
+		}
+
+		thisCache = cache[ id ];
+
+		// jQuery data() is stored in a separate object inside the object's internal data
+		// cache in order to avoid key collisions between internal data and user-defined
+		// data.
+		if ( !pvt ) {
+			if ( !thisCache.data ) {
+				thisCache.data = {};
+			}
+
+			thisCache = thisCache.data;
+		}
+
+		if ( data !== undefined ) {
+			thisCache[ jQuery.camelCase( name ) ] = data;
+		}
+
+		// Check for both converted-to-camel and non-converted data property names
+		// If a data property was specified
+		if ( getByName ) {
+
+			// First Try to find as-is property data
+			ret = thisCache[ name ];
+
+			// Test for null|undefined property data
+			if ( ret == null ) {
+
+				// Try to find the camelCased property
+				ret = thisCache[ jQuery.camelCase( name ) ];
+			}
+		} else {
+			ret = thisCache;
+		}
+
+		return ret;
+	},
+
+	removeData: function( elem, name, pvt /* Internal Use Only */ ) {
+		if ( !jQuery.acceptData( elem ) ) {
+			return;
+		}
+
+		var thisCache, i, l,
+
+			isNode = elem.nodeType,
+
+			// See jQuery.data for more information
+			cache = isNode ? jQuery.cache : elem,
+			id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+		// If there is already no cache entry for this object, there is no
+		// purpose in continuing
+		if ( !cache[ id ] ) {
+			return;
+		}
+
+		if ( name ) {
+
+			thisCache = pvt ? cache[ id ] : cache[ id ].data;
+
+			if ( thisCache ) {
+
+				// Support array or space separated string names for data keys
+				if ( !jQuery.isArray( name ) ) {
+
+					// try the string as a key before any manipulation
+					if ( name in thisCache ) {
+						name = [ name ];
+					} else {
+
+						// split the camel cased version by spaces unless a key with the spaces exists
+						name = jQuery.camelCase( name );
+						if ( name in thisCache ) {
+							name = [ name ];
+						} else {
+							name = name.split(" ");
+						}
+					}
+				}
+
+				for ( i = 0, l = name.length; i < l; i++ ) {
+					delete thisCache[ name[i] ];
+				}
+
+				// If there is no data left in the cache, we want to continue
+				// and let the cache object itself get destroyed
+				if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
+					return;
+				}
+			}
+		}
+
+		// See jQuery.data for more information
+		if ( !pvt ) {
+			delete cache[ id ].data;
+
+			// Don't destroy the parent cache unless the internal data object
+			// had been the only thing left in it
+			if ( !isEmptyDataObject( cache[ id ] ) ) {
+				return;
+			}
+		}
+
+		// Destroy the cache
+		if ( isNode ) {
+			jQuery.cleanData( [ elem ], true );
+
+		// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
+		} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
+			delete cache[ id ];
+
+		// When all else fails, null
+		} else {
+			cache[ id ] = null;
+		}
+	},
+
+	// For internal use only.
+	_data: function( elem, name, data ) {
+		return jQuery.data( elem, name, data, true );
+	},
+
+	// A method for determining if a DOM node can handle the data expando
+	acceptData: function( elem ) {
+		var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+		// nodes accept data unless otherwise specified; rejection can be conditional
+		return !noData || noData !== true && elem.getAttribute("classid") === noData;
+	}
+});
+
+jQuery.fn.extend({
+	data: function( key, value ) {
+		var parts, part, attr, name, l,
+			elem = this[0],
+			i = 0,
+			data = null;
+
+		// Gets all values
+		if ( key === undefined ) {
+			if ( this.length ) {
+				data = jQuery.data( elem );
+
+				if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+					attr = elem.attributes;
+					for ( l = attr.length; i < l; i++ ) {
+						name = attr[i].name;
+
+						if ( !name.indexOf( "data-" ) ) {
+							name = jQuery.camelCase( name.substring(5) );
+
+							dataAttr( elem, name, data[ name ] );
+						}
+					}
+					jQuery._data( elem, "parsedAttrs", true );
+				}
+			}
+
+			return data;
+		}
+
+		// Sets multiple values
+		if ( typeof key === "object" ) {
+			return this.each(function() {
+				jQuery.data( this, key );
+			});
+		}
+
+		parts = key.split( ".", 2 );
+		parts[1] = parts[1] ? "." + parts[1] : "";
+		part = parts[1] + "!";
+
+		return jQuery.access( this, function( value ) {
+
+			if ( value === undefined ) {
+				data = this.triggerHandler( "getData" + part, [ parts[0] ] );
+
+				// Try to fetch any internally stored data first
+				if ( data === undefined && elem ) {
+					data = jQuery.data( elem, key );
+					data = dataAttr( elem, key, data );
+				}
+
+				return data === undefined && parts[1] ?
+					this.data( parts[0] ) :
+					data;
+			}
+
+			parts[1] = value;
+			this.each(function() {
+				var self = jQuery( this );
+
+				self.triggerHandler( "setData" + part, parts );
+				jQuery.data( this, key, value );
+				self.triggerHandler( "changeData" + part, parts );
+			});
+		}, null, value, arguments.length > 1, null, false );
+	},
+
+	removeData: function( key ) {
+		return this.each(function() {
+			jQuery.removeData( this, key );
+		});
+	}
+});
+
+function dataAttr( elem, key, data ) {
+	// If nothing was found internally, try to fetch any
+	// data from the HTML5 data-* attribute
+	if ( data === undefined && elem.nodeType === 1 ) {
+
+		var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
+
+		data = elem.getAttribute( name );
+
+		if ( typeof data === "string" ) {
+			try {
+				data = data === "true" ? true :
+				data === "false" ? false :
+				data === "null" ? null :
+				// Only convert to a number if it doesn't change the string
+				+data + "" === data ? +data :
+				rbrace.test( data ) ? jQuery.parseJSON( data ) :
+					data;
+			} catch( e ) {}
+
+			// Make sure we set the data so it isn't changed later
+			jQuery.data( elem, key, data );
+
+		} else {
+			data = undefined;
+		}
+	}
+
+	return data;
+}
+
+// checks a cache object for emptiness
+function isEmptyDataObject( obj ) {
+	var name;
+	for ( name in obj ) {
+
+		// if the public data object is empty, the private is still empty
+		if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
+			continue;
+		}
+		if ( name !== "toJSON" ) {
+			return false;
+		}
+	}
+
+	return true;
+}
+jQuery.extend({
+	queue: function( elem, type, data ) {
+		var queue;
+
+		if ( elem ) {
+			type = ( type || "fx" ) + "queue";
+			queue = jQuery._data( elem, type );
+
+			// Speed up dequeue by getting out quickly if this is just a lookup
+			if ( data ) {
+				if ( !queue || jQuery.isArray(data) ) {
+					queue = jQuery._data( elem, type, jQuery.makeArray(data) );
+				} else {
+					queue.push( data );
+				}
+			}
+			return queue || [];
+		}
+	},
+
+	dequeue: function( elem, type ) {
+		type = type || "fx";
+
+		var queue = jQuery.queue( elem, type ),
+			startLength = queue.length,
+			fn = queue.shift(),
+			hooks = jQuery._queueHooks( elem, type ),
+			next = function() {
+				jQuery.dequeue( elem, type );
+			};
+
+		// If the fx queue is dequeued, always remove the progress sentinel
+		if ( fn === "inprogress" ) {
+			fn = queue.shift();
+			startLength--;
+		}
+
+		if ( fn ) {
+
+			// Add a progress sentinel to prevent the fx queue from being
+			// automatically dequeued
+			if ( type === "fx" ) {
+				queue.unshift( "inprogress" );
+			}
+
+			// clear up the last queue stop function
+			delete hooks.stop;
+			fn.call( elem, next, hooks );
+		}
+
+		if ( !startLength && hooks ) {
+			hooks.empty.fire();
+		}
+	},
+
+	// not intended for public consumption - generates a queueHooks object, or returns the current one
+	_queueHooks: function( elem, type ) {
+		var key = type + "queueHooks";
+		return jQuery._data( elem, key ) || jQuery._data( elem, key, {
+			empty: jQuery.Callbacks("once memory").add(function() {
+				jQuery.removeData( elem, type + "queue", true );
+				jQuery.removeData( elem, key, true );
+			})
+		});
+	}
+});
+
+jQuery.fn.extend({
+	queue: function( type, data ) {
+		var setter = 2;
+
+		if ( typeof type !== "string" ) {
+			data = type;
+			type = "fx";
+			setter--;
+		}
+
+		if ( arguments.length < setter ) {
+			return jQuery.queue( this[0], type );
+		}
+
+		return data === undefined ?
+			this :
+			this.each(function() {
+				var queue = jQuery.queue( this, type, data );
+
+				// ensure a hooks for this queue
+				jQuery._queueHooks( this, type );
+
+				if ( type === "fx" && queue[0] !== "inprogress" ) {
+					jQuery.dequeue( this, type );
+				}
+			});
+	},
+	dequeue: function( type ) {
+		return this.each(function() {
+			jQuery.dequeue( this, type );
+		});
+	},
+	// Based off of the plugin by Clint Helfers, with permission.
+	// http://blindsignals.com/index.php/2009/07/jquery-delay/
+	delay: function( time, type ) {
+		time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
+		type = type || "fx";
+
+		return this.queue( type, function( next, hooks ) {
+			var timeout = setTimeout( next, time );
+			hooks.stop = function() {
+				clearTimeout( timeout );
+			};
+		});
+	},
+	clearQueue: function( type ) {
+		return this.queue( type || "fx", [] );
+	},
+	// Get a promise resolved when queues of a certain type
+	// are emptied (fx is the type by default)
+	promise: function( type, obj ) {
+		var tmp,
+			count = 1,
+			defer = jQuery.Deferred(),
+			elements = this,
+			i = this.length,
+			resolve = function() {
+				if ( !( --count ) ) {
+					defer.resolveWith( elements, [ elements ] );
+				}
+			};
+
+		if ( typeof type !== "string" ) {
+			obj = type;
+			type = undefined;
+		}
+		type = type || "fx";
+
+		while( i-- ) {
+			tmp = jQuery._data( elements[ i ], type + "queueHooks" );
+			if ( tmp && tmp.empty ) {
+				count++;
+				tmp.empty.add( resolve );
+			}
+		}
+		resolve();
+		return defer.promise( obj );
+	}
+});
+var nodeHook, boolHook, fixSpecified,
+	rclass = /[\t\r\n]/g,
+	rreturn = /\r/g,
+	rtype = /^(?:button|input)$/i,
+	rfocusable = /^(?:button|input|object|select|textarea)$/i,
+	rclickable = /^a(?:rea|)$/i,
+	rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
+	getSetAttribute = jQuery.support.getSetAttribute;
+
+jQuery.fn.extend({
+	attr: function( name, value ) {
+		return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
+	},
+
+	removeAttr: function( name ) {
+		return this.each(function() {
+			jQuery.removeAttr( this, name );
+		});
+	},
+
+	prop: function( name, value ) {
+		return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );
+	},
+
+	removeProp: function( name ) {
+		name = jQuery.propFix[ name ] || name;
+		return this.each(function() {
+			// try/catch handles cases where IE balks (such as removing a property on window)
+			try {
+				this[ name ] = undefined;
+				delete this[ name ];
+			} catch( e ) {}
+		});
+	},
+
+	addClass: function( value ) {
+		var classNames, i, l, elem,
+			setClass, c, cl;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).addClass( value.call(this, j, this.className) );
+			});
+		}
+
+		if ( value && typeof value === "string" ) {
+			classNames = value.split( core_rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+
+				if ( elem.nodeType === 1 ) {
+					if ( !elem.className && classNames.length === 1 ) {
+						elem.className = value;
+
+					} else {
+						setClass = " " + elem.className + " ";
+
+						for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+							if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) {
+								setClass += classNames[ c ] + " ";
+							}
+						}
+						elem.className = jQuery.trim( setClass );
+					}
+				}
+			}
+		}
+
+		return this;
+	},
+
+	removeClass: function( value ) {
+		var removes, className, elem, c, cl, i, l;
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( j ) {
+				jQuery( this ).removeClass( value.call(this, j, this.className) );
+			});
+		}
+		if ( (value && typeof value === "string") || value === undefined ) {
+			removes = ( value || "" ).split( core_rspace );
+
+			for ( i = 0, l = this.length; i < l; i++ ) {
+				elem = this[ i ];
+				if ( elem.nodeType === 1 && elem.className ) {
+
+					className = (" " + elem.className + " ").replace( rclass, " " );
+
+					// loop over each item in the removal list
+					for ( c = 0, cl = removes.length; c < cl; c++ ) {
+						// Remove until there is nothing to remove,
+						while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) {
+							className = className.replace( " " + removes[ c ] + " " , " " );
+						}
+					}
+					elem.className = value ? jQuery.trim( className ) : "";
+				}
+			}
+		}
+
+		return this;
+	},
+
+	toggleClass: function( value, stateVal ) {
+		var type = typeof value,
+			isBool = typeof stateVal === "boolean";
+
+		if ( jQuery.isFunction( value ) ) {
+			return this.each(function( i ) {
+				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+			});
+		}
+
+		return this.each(function() {
+			if ( type === "string" ) {
+				// toggle individual class names
+				var className,
+					i = 0,
+					self = jQuery( this ),
+					state = stateVal,
+					classNames = value.split( core_rspace );
+
+				while ( (className = classNames[ i++ ]) ) {
+					// check each className given, space separated list
+					state = isBool ? state : !self.hasClass( className );
+					self[ state ? "addClass" : "removeClass" ]( className );
+				}
+
+			} else if ( type === "undefined" || type === "boolean" ) {
+				if ( this.className ) {
+					// store className if set
+					jQuery._data( this, "__className__", this.className );
+				}
+
+				// toggle whole className
+				this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+			}
+		});
+	},
+
+	hasClass: function( selector ) {
+		var className = " " + selector + " ",
+			i = 0,
+			l = this.length;
+		for ( ; i < l; i++ ) {
+			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
+				return true;
+			}
+		}
+
+		return false;
+	},
+
+	val: function( value ) {
+		var hooks, ret, isFunction,
+			elem = this[0];
+
+		if ( !arguments.length ) {
+			if ( elem ) {
+				hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
+
+				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+					return ret;
+				}
+
+				ret = elem.value;
+
+				return typeof ret === "string" ?
+					// handle most common string cases
+					ret.replace(rreturn, "") :
+					// handle cases where value is null/undef or number
+					ret == null ? "" : ret;
+			}
+
+			return;
+		}
+
+		isFunction = jQuery.isFunction( value );
+
+		return this.each(function( i ) {
+			var val,
+				self = jQuery(this);
+
+			if ( this.nodeType !== 1 ) {
+				return;
+			}
+
+			if ( isFunction ) {
+				val = value.call( this, i, self.val() );
+			} else {
+				val = value;
+			}
+
+			// Treat null/undefined as ""; convert numbers to string
+			if ( val == null ) {
+				val = "";
+			} else if ( typeof val === "number" ) {
+				val += "";
+			} else if ( jQuery.isArray( val ) ) {
+				val = jQuery.map(val, function ( value ) {
+					return value == null ? "" : value + "";
+				});
+			}
+
+			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
+
+			// If set returns undefined, fall back to normal setting
+			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+				this.value = val;
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	valHooks: {
+		option: {
+			get: function( elem ) {
+				// attributes.value is undefined in Blackberry 4.7 but
+				// uses .value. See #6932
+				var val = elem.attributes.value;
+				return !val || val.specified ? elem.value : elem.text;
+			}
+		},
+		select: {
+			get: function( elem ) {
+				var value, i, max, option,
+					index = elem.selectedIndex,
+					values = [],
+					options = elem.options,
+					one = elem.type === "select-one";
+
+				// Nothing was selected
+				if ( index < 0 ) {
+					return null;
+				}
+
+				// Loop through all the selected options
+				i = one ? index : 0;
+				max = one ? index + 1 : options.length;
+				for ( ; i < max; i++ ) {
+					option = options[ i ];
+
+					// Don't return options that are disabled or in a disabled optgroup
+					if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
+							(!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+
+						// Get the specific value for the option
+						value = jQuery( option ).val();
+
+						// We don't need an array for one selects
+						if ( one ) {
+							return value;
+						}
+
+						// Multi-Selects return an array
+						values.push( value );
+					}
+				}
+
+				// Fixes Bug #2551 -- select.val() broken in IE after form.reset()
+				if ( one && !values.length && options.length ) {
+					return jQuery( options[ index ] ).val();
+				}
+
+				return values;
+			},
+
+			set: function( elem, value ) {
+				var values = jQuery.makeArray( value );
+
+				jQuery(elem).find("option").each(function() {
+					this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+				});
+
+				if ( !values.length ) {
+					elem.selectedIndex = -1;
+				}
+				return values;
+			}
+		}
+	},
+
+	// Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9
+	attrFn: {},
+
+	attr: function( elem, name, value, pass ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set attributes on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) {
+			return jQuery( elem )[ name ]( value );
+		}
+
+		// Fallback to prop when attributes are not supported
+		if ( typeof elem.getAttribute === "undefined" ) {
+			return jQuery.prop( elem, name, value );
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		// All attributes are lowercase
+		// Grab necessary hook if one is defined
+		if ( notxml ) {
+			name = name.toLowerCase();
+			hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
+		}
+
+		if ( value !== undefined ) {
+
+			if ( value === null ) {
+				jQuery.removeAttr( elem, name );
+				return;
+
+			} else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				elem.setAttribute( name, value + "" );
+				return value;
+			}
+
+		} else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
+			return ret;
+
+		} else {
+
+			ret = elem.getAttribute( name );
+
+			// Non-existent attributes return null, we normalize to undefined
+			return ret === null ?
+				undefined :
+				ret;
+		}
+	},
+
+	removeAttr: function( elem, value ) {
+		var propName, attrNames, name, isBool,
+			i = 0;
+
+		if ( value && elem.nodeType === 1 ) {
+
+			attrNames = value.split( core_rspace );
+
+			for ( ; i < attrNames.length; i++ ) {
+				name = attrNames[ i ];
+
+				if ( name ) {
+					propName = jQuery.propFix[ name ] || name;
+					isBool = rboolean.test( name );
+
+					// See #9699 for explanation of this approach (setting first, then removal)
+					// Do not do this for boolean attributes (see #10870)
+					if ( !isBool ) {
+						jQuery.attr( elem, name, "" );
+					}
+					elem.removeAttribute( getSetAttribute ? name : propName );
+
+					// Set corresponding property to false for boolean attributes
+					if ( isBool && propName in elem ) {
+						elem[ propName ] = false;
+					}
+				}
+			}
+		}
+	},
+
+	attrHooks: {
+		type: {
+			set: function( elem, value ) {
+				// We can't allow the type property to be changed (since it causes problems in IE)
+				if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
+					jQuery.error( "type property can't be changed" );
+				} else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+					// Setting the type on a radio button after the value resets the value in IE6-9
+					// Reset value to it's default in case type is set after value
+					// This is for element creation
+					var val = elem.value;
+					elem.setAttribute( "type", value );
+					if ( val ) {
+						elem.value = val;
+					}
+					return value;
+				}
+			}
+		},
+		// Use the value property for back compat
+		// Use the nodeHook for button elements in IE6/7 (#1954)
+		value: {
+			get: function( elem, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.get( elem, name );
+				}
+				return name in elem ?
+					elem.value :
+					null;
+			},
+			set: function( elem, value, name ) {
+				if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
+					return nodeHook.set( elem, value, name );
+				}
+				// Does not return so that setAttribute is also used
+				elem.value = value;
+			}
+		}
+	},
+
+	propFix: {
+		tabindex: "tabIndex",
+		readonly: "readOnly",
+		"for": "htmlFor",
+		"class": "className",
+		maxlength: "maxLength",
+		cellspacing: "cellSpacing",
+		cellpadding: "cellPadding",
+		rowspan: "rowSpan",
+		colspan: "colSpan",
+		usemap: "useMap",
+		frameborder: "frameBorder",
+		contenteditable: "contentEditable"
+	},
+
+	prop: function( elem, name, value ) {
+		var ret, hooks, notxml,
+			nType = elem.nodeType;
+
+		// don't get/set properties on text, comment and attribute nodes
+		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+			return;
+		}
+
+		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+		if ( notxml ) {
+			// Fix name and attach hooks
+			name = jQuery.propFix[ name ] || name;
+			hooks = jQuery.propHooks[ name ];
+		}
+
+		if ( value !== undefined ) {
+			if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+				return ret;
+
+			} else {
+				return ( elem[ name ] = value );
+			}
+
+		} else {
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
+				return ret;
+
+			} else {
+				return elem[ name ];
+			}
+		}
+	},
+
+	propHooks: {
+		tabIndex: {
+			get: function( elem ) {
+				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+				var attributeNode = elem.getAttributeNode("tabindex");
+
+				return attributeNode && attributeNode.specified ?
+					parseInt( attributeNode.value, 10 ) :
+					rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+						0 :
+						undefined;
+			}
+		}
+	}
+});
+
+// Hook for boolean attributes
+boolHook = {
+	get: function( elem, name ) {
+		// Align boolean attributes with corresponding properties
+		// Fall back to attribute presence where some booleans are not supported
+		var attrNode,
+			property = jQuery.prop( elem, name );
+		return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
+			name.toLowerCase() :
+			undefined;
+	},
+	set: function( elem, value, name ) {
+		var propName;
+		if ( value === false ) {
+			// Remove boolean attributes when set to false
+			jQuery.removeAttr( elem, name );
+		} else {
+			// value is true since we know at this point it's type boolean and not false
+			// Set boolean attributes to the same name and set the DOM property
+			propName = jQuery.propFix[ name ] || name;
+			if ( propName in elem ) {
+				// Only set the IDL specifically if it already exists on the element
+				elem[ propName ] = true;
+			}
+
+			elem.setAttribute( name, name.toLowerCase() );
+		}
+		return name;
+	}
+};
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !getSetAttribute ) {
+
+	fixSpecified = {
+		name: true,
+		id: true,
+		coords: true
+	};
+
+	// Use this for any attribute in IE6/7
+	// This fixes almost every IE6/7 issue
+	nodeHook = jQuery.valHooks.button = {
+		get: function( elem, name ) {
+			var ret;
+			ret = elem.getAttributeNode( name );
+			return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ?
+				ret.value :
+				undefined;
+		},
+		set: function( elem, value, name ) {
+			// Set the existing or create a new attribute node
+			var ret = elem.getAttributeNode( name );
+			if ( !ret ) {
+				ret = document.createAttribute( name );
+				elem.setAttributeNode( ret );
+			}
+			return ( ret.value = value + "" );
+		}
+	};
+
+	// Set width and height to auto instead of 0 on empty string( Bug #8150 )
+	// This is for removals
+	jQuery.each([ "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			set: function( elem, value ) {
+				if ( value === "" ) {
+					elem.setAttribute( name, "auto" );
+					return value;
+				}
+			}
+		});
+	});
+
+	// Set contenteditable to false on removals(#10429)
+	// Setting to empty string throws an error as an invalid value
+	jQuery.attrHooks.contenteditable = {
+		get: nodeHook.get,
+		set: function( elem, value, name ) {
+			if ( value === "" ) {
+				value = "false";
+			}
+			nodeHook.set( elem, value, name );
+		}
+	};
+}
+
+
+// Some attributes require a special call on IE
+if ( !jQuery.support.hrefNormalized ) {
+	jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+		jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+			get: function( elem ) {
+				var ret = elem.getAttribute( name, 2 );
+				return ret === null ? undefined : ret;
+			}
+		});
+	});
+}
+
+if ( !jQuery.support.style ) {
+	jQuery.attrHooks.style = {
+		get: function( elem ) {
+			// Return undefined in the case of empty string
+			// Normalize to lowercase since IE uppercases css property names
+			return elem.style.cssText.toLowerCase() || undefined;
+		},
+		set: function( elem, value ) {
+			return ( elem.style.cssText = value + "" );
+		}
+	};
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+	jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+		get: function( elem ) {
+			var parent = elem.parentNode;
+
+			if ( parent ) {
+				parent.selectedIndex;
+
+				// Make sure that it also works with optgroups, see #5701
+				if ( parent.parentNode ) {
+					parent.parentNode.selectedIndex;
+				}
+			}
+			return null;
+		}
+	});
+}
+
+// IE6/7 call enctype encoding
+if ( !jQuery.support.enctype ) {
+	jQuery.propFix.enctype = "encoding";
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+	jQuery.each([ "radio", "checkbox" ], function() {
+		jQuery.valHooks[ this ] = {
+			get: function( elem ) {
+				// Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+				return elem.getAttribute("value") === null ? "on" : elem.value;
+			}
+		};
+	});
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+	jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+		set: function( elem, value ) {
+			if ( jQuery.isArray( value ) ) {
+				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
+			}
+		}
+	});
+});
+var rformElems = /^(?:textarea|input|select)$/i,
+	rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/,
+	rhoverHack = /(?:^|\s)hover(\.\S+|)\b/,
+	rkeyEvent = /^key/,
+	rmouseEvent = /^(?:mouse|contextmenu)|click/,
+	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
+	hoverHack = function( events ) {
+		return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
+	};
+
+/*
+ * Helper functions for managing events -- not part of the public interface.
+ * Props to Dean Edwards' addEvent library for many of the ideas.
+ */
+jQuery.event = {
+
+	add: function( elem, types, handler, data, selector ) {
+
+		var elemData, eventHandle, events,
+			t, tns, type, namespaces, handleObj,
+			handleObjIn, handlers, special;
+
+		// Don't attach events to noData or text/comment nodes (allow plain objects tho)
+		if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
+			return;
+		}
+
+		// Caller can pass in an object of custom data in lieu of the handler
+		if ( handler.handler ) {
+			handleObjIn = handler;
+			handler = handleObjIn.handler;
+			selector = handleObjIn.selector;
+		}
+
+		// Make sure that the handler has a unique ID, used to find/remove it later
+		if ( !handler.guid ) {
+			handler.guid = jQuery.guid++;
+		}
+
+		// Init the element's event structure and main handler, if this is the first
+		events = elemData.events;
+		if ( !events ) {
+			elemData.events = events = {};
+		}
+		eventHandle = elemData.handle;
+		if ( !eventHandle ) {
+			elemData.handle = eventHandle = function( e ) {
+				// Discard the second event of a jQuery.event.trigger() and
+				// when an event is called after a page has unloaded
+				return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
+					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
+					undefined;
+			};
+			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
+			eventHandle.elem = elem;
+		}
+
+		// Handle multiple events separated by a space
+		// jQuery(...).bind("mouseover mouseout", fn);
+		types = jQuery.trim( hoverHack(types) ).split( " " );
+		for ( t = 0; t < types.length; t++ ) {
+
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = tns[1];
+			namespaces = ( tns[2] || "" ).split( "." ).sort();
+
+			// If event changes its type, use the special event handlers for the changed type
+			special = jQuery.event.special[ type ] || {};
+
+			// If selector defined, determine special event api type, otherwise given type
+			type = ( selector ? special.delegateType : special.bindType ) || type;
+
+			// Update special based on newly reset type
+			special = jQuery.event.special[ type ] || {};
+
+			// handleObj is passed to all event handlers
+			handleObj = jQuery.extend({
+				type: type,
+				origType: tns[1],
+				data: data,
+				handler: handler,
+				guid: handler.guid,
+				selector: selector,
+				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
+				namespace: namespaces.join(".")
+			}, handleObjIn );
+
+			// Init the event handler queue if we're the first
+			handlers = events[ type ];
+			if ( !handlers ) {
+				handlers = events[ type ] = [];
+				handlers.delegateCount = 0;
+
+				// Only use addEventListener/attachEvent if the special events handler returns false
+				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+					// Bind the global event handler to the element
+					if ( elem.addEventListener ) {
+						elem.addEventListener( type, eventHandle, false );
+
+					} else if ( elem.attachEvent ) {
+						elem.attachEvent( "on" + type, eventHandle );
+					}
+				}
+			}
+
+			if ( special.add ) {
+				special.add.call( elem, handleObj );
+
+				if ( !handleObj.handler.guid ) {
+					handleObj.handler.guid = handler.guid;
+				}
+			}
+
+			// Add to the element's handler list, delegates in front
+			if ( selector ) {
+				handlers.splice( handlers.delegateCount++, 0, handleObj );
+			} else {
+				handlers.push( handleObj );
+			}
+
+			// Keep track of which events have ever been used, for event optimization
+			jQuery.event.global[ type ] = true;
+		}
+
+		// Nullify elem to prevent memory leaks in IE
+		elem = null;
+	},
+
+	global: {},
+
+	// Detach an event or set of events from an element
+	remove: function( elem, types, handler, selector, mappedTypes ) {
+
+		var t, tns, type, origType, namespaces, origCount,
+			j, events, special, eventType, handleObj,
+			elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+
+		if ( !elemData || !(events = elemData.events) ) {
+			return;
+		}
+
+		// Once for each type.namespace in types; type may be omitted
+		types = jQuery.trim( hoverHack( types || "" ) ).split(" ");
+		for ( t = 0; t < types.length; t++ ) {
+			tns = rtypenamespace.exec( types[t] ) || [];
+			type = origType = tns[1];
+			namespaces = tns[2];
+
+			// Unbind all events (on this namespace, if provided) for the element
+			if ( !type ) {
+				for ( type in events ) {
+					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
+				}
+				continue;
+			}
+
+			special = jQuery.event.special[ type ] || {};
+			type = ( selector? special.delegateType : special.bindType ) || type;
+			eventType = events[ type ] || [];
+			origCount = eventType.length;
+			namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+
+			// Remove matching events
+			for ( j = 0; j < eventType.length; j++ ) {
+				handleObj = eventType[ j ];
+
+				if ( ( mappedTypes || origType === handleObj.origType ) &&
+					 ( !handler || handler.guid === handleObj.guid ) &&
+					 ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
+					 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
+					eventType.splice( j--, 1 );
+
+					if ( handleObj.selector ) {
+						eventType.delegateCount--;
+					}
+					if ( special.remove ) {
+						special.remove.call( elem, handleObj );
+					}
+				}
+			}
+
+			// Remove generic event handler if we removed something and no more handlers exist
+			// (avoids potential for endless recursion during removal of special event handlers)
+			if ( eventType.length === 0 && origCount !== eventType.length ) {
+				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
+					jQuery.removeEvent( elem, type, elemData.handle );
+				}
+
+				delete events[ type ];
+			}
+		}
+
+		// Remove the expando if it's no longer used
+		if ( jQuery.isEmptyObject( events ) ) {
+			delete elemData.handle;
+
+			// removeData also checks for emptiness and clears the expando if empty
+			// so use it instead of delete
+			jQuery.removeData( elem, "events", true );
+		}
+	},
+
+	// Events that are safe to short-circuit if no handlers are attached.
+	// Native DOM events should not be added, they may have inline handlers.
+	customEvent: {
+		"getData": true,
+		"setData": true,
+		"changeData": true
+	},
+
+	trigger: function( event, data, elem, onlyHandlers ) {
+		// Don't do events on text and comment nodes
+		if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
+			return;
+		}
+
+		// Event object or event type
+		var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType,
+			type = event.type || event,
+			namespaces = [];
+
+		// focus/blur morphs to focusin/out; ensure we're not firing them right now
+		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
+			return;
+		}
+
+		if ( type.indexOf( "!" ) >= 0 ) {
+			// Exclusive events trigger only for the exact event (no namespaces)
+			type = type.slice(0, -1);
+			exclusive = true;
+		}
+
+		if ( type.indexOf( "." ) >= 0 ) {
+			// Namespaced trigger; create a regexp to match event type in handle()
+			namespaces = type.split(".");
+			type = namespaces.shift();
+			namespaces.sort();
+		}
+
+		if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
+			// No jQuery handlers for this event type, and it can't have inline handlers
+			return;
+		}
+
+		// Caller can pass in an Event, Object, or just an event type string
+		event = typeof event === "object" ?
+			// jQuery.Event object
+			event[ jQuery.expando ] ? event :
+			// Object literal
+			new jQuery.Event( type, event ) :
+			// Just the event type (string)
+			new jQuery.Event( type );
+
+		event.type = type;
+		event.isTrigger = true;
+		event.exclusive = exclusive;
+		event.namespace = namespaces.join( "." );
+		event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
+		ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
+
+		// Handle a global trigger
+		if ( !elem ) {
+
+			// TODO: Stop taunting the data cache; remove global events and always attach to document
+			cache = jQuery.cache;
+			for ( i in cache ) {
+				if ( cache[ i ].events && cache[ i ].events[ type ] ) {
+					jQuery.event.trigger( event, data, cache[ i ].handle.elem, true );
+				}
+			}
+			return;
+		}
+
+		// Clean up the event in case it is being reused
+		event.result = undefined;
+		if ( !event.target ) {
+			event.target = elem;
+		}
+
+		// Clone any incoming data and prepend the event, creating the handler arg list
+		data = data != null ? jQuery.makeArray( data ) : [];
+		data.unshift( event );
+
+		// Allow special events to draw outside the lines
+		special = jQuery.event.special[ type ] || {};
+		if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
+			return;
+		}
+
+		// Determine event propagation path in advance, per W3C events spec (#9951)
+		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
+		eventPath = [[ elem, special.bindType || type ]];
+		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
+
+			bubbleType = special.delegateType || type;
+			cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;
+			for ( old = elem; cur; cur = cur.parentNode ) {
+				eventPath.push([ cur, bubbleType ]);
+				old = cur;
+			}
+
+			// Only add window if we got to document (e.g., not plain obj or detached DOM)
+			if ( old === (elem.ownerDocument || document) ) {
+				eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);
+			}
+		}
+
+		// Fire handlers on the event path
+		for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {
+
+			cur = eventPath[i][0];
+			event.type = eventPath[i][1];
+
+			handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
+			if ( handle ) {
+				handle.apply( cur, data );
+			}
+			// Note that this is a bare JS function and not a jQuery handler
+			handle = ontype && cur[ ontype ];
+			if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
+				event.preventDefault();
+			}
+		}
+		event.type = type;
+
+		// If nobody prevented the default action, do it now
+		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
+
+			if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
+				!(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+				// Call a native DOM method on the target with the same name name as the event.
+				// Can't use an .isFunction() check here because IE6/7 fails that test.
+				// Don't do default actions on window, that's where global variables be (#6170)
+				// IE<9 dies on focus/blur to hidden element (#1486)
+				if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {
+
+					// Don't re-trigger an onFOO event when we call its FOO() method
+					old = elem[ ontype ];
+
+					if ( old ) {
+						elem[ ontype ] = null;
+					}
+
+					// Prevent re-triggering of the same event, since we already bubbled it above
+					jQuery.event.triggered = type;
+					elem[ type ]();
+					jQuery.event.triggered = undefined;
+
+					if ( old ) {
+						elem[ ontype ] = old;
+					}
+				}
+			}
+		}
+
+		return event.result;
+	},
+
+	dispatch: function( event ) {
+
+		// Make a writable jQuery.Event from the native event object
+		event = jQuery.event.fix( event || window.event );
+
+		var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related,
+			handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
+			delegateCount = handlers.delegateCount,
+			args = core_slice.call( arguments ),
+			run_all = !event.exclusive && !event.namespace,
+			special = jQuery.event.special[ event.type ] || {},
+			handlerQueue = [];
+
+		// Use the fix-ed jQuery.Event rather than the (read-only) native event
+		args[0] = event;
+		event.delegateTarget = this;
+
+		// Call the preDispatch hook for the mapped type, and let it bail if desired
+		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
+			return;
+		}
+
+		// Determine handlers that should run if there are delegated events
+		// Avoid non-left-click bubbling in Firefox (#3861)
+		if ( delegateCount && !(event.button && event.type === "click") ) {
+
+			for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
+
+				// Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
+				if ( cur.disabled !== true || event.type !== "click" ) {
+					selMatch = {};
+					matches = [];
+					for ( i = 0; i < delegateCount; i++ ) {
+						handleObj = handlers[ i ];
+						sel = handleObj.selector;
+
+						if ( selMatch[ sel ] === undefined ) {
+							selMatch[ sel ] = handleObj.needsContext ?
+								jQuery( sel, this ).index( cur ) >= 0 :
+								jQuery.find( sel, this, null, [ cur ] ).length;
+						}
+						if ( selMatch[ sel ] ) {
+							matches.push( handleObj );
+						}
+					}
+					if ( matches.length ) {
+						handlerQueue.push({ elem: cur, matches: matches });
+					}
+				}
+			}
+		}
+
+		// Add the remaining (directly-bound) handlers
+		if ( handlers.length > delegateCount ) {
+			handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
+		}
+
+		// Run delegates first; they may want to stop propagation beneath us
+		for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
+			matched = handlerQueue[ i ];
+			event.currentTarget = matched.elem;
+
+			for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
+				handleObj = matched.matches[ j ];
+
+				// Triggered event must either 1) be non-exclusive and have no namespace, or
+				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
+				if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
+
+					event.data = handleObj.data;
+					event.handleObj = handleObj;
+
+					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
+							.apply( matched.elem, args );
+
+					if ( ret !== undefined ) {
+						event.result = ret;
+						if ( ret === false ) {
+							event.preventDefault();
+							event.stopPropagation();
+						}
+					}
+				}
+			}
+		}
+
+		// Call the postDispatch hook for the mapped type
+		if ( special.postDispatch ) {
+			special.postDispatch.call( this, event );
+		}
+
+		return event.result;
+	},
+
+	// Includes some event props shared by KeyEvent and MouseEvent
+	// *** attrChange attrName relatedNode srcElement  are not normalized, non-W3C, deprecated, will be removed in 1.8 ***
+	props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
+
+	fixHooks: {},
+
+	keyHooks: {
+		props: "char charCode key keyCode".split(" "),
+		filter: function( event, original ) {
+
+			// Add which for key events
+			if ( event.which == null ) {
+				event.which = original.charCode != null ? original.charCode : original.keyCode;
+			}
+
+			return event;
+		}
+	},
+
+	mouseHooks: {
+		props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
+		filter: function( event, original ) {
+			var eventDoc, doc, body,
+				button = original.button,
+				fromElement = original.fromElement;
+
+			// Calculate pageX/Y if missing and clientX/Y available
+			if ( event.pageX == null && original.clientX != null ) {
+				eventDoc = event.target.ownerDocument || document;
+				doc = eventDoc.documentElement;
+				body = eventDoc.body;
+
+				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
+			}
+
+			// Add relatedTarget, if necessary
+			if ( !event.relatedTarget && fromElement ) {
+				event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
+			}
+
+			// Add which for click: 1 === left; 2 === middle; 3 === right
+			// Note: button is not normalized, so don't use it
+			if ( !event.which && button !== undefined ) {
+				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+			}
+
+			return event;
+		}
+	},
+
+	fix: function( event ) {
+		if ( event[ jQuery.expando ] ) {
+			return event;
+		}
+
+		// Create a writable copy of the event object and normalize some properties
+		var i, prop,
+			originalEvent = event,
+			fixHook = jQuery.event.fixHooks[ event.type ] || {},
+			copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
+
+		event = jQuery.Event( originalEvent );
+
+		for ( i = copy.length; i; ) {
+			prop = copy[ --i ];
+			event[ prop ] = originalEvent[ prop ];
+		}
+
+		// Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
+		if ( !event.target ) {
+			event.target = originalEvent.srcElement || document;
+		}
+
+		// Target should not be a text node (#504, Safari)
+		if ( event.target.nodeType === 3 ) {
+			event.target = event.target.parentNode;
+		}
+
+		// For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8)
+		event.metaKey = !!event.metaKey;
+
+		return fixHook.filter? fixHook.filter( event, originalEvent ) : event;
+	},
+
+	special: {
+		load: {
+			// Prevent triggered image.load events from bubbling to window.load
+			noBubble: true
+		},
+
+		focus: {
+			delegateType: "focusin"
+		},
+		blur: {
+			delegateType: "focusout"
+		},
+
+		beforeunload: {
+			setup: function( data, namespaces, eventHandle ) {
+				// We only want to do this special case on windows
+				if ( jQuery.isWindow( this ) ) {
+					this.onbeforeunload = eventHandle;
+				}
+			},
+
+			teardown: function( namespaces, eventHandle ) {
+				if ( this.onbeforeunload === eventHandle ) {
+					this.onbeforeunload = null;
+				}
+			}
+		}
+	},
+
+	simulate: function( type, elem, event, bubble ) {
+		// Piggyback on a donor event to simulate a different one.
+		// Fake originalEvent to avoid donor's stopPropagation, but if the
+		// simulated event prevents default then we do the same on the donor.
+		var e = jQuery.extend(
+			new jQuery.Event(),
+			event,
+			{ type: type,
+				isSimulated: true,
+				originalEvent: {}
+			}
+		);
+		if ( bubble ) {
+			jQuery.event.trigger( e, null, elem );
+		} else {
+			jQuery.event.dispatch.call( elem, e );
+		}
+		if ( e.isDefaultPrevented() ) {
+			event.preventDefault();
+		}
+	}
+};
+
+// Some plugins are using, but it's undocumented/deprecated and will be removed.
+// The 1.7 special event interface should provide all the hooks needed now.
+jQuery.event.handle = jQuery.event.dispatch;
+
+jQuery.removeEvent = document.removeEventListener ?
+	function( elem, type, handle ) {
+		if ( elem.removeEventListener ) {
+			elem.removeEventListener( type, handle, false );
+		}
+	} :
+	function( elem, type, handle ) {
+		var name = "on" + type;
+
+		if ( elem.detachEvent ) {
+
+			// #8545, #7054, preventing memory leaks for custom events in IE6-8 –
+			// detachEvent needed property on element, by name of that event, to properly expose it to GC
+			if ( typeof elem[ name ] === "undefined" ) {
+				elem[ name ] = null;
+			}
+
+			elem.detachEvent( name, handle );
+		}
+	};
+
+jQuery.Event = function( src, props ) {
+	// Allow instantiation without the 'new' keyword
+	if ( !(this instanceof jQuery.Event) ) {
+		return new jQuery.Event( src, props );
+	}
+
+	// Event object
+	if ( src && src.type ) {
+		this.originalEvent = src;
+		this.type = src.type;
+
+		// Events bubbling up the document may have been marked as prevented
+		// by a handler lower down the tree; reflect the correct value.
+		this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
+			src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
+
+	// Event type
+	} else {
+		this.type = src;
+	}
+
+	// Put explicitly provided properties onto the event object
+	if ( props ) {
+		jQuery.extend( this, props );
+	}
+
+	// Create a timestamp if incoming event doesn't have one
+	this.timeStamp = src && src.timeStamp || jQuery.now();
+
+	// Mark it as fixed
+	this[ jQuery.expando ] = true;
+};
+
+function returnFalse() {
+	return false;
+}
+function returnTrue() {
+	return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+	preventDefault: function() {
+		this.isDefaultPrevented = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+
+		// if preventDefault exists run it on the original event
+		if ( e.preventDefault ) {
+			e.preventDefault();
+
+		// otherwise set the returnValue property of the original event to false (IE)
+		} else {
+			e.returnValue = false;
+		}
+	},
+	stopPropagation: function() {
+		this.isPropagationStopped = returnTrue;
+
+		var e = this.originalEvent;
+		if ( !e ) {
+			return;
+		}
+		// if stopPropagation exists run it on the original event
+		if ( e.stopPropagation ) {
+			e.stopPropagation();
+		}
+		// otherwise set the cancelBubble property of the original event to true (IE)
+		e.cancelBubble = true;
+	},
+	stopImmediatePropagation: function() {
+		this.isImmediatePropagationStopped = returnTrue;
+		this.stopPropagation();
+	},
+	isDefaultPrevented: returnFalse,
+	isPropagationStopped: returnFalse,
+	isImmediatePropagationStopped: returnFalse
+};
+
+// Create mouseenter/leave events using mouseover/out and event-time checks
+jQuery.each({
+	mouseenter: "mouseover",
+	mouseleave: "mouseout"
+}, function( orig, fix ) {
+	jQuery.event.special[ orig ] = {
+		delegateType: fix,
+		bindType: fix,
+
+		handle: function( event ) {
+			var ret,
+				target = this,
+				related = event.relatedTarget,
+				handleObj = event.handleObj,
+				selector = handleObj.selector;
+
+			// For mousenter/leave call the handler if related is outside the target.
+			// NB: No relatedTarget if the mouse left/entered the browser window
+			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
+				event.type = handleObj.origType;
+				ret = handleObj.handler.apply( this, arguments );
+				event.type = fix;
+			}
+			return ret;
+		}
+	};
+});
+
+// IE submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+	jQuery.event.special.submit = {
+		setup: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Lazy-add a submit handler when a descendant form may potentially be submitted
+			jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
+				// Node name check avoids a VML-related crash in IE (#9807)
+				var elem = e.target,
+					form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
+				if ( form && !jQuery._data( form, "_submit_attached" ) ) {
+					jQuery.event.add( form, "submit._submit", function( event ) {
+						event._submit_bubble = true;
+					});
+					jQuery._data( form, "_submit_attached", true );
+				}
+			});
+			// return undefined since we don't need an event listener
+		},
+
+		postDispatch: function( event ) {
+			// If form was submitted by the user, bubble the event up the tree
+			if ( event._submit_bubble ) {
+				delete event._submit_bubble;
+				if ( this.parentNode && !event.isTrigger ) {
+					jQuery.event.simulate( "submit", this.parentNode, event, true );
+				}
+			}
+		},
+
+		teardown: function() {
+			// Only need this for delegated form submit events
+			if ( jQuery.nodeName( this, "form" ) ) {
+				return false;
+			}
+
+			// Remove delegated handlers; cleanData eventually reaps submit handlers attached above
+			jQuery.event.remove( this, "._submit" );
+		}
+	};
+}
+
+// IE change delegation and checkbox/radio fix
+if ( !jQuery.support.changeBubbles ) {
+
+	jQuery.event.special.change = {
+
+		setup: function() {
+
+			if ( rformElems.test( this.nodeName ) ) {
+				// IE doesn't fire change on a check/radio until blur; trigger it on click
+				// after a propertychange. Eat the blur-change in special.change.handle.
+				// This still fires onchange a second time for check/radio after blur.
+				if ( this.type === "checkbox" || this.type === "radio" ) {
+					jQuery.event.add( this, "propertychange._change", function( event ) {
+						if ( event.originalEvent.propertyName === "checked" ) {
+							this._just_changed = true;
+						}
+					});
+					jQuery.event.add( this, "click._change", function( event ) {
+						if ( this._just_changed && !event.isTrigger ) {
+							this._just_changed = false;
+						}
+						// Allow triggered, simulated change events (#11500)
+						jQuery.event.simulate( "change", this, event, true );
+					});
+				}
+				return false;
+			}
+			// Delegated event; lazy-add a change handler on descendant inputs
+			jQuery.event.add( this, "beforeactivate._change", function( e ) {
+				var elem = e.target;
+
+				if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) {
+					jQuery.event.add( elem, "change._change", function( event ) {
+						if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
+							jQuery.event.simulate( "change", this.parentNode, event, true );
+						}
+					});
+					jQuery._data( elem, "_change_attached", true );
+				}
+			});
+		},
+
+		handle: function( event ) {
+			var elem = event.target;
+
+			// Swallow native change events from checkbox/radio, we already triggered them above
+			if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
+				return event.handleObj.handler.apply( this, arguments );
+			}
+		},
+
+		teardown: function() {
+			jQuery.event.remove( this, "._change" );
+
+			return !rformElems.test( this.nodeName );
+		}
+	};
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+		// Attach a single capturing handler while someone wants focusin/focusout
+		var attaches = 0,
+			handler = function( event ) {
+				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
+			};
+
+		jQuery.event.special[ fix ] = {
+			setup: function() {
+				if ( attaches++ === 0 ) {
+					document.addEventListener( orig, handler, true );
+				}
+			},
+			teardown: function() {
+				if ( --attaches === 0 ) {
+					document.removeEventListener( orig, handler, true );
+				}
+			}
+		};
+	});
+}
+
+jQuery.fn.extend({
+
+	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
+		var origFn, type;
+
+		// Types can be a map of types/handlers
+		if ( typeof types === "object" ) {
+			// ( types-Object, selector, data )
+			if ( typeof selector !== "string" ) { // && selector != null
+				// ( types-Object, data )
+				data = data || selector;
+				selector = undefined;
+			}
+			for ( type in types ) {
+				this.on( type, selector, data, types[ type ], one );
+			}
+			return this;
+		}
+
+		if ( data == null && fn == null ) {
+			// ( types, fn )
+			fn = selector;
+			data = selector = undefined;
+		} else if ( fn == null ) {
+			if ( typeof selector === "string" ) {
+				// ( types, selector, fn )
+				fn = data;
+				data = undefined;
+			} else {
+				// ( types, data, fn )
+				fn = data;
+				data = selector;
+				selector = undefined;
+			}
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		} else if ( !fn ) {
+			return this;
+		}
+
+		if ( one === 1 ) {
+			origFn = fn;
+			fn = function( event ) {
+				// Can use an empty set, since event contains the info
+				jQuery().off( event );
+				return origFn.apply( this, arguments );
+			};
+			// Use same guid so caller can remove using origFn
+			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
+		}
+		return this.each( function() {
+			jQuery.event.add( this, types, fn, data, selector );
+		});
+	},
+	one: function( types, selector, data, fn ) {
+		return this.on( types, selector, data, fn, 1 );
+	},
+	off: function( types, selector, fn ) {
+		var handleObj, type;
+		if ( types && types.preventDefault && types.handleObj ) {
+			// ( event )  dispatched jQuery.Event
+			handleObj = types.handleObj;
+			jQuery( types.delegateTarget ).off(
+				handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
+				handleObj.selector,
+				handleObj.handler
+			);
+			return this;
+		}
+		if ( typeof types === "object" ) {
+			// ( types-object [, selector] )
+			for ( type in types ) {
+				this.off( type, selector, types[ type ] );
+			}
+			return this;
+		}
+		if ( selector === false || typeof selector === "function" ) {
+			// ( types [, fn] )
+			fn = selector;
+			selector = undefined;
+		}
+		if ( fn === false ) {
+			fn = returnFalse;
+		}
+		return this.each(function() {
+			jQuery.event.remove( this, types, fn, selector );
+		});
+	},
+
+	bind: function( types, data, fn ) {
+		return this.on( types, null, data, fn );
+	},
+	unbind: function( types, fn ) {
+		return this.off( types, null, fn );
+	},
+
+	live: function( types, data, fn ) {
+		jQuery( this.context ).on( types, this.selector, data, fn );
+		return this;
+	},
+	die: function( types, fn ) {
+		jQuery( this.context ).off( types, this.selector || "**", fn );
+		return this;
+	},
+
+	delegate: function( selector, types, data, fn ) {
+		return this.on( types, selector, data, fn );
+	},
+	undelegate: function( selector, types, fn ) {
+		// ( namespace ) or ( selector, types [, fn] )
+		return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
+	},
+
+	trigger: function( type, data ) {
+		return this.each(function() {
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	triggerHandler: function( type, data ) {
+		if ( this[0] ) {
+			return jQuery.event.trigger( type, data, this[0], true );
+		}
+	},
+
+	toggle: function( fn ) {
+		// Save reference to arguments for access in closure
+		var args = arguments,
+			guid = fn.guid || jQuery.guid++,
+			i = 0,
+			toggler = function( event ) {
+				// Figure out which function to execute
+				var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+				jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+				// Make sure that clicks stop
+				event.preventDefault();
+
+				// and execute the function
+				return args[ lastToggle ].apply( this, arguments ) || false;
+			};
+
+		// link all the functions, so any of them can unbind this click handler
+		toggler.guid = guid;
+		while ( i < args.length ) {
+			args[ i++ ].guid = guid;
+		}
+
+		return this.click( toggler );
+	},
+
+	hover: function( fnOver, fnOut ) {
+		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+	}
+});
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
+
+	// Handle event binding
+	jQuery.fn[ name ] = function( data, fn ) {
+		if ( fn == null ) {
+			fn = data;
+			data = null;
+		}
+
+		return arguments.length > 0 ?
+			this.on( name, null, data, fn ) :
+			this.trigger( name );
+	};
+
+	if ( rkeyEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;
+	}
+
+	if ( rmouseEvent.test( name ) ) {
+		jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;
+	}
+});
+/*!
+ * Sizzle CSS Selector Engine
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://sizzlejs.com/
+ */
+(function( window, undefined ) {
+
+var cachedruns,
+	assertGetIdNotName,
+	Expr,
+	getText,
+	isXML,
+	contains,
+	compile,
+	sortOrder,
+	hasDuplicate,
+	outermostContext,
+
+	baseHasDuplicate = true,
+	strundefined = "undefined",
+
+	expando = ( "sizcache" + Math.random() ).replace( ".", "" ),
+
+	Token = String,
+	document = window.document,
+	docElem = document.documentElement,
+	dirruns = 0,
+	done = 0,
+	pop = [].pop,
+	push = [].push,
+	slice = [].slice,
+	// Use a stripped-down indexOf if a native one is unavailable
+	indexOf = [].indexOf || function( elem ) {
+		var i = 0,
+			len = this.length;
+		for ( ; i < len; i++ ) {
+			if ( this[i] === elem ) {
+				return i;
+			}
+		}
+		return -1;
+	},
+
+	// Augment a function for special use by Sizzle
+	markFunction = function( fn, value ) {
+		fn[ expando ] = value == null || value;
+		return fn;
+	},
+
+	createCache = function() {
+		var cache = {},
+			keys = [];
+
+		return markFunction(function( key, value ) {
+			// Only keep the most recent entries
+			if ( keys.push( key ) > Expr.cacheLength ) {
+				delete cache[ keys.shift() ];
+			}
+
+			return (cache[ key ] = value);
+		}, cache );
+	},
+
+	classCache = createCache(),
+	tokenCache = createCache(),
+	compilerCache = createCache(),
+
+	// Regex
+
+	// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+	whitespace = "[\\x20\\t\\r\\n\\f]",
+	// http://www.w3.org/TR/css3-syntax/#characters
+	characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",
+
+	// Loosely modeled on CSS identifier characters
+	// An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors)
+	// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+	identifier = characterEncoding.replace( "w", "w#" ),
+
+	// Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+	operators = "([*^$|!~]?=)",
+	attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+		"*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+	// Prefer arguments not in parens/brackets,
+	//   then attribute selectors and non-pseudos (denoted by :),
+	//   then anything else
+	// These preferences are here to reduce the number of selectors
+	//   needing tokenize in the PSEUDO preFilter
+	pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)",
+
+	// For matchExpr.POS and matchExpr.needsContext
+	pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
+		"*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)",
+
+	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+	rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ),
+	rpseudo = new RegExp( pseudos ),
+
+	// Easily-parseable/retrievable ID or TAG or CLASS selectors
+	rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,
+
+	rnot = /^:not/,
+	rsibling = /[\x20\t\r\n\f]*[+~]/,
+	rendsWithNot = /:not\($/,
+
+	rheader = /h\d/i,
+	rinputs = /input|select|textarea|button/i,
+
+	rbackslash = /\\(?!\\)/g,
+
+	matchExpr = {
+		"ID": new RegExp( "^#(" + characterEncoding + ")" ),
+		"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+		"NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ),
+		"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+		"ATTR": new RegExp( "^" + attributes ),
+		"PSEUDO": new RegExp( "^" + pseudos ),
+		"POS": new RegExp( pos, "i" ),
+		"CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace +
+			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+		// For use in libraries implementing .is()
+		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" )
+	},
+
+	// Support
+
+	// Used for testing something on an element
+	assert = function( fn ) {
+		var div = document.createElement("div");
+
+		try {
+			return fn( div );
+		} catch (e) {
+			return false;
+		} finally {
+			// release memory in IE
+			div = null;
+		}
+	},
+
+	// Check if getElementsByTagName("*") returns only elements
+	assertTagNameNoComments = assert(function( div ) {
+		div.appendChild( document.createComment("") );
+		return !div.getElementsByTagName("*").length;
+	}),
+
+	// Check if getAttribute returns normalized href attributes
+	assertHrefNotNormalized = assert(function( div ) {
+		div.innerHTML = "<a href='#'></a>";
+		return div.firstChild && typeof div.firstChild.getAttribute !== strundefined &&
+			div.firstChild.getAttribute("href") === "#";
+	}),
+
+	// Check if attributes should be retrieved by attribute nodes
+	assertAttributes = assert(function( div ) {
+		div.innerHTML = "<select></select>";
+		var type = typeof div.lastChild.getAttribute("multiple");
+		// IE8 returns a string for some attributes even when not present
+		return type !== "boolean" && type !== "string";
+	}),
+
+	// Check if getElementsByClassName can be trusted
+	assertUsableClassName = assert(function( div ) {
+		// Opera can't find a second classname (in 9.6)
+		div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>";
+		if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) {
+			return false;
+		}
+
+		// Safari 3.2 caches class attributes and doesn't catch changes
+		div.lastChild.className = "e";
+		return div.getElementsByClassName("e").length === 2;
+	}),
+
+	// Check if getElementById returns elements by name
+	// Check if getElementsByName privileges form controls or returns elements by ID
+	assertUsableName = assert(function( div ) {
+		// Inject content
+		div.id = expando + 0;
+		div.innerHTML = "<a name='" + expando + "'></a><div name='" + expando + "'></div>";
+		docElem.insertBefore( div, docElem.firstChild );
+
+		// Test
+		var pass = document.getElementsByName &&
+			// buggy browsers will return fewer than the correct 2
+			document.getElementsByName( expando ).length === 2 +
+			// buggy browsers will return more than the correct 0
+			document.getElementsByName( expando + 0 ).length;
+		assertGetIdNotName = !document.getElementById( expando );
+
+		// Cleanup
+		docElem.removeChild( div );
+
+		return pass;
+	});
+
+// If slice is not available, provide a backup
+try {
+	slice.call( docElem.childNodes, 0 )[0].nodeType;
+} catch ( e ) {
+	slice = function( i ) {
+		var elem,
+			results = [];
+		for ( ; (elem = this[i]); i++ ) {
+			results.push( elem );
+		}
+		return results;
+	};
+}
+
+function Sizzle( selector, context, results, seed ) {
+	results = results || [];
+	context = context || document;
+	var match, elem, xml, m,
+		nodeType = context.nodeType;
+
+	if ( !selector || typeof selector !== "string" ) {
+		return results;
+	}
+
+	if ( nodeType !== 1 && nodeType !== 9 ) {
+		return [];
+	}
+
+	xml = isXML( context );
+
+	if ( !xml && !seed ) {
+		if ( (match = rquickExpr.exec( selector )) ) {
+			// Speed-up: Sizzle("#ID")
+			if ( (m = match[1]) ) {
+				if ( nodeType === 9 ) {
+					elem = context.getElementById( m );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					if ( elem && elem.parentNode ) {
+						// Handle the case where IE, Opera, and Webkit return items
+						// by name instead of ID
+						if ( elem.id === m ) {
+							results.push( elem );
+							return results;
+						}
+					} else {
+						return results;
+					}
+				} else {
+					// Context is not a document
+					if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+						contains( context, elem ) && elem.id === m ) {
+						results.push( elem );
+						return results;
+					}
+				}
+
+			// Speed-up: Sizzle("TAG")
+			} else if ( match[2] ) {
+				push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) );
+				return results;
+
+			// Speed-up: Sizzle(".CLASS")
+			} else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) {
+				push.apply( results, slice.call(context.getElementsByClassName( m ), 0) );
+				return results;
+			}
+		}
+	}
+
+	// All others
+	return select( selector.replace( rtrim, "$1" ), context, results, seed, xml );
+}
+
+Sizzle.matches = function( expr, elements ) {
+	return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+	return Sizzle( expr, null, null, [ elem ] ).length > 0;
+};
+
+// Returns a function to use in pseudos for input types
+function createInputPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return name === "input" && elem.type === type;
+	};
+}
+
+// Returns a function to use in pseudos for buttons
+function createButtonPseudo( type ) {
+	return function( elem ) {
+		var name = elem.nodeName.toLowerCase();
+		return (name === "input" || name === "button") && elem.type === type;
+	};
+}
+
+// Returns a function to use in pseudos for positionals
+function createPositionalPseudo( fn ) {
+	return markFunction(function( argument ) {
+		argument = +argument;
+		return markFunction(function( seed, matches ) {
+			var j,
+				matchIndexes = fn( [], seed.length, argument ),
+				i = matchIndexes.length;
+
+			// Match elements found at the specified indexes
+			while ( i-- ) {
+				if ( seed[ (j = matchIndexes[i]) ] ) {
+					seed[j] = !(matches[j] = seed[j]);
+				}
+			}
+		});
+	});
+}
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+	var node,
+		ret = "",
+		i = 0,
+		nodeType = elem.nodeType;
+
+	if ( nodeType ) {
+		if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+			// Use textContent for elements
+			// innerText usage removed for consistency of new lines (see #11153)
+			if ( typeof elem.textContent === "string" ) {
+				return elem.textContent;
+			} else {
+				// Traverse its children
+				for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+					ret += getText( elem );
+				}
+			}
+		} else if ( nodeType === 3 || nodeType === 4 ) {
+			return elem.nodeValue;
+		}
+		// Do not include comment or processing instruction nodes
+	} else {
+
+		// If no nodeType, this is expected to be an array
+		for ( ; (node = elem[i]); i++ ) {
+			// Do not traverse comment nodes
+			ret += getText( node );
+		}
+	}
+	return ret;
+};
+
+isXML = Sizzle.isXML = function( elem ) {
+	// documentElement is verified for cases where it doesn't yet exist
+	// (such as loading iframes in IE - #4833)
+	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+	return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+// Element contains another
+contains = Sizzle.contains = docElem.contains ?
+	function( a, b ) {
+		var adown = a.nodeType === 9 ? a.documentElement : a,
+			bup = b && b.parentNode;
+		return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) );
+	} :
+	docElem.compareDocumentPosition ?
+	function( a, b ) {
+		return b && !!( a.compareDocumentPosition( b ) & 16 );
+	} :
+	function( a, b ) {
+		while ( (b = b.parentNode) ) {
+			if ( b === a ) {
+				return true;
+			}
+		}
+		return false;
+	};
+
+Sizzle.attr = function( elem, name ) {
+	var val,
+		xml = isXML( elem );
+
+	if ( !xml ) {
+		name = name.toLowerCase();
+	}
+	if ( (val = Expr.attrHandle[ name ]) ) {
+		return val( elem );
+	}
+	if ( xml || assertAttributes ) {
+		return elem.getAttribute( name );
+	}
+	val = elem.getAttributeNode( name );
+	return val ?
+		typeof elem[ name ] === "boolean" ?
+			elem[ name ] ? name : null :
+			val.specified ? val.value : null :
+		null;
+};
+
+Expr = Sizzle.selectors = {
+
+	// Can be adjusted by the user
+	cacheLength: 50,
+
+	createPseudo: markFunction,
+
+	match: matchExpr,
+
+	// IE6/7 return a modified href
+	attrHandle: assertHrefNotNormalized ?
+		{} :
+		{
+			"href": function( elem ) {
+				return elem.getAttribute( "href", 2 );
+			},
+			"type": function( elem ) {
+				return elem.getAttribute("type");
+			}
+		},
+
+	find: {
+		"ID": assertGetIdNotName ?
+			function( id, context, xml ) {
+				if ( typeof context.getElementById !== strundefined && !xml ) {
+					var m = context.getElementById( id );
+					// Check parentNode to catch when Blackberry 4.6 returns
+					// nodes that are no longer in the document #6963
+					return m && m.parentNode ? [m] : [];
+				}
+			} :
+			function( id, context, xml ) {
+				if ( typeof context.getElementById !== strundefined && !xml ) {
+					var m = context.getElementById( id );
+
+					return m ?
+						m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ?
+							[m] :
+							undefined :
+						[];
+				}
+			},
+
+		"TAG": assertTagNameNoComments ?
+			function( tag, context ) {
+				if ( typeof context.getElementsByTagName !== strundefined ) {
+					return context.getElementsByTagName( tag );
+				}
+			} :
+			function( tag, context ) {
+				var results = context.getElementsByTagName( tag );
+
+				// Filter out possible comments
+				if ( tag === "*" ) {
+					var elem,
+						tmp = [],
+						i = 0;
+
+					for ( ; (elem = results[i]); i++ ) {
+						if ( elem.nodeType === 1 ) {
+							tmp.push( elem );
+						}
+					}
+
+					return tmp;
+				}
+				return results;
+			},
+
+		"NAME": assertUsableName && function( tag, context ) {
+			if ( typeof context.getElementsByName !== strundefined ) {
+				return context.getElementsByName( name );
+			}
+		},
+
+		"CLASS": assertUsableClassName && function( className, context, xml ) {
+			if ( typeof context.getElementsByClassName !== strundefined && !xml ) {
+				return context.getElementsByClassName( className );
+			}
+		}
+	},
+
+	relative: {
+		">": { dir: "parentNode", first: true },
+		" ": { dir: "parentNode" },
+		"+": { dir: "previousSibling", first: true },
+		"~": { dir: "previousSibling" }
+	},
+
+	preFilter: {
+		"ATTR": function( match ) {
+			match[1] = match[1].replace( rbackslash, "" );
+
+			// Move the given value to match[3] whether quoted or unquoted
+			match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" );
+
+			if ( match[2] === "~=" ) {
+				match[3] = " " + match[3] + " ";
+			}
+
+			return match.slice( 0, 4 );
+		},
+
+		"CHILD": function( match ) {
+			/* matches from matchExpr["CHILD"]
+				1 type (only|nth|...)
+				2 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+				3 xn-component of xn+y argument ([+-]?\d*n|)
+				4 sign of xn-component
+				5 x of xn-component
+				6 sign of y-component
+				7 y of y-component
+			*/
+			match[1] = match[1].toLowerCase();
+
+			if ( match[1] === "nth" ) {
+				// nth-child requires argument
+				if ( !match[2] ) {
+					Sizzle.error( match[0] );
+				}
+
+				// numeric x and y parameters for Expr.filter.CHILD
+				// remember that false/true cast respectively to 0/1
+				match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) );
+				match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" );
+
+			// other types prohibit arguments
+			} else if ( match[2] ) {
+				Sizzle.error( match[0] );
+			}
+
+			return match;
+		},
+
+		"PSEUDO": function( match ) {
+			var unquoted, excess;
+			if ( matchExpr["CHILD"].test( match[0] ) ) {
+				return null;
+			}
+
+			if ( match[3] ) {
+				match[2] = match[3];
+			} else if ( (unquoted = match[4]) ) {
+				// Only check arguments that contain a pseudo
+				if ( rpseudo.test(unquoted) &&
+					// Get excess from tokenize (recursively)
+					(excess = tokenize( unquoted, true )) &&
+					// advance to the next closing parenthesis
+					(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+					// excess is a negative index
+					unquoted = unquoted.slice( 0, excess );
+					match[0] = match[0].slice( 0, excess );
+				}
+				match[2] = unquoted;
+			}
+
+			// Return only captures needed by the pseudo filter method (type and argument)
+			return match.slice( 0, 3 );
+		}
+	},
+
+	filter: {
+		"ID": assertGetIdNotName ?
+			function( id ) {
+				id = id.replace( rbackslash, "" );
+				return function( elem ) {
+					return elem.getAttribute("id") === id;
+				};
+			} :
+			function( id ) {
+				id = id.replace( rbackslash, "" );
+				return function( elem ) {
+					var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+					return node && node.value === id;
+				};
+			},
+
+		"TAG": function( nodeName ) {
+			if ( nodeName === "*" ) {
+				return function() { return true; };
+			}
+			nodeName = nodeName.replace( rbackslash, "" ).toLowerCase();
+
+			return function( elem ) {
+				return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+			};
+		},
+
+		"CLASS": function( className ) {
+			var pattern = classCache[ expando ][ className ];
+			if ( !pattern ) {
+				pattern = classCache( className, new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)") );
+			}
+			return function( elem ) {
+				return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
+			};
+		},
+
+		"ATTR": function( name, operator, check ) {
+			return function( elem, context ) {
+				var result = Sizzle.attr( elem, name );
+
+				if ( result == null ) {
+					return operator === "!=";
+				}
+				if ( !operator ) {
+					return true;
+				}
+
+				result += "";
+
+				return operator === "=" ? result === check :
+					operator === "!=" ? result !== check :
+					operator === "^=" ? check && result.indexOf( check ) === 0 :
+					operator === "*=" ? check && result.indexOf( check ) > -1 :
+					operator === "$=" ? check && result.substr( result.length - check.length ) === check :
+					operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+					operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" :
+					false;
+			};
+		},
+
+		"CHILD": function( type, argument, first, last ) {
+
+			if ( type === "nth" ) {
+				return function( elem ) {
+					var node, diff,
+						parent = elem.parentNode;
+
+					if ( first === 1 && last === 0 ) {
+						return true;
+					}
+
+					if ( parent ) {
+						diff = 0;
+						for ( node = parent.firstChild; node; node = node.nextSibling ) {
+							if ( node.nodeType === 1 ) {
+								diff++;
+								if ( elem === node ) {
+									break;
+								}
+							}
+						}
+					}
+
+					// Incorporate the offset (or cast to NaN), then check against cycle size
+					diff -= last;
+					return diff === first || ( diff % first === 0 && diff / first >= 0 );
+				};
+			}
+
+			return function( elem ) {
+				var node = elem;
+
+				switch ( type ) {
+					case "only":
+					case "first":
+						while ( (node = node.previousSibling) ) {
+							if ( node.nodeType === 1 ) {
+								return false;
+							}
+						}
+
+						if ( type === "first" ) {
+							return true;
+						}
+
+						node = elem;
+
+						/* falls through */
+					case "last":
+						while ( (node = node.nextSibling) ) {
+							if ( node.nodeType === 1 ) {
+								return false;
+							}
+						}
+
+						return true;
+				}
+			};
+		},
+
+		"PSEUDO": function( pseudo, argument ) {
+			// pseudo-class names are case-insensitive
+			// http://www.w3.org/TR/selectors/#pseudo-classes
+			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+			// Remember that setFilters inherits from pseudos
+			var args,
+				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+					Sizzle.error( "unsupported pseudo: " + pseudo );
+
+			// The user may use createPseudo to indicate that
+			// arguments are needed to create the filter function
+			// just as Sizzle does
+			if ( fn[ expando ] ) {
+				return fn( argument );
+			}
+
+			// But maintain support for old signatures
+			if ( fn.length > 1 ) {
+				args = [ pseudo, pseudo, "", argument ];
+				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+					markFunction(function( seed, matches ) {
+						var idx,
+							matched = fn( seed, argument ),
+							i = matched.length;
+						while ( i-- ) {
+							idx = indexOf.call( seed, matched[i] );
+							seed[ idx ] = !( matches[ idx ] = matched[i] );
+						}
+					}) :
+					function( elem ) {
+						return fn( elem, 0, args );
+					};
+			}
+
+			return fn;
+		}
+	},
+
+	pseudos: {
+		"not": markFunction(function( selector ) {
+			// Trim the selector passed to compile
+			// to avoid treating leading and trailing
+			// spaces as combinators
+			var input = [],
+				results = [],
+				matcher = compile( selector.replace( rtrim, "$1" ) );
+
+			return matcher[ expando ] ?
+				markFunction(function( seed, matches, context, xml ) {
+					var elem,
+						unmatched = matcher( seed, null, xml, [] ),
+						i = seed.length;
+
+					// Match elements unmatched by `matcher`
+					while ( i-- ) {
+						if ( (elem = unmatched[i]) ) {
+							seed[i] = !(matches[i] = elem);
+						}
+					}
+				}) :
+				function( elem, context, xml ) {
+					input[0] = elem;
+					matcher( input, null, xml, results );
+					return !results.pop();
+				};
+		}),
+
+		"has": markFunction(function( selector ) {
+			return function( elem ) {
+				return Sizzle( selector, elem ).length > 0;
+			};
+		}),
+
+		"contains": markFunction(function( text ) {
+			return function( elem ) {
+				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+			};
+		}),
+
+		"enabled": function( elem ) {
+			return elem.disabled === false;
+		},
+
+		"disabled": function( elem ) {
+			return elem.disabled === true;
+		},
+
+		"checked": function( elem ) {
+			// In CSS3, :checked should return both checked and selected elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			var nodeName = elem.nodeName.toLowerCase();
+			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+		},
+
+		"selected": function( elem ) {
+			// Accessing this property makes selected-by-default
+			// options in Safari work properly
+			if ( elem.parentNode ) {
+				elem.parentNode.selectedIndex;
+			}
+
+			return elem.selected === true;
+		},
+
+		"parent": function( elem ) {
+			return !Expr.pseudos["empty"]( elem );
+		},
+
+		"empty": function( elem ) {
+			// http://www.w3.org/TR/selectors/#empty-pseudo
+			// :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+			//   not comment, processing instructions, or others
+			// Thanks to Diego Perini for the nodeName shortcut
+			//   Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+			var nodeType;
+			elem = elem.firstChild;
+			while ( elem ) {
+				if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) {
+					return false;
+				}
+				elem = elem.nextSibling;
+			}
+			return true;
+		},
+
+		"header": function( elem ) {
+			return rheader.test( elem.nodeName );
+		},
+
+		"text": function( elem ) {
+			var type, attr;
+			// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+			// use getAttribute instead to test this case
+			return elem.nodeName.toLowerCase() === "input" &&
+				(type = elem.type) === "text" &&
+				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type );
+		},
+
+		// Input types
+		"radio": createInputPseudo("radio"),
+		"checkbox": createInputPseudo("checkbox"),
+		"file": createInputPseudo("file"),
+		"password": createInputPseudo("password"),
+		"image": createInputPseudo("image"),
+
+		"submit": createButtonPseudo("submit"),
+		"reset": createButtonPseudo("reset"),
+
+		"button": function( elem ) {
+			var name = elem.nodeName.toLowerCase();
+			return name === "input" && elem.type === "button" || name === "button";
+		},
+
+		"input": function( elem ) {
+			return rinputs.test( elem.nodeName );
+		},
+
+		"focus": function( elem ) {
+			var doc = elem.ownerDocument;
+			return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href);
+		},
+
+		"active": function( elem ) {
+			return elem === elem.ownerDocument.activeElement;
+		},
+
+		// Positional types
+		"first": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ 0 ];
+		}),
+
+		"last": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ length - 1 ];
+		}),
+
+		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			return [ argument < 0 ? argument + length : argument ];
+		}),
+
+		"even": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = 0; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"odd": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = 1; i < length; i += 2 ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		}),
+
+		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+			for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) {
+				matchIndexes.push( i );
+			}
+			return matchIndexes;
+		})
+	}
+};
+
+function siblingCheck( a, b, ret ) {
+	if ( a === b ) {
+		return ret;
+	}
+
+	var cur = a.nextSibling;
+
+	while ( cur ) {
+		if ( cur === b ) {
+			return -1;
+		}
+
+		cur = cur.nextSibling;
+	}
+
+	return 1;
+}
+
+sortOrder = docElem.compareDocumentPosition ?
+	function( a, b ) {
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+		}
+
+		return ( !a.compareDocumentPosition || !b.compareDocumentPosition ?
+			a.compareDocumentPosition :
+			a.compareDocumentPosition(b) & 4
+		) ? -1 : 1;
+	} :
+	function( a, b ) {
+		// The nodes are identical, we can exit early
+		if ( a === b ) {
+			hasDuplicate = true;
+			return 0;
+
+		// Fallback to using sourceIndex (in IE) if it's available on both nodes
+		} else if ( a.sourceIndex && b.sourceIndex ) {
+			return a.sourceIndex - b.sourceIndex;
+		}
+
+		var al, bl,
+			ap = [],
+			bp = [],
+			aup = a.parentNode,
+			bup = b.parentNode,
+			cur = aup;
+
+		// If the nodes are siblings (or identical) we can do a quick check
+		if ( aup === bup ) {
+			return siblingCheck( a, b );
+
+		// If no parents were found then the nodes are disconnected
+		} else if ( !aup ) {
+			return -1;
+
+		} else if ( !bup ) {
+			return 1;
+		}
+
+		// Otherwise they're somewhere else in the tree so we need
+		// to build up a full list of the parentNodes for comparison
+		while ( cur ) {
+			ap.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		cur = bup;
+
+		while ( cur ) {
+			bp.unshift( cur );
+			cur = cur.parentNode;
+		}
+
+		al = ap.length;
+		bl = bp.length;
+
+		// Start walking down the tree looking for a discrepancy
+		for ( var i = 0; i < al && i < bl; i++ ) {
+			if ( ap[i] !== bp[i] ) {
+				return siblingCheck( ap[i], bp[i] );
+			}
+		}
+
+		// We ended someplace up the tree so do a sibling check
+		return i === al ?
+			siblingCheck( a, bp[i], -1 ) :
+			siblingCheck( ap[i], b, 1 );
+	};
+
+// Always assume the presence of duplicates if sort doesn't
+// pass them to our comparison function (as in Google Chrome).
+[0, 0].sort( sortOrder );
+baseHasDuplicate = !hasDuplicate;
+
+// Document sorting and removing duplicates
+Sizzle.uniqueSort = function( results ) {
+	var elem,
+		i = 1;
+
+	hasDuplicate = baseHasDuplicate;
+	results.sort( sortOrder );
+
+	if ( hasDuplicate ) {
+		for ( ; (elem = results[i]); i++ ) {
+			if ( elem === results[ i - 1 ] ) {
+				results.splice( i--, 1 );
+			}
+		}
+	}
+
+	return results;
+};
+
+Sizzle.error = function( msg ) {
+	throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+function tokenize( selector, parseOnly ) {
+	var matched, match, tokens, type, soFar, groups, preFilters,
+		cached = tokenCache[ expando ][ selector ];
+
+	if ( cached ) {
+		return parseOnly ? 0 : cached.slice( 0 );
+	}
+
+	soFar = selector;
+	groups = [];
+	preFilters = Expr.preFilter;
+
+	while ( soFar ) {
+
+		// Comma and first run
+		if ( !matched || (match = rcomma.exec( soFar )) ) {
+			if ( match ) {
+				soFar = soFar.slice( match[0].length );
+			}
+			groups.push( tokens = [] );
+		}
+
+		matched = false;
+
+		// Combinators
+		if ( (match = rcombinators.exec( soFar )) ) {
+			tokens.push( matched = new Token( match.shift() ) );
+			soFar = soFar.slice( matched.length );
+
+			// Cast descendant combinators to space
+			matched.type = match[0].replace( rtrim, " " );
+		}
+
+		// Filters
+		for ( type in Expr.filter ) {
+			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+				// The last two arguments here are (context, xml) for backCompat
+				(match = preFilters[ type ]( match, document, true ))) ) {
+
+				tokens.push( matched = new Token( match.shift() ) );
+				soFar = soFar.slice( matched.length );
+				matched.type = type;
+				matched.matches = match;
+			}
+		}
+
+		if ( !matched ) {
+			break;
+		}
+	}
+
+	// Return the length of the invalid excess
+	// if we're just parsing
+	// Otherwise, throw an error or return tokens
+	return parseOnly ?
+		soFar.length :
+		soFar ?
+			Sizzle.error( selector ) :
+			// Cache the tokens
+			tokenCache( selector, groups ).slice( 0 );
+}
+
+function addCombinator( matcher, combinator, base ) {
+	var dir = combinator.dir,
+		checkNonElements = base && combinator.dir === "parentNode",
+		doneName = done++;
+
+	return combinator.first ?
+		// Check against closest ancestor/preceding element
+		function( elem, context, xml ) {
+			while ( (elem = elem[ dir ]) ) {
+				if ( checkNonElements || elem.nodeType === 1  ) {
+					return matcher( elem, context, xml );
+				}
+			}
+		} :
+
+		// Check against all ancestor/preceding elements
+		function( elem, context, xml ) {
+			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+			if ( !xml ) {
+				var cache,
+					dirkey = dirruns + " " + doneName + " ",
+					cachedkey = dirkey + cachedruns;
+				while ( (elem = elem[ dir ]) ) {
+					if ( checkNonElements || elem.nodeType === 1 ) {
+						if ( (cache = elem[ expando ]) === cachedkey ) {
+							return elem.sizset;
+						} else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) {
+							if ( elem.sizset ) {
+								return elem;
+							}
+						} else {
+							elem[ expando ] = cachedkey;
+							if ( matcher( elem, context, xml ) ) {
+								elem.sizset = true;
+								return elem;
+							}
+							elem.sizset = false;
+						}
+					}
+				}
+			} else {
+				while ( (elem = elem[ dir ]) ) {
+					if ( checkNonElements || elem.nodeType === 1 ) {
+						if ( matcher( elem, context, xml ) ) {
+							return elem;
+						}
+					}
+				}
+			}
+		};
+}
+
+function elementMatcher( matchers ) {
+	return matchers.length > 1 ?
+		function( elem, context, xml ) {
+			var i = matchers.length;
+			while ( i-- ) {
+				if ( !matchers[i]( elem, context, xml ) ) {
+					return false;
+				}
+			}
+			return true;
+		} :
+		matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+	var elem,
+		newUnmatched = [],
+		i = 0,
+		len = unmatched.length,
+		mapped = map != null;
+
+	for ( ; i < len; i++ ) {
+		if ( (elem = unmatched[i]) ) {
+			if ( !filter || filter( elem, context, xml ) ) {
+				newUnmatched.push( elem );
+				if ( mapped ) {
+					map.push( i );
+				}
+			}
+		}
+	}
+
+	return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+	if ( postFilter && !postFilter[ expando ] ) {
+		postFilter = setMatcher( postFilter );
+	}
+	if ( postFinder && !postFinder[ expando ] ) {
+		postFinder = setMatcher( postFinder, postSelector );
+	}
+	return markFunction(function( seed, results, context, xml ) {
+		// Positional selectors apply to seed elements, so it is invalid to follow them with relative ones
+		if ( seed && postFinder ) {
+			return;
+		}
+
+		var i, elem, postFilterIn,
+			preMap = [],
+			postMap = [],
+			preexisting = results.length,
+
+			// Get initial elements from seed or context
+			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [], seed ),
+
+			// Prefilter to get matcher input, preserving a map for seed-results synchronization
+			matcherIn = preFilter && ( seed || !selector ) ?
+				condense( elems, preMap, preFilter, context, xml ) :
+				elems,
+
+			matcherOut = matcher ?
+				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+					// ...intermediate processing is necessary
+					[] :
+
+					// ...otherwise use results directly
+					results :
+				matcherIn;
+
+		// Find primary matches
+		if ( matcher ) {
+			matcher( matcherIn, matcherOut, context, xml );
+		}
+
+		// Apply postFilter
+		if ( postFilter ) {
+			postFilterIn = condense( matcherOut, postMap );
+			postFilter( postFilterIn, [], context, xml );
+
+			// Un-match failing elements by moving them back to matcherIn
+			i = postFilterIn.length;
+			while ( i-- ) {
+				if ( (elem = postFilterIn[i]) ) {
+					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+				}
+			}
+		}
+
+		// Keep seed and results synchronized
+		if ( seed ) {
+			// Ignore postFinder because it can't coexist with seed
+			i = preFilter && matcherOut.length;
+			while ( i-- ) {
+				if ( (elem = matcherOut[i]) ) {
+					seed[ preMap[i] ] = !(results[ preMap[i] ] = elem);
+				}
+			}
+		} else {
+			matcherOut = condense(
+				matcherOut === results ?
+					matcherOut.splice( preexisting, matcherOut.length ) :
+					matcherOut
+			);
+			if ( postFinder ) {
+				postFinder( null, results, matcherOut, xml );
+			} else {
+				push.apply( results, matcherOut );
+			}
+		}
+	});
+}
+
+function matcherFromTokens( tokens ) {
+	var checkContext, matcher, j,
+		len = tokens.length,
+		leadingRelative = Expr.relative[ tokens[0].type ],
+		implicitRelative = leadingRelative || Expr.relative[" "],
+		i = leadingRelative ? 1 : 0,
+
+		// The foundational matcher ensures that elements are reachable from top-level context(s)
+		matchContext = addCombinator( function( elem ) {
+			return elem === checkContext;
+		}, implicitRelative, true ),
+		matchAnyContext = addCombinator( function( elem ) {
+			return indexOf.call( checkContext, elem ) > -1;
+		}, implicitRelative, true ),
+		matchers = [ function( elem, context, xml ) {
+			return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+				(checkContext = context).nodeType ?
+					matchContext( elem, context, xml ) :
+					matchAnyContext( elem, context, xml ) );
+		} ];
+
+	for ( ; i < len; i++ ) {
+		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+			matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
+		} else {
+			// The concatenated values are (context, xml) for backCompat
+			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+			// Return special upon seeing a positional matcher
+			if ( matcher[ expando ] ) {
+				// Find the next relative operator (if any) for proper handling
+				j = ++i;
+				for ( ; j < len; j++ ) {
+					if ( Expr.relative[ tokens[j].type ] ) {
+						break;
+					}
+				}
+				return setMatcher(
+					i > 1 && elementMatcher( matchers ),
+					i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ),
+					matcher,
+					i < j && matcherFromTokens( tokens.slice( i, j ) ),
+					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+					j < len && tokens.join("")
+				);
+			}
+			matchers.push( matcher );
+		}
+	}
+
+	return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+	var bySet = setMatchers.length > 0,
+		byElement = elementMatchers.length > 0,
+		superMatcher = function( seed, context, xml, results, expandContext ) {
+			var elem, j, matcher,
+				setMatched = [],
+				matchedCount = 0,
+				i = "0",
+				unmatched = seed && [],
+				outermost = expandContext != null,
+				contextBackup = outermostContext,
+				// We must always have either seed elements or context
+				elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+				// Nested matchers should use non-integer dirruns
+				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E);
+
+			if ( outermost ) {
+				outermostContext = context !== document && context;
+				cachedruns = superMatcher.el;
+			}
+
+			// Add elements passing elementMatchers directly to results
+			for ( ; (elem = elems[i]) != null; i++ ) {
+				if ( byElement && elem ) {
+					for ( j = 0; (matcher = elementMatchers[j]); j++ ) {
+						if ( matcher( elem, context, xml ) ) {
+							results.push( elem );
+							break;
+						}
+					}
+					if ( outermost ) {
+						dirruns = dirrunsUnique;
+						cachedruns = ++superMatcher.el;
+					}
+				}
+
+				// Track unmatched elements for set filters
+				if ( bySet ) {
+					// They will have gone through all possible matchers
+					if ( (elem = !matcher && elem) ) {
+						matchedCount--;
+					}
+
+					// Lengthen the array for every element, matched or not
+					if ( seed ) {
+						unmatched.push( elem );
+					}
+				}
+			}
+
+			// Apply set filters to unmatched elements
+			matchedCount += i;
+			if ( bySet && i !== matchedCount ) {
+				for ( j = 0; (matcher = setMatchers[j]); j++ ) {
+					matcher( unmatched, setMatched, context, xml );
+				}
+
+				if ( seed ) {
+					// Reintegrate element matches to eliminate the need for sorting
+					if ( matchedCount > 0 ) {
+						while ( i-- ) {
+							if ( !(unmatched[i] || setMatched[i]) ) {
+								setMatched[i] = pop.call( results );
+							}
+						}
+					}
+
+					// Discard index placeholder values to get only actual matches
+					setMatched = condense( setMatched );
+				}
+
+				// Add matches to results
+				push.apply( results, setMatched );
+
+				// Seedless set matches succeeding multiple successful matchers stipulate sorting
+				if ( outermost && !seed && setMatched.length > 0 &&
+					( matchedCount + setMatchers.length ) > 1 ) {
+
+					Sizzle.uniqueSort( results );
+				}
+			}
+
+			// Override manipulation of globals by nested matchers
+			if ( outermost ) {
+				dirruns = dirrunsUnique;
+				outermostContext = contextBackup;
+			}
+
+			return unmatched;
+		};
+
+	superMatcher.el = 0;
+	return bySet ?
+		markFunction( superMatcher ) :
+		superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+	var i,
+		setMatchers = [],
+		elementMatchers = [],
+		cached = compilerCache[ expando ][ selector ];
+
+	if ( !cached ) {
+		// Generate a function of recursive functions that can be used to check each element
+		if ( !group ) {
+			group = tokenize( selector );
+		}
+		i = group.length;
+		while ( i-- ) {
+			cached = matcherFromTokens( group[i] );
+			if ( cached[ expando ] ) {
+				setMatchers.push( cached );
+			} else {
+				elementMatchers.push( cached );
+			}
+		}
+
+		// Cache the compiled function
+		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+	}
+	return cached;
+};
+
+function multipleContexts( selector, contexts, results, seed ) {
+	var i = 0,
+		len = contexts.length;
+	for ( ; i < len; i++ ) {
+		Sizzle( selector, contexts[i], results, seed );
+	}
+	return results;
+}
+
+function select( selector, context, results, seed, xml ) {
+	var i, tokens, token, type, find,
+		match = tokenize( selector ),
+		j = match.length;
+
+	if ( !seed ) {
+		// Try to minimize operations if there is only one group
+		if ( match.length === 1 ) {
+
+			// Take a shortcut and set the context if the root selector is an ID
+			tokens = match[0] = match[0].slice( 0 );
+			if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+					context.nodeType === 9 && !xml &&
+					Expr.relative[ tokens[1].type ] ) {
+
+				context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0];
+				if ( !context ) {
+					return results;
+				}
+
+				selector = selector.slice( tokens.shift().length );
+			}
+
+			// Fetch a seed set for right-to-left matching
+			for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) {
+				token = tokens[i];
+
+				// Abort if we hit a combinator
+				if ( Expr.relative[ (type = token.type) ] ) {
+					break;
+				}
+				if ( (find = Expr.find[ type ]) ) {
+					// Search, expanding context for leading sibling combinators
+					if ( (seed = find(
+						token.matches[0].replace( rbackslash, "" ),
+						rsibling.test( tokens[0].type ) && context.parentNode || context,
+						xml
+					)) ) {
+
+						// If seed is empty or no tokens remain, we can return early
+						tokens.splice( i, 1 );
+						selector = seed.length && tokens.join("");
+						if ( !selector ) {
+							push.apply( results, slice.call( seed, 0 ) );
+							return results;
+						}
+
+						break;
+					}
+				}
+			}
+		}
+	}
+
+	// Compile and execute a filtering function
+	// Provide `match` to avoid retokenization if we modified the selector above
+	compile( selector, match )(
+		seed,
+		context,
+		xml,
+		results,
+		rsibling.test( selector )
+	);
+	return results;
+}
+
+if ( document.querySelectorAll ) {
+	(function() {
+		var disconnectedMatch,
+			oldSelect = select,
+			rescape = /'|\\/g,
+			rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,
+
+			// qSa(:focus) reports false when true (Chrome 21),
+			// A support test would require too much code (would include document ready)
+			rbuggyQSA = [":focus"],
+
+			// matchesSelector(:focus) reports false when true (Chrome 21),
+			// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+			// A support test would require too much code (would include document ready)
+			// just skip matchesSelector for :active
+			rbuggyMatches = [ ":active", ":focus" ],
+			matches = docElem.matchesSelector ||
+				docElem.mozMatchesSelector ||
+				docElem.webkitMatchesSelector ||
+				docElem.oMatchesSelector ||
+				docElem.msMatchesSelector;
+
+		// Build QSA regex
+		// Regex strategy adopted from Diego Perini
+		assert(function( div ) {
+			// Select is set to empty string on purpose
+			// This is to test IE's treatment of not explictly
+			// setting a boolean content attribute,
+			// since its presence should be enough
+			// http://bugs.jquery.com/ticket/12359
+			div.innerHTML = "<select><option selected=''></option></select>";
+
+			// IE8 - Some boolean attributes are not treated correctly
+			if ( !div.querySelectorAll("[selected]").length ) {
+				rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" );
+			}
+
+			// Webkit/Opera - :checked should return selected option elements
+			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+			// IE8 throws error here (do not put tests after this one)
+			if ( !div.querySelectorAll(":checked").length ) {
+				rbuggyQSA.push(":checked");
+			}
+		});
+
+		assert(function( div ) {
+
+			// Opera 10-12/IE9 - ^= $= *= and empty values
+			// Should not select anything
+			div.innerHTML = "<p test=''></p>";
+			if ( div.querySelectorAll("[test^='']").length ) {
+				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" );
+			}
+
+			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+			// IE8 throws error here (do not put tests after this one)
+			div.innerHTML = "<input type='hidden'/>";
+			if ( !div.querySelectorAll(":enabled").length ) {
+				rbuggyQSA.push(":enabled", ":disabled");
+			}
+		});
+
+		// rbuggyQSA always contains :focus, so no need for a length check
+		rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") );
+
+		select = function( selector, context, results, seed, xml ) {
+			// Only use querySelectorAll when not filtering,
+			// when this is not xml,
+			// and when no QSA bugs apply
+			if ( !seed && !xml && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
+				var groups, i,
+					old = true,
+					nid = expando,
+					newContext = context,
+					newSelector = context.nodeType === 9 && selector;
+
+				// qSA works strangely on Element-rooted queries
+				// We can work around this by specifying an extra ID on the root
+				// and working up from there (Thanks to Andrew Dupont for the technique)
+				// IE 8 doesn't work on object elements
+				if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+					groups = tokenize( selector );
+
+					if ( (old = context.getAttribute("id")) ) {
+						nid = old.replace( rescape, "\\$&" );
+					} else {
+						context.setAttribute( "id", nid );
+					}
+					nid = "[id='" + nid + "'] ";
+
+					i = groups.length;
+					while ( i-- ) {
+						groups[i] = nid + groups[i].join("");
+					}
+					newContext = rsibling.test( selector ) && context.parentNode || context;
+					newSelector = groups.join(",");
+				}
+
+				if ( newSelector ) {
+					try {
+						push.apply( results, slice.call( newContext.querySelectorAll(
+							newSelector
+						), 0 ) );
+						return results;
+					} catch(qsaError) {
+					} finally {
+						if ( !old ) {
+							context.removeAttribute("id");
+						}
+					}
+				}
+			}
+
+			return oldSelect( selector, context, results, seed, xml );
+		};
+
+		if ( matches ) {
+			assert(function( div ) {
+				// Check to see if it's possible to do matchesSelector
+				// on a disconnected node (IE 9)
+				disconnectedMatch = matches.call( div, "div" );
+
+				// This should fail with an exception
+				// Gecko does not error, returns false instead
+				try {
+					matches.call( div, "[test!='']:sizzle" );
+					rbuggyMatches.push( "!=", pseudos );
+				} catch ( e ) {}
+			});
+
+			// rbuggyMatches always contains :active and :focus, so no need for a length check
+			rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") );
+
+			Sizzle.matchesSelector = function( elem, expr ) {
+				// Make sure that attribute selectors are quoted
+				expr = expr.replace( rattributeQuotes, "='$1']" );
+
+				// rbuggyMatches always contains :active, so no need for an existence check
+				if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && (!rbuggyQSA || !rbuggyQSA.test( expr )) ) {
+					try {
+						var ret = matches.call( elem, expr );
+
+						// IE 9's matchesSelector returns false on disconnected nodes
+						if ( ret || disconnectedMatch ||
+								// As well, disconnected nodes are said to be in a document
+								// fragment in IE 9
+								elem.document && elem.document.nodeType !== 11 ) {
+							return ret;
+						}
+					} catch(e) {}
+				}
+
+				return Sizzle( expr, null, null, [ elem ] ).length > 0;
+			};
+		}
+	})();
+}
+
+// Deprecated
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Back-compat
+function setFilters() {}
+Expr.filters = setFilters.prototype = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+// Override sizzle attribute retrieval
+Sizzle.attr = jQuery.attr;
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.pseudos;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})( window );
+var runtil = /Until$/,
+	rparentsprev = /^(?:parents|prev(?:Until|All))/,
+	isSimple = /^.[^:#\[\.,]*$/,
+	rneedsContext = jQuery.expr.match.needsContext,
+	// methods guaranteed to produce a unique set when starting from a unique set
+	guaranteedUnique = {
+		children: true,
+		contents: true,
+		next: true,
+		prev: true
+	};
+
+jQuery.fn.extend({
+	find: function( selector ) {
+		var i, l, length, n, r, ret,
+			self = this;
+
+		if ( typeof selector !== "string" ) {
+			return jQuery( selector ).filter(function() {
+				for ( i = 0, l = self.length; i < l; i++ ) {
+					if ( jQuery.contains( self[ i ], this ) ) {
+						return true;
+					}
+				}
+			});
+		}
+
+		ret = this.pushStack( "", "find", selector );
+
+		for ( i = 0, l = this.length; i < l; i++ ) {
+			length = ret.length;
+			jQuery.find( selector, this[i], ret );
+
+			if ( i > 0 ) {
+				// Make sure that the results are unique
+				for ( n = length; n < ret.length; n++ ) {
+					for ( r = 0; r < length; r++ ) {
+						if ( ret[r] === ret[n] ) {
+							ret.splice(n--, 1);
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	has: function( target ) {
+		var i,
+			targets = jQuery( target, this ),
+			len = targets.length;
+
+		return this.filter(function() {
+			for ( i = 0; i < len; i++ ) {
+				if ( jQuery.contains( this, targets[i] ) ) {
+					return true;
+				}
+			}
+		});
+	},
+
+	not: function( selector ) {
+		return this.pushStack( winnow(this, selector, false), "not", selector);
+	},
+
+	filter: function( selector ) {
+		return this.pushStack( winnow(this, selector, true), "filter", selector );
+	},
+
+	is: function( selector ) {
+		return !!selector && (
+			typeof selector === "string" ?
+				// If this is a positional/relative selector, check membership in the returned set
+				// so $("p:first").is("p:last") won't return true for a doc with two "p".
+				rneedsContext.test( selector ) ?
+					jQuery( selector, this.context ).index( this[0] ) >= 0 :
+					jQuery.filter( selector, this ).length > 0 :
+				this.filter( selector ).length > 0 );
+	},
+
+	closest: function( selectors, context ) {
+		var cur,
+			i = 0,
+			l = this.length,
+			ret = [],
+			pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
+				jQuery( selectors, context || this.context ) :
+				0;
+
+		for ( ; i < l; i++ ) {
+			cur = this[i];
+
+			while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) {
+				if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+					ret.push( cur );
+					break;
+				}
+				cur = cur.parentNode;
+			}
+		}
+
+		ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
+
+		return this.pushStack( ret, "closest", selectors );
+	},
+
+	// Determine the position of an element within
+	// the matched set of elements
+	index: function( elem ) {
+
+		// No argument, return index in parent
+		if ( !elem ) {
+			return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;
+		}
+
+		// index in selector
+		if ( typeof elem === "string" ) {
+			return jQuery.inArray( this[0], jQuery( elem ) );
+		}
+
+		// Locate the position of the desired element
+		return jQuery.inArray(
+			// If it receives a jQuery object, the first element is used
+			elem.jquery ? elem[0] : elem, this );
+	},
+
+	add: function( selector, context ) {
+		var set = typeof selector === "string" ?
+				jQuery( selector, context ) :
+				jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+			all = jQuery.merge( this.get(), set );
+
+		return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+			all :
+			jQuery.unique( all ) );
+	},
+
+	addBack: function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter(selector)
+		);
+	}
+});
+
+jQuery.fn.andSelf = jQuery.fn.addBack;
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+	return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+function sibling( cur, dir ) {
+	do {
+		cur = cur[ dir ];
+	} while ( cur && cur.nodeType !== 1 );
+
+	return cur;
+}
+
+jQuery.each({
+	parent: function( elem ) {
+		var parent = elem.parentNode;
+		return parent && parent.nodeType !== 11 ? parent : null;
+	},
+	parents: function( elem ) {
+		return jQuery.dir( elem, "parentNode" );
+	},
+	parentsUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "parentNode", until );
+	},
+	next: function( elem ) {
+		return sibling( elem, "nextSibling" );
+	},
+	prev: function( elem ) {
+		return sibling( elem, "previousSibling" );
+	},
+	nextAll: function( elem ) {
+		return jQuery.dir( elem, "nextSibling" );
+	},
+	prevAll: function( elem ) {
+		return jQuery.dir( elem, "previousSibling" );
+	},
+	nextUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "nextSibling", until );
+	},
+	prevUntil: function( elem, i, until ) {
+		return jQuery.dir( elem, "previousSibling", until );
+	},
+	siblings: function( elem ) {
+		return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
+	},
+	children: function( elem ) {
+		return jQuery.sibling( elem.firstChild );
+	},
+	contents: function( elem ) {
+		return jQuery.nodeName( elem, "iframe" ) ?
+			elem.contentDocument || elem.contentWindow.document :
+			jQuery.merge( [], elem.childNodes );
+	}
+}, function( name, fn ) {
+	jQuery.fn[ name ] = function( until, selector ) {
+		var ret = jQuery.map( this, fn, until );
+
+		if ( !runtil.test( name ) ) {
+			selector = until;
+		}
+
+		if ( selector && typeof selector === "string" ) {
+			ret = jQuery.filter( selector, ret );
+		}
+
+		ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+		if ( this.length > 1 && rparentsprev.test( name ) ) {
+			ret = ret.reverse();
+		}
+
+		return this.pushStack( ret, name, core_slice.call( arguments ).join(",") );
+	};
+});
+
+jQuery.extend({
+	filter: function( expr, elems, not ) {
+		if ( not ) {
+			expr = ":not(" + expr + ")";
+		}
+
+		return elems.length === 1 ?
+			jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+			jQuery.find.matches(expr, elems);
+	},
+
+	dir: function( elem, dir, until ) {
+		var matched = [],
+			cur = elem[ dir ];
+
+		while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+			if ( cur.nodeType === 1 ) {
+				matched.push( cur );
+			}
+			cur = cur[dir];
+		}
+		return matched;
+	},
+
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType === 1 && n !== elem ) {
+				r.push( n );
+			}
+		}
+
+		return r;
+	}
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+	// Can't pass null or undefined to indexOf in Firefox 4
+	// Set to 0 to skip string check
+	qualifier = qualifier || 0;
+
+	if ( jQuery.isFunction( qualifier ) ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			var retVal = !!qualifier.call( elem, i, elem );
+			return retVal === keep;
+		});
+
+	} else if ( qualifier.nodeType ) {
+		return jQuery.grep(elements, function( elem, i ) {
+			return ( elem === qualifier ) === keep;
+		});
+
+	} else if ( typeof qualifier === "string" ) {
+		var filtered = jQuery.grep(elements, function( elem ) {
+			return elem.nodeType === 1;
+		});
+
+		if ( isSimple.test( qualifier ) ) {
+			return jQuery.filter(qualifier, filtered, !keep);
+		} else {
+			qualifier = jQuery.filter( qualifier, filtered );
+		}
+	}
+
+	return jQuery.grep(elements, function( elem, i ) {
+		return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
+	});
+}
+function createSafeFragment( document ) {
+	var list = nodeNames.split( "|" ),
+	safeFrag = document.createDocumentFragment();
+
+	if ( safeFrag.createElement ) {
+		while ( list.length ) {
+			safeFrag.createElement(
+				list.pop()
+			);
+		}
+	}
+	return safeFrag;
+}
+
+var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
+		"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
+	rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g,
+	rleadingWhitespace = /^\s+/,
+	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+	rtagName = /<([\w:]+)/,
+	rtbody = /<tbody/i,
+	rhtml = /<|&#?\w+;/,
+	rnoInnerhtml = /<(?:script|style|link)/i,
+	rnocache = /<(?:script|object|embed|option|style)/i,
+	rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"),
+	rcheckableType = /^(?:checkbox|radio)$/,
+	// checked="checked" or checked
+	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+	rscriptType = /\/(java|ecma)script/i,
+	rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,
+	wrapMap = {
+		option: [ 1, "<select multiple='multiple'>", "</select>" ],
+		legend: [ 1, "<fieldset>", "</fieldset>" ],
+		thead: [ 1, "<table>", "</table>" ],
+		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+		col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+		area: [ 1, "<map>", "</map>" ],
+		_default: [ 0, "", "" ]
+	},
+	safeFragment = createSafeFragment( document ),
+	fragmentDiv = safeFragment.appendChild( document.createElement("div") );
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
+// unless wrapped in a div with non-breaking characters in front of it.
+if ( !jQuery.support.htmlSerialize ) {
+	wrapMap._default = [ 1, "X<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+	text: function( value ) {
+		return jQuery.access( this, function( value ) {
+			return value === undefined ?
+				jQuery.text( this ) :
+				this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
+		}, null, value, arguments.length );
+	},
+
+	wrapAll: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapAll( html.call(this, i) );
+			});
+		}
+
+		if ( this[0] ) {
+			// The elements to wrap the target around
+			var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+			if ( this[0].parentNode ) {
+				wrap.insertBefore( this[0] );
+			}
+
+			wrap.map(function() {
+				var elem = this;
+
+				while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+					elem = elem.firstChild;
+				}
+
+				return elem;
+			}).append( this );
+		}
+
+		return this;
+	},
+
+	wrapInner: function( html ) {
+		if ( jQuery.isFunction( html ) ) {
+			return this.each(function(i) {
+				jQuery(this).wrapInner( html.call(this, i) );
+			});
+		}
+
+		return this.each(function() {
+			var self = jQuery( this ),
+				contents = self.contents();
+
+			if ( contents.length ) {
+				contents.wrapAll( html );
+
+			} else {
+				self.append( html );
+			}
+		});
+	},
+
+	wrap: function( html ) {
+		var isFunction = jQuery.isFunction( html );
+
+		return this.each(function(i) {
+			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
+		});
+	},
+
+	unwrap: function() {
+		return this.parent().each(function() {
+			if ( !jQuery.nodeName( this, "body" ) ) {
+				jQuery( this ).replaceWith( this.childNodes );
+			}
+		}).end();
+	},
+
+	append: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 ) {
+				this.appendChild( elem );
+			}
+		});
+	},
+
+	prepend: function() {
+		return this.domManip(arguments, true, function( elem ) {
+			if ( this.nodeType === 1 || this.nodeType === 11 ) {
+				this.insertBefore( elem, this.firstChild );
+			}
+		});
+	},
+
+	before: function() {
+		if ( !isDisconnected( this[0] ) ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this );
+			});
+		}
+
+		if ( arguments.length ) {
+			var set = jQuery.clean( arguments );
+			return this.pushStack( jQuery.merge( set, this ), "before", this.selector );
+		}
+	},
+
+	after: function() {
+		if ( !isDisconnected( this[0] ) ) {
+			return this.domManip(arguments, false, function( elem ) {
+				this.parentNode.insertBefore( elem, this.nextSibling );
+			});
+		}
+
+		if ( arguments.length ) {
+			var set = jQuery.clean( arguments );
+			return this.pushStack( jQuery.merge( this, set ), "after", this.selector );
+		}
+	},
+
+	// keepData is for internal use only--do not document
+	remove: function( selector, keepData ) {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+				if ( !keepData && elem.nodeType === 1 ) {
+					jQuery.cleanData( elem.getElementsByTagName("*") );
+					jQuery.cleanData( [ elem ] );
+				}
+
+				if ( elem.parentNode ) {
+					elem.parentNode.removeChild( elem );
+				}
+			}
+		}
+
+		return this;
+	},
+
+	empty: function() {
+		var elem,
+			i = 0;
+
+		for ( ; (elem = this[i]) != null; i++ ) {
+			// Remove element nodes and prevent memory leaks
+			if ( elem.nodeType === 1 ) {
+				jQuery.cleanData( elem.getElementsByTagName("*") );
+			}
+
+			// Remove any remaining nodes
+			while ( elem.firstChild ) {
+				elem.removeChild( elem.firstChild );
+			}
+		}
+
+		return this;
+	},
+
+	clone: function( dataAndEvents, deepDataAndEvents ) {
+		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+		return this.map( function () {
+			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+		});
+	},
+
+	html: function( value ) {
+		return jQuery.access( this, function( value ) {
+			var elem = this[0] || {},
+				i = 0,
+				l = this.length;
+
+			if ( value === undefined ) {
+				return elem.nodeType === 1 ?
+					elem.innerHTML.replace( rinlinejQuery, "" ) :
+					undefined;
+			}
+
+			// See if we can take a shortcut and just use innerHTML
+			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
+				( jQuery.support.htmlSerialize || !rnoshimcache.test( value )  ) &&
+				( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
+				!wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
+
+				value = value.replace( rxhtmlTag, "<$1></$2>" );
+
+				try {
+					for (; i < l; i++ ) {
+						// Remove element nodes and prevent memory leaks
+						elem = this[i] || {};
+						if ( elem.nodeType === 1 ) {
+							jQuery.cleanData( elem.getElementsByTagName( "*" ) );
+							elem.innerHTML = value;
+						}
+					}
+
+					elem = 0;
+
+				// If using innerHTML throws an exception, use the fallback method
+				} catch(e) {}
+			}
+
+			if ( elem ) {
+				this.empty().append( value );
+			}
+		}, null, value, arguments.length );
+	},
+
+	replaceWith: function( value ) {
+		if ( !isDisconnected( this[0] ) ) {
+			// Make sure that the elements are removed from the DOM before they are inserted
+			// this can help fix replacing a parent with child elements
+			if ( jQuery.isFunction( value ) ) {
+				return this.each(function(i) {
+					var self = jQuery(this), old = self.html();
+					self.replaceWith( value.call( this, i, old ) );
+				});
+			}
+
+			if ( typeof value !== "string" ) {
+				value = jQuery( value ).detach();
+			}
+
+			return this.each(function() {
+				var next = this.nextSibling,
+					parent = this.parentNode;
+
+				jQuery( this ).remove();
+
+				if ( next ) {
+					jQuery(next).before( value );
+				} else {
+					jQuery(parent).append( value );
+				}
+			});
+		}
+
+		return this.length ?
+			this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+			this;
+	},
+
+	detach: function( selector ) {
+		return this.remove( selector, true );
+	},
+
+	domManip: function( args, table, callback ) {
+
+		// Flatten any nested arrays
+		args = [].concat.apply( [], args );
+
+		var results, first, fragment, iNoClone,
+			i = 0,
+			value = args[0],
+			scripts = [],
+			l = this.length;
+
+		// We can't cloneNode fragments that contain checked, in WebKit
+		if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) {
+			return this.each(function() {
+				jQuery(this).domManip( args, table, callback );
+			});
+		}
+
+		if ( jQuery.isFunction(value) ) {
+			return this.each(function(i) {
+				var self = jQuery(this);
+				args[0] = value.call( this, i, table ? self.html() : undefined );
+				self.domManip( args, table, callback );
+			});
+		}
+
+		if ( this[0] ) {
+			results = jQuery.buildFragment( args, this, scripts );
+			fragment = results.fragment;
+			first = fragment.firstChild;
+
+			if ( fragment.childNodes.length === 1 ) {
+				fragment = first;
+			}
+
+			if ( first ) {
+				table = table && jQuery.nodeName( first, "tr" );
+
+				// Use the original fragment for the last item instead of the first because it can end up
+				// being emptied incorrectly in certain situations (#8070).
+				// Fragments from the fragment cache must always be cloned and never used in place.
+				for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) {
+					callback.call(
+						table && jQuery.nodeName( this[i], "table" ) ?
+							findOrAppend( this[i], "tbody" ) :
+							this[i],
+						i === iNoClone ?
+							fragment :
+							jQuery.clone( fragment, true, true )
+					);
+				}
+			}
+
+			// Fix #11809: Avoid leaking memory
+			fragment = first = null;
+
+			if ( scripts.length ) {
+				jQuery.each( scripts, function( i, elem ) {
+					if ( elem.src ) {
+						if ( jQuery.ajax ) {
+							jQuery.ajax({
+								url: elem.src,
+								type: "GET",
+								dataType: "script",
+								async: false,
+								global: false,
+								"throws": true
+							});
+						} else {
+							jQuery.error("no ajax");
+						}
+					} else {
+						jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) );
+					}
+
+					if ( elem.parentNode ) {
+						elem.parentNode.removeChild( elem );
+					}
+				});
+			}
+		}
+
+		return this;
+	}
+});
+
+function findOrAppend( elem, tag ) {
+	return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) );
+}
+
+function cloneCopyEvent( src, dest ) {
+
+	if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+		return;
+	}
+
+	var type, i, l,
+		oldData = jQuery._data( src ),
+		curData = jQuery._data( dest, oldData ),
+		events = oldData.events;
+
+	if ( events ) {
+		delete curData.handle;
+		curData.events = {};
+
+		for ( type in events ) {
+			for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+				jQuery.event.add( dest, type, events[ type ][ i ] );
+			}
+		}
+	}
+
+	// make the cloned public data object a copy from the original
+	if ( curData.data ) {
+		curData.data = jQuery.extend( {}, curData.data );
+	}
+}
+
+function cloneFixAttributes( src, dest ) {
+	var nodeName;
+
+	// We do not need to do anything for non-Elements
+	if ( dest.nodeType !== 1 ) {
+		return;
+	}
+
+	// clearAttributes removes the attributes, which we don't want,
+	// but also removes the attachEvent events, which we *do* want
+	if ( dest.clearAttributes ) {
+		dest.clearAttributes();
+	}
+
+	// mergeAttributes, in contrast, only merges back on the
+	// original attributes, not the events
+	if ( dest.mergeAttributes ) {
+		dest.mergeAttributes( src );
+	}
+
+	nodeName = dest.nodeName.toLowerCase();
+
+	if ( nodeName === "object" ) {
+		// IE6-10 improperly clones children of object elements using classid.
+		// IE10 throws NoModificationAllowedError if parent is null, #12132.
+		if ( dest.parentNode ) {
+			dest.outerHTML = src.outerHTML;
+		}
+
+		// This path appears unavoidable for IE9. When cloning an object
+		// element in IE9, the outerHTML strategy above is not sufficient.
+		// If the src has innerHTML and the destination does not,
+		// copy the src.innerHTML into the dest.innerHTML. #10324
+		if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) {
+			dest.innerHTML = src.innerHTML;
+		}
+
+	} else if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
+		// IE6-8 fails to persist the checked state of a cloned checkbox
+		// or radio button. Worse, IE6-7 fail to give the cloned element
+		// a checked appearance if the defaultChecked value isn't also set
+
+		dest.defaultChecked = dest.checked = src.checked;
+
+		// IE6-7 get confused and end up setting the value of a cloned
+		// checkbox/radio button to an empty string instead of "on"
+		if ( dest.value !== src.value ) {
+			dest.value = src.value;
+		}
+
+	// IE6-8 fails to return the selected option to the default selected
+	// state when cloning options
+	} else if ( nodeName === "option" ) {
+		dest.selected = src.defaultSelected;
+
+	// IE6-8 fails to set the defaultValue to the correct value when
+	// cloning other types of input fields
+	} else if ( nodeName === "input" || nodeName === "textarea" ) {
+		dest.defaultValue = src.defaultValue;
+
+	// IE blanks contents when cloning scripts
+	} else if ( nodeName === "script" && dest.text !== src.text ) {
+		dest.text = src.text;
+	}
+
+	// Event data gets referenced instead of copied if the expando
+	// gets copied too
+	dest.removeAttribute( jQuery.expando );
+}
+
+jQuery.buildFragment = function( args, context, scripts ) {
+	var fragment, cacheable, cachehit,
+		first = args[ 0 ];
+
+	// Set context from what may come in as undefined or a jQuery collection or a node
+	// Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 &
+	// also doubles as fix for #8950 where plain objects caused createDocumentFragment exception
+	context = context || document;
+	context = !context.nodeType && context[0] || context;
+	context = context.ownerDocument || context;
+
+	// Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+	// Cloning options loses the selected state, so don't cache them
+	// IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+	// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+	// Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501
+	if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document &&
+		first.charAt(0) === "<" && !rnocache.test( first ) &&
+		(jQuery.support.checkClone || !rchecked.test( first )) &&
+		(jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {
+
+		// Mark cacheable and look for a hit
+		cacheable = true;
+		fragment = jQuery.fragments[ first ];
+		cachehit = fragment !== undefined;
+	}
+
+	if ( !fragment ) {
+		fragment = context.createDocumentFragment();
+		jQuery.clean( args, context, fragment, scripts );
+
+		// Update the cache, but only store false
+		// unless this is a second parsing of the same content
+		if ( cacheable ) {
+			jQuery.fragments[ first ] = cachehit && fragment;
+		}
+	}
+
+	return { fragment: fragment, cacheable: cacheable };
+};
+
+jQuery.fragments = {};
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after",
+	replaceAll: "replaceWith"
+}, function( name, original ) {
+	jQuery.fn[ name ] = function( selector ) {
+		var elems,
+			i = 0,
+			ret = [],
+			insert = jQuery( selector ),
+			l = insert.length,
+			parent = this.length === 1 && this[0].parentNode;
+
+		if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) {
+			insert[ original ]( this[0] );
+			return this;
+		} else {
+			for ( ; i < l; i++ ) {
+				elems = ( i > 0 ? this.clone(true) : this ).get();
+				jQuery( insert[i] )[ original ]( elems );
+				ret = ret.concat( elems );
+			}
+
+			return this.pushStack( ret, name, insert.selector );
+		}
+	};
+});
+
+function getAll( elem ) {
+	if ( typeof elem.getElementsByTagName !== "undefined" ) {
+		return elem.getElementsByTagName( "*" );
+
+	} else if ( typeof elem.querySelectorAll !== "undefined" ) {
+		return elem.querySelectorAll( "*" );
+
+	} else {
+		return [];
+	}
+}
+
+// Used in clean, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+	if ( rcheckableType.test( elem.type ) ) {
+		elem.defaultChecked = elem.checked;
+	}
+}
+
+jQuery.extend({
+	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+		var srcElements,
+			destElements,
+			i,
+			clone;
+
+		if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) {
+			clone = elem.cloneNode( true );
+
+		// IE<=8 does not properly clone detached, unknown element nodes
+		} else {
+			fragmentDiv.innerHTML = elem.outerHTML;
+			fragmentDiv.removeChild( clone = fragmentDiv.firstChild );
+		}
+
+		if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+				(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+			// IE copies events bound via attachEvent when using cloneNode.
+			// Calling detachEvent on the clone will also remove the events
+			// from the original. In order to get around this, we use some
+			// proprietary methods to clear the events. Thanks to MooTools
+			// guys for this hotness.
+
+			cloneFixAttributes( elem, clone );
+
+			// Using Sizzle here is crazy slow, so we use getElementsByTagName instead
+			srcElements = getAll( elem );
+			destElements = getAll( clone );
+
+			// Weird iteration because IE will replace the length property
+			// with an element if you are cloning the body and one of the
+			// elements on the page has a name or id of "length"
+			for ( i = 0; srcElements[i]; ++i ) {
+				// Ensure that the destination node is not null; Fixes #9587
+				if ( destElements[i] ) {
+					cloneFixAttributes( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		// Copy the events from the original to the clone
+		if ( dataAndEvents ) {
+			cloneCopyEvent( elem, clone );
+
+			if ( deepDataAndEvents ) {
+				srcElements = getAll( elem );
+				destElements = getAll( clone );
+
+				for ( i = 0; srcElements[i]; ++i ) {
+					cloneCopyEvent( srcElements[i], destElements[i] );
+				}
+			}
+		}
+
+		srcElements = destElements = null;
+
+		// Return the cloned set
+		return clone;
+	},
+
+	clean: function( elems, context, fragment, scripts ) {
+		var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags,
+			safe = context === document && safeFragment,
+			ret = [];
+
+		// Ensure that context is a document
+		if ( !context || typeof context.createDocumentFragment === "undefined" ) {
+			context = document;
+		}
+
+		// Use the already-created safe fragment if context permits
+		for ( i = 0; (elem = elems[i]) != null; i++ ) {
+			if ( typeof elem === "number" ) {
+				elem += "";
+			}
+
+			if ( !elem ) {
+				continue;
+			}
+
+			// Convert html string into DOM nodes
+			if ( typeof elem === "string" ) {
+				if ( !rhtml.test( elem ) ) {
+					elem = context.createTextNode( elem );
+				} else {
+					// Ensure a safe container in which to render the html
+					safe = safe || createSafeFragment( context );
+					div = context.createElement("div");
+					safe.appendChild( div );
+
+					// Fix "XHTML"-style tags in all browsers
+					elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+					// Go to html and back, then peel off extra wrappers
+					tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase();
+					wrap = wrapMap[ tag ] || wrapMap._default;
+					depth = wrap[0];
+					div.innerHTML = wrap[1] + elem + wrap[2];
+
+					// Move to the right depth
+					while ( depth-- ) {
+						div = div.lastChild;
+					}
+
+					// Remove IE's autoinserted <tbody> from table fragments
+					if ( !jQuery.support.tbody ) {
+
+						// String was a <table>, *may* have spurious <tbody>
+						hasBody = rtbody.test(elem);
+							tbody = tag === "table" && !hasBody ?
+								div.firstChild && div.firstChild.childNodes :
+
+								// String was a bare <thead> or <tfoot>
+								wrap[1] === "<table>" && !hasBody ?
+									div.childNodes :
+									[];
+
+						for ( j = tbody.length - 1; j >= 0 ; --j ) {
+							if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+								tbody[ j ].parentNode.removeChild( tbody[ j ] );
+							}
+						}
+					}
+
+					// IE completely kills leading whitespace when innerHTML is used
+					if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+						div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+					}
+
+					elem = div.childNodes;
+
+					// Take out of fragment container (we need a fresh div each time)
+					div.parentNode.removeChild( div );
+				}
+			}
+
+			if ( elem.nodeType ) {
+				ret.push( elem );
+			} else {
+				jQuery.merge( ret, elem );
+			}
+		}
+
+		// Fix #11356: Clear elements from safeFragment
+		if ( div ) {
+			elem = div = safe = null;
+		}
+
+		// Reset defaultChecked for any radios and checkboxes
+		// about to be appended to the DOM in IE 6/7 (#8060)
+		if ( !jQuery.support.appendChecked ) {
+			for ( i = 0; (elem = ret[i]) != null; i++ ) {
+				if ( jQuery.nodeName( elem, "input" ) ) {
+					fixDefaultChecked( elem );
+				} else if ( typeof elem.getElementsByTagName !== "undefined" ) {
+					jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
+				}
+			}
+		}
+
+		// Append elements to a provided document fragment
+		if ( fragment ) {
+			// Special handling of each script element
+			handleScript = function( elem ) {
+				// Check if we consider it executable
+				if ( !elem.type || rscriptType.test( elem.type ) ) {
+					// Detach the script and store it in the scripts array (if provided) or the fragment
+					// Return truthy to indicate that it has been handled
+					return scripts ?
+						scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) :
+						fragment.appendChild( elem );
+				}
+			};
+
+			for ( i = 0; (elem = ret[i]) != null; i++ ) {
+				// Check if we're done after handling an executable script
+				if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) {
+					// Append to fragment and handle embedded scripts
+					fragment.appendChild( elem );
+					if ( typeof elem.getElementsByTagName !== "undefined" ) {
+						// handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration
+						jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript );
+
+						// Splice the scripts into ret after their former ancestor and advance our index beyond them
+						ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
+						i += jsTags.length;
+					}
+				}
+			}
+		}
+
+		return ret;
+	},
+
+	cleanData: function( elems, /* internal */ acceptData ) {
+		var data, id, elem, type,
+			i = 0,
+			internalKey = jQuery.expando,
+			cache = jQuery.cache,
+			deleteExpando = jQuery.support.deleteExpando,
+			special = jQuery.event.special;
+
+		for ( ; (elem = elems[i]) != null; i++ ) {
+
+			if ( acceptData || jQuery.acceptData( elem ) ) {
+
+				id = elem[ internalKey ];
+				data = id && cache[ id ];
+
+				if ( data ) {
+					if ( data.events ) {
+						for ( type in data.events ) {
+							if ( special[ type ] ) {
+								jQuery.event.remove( elem, type );
+
+							// This is a shortcut to avoid jQuery.event.remove's overhead
+							} else {
+								jQuery.removeEvent( elem, type, data.handle );
+							}
+						}
+					}
+
+					// Remove cache only if it was not already removed by jQuery.event.remove
+					if ( cache[ id ] ) {
+
+						delete cache[ id ];
+
+						// IE does not allow us to delete expando properties from nodes,
+						// nor does it have a removeAttribute function on Document nodes;
+						// we must handle all of these cases
+						if ( deleteExpando ) {
+							delete elem[ internalKey ];
+
+						} else if ( elem.removeAttribute ) {
+							elem.removeAttribute( internalKey );
+
+						} else {
+							elem[ internalKey ] = null;
+						}
+
+						jQuery.deletedIds.push( id );
+					}
+				}
+			}
+		}
+	}
+});
+// Limit scope pollution from any deprecated API
+(function() {
+
+var matched, browser;
+
+// Use of jQuery.browser is frowned upon.
+// More details: http://api.jquery.com/jQuery.browser
+// jQuery.uaMatch maintained for back-compat
+jQuery.uaMatch = function( ua ) {
+	ua = ua.toLowerCase();
+
+	var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+		/(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+		/(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+		/(msie) ([\w.]+)/.exec( ua ) ||
+		ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
+		[];
+
+	return {
+		browser: match[ 1 ] || "",
+		version: match[ 2 ] || "0"
+	};
+};
+
+matched = jQuery.uaMatch( navigator.userAgent );
+browser = {};
+
+if ( matched.browser ) {
+	browser[ matched.browser ] = true;
+	browser.version = matched.version;
+}
+
+// Chrome is Webkit, but Webkit is also Safari.
+if ( browser.chrome ) {
+	browser.webkit = true;
+} else if ( browser.webkit ) {
+	browser.safari = true;
+}
+
+jQuery.browser = browser;
+
+jQuery.sub = function() {
+	function jQuerySub( selector, context ) {
+		return new jQuerySub.fn.init( selector, context );
+	}
+	jQuery.extend( true, jQuerySub, this );
+	jQuerySub.superclass = this;
+	jQuerySub.fn = jQuerySub.prototype = this();
+	jQuerySub.fn.constructor = jQuerySub;
+	jQuerySub.sub = this.sub;
+	jQuerySub.fn.init = function init( selector, context ) {
+		if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
+			context = jQuerySub( context );
+		}
+
+		return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
+	};
+	jQuerySub.fn.init.prototype = jQuerySub.fn;
+	var rootjQuerySub = jQuerySub(document);
+	return jQuerySub;
+};
+
+})();
+var curCSS, iframe, iframeDoc,
+	ralpha = /alpha\([^)]*\)/i,
+	ropacity = /opacity=([^)]*)/,
+	rposition = /^(top|right|bottom|left)$/,
+	// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
+	// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
+	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
+	rmargin = /^margin/,
+	rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ),
+	rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ),
+	rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ),
+	elemdisplay = {},
+
+	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+	cssNormalTransform = {
+		letterSpacing: 0,
+		fontWeight: 400
+	},
+
+	cssExpand = [ "Top", "Right", "Bottom", "Left" ],
+	cssPrefixes = [ "Webkit", "O", "Moz", "ms" ],
+
+	eventsToggle = jQuery.fn.toggle;
+
+// return a css property mapped to a potentially vendor prefixed property
+function vendorPropName( style, name ) {
+
+	// shortcut for names that are not vendor prefixed
+	if ( name in style ) {
+		return name;
+	}
+
+	// check for vendor prefixed names
+	var capName = name.charAt(0).toUpperCase() + name.slice(1),
+		origName = name,
+		i = cssPrefixes.length;
+
+	while ( i-- ) {
+		name = cssPrefixes[ i ] + capName;
+		if ( name in style ) {
+			return name;
+		}
+	}
+
+	return origName;
+}
+
+function isHidden( elem, el ) {
+	elem = el || elem;
+	return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
+}
+
+function showHide( elements, show ) {
+	var elem, display,
+		values = [],
+		index = 0,
+		length = elements.length;
+
+	for ( ; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		values[ index ] = jQuery._data( elem, "olddisplay" );
+		if ( show ) {
+			// Reset the inline display of this element to learn if it is
+			// being hidden by cascaded rules or not
+			if ( !values[ index ] && elem.style.display === "none" ) {
+				elem.style.display = "";
+			}
+
+			// Set elements which have been overridden with display: none
+			// in a stylesheet to whatever the default browser style is
+			// for such an element
+			if ( elem.style.display === "" && isHidden( elem ) ) {
+				values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) );
+			}
+		} else {
+			display = curCSS( elem, "display" );
+
+			if ( !values[ index ] && display !== "none" ) {
+				jQuery._data( elem, "olddisplay", display );
+			}
+		}
+	}
+
+	// Set the display of most of the elements in a second loop
+	// to avoid the constant reflow
+	for ( index = 0; index < length; index++ ) {
+		elem = elements[ index ];
+		if ( !elem.style ) {
+			continue;
+		}
+		if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
+			elem.style.display = show ? values[ index ] || "" : "none";
+		}
+	}
+
+	return elements;
+}
+
+jQuery.fn.extend({
+	css: function( name, value ) {
+		return jQuery.access( this, function( elem, name, value ) {
+			return value !== undefined ?
+				jQuery.style( elem, name, value ) :
+				jQuery.css( elem, name );
+		}, name, value, arguments.length > 1 );
+	},
+	show: function() {
+		return showHide( this, true );
+	},
+	hide: function() {
+		return showHide( this );
+	},
+	toggle: function( state, fn2 ) {
+		var bool = typeof state === "boolean";
+
+		if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) {
+			return eventsToggle.apply( this, arguments );
+		}
+
+		return this.each(function() {
+			if ( bool ? state : isHidden( this ) ) {
+				jQuery( this ).show();
+			} else {
+				jQuery( this ).hide();
+			}
+		});
+	}
+});
+
+jQuery.extend({
+	// Add in style property hooks for overriding the default
+	// behavior of getting and setting a style property
+	cssHooks: {
+		opacity: {
+			get: function( elem, computed ) {
+				if ( computed ) {
+					// We should always get a number back from opacity
+					var ret = curCSS( elem, "opacity" );
+					return ret === "" ? "1" : ret;
+
+				}
+			}
+		}
+	},
+
+	// Exclude the following css properties to add px
+	cssNumber: {
+		"fillOpacity": true,
+		"fontWeight": true,
+		"lineHeight": true,
+		"opacity": true,
+		"orphans": true,
+		"widows": true,
+		"zIndex": true,
+		"zoom": true
+	},
+
+	// Add in properties whose names you wish to fix before
+	// setting or getting the value
+	cssProps: {
+		// normalize float css property
+		"float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+	},
+
+	// Get and set the style property on a DOM Node
+	style: function( elem, name, value, extra ) {
+		// Don't set styles on text and comment nodes
+		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+			return;
+		}
+
+		// Make sure that we're working with the right name
+		var ret, type, hooks,
+			origName = jQuery.camelCase( name ),
+			style = elem.style;
+
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// Check if we're setting a value
+		if ( value !== undefined ) {
+			type = typeof value;
+
+			// convert relative number strings (+= or -=) to relative numbers. #7345
+			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
+				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
+				// Fixes bug #9237
+				type = "number";
+			}
+
+			// Make sure that NaN and null values aren't set. See: #7116
+			if ( value == null || type === "number" && isNaN( value ) ) {
+				return;
+			}
+
+			// If a number was passed in, add 'px' to the (except for certain CSS properties)
+			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+				value += "px";
+			}
+
+			// If a hook was provided, use that value, otherwise just set the specified value
+			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
+				// Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+				// Fixes bug #5509
+				try {
+					style[ name ] = value;
+				} catch(e) {}
+			}
+
+		} else {
+			// If a hook was provided get the non-computed value from there
+			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+				return ret;
+			}
+
+			// Otherwise just get the value from the style object
+			return style[ name ];
+		}
+	},
+
+	css: function( elem, name, numeric, extra ) {
+		var val, num, hooks,
+			origName = jQuery.camelCase( name );
+
+		// Make sure that we're working with the right name
+		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
+
+		// gets hook for the prefixed version
+		// followed by the unprefixed version
+		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
+
+		// If a hook was provided get the computed value from there
+		if ( hooks && "get" in hooks ) {
+			val = hooks.get( elem, true, extra );
+		}
+
+		// Otherwise, if a way to get the computed value exists, use that
+		if ( val === undefined ) {
+			val = curCSS( elem, name );
+		}
+
+		//convert "normal" to computed value
+		if ( val === "normal" && name in cssNormalTransform ) {
+			val = cssNormalTransform[ name ];
+		}
+
+		// Return, converting to number if forced or a qualifier was provided and val looks numeric
+		if ( numeric || extra !== undefined ) {
+			num = parseFloat( val );
+			return numeric || jQuery.isNumeric( num ) ? num || 0 : val;
+		}
+		return val;
+	},
+
+	// A method for quickly swapping in/out CSS properties to get correct calculations
+	swap: function( elem, options, callback ) {
+		var ret, name,
+			old = {};
+
+		// Remember the old values, and insert the new ones
+		for ( name in options ) {
+			old[ name ] = elem.style[ name ];
+			elem.style[ name ] = options[ name ];
+		}
+
+		ret = callback.call( elem );
+
+		// Revert the old values
+		for ( name in options ) {
+			elem.style[ name ] = old[ name ];
+		}
+
+		return ret;
+	}
+});
+
+// NOTE: To any future maintainer, we've window.getComputedStyle
+// because jsdom on node.js will break without it.
+if ( window.getComputedStyle ) {
+	curCSS = function( elem, name ) {
+		var ret, width, minWidth, maxWidth,
+			computed = window.getComputedStyle( elem, null ),
+			style = elem.style;
+
+		if ( computed ) {
+
+			ret = computed[ name ];
+			if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
+				ret = jQuery.style( elem, name );
+			}
+
+			// A tribute to the "awesome hack by Dean Edwards"
+			// Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right
+			// Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
+			// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
+			if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
+				width = style.width;
+				minWidth = style.minWidth;
+				maxWidth = style.maxWidth;
+
+				style.minWidth = style.maxWidth = style.width = ret;
+				ret = computed.width;
+
+				style.width = width;
+				style.minWidth = minWidth;
+				style.maxWidth = maxWidth;
+			}
+		}
+
+		return ret;
+	};
+} else if ( document.documentElement.currentStyle ) {
+	curCSS = function( elem, name ) {
+		var left, rsLeft,
+			ret = elem.currentStyle && elem.currentStyle[ name ],
+			style = elem.style;
+
+		// Avoid setting ret to empty string here
+		// so we don't default to auto
+		if ( ret == null && style && style[ name ] ) {
+			ret = style[ name ];
+		}
+
+		// From the awesome hack by Dean Edwards
+		// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+		// If we're not dealing with a regular pixel number
+		// but a number that has a weird ending, we need to convert it to pixels
+		// but not position css attributes, as those are proportional to the parent element instead
+		// and we can't measure the parent instead because it might trigger a "stacking dolls" problem
+		if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) {
+
+			// Remember the original values
+			left = style.left;
+			rsLeft = elem.runtimeStyle && elem.runtimeStyle.left;
+
+			// Put in the new values to get a computed value out
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = elem.currentStyle.left;
+			}
+			style.left = name === "fontSize" ? "1em" : ret;
+			ret = style.pixelLeft + "px";
+
+			// Revert the changed values
+			style.left = left;
+			if ( rsLeft ) {
+				elem.runtimeStyle.left = rsLeft;
+			}
+		}
+
+		return ret === "" ? "auto" : ret;
+	};
+}
+
+function setPositiveNumber( elem, value, subtract ) {
+	var matches = rnumsplit.exec( value );
+	return matches ?
+			Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
+			value;
+}
+
+function augmentWidthOrHeight( elem, name, extra, isBorderBox ) {
+	var i = extra === ( isBorderBox ? "border" : "content" ) ?
+		// If we already have the right measurement, avoid augmentation
+		4 :
+		// Otherwise initialize for horizontal or vertical properties
+		name === "width" ? 1 : 0,
+
+		val = 0;
+
+	for ( ; i < 4; i += 2 ) {
+		// both box models exclude margin, so add it if we want it
+		if ( extra === "margin" ) {
+			// we use jQuery.css instead of curCSS here
+			// because of the reliableMarginRight CSS hook!
+			val += jQuery.css( elem, extra + cssExpand[ i ], true );
+		}
+
+		// From this point on we use curCSS for maximum performance (relevant in animations)
+		if ( isBorderBox ) {
+			// border-box includes padding, so remove it if we want content
+			if ( extra === "content" ) {
+				val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+			}
+
+			// at this point, extra isn't border nor margin, so remove border
+			if ( extra !== "margin" ) {
+				val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+			}
+		} else {
+			// at this point, extra isn't content, so add padding
+			val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0;
+
+			// at this point, extra isn't content nor padding, so add border
+			if ( extra !== "padding" ) {
+				val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0;
+			}
+		}
+	}
+
+	return val;
+}
+
+function getWidthOrHeight( elem, name, extra ) {
+
+	// Start with offset property, which is equivalent to the border-box value
+	var val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+		valueIsBorderBox = true,
+		isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box";
+
+	// some non-html elements return undefined for offsetWidth, so check for null/undefined
+	// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
+	// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
+	if ( val <= 0 || val == null ) {
+		// Fall back to computed then uncomputed css if necessary
+		val = curCSS( elem, name );
+		if ( val < 0 || val == null ) {
+			val = elem.style[ name ];
+		}
+
+		// Computed unit is not pixels. Stop here and return.
+		if ( rnumnonpx.test(val) ) {
+			return val;
+		}
+
+		// we need the check for style in case a browser which returns unreliable values
+		// for getComputedStyle silently falls back to the reliable elem.style
+		valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );
+
+		// Normalize "", auto, and prepare for extra
+		val = parseFloat( val ) || 0;
+	}
+
+	// use the active box-sizing model to add/subtract irrelevant styles
+	return ( val +
+		augmentWidthOrHeight(
+			elem,
+			name,
+			extra || ( isBorderBox ? "border" : "content" ),
+			valueIsBorderBox
+		)
+	) + "px";
+}
+
+
+// Try to determine the default display value of an element
+function css_defaultDisplay( nodeName ) {
+	if ( elemdisplay[ nodeName ] ) {
+		return elemdisplay[ nodeName ];
+	}
+
+	var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ),
+		display = elem.css("display");
+	elem.remove();
+
+	// If the simple way fails,
+	// get element's real default display by attaching it to a temp iframe
+	if ( display === "none" || display === "" ) {
+		// Use the already-created iframe if possible
+		iframe = document.body.appendChild(
+			iframe || jQuery.extend( document.createElement("iframe"), {
+				frameBorder: 0,
+				width: 0,
+				height: 0
+			})
+		);
+
+		// Create a cacheable copy of the iframe document on first call.
+		// IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML
+		// document to it; WebKit & Firefox won't allow reusing the iframe document.
+		if ( !iframeDoc || !iframe.createElement ) {
+			iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;
+			iframeDoc.write("<!doctype html><html><body>");
+			iframeDoc.close();
+		}
+
+		elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) );
+
+		display = curCSS( elem, "display" );
+		document.body.removeChild( iframe );
+	}
+
+	// Store the correct default display
+	elemdisplay[ nodeName ] = display;
+
+	return display;
+}
+
+jQuery.each([ "height", "width" ], function( i, name ) {
+	jQuery.cssHooks[ name ] = {
+		get: function( elem, computed, extra ) {
+			if ( computed ) {
+				// certain elements can have dimension info if we invisibly show them
+				// however, it must have a current display style that would benefit from this
+				if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) {
+					return jQuery.swap( elem, cssShow, function() {
+						return getWidthOrHeight( elem, name, extra );
+					});
+				} else {
+					return getWidthOrHeight( elem, name, extra );
+				}
+			}
+		},
+
+		set: function( elem, value, extra ) {
+			return setPositiveNumber( elem, value, extra ?
+				augmentWidthOrHeight(
+					elem,
+					name,
+					extra,
+					jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"
+				) : 0
+			);
+		}
+	};
+});
+
+if ( !jQuery.support.opacity ) {
+	jQuery.cssHooks.opacity = {
+		get: function( elem, computed ) {
+			// IE uses filters for opacity
+			return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+				( 0.01 * parseFloat( RegExp.$1 ) ) + "" :
+				computed ? "1" : "";
+		},
+
+		set: function( elem, value ) {
+			var style = elem.style,
+				currentStyle = elem.currentStyle,
+				opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "",
+				filter = currentStyle && currentStyle.filter || style.filter || "";
+
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			style.zoom = 1;
+
+			// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652
+			if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" &&
+				style.removeAttribute ) {
+
+				// Setting style.filter to null, "" & " " still leave "filter:" in the cssText
+				// if "filter:" is present at all, clearType is disabled, we want to avoid this
+				// style.removeAttribute is IE Only, but so apparently is this code path...
+				style.removeAttribute( "filter" );
+
+				// if there there is no filter style applied in a css rule, we are done
+				if ( currentStyle && !currentStyle.filter ) {
+					return;
+				}
+			}
+
+			// otherwise, set new filter values
+			style.filter = ralpha.test( filter ) ?
+				filter.replace( ralpha, opacity ) :
+				filter + " " + opacity;
+		}
+	};
+}
+
+// These hooks cannot be added until DOM ready because the support test
+// for it is not run until after DOM ready
+jQuery(function() {
+	if ( !jQuery.support.reliableMarginRight ) {
+		jQuery.cssHooks.marginRight = {
+			get: function( elem, computed ) {
+				// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+				// Work around by temporarily setting element display to inline-block
+				return jQuery.swap( elem, { "display": "inline-block" }, function() {
+					if ( computed ) {
+						return curCSS( elem, "marginRight" );
+					}
+				});
+			}
+		};
+	}
+
+	// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
+	// getComputedStyle returns percent when specified for top/left/bottom/right
+	// rather than make the css module depend on the offset module, we just check for it here
+	if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {
+		jQuery.each( [ "top", "left" ], function( i, prop ) {
+			jQuery.cssHooks[ prop ] = {
+				get: function( elem, computed ) {
+					if ( computed ) {
+						var ret = curCSS( elem, prop );
+						// if curCSS returns percentage, fallback to offset
+						return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret;
+					}
+				}
+			};
+		});
+	}
+
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.hidden = function( elem ) {
+		return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none");
+	};
+
+	jQuery.expr.filters.visible = function( elem ) {
+		return !jQuery.expr.filters.hidden( elem );
+	};
+}
+
+// These hooks are used by animate to expand properties
+jQuery.each({
+	margin: "",
+	padding: "",
+	border: "Width"
+}, function( prefix, suffix ) {
+	jQuery.cssHooks[ prefix + suffix ] = {
+		expand: function( value ) {
+			var i,
+
+				// assumes a single number if not a string
+				parts = typeof value === "string" ? value.split(" ") : [ value ],
+				expanded = {};
+
+			for ( i = 0; i < 4; i++ ) {
+				expanded[ prefix + cssExpand[ i ] + suffix ] =
+					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
+			}
+
+			return expanded;
+		}
+	};
+
+	if ( !rmargin.test( prefix ) ) {
+		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
+	}
+});
+var r20 = /%20/g,
+	rbracket = /\[\]$/,
+	rCRLF = /\r?\n/g,
+	rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
+	rselectTextarea = /^(?:select|textarea)/i;
+
+jQuery.fn.extend({
+	serialize: function() {
+		return jQuery.param( this.serializeArray() );
+	},
+	serializeArray: function() {
+		return this.map(function(){
+			return this.elements ? jQuery.makeArray( this.elements ) : this;
+		})
+		.filter(function(){
+			return this.name && !this.disabled &&
+				( this.checked || rselectTextarea.test( this.nodeName ) ||
+					rinput.test( this.type ) );
+		})
+		.map(function( i, elem ){
+			var val = jQuery( this ).val();
+
+			return val == null ?
+				null :
+				jQuery.isArray( val ) ?
+					jQuery.map( val, function( val, i ){
+						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+					}) :
+					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+		}).get();
+	}
+});
+
+//Serialize an array of form elements or a set of
+//key/values into a query string
+jQuery.param = function( a, traditional ) {
+	var prefix,
+		s = [],
+		add = function( key, value ) {
+			// If value is a function, invoke it and return its value
+			value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
+			s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+		};
+
+	// Set traditional to true for jQuery <= 1.3.2 behavior.
+	if ( traditional === undefined ) {
+		traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
+	}
+
+	// If an array was passed in, assume that it is an array of form elements.
+	if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+		// Serialize the form elements
+		jQuery.each( a, function() {
+			add( this.name, this.value );
+		});
+
+	} else {
+		// If traditional, encode the "old" way (the way 1.3.2 or older
+		// did it), otherwise encode params recursively.
+		for ( prefix in a ) {
+			buildParams( prefix, a[ prefix ], traditional, add );
+		}
+	}
+
+	// Return the resulting serialization
+	return s.join( "&" ).replace( r20, "+" );
+};
+
+function buildParams( prefix, obj, traditional, add ) {
+	var name;
+
+	if ( jQuery.isArray( obj ) ) {
+		// Serialize array item.
+		jQuery.each( obj, function( i, v ) {
+			if ( traditional || rbracket.test( prefix ) ) {
+				// Treat each array item as a scalar.
+				add( prefix, v );
+
+			} else {
+				// If array item is non-scalar (array or object), encode its
+				// numeric index to resolve deserialization ambiguity issues.
+				// Note that rack (as of 1.0.0) can't currently deserialize
+				// nested arrays properly, and attempting to do so may cause
+				// a server error. Possible fixes are to modify rack's
+				// deserialization algorithm or to provide an option or flag
+				// to force array serialization to be shallow.
+				buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
+			}
+		});
+
+	} else if ( !traditional && jQuery.type( obj ) === "object" ) {
+		// Serialize object item.
+		for ( name in obj ) {
+			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+		}
+
+	} else {
+		// Serialize scalar item.
+		add( prefix, obj );
+	}
+}
+var
+	// Document location
+	ajaxLocParts,
+	ajaxLocation,
+
+	rhash = /#.*$/,
+	rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+	// #7653, #8125, #8152: local protocol detection
+	rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,
+	rnoContent = /^(?:GET|HEAD)$/,
+	rprotocol = /^\/\//,
+	rquery = /\?/,
+	rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
+	rts = /([?&])_=[^&]*/,
+	rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,
+
+	// Keep a copy of the old load method
+	_load = jQuery.fn.load,
+
+	/* Prefilters
+	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+	 * 2) These are called:
+	 *    - BEFORE asking for a transport
+	 *    - AFTER param serialization (s.data is a string if s.processData is true)
+	 * 3) key is the dataType
+	 * 4) the catchall symbol "*" can be used
+	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+	 */
+	prefilters = {},
+
+	/* Transports bindings
+	 * 1) key is the dataType
+	 * 2) the catchall symbol "*" can be used
+	 * 3) selection will start with transport dataType and THEN go to "*" if needed
+	 */
+	transports = {},
+
+	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
+	allTypes = ["*/"] + ["*"];
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+	ajaxLocation = location.href;
+} catch( e ) {
+	// Use the href attribute of an A element
+	// since IE will modify it given document.location
+	ajaxLocation = document.createElement( "a" );
+	ajaxLocation.href = "";
+	ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+	// dataTypeExpression is optional and defaults to "*"
+	return function( dataTypeExpression, func ) {
+
+		if ( typeof dataTypeExpression !== "string" ) {
+			func = dataTypeExpression;
+			dataTypeExpression = "*";
+		}
+
+		var dataType, list, placeBefore,
+			dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ),
+			i = 0,
+			length = dataTypes.length;
+
+		if ( jQuery.isFunction( func ) ) {
+			// For each dataType in the dataTypeExpression
+			for ( ; i < length; i++ ) {
+				dataType = dataTypes[ i ];
+				// We control if we're asked to add before
+				// any existing element
+				placeBefore = /^\+/.test( dataType );
+				if ( placeBefore ) {
+					dataType = dataType.substr( 1 ) || "*";
+				}
+				list = structure[ dataType ] = structure[ dataType ] || [];
+				// then we add to the structure accordingly
+				list[ placeBefore ? "unshift" : "push" ]( func );
+			}
+		}
+	};
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
+		dataType /* internal */, inspected /* internal */ ) {
+
+	dataType = dataType || options.dataTypes[ 0 ];
+	inspected = inspected || {};
+
+	inspected[ dataType ] = true;
+
+	var selection,
+		list = structure[ dataType ],
+		i = 0,
+		length = list ? list.length : 0,
+		executeOnly = ( structure === prefilters );
+
+	for ( ; i < length && ( executeOnly || !selection ); i++ ) {
+		selection = list[ i ]( options, originalOptions, jqXHR );
+		// If we got redirected to another dataType
+		// we try there if executing only and not done already
+		if ( typeof selection === "string" ) {
+			if ( !executeOnly || inspected[ selection ] ) {
+				selection = undefined;
+			} else {
+				options.dataTypes.unshift( selection );
+				selection = inspectPrefiltersOrTransports(
+						structure, options, originalOptions, jqXHR, selection, inspected );
+			}
+		}
+	}
+	// If we're only executing or nothing was selected
+	// we try the catchall dataType if not done already
+	if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
+		selection = inspectPrefiltersOrTransports(
+				structure, options, originalOptions, jqXHR, "*", inspected );
+	}
+	// unnecessary when only executing (prefilters)
+	// but it'll be ignored by the caller in that case
+	return selection;
+}
+
+// A special extend for ajax options
+// that takes "flat" options (not to be deep extended)
+// Fixes #9887
+function ajaxExtend( target, src ) {
+	var key, deep,
+		flatOptions = jQuery.ajaxSettings.flatOptions || {};
+	for ( key in src ) {
+		if ( src[ key ] !== undefined ) {
+			( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];
+		}
+	}
+	if ( deep ) {
+		jQuery.extend( true, target, deep );
+	}
+}
+
+jQuery.fn.load = function( url, params, callback ) {
+	if ( typeof url !== "string" && _load ) {
+		return _load.apply( this, arguments );
+	}
+
+	// Don't do a request if no elements are being requested
+	if ( !this.length ) {
+		return this;
+	}
+
+	var selector, type, response,
+		self = this,
+		off = url.indexOf(" ");
+
+	if ( off >= 0 ) {
+		selector = url.slice( off, url.length );
+		url = url.slice( 0, off );
+	}
+
+	// If it's a function
+	if ( jQuery.isFunction( params ) ) {
+
+		// We assume that it's the callback
+		callback = params;
+		params = undefined;
+
+	// Otherwise, build a param string
+	} else if ( params && typeof params === "object" ) {
+		type = "POST";
+	}
+
+	// Request the remote document
+	jQuery.ajax({
+		url: url,
+
+		// if "type" variable is undefined, then "GET" method will be used
+		type: type,
+		dataType: "html",
+		data: params,
+		complete: function( jqXHR, status ) {
+			if ( callback ) {
+				self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+			}
+		}
+	}).done(function( responseText ) {
+
+		// Save response for use in complete callback
+		response = arguments;
+
+		// See if a selector was specified
+		self.html( selector ?
+
+			// Create a dummy div to hold the results
+			jQuery("<div>")
+
+				// inject the contents of the document in, removing the scripts
+				// to avoid any 'Permission Denied' errors in IE
+				.append( responseText.replace( rscript, "" ) )
+
+				// Locate the specified elements
+				.find( selector ) :
+
+			// If not, just inject the full result
+			responseText );
+
+	});
+
+	return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
+	jQuery.fn[ o ] = function( f ){
+		return this.on( o, f );
+	};
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+	jQuery[ method ] = function( url, data, callback, type ) {
+		// shift arguments if data argument was omitted
+		if ( jQuery.isFunction( data ) ) {
+			type = type || callback;
+			callback = data;
+			data = undefined;
+		}
+
+		return jQuery.ajax({
+			type: method,
+			url: url,
+			data: data,
+			success: callback,
+			dataType: type
+		});
+	};
+});
+
+jQuery.extend({
+
+	getScript: function( url, callback ) {
+		return jQuery.get( url, undefined, callback, "script" );
+	},
+
+	getJSON: function( url, data, callback ) {
+		return jQuery.get( url, data, callback, "json" );
+	},
+
+	// Creates a full fledged settings object into target
+	// with both ajaxSettings and settings fields.
+	// If target is omitted, writes into ajaxSettings.
+	ajaxSetup: function( target, settings ) {
+		if ( settings ) {
+			// Building a settings object
+			ajaxExtend( target, jQuery.ajaxSettings );
+		} else {
+			// Extending ajaxSettings
+			settings = target;
+			target = jQuery.ajaxSettings;
+		}
+		ajaxExtend( target, settings );
+		return target;
+	},
+
+	ajaxSettings: {
+		url: ajaxLocation,
+		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+		global: true,
+		type: "GET",
+		contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+		processData: true,
+		async: true,
+		/*
+		timeout: 0,
+		data: null,
+		dataType: null,
+		username: null,
+		password: null,
+		cache: null,
+		throws: false,
+		traditional: false,
+		headers: {},
+		*/
+
+		accepts: {
+			xml: "application/xml, text/xml",
+			html: "text/html",
+			text: "text/plain",
+			json: "application/json, text/javascript",
+			"*": allTypes
+		},
+
+		contents: {
+			xml: /xml/,
+			html: /html/,
+			json: /json/
+		},
+
+		responseFields: {
+			xml: "responseXML",
+			text: "responseText"
+		},
+
+		// List of data converters
+		// 1) key format is "source_type destination_type" (a single space in-between)
+		// 2) the catchall symbol "*" can be used for source_type
+		converters: {
+
+			// Convert anything to text
+			"* text": window.String,
+
+			// Text to html (true = no transformation)
+			"text html": true,
+
+			// Evaluate text as a json expression
+			"text json": jQuery.parseJSON,
+
+			// Parse text as xml
+			"text xml": jQuery.parseXML
+		},
+
+		// For options that shouldn't be deep extended:
+		// you can add your own custom options here if
+		// and when you create one that shouldn't be
+		// deep extended (see ajaxExtend)
+		flatOptions: {
+			context: true,
+			url: true
+		}
+	},
+
+	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+	ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+	// Main method
+	ajax: function( url, options ) {
+
+		// If url is an object, simulate pre-1.5 signature
+		if ( typeof url === "object" ) {
+			options = url;
+			url = undefined;
+		}
+
+		// Force options to be an object
+		options = options || {};
+
+		var // ifModified key
+			ifModifiedKey,
+			// Response headers
+			responseHeadersString,
+			responseHeaders,
+			// transport
+			transport,
+			// timeout handle
+			timeoutTimer,
+			// Cross-domain detection vars
+			parts,
+			// To know if global events are to be dispatched
+			fireGlobals,
+			// Loop variable
+			i,
+			// Create the final options object
+			s = jQuery.ajaxSetup( {}, options ),
+			// Callbacks context
+			callbackContext = s.context || s,
+			// Context for global events
+			// It's the callbackContext if one was provided in the options
+			// and if it's a DOM node or a jQuery collection
+			globalEventContext = callbackContext !== s &&
+				( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+						jQuery( callbackContext ) : jQuery.event,
+			// Deferreds
+			deferred = jQuery.Deferred(),
+			completeDeferred = jQuery.Callbacks( "once memory" ),
+			// Status-dependent callbacks
+			statusCode = s.statusCode || {},
+			// Headers (they are sent all at once)
+			requestHeaders = {},
+			requestHeadersNames = {},
+			// The jqXHR state
+			state = 0,
+			// Default abort message
+			strAbort = "canceled",
+			// Fake xhr
+			jqXHR = {
+
+				readyState: 0,
+
+				// Caches the header
+				setRequestHeader: function( name, value ) {
+					if ( !state ) {
+						var lname = name.toLowerCase();
+						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+						requestHeaders[ name ] = value;
+					}
+					return this;
+				},
+
+				// Raw string
+				getAllResponseHeaders: function() {
+					return state === 2 ? responseHeadersString : null;
+				},
+
+				// Builds headers hashtable if needed
+				getResponseHeader: function( key ) {
+					var match;
+					if ( state === 2 ) {
+						if ( !responseHeaders ) {
+							responseHeaders = {};
+							while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+							}
+						}
+						match = responseHeaders[ key.toLowerCase() ];
+					}
+					return match === undefined ? null : match;
+				},
+
+				// Overrides response content-type header
+				overrideMimeType: function( type ) {
+					if ( !state ) {
+						s.mimeType = type;
+					}
+					return this;
+				},
+
+				// Cancel the request
+				abort: function( statusText ) {
+					statusText = statusText || strAbort;
+					if ( transport ) {
+						transport.abort( statusText );
+					}
+					done( 0, statusText );
+					return this;
+				}
+			};
+
+		// Callback for when everything is done
+		// It is defined here because jslint complains if it is declared
+		// at the end of the function (which would be more logical and readable)
+		function done( status, nativeStatusText, responses, headers ) {
+			var isSuccess, success, error, response, modified,
+				statusText = nativeStatusText;
+
+			// Called once
+			if ( state === 2 ) {
+				return;
+			}
+
+			// State is "done" now
+			state = 2;
+
+			// Clear timeout if it exists
+			if ( timeoutTimer ) {
+				clearTimeout( timeoutTimer );
+			}
+
+			// Dereference transport for early garbage collection
+			// (no matter how long the jqXHR object will be used)
+			transport = undefined;
+
+			// Cache response headers
+			responseHeadersString = headers || "";
+
+			// Set readyState
+			jqXHR.readyState = status > 0 ? 4 : 0;
+
+			// Get response data
+			if ( responses ) {
+				response = ajaxHandleResponses( s, jqXHR, responses );
+			}
+
+			// If successful, handle type chaining
+			if ( status >= 200 && status < 300 || status === 304 ) {
+
+				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+				if ( s.ifModified ) {
+
+					modified = jqXHR.getResponseHeader("Last-Modified");
+					if ( modified ) {
+						jQuery.lastModified[ ifModifiedKey ] = modified;
+					}
+					modified = jqXHR.getResponseHeader("Etag");
+					if ( modified ) {
+						jQuery.etag[ ifModifiedKey ] = modified;
+					}
+				}
+
+				// If not modified
+				if ( status === 304 ) {
+
+					statusText = "notmodified";
+					isSuccess = true;
+
+				// If we have data
+				} else {
+
+					isSuccess = ajaxConvert( s, response );
+					statusText = isSuccess.state;
+					success = isSuccess.data;
+					error = isSuccess.error;
+					isSuccess = !error;
+				}
+			} else {
+				// We extract error from statusText
+				// then normalize statusText and status for non-aborts
+				error = statusText;
+				if ( !statusText || status ) {
+					statusText = "error";
+					if ( status < 0 ) {
+						status = 0;
+					}
+				}
+			}
+
+			// Set data for the fake xhr object
+			jqXHR.status = status;
+			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+			// Success/Error
+			if ( isSuccess ) {
+				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+			} else {
+				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+			}
+
+			// Status-dependent callbacks
+			jqXHR.statusCode( statusCode );
+			statusCode = undefined;
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+						[ jqXHR, s, isSuccess ? success : error ] );
+			}
+
+			// Complete
+			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+				// Handle the global AJAX counter
+				if ( !( --jQuery.active ) ) {
+					jQuery.event.trigger( "ajaxStop" );
+				}
+			}
+		}
+
+		// Attach deferreds
+		deferred.promise( jqXHR );
+		jqXHR.success = jqXHR.done;
+		jqXHR.error = jqXHR.fail;
+		jqXHR.complete = completeDeferred.add;
+
+		// Status-dependent callbacks
+		jqXHR.statusCode = function( map ) {
+			if ( map ) {
+				var tmp;
+				if ( state < 2 ) {
+					for ( tmp in map ) {
+						statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+					}
+				} else {
+					tmp = map[ jqXHR.status ];
+					jqXHR.always( tmp );
+				}
+			}
+			return this;
+		};
+
+		// Remove hash character (#7531: and string promotion)
+		// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+		// We also use the url parameter if available
+		s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+		// Extract dataTypes list
+		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace );
+
+		// A cross-domain request is in order when we have a protocol:host:port mismatch
+		if ( s.crossDomain == null ) {
+			parts = rurl.exec( s.url.toLowerCase() ) || false;
+			s.crossDomain = parts && ( parts.join(":") + ( parts[ 3 ] ? "" : parts[ 1 ] === "http:" ? 80 : 443 ) ) !==
+				( ajaxLocParts.join(":") + ( ajaxLocParts[ 3 ] ? "" : ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) );
+		}
+
+		// Convert data if not already a string
+		if ( s.data && s.processData && typeof s.data !== "string" ) {
+			s.data = jQuery.param( s.data, s.traditional );
+		}
+
+		// Apply prefilters
+		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+		// If request was aborted inside a prefilter, stop there
+		if ( state === 2 ) {
+			return jqXHR;
+		}
+
+		// We can fire global events as of now if asked to
+		fireGlobals = s.global;
+
+		// Uppercase the type
+		s.type = s.type.toUpperCase();
+
+		// Determine if request has content
+		s.hasContent = !rnoContent.test( s.type );
+
+		// Watch for a new set of requests
+		if ( fireGlobals && jQuery.active++ === 0 ) {
+			jQuery.event.trigger( "ajaxStart" );
+		}
+
+		// More options handling for requests with no content
+		if ( !s.hasContent ) {
+
+			// If data is available, append data to url
+			if ( s.data ) {
+				s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+				// #9682: remove data so that it's not used in an eventual retry
+				delete s.data;
+			}
+
+			// Get ifModifiedKey before adding the anti-cache parameter
+			ifModifiedKey = s.url;
+
+			// Add anti-cache in url if needed
+			if ( s.cache === false ) {
+
+				var ts = jQuery.now(),
+					// try replacing _= if it is there
+					ret = s.url.replace( rts, "$1_=" + ts );
+
+				// if nothing was replaced, add timestamp to the end
+				s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+			}
+		}
+
+		// Set the correct header, if data is being sent
+		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+			jqXHR.setRequestHeader( "Content-Type", s.contentType );
+		}
+
+		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+		if ( s.ifModified ) {
+			ifModifiedKey = ifModifiedKey || s.url;
+			if ( jQuery.lastModified[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] );
+			}
+			if ( jQuery.etag[ ifModifiedKey ] ) {
+				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] );
+			}
+		}
+
+		// Set the Accepts header for the server, depending on the dataType
+		jqXHR.setRequestHeader(
+			"Accept",
+			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+				s.accepts[ "*" ]
+		);
+
+		// Check for headers option
+		for ( i in s.headers ) {
+			jqXHR.setRequestHeader( i, s.headers[ i ] );
+		}
+
+		// Allow custom headers/mimetypes and early abort
+		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+				// Abort if not done already and return
+				return jqXHR.abort();
+
+		}
+
+		// aborting is no longer a cancellation
+		strAbort = "abort";
+
+		// Install callbacks on deferreds
+		for ( i in { success: 1, error: 1, complete: 1 } ) {
+			jqXHR[ i ]( s[ i ] );
+		}
+
+		// Get transport
+		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+		// If no transport, we auto-abort
+		if ( !transport ) {
+			done( -1, "No Transport" );
+		} else {
+			jqXHR.readyState = 1;
+			// Send global event
+			if ( fireGlobals ) {
+				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+			}
+			// Timeout
+			if ( s.async && s.timeout > 0 ) {
+				timeoutTimer = setTimeout( function(){
+					jqXHR.abort( "timeout" );
+				}, s.timeout );
+			}
+
+			try {
+				state = 1;
+				transport.send( requestHeaders, done );
+			} catch (e) {
+				// Propagate exception as error if not done
+				if ( state < 2 ) {
+					done( -1, e );
+				// Simply rethrow otherwise
+				} else {
+					throw e;
+				}
+			}
+		}
+
+		return jqXHR;
+	},
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Last-Modified header cache for next request
+	lastModified: {},
+	etag: {}
+
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+	var ct, type, finalDataType, firstDataType,
+		contents = s.contents,
+		dataTypes = s.dataTypes,
+		responseFields = s.responseFields;
+
+	// Fill responseXXX fields
+	for ( type in responseFields ) {
+		if ( type in responses ) {
+			jqXHR[ responseFields[type] ] = responses[ type ];
+		}
+	}
+
+	// Remove auto dataType and get content-type in the process
+	while( dataTypes[ 0 ] === "*" ) {
+		dataTypes.shift();
+		if ( ct === undefined ) {
+			ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
+		}
+	}
+
+	// Check if we're dealing with a known content-type
+	if ( ct ) {
+		for ( type in contents ) {
+			if ( contents[ type ] && contents[ type ].test( ct ) ) {
+				dataTypes.unshift( type );
+				break;
+			}
+		}
+	}
+
+	// Check to see if we have a response for the expected dataType
+	if ( dataTypes[ 0 ] in responses ) {
+		finalDataType = dataTypes[ 0 ];
+	} else {
+		// Try convertible dataTypes
+		for ( type in responses ) {
+			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+				finalDataType = type;
+				break;
+			}
+			if ( !firstDataType ) {
+				firstDataType = type;
+			}
+		}
+		// Or just use first one
+		finalDataType = finalDataType || firstDataType;
+	}
+
+	// If we found a dataType
+	// We add the dataType to the list if needed
+	// and return the corresponding response
+	if ( finalDataType ) {
+		if ( finalDataType !== dataTypes[ 0 ] ) {
+			dataTypes.unshift( finalDataType );
+		}
+		return responses[ finalDataType ];
+	}
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+
+	var conv, conv2, current, tmp,
+		// Work with a copy of dataTypes in case we need to modify it for conversion
+		dataTypes = s.dataTypes.slice(),
+		prev = dataTypes[ 0 ],
+		converters = {},
+		i = 0;
+
+	// Apply the dataFilter if provided
+	if ( s.dataFilter ) {
+		response = s.dataFilter( response, s.dataType );
+	}
+
+	// Create converters map with lowercased keys
+	if ( dataTypes[ 1 ] ) {
+		for ( conv in s.converters ) {
+			converters[ conv.toLowerCase() ] = s.converters[ conv ];
+		}
+	}
+
+	// Convert to each sequential dataType, tolerating list modification
+	for ( ; (current = dataTypes[++i]); ) {
+
+		// There's only work to do if current dataType is non-auto
+		if ( current !== "*" ) {
+
+			// Convert response if prev dataType is non-auto and differs from current
+			if ( prev !== "*" && prev !== current ) {
+
+				// Seek a direct converter
+				conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+				// If none found, seek a pair
+				if ( !conv ) {
+					for ( conv2 in converters ) {
+
+						// If conv2 outputs current
+						tmp = conv2.split(" ");
+						if ( tmp[ 1 ] === current ) {
+
+							// If prev can be converted to accepted input
+							conv = converters[ prev + " " + tmp[ 0 ] ] ||
+								converters[ "* " + tmp[ 0 ] ];
+							if ( conv ) {
+								// Condense equivalence converters
+								if ( conv === true ) {
+									conv = converters[ conv2 ];
+
+								// Otherwise, insert the intermediate dataType
+								} else if ( converters[ conv2 ] !== true ) {
+									current = tmp[ 0 ];
+									dataTypes.splice( i--, 0, current );
+								}
+
+								break;
+							}
+						}
+					}
+				}
+
+				// Apply converter (if not an equivalence)
+				if ( conv !== true ) {
+
+					// Unless errors are allowed to bubble, catch and return them
+					if ( conv && s["throws"] ) {
+						response = conv( response );
+					} else {
+						try {
+							response = conv( response );
+						} catch ( e ) {
+							return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+						}
+					}
+				}
+			}
+
+			// Update prev for next iteration
+			prev = current;
+		}
+	}
+
+	return { state: "success", data: response };
+}
+var oldCallbacks = [],
+	rquestion = /\?/,
+	rjsonp = /(=)\?(?=&|$)|\?\?/,
+	nonce = jQuery.now();
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+	jsonp: "callback",
+	jsonpCallback: function() {
+		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
+		this[ callback ] = true;
+		return callback;
+	}
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+	var callbackName, overwritten, responseContainer,
+		data = s.data,
+		url = s.url,
+		hasCallback = s.jsonp !== false,
+		replaceInUrl = hasCallback && rjsonp.test( url ),
+		replaceInData = hasCallback && !replaceInUrl && typeof data === "string" &&
+			!( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") &&
+			rjsonp.test( data );
+
+	// Handle iff the expected data type is "jsonp" or we have a parameter to set
+	if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) {
+
+		// Get callback name, remembering preexisting value associated with it
+		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+			s.jsonpCallback() :
+			s.jsonpCallback;
+		overwritten = window[ callbackName ];
+
+		// Insert callback into url or form data
+		if ( replaceInUrl ) {
+			s.url = url.replace( rjsonp, "$1" + callbackName );
+		} else if ( replaceInData ) {
+			s.data = data.replace( rjsonp, "$1" + callbackName );
+		} else if ( hasCallback ) {
+			s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+		}
+
+		// Use data converter to retrieve json after script execution
+		s.converters["script json"] = function() {
+			if ( !responseContainer ) {
+				jQuery.error( callbackName + " was not called" );
+			}
+			return responseContainer[ 0 ];
+		};
+
+		// force json dataType
+		s.dataTypes[ 0 ] = "json";
+
+		// Install callback
+		window[ callbackName ] = function() {
+			responseContainer = arguments;
+		};
+
+		// Clean-up function (fires after converters)
+		jqXHR.always(function() {
+			// Restore preexisting value
+			window[ callbackName ] = overwritten;
+
+			// Save back as free
+			if ( s[ callbackName ] ) {
+				// make sure that re-using the options doesn't screw things around
+				s.jsonpCallback = originalSettings.jsonpCallback;
+
+				// save the callback name for future use
+				oldCallbacks.push( callbackName );
+			}
+
+			// Call if it was a function and we have a response
+			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+				overwritten( responseContainer[ 0 ] );
+			}
+
+			responseContainer = overwritten = undefined;
+		});
+
+		// Delegate to script
+		return "script";
+	}
+});
+// Install script dataType
+jQuery.ajaxSetup({
+	accepts: {
+		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+	},
+	contents: {
+		script: /javascript|ecmascript/
+	},
+	converters: {
+		"text script": function( text ) {
+			jQuery.globalEval( text );
+			return text;
+		}
+	}
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+	if ( s.cache === undefined ) {
+		s.cache = false;
+	}
+	if ( s.crossDomain ) {
+		s.type = "GET";
+		s.global = false;
+	}
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+	// This transport only deals with cross domain requests
+	if ( s.crossDomain ) {
+
+		var script,
+			head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
+
+		return {
+
+			send: function( _, callback ) {
+
+				script = document.createElement( "script" );
+
+				script.async = "async";
+
+				if ( s.scriptCharset ) {
+					script.charset = s.scriptCharset;
+				}
+
+				script.src = s.url;
+
+				// Attach handlers for all browsers
+				script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+					if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+						// Handle memory leak in IE
+						script.onload = script.onreadystatechange = null;
+
+						// Remove the script
+						if ( head && script.parentNode ) {
+							head.removeChild( script );
+						}
+
+						// Dereference the script
+						script = undefined;
+
+						// Callback if not abort
+						if ( !isAbort ) {
+							callback( 200, "success" );
+						}
+					}
+				};
+				// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
+				// This arises when a base node is used (#2709 and #4378).
+				head.insertBefore( script, head.firstChild );
+			},
+
+			abort: function() {
+				if ( script ) {
+					script.onload( 0, 1 );
+				}
+			}
+		};
+	}
+});
+var xhrCallbacks,
+	// #5280: Internet Explorer will keep connections alive if we don't abort on unload
+	xhrOnUnloadAbort = window.ActiveXObject ? function() {
+		// Abort all pending requests
+		for ( var key in xhrCallbacks ) {
+			xhrCallbacks[ key ]( 0, 1 );
+		}
+	} : false,
+	xhrId = 0;
+
+// Functions to create xhrs
+function createStandardXHR() {
+	try {
+		return new window.XMLHttpRequest();
+	} catch( e ) {}
+}
+
+function createActiveXHR() {
+	try {
+		return new window.ActiveXObject( "Microsoft.XMLHTTP" );
+	} catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+	/* Microsoft failed to properly
+	 * implement the XMLHttpRequest in IE7 (can't request local files),
+	 * so we use the ActiveXObject when it is available
+	 * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+	 * we need a fallback.
+	 */
+	function() {
+		return !this.isLocal && createStandardXHR() || createActiveXHR();
+	} :
+	// For all other browsers, use the standard XMLHttpRequest object
+	createStandardXHR;
+
+// Determine support properties
+(function( xhr ) {
+	jQuery.extend( jQuery.support, {
+		ajax: !!xhr,
+		cors: !!xhr && ( "withCredentials" in xhr )
+	});
+})( jQuery.ajaxSettings.xhr() );
+
+// Create transport if the browser can provide an xhr
+if ( jQuery.support.ajax ) {
+
+	jQuery.ajaxTransport(function( s ) {
+		// Cross domain only allowed if supported through XMLHttpRequest
+		if ( !s.crossDomain || jQuery.support.cors ) {
+
+			var callback;
+
+			return {
+				send: function( headers, complete ) {
+
+					// Get a new xhr
+					var handle, i,
+						xhr = s.xhr();
+
+					// Open the socket
+					// Passing null username, generates a login popup on Opera (#2865)
+					if ( s.username ) {
+						xhr.open( s.type, s.url, s.async, s.username, s.password );
+					} else {
+						xhr.open( s.type, s.url, s.async );
+					}
+
+					// Apply custom fields if provided
+					if ( s.xhrFields ) {
+						for ( i in s.xhrFields ) {
+							xhr[ i ] = s.xhrFields[ i ];
+						}
+					}
+
+					// Override mime type if needed
+					if ( s.mimeType && xhr.overrideMimeType ) {
+						xhr.overrideMimeType( s.mimeType );
+					}
+
+					// X-Requested-With header
+					// For cross-domain requests, seeing as conditions for a preflight are
+					// akin to a jigsaw puzzle, we simply never set it to be sure.
+					// (it can always be set on a per-request basis or even using ajaxSetup)
+					// For same-domain requests, won't change header if already provided.
+					if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+						headers[ "X-Requested-With" ] = "XMLHttpRequest";
+					}
+
+					// Need an extra try/catch for cross domain requests in Firefox 3
+					try {
+						for ( i in headers ) {
+							xhr.setRequestHeader( i, headers[ i ] );
+						}
+					} catch( _ ) {}
+
+					// Do send the request
+					// This may raise an exception which is actually
+					// handled in jQuery.ajax (so no try/catch here)
+					xhr.send( ( s.hasContent && s.data ) || null );
+
+					// Listener
+					callback = function( _, isAbort ) {
+
+						var status,
+							statusText,
+							responseHeaders,
+							responses,
+							xml;
+
+						// Firefox throws exceptions when accessing properties
+						// of an xhr when a network error occurred
+						// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+						try {
+
+							// Was never called and is aborted or complete
+							if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+								// Only called once
+								callback = undefined;
+
+								// Do not keep as active anymore
+								if ( handle ) {
+									xhr.onreadystatechange = jQuery.noop;
+									if ( xhrOnUnloadAbort ) {
+										delete xhrCallbacks[ handle ];
+									}
+								}
+
+								// If it's an abort
+								if ( isAbort ) {
+									// Abort it manually if needed
+									if ( xhr.readyState !== 4 ) {
+										xhr.abort();
+									}
+								} else {
+									status = xhr.status;
+									responseHeaders = xhr.getAllResponseHeaders();
+									responses = {};
+									xml = xhr.responseXML;
+
+									// Construct response list
+									if ( xml && xml.documentElement /* #4958 */ ) {
+										responses.xml = xml;
+									}
+
+									// When requesting binary data, IE6-9 will throw an exception
+									// on any attempt to access responseText (#11426)
+									try {
+										responses.text = xhr.responseText;
+									} catch( _ ) {
+									}
+
+									// Firefox throws an exception when accessing
+									// statusText for faulty cross-domain requests
+									try {
+										statusText = xhr.statusText;
+									} catch( e ) {
+										// We normalize with Webkit giving an empty statusText
+										statusText = "";
+									}
+
+									// Filter status for non standard behaviors
+
+									// If the request is local and we have data: assume a success
+									// (success with no data won't get notified, that's the best we
+									// can do given current implementations)
+									if ( !status && s.isLocal && !s.crossDomain ) {
+										status = responses.text ? 200 : 404;
+									// IE - #1450: sometimes returns 1223 when it should be 204
+									} else if ( status === 1223 ) {
+										status = 204;
+									}
+								}
+							}
+						} catch( firefoxAccessException ) {
+							if ( !isAbort ) {
+								complete( -1, firefoxAccessException );
+							}
+						}
+
+						// Call complete if needed
+						if ( responses ) {
+							complete( status, statusText, responses, responseHeaders );
+						}
+					};
+
+					if ( !s.async ) {
+						// if we're in sync mode we fire the callback
+						callback();
+					} else if ( xhr.readyState === 4 ) {
+						// (IE6 & IE7) if it's in cache and has been
+						// retrieved directly we need to fire the callback
+						setTimeout( callback, 0 );
+					} else {
+						handle = ++xhrId;
+						if ( xhrOnUnloadAbort ) {
+							// Create the active xhrs callbacks list if needed
+							// and attach the unload handler
+							if ( !xhrCallbacks ) {
+								xhrCallbacks = {};
+								jQuery( window ).unload( xhrOnUnloadAbort );
+							}
+							// Add to list of active xhrs callbacks
+							xhrCallbacks[ handle ] = callback;
+						}
+						xhr.onreadystatechange = callback;
+					}
+				},
+
+				abort: function() {
+					if ( callback ) {
+						callback(0,1);
+					}
+				}
+			};
+		}
+	});
+}
+var fxNow, timerId,
+	rfxtypes = /^(?:toggle|show|hide)$/,
+	rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+	rrun = /queueHooks$/,
+	animationPrefilters = [ defaultPrefilter ],
+	tweeners = {
+		"*": [function( prop, value ) {
+			var end, unit,
+				tween = this.createTween( prop, value ),
+				parts = rfxnum.exec( value ),
+				target = tween.cur(),
+				start = +target || 0,
+				scale = 1,
+				maxIterations = 20;
+
+			if ( parts ) {
+				end = +parts[2];
+				unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+
+				// We need to compute starting value
+				if ( unit !== "px" && start ) {
+					// Iteratively approximate from a nonzero starting point
+					// Prefer the current property, because this process will be trivial if it uses the same units
+					// Fallback to end or a simple constant
+					start = jQuery.css( tween.elem, prop, true ) || end || 1;
+
+					do {
+						// If previous iteration zeroed out, double until we get *something*
+						// Use a string for doubling factor so we don't accidentally see scale as unchanged below
+						scale = scale || ".5";
+
+						// Adjust and apply
+						start = start / scale;
+						jQuery.style( tween.elem, prop, start + unit );
+
+					// Update scale, tolerating zero or NaN from tween.cur()
+					// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+					} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+				}
+
+				tween.unit = unit;
+				tween.start = start;
+				// If a +=/-= token was provided, we're doing a relative animation
+				tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end;
+			}
+			return tween;
+		}]
+	};
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+	setTimeout(function() {
+		fxNow = undefined;
+	}, 0 );
+	return ( fxNow = jQuery.now() );
+}
+
+function createTweens( animation, props ) {
+	jQuery.each( props, function( prop, value ) {
+		var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+			index = 0,
+			length = collection.length;
+		for ( ; index < length; index++ ) {
+			if ( collection[ index ].call( animation, prop, value ) ) {
+
+				// we're done with this property
+				return;
+			}
+		}
+	});
+}
+
+function Animation( elem, properties, options ) {
+	var result,
+		index = 0,
+		tweenerIndex = 0,
+		length = animationPrefilters.length,
+		deferred = jQuery.Deferred().always( function() {
+			// don't match elem in the :animated selector
+			delete tick.elem;
+		}),
+		tick = function() {
+			var currentTime = fxNow || createFxNow(),
+				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+				percent = 1 - ( remaining / animation.duration || 0 ),
+				index = 0,
+				length = animation.tweens.length;
+
+			for ( ; index < length ; index++ ) {
+				animation.tweens[ index ].run( percent );
+			}
+
+			deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+			if ( percent < 1 && length ) {
+				return remaining;
+			} else {
+				deferred.resolveWith( elem, [ animation ] );
+				return false;
+			}
+		},
+		animation = deferred.promise({
+			elem: elem,
+			props: jQuery.extend( {}, properties ),
+			opts: jQuery.extend( true, { specialEasing: {} }, options ),
+			originalProperties: properties,
+			originalOptions: options,
+			startTime: fxNow || createFxNow(),
+			duration: options.duration,
+			tweens: [],
+			createTween: function( prop, end, easing ) {
+				var tween = jQuery.Tween( elem, animation.opts, prop, end,
+						animation.opts.specialEasing[ prop ] || animation.opts.easing );
+				animation.tweens.push( tween );
+				return tween;
+			},
+			stop: function( gotoEnd ) {
+				var index = 0,
+					// if we are going to the end, we want to run all the tweens
+					// otherwise we skip this part
+					length = gotoEnd ? animation.tweens.length : 0;
+
+				for ( ; index < length ; index++ ) {
+					animation.tweens[ index ].run( 1 );
+				}
+
+				// resolve when we played the last frame
+				// otherwise, reject
+				if ( gotoEnd ) {
+					deferred.resolveWith( elem, [ animation, gotoEnd ] );
+				} else {
+					deferred.rejectWith( elem, [ animation, gotoEnd ] );
+				}
+				return this;
+			}
+		}),
+		props = animation.props;
+
+	propFilter( props, animation.opts.specialEasing );
+
+	for ( ; index < length ; index++ ) {
+		result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+		if ( result ) {
+			return result;
+		}
+	}
+
+	createTweens( animation, props );
+
+	if ( jQuery.isFunction( animation.opts.start ) ) {
+		animation.opts.start.call( elem, animation );
+	}
+
+	jQuery.fx.timer(
+		jQuery.extend( tick, {
+			anim: animation,
+			queue: animation.opts.queue,
+			elem: elem
+		})
+	);
+
+	// attach callbacks from options
+	return animation.progress( animation.opts.progress )
+		.done( animation.opts.done, animation.opts.complete )
+		.fail( animation.opts.fail )
+		.always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+	var index, name, easing, value, hooks;
+
+	// camelCase, specialEasing and expand cssHook pass
+	for ( index in props ) {
+		name = jQuery.camelCase( index );
+		easing = specialEasing[ name ];
+		value = props[ index ];
+		if ( jQuery.isArray( value ) ) {
+			easing = value[ 1 ];
+			value = props[ index ] = value[ 0 ];
+		}
+
+		if ( index !== name ) {
+			props[ name ] = value;
+			delete props[ index ];
+		}
+
+		hooks = jQuery.cssHooks[ name ];
+		if ( hooks && "expand" in hooks ) {
+			value = hooks.expand( value );
+			delete props[ name ];
+
+			// not quite $.extend, this wont overwrite keys already present.
+			// also - reusing 'index' from above because we have the correct "name"
+			for ( index in value ) {
+				if ( !( index in props ) ) {
+					props[ index ] = value[ index ];
+					specialEasing[ index ] = easing;
+				}
+			}
+		} else {
+			specialEasing[ name ] = easing;
+		}
+	}
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+	tweener: function( props, callback ) {
+		if ( jQuery.isFunction( props ) ) {
+			callback = props;
+			props = [ "*" ];
+		} else {
+			props = props.split(" ");
+		}
+
+		var prop,
+			index = 0,
+			length = props.length;
+
+		for ( ; index < length ; index++ ) {
+			prop = props[ index ];
+			tweeners[ prop ] = tweeners[ prop ] || [];
+			tweeners[ prop ].unshift( callback );
+		}
+	},
+
+	prefilter: function( callback, prepend ) {
+		if ( prepend ) {
+			animationPrefilters.unshift( callback );
+		} else {
+			animationPrefilters.push( callback );
+		}
+	}
+});
+
+function defaultPrefilter( elem, props, opts ) {
+	var index, prop, value, length, dataShow, tween, hooks, oldfire,
+		anim = this,
+		style = elem.style,
+		orig = {},
+		handled = [],
+		hidden = elem.nodeType && isHidden( elem );
+
+	// handle queue: false promises
+	if ( !opts.queue ) {
+		hooks = jQuery._queueHooks( elem, "fx" );
+		if ( hooks.unqueued == null ) {
+			hooks.unqueued = 0;
+			oldfire = hooks.empty.fire;
+			hooks.empty.fire = function() {
+				if ( !hooks.unqueued ) {
+					oldfire();
+				}
+			};
+		}
+		hooks.unqueued++;
+
+		anim.always(function() {
+			// doing this makes sure that the complete handler will be called
+			// before this completes
+			anim.always(function() {
+				hooks.unqueued--;
+				if ( !jQuery.queue( elem, "fx" ).length ) {
+					hooks.empty.fire();
+				}
+			});
+		});
+	}
+
+	// height/width overflow pass
+	if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+		// Make sure that nothing sneaks out
+		// Record all 3 overflow attributes because IE does not
+		// change the overflow attribute when overflowX and
+		// overflowY are set to the same value
+		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+		// Set display property to inline-block for height/width
+		// animations on inline elements that are having width/height animated
+		if ( jQuery.css( elem, "display" ) === "inline" &&
+				jQuery.css( elem, "float" ) === "none" ) {
+
+			// inline-level elements accept inline-block;
+			// block-level elements need to be inline with layout
+			if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+				style.display = "inline-block";
+
+			} else {
+				style.zoom = 1;
+			}
+		}
+	}
+
+	if ( opts.overflow ) {
+		style.overflow = "hidden";
+		if ( !jQuery.support.shrinkWrapBlocks ) {
+			anim.done(function() {
+				style.overflow = opts.overflow[ 0 ];
+				style.overflowX = opts.overflow[ 1 ];
+				style.overflowY = opts.overflow[ 2 ];
+			});
+		}
+	}
+
+
+	// show/hide pass
+	for ( index in props ) {
+		value = props[ index ];
+		if ( rfxtypes.exec( value ) ) {
+			delete props[ index ];
+			if ( value === ( hidden ? "hide" : "show" ) ) {
+				continue;
+			}
+			handled.push( index );
+		}
+	}
+
+	length = handled.length;
+	if ( length ) {
+		dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+		if ( hidden ) {
+			jQuery( elem ).show();
+		} else {
+			anim.done(function() {
+				jQuery( elem ).hide();
+			});
+		}
+		anim.done(function() {
+			var prop;
+			jQuery.removeData( elem, "fxshow", true );
+			for ( prop in orig ) {
+				jQuery.style( elem, prop, orig[ prop ] );
+			}
+		});
+		for ( index = 0 ; index < length ; index++ ) {
+			prop = handled[ index ];
+			tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+			orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
+
+			if ( !( prop in dataShow ) ) {
+				dataShow[ prop ] = tween.start;
+				if ( hidden ) {
+					tween.end = tween.start;
+					tween.start = prop === "width" || prop === "height" ? 1 : 0;
+				}
+			}
+		}
+	}
+}
+
+function Tween( elem, options, prop, end, easing ) {
+	return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+	constructor: Tween,
+	init: function( elem, options, prop, end, easing, unit ) {
+		this.elem = elem;
+		this.prop = prop;
+		this.easing = easing || "swing";
+		this.options = options;
+		this.start = this.now = this.cur();
+		this.end = end;
+		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+	},
+	cur: function() {
+		var hooks = Tween.propHooks[ this.prop ];
+
+		return hooks && hooks.get ?
+			hooks.get( this ) :
+			Tween.propHooks._default.get( this );
+	},
+	run: function( percent ) {
+		var eased,
+			hooks = Tween.propHooks[ this.prop ];
+
+		if ( this.options.duration ) {
+			this.pos = eased = jQuery.easing[ this.easing ](
+				percent, this.options.duration * percent, 0, 1, this.options.duration
+			);
+		} else {
+			this.pos = eased = percent;
+		}
+		this.now = ( this.end - this.start ) * eased + this.start;
+
+		if ( this.options.step ) {
+			this.options.step.call( this.elem, this.now, this );
+		}
+
+		if ( hooks && hooks.set ) {
+			hooks.set( this );
+		} else {
+			Tween.propHooks._default.set( this );
+		}
+		return this;
+	}
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+	_default: {
+		get: function( tween ) {
+			var result;
+
+			if ( tween.elem[ tween.prop ] != null &&
+				(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+				return tween.elem[ tween.prop ];
+			}
+
+			// passing any value as a 4th parameter to .css will automatically
+			// attempt a parseFloat and fallback to a string if the parse fails
+			// so, simple values such as "10px" are parsed to Float.
+			// complex values such as "rotate(1rad)" are returned as is.
+			result = jQuery.css( tween.elem, tween.prop, false, "" );
+			// Empty strings, null, undefined and "auto" are converted to 0.
+			return !result || result === "auto" ? 0 : result;
+		},
+		set: function( tween ) {
+			// use step hook for back compat - use cssHook if its there - use .style if its
+			// available and use plain properties where available
+			if ( jQuery.fx.step[ tween.prop ] ) {
+				jQuery.fx.step[ tween.prop ]( tween );
+			} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+			} else {
+				tween.elem[ tween.prop ] = tween.now;
+			}
+		}
+	}
+};
+
+// Remove in 2.0 - this supports IE8's panic based approach
+// to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+	set: function( tween ) {
+		if ( tween.elem.nodeType && tween.elem.parentNode ) {
+			tween.elem[ tween.prop ] = tween.now;
+		}
+	}
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+	var cssFn = jQuery.fn[ name ];
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return speed == null || typeof speed === "boolean" ||
+			// special check for .toggle( handler, handler, ... )
+			( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ?
+			cssFn.apply( this, arguments ) :
+			this.animate( genFx( name, true ), speed, easing, callback );
+	};
+});
+
+jQuery.fn.extend({
+	fadeTo: function( speed, to, easing, callback ) {
+
+		// show any hidden elements after setting opacity to 0
+		return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+			// animate to the value specified
+			.end().animate({ opacity: to }, speed, easing, callback );
+	},
+	animate: function( prop, speed, easing, callback ) {
+		var empty = jQuery.isEmptyObject( prop ),
+			optall = jQuery.speed( speed, easing, callback ),
+			doAnimation = function() {
+				// Operate on a copy of prop so per-property easing won't be lost
+				var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+
+				// Empty animations resolve immediately
+				if ( empty ) {
+					anim.stop( true );
+				}
+			};
+
+		return empty || optall.queue === false ?
+			this.each( doAnimation ) :
+			this.queue( optall.queue, doAnimation );
+	},
+	stop: function( type, clearQueue, gotoEnd ) {
+		var stopQueue = function( hooks ) {
+			var stop = hooks.stop;
+			delete hooks.stop;
+			stop( gotoEnd );
+		};
+
+		if ( typeof type !== "string" ) {
+			gotoEnd = clearQueue;
+			clearQueue = type;
+			type = undefined;
+		}
+		if ( clearQueue && type !== false ) {
+			this.queue( type || "fx", [] );
+		}
+
+		return this.each(function() {
+			var dequeue = true,
+				index = type != null && type + "queueHooks",
+				timers = jQuery.timers,
+				data = jQuery._data( this );
+
+			if ( index ) {
+				if ( data[ index ] && data[ index ].stop ) {
+					stopQueue( data[ index ] );
+				}
+			} else {
+				for ( index in data ) {
+					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+						stopQueue( data[ index ] );
+					}
+				}
+			}
+
+			for ( index = timers.length; index--; ) {
+				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+					timers[ index ].anim.stop( gotoEnd );
+					dequeue = false;
+					timers.splice( index, 1 );
+				}
+			}
+
+			// start the next in the queue if the last step wasn't forced
+			// timers currently will call their complete callbacks, which will dequeue
+			// but only if they were gotoEnd
+			if ( dequeue || !gotoEnd ) {
+				jQuery.dequeue( this, type );
+			}
+		});
+	}
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+	var which,
+		attrs = { height: type },
+		i = 0;
+
+	// if we include width, step value is 1 to do all cssExpand values,
+	// if we don't include width, step value is 2 to skip over Left and Right
+	includeWidth = includeWidth? 1 : 0;
+	for( ; i < 4 ; i += 2 - includeWidth ) {
+		which = cssExpand[ i ];
+		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+	}
+
+	if ( includeWidth ) {
+		attrs.opacity = attrs.width = type;
+	}
+
+	return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+	slideDown: genFx("show"),
+	slideUp: genFx("hide"),
+	slideToggle: genFx("toggle"),
+	fadeIn: { opacity: "show" },
+	fadeOut: { opacity: "hide" },
+	fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+	jQuery.fn[ name ] = function( speed, easing, callback ) {
+		return this.animate( props, speed, easing, callback );
+	};
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+		complete: fn || !fn && easing ||
+			jQuery.isFunction( speed ) && speed,
+		duration: speed,
+		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+	};
+
+	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+	// normalize opt.queue - true/undefined/null -> "fx"
+	if ( opt.queue == null || opt.queue === true ) {
+		opt.queue = "fx";
+	}
+
+	// Queueing
+	opt.old = opt.complete;
+
+	opt.complete = function() {
+		if ( jQuery.isFunction( opt.old ) ) {
+			opt.old.call( this );
+		}
+
+		if ( opt.queue ) {
+			jQuery.dequeue( this, opt.queue );
+		}
+	};
+
+	return opt;
+};
+
+jQuery.easing = {
+	linear: function( p ) {
+		return p;
+	},
+	swing: function( p ) {
+		return 0.5 - Math.cos( p*Math.PI ) / 2;
+	}
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+	var timer,
+		timers = jQuery.timers,
+		i = 0;
+
+	for ( ; i < timers.length; i++ ) {
+		timer = timers[ i ];
+		// Checks the timer has not already been removed
+		if ( !timer() && timers[ i ] === timer ) {
+			timers.splice( i--, 1 );
+		}
+	}
+
+	if ( !timers.length ) {
+		jQuery.fx.stop();
+	}
+};
+
+jQuery.fx.timer = function( timer ) {
+	if ( timer() && jQuery.timers.push( timer ) && !timerId ) {
+		timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+	}
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.stop = function() {
+	clearInterval( timerId );
+	timerId = null;
+};
+
+jQuery.fx.speeds = {
+	slow: 600,
+	fast: 200,
+	// Default speed
+	_default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+	jQuery.expr.filters.animated = function( elem ) {
+		return jQuery.grep(jQuery.timers, function( fn ) {
+			return elem === fn.elem;
+		}).length;
+	};
+}
+var rroot = /^(?:body|html)$/i;
+
+jQuery.fn.offset = function( options ) {
+	if ( arguments.length ) {
+		return options === undefined ?
+			this :
+			this.each(function( i ) {
+				jQuery.offset.setOffset( this, options, i );
+			});
+	}
+
+	var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft,
+		box = { top: 0, left: 0 },
+		elem = this[ 0 ],
+		doc = elem && elem.ownerDocument;
+
+	if ( !doc ) {
+		return;
+	}
+
+	if ( (body = doc.body) === elem ) {
+		return jQuery.offset.bodyOffset( elem );
+	}
+
+	docElem = doc.documentElement;
+
+	// Make sure it's not a disconnected DOM node
+	if ( !jQuery.contains( docElem, elem ) ) {
+		return box;
+	}
+
+	// If we don't have gBCR, just use 0,0 rather than error
+	// BlackBerry 5, iOS 3 (original iPhone)
+	if ( typeof elem.getBoundingClientRect !== "undefined" ) {
+		box = elem.getBoundingClientRect();
+	}
+	win = getWindow( doc );
+	clientTop  = docElem.clientTop  || body.clientTop  || 0;
+	clientLeft = docElem.clientLeft || body.clientLeft || 0;
+	scrollTop  = win.pageYOffset || docElem.scrollTop;
+	scrollLeft = win.pageXOffset || docElem.scrollLeft;
+	return {
+		top: box.top  + scrollTop  - clientTop,
+		left: box.left + scrollLeft - clientLeft
+	};
+};
+
+jQuery.offset = {
+
+	bodyOffset: function( body ) {
+		var top = body.offsetTop,
+			left = body.offsetLeft;
+
+		if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) {
+			top  += parseFloat( jQuery.css(body, "marginTop") ) || 0;
+			left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
+		}
+
+		return { top: top, left: left };
+	},
+
+	setOffset: function( elem, options, i ) {
+		var position = jQuery.css( elem, "position" );
+
+		// set position first, in-case top/left are set even on static elem
+		if ( position === "static" ) {
+			elem.style.position = "relative";
+		}
+
+		var curElem = jQuery( elem ),
+			curOffset = curElem.offset(),
+			curCSSTop = jQuery.css( elem, "top" ),
+			curCSSLeft = jQuery.css( elem, "left" ),
+			calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+			props = {}, curPosition = {}, curTop, curLeft;
+
+		// need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+		if ( calculatePosition ) {
+			curPosition = curElem.position();
+			curTop = curPosition.top;
+			curLeft = curPosition.left;
+		} else {
+			curTop = parseFloat( curCSSTop ) || 0;
+			curLeft = parseFloat( curCSSLeft ) || 0;
+		}
+
+		if ( jQuery.isFunction( options ) ) {
+			options = options.call( elem, i, curOffset );
+		}
+
+		if ( options.top != null ) {
+			props.top = ( options.top - curOffset.top ) + curTop;
+		}
+		if ( options.left != null ) {
+			props.left = ( options.left - curOffset.left ) + curLeft;
+		}
+
+		if ( "using" in options ) {
+			options.using.call( elem, props );
+		} else {
+			curElem.css( props );
+		}
+	}
+};
+
+
+jQuery.fn.extend({
+
+	position: function() {
+		if ( !this[0] ) {
+			return;
+		}
+
+		var elem = this[0],
+
+		// Get *real* offsetParent
+		offsetParent = this.offsetParent(),
+
+		// Get correct offsets
+		offset       = this.offset(),
+		parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+		// Subtract element margins
+		// note: when an element has margin: auto the offsetLeft and marginLeft
+		// are the same in Safari causing offset.left to incorrectly be 0
+		offset.top  -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+		offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+		// Add offsetParent borders
+		parentOffset.top  += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+		parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+		// Subtract the two offsets
+		return {
+			top:  offset.top  - parentOffset.top,
+			left: offset.left - parentOffset.left
+		};
+	},
+
+	offsetParent: function() {
+		return this.map(function() {
+			var offsetParent = this.offsetParent || document.body;
+			while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+				offsetParent = offsetParent.offsetParent;
+			}
+			return offsetParent || document.body;
+		});
+	}
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+	var top = /Y/.test( prop );
+
+	jQuery.fn[ method ] = function( val ) {
+		return jQuery.access( this, function( elem, method, val ) {
+			var win = getWindow( elem );
+
+			if ( val === undefined ) {
+				return win ? (prop in win) ? win[ prop ] :
+					win.document.documentElement[ method ] :
+					elem[ method ];
+			}
+
+			if ( win ) {
+				win.scrollTo(
+					!top ? val : jQuery( win ).scrollLeft(),
+					 top ? val : jQuery( win ).scrollTop()
+				);
+
+			} else {
+				elem[ method ] = val;
+			}
+		}, method, val, arguments.length, null );
+	};
+});
+
+function getWindow( elem ) {
+	return jQuery.isWindow( elem ) ?
+		elem :
+		elem.nodeType === 9 ?
+			elem.defaultView || elem.parentWindow :
+			false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+		// margin is only for outerHeight, outerWidth
+		jQuery.fn[ funcName ] = function( margin, value ) {
+			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+			return jQuery.access( this, function( elem, type, value ) {
+				var doc;
+
+				if ( jQuery.isWindow( elem ) ) {
+					// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+					// isn't a whole lot we can do. See pull request at this URL for discussion:
+					// https://github.com/jquery/jquery/pull/764
+					return elem.document.documentElement[ "client" + name ];
+				}
+
+				// Get document width or height
+				if ( elem.nodeType === 9 ) {
+					doc = elem.documentElement;
+
+					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+					// unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+					return Math.max(
+						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+						elem.body[ "offset" + name ], doc[ "offset" + name ],
+						doc[ "client" + name ]
+					);
+				}
+
+				return value === undefined ?
+					// Get width or height on the element, requesting but not forcing parseFloat
+					jQuery.css( elem, type, value, extra ) :
+
+					// Set width or height on the element
+					jQuery.style( elem, type, value, extra );
+			}, type, chainable ? margin : undefined, chainable, null );
+		};
+	});
+});
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+	define( "jquery", [], function () { return jQuery; } );
+}
+
+})( window );
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.accordion.css b/web-app/js/jquery-ui-menubar/jquery.ui.accordion.css
new file mode 100644
index 0000000..60975ac
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.accordion.css
@@ -0,0 +1,16 @@
+/*!
+ * jQuery UI Accordion @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+.ui-accordion .ui-accordion-header { display: block; cursor: pointer; position: relative; margin-top: 2px; padding: .5em .5em .5em .7em; zoom: 1; }
+.ui-accordion .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-noicons { padding-left: .7em; }
+.ui-accordion .ui-accordion-icons .ui-accordion-icons { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-accordion-header-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; overflow: auto; zoom: 1; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.all.css b/web-app/js/jquery-ui-menubar/jquery.ui.all.css
new file mode 100644
index 0000000..c92e2a5
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.all.css
@@ -0,0 +1,12 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import "jquery.ui.base.css";
+ at import "jquery.ui.theme.css";
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.autocomplete.css b/web-app/js/jquery-ui-menubar/jquery.ui.autocomplete.css
new file mode 100644
index 0000000..05ae310
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.autocomplete.css
@@ -0,0 +1,18 @@
+/*!
+ * jQuery UI Autocomplete @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete {
+	position: absolute;
+	top: 0; /* #8656 */
+	cursor: default;
+}
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.base.css b/web-app/js/jquery-ui-menubar/jquery.ui.base.css
new file mode 100644
index 0000000..916f0b5
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.base.css
@@ -0,0 +1,26 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming
+ */
+ at import url("jquery.ui.core.css");
+
+ at import url("jquery.ui.accordion.css");
+ at import url("jquery.ui.autocomplete.css");
+ at import url("jquery.ui.button.css");
+ at import url("jquery.ui.datepicker.css");
+ at import url("jquery.ui.dialog.css");
+ at import url("jquery.ui.menu.css");
+ at import url("jquery.ui.menubar.css");
+ at import url("jquery.ui.progressbar.css");
+ at import url("jquery.ui.resizable.css");
+ at import url("jquery.ui.selectable.css");
+ at import url("jquery.ui.slider.css");
+ at import url("jquery.ui.spinner.css");
+ at import url("jquery.ui.tabs.css");
+ at import url("jquery.ui.tooltip.css");
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.button.css b/web-app/js/jquery-ui-menubar/jquery.ui.button.css
new file mode 100644
index 0000000..1faeff6
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.button.css
@@ -0,0 +1,40 @@
+/*!
+ * jQuery UI Button @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button, .ui-button:link, .ui-button:visited, .ui-button:hover, .ui-button:active { text-decoration: none; }
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; } 
+button.ui-button-icons-only { width: 3.7em; } 
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.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; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.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; }
+.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; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.button.js b/web-app/js/jquery-ui-menubar/jquery.ui.button.js
new file mode 100644
index 0000000..5ae5264
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.button.js
@@ -0,0 +1,415 @@
+/*!
+ * jQuery UI Button @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/button/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var lastActive, startXPos, startYPos, clickDragged,
+	baseClasses = "ui-button ui-widget ui-state-default ui-corner-all",
+	stateClasses = "ui-state-hover ui-state-active ",
+	typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",
+	formResetHandler = function() {
+		var buttons = $( this ).find( ":ui-button" );
+		setTimeout(function() {
+			buttons.button( "refresh" );
+		}, 1 );
+	},
+	radioGroup = function( radio ) {
+		var name = radio.name,
+			form = radio.form,
+			radios = $( [] );
+		if ( name ) {
+			if ( form ) {
+				radios = $( form ).find( "[name='" + name + "']" );
+			} else {
+				radios = $( "[name='" + name + "']", radio.ownerDocument )
+					.filter(function() {
+						return !this.form;
+					});
+			}
+		}
+		return radios;
+	};
+
+$.widget( "ui.button", {
+	version: "@VERSION",
+	defaultElement: "<button>",
+	options: {
+		disabled: null,
+		text: true,
+		label: null,
+		icons: {
+			primary: null,
+			secondary: null
+		}
+	},
+	_create: function() {
+		this.element.closest( "form" )
+			.unbind( "reset" + this.eventNamespace )
+			.bind( "reset" + this.eventNamespace, formResetHandler );
+
+		if ( typeof this.options.disabled !== "boolean" ) {
+			this.options.disabled = !!this.element.prop( "disabled" );
+		} else {
+			this.element.prop( "disabled", this.options.disabled );
+		}
+
+		this._determineButtonType();
+		this.hasTitle = !!this.buttonElement.attr( "title" );
+
+		var that = this,
+			options = this.options,
+			toggleButton = this.type === "checkbox" || this.type === "radio",
+			hoverClass = "ui-state-hover" + ( !toggleButton ? " ui-state-active" : "" ),
+			focusClass = "ui-state-focus";
+
+		if ( options.label === null ) {
+			options.label = (this.type === "input" ? this.buttonElement.val() : this.buttonElement.html());
+		}
+
+		this.buttonElement
+			.addClass( baseClasses )
+			.attr( "role", "button" )
+			.bind( "mouseenter" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).addClass( "ui-state-hover" );
+				if ( this === lastActive ) {
+					$( this ).addClass( "ui-state-active" );
+				}
+			})
+			.bind( "mouseleave" + this.eventNamespace, function() {
+				if ( options.disabled ) {
+					return;
+				}
+				$( this ).removeClass( hoverClass );
+			})
+			.bind( "click" + this.eventNamespace, function( event ) {
+				if ( options.disabled ) {
+					event.preventDefault();
+					event.stopImmediatePropagation();
+				}
+			});
+
+		this.element
+			.bind( "focus" + this.eventNamespace, function() {
+				// no need to check disabled, focus won't be triggered anyway
+				that.buttonElement.addClass( focusClass );
+			})
+			.bind( "blur" + this.eventNamespace, function() {
+				that.buttonElement.removeClass( focusClass );
+			});
+
+		if ( toggleButton ) {
+			this.element.bind( "change" + this.eventNamespace, function() {
+				if ( clickDragged ) {
+					return;
+				}
+				that.refresh();
+			});
+			// if mouse moves between mousedown and mouseup (drag) set clickDragged flag
+			// prevents issue where button state changes but checkbox/radio checked state
+			// does not in Firefox (see ticket #6970)
+			this.buttonElement
+				.bind( "mousedown" + this.eventNamespace, function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					clickDragged = false;
+					startXPos = event.pageX;
+					startYPos = event.pageY;
+				})
+				.bind( "mouseup" + this.eventNamespace, function( event ) {
+					if ( options.disabled ) {
+						return;
+					}
+					if ( startXPos !== event.pageX || startYPos !== event.pageY ) {
+						clickDragged = true;
+					}
+			});
+		}
+
+		if ( this.type === "checkbox" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).toggleClass( "ui-state-active" );
+				that.buttonElement.attr( "aria-pressed", that.element[0].checked );
+			});
+		} else if ( this.type === "radio" ) {
+			this.buttonElement.bind( "click" + this.eventNamespace, function() {
+				if ( options.disabled || clickDragged ) {
+					return false;
+				}
+				$( this ).addClass( "ui-state-active" );
+				that.buttonElement.attr( "aria-pressed", "true" );
+
+				var radio = that.element[ 0 ];
+				radioGroup( radio )
+					.not( radio )
+					.map(function() {
+						return $( this ).button( "widget" )[ 0 ];
+					})
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			});
+		} else {
+			this.buttonElement
+				.bind( "mousedown" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).addClass( "ui-state-active" );
+					lastActive = this;
+					that.document.one( "mouseup", function() {
+						lastActive = null;
+					});
+				})
+				.bind( "mouseup" + this.eventNamespace, function() {
+					if ( options.disabled ) {
+						return false;
+					}
+					$( this ).removeClass( "ui-state-active" );
+				})
+				.bind( "keydown" + this.eventNamespace, function(event) {
+					if ( options.disabled ) {
+						return false;
+					}
+					if ( event.keyCode === $.ui.keyCode.SPACE || event.keyCode === $.ui.keyCode.ENTER ) {
+						$( this ).addClass( "ui-state-active" );
+					}
+				})
+				.bind( "keyup" + this.eventNamespace, function() {
+					$( this ).removeClass( "ui-state-active" );
+				});
+
+			if ( this.buttonElement.is("a") ) {
+				this.buttonElement.keyup(function(event) {
+					if ( event.keyCode === $.ui.keyCode.SPACE ) {
+						// TODO pass through original event correctly (just as 2nd argument doesn't work)
+						$( this ).click();
+					}
+				});
+			}
+		}
+
+		// TODO: pull out $.Widget's handling for the disabled option into
+		// $.Widget.prototype._setOptionDisabled so it's easy to proxy and can
+		// be overridden by individual plugins
+		this._setOption( "disabled", options.disabled );
+		this._resetButton();
+	},
+
+	_determineButtonType: function() {
+		var ancestor, labelSelector, checked;
+
+		if ( this.element.is("[type=checkbox]") ) {
+			this.type = "checkbox";
+		} else if ( this.element.is("[type=radio]") ) {
+			this.type = "radio";
+		} else if ( this.element.is("input") ) {
+			this.type = "input";
+		} else {
+			this.type = "button";
+		}
+
+		if ( this.type === "checkbox" || this.type === "radio" ) {
+			// we don't search against the document in case the element
+			// is disconnected from the DOM
+			ancestor = this.element.parents().last();
+			labelSelector = "label[for='" + this.element.attr("id") + "']";
+			this.buttonElement = ancestor.find( labelSelector );
+			if ( !this.buttonElement.length ) {
+				ancestor = ancestor.length ? ancestor.siblings() : this.element.siblings();
+				this.buttonElement = ancestor.filter( labelSelector );
+				if ( !this.buttonElement.length ) {
+					this.buttonElement = ancestor.find( labelSelector );
+				}
+			}
+			this.element.addClass( "ui-helper-hidden-accessible" );
+
+			checked = this.element.is( ":checked" );
+			if ( checked ) {
+				this.buttonElement.addClass( "ui-state-active" );
+			}
+			this.buttonElement.prop( "aria-pressed", checked );
+		} else {
+			this.buttonElement = this.element;
+		}
+	},
+
+	widget: function() {
+		return this.buttonElement;
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass( "ui-helper-hidden-accessible" );
+		this.buttonElement
+			.removeClass( baseClasses + " " + stateClasses + " " + typeClasses )
+			.removeAttr( "role" )
+			.removeAttr( "aria-pressed" )
+			.html( this.buttonElement.find(".ui-button-text").html() );
+
+		if ( !this.hasTitle ) {
+			this.buttonElement.removeAttr( "title" );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		this._super( key, value );
+		if ( key === "disabled" ) {
+			if ( value ) {
+				this.element.prop( "disabled", true );
+			} else {
+				this.element.prop( "disabled", false );
+			}
+			return;
+		}
+		this._resetButton();
+	},
+
+	refresh: function() {
+		var isDisabled = this.element.is( ":disabled" );
+		if ( isDisabled !== this.options.disabled ) {
+			this._setOption( "disabled", isDisabled );
+		}
+		if ( this.type === "radio" ) {
+			radioGroup( this.element[0] ).each(function() {
+				if ( $( this ).is( ":checked" ) ) {
+					$( this ).button( "widget" )
+						.addClass( "ui-state-active" )
+						.attr( "aria-pressed", "true" );
+				} else {
+					$( this ).button( "widget" )
+						.removeClass( "ui-state-active" )
+						.attr( "aria-pressed", "false" );
+				}
+			});
+		} else if ( this.type === "checkbox" ) {
+			if ( this.element.is( ":checked" ) ) {
+				this.buttonElement
+					.addClass( "ui-state-active" )
+					.attr( "aria-pressed", "true" );
+			} else {
+				this.buttonElement
+					.removeClass( "ui-state-active" )
+					.attr( "aria-pressed", "false" );
+			}
+		}
+	},
+
+	_resetButton: function() {
+		if ( this.type === "input" ) {
+			if ( this.options.label ) {
+				this.element.val( this.options.label );
+			}
+			return;
+		}
+		var buttonElement = this.buttonElement.removeClass( typeClasses ),
+			buttonText = $( "<span></span>", this.document[0] )
+				.addClass( "ui-button-text" )
+				.html( this.options.label )
+				.appendTo( buttonElement.empty() )
+				.text(),
+			icons = this.options.icons,
+			multipleIcons = icons.primary && icons.secondary,
+			buttonClasses = [];
+
+		if ( icons.primary || icons.secondary ) {
+			if ( this.options.text ) {
+				buttonClasses.push( "ui-button-text-icon" + ( multipleIcons ? "s" : ( icons.primary ? "-primary" : "-secondary" ) ) );
+			}
+
+			if ( icons.primary ) {
+				buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" );
+			}
+
+			if ( icons.secondary ) {
+				buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" );
+			}
+
+			if ( !this.options.text ) {
+				buttonClasses.push( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" );
+
+				if ( !this.hasTitle ) {
+					buttonElement.attr( "title", $.trim( buttonText ) );
+				}
+			}
+		} else {
+			buttonClasses.push( "ui-button-text-only" );
+		}
+		buttonElement.addClass( buttonClasses.join( " " ) );
+	}
+});
+
+$.widget( "ui.buttonset", {
+	version: "@VERSION",
+	options: {
+		items: "button, input[type=button], input[type=submit], input[type=reset], input[type=checkbox], input[type=radio], a, :data(button)"
+	},
+
+	_create: function() {
+		this.element.addClass( "ui-buttonset" );
+	},
+
+	_init: function() {
+		this.refresh();
+	},
+
+	_setOption: function( key, value ) {
+		if ( key === "disabled" ) {
+			this.buttons.button( "option", key, value );
+		}
+
+		this._super( key, value );
+	},
+
+	refresh: function() {
+		var rtl = this.element.css( "direction" ) === "rtl";
+
+		this.buttons = this.element.find( this.options.items )
+			.filter( ":ui-button" )
+				.button( "refresh" )
+			.end()
+			.not( ":ui-button" )
+				.button()
+			.end()
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-all ui-corner-left ui-corner-right" )
+				.filter( ":first" )
+					.addClass( rtl ? "ui-corner-right" : "ui-corner-left" )
+				.end()
+				.filter( ":last" )
+					.addClass( rtl ? "ui-corner-left" : "ui-corner-right" )
+				.end()
+			.end();
+	},
+
+	_destroy: function() {
+		this.element.removeClass( "ui-buttonset" );
+		this.buttons
+			.map(function() {
+				return $( this ).button( "widget" )[ 0 ];
+			})
+				.removeClass( "ui-corner-left ui-corner-right" )
+			.end()
+			.button( "destroy" );
+	}
+});
+
+}( jQuery ) );
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.core.css b/web-app/js/jquery-ui-menubar/jquery.ui.core.css
new file mode 100644
index 0000000..8cf4d02
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.core.css
@@ -0,0 +1,39 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.core.js b/web-app/js/jquery-ui-menubar/jquery.ui.core.js
new file mode 100644
index 0000000..e569eea
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.core.js
@@ -0,0 +1,343 @@
+/*!
+ * jQuery UI Core @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/ui-core/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	runiqueId = /^ui-id-\d+$/;
+
+// prevent duplicate loading
+// this is only a problem because we proxy existing functions
+// and we don't want to double proxy them
+$.ui = $.ui || {};
+if ( $.ui.version ) {
+	return;
+}
+
+$.extend( $.ui, {
+	version: "@VERSION",
+
+	keyCode: {
+		BACKSPACE: 8,
+		COMMA: 188,
+		DELETE: 46,
+		DOWN: 40,
+		END: 35,
+		ENTER: 13,
+		ESCAPE: 27,
+		HOME: 36,
+		LEFT: 37,
+		NUMPAD_ADD: 107,
+		NUMPAD_DECIMAL: 110,
+		NUMPAD_DIVIDE: 111,
+		NUMPAD_ENTER: 108,
+		NUMPAD_MULTIPLY: 106,
+		NUMPAD_SUBTRACT: 109,
+		PAGE_DOWN: 34,
+		PAGE_UP: 33,
+		PERIOD: 190,
+		RIGHT: 39,
+		SPACE: 32,
+		TAB: 9,
+		UP: 38
+	}
+});
+
+// plugins
+$.fn.extend({
+	_focus: $.fn.focus,
+	focus: function( delay, fn ) {
+		return typeof delay === "number" ?
+			this.each(function() {
+				var elem = this;
+				setTimeout(function() {
+					$( elem ).focus();
+					if ( fn ) {
+						fn.call( elem );
+					}
+				}, delay );
+			}) :
+			this._focus.apply( this, arguments );
+	},
+
+	scrollParent: function() {
+		var scrollParent;
+		if (($.ui.ie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) {
+			scrollParent = this.parents().filter(function() {
+				return (/(relative|absolute|fixed)/).test($.css(this,'position')) && (/(auto|scroll)/).test($.css(this,'overflow')+$.css(this,'overflow-y')+$.css(this,'overflow-x'));
+			}).eq(0);
+		} else {
+			scrollParent = this.parents().filter(function() {
+				return (/(auto|scroll)/).test($.css(this,'overflow')+$.css(this,'overflow-y')+$.css(this,'overflow-x'));
+			}).eq(0);
+		}
+
+		return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent;
+	},
+
+	zIndex: function( zIndex ) {
+		if ( zIndex !== undefined ) {
+			return this.css( "zIndex", zIndex );
+		}
+
+		if ( this.length ) {
+			var elem = $( this[ 0 ] ), position, value;
+			while ( elem.length && elem[ 0 ] !== document ) {
+				// Ignore z-index if position is set to a value where z-index is ignored by the browser
+				// This makes behavior of this function consistent across browsers
+				// WebKit always returns auto if the element is positioned
+				position = elem.css( "position" );
+				if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+					// IE returns 0 when zIndex is not specified
+					// other browsers return a string
+					// we ignore the case of nested elements with an explicit value of 0
+					// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+					value = parseInt( elem.css( "zIndex" ), 10 );
+					if ( !isNaN( value ) && value !== 0 ) {
+						return value;
+					}
+				}
+				elem = elem.parent();
+			}
+		}
+
+		return 0;
+	},
+
+	uniqueId: function() {
+		return this.each(function() {
+			if ( !this.id ) {
+				this.id = "ui-id-" + (++uuid);
+			}
+		});
+	},
+
+	removeUniqueId: function() {
+		return this.each(function() {
+			if ( runiqueId.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		});
+	}
+});
+
+// support: jQuery <1.8
+if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
+	$.each( [ "Width", "Height" ], function( i, name ) {
+		var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
+			type = name.toLowerCase(),
+			orig = {
+				innerWidth: $.fn.innerWidth,
+				innerHeight: $.fn.innerHeight,
+				outerWidth: $.fn.outerWidth,
+				outerHeight: $.fn.outerHeight
+			};
+
+		function reduce( elem, size, border, margin ) {
+			$.each( side, function() {
+				size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
+				if ( border ) {
+					size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
+				}
+				if ( margin ) {
+					size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
+				}
+			});
+			return size;
+		}
+
+		$.fn[ "inner" + name ] = function( size ) {
+			if ( size === undefined ) {
+				return orig[ "inner" + name ].call( this );
+			}
+
+			return this.each(function() {
+				$( this ).css( type, reduce( this, size ) + "px" );
+			});
+		};
+
+		$.fn[ "outer" + name] = function( size, margin ) {
+			if ( typeof size !== "number" ) {
+				return orig[ "outer" + name ].call( this, size );
+			}
+
+			return this.each(function() {
+				$( this).css( type, reduce( this, size, true, margin ) + "px" );
+			});
+		};
+	});
+}
+
+// selectors
+function focusable( element, isTabIndexNotNaN ) {
+	var map, mapName, img,
+		nodeName = element.nodeName.toLowerCase();
+	if ( "area" === nodeName ) {
+		map = element.parentNode;
+		mapName = map.name;
+		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
+			return false;
+		}
+		img = $( "img[usemap=#" + mapName + "]" )[0];
+		return !!img && visible( img );
+	}
+	return ( /input|select|textarea|button|object/.test( nodeName ) ?
+		!element.disabled :
+		"a" === nodeName ?
+			element.href || isTabIndexNotNaN :
+			isTabIndexNotNaN) &&
+		// the element and all of its ancestors must be visible
+		visible( element );
+}
+
+function visible( element ) {
+	return !$( element ).parents().andSelf().filter(function() {
+		return $.css( this, "visibility" ) === "hidden" ||
+			$.expr.filters.hidden( this );
+	}).length;
+}
+
+$.extend( $.expr[ ":" ], {
+	data: $.expr.createPseudo ?
+		$.expr.createPseudo(function( dataName ) {
+			return function( elem ) {
+				return !!$.data( elem, dataName );
+			};
+		}) :
+		// support: jQuery <1.8
+		function( elem, i, match ) {
+			return !!$.data( elem, match[ 3 ] );
+		},
+
+	focusable: function( element ) {
+		return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
+	},
+
+	tabbable: function( element ) {
+		var tabIndex = $.attr( element, "tabindex" ),
+			isTabIndexNaN = isNaN( tabIndex );
+		return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
+	}
+});
+
+// support
+$(function() {
+	var body = document.body,
+		div = body.appendChild( div = document.createElement( "div" ) );
+
+	// access offsetHeight before setting the style to prevent a layout bug
+	// in IE 9 which causes the element to continue to take up space even
+	// after it is removed from the DOM (#8026)
+	div.offsetHeight;
+
+	$.extend( div.style, {
+		minHeight: "100px",
+		height: "auto",
+		padding: 0,
+		borderWidth: 0
+	});
+
+	$.support.minHeight = div.offsetHeight === 100;
+	$.support.selectstart = "onselectstart" in div;
+
+	// set display to none to avoid a layout bug in IE
+	// http://dev.jquery.com/ticket/4014
+	body.removeChild( div ).style.display = "none";
+});
+
+
+
+
+
+// deprecated
+
+(function() {
+	var uaMatch = /msie ([\w.]+)/.exec( navigator.userAgent.toLowerCase() ) || [];
+	$.ui.ie = uaMatch.length ? true : false;
+	$.ui.ie6 = parseFloat( uaMatch[ 1 ], 10 ) === 6;
+})();
+
+$.fn.extend({
+	disableSelection: function() {
+		return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
+			".ui-disableSelection", function( event ) {
+				event.preventDefault();
+			});
+	},
+
+	enableSelection: function() {
+		return this.unbind( ".ui-disableSelection" );
+	}
+});
+
+$.extend( $.ui, {
+	// $.ui.plugin is deprecated.  Use the proxy pattern instead.
+	plugin: {
+		add: function( module, option, set ) {
+			var i,
+				proto = $.ui[ module ].prototype;
+			for ( i in set ) {
+				proto.plugins[ i ] = proto.plugins[ i ] || [];
+				proto.plugins[ i ].push( [ option, set[ i ] ] );
+			}
+		},
+		call: function( instance, name, args ) {
+			var i,
+				set = instance.plugins[ name ];
+			if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {
+				return;
+			}
+
+			for ( i = 0; i < set.length; i++ ) {
+				if ( instance.options[ set[ i ][ 0 ] ] ) {
+					set[ i ][ 1 ].apply( instance.element, args );
+				}
+			}
+		}
+	},
+
+	contains: $.contains,
+
+	// only used by resizable
+	hasScroll: function( el, a ) {
+
+		//If overflow is hidden, the element might have extra content, but the user wants to hide it
+		if ( $( el ).css( "overflow" ) === "hidden") {
+			return false;
+		}
+
+		var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
+			has = false;
+
+		if ( el[ scroll ] > 0 ) {
+			return true;
+		}
+
+		// TODO: determine which cases actually cause this to happen
+		// if the element doesn't have the scroll set, see if it's possible to
+		// set the scroll
+		el[ scroll ] = 1;
+		has = ( el[ scroll ] > 0 );
+		el[ scroll ] = 0;
+		return has;
+	},
+
+	// these are odd functions, fix the API or move into individual plugins
+	isOverAxis: function( x, reference, size ) {
+		//Determines when x coordinate is over "b" element axis
+		return ( x > reference ) && ( x < ( reference + size ) );
+	},
+	isOver: function( y, x, top, left, height, width ) {
+		//Determines when x, y coordinates is over "b" element
+		return $.ui.isOverAxis( y, top, height ) && $.ui.isOverAxis( x, left, width );
+	}
+});
+
+})( jQuery );
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.datepicker.css b/web-app/js/jquery-ui-menubar/jquery.ui.datepicker.css
new file mode 100644
index 0000000..c1f2699
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.datepicker.css
@@ -0,0 +1,67 @@
+/*!
+ * jQuery UI Datepicker @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.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;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.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; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.dialog.css b/web-app/js/jquery-ui-menubar/jquery.ui.dialog.css
new file mode 100644
index 0000000..2937af9
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.dialog.css
@@ -0,0 +1,22 @@
+/*!
+ * jQuery UI Dialog @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.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; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.dialog.js b/web-app/js/jquery-ui-menubar/jquery.ui.dialog.js
new file mode 100644
index 0000000..8593fff
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.dialog.js
@@ -0,0 +1,847 @@
+/*!
+ * jQuery UI Dialog @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/dialog/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *  jquery.ui.button.js
+ *	jquery.ui.draggable.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.position.js
+ *	jquery.ui.resizable.js
+ */
+(function( $, undefined ) {
+
+var uiDialogClasses = "ui-dialog ui-widget ui-widget-content ui-corner-all ",
+	sizeRelatedOptions = {
+		buttons: true,
+		height: true,
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true,
+		width: true
+	},
+	resizableRelatedOptions = {
+		maxHeight: true,
+		maxWidth: true,
+		minHeight: true,
+		minWidth: true
+	};
+
+$.widget("ui.dialog", {
+	version: "@VERSION",
+	options: {
+		autoOpen: true,
+		buttons: {},
+		closeOnEscape: true,
+		closeText: "close",
+		dialogClass: "",
+		draggable: true,
+		hide: null,
+		height: "auto",
+		maxHeight: false,
+		maxWidth: false,
+		minHeight: 150,
+		minWidth: 150,
+		modal: false,
+		position: {
+			my: "center",
+			at: "center",
+			of: window,
+			collision: "fit",
+			// ensure that the titlebar is never outside the document
+			using: function( pos ) {
+				var topOffset = $( this ).css( pos ).offset().top;
+				if ( topOffset < 0 ) {
+					$( this ).css( "top", pos.top - topOffset );
+				}
+			}
+		},
+		resizable: true,
+		show: null,
+		stack: true,
+		title: "",
+		width: 300,
+		zIndex: 1000
+	},
+
+	_create: function() {
+		this.originalTitle = this.element.attr( "title" );
+		// #5742 - .attr() might return a DOMElement
+		if ( typeof this.originalTitle !== "string" ) {
+			this.originalTitle = "";
+		}
+		this.oldPosition = {
+			parent: this.element.parent(),
+			index: this.element.parent().children().index( this.element )
+		};
+		this.options.title = this.options.title || this.originalTitle;
+		var that = this,
+			options = this.options,
+
+			title = options.title || " ",
+
+			uiDialog = ( this.uiDialog = $( "<div>" ) )
+				.addClass( uiDialogClasses + options.dialogClass )
+				.css({
+					display: "none",
+					outline: 0, // TODO: move to stylesheet
+					zIndex: options.zIndex
+				})
+				// setting tabIndex makes the div focusable
+				.attr( "tabIndex", -1)
+				.keydown(function( event ) {
+					if ( options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+							event.keyCode === $.ui.keyCode.ESCAPE ) {
+						that.close( event );
+						event.preventDefault();
+					}
+				})
+				.mousedown(function( event ) {
+					that.moveToTop( false, event );
+				})
+				.appendTo( "body" ),
+
+			uiDialogContent = this.element
+				.show()
+				.removeAttr( "title" )
+				.addClass( "ui-dialog-content ui-widget-content" )
+				.appendTo( uiDialog ),
+
+			uiDialogTitlebar = ( this.uiDialogTitlebar = $( "<div>" ) )
+				.addClass( "ui-dialog-titlebar  ui-widget-header  " +
+					"ui-corner-all  ui-helper-clearfix" )
+				.prependTo( uiDialog ),
+
+			uiDialogTitlebarClose = $( "<a href='#'></a>" )
+				.addClass( "ui-dialog-titlebar-close  ui-corner-all" )
+				.attr( "role", "button" )
+				.click(function( event ) {
+					event.preventDefault();
+					that.close( event );
+				})
+				.appendTo( uiDialogTitlebar ),
+
+			uiDialogTitlebarCloseText = ( this.uiDialogTitlebarCloseText = $( "<span>" ) )
+				.addClass( "ui-icon ui-icon-closethick" )
+				.text( options.closeText )
+				.appendTo( uiDialogTitlebarClose ),
+
+			uiDialogTitle = $( "<span>" )
+				.uniqueId()
+				.addClass( "ui-dialog-title" )
+				.html( title )
+				.prependTo( uiDialogTitlebar ),
+
+			uiDialogButtonPane = ( this.uiDialogButtonPane = $( "<div>" ) )
+				.addClass( "ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" ),
+
+			uiButtonSet = ( this.uiButtonSet = $( "<div>" ) )
+				.addClass( "ui-dialog-buttonset" )
+				.appendTo( uiDialogButtonPane );
+
+		uiDialog.attr({
+			role: "dialog",
+			"aria-labelledby": uiDialogTitle.attr( "id" )
+		});
+
+		uiDialogTitlebar.find( "*" ).add( uiDialogTitlebar ).disableSelection();
+		this._hoverable( uiDialogTitlebarClose );
+		this._focusable( uiDialogTitlebarClose );
+
+		if ( options.draggable && $.fn.draggable ) {
+			this._makeDraggable();
+		}
+		if ( options.resizable && $.fn.resizable ) {
+			this._makeResizable();
+		}
+
+		this._createButtons( options.buttons );
+		this._isOpen = false;
+
+		if ( $.fn.bgiframe ) {
+			uiDialog.bgiframe();
+		}
+
+		// prevent tabbing out of modal dialogs
+		this._on( uiDialog, { keydown: function( event ) {
+			if ( !options.modal || event.keyCode !== $.ui.keyCode.TAB ) {
+				return;
+			}
+
+			var tabbables = $( ":tabbable", uiDialog ),
+				first = tabbables.filter( ":first" ),
+				last  = tabbables.filter( ":last" );
+
+			if ( event.target === last[0] && !event.shiftKey ) {
+				first.focus( 1 );
+				return false;
+			} else if ( event.target === first[0] && event.shiftKey ) {
+				last.focus( 1 );
+				return false;
+			}
+		}});
+	},
+
+	_init: function() {
+		if ( this.options.autoOpen ) {
+			this.open();
+		}
+	},
+
+	_destroy: function() {
+		var next,
+			oldPosition = this.oldPosition;
+
+		if ( this.overlay ) {
+			this.overlay.destroy();
+		}
+		this.uiDialog.hide();
+		this.element
+			.removeClass( "ui-dialog-content ui-widget-content" )
+			.hide()
+			.appendTo( "body" );
+		this.uiDialog.remove();
+
+		if ( this.originalTitle ) {
+			this.element.attr( "title", this.originalTitle );
+		}
+
+		next = oldPosition.parent.children().eq( oldPosition.index );
+		// Don't try to place the dialog next to itself (#8613)
+		if ( next.length && next[ 0 ] !== this.element[ 0 ] ) {
+			next.before( this.element );
+		} else {
+			oldPosition.parent.append( this.element );
+		}
+	},
+
+	widget: function() {
+		return this.uiDialog;
+	},
+
+	close: function( event ) {
+		var that = this,
+			maxZ, thisZ;
+
+		if ( !this._isOpen ) {
+			return;
+		}
+
+		if ( false === this._trigger( "beforeClose", event ) ) {
+			return;
+		}
+
+		this._isOpen = false;
+
+		if ( this.overlay ) {
+			this.overlay.destroy();
+		}
+
+		if ( this.options.hide ) {
+			this.uiDialog.hide( this.options.hide, function() {
+				that._trigger( "close", event );
+			});
+		} else {
+			this.uiDialog.hide();
+			this._trigger( "close", event );
+		}
+
+		$.ui.dialog.overlay.resize();
+
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		if ( this.options.modal ) {
+			maxZ = 0;
+			$( ".ui-dialog" ).each(function() {
+				if ( this !== that.uiDialog[0] ) {
+					thisZ = $( this ).css( "z-index" );
+					if ( !isNaN( thisZ ) ) {
+						maxZ = Math.max( maxZ, thisZ );
+					}
+				}
+			});
+			$.ui.dialog.maxZ = maxZ;
+		}
+
+		return this;
+	},
+
+	isOpen: function() {
+		return this._isOpen;
+	},
+
+	// the force parameter allows us to move modal dialogs to their correct
+	// position on open
+	moveToTop: function( force, event ) {
+		var options = this.options,
+			saveScroll;
+
+		if ( ( options.modal && !force ) ||
+				( !options.stack && !options.modal ) ) {
+			return this._trigger( "focus", event );
+		}
+
+		if ( options.zIndex > $.ui.dialog.maxZ ) {
+			$.ui.dialog.maxZ = options.zIndex;
+		}
+		if ( this.overlay ) {
+			$.ui.dialog.maxZ += 1;
+			$.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ;
+			this.overlay.$el.css( "z-index", $.ui.dialog.overlay.maxZ );
+		}
+
+		// Save and then restore scroll
+		// Opera 9.5+ resets when parent z-index is changed.
+		// http://bugs.jqueryui.com/ticket/3193
+		saveScroll = {
+			scrollTop: this.element.scrollTop(),
+			scrollLeft: this.element.scrollLeft()
+		};
+		$.ui.dialog.maxZ += 1;
+		this.uiDialog.css( "z-index", $.ui.dialog.maxZ );
+		this.element.attr( saveScroll );
+		this._trigger( "focus", event );
+
+		return this;
+	},
+
+	open: function() {
+		if ( this._isOpen ) {
+			return;
+		}
+
+		var hasFocus,
+			options = this.options,
+			uiDialog = this.uiDialog;
+
+		this._size();
+		this._position( options.position );
+		uiDialog.show( options.show );
+		this.overlay = options.modal ? new $.ui.dialog.overlay( this ) : null;
+		this.moveToTop( true );
+
+		// set focus to the first tabbable element in the content area or the first button
+		// if there are no tabbable elements, set focus on the dialog itself
+		hasFocus = this.element.find( ":tabbable" );
+		if ( !hasFocus.length ) {
+			hasFocus = this.uiDialogButtonPane.find( ":tabbable" );
+			if ( !hasFocus.length ) {
+				hasFocus = uiDialog;
+			}
+		}
+		hasFocus.eq( 0 ).focus();
+
+		this._isOpen = true;
+		this._trigger( "open" );
+
+		return this;
+	},
+
+	_createButtons: function( buttons ) {
+		var uiDialogButtonPane, uiButtonSet,
+			that = this,
+			hasButtons = false;
+
+		// if we already have a button pane, remove it
+		this.uiDialogButtonPane.remove();
+		this.uiButtonSet.empty();
+
+		if ( typeof buttons === "object" && buttons !== null ) {
+			$.each( buttons, function() {
+				return !(hasButtons = true);
+			});
+		}
+		if ( hasButtons ) {
+			$.each( buttons, function( name, props ) {
+				props = $.isFunction( props ) ?
+					{ click: props, text: name } :
+					props;
+				var button = $( "<button type='button'></button>" )
+					.attr( props, true )
+					.unbind( "click" )
+					.click(function() {
+						props.click.apply( that.element[0], arguments );
+					})
+					.appendTo( that.uiButtonSet );
+				if ( $.fn.button ) {
+					button.button();
+				}
+			});
+			this.uiDialog.addClass( "ui-dialog-buttons" );
+			this.uiDialogButtonPane.appendTo( this.uiDialog );
+		} else {
+			this.uiDialog.removeClass( "ui-dialog-buttons" );
+		}
+	},
+
+	_makeDraggable: function() {
+		var that = this,
+			options = this.options;
+
+		function filteredUi( ui ) {
+			return {
+				position: ui.position,
+				offset: ui.offset
+			};
+		}
+
+		this.uiDialog.draggable({
+			cancel: ".ui-dialog-content, .ui-dialog-titlebar-close",
+			handle: ".ui-dialog-titlebar",
+			containment: "document",
+			start: function( event, ui ) {
+				$( this )
+					.addClass( "ui-dialog-dragging" );
+				that._trigger( "dragStart", event, filteredUi( ui ) );
+			},
+			drag: function( event, ui ) {
+				that._trigger( "drag", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				options.position = [
+					ui.position.left - that.document.scrollLeft(),
+					ui.position.top - that.document.scrollTop()
+				];
+				$( this )
+					.removeClass( "ui-dialog-dragging" );
+				that._trigger( "dragStop", event, filteredUi( ui ) );
+				$.ui.dialog.overlay.resize();
+			}
+		});
+	},
+
+	_makeResizable: function( handles ) {
+		handles = (handles === undefined ? this.options.resizable : handles);
+		var that = this,
+			options = this.options,
+			// .ui-resizable has position: relative defined in the stylesheet
+			// but dialogs have to use absolute or fixed positioning
+			position = this.uiDialog.css( "position" ),
+			resizeHandles = typeof handles === 'string' ?
+				handles	:
+				"n,e,s,w,se,sw,ne,nw";
+
+		function filteredUi( ui ) {
+			return {
+				originalPosition: ui.originalPosition,
+				originalSize: ui.originalSize,
+				position: ui.position,
+				size: ui.size
+			};
+		}
+
+		this.uiDialog.resizable({
+			cancel: ".ui-dialog-content",
+			containment: "document",
+			alsoResize: this.element,
+			maxWidth: options.maxWidth,
+			maxHeight: options.maxHeight,
+			minWidth: options.minWidth,
+			minHeight: this._minHeight(),
+			handles: resizeHandles,
+			start: function( event, ui ) {
+				$( this ).addClass( "ui-dialog-resizing" );
+				that._trigger( "resizeStart", event, filteredUi( ui ) );
+			},
+			resize: function( event, ui ) {
+				that._trigger( "resize", event, filteredUi( ui ) );
+			},
+			stop: function( event, ui ) {
+				$( this ).removeClass( "ui-dialog-resizing" );
+				options.height = $( this ).height();
+				options.width = $( this ).width();
+				that._trigger( "resizeStop", event, filteredUi( ui ) );
+				$.ui.dialog.overlay.resize();
+			}
+		})
+		.css( "position", position )
+		.find( ".ui-resizable-se" )
+			.addClass( "ui-icon ui-icon-grip-diagonal-se" );
+	},
+
+	_minHeight: function() {
+		var options = this.options;
+
+		if ( options.height === "auto" ) {
+			return options.minHeight;
+		} else {
+			return Math.min( options.minHeight, options.height );
+		}
+	},
+
+	_position: function( position ) {
+		var myAt = [],
+			offset = [ 0, 0 ],
+			isVisible;
+
+		if ( position ) {
+			// deep extending converts arrays to objects in jQuery <= 1.3.2 :-(
+	//		if (typeof position == 'string' || $.isArray(position)) {
+	//			myAt = $.isArray(position) ? position : position.split(' ');
+
+			if ( typeof position === "string" || (typeof position === "object" && "0" in position ) ) {
+				myAt = position.split ? position.split( " " ) : [ position[ 0 ], position[ 1 ] ];
+				if ( myAt.length === 1 ) {
+					myAt[ 1 ] = myAt[ 0 ];
+				}
+
+				$.each( [ "left", "top" ], function( i, offsetPosition ) {
+					if ( +myAt[ i ] === myAt[ i ] ) {
+						offset[ i ] = myAt[ i ];
+						myAt[ i ] = offsetPosition;
+					}
+				});
+
+				position = {
+					my: myAt.join( " " ),
+					at: myAt.join( " " ),
+					offset: offset.join( " " )
+				};
+			}
+
+			position = $.extend( {}, $.ui.dialog.prototype.options.position, position );
+		} else {
+			position = $.ui.dialog.prototype.options.position;
+		}
+
+		// need to show the dialog to get the actual offset in the position plugin
+		isVisible = this.uiDialog.is( ":visible" );
+		if ( !isVisible ) {
+			this.uiDialog.show();
+		}
+		this.uiDialog.position( position );
+		if ( !isVisible ) {
+			this.uiDialog.hide();
+		}
+	},
+
+	_setOptions: function( options ) {
+		var that = this,
+			resizableOptions = {},
+			resize = false;
+
+		$.each( options, function( key, value ) {
+			that._setOption( key, value );
+
+			if ( key in sizeRelatedOptions ) {
+				resize = true;
+			}
+			if ( key in resizableRelatedOptions ) {
+				resizableOptions[ key ] = value;
+			}
+		});
+
+		if ( resize ) {
+			this._size();
+		}
+		if ( this.uiDialog.is( ":data(resizable)" ) ) {
+			this.uiDialog.resizable( "option", resizableOptions );
+		}
+	},
+
+	_setOption: function( key, value ) {
+		var isDraggable, isResizable,
+			uiDialog = this.uiDialog;
+
+		switch ( key ) {
+			case "buttons":
+				this._createButtons( value );
+				break;
+			case "closeText":
+				// ensure that we always pass a string
+				this.uiDialogTitlebarCloseText.text( "" + value );
+				break;
+			case "dialogClass":
+				uiDialog
+					.removeClass( this.options.dialogClass )
+					.addClass( uiDialogClasses + value );
+				break;
+			case "disabled":
+				if ( value ) {
+					uiDialog.addClass( "ui-dialog-disabled" );
+				} else {
+					uiDialog.removeClass( "ui-dialog-disabled" );
+				}
+				break;
+			case "draggable":
+				isDraggable = uiDialog.is( ":data(draggable)" );
+				if ( isDraggable && !value ) {
+					uiDialog.draggable( "destroy" );
+				}
+
+				if ( !isDraggable && value ) {
+					this._makeDraggable();
+				}
+				break;
+			case "position":
+				this._position( value );
+				break;
+			case "resizable":
+				// currently resizable, becoming non-resizable
+				isResizable = uiDialog.is( ":data(resizable)" );
+				if ( isResizable && !value ) {
+					uiDialog.resizable( "destroy" );
+				}
+
+				// currently resizable, changing handles
+				if ( isResizable && typeof value === "string" ) {
+					uiDialog.resizable( "option", "handles", value );
+				}
+
+				// currently non-resizable, becoming resizable
+				if ( !isResizable && value !== false ) {
+					this._makeResizable( value );
+				}
+				break;
+			case "title":
+				// convert whatever was passed in o a string, for html() to not throw up
+				$( ".ui-dialog-title", this.uiDialogTitlebar )
+					.html( "" + ( value || " " ) );
+				break;
+		}
+
+		this._super( key, value );
+	},
+
+	_size: function() {
+		/* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content
+		 * divs will both have width and height set, so we need to reset them
+		 */
+		var nonContentHeight, minContentHeight, autoHeight,
+			options = this.options,
+			isVisible = this.uiDialog.is( ":visible" );
+
+		// reset content sizing
+		this.element.show().css({
+			width: "auto",
+			minHeight: 0,
+			height: 0
+		});
+
+		if ( options.minWidth > options.width ) {
+			options.width = options.minWidth;
+		}
+
+		// reset wrapper sizing
+		// determine the height of all the non-content elements
+		nonContentHeight = this.uiDialog.css({
+				height: "auto",
+				width: options.width
+			})
+			.outerHeight();
+		minContentHeight = Math.max( 0, options.minHeight - nonContentHeight );
+
+		if ( options.height === "auto" ) {
+			// only needed for IE6 support
+			if ( $.support.minHeight ) {
+				this.element.css({
+					minHeight: minContentHeight,
+					height: "auto"
+				});
+			} else {
+				this.uiDialog.show();
+				autoHeight = this.element.css( "height", "auto" ).height();
+				if ( !isVisible ) {
+					this.uiDialog.hide();
+				}
+				this.element.height( Math.max( autoHeight, minContentHeight ) );
+			}
+		} else {
+			this.element.height( Math.max( options.height - nonContentHeight, 0 ) );
+		}
+
+		if (this.uiDialog.is( ":data(resizable)" ) ) {
+			this.uiDialog.resizable( "option", "minHeight", this._minHeight() );
+		}
+	}
+});
+
+$.extend($.ui.dialog, {
+	uuid: 0,
+	maxZ: 0,
+
+	getTitleId: function($el) {
+		var id = $el.attr( "id" );
+		if ( !id ) {
+			this.uuid += 1;
+			id = this.uuid;
+		}
+		return "ui-dialog-title-" + id;
+	},
+
+	overlay: function( dialog ) {
+		this.$el = $.ui.dialog.overlay.create( dialog );
+	}
+});
+
+$.extend( $.ui.dialog.overlay, {
+	instances: [],
+	// reuse old instances due to IE memory leak with alpha transparency (see #5185)
+	oldInstances: [],
+	maxZ: 0,
+	events: $.map(
+		"focus,mousedown,mouseup,keydown,keypress,click".split( "," ),
+		function( event ) {
+			return event + ".dialog-overlay";
+		}
+	).join( " " ),
+	create: function( dialog ) {
+		if ( this.instances.length === 0 ) {
+			// prevent use of anchors and inputs
+			// we use a setTimeout in case the overlay is created from an
+			// event that we're going to be cancelling (see #2804)
+			setTimeout(function() {
+				// handle $(el).dialog().dialog('close') (see #4065)
+				if ( $.ui.dialog.overlay.instances.length ) {
+					$( document ).bind( $.ui.dialog.overlay.events, function( event ) {
+						// stop events if the z-index of the target is < the z-index of the overlay
+						// we cannot return true when we don't want to cancel the event (#3523)
+						if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) {
+							return false;
+						}
+					});
+				}
+			}, 1 );
+
+			// handle window resize
+			$( window ).bind( "resize.dialog-overlay", $.ui.dialog.overlay.resize );
+		}
+
+		var $el = ( this.oldInstances.pop() || $( "<div>" ).addClass( "ui-widget-overlay" ) );
+
+		// allow closing by pressing the escape key
+		$( document ).bind( "keydown.dialog-overlay", function( event ) {
+			var instances = $.ui.dialog.overlay.instances;
+			// only react to the event if we're the top overlay
+			if ( instances.length !== 0 && instances[ instances.length - 1 ] === $el &&
+				dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode &&
+				event.keyCode === $.ui.keyCode.ESCAPE ) {
+
+				dialog.close( event );
+				event.preventDefault();
+			}
+		});
+
+		$el.appendTo( document.body ).css({
+			width: this.width(),
+			height: this.height()
+		});
+
+		if ( $.fn.bgiframe ) {
+			$el.bgiframe();
+		}
+
+		this.instances.push( $el );
+		return $el;
+	},
+
+	destroy: function( $el ) {
+		var indexOf = $.inArray( $el, this.instances ),
+			maxZ = 0;
+
+		if ( indexOf !== -1 ) {
+			this.oldInstances.push( this.instances.splice( indexOf, 1 )[ 0 ] );
+		}
+
+		if ( this.instances.length === 0 ) {
+			$( [ document, window ] ).unbind( ".dialog-overlay" );
+		}
+
+		$el.height( 0 ).width( 0 ).remove();
+
+		// adjust the maxZ to allow other modal dialogs to continue to work (see #4309)
+		$.each( this.instances, function() {
+			maxZ = Math.max( maxZ, this.css( "z-index" ) );
+		});
+		this.maxZ = maxZ;
+	},
+
+	height: function() {
+		var scrollHeight,
+			offsetHeight;
+		// handle IE
+		if ( $.ui.ie ) {
+			scrollHeight = Math.max(
+				document.documentElement.scrollHeight,
+				document.body.scrollHeight
+			);
+			offsetHeight = Math.max(
+				document.documentElement.offsetHeight,
+				document.body.offsetHeight
+			);
+
+			if ( scrollHeight < offsetHeight ) {
+				return $( window ).height() + "px";
+			} else {
+				return scrollHeight + "px";
+			}
+		// handle "good" browsers
+		} else {
+			return $( document ).height() + "px";
+		}
+	},
+
+	width: function() {
+		var scrollWidth,
+			offsetWidth;
+		// handle IE
+		if ( $.ui.ie ) {
+			scrollWidth = Math.max(
+				document.documentElement.scrollWidth,
+				document.body.scrollWidth
+			);
+			offsetWidth = Math.max(
+				document.documentElement.offsetWidth,
+				document.body.offsetWidth
+			);
+
+			if ( scrollWidth < offsetWidth ) {
+				return $( window ).width() + "px";
+			} else {
+				return scrollWidth + "px";
+			}
+		// handle "good" browsers
+		} else {
+			return $( document ).width() + "px";
+		}
+	},
+
+	resize: function() {
+		/* If the dialog is draggable and the user drags it past the
+		 * right edge of the window, the document becomes wider so we
+		 * need to stretch the overlay. If the user then drags the
+		 * dialog back to the left, the document will become narrower,
+		 * so we need to shrink the overlay to the appropriate size.
+		 * This is handled by shrinking the overlay before setting it
+		 * to the full document size.
+		 */
+		var $overlays = $( [] );
+		$.each( $.ui.dialog.overlay.instances, function() {
+			$overlays = $overlays.add( this );
+		});
+
+		$overlays.css({
+			width: 0,
+			height: 0
+		}).css({
+			width: $.ui.dialog.overlay.width(),
+			height: $.ui.dialog.overlay.height()
+		});
+	}
+});
+
+$.extend( $.ui.dialog.overlay.prototype, {
+	destroy: function() {
+		$.ui.dialog.overlay.destroy( this.$el );
+	}
+});
+
+}( jQuery ) );
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.menu.css b/web-app/js/jquery-ui-menubar/jquery.ui.menu.css
new file mode 100644
index 0000000..ea4b24e
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.menu.css
@@ -0,0 +1,30 @@
+/*!
+ * jQuery UI Menu @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; }
+.ui-menu .ui-menu { margin-top: -3px; position: absolute; }
+.ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; }
+.ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; }
+.ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; }
+.ui-menu .ui-menu-item a.ui-state-focus,
+.ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; }
+
+.ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; }
+.ui-menu .ui-state-disabled a { cursor: default; }
+
+/* icon support */
+.ui-menu-icons { position: relative; }
+.ui-menu-icons .ui-menu-item a { position: relative; padding-left: 2em; }
+
+/* left-aligned */
+.ui-menu .ui-icon { position: absolute; top: .2em; left: .2em; }
+
+/* right-aligned */
+.ui-menu .ui-menu-icon { position: static; float: right; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.menu.js b/web-app/js/jquery-ui-menubar/jquery.ui.menu.js
new file mode 100644
index 0000000..59cb6d9
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.menu.js
@@ -0,0 +1,628 @@
+/*!
+ * jQuery UI Menu @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/menu/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ */
+(function( $, undefined ) {
+
+var mouseHandled = false;
+
+$.widget( "ui.menu", {
+	version: "@VERSION",
+	defaultElement: "<ul>",
+	delay: 300,
+	options: {
+		icons: {
+			submenu: "ui-icon-carat-1-e"
+		},
+		menus: "ul",
+		position: {
+			my: "left top",
+			at: "right top"
+		},
+		role: "menu",
+
+		// callbacks
+		blur: null,
+		focus: null,
+		select: null
+	},
+
+	_create: function() {
+		this.activeMenu = this.element;
+		this.element
+			.uniqueId()
+			.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
+			.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
+			.attr({
+				role: this.options.role,
+				tabIndex: 0
+			})
+			// need to catch all clicks on disabled menu
+			// not possible through _on
+			.bind( "click" + this.eventNamespace, $.proxy(function( event ) {
+				if ( this.options.disabled ) {
+					event.preventDefault();
+				}
+			}, this ));
+
+		if ( this.options.disabled ) {
+			this.element
+				.addClass( "ui-state-disabled" )
+				.attr( "aria-disabled", "true" );
+		}
+
+		this._on({
+			// Prevent focus from sticking to links inside menu after clicking
+			// them (focus should always stay on UL during navigation).
+			"mousedown .ui-menu-item > a": function( event ) {
+				event.preventDefault();
+			},
+			"click .ui-state-disabled > a": function( event ) {
+				event.preventDefault();
+			},
+			"click .ui-menu-item:has(a)": function( event ) {
+				var target = $( event.target ).closest( ".ui-menu-item" );
+				if ( !mouseHandled && target.not( ".ui-state-disabled" ).length ) {
+					mouseHandled = true;
+
+					this.select( event );
+					// Open submenu on click
+					if ( target.has( ".ui-menu" ).length ) {
+						this.expand( event );
+					} else if ( !this.element.is( ":focus" ) ) {
+						// Redirect focus to the menu
+						this.element.trigger( "focus", [ true ] );
+
+						// If the active item is on the top level, let it stay active.
+						// Otherwise, blur the active item since it is no longer visible.
+						if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
+							clearTimeout( this.timer );
+						}
+					}
+				}
+			},
+			"mouseenter .ui-menu-item": function( event ) {
+				var target = $( event.currentTarget );
+				// Remove ui-state-active class from siblings of the newly focused menu item
+				// to avoid a jump caused by adjacent elements both having a class with a border
+				target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" );
+				this.focus( event, target );
+			},
+			mouseleave: "collapseAll",
+			"mouseleave .ui-menu": "collapseAll",
+			focus: function( event, keepActiveItem ) {
+				// If there's already an active item, keep it active
+				// If not, activate the first item
+				var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 );
+
+				if ( !keepActiveItem ) {
+					this.focus( event, item );
+				}
+			},
+			blur: function( event ) {
+				this._delay(function() {
+					if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
+						this.collapseAll( event );
+					}
+				});
+			},
+			keydown: "_keydown"
+		});
+
+		this.refresh();
+
+		// Clicks outside of a menu collapse any open menus
+		this._on( this.document, {
+			click: function( event ) {
+				if ( !$( event.target ).closest( ".ui-menu" ).length ) {
+					this.collapseAll( event );
+				}
+
+				// Reset the mouseHandled flag
+				mouseHandled = false;
+			}
+		});
+
+		if ( this.options.trigger ) {
+			this.element.popup({
+				trigger: this.options.trigger,
+				managed: true,
+				focusPopup: $.proxy( function( event, ui ) {
+					this.focus( event, this.element.children( ".ui-menu-item" ).first() );
+					this.element.focus( 1 );
+				}, this)
+			});
+		}
+	},
+
+	_destroy: function() {
+		if ( this.options.trigger ) {
+			this.element.popup( "destroy" );
+		}
+
+		// Destroy (sub)menus
+		this.element
+			.removeAttr( "aria-activedescendant" )
+			.find( ".ui-menu" ).andSelf()
+				.removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" )
+				.removeAttr( "role" )
+				.removeAttr( "tabIndex" )
+				.removeAttr( "aria-labelledby" )
+				.removeAttr( "aria-expanded" )
+				.removeAttr( "aria-hidden" )
+				.removeAttr( "aria-disabled" )
+				.removeUniqueId()
+				.show();
+
+		// Destroy menu items
+		this.element.find( ".ui-menu-item" )
+			.removeClass( "ui-menu-item" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-disabled" )
+			.children( "a" )
+				.removeUniqueId()
+				.removeClass( "ui-corner-all ui-state-hover" )
+				.removeAttr( "tabIndex" )
+				.removeAttr( "role" )
+				.removeAttr( "aria-haspopup" )
+				.children().each( function() {
+					var elem = $( this );
+					if ( elem.data( "ui-menu-submenu-carat" ) ) {
+						elem.remove();
+					}
+				});
+
+		// Destroy menu dividers
+		this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
+	},
+
+	_keydown: function( event ) {
+		var match, prev, character, skip, regex,
+			preventDefault = true;
+
+		function escape( value ) {
+			return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
+		}
+
+		switch ( event.keyCode ) {
+		case $.ui.keyCode.PAGE_UP:
+			this.previousPage( event );
+			break;
+		case $.ui.keyCode.PAGE_DOWN:
+			this.nextPage( event );
+			break;
+		case $.ui.keyCode.HOME:
+			this._move( "first", "first", event );
+			break;
+		case $.ui.keyCode.END:
+			this._move( "last", "last", event );
+			break;
+		case $.ui.keyCode.UP:
+			this.previous( event );
+			break;
+		case $.ui.keyCode.DOWN:
+			this.next( event );
+			break;
+		case $.ui.keyCode.LEFT:
+			this.collapse( event );
+			break;
+		case $.ui.keyCode.RIGHT:
+			if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
+				this.expand( event );
+			}
+			break;
+		case $.ui.keyCode.ENTER:
+		case $.ui.keyCode.SPACE:
+			this._activate( event );
+			break;
+		case $.ui.keyCode.ESCAPE:
+			this.collapse( event );
+			break;
+		default:
+			preventDefault = false;
+			prev = this.previousFilter || "";
+			character = String.fromCharCode( event.keyCode );
+			skip = false;
+
+			clearTimeout( this.filterTimer );
+
+			if ( character === prev ) {
+				skip = true;
+			} else {
+				character = prev + character;
+			}
+
+			regex = new RegExp( "^" + escape( character ), "i" );
+			match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
+				return regex.test( $( this ).children( "a" ).text() );
+			});
+			match = skip && match.index( this.active.next() ) !== -1 ?
+				this.active.nextAll( ".ui-menu-item" ) :
+				match;
+
+			// If no matches on the current filter, reset to the last character pressed
+			// to move down the menu to the first item that starts with that character
+			if ( !match.length ) {
+				character = String.fromCharCode( event.keyCode );
+				regex = new RegExp( "^" + escape( character ), "i" );
+				match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
+					return regex.test( $( this ).children( "a" ).text() );
+				});
+			}
+
+			if ( match.length ) {
+				this.focus( event, match );
+				if ( match.length > 1 ) {
+					this.previousFilter = character;
+					this.filterTimer = this._delay(function() {
+						delete this.previousFilter;
+					}, 1000 );
+				} else {
+					delete this.previousFilter;
+				}
+			} else {
+				delete this.previousFilter;
+			}
+		}
+
+		if ( preventDefault ) {
+			event.preventDefault();
+		}
+	},
+
+	_activate: function( event ) {
+		if ( !this.active.is( ".ui-state-disabled" ) ) {
+			if ( this.active.children( "a[aria-haspopup='true']" ).length ) {
+				this.expand( event );
+			} else {
+				this.select( event );
+			}
+		}
+	},
+
+	refresh: function() {
+		// Initialize nested menus
+		var menus,
+			icon = this.options.icons.submenu,
+			submenus = this.element.find( this.options.menus + ":not(.ui-menu)" )
+				.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
+				.hide()
+				.attr({
+					role: this.options.role,
+					"aria-hidden": "true",
+					"aria-expanded": "false"
+				});
+
+		// Don't refresh list items that are already adapted
+		menus = submenus.add( this.element );
+
+		menus.children( ":not(.ui-menu-item):has(a)" )
+			.addClass( "ui-menu-item" )
+			.attr( "role", "presentation" )
+			.children( "a" )
+				.uniqueId()
+				.addClass( "ui-corner-all" )
+				.attr({
+					tabIndex: -1,
+					role: this._itemRole()
+				});
+
+		// Initialize unlinked menu-items containing spaces and/or dashes only as dividers
+		menus.children( ":not(.ui-menu-item)" ).each(function() {
+			var item = $( this );
+			// hyphen, em dash, en dash
+			if ( !/[^\-—–\s]/.test( item.text() ) ) {
+				item.addClass( "ui-widget-content ui-menu-divider" );
+			}
+		});
+
+		// Add aria-disabled attribute to any disabled menu item
+		menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
+
+		submenus.each(function() {
+			var menu = $( this ),
+				item = menu.prev( "a" ),
+				submenuCarat = $( "<span>" )
+					.addClass( "ui-menu-icon ui-icon " + icon )
+					.data( "ui-menu-submenu-carat", true );
+
+			item
+				.attr( "aria-haspopup", "true" )
+				.prepend( submenuCarat );
+			menu.attr( "aria-labelledby", item.attr( "id" ) );
+		});
+
+		// If the active item has been removed, blur the menu
+		if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
+			this.blur();
+		}
+	},
+
+	_itemRole: function() {
+		return {
+			menu: "menuitem",
+			listbox: "option"
+		}[ this.options.role ];
+	},
+
+	focus: function( event, item ) {
+		var nested, focused;
+		this.blur( event, event && event.type === "focus" );
+
+		this._scrollIntoView( item );
+
+		this.active = item.first();
+		focused = this.active.children( "a" ).addClass( "ui-state-focus" );
+		// Only update aria-activedescendant if there's a role
+		// otherwise we assume focus is managed elsewhere
+		if ( this.options.role ) {
+			this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
+		}
+
+		// Highlight active parent menu item, if any
+		this.active
+			.parent()
+			.closest( ".ui-menu-item" )
+			.children( "a:first" )
+			.addClass( "ui-state-active" );
+
+		if ( event && event.type === "keydown" ) {
+			this._close();
+		} else {
+			this.timer = this._delay(function() {
+				this._close();
+			}, this.delay );
+		}
+
+		nested = item.children( ".ui-menu" );
+		if ( nested.length && ( /^mouse/.test( event.type ) ) ) {
+			this._startOpening(nested);
+		}
+		this.activeMenu = item.parent();
+
+		this._trigger( "focus", event, { item: item } );
+	},
+
+	_scrollIntoView: function( item ) {
+		var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
+		if ( this._hasScroll() ) {
+			borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
+			paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
+			offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
+			scroll = this.activeMenu.scrollTop();
+			elementHeight = this.activeMenu.height();
+			itemHeight = item.height();
+
+			if ( offset < 0 ) {
+				this.activeMenu.scrollTop( scroll + offset );
+			} else if ( offset + itemHeight > elementHeight ) {
+				this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
+			}
+		}
+	},
+
+	blur: function( event, fromFocus ) {
+		if ( !fromFocus ) {
+			clearTimeout( this.timer );
+		}
+
+		if ( !this.active ) {
+			return;
+		}
+
+		this.active.children( "a" ).removeClass( "ui-state-focus" );
+		this.active = null;
+
+		this._trigger( "blur", event, { item: this.active } );
+	},
+
+	_startOpening: function( submenu ) {
+		clearTimeout( this.timer );
+
+		// Don't open if already open fixes a Firefox bug that caused a .5 pixel
+		// shift in the submenu position when mousing over the carat icon
+		if ( submenu.attr( "aria-hidden" ) !== "true" ) {
+			return;
+		}
+
+		this.timer = this._delay(function() {
+			this._close();
+			this._open( submenu );
+		}, this.delay );
+	},
+
+	_open: function( submenu ) {
+		var position = $.extend({
+			of: this.active
+		}, this.options.position );
+
+		clearTimeout( this.timer );
+		this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
+			.hide()
+			.attr( "aria-hidden", "true" );
+
+		submenu
+			.show()
+			.removeAttr( "aria-hidden" )
+			.attr( "aria-expanded", "true" )
+			.position( position );
+	},
+
+	collapseAll: function( event, all ) {
+		clearTimeout( this.timer );
+		this.timer = this._delay(function() {
+			// If we were passed an event, look for the submenu that contains the event
+			var currentMenu = all ? this.element :
+				$( event && event.target ).closest( this.element.find( ".ui-menu" ) );
+
+			// If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
+			if ( !currentMenu.length ) {
+				currentMenu = this.element;
+			}
+
+			this._close( currentMenu );
+
+			this.blur( event );
+			this.activeMenu = currentMenu;
+		}, this.delay );
+	},
+
+	// With no arguments, closes the currently active menu - if nothing is active
+	// it closes all menus.  If passed an argument, it will search for menus BELOW
+	_close: function( startMenu ) {
+		if ( !startMenu ) {
+			startMenu = this.active ? this.active.parent() : this.element;
+		}
+
+		startMenu
+			.find( ".ui-menu" )
+				.hide()
+				.attr( "aria-hidden", "true" )
+				.attr( "aria-expanded", "false" )
+			.end()
+			.find( "a.ui-state-active" )
+				.removeClass( "ui-state-active" );
+	},
+
+	collapse: function( event ) {
+		var newItem = this.active &&
+			this.active.parent().closest( ".ui-menu-item", this.element );
+		if ( newItem && newItem.length ) {
+			this._close();
+			this.focus( event, newItem );
+		}
+	},
+
+	expand: function( event ) {
+		var newItem = this.active &&
+			this.active
+				.children( ".ui-menu " )
+				.children( ".ui-menu-item" )
+				.first();
+
+		if ( newItem && newItem.length ) {
+			this._open( newItem.parent() );
+
+			// Delay so Firefox will not hide activedescendant change in expanding submenu from AT
+			this._delay(function() {
+				this.focus( event, newItem );
+			});
+		}
+	},
+
+	next: function( event ) {
+		this._move( "next", "first", event );
+	},
+
+	previous: function( event ) {
+		this._move( "prev", "last", event );
+	},
+
+	isFirstItem: function() {
+		return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
+	},
+
+	isLastItem: function() {
+		return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
+	},
+
+	_move: function( direction, filter, event ) {
+		var next;
+		if ( this.active ) {
+			if ( direction === "first" || direction === "last" ) {
+				next = this.active
+					[ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
+					.eq( -1 );
+			} else {
+				next = this.active
+					[ direction + "All" ]( ".ui-menu-item" )
+					.eq( 0 );
+			}
+		}
+		if ( !next || !next.length || !this.active ) {
+			next = this.activeMenu.children( ".ui-menu-item" )[ filter ]();
+		}
+
+		this.focus( event, next );
+	},
+
+	nextPage: function( event ) {
+		var item, base, height;
+
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isLastItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.nextAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base - height < 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.children( ".ui-menu-item" )
+				[ !this.active ? "first" : "last" ]() );
+		}
+	},
+
+	previousPage: function( event ) {
+		var item, base, height;
+		if ( !this.active ) {
+			this.next( event );
+			return;
+		}
+		if ( this.isFirstItem() ) {
+			return;
+		}
+		if ( this._hasScroll() ) {
+			base = this.active.offset().top;
+			height = this.element.height();
+			this.active.prevAll( ".ui-menu-item" ).each(function() {
+				item = $( this );
+				return item.offset().top - base + height > 0;
+			});
+
+			this.focus( event, item );
+		} else {
+			this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() );
+		}
+	},
+
+	_hasScroll: function() {
+		return this.element.outerHeight() < this.element.prop( "scrollHeight" );
+	},
+
+	select: function( event ) {
+		// TODO: It should never be possible to not have an active item at this
+		// point, but the tests don't trigger mouseenter before click.
+		this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
+		var ui = { item: this.active };
+		if ( !this.active.has( ".ui-menu" ).length ) {
+			this.collapseAll( event, true );
+		}
+		if ( this.options.trigger ) {
+			$( this.options.trigger ).focus( 1 );
+			this.element.popup( "close" );
+		}
+		this._trigger( "select", event, ui );
+	}
+});
+
+}( jQuery ));
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.menubar.css b/web-app/js/jquery-ui-menubar/jquery.ui.menubar.css
new file mode 100644
index 0000000..8b175f2
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.menubar.css
@@ -0,0 +1,15 @@
+/*
+ * jQuery UI Menubar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+.ui-menubar { list-style: none; margin: 0; padding-left: 0; }
+
+.ui-menubar-item { float: left; }
+
+.ui-menubar .ui-button { float: left; font-weight: normal; border-top-width: 0 !important; border-bottom-width: 0 !important; margin: 0; outline: none; }
+.ui-menubar .ui-menubar-link { border-right: 1px dashed transparent; border-left: 1px dashed transparent; }
+
+.ui-menubar .ui-menu { width: 200px; position: absolute; z-index: 9999; font-weight: normal; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.menubar.js b/web-app/js/jquery-ui-menubar/jquery.ui.menubar.js
new file mode 100644
index 0000000..4936ed4
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.menubar.js
@@ -0,0 +1,327 @@
+/*
+ * jQuery UI Menubar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menubar
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.widget.js
+ *	jquery.ui.position.js
+ *	jquery.ui.menu.js
+ */
+(function( $ ) {
+
+// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
+// there has to be just one item that has tabindex
+$.widget( "ui.menubar", {
+	version: "@VERSION",
+	options: {
+		autoExpand: false,
+		buttons: false,
+		items: "li",
+		menuElement: "ul",
+		menuIcon: false,
+		position: {
+			my: "left top",
+			at: "left bottom"
+		}
+	},
+	_create: function() {
+		var that = this;
+		this.menuItems = this.element.children( this.options.items );
+		this.items = this.menuItems.children( "button, a" );
+
+		this.menuItems
+			.addClass( "ui-menubar-item" )
+			.attr( "role", "presentation" );
+		// let only the first item receive focus
+		this.items.slice(1).attr( "tabIndex", -1 );
+
+		this.element
+			.addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+			.attr( "role", "menubar" );
+		this._focusable( this.items );
+		this._hoverable( this.items );
+		this.items.siblings( this.options.menuElement )
+			.menu({
+				position: {
+					within: this.options.position.within
+				},
+				select: function( event, ui ) {
+					ui.item.parents( "ul.ui-menu:last" ).hide();
+					that._close();
+					// TODO what is this targetting? there's probably a better way to access it
+					$(event.target).prev().focus();
+					that._trigger( "select", event, ui );
+				},
+				menus: that.options.menuElement
+			})
+			.hide()
+			.attr({
+				"aria-hidden": "true",
+				"aria-expanded": "false"
+			})
+			// TODO use _on
+			.bind( "keydown.menubar", function( event ) {
+				var menu = $( this );
+				if ( menu.is( ":hidden" ) ) {
+					return;
+				}
+				switch ( event.keyCode ) {
+				case $.ui.keyCode.LEFT:
+					that.previous( event );
+					event.preventDefault();
+					break;
+				case $.ui.keyCode.RIGHT:
+					that.next( event );
+					event.preventDefault();
+					break;
+				}
+			});
+		this.items.each(function() {
+			var input = $(this),
+				// TODO menu var is only used on two places, doesn't quite justify the .each
+				menu = input.next( that.options.menuElement );
+
+			// might be a non-menu button
+			if ( menu.length ) {
+				// TODO use _on
+				input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
+					// ignore triggered focus event
+					if ( event.type === "focus" && !event.originalEvent ) {
+						return;
+					}
+					event.preventDefault();
+					// TODO can we simplify or extractthis check? especially the last two expressions
+					// there's a similar active[0] == menu[0] check in _open
+					if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
+						that._close();
+						return;
+					}
+					if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
+						if( that.options.autoExpand ) {
+							clearTimeout( that.closeTimer );
+						}
+
+						that._open( event, menu );
+					}
+				})
+				// TODO use _on
+				.bind( "keydown", function( event ) {
+					switch ( event.keyCode ) {
+					case $.ui.keyCode.SPACE:
+					case $.ui.keyCode.UP:
+					case $.ui.keyCode.DOWN:
+						that._open( event, $( this ).next() );
+						event.preventDefault();
+						break;
+					case $.ui.keyCode.LEFT:
+						that.previous( event );
+						event.preventDefault();
+						break;
+					case $.ui.keyCode.RIGHT:
+						that.next( event );
+						event.preventDefault();
+						break;
+					}
+				})
+				.attr( "aria-haspopup", "true" );
+
+				// TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
+				if ( that.options.menuIcon ) {
+					input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
+					input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
+				}
+			} else {
+				// TODO use _on
+				input.bind( "click.menubar mouseenter.menubar", function( event ) {
+					if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
+						that._close();
+					}
+				});
+			}
+
+			input
+				.addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
+				.attr( "role", "menuitem" )
+				.wrapInner( "<span class='ui-button-text'></span>" );
+
+			if ( that.options.buttons ) {
+				input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
+			}
+		});
+		that._on( {
+			keydown: function( event ) {
+				if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
+					var active = that.active;
+					that.active.blur();
+					that._close( event );
+					active.prev().focus();
+				}
+			},
+			focusin: function( event ) {
+				clearTimeout( that.closeTimer );
+			},
+			focusout: function( event ) {
+				that.closeTimer = setTimeout( function() {
+					that._close( event );
+				}, 150);
+			},
+			"mouseleave .ui-menubar-item": function( event ) {
+				if ( that.options.autoExpand ) {
+					that.closeTimer = setTimeout( function() {
+						that._close( event );
+					}, 150);
+				}
+			},
+			"mouseenter .ui-menubar-item": function( event ) {
+				clearTimeout( that.closeTimer );
+			}
+		});
+
+		// Keep track of open submenus
+		this.openSubmenus = 0;
+	},
+
+	_destroy : function() {
+		this.menuItems
+			.removeClass( "ui-menubar-item" )
+			.removeAttr( "role" );
+
+		this.element
+			.removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+			.removeAttr( "role" )
+			.unbind( ".menubar" );
+
+		this.items
+			.unbind( ".menubar" )
+			.removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
+			.removeAttr( "role" )
+			.removeAttr( "aria-haspopup" )
+			// TODO unwrap?
+			.children( "span.ui-button-text" ).each(function( i, e ) {
+				var item = $( this );
+				item.parent().html( item.html() );
+			})
+			.end()
+			.children( ".ui-icon" ).remove();
+
+		this.element.find( ":ui-menu" )
+			.menu( "destroy" )
+			.show()
+			.removeAttr( "aria-hidden" )
+			.removeAttr( "aria-expanded" )
+			.removeAttr( "tabindex" )
+			.unbind( ".menubar" );
+	},
+
+	_close: function() {
+		if ( !this.active || !this.active.length ) {
+			return;
+		}
+		this.active
+			.menu( "collapseAll" )
+			.hide()
+			.attr({
+				"aria-hidden": "true",
+				"aria-expanded": "false"
+			});
+		this.active
+			.prev()
+			.removeClass( "ui-state-active" )
+			.removeAttr( "tabIndex" );
+		this.active = null;
+		this.open = false;
+		this.openSubmenus = 0;
+	},
+
+	_open: function( event, menu ) {
+		// on a single-button menubar, ignore reopening the same menu
+		if ( this.active && this.active[0] === menu[0] ) {
+			return;
+		}
+		// TODO refactor, almost the same as _close above, but don't remove tabIndex
+		if ( this.active ) {
+			this.active
+				.menu( "collapseAll" )
+				.hide()
+				.attr({
+					"aria-hidden": "true",
+					"aria-expanded": "false"
+				});
+			this.active
+				.prev()
+				.removeClass( "ui-state-active" );
+		}
+		// set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
+		var button = menu.prev().addClass( "ui-state-active" ).attr( "tabIndex", -1 );
+		this.active = menu
+			.show()
+			.position( $.extend({
+				of: button
+			}, this.options.position ) )
+			.removeAttr( "aria-hidden" )
+			.attr( "aria-expanded", "true" )
+			.menu("focus", event, menu.children( ".ui-menu-item" ).first() )
+			// TODO need a comment here why both events are triggered
+			.focus()
+			.focusin();
+		this.open = true;
+	},
+
+	next: function( event ) {
+		if ( this.open && this.active.data( "menu" ).active.has( ".ui-menu" ).length ) {
+			// Track number of open submenus and prevent moving to next menubar item
+			this.openSubmenus++;
+			return;
+		}
+		this.openSubmenus = 0;
+		this._move( "next", "first", event );
+	},
+
+	previous: function( event ) {
+		if ( this.open && this.openSubmenus ) {
+			// Track number of open submenus and prevent moving to previous menubar item
+			this.openSubmenus--;
+			return;
+		}
+		this.openSubmenus = 0;
+		this._move( "prev", "last", event );
+	},
+
+	_move: function( direction, filter, event ) {
+		var next,
+			wrapItem;
+		if ( this.open ) {
+			next = this.active.closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).first().children( ".ui-menu" ).eq( 0 );
+			wrapItem = this.menuItems[ filter ]().children( ".ui-menu" ).eq( 0 );
+		} else {
+			if ( event ) {
+				next = $( event.target ).closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).children( ".ui-menubar-link" ).eq( 0 );
+				wrapItem = this.menuItems[ filter ]().children( ".ui-menubar-link" ).eq( 0 );
+			} else {
+				next = wrapItem = this.menuItems.children( "a" ).eq( 0 );
+			}
+		}
+
+		if ( next.length ) {
+			if ( this.open ) {
+				this._open( event, next );
+			} else {
+				next.removeAttr( "tabIndex")[0].focus();
+			}
+		} else {
+			if ( this.open ) {
+				this._open( event, wrapItem );
+			} else {
+				wrapItem.removeAttr( "tabIndex")[0].focus();
+			}
+		}
+	}
+});
+
+}( jQuery ));
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.position.js b/web-app/js/jquery-ui-menubar/jquery.ui.position.js
new file mode 100644
index 0000000..5b595a8
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.position.js
@@ -0,0 +1,517 @@
+/*!
+ * jQuery UI Position @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/position/
+ */
+(function( $, undefined ) {
+
+$.ui = $.ui || {};
+
+var cachedScrollbarWidth,
+	max = Math.max,
+	abs = Math.abs,
+	round = Math.round,
+	rhorizontal = /left|center|right/,
+	rvertical = /top|center|bottom/,
+	roffset = /[\+\-]\d+%?/,
+	rposition = /^\w+/,
+	rpercent = /%$/,
+	_position = $.fn.position;
+
+function getOffsets( offsets, width, height ) {
+	return [
+		parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
+		parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
+	];
+}
+function parseCss( element, property ) {
+	return parseInt( $.css( element, property ), 10 ) || 0;
+}
+
+$.position = {
+	scrollbarWidth: function() {
+		if ( cachedScrollbarWidth !== undefined ) {
+			return cachedScrollbarWidth;
+		}
+		var w1, w2,
+			div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
+			innerDiv = div.children()[0];
+
+		$( "body" ).append( div );
+		w1 = innerDiv.offsetWidth;
+		div.css( "overflow", "scroll" );
+
+		w2 = innerDiv.offsetWidth;
+
+		if ( w1 === w2 ) {
+			w2 = div[0].clientWidth;
+		}
+
+		div.remove();
+
+		return (cachedScrollbarWidth = w1 - w2);
+	},
+	getScrollInfo: function( within ) {
+		var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ),
+			overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ),
+			hasOverflowX = overflowX === "scroll" ||
+				( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
+			hasOverflowY = overflowY === "scroll" ||
+				( overflowY === "auto" && within.height < within.element[0].scrollHeight );
+		return {
+			width: hasOverflowX ? $.position.scrollbarWidth() : 0,
+			height: hasOverflowY ? $.position.scrollbarWidth() : 0
+		};
+	},
+	getWithinInfo: function( element ) {
+		var withinElement = $( element || window ),
+			isWindow = $.isWindow( withinElement[0] );
+		return {
+			element: withinElement,
+			isWindow: isWindow,
+			offset: withinElement.offset() || { left: 0, top: 0 },
+			scrollLeft: withinElement.scrollLeft(),
+			scrollTop: withinElement.scrollTop(),
+			width: isWindow ? withinElement.width() : withinElement.outerWidth(),
+			height: isWindow ? withinElement.height() : withinElement.outerHeight()
+		};
+	}
+};
+
+$.fn.position = function( options ) {
+	if ( !options || !options.of ) {
+		return _position.apply( this, arguments );
+	}
+
+	// make a copy, we don't want to modify arguments
+	options = $.extend( {}, options );
+
+	var atOffset, targetWidth, targetHeight, targetOffset, basePosition,
+		target = $( options.of ),
+		within = $.position.getWithinInfo( options.within ),
+		scrollInfo = $.position.getScrollInfo( within ),
+		targetElem = target[0],
+		collision = ( options.collision || "flip" ).split( " " ),
+		offsets = {};
+
+	if ( targetElem.nodeType === 9 ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		targetOffset = { top: 0, left: 0 };
+	} else if ( $.isWindow( targetElem ) ) {
+		targetWidth = target.width();
+		targetHeight = target.height();
+		targetOffset = { top: target.scrollTop(), left: target.scrollLeft() };
+	} else if ( targetElem.preventDefault ) {
+		// force left top to allow flipping
+		options.at = "left top";
+		targetWidth = targetHeight = 0;
+		targetOffset = { top: targetElem.pageY, left: targetElem.pageX };
+	} else {
+		targetWidth = target.outerWidth();
+		targetHeight = target.outerHeight();
+		targetOffset = target.offset();
+	}
+	// clone to reuse original targetOffset later
+	basePosition = $.extend( {}, targetOffset );
+
+	// force my and at to have valid horizontal and vertical positions
+	// if a value is missing or invalid, it will be converted to center
+	$.each( [ "my", "at" ], function() {
+		var pos = ( options[ this ] || "" ).split( " " ),
+			horizontalOffset,
+			verticalOffset;
+
+		if ( pos.length === 1) {
+			pos = rhorizontal.test( pos[ 0 ] ) ?
+				pos.concat( [ "center" ] ) :
+				rvertical.test( pos[ 0 ] ) ?
+					[ "center" ].concat( pos ) :
+					[ "center", "center" ];
+		}
+		pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
+		pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
+
+		// calculate offsets
+		horizontalOffset = roffset.exec( pos[ 0 ] );
+		verticalOffset = roffset.exec( pos[ 1 ] );
+		offsets[ this ] = [
+			horizontalOffset ? horizontalOffset[ 0 ] : 0,
+			verticalOffset ? verticalOffset[ 0 ] : 0
+		];
+
+		// reduce to just the positions without the offsets
+		options[ this ] = [
+			rposition.exec( pos[ 0 ] )[ 0 ],
+			rposition.exec( pos[ 1 ] )[ 0 ]
+		];
+	});
+
+	// normalize collision option
+	if ( collision.length === 1 ) {
+		collision[ 1 ] = collision[ 0 ];
+	}
+
+	if ( options.at[ 0 ] === "right" ) {
+		basePosition.left += targetWidth;
+	} else if ( options.at[ 0 ] === "center" ) {
+		basePosition.left += targetWidth / 2;
+	}
+
+	if ( options.at[ 1 ] === "bottom" ) {
+		basePosition.top += targetHeight;
+	} else if ( options.at[ 1 ] === "center" ) {
+		basePosition.top += targetHeight / 2;
+	}
+
+	atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
+	basePosition.left += atOffset[ 0 ];
+	basePosition.top += atOffset[ 1 ];
+
+	return this.each(function() {
+		var collisionPosition, using,
+			elem = $( this ),
+			elemWidth = elem.outerWidth(),
+			elemHeight = elem.outerHeight(),
+			marginLeft = parseCss( this, "marginLeft" ),
+			marginTop = parseCss( this, "marginTop" ),
+			collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
+			collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
+			position = $.extend( {}, basePosition ),
+			myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
+
+		if ( options.my[ 0 ] === "right" ) {
+			position.left -= elemWidth;
+		} else if ( options.my[ 0 ] === "center" ) {
+			position.left -= elemWidth / 2;
+		}
+
+		if ( options.my[ 1 ] === "bottom" ) {
+			position.top -= elemHeight;
+		} else if ( options.my[ 1 ] === "center" ) {
+			position.top -= elemHeight / 2;
+		}
+
+		position.left += myOffset[ 0 ];
+		position.top += myOffset[ 1 ];
+
+		// if the browser doesn't support fractions, then round for consistent results
+		if ( !$.support.offsetFractions ) {
+			position.left = round( position.left );
+			position.top = round( position.top );
+		}
+
+		collisionPosition = {
+			marginLeft: marginLeft,
+			marginTop: marginTop
+		};
+
+		$.each( [ "left", "top" ], function( i, dir ) {
+			if ( $.ui.position[ collision[ i ] ] ) {
+				$.ui.position[ collision[ i ] ][ dir ]( position, {
+					targetWidth: targetWidth,
+					targetHeight: targetHeight,
+					elemWidth: elemWidth,
+					elemHeight: elemHeight,
+					collisionPosition: collisionPosition,
+					collisionWidth: collisionWidth,
+					collisionHeight: collisionHeight,
+					offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
+					my: options.my,
+					at: options.at,
+					within: within,
+					elem : elem
+				});
+			}
+		});
+
+		if ( $.fn.bgiframe ) {
+			elem.bgiframe();
+		}
+
+		if ( options.using ) {
+			// adds feedback as second argument to using callback, if present
+			using = function( props ) {
+				var left = targetOffset.left - position.left,
+					right = left + targetWidth - elemWidth,
+					top = targetOffset.top - position.top,
+					bottom = top + targetHeight - elemHeight,
+					feedback = {
+						target: {
+							element: target,
+							left: targetOffset.left,
+							top: targetOffset.top,
+							width: targetWidth,
+							height: targetHeight
+						},
+						element: {
+							element: elem,
+							left: position.left,
+							top: position.top,
+							width: elemWidth,
+							height: elemHeight
+						},
+						horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
+						vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
+					};
+				if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
+					feedback.horizontal = "center";
+				}
+				if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
+					feedback.vertical = "middle";
+				}
+				if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
+					feedback.important = "horizontal";
+				} else {
+					feedback.important = "vertical";
+				}
+				options.using.call( this, props, feedback );
+			};
+		}
+
+		elem.offset( $.extend( position, { using: using } ) );
+	});
+};
+
+$.ui.position = {
+	fit: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
+				outerWidth = within.width,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = withinOffset - collisionPosLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
+				newOverRight;
+
+			// element is wider than within
+			if ( data.collisionWidth > outerWidth ) {
+				// element is initially over the left side of within
+				if ( overLeft > 0 && overRight <= 0 ) {
+					newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
+					position.left += overLeft - newOverRight;
+				// element is initially over right side of within
+				} else if ( overRight > 0 && overLeft <= 0 ) {
+					position.left = withinOffset;
+				// element is initially over both left and right sides of within
+				} else {
+					if ( overLeft > overRight ) {
+						position.left = withinOffset + outerWidth - data.collisionWidth;
+					} else {
+						position.left = withinOffset;
+					}
+				}
+			// too far left -> align with left edge
+			} else if ( overLeft > 0 ) {
+				position.left += overLeft;
+			// too far right -> align with right edge
+			} else if ( overRight > 0 ) {
+				position.left -= overRight;
+			// adjust based on position and margin
+			} else {
+				position.left = max( position.left - collisionPosLeft, position.left );
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
+				outerHeight = data.within.height,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = withinOffset - collisionPosTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
+				newOverBottom;
+
+			// element is taller than within
+			if ( data.collisionHeight > outerHeight ) {
+				// element is initially over the top of within
+				if ( overTop > 0 && overBottom <= 0 ) {
+					newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
+					position.top += overTop - newOverBottom;
+				// element is initially over bottom of within
+				} else if ( overBottom > 0 && overTop <= 0 ) {
+					position.top = withinOffset;
+				// element is initially over both top and bottom of within
+				} else {
+					if ( overTop > overBottom ) {
+						position.top = withinOffset + outerHeight - data.collisionHeight;
+					} else {
+						position.top = withinOffset;
+					}
+				}
+			// too far up -> align with top
+			} else if ( overTop > 0 ) {
+				position.top += overTop;
+			// too far down -> align with bottom edge
+			} else if ( overBottom > 0 ) {
+				position.top -= overBottom;
+			// adjust based on position and margin
+			} else {
+				position.top = max( position.top - collisionPosTop, position.top );
+			}
+		}
+	},
+	flip: {
+		left: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.left + within.scrollLeft,
+				outerWidth = within.width,
+				offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
+				collisionPosLeft = position.left - data.collisionPosition.marginLeft,
+				overLeft = collisionPosLeft - offsetLeft,
+				overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
+				myOffset = data.my[ 0 ] === "left" ?
+					-data.elemWidth :
+					data.my[ 0 ] === "right" ?
+						data.elemWidth :
+						0,
+				atOffset = data.at[ 0 ] === "left" ?
+					data.targetWidth :
+					data.at[ 0 ] === "right" ?
+						-data.targetWidth :
+						0,
+				offset = -2 * data.offset[ 0 ],
+				newOverRight,
+				newOverLeft;
+
+			if ( overLeft < 0 ) {
+				newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
+				if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+			else if ( overRight > 0 ) {
+				newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
+				if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
+					position.left += myOffset + atOffset + offset;
+				}
+			}
+		},
+		top: function( position, data ) {
+			var within = data.within,
+				withinOffset = within.offset.top + within.scrollTop,
+				outerHeight = within.height,
+				offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
+				collisionPosTop = position.top - data.collisionPosition.marginTop,
+				overTop = collisionPosTop - offsetTop,
+				overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
+				top = data.my[ 1 ] === "top",
+				myOffset = top ?
+					-data.elemHeight :
+					data.my[ 1 ] === "bottom" ?
+						data.elemHeight :
+						0,
+				atOffset = data.at[ 1 ] === "top" ?
+					data.targetHeight :
+					data.at[ 1 ] === "bottom" ?
+						-data.targetHeight :
+						0,
+				offset = -2 * data.offset[ 1 ],
+				newOverTop,
+				newOverBottom;
+			if ( overTop < 0 ) {
+				newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
+				if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+			else if ( overBottom > 0 ) {
+				newOverTop = position.top -  data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
+				if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
+					position.top += myOffset + atOffset + offset;
+				}
+			}
+		}
+	},
+	flipfit: {
+		left: function() {
+			$.ui.position.flip.left.apply( this, arguments );
+			$.ui.position.fit.left.apply( this, arguments );
+		},
+		top: function() {
+			$.ui.position.flip.top.apply( this, arguments );
+			$.ui.position.fit.top.apply( this, arguments );
+		}
+	}
+};
+
+// fraction support test
+(function () {
+	var testElement, testElementParent, testElementStyle, offsetLeft, i,
+		body = document.getElementsByTagName( "body" )[ 0 ],
+		div = document.createElement( "div" );
+
+	//Create a "fake body" for testing based on method used in jQuery.support
+	testElement = document.createElement( body ? "div" : "body" );
+	testElementStyle = {
+		visibility: "hidden",
+		width: 0,
+		height: 0,
+		border: 0,
+		margin: 0,
+		background: "none"
+	};
+	if ( body ) {
+		$.extend( testElementStyle, {
+			position: "absolute",
+			left: "-1000px",
+			top: "-1000px"
+		});
+	}
+	for ( i in testElementStyle ) {
+		testElement.style[ i ] = testElementStyle[ i ];
+	}
+	testElement.appendChild( div );
+	testElementParent = body || document.documentElement;
+	testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+	div.style.cssText = "position: absolute; left: 10.7432222px;";
+
+	offsetLeft = $( div ).offset().left;
+	$.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
+
+	testElement.innerHTML = "";
+	testElementParent.removeChild( testElement );
+})();
+
+// DEPRECATED
+if ( $.uiBackCompat !== false ) {
+	// offset option
+	(function( $ ) {
+		var _position = $.fn.position;
+		$.fn.position = function( options ) {
+			if ( !options || !options.offset ) {
+				return _position.call( this, options );
+			}
+			var offset = options.offset.split( " " ),
+				at = options.at.split( " " );
+			if ( offset.length === 1 ) {
+				offset[ 1 ] = offset[ 0 ];
+			}
+			if ( /^\d/.test( offset[ 0 ] ) ) {
+				offset[ 0 ] = "+" + offset[ 0 ];
+			}
+			if ( /^\d/.test( offset[ 1 ] ) ) {
+				offset[ 1 ] = "+" + offset[ 1 ];
+			}
+			if ( at.length === 1 ) {
+				if ( /left|center|right/.test( at[ 0 ] ) ) {
+					at[ 1 ] = "center";
+				} else {
+					at[ 1 ] = at[ 0 ];
+					at[ 0 ] = "center";
+				}
+			}
+			return _position.call( this, $.extend( options, {
+				at: at[ 0 ] + offset[ 0 ] + " " + at[ 1 ] + offset[ 1 ],
+				offset: undefined
+			} ) );
+		};
+	}( jQuery ) );
+}
+
+}( jQuery ) );
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.progressbar.css b/web-app/js/jquery-ui-menubar/jquery.ui.progressbar.css
new file mode 100644
index 0000000..a8b3d74
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.progressbar.css
@@ -0,0 +1,12 @@
+/*!
+ * jQuery UI Progressbar @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.resizable.css b/web-app/js/jquery-ui-menubar/jquery.ui.resizable.css
new file mode 100644
index 0000000..291bdb2
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.resizable.css
@@ -0,0 +1,21 @@
+/*!
+ * jQuery UI Resizable @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.selectable.css b/web-app/js/jquery-ui-menubar/jquery.ui.selectable.css
new file mode 100644
index 0000000..e00779d
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.selectable.css
@@ -0,0 +1,11 @@
+/*!
+ * jQuery UI Selectable @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.slider.css b/web-app/js/jquery-ui-menubar/jquery.ui.slider.css
new file mode 100644
index 0000000..5e7a973
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.slider.css
@@ -0,0 +1,25 @@
+/*!
+ * jQuery UI Slider @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.spinner.css b/web-app/js/jquery-ui-menubar/jquery.ui.spinner.css
new file mode 100644
index 0000000..94b73fa
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.spinner.css
@@ -0,0 +1,24 @@
+/*!
+ * jQuery UI Spinner @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Spinner#theming
+ */
+.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; }
+.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; }
+.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; z-index: 100; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; }
+.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */
+.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */
+.ui-spinner-up { top: 0; }
+.ui-spinner-down { bottom: 0; }
+
+/* TR overrides */
+span.ui-spinner { background: none; }
+.ui-spinner .ui-icon-triangle-1-s {
+	/* need to fix icons sprite */
+	background-position:-65px -16px;
+}
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.tabs.css b/web-app/js/jquery-ui-menubar/jquery.ui.tabs.css
new file mode 100644
index 0000000..405b693
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.tabs.css
@@ -0,0 +1,18 @@
+/*!
+ * jQuery UI Tabs @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 0; margin: 1px .2em 0 0; border-bottom: 0; padding: 0; white-space: nowrap; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-active { margin-bottom: -1px; padding-bottom: 1px; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-tabs-loading a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.theme.css b/web-app/js/jquery-ui-menubar/jquery.ui.theme.css
new file mode 100644
index 0000000..d70b9df
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.theme.css
@@ -0,0 +1,247 @@
+/*!
+ * jQuery UI CSS Framework @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(images/ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; }
+.ui-widget-content a { color: #222222/*{fcContent}*/; }
+.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; }
+.ui-widget-header a { color: #222222/*{fcHeader}*/; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; }
+.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 #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; }
+.ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #212121/*{fcHover}*/; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-on { background-position: -96px -144px; }
+.ui-icon-radio-off { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px/*{cornerRadius}*/; -webkit-border-top-left-radius: 4px/*{cornerRadius}*/; -khtml-border-top-left-radius: 4px/*{cornerRadius}*/; border-top-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px/*{cornerRadius}*/; -webkit-border-top-right-radius: 4px/*{cornerRadius}*/; -khtml-border-top-right-radius: 4px/*{cornerRadius}*/; border-top-right-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px/*{cornerRadius}*/; -webkit-border-bottom-left-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-left-radius: 4px/*{cornerRadius}*/; border-bottom-left-radius: 4px/*{cornerRadius}*/; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; }
+.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRad [...]
\ No newline at end of file
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.tooltip.css b/web-app/js/jquery-ui-menubar/jquery.ui.tooltip.css
new file mode 100644
index 0000000..e293eeb
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.tooltip.css
@@ -0,0 +1,22 @@
+/*!
+ * jQuery UI Tooltip @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ */
+.ui-tooltip {
+	padding:8px;
+	position:absolute;
+	z-index:9999;
+	-o-box-shadow: 0 0 5px #aaa;
+	-moz-box-shadow: 0 0 5px #aaa;
+	-webkit-box-shadow: 0 0 5px #aaa;
+	box-shadow: 0 0 5px #aaa;
+}
+/* Fades and background-images don't work well together in IE6, drop the image */
+* html .ui-tooltip {
+	background-image: none;
+}
+body .ui-tooltip { border-width:2px; }
diff --git a/web-app/js/jquery-ui-menubar/jquery.ui.widget.js b/web-app/js/jquery-ui-menubar/jquery.ui.widget.js
new file mode 100644
index 0000000..a125dd5
--- /dev/null
+++ b/web-app/js/jquery-ui-menubar/jquery.ui.widget.js
@@ -0,0 +1,502 @@
+/*!
+ * jQuery UI Widget @VERSION
+ * http://jqueryui.com
+ *
+ * Copyright 2012 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/jQuery.widget/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	slice = Array.prototype.slice,
+	_cleanData = $.cleanData;
+$.cleanData = function( elems ) {
+	for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+		try {
+			$( elem ).triggerHandler( "remove" );
+		// http://bugs.jquery.com/ticket/8235
+		} catch( e ) {}
+	}
+	_cleanData( elems );
+};
+
+$.widget = function( name, base, prototype ) {
+	var fullName, existingConstructor, constructor, basePrototype,
+		namespace = name.split( "." )[ 0 ];
+
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+	// extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+		// copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+		// track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	});
+
+	basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( $.isFunction( value ) ) {
+			prototype[ prop ] = (function() {
+				var _super = function() {
+						return base.prototype[ prop ].apply( this, arguments );
+					},
+					_superApply = function( args ) {
+						return base.prototype[ prop ].apply( this, args );
+					};
+				return function() {
+					var __super = this._super,
+						__superApply = this._superApply,
+						returnValue;
+
+					this._super = _super;
+					this._superApply = _superApply;
+
+					returnValue = value.apply( this, arguments );
+
+					this._super = __super;
+					this._superApply = __superApply;
+
+					return returnValue;
+				};
+			})();
+		}
+	});
+	constructor.prototype = $.widget.extend( basePrototype, {
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: name
+	}, prototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		// TODO remove widgetBaseClass, see #8155
+		widgetBaseClass: fullName,
+		widgetFullName: fullName
+	});
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
+		});
+		// remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+};
+
+$.widget.extend = function( target ) {
+	var input = slice.call( arguments, 1 ),
+		inputIndex = 0,
+		inputLength = input.length,
+		key,
+		value;
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if (input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+				target[ key ] = $.isPlainObject( value ) ? $.widget.extend( {}, target[ key ], value ) : value;
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.widget.extend.apply( null, [ options ].concat(args) ) :
+			options;
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var methodValue,
+					instance = $.data( this, fullName );
+				if ( !instance ) {
+					return $.error( "cannot call methods on " + name + " prior to initialization; " +
+						"attempted to call method '" + options + "'" );
+				}
+				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
+					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
+				}
+				methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue && methodValue.jquery ?
+						returnValue.pushStack( methodValue.get() ) :
+						methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					new object( options, this );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( options, element ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+	options: {
+		disabled: false,
+
+		// callbacks
+		create: null
+	},
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = uuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+
+		if ( element !== this ) {
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			$.data( element, this.widgetName, this );
+			$.data( element, this.widgetFullName, this );
+			this._on({ remove: "destroy" });
+			this.document = $( element.style ?
+				// element within the document
+				element.ownerDocument :
+				// element is window or document
+				element.document || element );
+			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
+		}
+
+		this._create();
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+	_getCreateOptions: $.noop,
+	_getCreateEventData: $.noop,
+	_create: $.noop,
+	_init: $.noop,
+
+	destroy: function() {
+		this._destroy();
+		// we can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.unbind( this.eventNamespace )
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			.removeData( this.widgetName )
+			.removeData( this.widgetFullName )
+			// support: jquery <1.6.3
+			// http://bugs.jquery.com/ticket/9413
+			.removeData( $.camelCase( this.widgetFullName ) );
+		this.widget()
+			.unbind( this.eventNamespace )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetFullName + "-disabled " +
+				"ui-state-disabled" );
+
+		// clean up events and states
+		this.bindings.unbind( this.eventNamespace );
+		this.hoverable.removeClass( "ui-state-hover" );
+		this.focusable.removeClass( "ui-state-focus" );
+	},
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key,
+			parts,
+			curOption,
+			i;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( value === undefined ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( value === undefined ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+			this.hoverable.removeClass( "ui-state-hover" );
+			this.focusable.removeClass( "ui-state-focus" );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_on: function( element, handlers ) {
+		// no element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+		} else {
+			// accept selectors, DOM elements
+			element = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		var instance = this;
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+				// allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( instance.options.disabled === true ||
+						$( this ).hasClass( "ui-state-disabled" ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^(\w+)\s*(.*)$/ ),
+				eventName = match[1] + instance.eventNamespace,
+				selector = match[2];
+			if ( selector ) {
+				instance.widget().delegate( selector, eventName, handlerProxy );
+			} else {
+				element.bind( eventName, handlerProxy );
+			}
+		});
+	},
+
+	_off: function( element, eventName ) {
+		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
+		element.unbind( eventName ).undelegate( eventName );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-hover" );
+			}
+		});
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-focus" );
+			}
+		});
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+		var hasOptions,
+			effectName = !options ?
+				method :
+				options === true || typeof options === "number" ?
+					defaultEffect :
+					options.effect || defaultEffect;
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+		if ( hasOptions && $.effects && ( $.effects.effect[ effectName ] || $.uiBackCompat !== false && $.effects[ effectName ] ) ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue(function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			});
+		}
+	};
+});
+
+// DEPRECATED
+if ( $.uiBackCompat !== false ) {
+	$.Widget.prototype._getCreateOptions = function() {
+		return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
+	};
+}
+
+})( jQuery );
diff --git a/web-app/js/restapidoc/restapidoc.json b/web-app/js/restapidoc/restapidoc.json
new file mode 100644
index 0000000..b4a40f6
--- /dev/null
+++ b/web-app/js/restapidoc/restapidoc.json
@@ -0,0 +1,6886 @@
+{
+   "basePath": "Fill with basePath config",
+   "apis": [
+      {
+         "jsondocId": "cae80c7a-347a-4383-9638-58295b541fd9",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "3eaeb406-1801-493d-8044-60204e1e534f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3d4abd03-fbd0-4692-bcf2-8c504173e738",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7e541af5-49c6-4656-b18e-5c85d6c8cdb6",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4f0df9df-78bd-4c7b-a7c5-abcd76755d3c",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cae4a76e-e148-4270-8974-955239f5211a",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','name':'gene01'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set name of a feature",
+               "methodName": "setName",
+               "jsondocId": "80d60c4c-6dd8-4584-b21a-a77cf1d62707",
+               "bodyobject": {
+                  "jsondocId": "2abf6a4f-f77e-48b3-93cb-5e55036c4c99",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setName",
+               "response": {
+                  "jsondocId": "cee8223c-cb03-4bc0-8bd2-d72012f21016",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "cb0b8386-ea2f-480c-ab03-ac2f069504c3",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "402838a5-9823-41aa-9f6c-578b5a1b2c80",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2503fa35-d0de-4344-b1cb-ac653e9330c7",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b68bdd3a-16bc-4682-a4c2-7e4c54250e6d",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "45d73c31-4146-4b9f-8d45-b7ac47eec5e2",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','symbol':'Pax6a'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set symbol of a feature",
+               "methodName": "setSymbol",
+               "jsondocId": "d3cdce21-d0f9-4fec-bf3c-adb747466fa2",
+               "bodyobject": {
+                  "jsondocId": "09f6bc3b-a18f-4eae-97cc-bcb376778a09",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setSymbol",
+               "response": {
+                  "jsondocId": "941f61ab-c43c-4df5-87af-112b6a5cf7ed",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "4cbe8e5c-2f7c-4bb1-acc9-1283b8c02e3e",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ae2285e2-97af-47c5-b918-620720efe6a4",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "26dc9302-0fb6-4306-a4a8-2e60baf12721",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b7f0f5d8-427d-4403-883a-a70b5456e658",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c7b0a90a-4cc4-4628-90ea-2f05ad54f08a",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','status':'existing-status-string'}.  Available status found here: /availableStatus/ ",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set status of a feature",
+               "methodName": "setStatus",
+               "jsondocId": "a6623b67-f9b1-4698-ab1d-1c7fc23f0311",
+               "bodyobject": {
+                  "jsondocId": "508e0d74-9969-4c4a-85a7-0f12c6a75819",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setStatus",
+               "response": {
+                  "jsondocId": "aef2b833-4afe-49f6-9095-19d6443c91f5",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "05559509-3795-4d31-ba5d-ccbad4b803dc",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "50ce56ed-adc8-4f1d-8a9b-bc56e406ca0b",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3e4d4445-02e3-4504-9319-87a91718af5d",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "74696740-e4d8-49c3-bf09-3f8b857c5790",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9f9488c7-9722-4c48-b697-74fbc02adbc7",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','description':'some descriptive test'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set description for a feature",
+               "methodName": "setDescription",
+               "jsondocId": "428eec1a-53db-4eb9-98c4-fb8aeac51e93",
+               "bodyobject": {
+                  "jsondocId": "636d6165-c05c-4f74-950a-acf436519541",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setDescription",
+               "response": {
+                  "jsondocId": "8f1e0966-3a7c-487d-ae71-04a5350a111d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "9b650032-95b5-4c76-a68a-455129c209bf",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "60c80c0d-0338-4d8a-9298-10cbb3d90999",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ce2c1a3b-2e85-42b9-8e44-4cabdaace435",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "15b74b37-25db-4807-9d44-59679fc6b08b",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "752d8d18-dda1-497b-a19f-23331727d1d3",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects ('uniquename' required) JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get comments",
+               "methodName": "getComments",
+               "jsondocId": "7bab8226-22a5-4c84-907e-e5cf8f20fee7",
+               "bodyobject": {
+                  "jsondocId": "ee26c507-5726-4ce4-92b7-d0fc9b5306bd",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getComments",
+               "response": {
+                  "jsondocId": "f9b6c469-6457-4150-ad51-3838c06af010",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e5e5f8b8-c5ed-4e98-ac30-88bdc8cafb7c",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1facd11b-670f-4355-960e-cb5a08f412d0",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "dd55f419-b8c7-4e85-a19d-2e942dd9a202",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "81b5ce9f-b178-48ad-88ed-1afff5e0e71e",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1a41e560-5a84-4096-aeb4-b70cabb3c50f",
+                     "name": "suppressHistory",
+                     "format": "",
+                     "description": "Suppress the history of this operation",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3f523e5f-29ec-4968-be64-a21b343403aa",
+                     "name": "suppressEvents",
+                     "format": "",
+                     "description": "Suppress instant update of the user interface",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d8454b2a-a76f-450f-a0a0-6c176f65c145",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set exon feature boundaries",
+               "methodName": "setExonBoundaries",
+               "jsondocId": "22f74ded-4dcc-4d61-9b06-1ba8e25ac5d3",
+               "bodyobject": {
+                  "jsondocId": "07981b54-03ae-4598-a083-bb4a5a61fb29",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setExonBoundaries",
+               "response": {
+                  "jsondocId": "7e5fa624-dec9-4bc1-b2d0-f5012356bbee",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [],
+               "verb": "POST",
+               "description": "Returns a translation table as JSON",
+               "methodName": "getTranslationTable",
+               "jsondocId": "a9ff9f38-1f14-48f4-9ffc-91f6539b7d32",
+               "bodyobject": {
+                  "jsondocId": "c1207a88-3291-4f8c-a567-6fce5a068b80",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getTranslationTable",
+               "response": {
+                  "jsondocId": "b1cffefa-3961-4105-b720-ea21d9eb48dc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "d2eec637-7336-4571-836a-bcfc934246e9",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f0cefc5b-6f14-4604-b824-6c0028dbe99b",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4f60a9d0-53d5-4371-b7fe-04c1441fbbf4",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7c1cb0ee-1e1c-43d4-a3e7-90a55cbc33f9",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d3d8d45b-a0c5-41ee-b961-a7052a6b881d",
+                     "name": "suppressHistory",
+                     "format": "",
+                     "description": "Suppress the history of this operation",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "eff48d2b-a5b7-445c-bcb0-eabc852d5da0",
+                     "name": "suppressEvents",
+                     "format": "",
+                     "description": "Suppress instant update of the user interface",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4352fc9d-a84f-40d7-9589-8c7e463f33a8",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add non-coding genomic feature",
+               "methodName": "addFeature",
+               "jsondocId": "fd06dc8e-a552-4886-9010-6806f4a67306",
+               "bodyobject": {
+                  "jsondocId": "0bfc35f8-e89a-464d-8098-f8d441982660",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addFeature",
+               "response": {
+                  "jsondocId": "b8885eb0-2dc1-41ea-ad22-462340eff154",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "8ab9bb7c-6b44-4be7-aecf-0b1a449007fb",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "786b1341-19df-49e9-a96f-f2d8e3be11ed",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "93a4d37b-74b5-4c32-8e95-70daff97b999",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "dc2c6ade-17d5-4b9d-ba45-e8aa1ab2dfdf",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "940a7d6a-718c-448a-99e6-6c9b5cf39a31",
+                     "name": "suppressHistory",
+                     "format": "",
+                     "description": "Suppress the history of this operation",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "25c5546e-a4de-405a-9786-edb0076d12e1",
+                     "name": "suppressEvents",
+                     "format": "",
+                     "description": "Suppress instant update of the user interface",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8ae8c415-0d5a-4c6d-b6d9-4e133ed65713",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add an exon",
+               "methodName": "addExon",
+               "jsondocId": "8a8a9558-f55f-4f31-803f-14cc594e137e",
+               "bodyobject": {
+                  "jsondocId": "fa2e4826-5d01-4968-ac8c-c19b845fe518",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addExon",
+               "response": {
+                  "jsondocId": "ffbd5d67-4441-4452-8a80-7dd65ec9198b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "dbab200a-f054-4e50-a608-7fbf28daf463",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f1a8fdfc-5143-48d9-bc50-db56f6d499c3",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5c4251d5-6cb0-4c17-ba5c-b91f9bcfe38b",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "05e74714-36a2-4e4b-92aa-d0c2c5dc6ae0",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1f06d543-6fa6-4144-9907-ad3a9c07a028",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects ('uniquename' required) that include an added 'comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add comments",
+               "methodName": "addComments",
+               "jsondocId": "e5e679e4-22c9-417d-ab7d-d3d819cb3c93",
+               "bodyobject": {
+                  "jsondocId": "e8b60b40-91d6-4123-ae53-ed8f08105cb9",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addComments",
+               "response": {
+                  "jsondocId": "7a7b6f65-f051-40eb-97c0-e0b495c724f4",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e9163935-0447-428d-b537-3cc5e12ef422",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7649042e-24f8-4cfb-ade2-3b763ffa8a82",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4e9f72f7-f8b4-4b31-9ed0-749b56a6356c",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "702d4a31-9b15-4d5b-be1a-635cc87810e0",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "42aee094-450c-41b6-9574-68c4b3dc1d94",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects ('uniquename' required) that include an added 'comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete comments",
+               "methodName": "deleteComments",
+               "jsondocId": "705b0655-23ab-498a-b09f-307fd0857b1e",
+               "bodyobject": {
+                  "jsondocId": "f3995b15-3feb-46d9-9717-257b7b983245",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteComments",
+               "response": {
+                  "jsondocId": "08f118ca-fc9e-4422-9ffd-2bbb869c4dd0",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "5b408e85-a7a5-4225-9a8e-7f082b3182ea",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e66e16e7-1eb0-4ab7-99df-263c572bf83e",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d94aea49-4faf-46f8-903b-9193fc35ae4d",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "10d1966c-b348-4040-88e6-494c7c0c3497",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1b3a984b-3604-4635-b667-1a159cff4419",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects ('uniquename' required) that include an added 'old_comments','new_comments' JSONArray described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update comments",
+               "methodName": "updateComments",
+               "jsondocId": "86015cdb-6301-464c-9889-d6ece88b8ab5",
+               "bodyobject": {
+                  "jsondocId": "c9fab33c-f067-4dda-afa1-bcee33b53062",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/updateComments",
+               "response": {
+                  "jsondocId": "a1bc4a53-8fc9-4633-8bb0-e0f4acb05ec6",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "50b92e58-992e-4e96-8974-d76e1f33e290",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1d4a7e37-0bad-40e7-bc55-13b7418133e3",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5d21c9ed-6a9a-4420-b624-373b79b5a3e1",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7df56231-60c2-468c-8f5a-b93e41df8b89",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4e1d81f3-d394-4da9-b8fa-9fcf8a46f157",
+                     "name": "suppressHistory",
+                     "format": "",
+                     "description": "Suppress the history of this operation",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "65206c87-b17d-4e60-bbb9-c982ae41d33e",
+                     "name": "suppressEvents",
+                     "format": "",
+                     "description": "Suppress instant update of the user interface",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "dfce6d8b-5764-45e9-8d1e-0a49fa625569",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of JSON feature objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/Feature.groovy",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add transcript",
+               "methodName": "addTranscript",
+               "jsondocId": "7aa4b267-4e58-457a-99da-e24680791799",
+               "bodyobject": {
+                  "jsondocId": "2b62e48c-68ca-412b-b5b8-c607106eb828",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addTranscript",
+               "response": {
+                  "jsondocId": "f9b57faf-bdf8-4d41-a5c1-a526efb1fc83",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "feeca2d4-96a1-4536-811e-8d5108d8f8cd",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2c8d83f5-af5e-4361-9827-d54d5cc40b28",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "69a95a7c-31a0-4a7f-8e37-13dbbc0119b2",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e80c07dc-2c1f-4706-b343-19f6e2cec1cd",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8c338b88-acc5-458d-b959-9a467562f626",
+                     "name": "suppressHistory",
+                     "format": "",
+                     "description": "Suppress the history of this operation",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7bdf38dd-179f-42ee-a786-c8e533cea132",
+                     "name": "suppressEvents",
+                     "format": "",
+                     "description": "Suppress instant update of the user interface",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e95ceb5a-c050-4744-9e4e-bf30efc3bc99",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing a single JSONObject feature that contains 'uniquename'",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Duplicate transcript",
+               "methodName": "duplicateTranscript",
+               "jsondocId": "1cb6ffec-2fa7-4f97-b2ca-60ae9e9a9d0d",
+               "bodyobject": {
+                  "jsondocId": "9c32c752-22dc-4845-ae0a-d83b4c302636",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/duplicateTranscript",
+               "response": {
+                  "jsondocId": "ee17c911-05ed-476c-98dd-00c0919623b7",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "1c02884a-64ff-470d-b624-6bdc48176bdf",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5127d0e5-a9a7-4e9b-ab5e-93f6b71525ac",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f4b9aa73-8121-4b22-9885-46d573f4fa36",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f6588dbb-3faf-4a32-a457-6fce0877f571",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "fc9c4093-aa2c-4e67-afe6-86c1991a24c7",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmin':12}}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set translation start",
+               "methodName": "setTranslationStart",
+               "jsondocId": "5ade75a5-844d-4e6c-9fea-a248a56482e8",
+               "bodyobject": {
+                  "jsondocId": "68c901ad-e51a-4644-96d4-f7efe7f6f555",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setTranslationStart",
+               "response": {
+                  "jsondocId": "f08bb173-b527-47c8-a5d0-d1fb878003b2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "179f993a-d2d8-4c71-9899-32f9ec36cce9",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c8022e87-00ea-4d5a-a616-8e1d98eb96d8",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f7af54bd-d76f-4fdd-b7c5-407cca8f3ca1",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "bfcf8460-949e-42b0-b404-0008bf83aaab",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "735c0640-ce99-464b-a317-7495f3453ed6",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmax':12}}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set translation end",
+               "methodName": "setTranslationEnd",
+               "jsondocId": "3821b95a-cd3a-4365-a32a-a9c984b1e18e",
+               "bodyobject": {
+                  "jsondocId": "e356301d-6404-45b0-964a-8391a258d42b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setTranslationEnd",
+               "response": {
+                  "jsondocId": "8013dfdb-8a5d-49f8-a9e8-db0d67bb60d8",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "ec2aff49-c9be-4cfd-9df3-17629581f964",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7c1493fe-bcb3-4660-84ef-36d685370c8a",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b211ac5a-69a4-4ba5-bbe4-1fd4d79c2de4",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8f755702-aefb-4d10-9eb6-d45215445f92",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c9debeb9-abfe-4992-b94c-086f02184ce5",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set longest ORF",
+               "methodName": "setLongestOrf",
+               "jsondocId": "ed427d5b-e1c0-47e6-ac8c-267b2444955e",
+               "bodyobject": {
+                  "jsondocId": "2319996f-76fc-4cf7-b9da-c2cb46d7a2aa",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setLongestOrf",
+               "response": {
+                  "jsondocId": "a815b34e-ca5e-4ab6-a78e-141059679716",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "243ed2c0-441c-473b-b70f-4111cc5283ca",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "bc9fcffc-a652-4bfd-87b0-6c8c7a09d592",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "41cc4027-c0d8-4dc6-bb4c-df8fff4717d1",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "848fc324-aee2-4399-983c-437a2a47997d",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ecc4cc79-1d6d-49ca-b449-9e3acdf121dc",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing feature objects with the location object defined {'uniquename':'ABCD-1234','location':{'fmin':2,'fmax':12}}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set boundaries of genomic feature",
+               "methodName": "setBoundaries",
+               "jsondocId": "ec6555e2-f25f-42ef-b8df-fac0f673556e",
+               "bodyobject": {
+                  "jsondocId": "f3647ece-7182-48a6-9361-8237c2f60d7f",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setBoundaries",
+               "response": {
+                  "jsondocId": "01fea87a-b21a-4c14-8da1-0147c42a9016",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "6f4e8051-7d61-4d81-9c0c-a7acce55aa4a",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5bdf335d-f9f7-4516-95a7-4cb1cc0827df",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ea78351e-27c3-47be-83bd-88e3e4b2c8a8",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "36ef1a96-3b8e-46a9-9a6e-e00f8d9c8b24",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get all annotated features for a sequence",
+               "methodName": "getFeatures",
+               "jsondocId": "270e25e0-a3c2-4c97-9b2c-d52d4c276f0c",
+               "bodyobject": {
+                  "jsondocId": "9902e408-9500-4855-98ed-21230770f94d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getFeatures",
+               "response": {
+                  "jsondocId": "ad8eba09-c63f-4dce-8c01-10a029a6ebee",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "eba3907f-0e7a-4032-917c-df497eee7ccc",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "45703707-0a19-4e04-aee0-97cdcae59a9d",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "517c0275-ca21-4024-96bf-a0dd853ce3eb",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8cf96184-c8fd-4da1-9511-4cfe1ee63a71",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get sequence alterations for a given sequence",
+               "methodName": "getSequenceAlterations",
+               "jsondocId": "36bed71f-cf7c-4216-88bf-c0c926a882d4",
+               "bodyobject": {
+                  "jsondocId": "2577a58d-9cd7-4ede-b95f-3b1ee7289f72",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getSequenceAlterations",
+               "response": {
+                  "jsondocId": "52a89f8b-e20f-4518-83ab-3f67c8979d56",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "ee205e9b-5a3d-4a3a-b662-47d73fc359db",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "17e508f4-4cbc-438c-9d32-dea1fd1698c0",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cad68e5a-811f-476b-b779-6539287f4708",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "11af80da-468f-479b-898f-21d224c61193",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ad8bc531-5df5-4e20-9464-f462ff27c3ef",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','non_reserved_properties':[{'tag':'clockwork','value':'orange'},{'tag':'color','value':'purple'}]}.  Available status found here: /availableStatus/ ",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete attribute (key,value pair) for feature",
+               "methodName": "deleteAttribute",
+               "jsondocId": "9a58d879-a75c-43e1-b67e-838a9c016c6d",
+               "bodyobject": {
+                  "jsondocId": "5e11069a-8078-42f1-b3a2-cd46b8d363d2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteAttribute",
+               "response": {
+                  "jsondocId": "9f93b36a-5072-4941-9d5c-4e3ccd4a6a11",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "ca99d645-ba90-45f3-9955-eba16fe8f944",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "992008cf-ec09-498e-8c51-8de83b151983",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8165ca19-a42f-4cbd-bbec-36e0e43184aa",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "68aa4423-2b19-42bd-87e8-cc461d5c51c0",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8a765b70-5b37-4236-8c74-f507d69ccdde",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','old_non_reserved_properties':[{'color': 'red'}], 'new_non_reserved_properties': [{'color': 'green'}]}.",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update attribute (key,value pair) for feature",
+               "methodName": "updateAttribute",
+               "jsondocId": "52070980-efbf-4e6e-9887-b328e5eb4a09",
+               "bodyobject": {
+                  "jsondocId": "69ac59e8-6155-4df9-b28d-350903757fb4",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/updateAttribute",
+               "response": {
+                  "jsondocId": "6633c9dc-bb78-4b76-9458-dc94d53866ed",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "fbc8f928-0a7e-4e3a-9b51-5de689c60508",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8d04ce49-d094-4238-ad59-ce03de303bb0",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a86275ef-6bdf-4161-b3a3-bd05a1442ca4",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "13463d37-0c27-4065-8aba-179fd6a62cc5",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a4da99e4-0db0-433a-a67f-8616d2594e92",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','dbxrefs': [{'db': 'PMID', 'accession': '19448641'}]}.",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add dbxref (db,id pair) to feature",
+               "methodName": "addDbxref",
+               "jsondocId": "97b3e779-47a1-40b6-bd78-96990e7c01c3",
+               "bodyobject": {
+                  "jsondocId": "8598a8e8-dba7-4e17-ab07-ffdd702de688",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addDbxref",
+               "response": {
+                  "jsondocId": "39536b10-676a-480b-927f-dc6de5a174ec",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c8103b23-c409-4ad8-8a70-54529bdd2b82",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "bda774d5-851a-42fe-9bc0-642115fb0ecb",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6abbbb11-a7a0-4ff7-896d-8f69ea68fd5c",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f6f56a0f-e586-4783-8d6a-8282e001ef2c",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3256f733-c2fa-402f-9666-603dbf862472",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','old_dbxrefs': [{'db': 'PMID', 'accession': '19448641'}], 'new_dbxrefs': [{'db': 'PMID', 'accession': '19448642'}]}.",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update dbxrefs (db,id pairs) for a feature",
+               "methodName": "updateDbxref",
+               "jsondocId": "6b03cfd8-3d35-423d-a05f-3f935d25fd82",
+               "bodyobject": {
+                  "jsondocId": "c30b786b-785d-48f3-852f-48d8540e016b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/updateDbxref",
+               "response": {
+                  "jsondocId": "d811b9e4-2570-4bb4-8caa-56d687abc0d8",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "aeaec2de-cb09-42b8-bfce-3aee03858c63",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "26343a43-208f-44e3-934d-c69aed676931",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "412520c6-0fc6-4bd5-8adc-fdce79ddf048",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e9cf864b-9c7a-488b-9f97-af0bc6ec0cae",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "de7f95a2-6cc8-4334-890a-bafd3a132e26",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','dbxrefs': [{'db': 'PMID', 'accession': '19448641'}]}.",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete dbxrefs (db,id pairs) for a feature",
+               "methodName": "deleteDbxref",
+               "jsondocId": "c8c0ab95-0ded-4d22-a240-3f85c0da33fe",
+               "bodyobject": {
+                  "jsondocId": "7f5215d5-ee36-4900-90f0-a14edf683a3e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteDbxref",
+               "response": {
+                  "jsondocId": "b980600f-1577-4d78-bbde-a393b5089497",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c3566b91-5b42-472e-a825-7741ada942a5",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c3aa1625-4258-40ac-a6f7-feed55853218",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "14b90304-cc8e-46e1-a70d-670b6ac45f4f",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b8335078-c81f-4f61-8197-d4e9edbdeb9d",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "785d0059-076b-470f-a4e0-d6ab16f3bae6",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with one feature object {'uniquename':'ABCD-1234'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Set readthrough stop codon",
+               "methodName": "setReadthroughStopCodon",
+               "jsondocId": "9505dca0-e4a6-43bd-a96e-570fb67e5b4b",
+               "bodyobject": {
+                  "jsondocId": "0417835a-fb09-4d4e-a762-f03fb43cf126",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/setReadthroughStopCodon",
+               "response": {
+                  "jsondocId": "78f1e2f5-e737-4937-bf81-5dbfbcd10338",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "59da607b-ffb7-4d6f-ac32-291236dff1e6",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4b0589b3-b469-47ae-92f3-509825cb5f86",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7ef9181e-123c-4550-a1f2-20b07df2bd5f",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "228b035e-3e99-4479-9da2-0bf3de366936",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8f38759c-a714-4eb3-86d5-775ce1be9ec8",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with Sequence Alteration (Insertion, Deletion, Substituion) objects described by https://github.com/GMOD/Apollo/blob/master/grails-app/domain/org/bbop/apollo/",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add sequence alteration",
+               "methodName": "addSequenceAlteration",
+               "jsondocId": "b1ad61ce-7393-4e3a-b01e-0441bc091cf0",
+               "bodyobject": {
+                  "jsondocId": "9276f8f0-852a-440a-a2f3-22588d8104ae",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addSequenceAlteration",
+               "response": {
+                  "jsondocId": "acc31ad7-55d7-488c-88f9-dc937d84f3e2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "ae228d8b-00d5-478e-8dbd-e770e18c0385",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "498599b9-1541-47c8-ac23-75ad262fe9a1",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8ed7c0bf-2877-45d2-a080-454c1e1739d2",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3279485a-3f80-4506-9732-d7117f6086cb",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "20ea0174-5c33-4070-ab2c-08d78394b969",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with Sequence Alteration identified by unique names {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete sequence alteration",
+               "methodName": "deleteSequenceAlteration",
+               "jsondocId": "0d00e0a4-f3b1-4781-b099-381c8ac8e439",
+               "bodyobject": {
+                  "jsondocId": "bce12853-62f2-4638-a274-dc7e91434acd",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteSequenceAlteration",
+               "response": {
+                  "jsondocId": "1bc698bd-8b02-4a7a-b7e2-ab4b713c3869",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "93c1aff3-107e-4ed2-8bb5-7ca7ccac5497",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6735cc81-d9ce-41ef-9e64-ec69bbeda824",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "464ef2af-1bdb-4a00-91bb-ebf99073e949",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2b8c7851-9d92-4611-abed-e43b16bcac45",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "28225bcc-1c36-408b-8162-0e0712378736",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with with objects of features defined as {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Flip strand",
+               "methodName": "flipStrand",
+               "jsondocId": "e7bbf45f-7892-4c00-8daf-5d1d63d8f2e7",
+               "bodyobject": {
+                  "jsondocId": "688aec24-cf76-4af4-8664-a69b27a3b6bc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/flipStrand",
+               "response": {
+                  "jsondocId": "c88d3c13-9150-4cd4-ad21-465e5fe99f59",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "a22d5310-0d6a-46fa-a55c-45d8c420db48",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9366c90f-b714-4ffd-a5ae-d63000d7d934",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "63d5ff7a-1e11-4e62-ad3a-f25a28e5038f",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3de87bd7-a131-4cec-bbc9-9b4b668fa0ba",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "30426172-2363-4304-a844-acb00dc5620a",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with with two objects of referred to as defined as {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Merge exons",
+               "methodName": "mergeExons",
+               "jsondocId": "efc9f74a-b6d0-4be8-b26a-65db40bd364c",
+               "bodyobject": {
+                  "jsondocId": "b2236936-1386-4840-99ab-5c9cc36dd95a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/mergeExons",
+               "response": {
+                  "jsondocId": "1a2c3bf4-32a9-4cb1-8db5-6aac488b4093",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "18921b0e-7728-470b-abbc-6d31e2aa7c3d",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ee3d34c1-a4bc-4a74-b80d-622f55e1dc8c",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "35314d81-4081-4d0d-8f0f-ce8121022981",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a497bf66-6f54-4d29-9d0e-86502198752c",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b1ff9c45-f6c3-47b0-a410-7467e8676e06",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing feature objects with the location object defined {'uniquename':'ABCD-1234','location':{'fmin':2,'fmax':12}}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Split exons",
+               "methodName": "splitExon",
+               "jsondocId": "4498953e-2dc7-4095-861b-fa32ba636d1e",
+               "bodyobject": {
+                  "jsondocId": "62dd1288-f100-4e4a-b73a-07fd5b47eb2a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/splitExon",
+               "response": {
+                  "jsondocId": "2adae8cb-c464-4896-bf56-ab1fba3616a0",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "b178f6b2-db1b-40c4-9108-0170725011cc",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cdc00541-f120-4143-9fab-f9ff68d91859",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ea69c8a6-be0d-4075-bb20-3ee0efbbf8d8",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1ab16bed-d2d3-48cf-9121-9258267546d3",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0d4fd1cd-36a5-46cc-bd15-3e4c967976ca",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of features objects to delete defined by unique name {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete feature",
+               "methodName": "deleteFeature",
+               "jsondocId": "9b90983e-e2b8-4444-8156-cc426e686412",
+               "bodyobject": {
+                  "jsondocId": "28401994-cb97-4f0e-966a-8b8cb2fd7e25",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteFeature",
+               "response": {
+                  "jsondocId": "4328d1d3-84ac-462b-a9b6-f97d34ba446e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "90186024-2f43-4a60-ae0e-f3870f339955",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ca72c351-3cff-4f28-9577-233290397077",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e0320df9-ca44-4cc1-90ba-6769f6a64b66",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0f9f87a6-1929-4750-8440-829f147c9760",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "35b16c54-1c45-4981-a8e8-9e50304e20b7",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of features objects, where the first is the parent transcript and the remaining are exons all defined by a unique name {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete exons",
+               "methodName": "deleteExon",
+               "jsondocId": "65984384-1999-4426-b425-70308174c1fd",
+               "bodyobject": {
+                  "jsondocId": "9a8d819c-f929-4f5d-9ae9-7c3880eff3ab",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/deleteExon",
+               "response": {
+                  "jsondocId": "3be6416f-b159-4426-9ee6-2d3f1bda9634",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "cbed124a-9d3f-4f20-8352-c209ac889841",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "022b534f-98a1-4a22-b081-afad679d1859",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "168b4d08-ec48-4c7f-9fc3-0c9022d6b57a",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "274e19aa-caf4-4102-8f52-158c6a038ae7",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "65d67aaa-7832-4532-8b70-1c401b820bd0",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing a single JSONObject feature that contains {'uniquename':'ABCD-1234','location':{'fmin':12}}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Make intron",
+               "methodName": "makeIntron",
+               "jsondocId": "e3967289-b961-4221-895f-8a5ed948c2df",
+               "bodyobject": {
+                  "jsondocId": "c1a24bf0-0686-4733-ac51-4df9acab8035",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/makeIntron",
+               "response": {
+                  "jsondocId": "0dbb3a3b-02f5-4b27-98f6-e6b8aecceea0",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "d0a7bb48-0fe0-4638-9853-2b6afff38849",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "600d3bb5-543c-44a3-9179-c91809484f04",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3a5246a4-7b00-4201-b350-0db8b76cf26d",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f96186ae-bd2c-4fbd-aed7-81d3d3298c1c",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e030532c-b1f3-4c8c-b6f5-aa2913dedc68",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with with two exon objects referred to their unique names {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Split transcript",
+               "methodName": "splitTranscript",
+               "jsondocId": "d76535e8-9cc7-40aa-9ba4-eef8fbfa47c5",
+               "bodyobject": {
+                  "jsondocId": "af204475-4405-466b-a105-4554b33ff233",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/splitTranscript",
+               "response": {
+                  "jsondocId": "076b1510-112f-4fe8-bda2-7b4fd30683e2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e6ac8b32-7e44-4c47-af69-32eae2c37232",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "47f13f4e-4872-41ad-a1ad-eb08ac868cfc",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7e06b1c1-5265-47bc-9933-d2d2dbeda649",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7676e507-8fea-40e3-b917-f861d93c2139",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "937917ce-6c96-4c79-a1c0-3989e0085989",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray with with two transcript objects referred to their unique names {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Merge transcripts",
+               "methodName": "mergeTranscripts",
+               "jsondocId": "aad9eb33-5cab-4b8d-909d-ec4a3f79b646",
+               "bodyobject": {
+                  "jsondocId": "3ca25893-74a6-4bfb-b948-48fede2ba58a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/mergeTranscripts",
+               "response": {
+                  "jsondocId": "deb1cc0b-2af1-49b2-8d0d-f81342804229",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "f2473fe2-1019-4cb5-ac33-fb7a83802902",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b524e823-5e49-4d6a-99cf-8c38516633fe",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f314ecf9-2495-4d3e-aa54-7438eadac0d5",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "301c7f1c-c170-40f0-b4b1-fd373721eecd",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "58f20323-21ac-4d2a-b813-19fec92172c7",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of features objects to export defined by a unique name {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get sequences for features",
+               "methodName": "getSequence",
+               "jsondocId": "9110c467-7619-42e0-8e03-fc83d07a22ea",
+               "bodyobject": {
+                  "jsondocId": "51c5ae61-f358-4e51-b194-007dc2fd5067",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getSequences",
+               "response": {
+                  "jsondocId": "cfe1b37f-dc62-44ff-9482-f8f97e0d25e8",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "jsondocId": "c86f7e32-4b71-4dcd-8780-f602f8adb98e",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/annotationEditor/getSequenceSearchTools",
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [],
+               "response": {
+                  "jsondocId": "c3532f46-33e2-420b-9560-0e6b365f19d8",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "description": "Get sequences search tools",
+               "methodName": "getSequenceSearchTools",
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "213468ca-0d5d-4569-971a-a92d39a54e01",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e36be472-562f-478c-a719-b95c153194fb",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get canned comments",
+               "methodName": "getCannedComments",
+               "jsondocId": "7e30dec0-2093-45f4-ad4c-7ab2ce0bd93d",
+               "bodyobject": {
+                  "jsondocId": "31771f95-146b-4fca-9bd6-9722f366e3e0",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getCannedComments",
+               "response": {
+                  "jsondocId": "e9cc9ec8-b00c-4925-92fa-b80e1e4ff93f",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "6cf76d3f-af63-462c-ab40-1d7cc9263158",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cb19fa9a-cd31-4896-83a3-2671b2b09eb6",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "02cea335-8b54-4603-be47-9fb60278fe60",
+                     "name": "client_token",
+                     "format": "",
+                     "description": "Organism ID/Name or Client-generated ",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "30ba1060-f14b-49a8-871b-ea836163814e",
+                     "name": "search",
+                     "format": "",
+                     "description": "{'key':'blat','residues':'ATACTAGAGATAC':'database_id':'abc123'}",
+                     "type": "JSONObject",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Search sequences",
+               "methodName": "searchSequence",
+               "jsondocId": "57db1ff8-12a5-4b8b-9921-7ecce10e38fe",
+               "bodyobject": {
+                  "jsondocId": "760198e8-b2c9-4623-a981-7554ef347eb1",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/searchSequences",
+               "response": {
+                  "jsondocId": "628ff8c3-8b22-4bd1-9b45-80c5ce058173",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c6a0093a-ad23-4b24-a4e2-1ada0b5a6635",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cb4df74c-16cd-4838-869f-fa50eaacb2a5",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0b6fd6e5-c9fb-4381-b7d7-76a687ec28cd",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray of features objects to export defined by a unique name {'uniquename':'ABC123'}",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get gff3",
+               "methodName": "getGff3",
+               "jsondocId": "15f1fe45-bfdd-4809-9794-3b95c91adcab",
+               "bodyobject": {
+                  "jsondocId": "379cf94a-ec35-4b79-82ee-293ebfd613ff",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/getGff3",
+               "response": {
+                  "jsondocId": "c5d817a2-7a06-44d6-87ef-4a99aab60a1c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "84da0b05-844d-4944-bcf2-c4d413d1ba52",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3f1d190f-7c95-4165-bdae-4f0c0373c38f",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b5336c75-83e7-406c-835c-89f4bfef9a01",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "(optional) Sequence name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9abc64d6-8ffd-45fb-90fd-7b41036a6e7c",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) Organism ID or common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9091a4cd-a3a5-4a38-aa1b-8b5311ff9721",
+                     "name": "features",
+                     "format": "",
+                     "description": "JSONArray containing JSON objects with {'uniquename':'ABCD-1234','non_reserved_properties':[{'tag':'clockwork','value':'orange'},{'tag':'color','value':'purple'}]}.  Available status found here: /availableStatus/ ",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add attribute (key,value pair) to feature",
+               "methodName": "addAttribute",
+               "jsondocId": "47827a9d-ebcf-4341-a49f-e39761c0d2fa",
+               "bodyobject": {
+                  "jsondocId": "9e049f77-a5c1-456a-b08e-e6fd68686717",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "annotation editor"
+               },
+               "apierrors": [],
+               "path": "/annotationEditor/addAttribute",
+               "response": {
+                  "jsondocId": "4fabfcde-4b89-40af-86c8-1be7d9bf7ba4",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "annotation editor"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Annotation Services",
+         "description": "Methods for running the annotation engine"
+      },
+      {
+         "jsondocId": "85cdf2e4-1bc1-4200-bdb0-3311e86f30bb",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "12c22e44-b01c-44f0-9ec3-2eafb53bff48",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "57137a0a-66b8-47cb-9d00-823bc0283901",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "47d14692-9afd-43c5-b996-dfe558911b91",
+                     "name": "id",
+                     "format": "",
+                     "description": "Status ID to update (or specify the old_value)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c95b61ac-83d7-4002-8d58-7e615ae91004",
+                     "name": "old_value",
+                     "format": "",
+                     "description": "Status name to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a01b1755-b211-4473-8e7c-42d34204a356",
+                     "name": "new_value",
+                     "format": "",
+                     "description": "Status name to change to (the only editable option)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update status",
+               "methodName": "updateStatus",
+               "jsondocId": "1553c8b1-0f66-4c36-80d3-f7968d992897",
+               "bodyobject": {
+                  "jsondocId": "63e777cd-d9dd-4753-a62e-25e3ecd1aa73",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "available status"
+               },
+               "apierrors": [],
+               "path": "/availableStatus/updateStatus",
+               "response": {
+                  "jsondocId": "a49946bc-303b-49dc-98b2-0d636d4b9abf",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "available status"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c415afea-867a-4d66-8183-a9c5ae7f2cd8",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "67f56b7e-1f9c-4324-934b-d58981e113cf",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "37d84f99-952a-4103-8e5d-8a61480bdff3",
+                     "name": "value",
+                     "format": "",
+                     "description": "Status name to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create status",
+               "methodName": "createStatus",
+               "jsondocId": "aa6a4077-8a90-4161-9414-85ac2c863e2c",
+               "bodyobject": {
+                  "jsondocId": "1cf91a61-ad55-4537-8910-484e50588015",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "available status"
+               },
+               "apierrors": [],
+               "path": "/availableStatus/createStatus",
+               "response": {
+                  "jsondocId": "2b315834-5971-4573-a002-17a2df3817af",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "available status"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "0aad4b11-3cc6-48d2-a68d-ff5987858b6a",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5af8c2f1-b033-45d0-8ced-3b245c55f411",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8cb10bf5-1ad9-46bf-ad92-190006e670f6",
+                     "name": "id",
+                     "format": "",
+                     "description": "Status ID to remove (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "01016b40-62dc-406d-826b-328617d58851",
+                     "name": "value",
+                     "format": "",
+                     "description": "Status name to delete",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove a status",
+               "methodName": "deleteStatus",
+               "jsondocId": "0a0a9d48-2199-42b7-8f92-f84d974806d5",
+               "bodyobject": {
+                  "jsondocId": "dfc2d3bb-c065-4bce-95c3-10311f8b7cbe",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "available status"
+               },
+               "apierrors": [],
+               "path": "/availableStatus/deleteStatus",
+               "response": {
+                  "jsondocId": "d675d356-8911-43bd-8dd0-832b9e8c7e3f",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "available status"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e5b458d5-3886-45aa-9938-9253c38cb50f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f1f40ac4-1b88-447e-89cf-a80be4406dee",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6ff80c5b-a120-427e-aded-adf4ce42dbb8",
+                     "name": "id",
+                     "format": "",
+                     "description": "Status ID to show (or specify a name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d3a8d8c2-003a-4690-9d8e-4c16c4f1dcdf",
+                     "name": "name",
+                     "format": "",
+                     "description": "Status name to show",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Returns a JSON array of all statuses, or optionally, gets information about a specific status",
+               "methodName": "showStatus",
+               "jsondocId": "f21791ca-2c73-42e4-bcb6-e72acaed1e6d",
+               "bodyobject": {
+                  "jsondocId": "ec3ec3b2-faef-4bdd-ac71-e557c1cd36bf",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "available status"
+               },
+               "apierrors": [],
+               "path": "/availableStatus/showStatus",
+               "response": {
+                  "jsondocId": "1fd837ab-8359-4408-92f4-00208462412e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "available status"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Available Status Services",
+         "description": "Methods for managing available statuses"
+      },
+      {
+         "jsondocId": "0c7c6d5a-2a9a-4eb7-87dc-fbd4517d11dd",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "691e0246-7949-4268-851a-e0cb05d161d4",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f6bd6cf1-fca8-416f-bc7b-f50ec44141a3",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1e3a4c6c-31f8-420f-86cd-5bc8f5614af5",
+                     "name": "comment",
+                     "format": "",
+                     "description": "Canned comment to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a8ee37ac-d5a4-4375-a99b-055d727d732d",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create canned comment",
+               "methodName": "createComment",
+               "jsondocId": "a9f3f398-ac48-48f8-b2c3-26a7ca720b90",
+               "bodyobject": {
+                  "jsondocId": "757be3c9-1566-4023-954b-f45be9ab1b83",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned comment"
+               },
+               "apierrors": [],
+               "path": "/cannedComment/createComment",
+               "response": {
+                  "jsondocId": "c12d718e-7d79-438f-9585-716686fee683",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned comment"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "a17ba5bf-2a9b-4c2b-b951-2ba282e72894",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "20788ff8-8b51-40c3-abc6-ecb66a179a1c",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "436fb4af-e787-4228-b367-37ddb33aec2a",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned comment ID to update (or specify the old_comment)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "dd70de73-73c3-4403-8605-00e1afa7de89",
+                     "name": "old_comment",
+                     "format": "",
+                     "description": "Canned comment to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c848c8d4-0859-47e0-aa4f-717bdb346976",
+                     "name": "new_comment",
+                     "format": "",
+                     "description": "Canned comment to change to (the only editable option)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "33a4b9d7-f125-4c52-a04d-cfc4739cdc11",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update canned comment",
+               "methodName": "updateComment",
+               "jsondocId": "1de7f4a0-a4bd-49a6-9ca8-846a4cc818e3",
+               "bodyobject": {
+                  "jsondocId": "09be3404-ba75-4c04-99a6-9bb90b138e23",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned comment"
+               },
+               "apierrors": [],
+               "path": "/cannedComment/updateComment",
+               "response": {
+                  "jsondocId": "3831cc31-fd82-4147-a23f-3f2413cd715b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned comment"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c6758ab1-a950-49fa-85e5-73843c293efa",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "34065dd1-0c2b-4f53-bd95-a8a36585c9d8",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f707bc2c-85a7-43db-9c50-8f81ff5fbb8a",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned comment ID to remove (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cc54c3fc-a4d7-4e42-a80a-9c5bd65ca2e5",
+                     "name": "comment",
+                     "format": "",
+                     "description": "Canned comment to delete",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove a canned comment",
+               "methodName": "deleteComment",
+               "jsondocId": "6ada4c1c-4bbb-4abc-add7-f94ce6f89de6",
+               "bodyobject": {
+                  "jsondocId": "67071592-fc8f-449b-9310-b6034505edf5",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned comment"
+               },
+               "apierrors": [],
+               "path": "/cannedComment/deleteComment",
+               "response": {
+                  "jsondocId": "ffa34c63-bbd2-4ee2-8407-e88ac9a7570f",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned comment"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "f0be6003-533a-46af-be7d-85875d553def",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "def7905f-5183-4b63-b598-f44a4470562e",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "105391a4-9dfe-4461-8099-717fa63486d0",
+                     "name": "id",
+                     "format": "",
+                     "description": "Comment ID to show (or specify a comment)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "39ad31c8-b29b-42db-9496-7c3f46bd38d9",
+                     "name": "comment",
+                     "format": "",
+                     "description": "Comment to show",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Returns a JSON array of all canned comments, or optionally, gets information about a specific canned comment",
+               "methodName": "showComment",
+               "jsondocId": "d45b13a2-672a-4c10-8ee7-36befc4dc4ea",
+               "bodyobject": {
+                  "jsondocId": "cf31e2fc-405e-4110-9d21-bcc8a523163e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned comment"
+               },
+               "apierrors": [],
+               "path": "/cannedComment/showComment",
+               "response": {
+                  "jsondocId": "941ee5f0-a395-4755-8a8a-8ef24cebf583",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned comment"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Canned Comments Services",
+         "description": "Methods for managing canned comments"
+      },
+      {
+         "jsondocId": "e89e730c-834b-4961-b862-9451690221f3",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "45ed713f-eb7b-4fba-ac44-76f3f03667f7",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a2c4a28b-d13d-4a5b-9e33-86ce3ac3470b",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a51a9970-6141-4c19-8958-a113696d408e",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned key ID to update (or specify the old_key)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ab302661-3277-47b5-85df-aefe27ccf9cf",
+                     "name": "old_key",
+                     "format": "",
+                     "description": "Canned key to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "819534b2-a7a8-4266-9c32-678c582daff2",
+                     "name": "new_key",
+                     "format": "",
+                     "description": "Canned key to change to (the only editable option)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8e500b39-0c91-4e10-9f28-e47a6c5d820d",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update canned key",
+               "methodName": "updateKey",
+               "jsondocId": "7d5c6730-0fe8-47c5-90a8-ca7e840288f9",
+               "bodyobject": {
+                  "jsondocId": "86995a8a-2226-4f45-a3d8-b8b3f9cff243",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned key"
+               },
+               "apierrors": [],
+               "path": "/cannedKey/updateKey",
+               "response": {
+                  "jsondocId": "cea38a48-cf4c-43c0-9ab8-d8580f3eced7",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned key"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "87578b57-de30-45ad-a416-95a56a65e394",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "fd806323-5069-486f-9d59-866d7a982823",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c6b9377d-d9da-4440-9786-780efcc9d710",
+                     "name": "key",
+                     "format": "",
+                     "description": "Canned key to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ff8f84e6-f492-40de-a557-1541c11bf4c6",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create canned key",
+               "methodName": "createKey",
+               "jsondocId": "fdea057e-ff74-4e6a-a892-b3e65b6522e3",
+               "bodyobject": {
+                  "jsondocId": "9a085982-c9ef-47e7-a431-322f9fb2b52a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned key"
+               },
+               "apierrors": [],
+               "path": "/cannedKey/createKey",
+               "response": {
+                  "jsondocId": "0ac746ff-8fc7-49b9-a93f-f14606496d5c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned key"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "c788403c-3c10-4d7f-abfb-36fb3088750d",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d6523e62-470a-4f43-bcd6-cb3938341882",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "93572b4b-6386-4c3a-a878-b7588b99ea6e",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned key ID to remove (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6c791cb4-36cf-4bb3-bfd0-da391c9f58b4",
+                     "name": "key",
+                     "format": "",
+                     "description": "Canned key to delete",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove a canned key",
+               "methodName": "deleteKey",
+               "jsondocId": "19082cee-8678-4688-8b3c-dc1c3fadf3d3",
+               "bodyobject": {
+                  "jsondocId": "71c7355d-a3e5-4a01-a90e-8642d4723d4a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned key"
+               },
+               "apierrors": [],
+               "path": "/cannedKey/deleteKey",
+               "response": {
+                  "jsondocId": "52d0d7ea-b2f8-4540-bf34-88926f43d65c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned key"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e77e271c-8794-41b2-801b-39b4a079d6a6",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3737cb5f-038f-4f04-bb51-4c2ebfb1e1f6",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e3863724-39d4-4b38-99f8-3b99dcd13bdc",
+                     "name": "id",
+                     "format": "",
+                     "description": "Key ID to show (or specify a key)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f07ca15c-e1c5-4253-bdb9-82a15042367c",
+                     "name": "key",
+                     "format": "",
+                     "description": "Key to show",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Returns a JSON array of all canned keys, or optionally, gets information about a specific canned key",
+               "methodName": "showKey",
+               "jsondocId": "37e152af-43c0-4ec9-9b3e-87965814e924",
+               "bodyobject": {
+                  "jsondocId": "bbb4013d-16d9-4410-adde-13a63c985dde",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned key"
+               },
+               "apierrors": [],
+               "path": "/cannedKey/showKey",
+               "response": {
+                  "jsondocId": "931b01ba-cc5e-4942-a3cb-30cb8f0e15a6",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned key"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Canned Keys Services",
+         "description": "Methods for managing canned keys"
+      },
+      {
+         "jsondocId": "3dcb3f07-c401-4a04-950a-1dab15675f75",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "b1b9dfeb-391e-43cc-b24d-541e8acf44b9",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "deded65c-13a1-4ae3-ab98-0f93220e85ef",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2131d5b7-178a-4274-af37-0517cd8dd35a",
+                     "name": "value",
+                     "format": "",
+                     "description": "Canned value to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2982908b-ca03-4aae-aa87-346b84cff54d",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create canned value",
+               "methodName": "createValue",
+               "jsondocId": "e227f875-bfef-4990-92bc-ff517e0e4a05",
+               "bodyobject": {
+                  "jsondocId": "f28b9380-5dda-4073-8d53-16a45f3e36d2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned value"
+               },
+               "apierrors": [],
+               "path": "/cannedValue/createValue",
+               "response": {
+                  "jsondocId": "df5cdf6e-0152-4ded-85d0-701f49dfa2c4",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned value"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "8d629f57-4b8b-4a04-99f9-f8ef65a9b04e",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9788355c-9782-4bb4-80cd-cb9d9cf83122",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e26374f6-8702-4ba5-a8ae-e1546e13c0ee",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned value ID to update (or specify the old_value)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2ae43563-3793-4188-beec-2edc78af350b",
+                     "name": "old_value",
+                     "format": "",
+                     "description": "Canned value to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "35c0175b-6ee9-49d4-ae2c-095caf7d5f46",
+                     "name": "new_value",
+                     "format": "",
+                     "description": "Canned value to change to (the only editable option)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "94466b10-6735-4276-9194-61c43f17c123",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "Optional additional information",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update canned value",
+               "methodName": "updateValue",
+               "jsondocId": "d3692aed-6448-49b1-a71c-124fc8f620fe",
+               "bodyobject": {
+                  "jsondocId": "011edf61-a1d1-40ba-b07e-43a8dcc8226e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned value"
+               },
+               "apierrors": [],
+               "path": "/cannedValue/updateValue",
+               "response": {
+                  "jsondocId": "cd4ef9b1-bb35-4601-baa2-219102c9fecc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned value"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "5d26b78f-bb63-47cc-92e8-b6ffa441bd7b",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f995434f-1467-4e11-aaec-c2edef25bf6d",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c718eb2d-98b7-4aa6-821a-f92104856faa",
+                     "name": "id",
+                     "format": "",
+                     "description": "Canned value ID to remove (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0d4cfabd-5dfb-4f01-8110-af805e27a696",
+                     "name": "value",
+                     "format": "",
+                     "description": "Canned value to delete",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove a canned value",
+               "methodName": "deleteValue",
+               "jsondocId": "28d92100-e1b5-422d-bafd-3452f75dd661",
+               "bodyobject": {
+                  "jsondocId": "fb0b225f-b469-4c68-8393-85048b6d4393",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned value"
+               },
+               "apierrors": [],
+               "path": "/cannedValue/deleteValue",
+               "response": {
+                  "jsondocId": "145881d3-2510-48b7-a5d9-4290b823fe57",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned value"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "6dc9dea4-caaf-4f51-a415-11303de9b88c",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2e3bfdd9-27d1-4b13-bb7b-205535249a2c",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "87dd8a72-3d7f-4c14-9311-82bbf8fa2d12",
+                     "name": "id",
+                     "format": "",
+                     "description": "Value ID to show (or specify a value)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "eef77796-8135-4084-b7f5-ab4157614330",
+                     "name": "value",
+                     "format": "",
+                     "description": "Value to show",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Returns a JSON array of all canned values, or optionally, gets information about a specific canned value",
+               "methodName": "showValue",
+               "jsondocId": "d24ab784-8f02-42c3-b52e-af6c42a0e1fd",
+               "bodyobject": {
+                  "jsondocId": "19dbab51-1934-4087-9853-9cb02cefbf23",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "canned value"
+               },
+               "apierrors": [],
+               "path": "/cannedValue/showValue",
+               "response": {
+                  "jsondocId": "ca6d4458-fa11-42cf-8ab5-ce37aa2718bd",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "canned value"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Canned Values Services",
+         "description": "Methods for managing canned values"
+      },
+      {
+         "jsondocId": "3108bec5-0879-4a29-84bc-c0fbf33cf7e3",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "76329db8-d855-4cbe-9149-f4f1f8f8ebf7",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "119cb6b3-574b-464e-a4a5-529d9cbef89d",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f1f2007e-638c-44d4-b7d0-e91bfade8d9b",
+                     "name": "name",
+                     "format": "",
+                     "description": "Group name to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create group",
+               "methodName": "createGroup",
+               "jsondocId": "619121e4-cd2d-4238-a32b-48398f32ee79",
+               "bodyobject": {
+                  "jsondocId": "71583a3c-e6a2-4c92-ad24-56b560ceabdb",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/createGroup",
+               "response": {
+                  "jsondocId": "989ea557-b9ed-4370-8330-a94bf654e850",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "4e6f2cb3-f118-4f24-be29-42b8f607c3ec",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f32693bf-9b26-45eb-b0f3-985bae475301",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d3e20e64-30fb-441d-9978-509e3b8fb6e9",
+                     "name": "id",
+                     "format": "",
+                     "description": "Group ID (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "25bee69d-f73b-49b0-a9d4-8ef3d1227e76",
+                     "name": "name",
+                     "format": "",
+                     "description": "Group name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get organism permissions for group",
+               "methodName": "getOrganismPermissionsForGroup",
+               "jsondocId": "7c565a91-a885-49c0-a289-7318fcec8530",
+               "bodyobject": {
+                  "jsondocId": "b096038a-a057-4544-8107-d77cf6c72868",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/getOrganismPermissionsForGroup",
+               "response": {
+                  "jsondocId": "d8682d80-c66b-4dc6-a081-1186934a3191",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "e0c53e4a-809e-4f7f-80d9-7144a07c8fb7",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a0f9087b-ce7b-493a-945a-c0a5764a8142",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8cbb6e1f-6772-4eed-9a99-d3d85a5160ec",
+                     "name": "groupId",
+                     "format": "",
+                     "description": "Optional only load a specific groupId",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Load all groups",
+               "methodName": "loadGroups",
+               "jsondocId": "f9192522-f06e-4477-bd58-9d2708421462",
+               "bodyobject": {
+                  "jsondocId": "cf179122-82e5-4f8c-b899-aa1bd82127c1",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/loadGroups",
+               "response": {
+                  "jsondocId": "473ad97b-e942-4c0f-9c53-a78d90513643",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "d9f267ce-800b-4999-9040-117bc7055c2b",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b2300b5a-5ec2-4db5-be86-a9a76d760e25",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a247e0d1-1d27-4341-80a0-5e99c592852b",
+                     "name": "id",
+                     "format": "",
+                     "description": "Group ID to remove (or specify the name)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "72425372-d266-4b82-9f55-30bc22e3dc8f",
+                     "name": "name",
+                     "format": "",
+                     "description": "Group name to remove",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete a group",
+               "methodName": "deleteGroup",
+               "jsondocId": "b26f2651-d480-4d88-81bb-d1958dac96b0",
+               "bodyobject": {
+                  "jsondocId": "c5a4487b-ac1d-460a-9a54-0e733f33e082",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/deleteGroup",
+               "response": {
+                  "jsondocId": "fc553b47-8be1-4a9d-b75c-39b04f11c28b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "19ed7ed8-32a8-4a7e-b732-180e82194e4f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "852f72ea-02f7-4d45-9934-15c50dd157f3",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7097bb2c-ff14-4cd1-bb56-e3a9edcf4fa9",
+                     "name": "id",
+                     "format": "",
+                     "description": "Group ID to update",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b3089ac3-09c2-4194-b340-6cba06562edb",
+                     "name": "name",
+                     "format": "",
+                     "description": "Group name to change to (the only editable optoin)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update group",
+               "methodName": "updateGroup",
+               "jsondocId": "4919a684-cfcf-4aeb-8bba-a328162a9220",
+               "bodyobject": {
+                  "jsondocId": "753b268d-ab86-4fe9-8b98-d356f5d36025",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/updateGroup",
+               "response": {
+                  "jsondocId": "8fc6cf31-4666-42fd-83ea-42fe53ee42ca",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "f5000d79-76b4-4cda-b7f7-c8dc51edd27a",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "05a55d28-9b28-42b9-8800-6dc02a8af56a",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2cd89299-47f9-4c94-a103-7eef700cbf5a",
+                     "name": "groupId",
+                     "format": "",
+                     "description": "Group ID to modify permissions for (must provide this or 'name')",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4af675bb-a778-4486-8739-90cba8620a82",
+                     "name": "name",
+                     "format": "",
+                     "description": "Group name to modify permissions for (must provide this or 'groupId')",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "93f35f07-6fd3-4947-8491-2f996f6f4f0b",
+                     "name": "organism",
+                     "format": "",
+                     "description": "Organism common name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8b535f02-a107-40ef-bf3e-d5ca6f666ab9",
+                     "name": "ADMINISTRATE",
+                     "format": "",
+                     "description": "Indicate if user has administrative and all lesser (including user/group) privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a0b21041-fbad-489b-b474-a4a68f78d882",
+                     "name": "WRITE",
+                     "format": "",
+                     "description": "Indicate if user has write and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "489f0a1e-1da0-4653-b1cf-cc021928c038",
+                     "name": "EXPORT",
+                     "format": "",
+                     "description": "Indicate if user has export and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "546829dc-5a69-4bc6-9111-6fa26a912341",
+                     "name": "READ",
+                     "format": "",
+                     "description": "Indicate if user has read and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update organism permission",
+               "methodName": "updateOrganismPermission",
+               "jsondocId": "575b29af-ea49-40b4-8389-0ae01b5d5eb7",
+               "bodyobject": {
+                  "jsondocId": "4deff59d-92c2-42d2-89c2-711b4815d119",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/updateOrganismPermission",
+               "response": {
+                  "jsondocId": "89b98142-1029-4562-9ec0-e56d3835320c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "bff13471-8704-4c16-b1b9-5dffc6ea1c4f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a6c38388-98d8-45f6-96fd-27e0085e04e6",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "81943d93-99db-46de-93bb-ca510fdfb032",
+                     "name": "groupId",
+                     "format": "",
+                     "description": "Group ID to alter membership of",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "91fd12c8-412b-4a43-a92c-d74927e677d5",
+                     "name": "users",
+                     "format": "",
+                     "description": "A JSON array of strings of emails of users the now belong to the group",
+                     "type": "JSONArray",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update group membership",
+               "methodName": "updateMembership",
+               "jsondocId": "0dfcd9c6-348f-4c54-aae4-527b52d735d6",
+               "bodyobject": {
+                  "jsondocId": "7afeed11-a19e-44dd-ab4f-0fb2a5df2586",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "group"
+               },
+               "apierrors": [],
+               "path": "/group/updateMembership",
+               "response": {
+                  "jsondocId": "ba3fb233-910f-4ef1-af57-2397cda85eac",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "group"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Group Services",
+         "description": "Methods for managing groups"
+      },
+      {
+         "jsondocId": "b8fe0cda-eefb-428e-bf02-05850ebbf081",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "fda92bc3-b89a-4207-87fa-2b951d811d8d",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1cdb6248-9b53-4b69-be7b-a0308f14131e",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e93f111f-d7af-4fff-ba89-e9c90eb47eee",
+                     "name": "type",
+                     "format": "",
+                     "description": "Type of annotated genomic features to export 'FASTA','GFF3','CHADO'.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0bb82c1f-902a-4418-8ed7-32b2959aaff2",
+                     "name": "seqType",
+                     "format": "",
+                     "description": "Type of output sequence 'peptide','cds','cdna','genomic'.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "46463e2c-b9e5-4b95-9cd8-5f510e5fd213",
+                     "name": "format",
+                     "format": "",
+                     "description": "'gzip' or 'text'",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "36c56f92-4c9f-416b-87b2-c6b88030198f",
+                     "name": "sequences",
+                     "format": "",
+                     "description": "Names of references sequences to add (default is all).",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8504ba66-f4d8-4769-9404-e78ac0d38722",
+                     "name": "organism",
+                     "format": "",
+                     "description": "Name of organism that sequences belong to (will default to last organism).",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "36bf54a0-5e4e-4012-a227-8551f26e2491",
+                     "name": "output",
+                     "format": "",
+                     "description": "Output method 'file','text'",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "57d6db8e-461d-4465-ad5a-643ea1d494c3",
+                     "name": "exportAllSequences",
+                     "format": "",
+                     "description": "Export all reference sequences for an organism (over-rides 'sequences')",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c8d9d9bf-0e76-4178-8360-a54180f235f8",
+                     "name": "exportGff3Fasta",
+                     "format": "",
+                     "description": "Export reference sequence when exporting GFF3 annotations.",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "03c2d45e-7739-46c5-a171-dcc7c87a3ede",
+                     "name": "region",
+                     "format": "",
+                     "description": "Highlighted genomic region to export in form sequence:min..max  e.g., chr3:1001..1034",
+                     "type": "String",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Write out genomic data.  An example script is used in the https://github.com/GMOD/Apollo/blob/master/docs/web_services/examples/groovy/get_gff3.groovy",
+               "methodName": "write",
+               "jsondocId": "2d4cf62b-8765-4eb5-bd54-2d6d051665ec",
+               "bodyobject": {
+                  "jsondocId": "4e01b3a9-1f23-46f1-af99-e821cdc6d849",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "i o service"
+               },
+               "apierrors": [],
+               "path": "/IOService/write",
+               "response": {
+                  "jsondocId": "19d95922-d748-4223-9d9f-8cbd35ba584d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "i o service"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "7354eb40-91ae-4668-8eca-3d8697863411",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6dd0bae4-03c2-42c6-994b-7c6b80f58c17",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "802fd5e2-5c02-43c7-af25-731d33dde11c",
+                     "name": "uuid",
+                     "format": "",
+                     "description": "UUID that holds the key to the stored download.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "768d1575-673a-4769-84e7-91a5b3f48be4",
+                     "name": "format",
+                     "format": "",
+                     "description": "'gzip' or 'text'",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "This is used to retrieve the a download link once the write operation was initialized using output: file.",
+               "methodName": "download",
+               "jsondocId": "d29e8a4b-944d-42fd-a39a-8e5c4452031a",
+               "bodyobject": {
+                  "jsondocId": "ada6ab20-0d8e-4aeb-ab3a-027097a4accc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "i o service"
+               },
+               "apierrors": [],
+               "path": "/IOService/download",
+               "response": {
+                  "jsondocId": "3e25e6be-d41e-40ea-bee5-59503529a7a1",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "i o service"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "IO Services",
+         "description": "Methods for bulk importing and exporting sequence data"
+      },
+      {
+         "jsondocId": "5497abac-8161-4446-8abe-6e09296bd245",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "363b8ca7-1521-4f0b-95d5-153b9096665b",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "40f5f942-a2d9-45e6-af6d-f4d0cdc492ca",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b2c6b187-a195-4b2b-b6cf-079202099670",
+                     "name": "organism",
+                     "format": "",
+                     "description": "Pass an Organism JSON object with an 'id' that corresponds to the organism to be removed",
+                     "type": "json",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove an organism",
+               "methodName": "deleteOrganism",
+               "jsondocId": "b8af2891-2219-4cf0-950c-136dcc1e9182",
+               "bodyobject": {
+                  "jsondocId": "3e555e58-ea02-40e4-80f3-0e7c51d92bb2",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/deleteOrganism",
+               "response": {
+                  "jsondocId": "6e9469eb-a8b9-40d4-af29-d7b35e20adde",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "eed951ea-0886-42a9-a08c-398145e07e3c",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a089a7e0-d39b-46c1-ac80-f19d97c19c44",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e415acfc-0ce5-44d3-a1f8-f59c3febb989",
+                     "name": "organism",
+                     "format": "",
+                     "description": "ID or commonName that can be used to uniquely identify an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete an organism along with its data directory and returns a JSON object containing properties of the deleted organism",
+               "methodName": "deleteOrganismWithSequence",
+               "jsondocId": "d7803e3f-4a67-4791-8dce-ade7873cce2c",
+               "bodyobject": {
+                  "jsondocId": "5da73be4-d019-4dfa-8200-acbd525287fc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/deleteOrganismWithSequence",
+               "response": {
+                  "jsondocId": "0fd9ab25-2774-439a-850a-1625687e7cbb",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "a48f943a-b805-4a21-af94-93282d201b30",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e0a57acb-979c-488c-9840-175763debb11",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "80e0b388-a6ca-4081-bb62-e056400a9b10",
+                     "name": "organism",
+                     "format": "",
+                     "description": "ID or commonName that can be used to uniquely identify an organism.",
+                     "type": "json",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove features from an organism",
+               "methodName": "deleteOrganismFeatures",
+               "jsondocId": "132dc8f3-4926-4da6-af96-6dc0fb507953",
+               "bodyobject": {
+                  "jsondocId": "fcb6d228-bfc7-4f39-8634-81790ef09d40",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/deleteOrganismFeatures",
+               "response": {
+                  "jsondocId": "905f7456-2de8-4fa7-9125-e7963fad114c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "17234212-648c-4ba0-8c30-15f000efa75d",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0f2263d7-6cd9-438d-8275-fe08a8653f1b",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8b525eb5-ec8a-4f0d-8b0b-b91efee3daf6",
+                     "name": "species",
+                     "format": "",
+                     "description": "species name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "adecdabb-a134-4f9b-8c1d-3a9af3ed3ef1",
+                     "name": "genus",
+                     "format": "",
+                     "description": "species genus",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "142300f9-8367-4305-9729-f01e050f5b19",
+                     "name": "blatdb",
+                     "format": "",
+                     "description": "filesystem path for a BLAT database (e.g. a .2bit file)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0d3f8915-f255-4a1e-96d2-65ebe33b9c6c",
+                     "name": "publicMode",
+                     "format": "",
+                     "description": "a flag for whether the organism appears as in the public genomes list",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "88c830e1-a998-4453-ade7-647b659ea9d5",
+                     "name": "commonName",
+                     "format": "",
+                     "description": "commonName for an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "552b3477-22bc-4071-b17c-8bf2b73d7bc8",
+                     "name": "nonDefaultTranslationTable",
+                     "format": "",
+                     "description": "non-default translation table",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e6bc2a03-f20b-4649-aee5-8f8f55c78ac5",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "organism metadata",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3e78c775-19ce-4d9c-904d-4dce1d7a5ae4",
+                     "name": "organismData",
+                     "format": "",
+                     "description": "zip or tar.gz compressed data directory",
+                     "type": "file",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Adds an organism returning a JSON array of all organisms",
+               "methodName": "addOrganismWithSequence",
+               "jsondocId": "3872b383-3d40-4629-af2e-c6d331950396",
+               "bodyobject": {
+                  "jsondocId": "ef7f0a78-7d0b-4897-b0fa-4b5959827cda",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/addOrganismWithSequence",
+               "response": {
+                  "jsondocId": "443cbaa1-2688-44cf-82c6-9f4344e62d7d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "13a61b6a-f5e9-412e-9024-c8837a4ed589",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2c380db6-56fa-40a2-be7a-59e69e87b3b9",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "565e3907-c244-4f48-bd70-86f6f9e4722a",
+                     "name": "organism",
+                     "format": "",
+                     "description": "ID or commonName that can be used to uniquely identify an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1f0d7744-c06a-4280-a604-5f9cf80bb9a1",
+                     "name": "trackData",
+                     "format": "",
+                     "description": "zip or tar.gz compressed track data",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9ad709b3-ba36-4040-bf8f-ccc99595cb50",
+                     "name": "trackFile",
+                     "format": "",
+                     "description": "track file (*.bam, *.vcf, *.bw)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e54e2899-3913-4fb9-b195-ae9a0e536f23",
+                     "name": "trackFileIndex",
+                     "format": "",
+                     "description": "index (*.bai, *.tbi)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4fb69aa8-6599-43ed-90a8-e8a6608dd2a3",
+                     "name": "trackConfig",
+                     "format": "",
+                     "description": "Track configuration (JBrowse JSON)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Adds a track to an existing organism returning a JSON object containing all tracks for the current organism.",
+               "methodName": "addTrackToOrganism",
+               "jsondocId": "5462fe6d-5971-4c76-b6f7-459eb7a93972",
+               "bodyobject": {
+                  "jsondocId": "081c2257-1768-4842-95f0-484ca636398d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/addTrackToOrganism",
+               "response": {
+                  "jsondocId": "81381bf9-95da-4d68-bce5-683a70128739",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "610a25f0-c86e-4bab-b1d9-6910e24db598",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "787c3a45-f9ca-4a01-a99b-944522c2f97b",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b08c545b-b7df-4295-8f8c-060840d6c470",
+                     "name": "organism",
+                     "format": "",
+                     "description": "ID or commonName that can be used to uniquely identify an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "750f57a1-fe3b-4598-891a-37b7665a45a1",
+                     "name": "trackLabel",
+                     "format": "",
+                     "description": "Track label corresponding to the track that is to be deleted",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Deletes a track from an existing organism and returns a JSON object of the deleted track's configuration",
+               "methodName": "deleteTrackFromOrganism",
+               "jsondocId": "73b582c9-0e36-4019-855e-102d058c6a70",
+               "bodyobject": {
+                  "jsondocId": "3fc9f4f6-f470-4eb6-abda-a34c335869c4",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/deleteTrackFromOrganism",
+               "response": {
+                  "jsondocId": "50796a76-88c8-457d-8d84-f7bb9cac0b7d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "903e4ac8-92d3-4f49-9612-0bf4a600ee4f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2808e288-7b99-4ab1-a85c-92b0659a5320",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0d292581-7731-4264-89cd-d16daf4ac94a",
+                     "name": "organism",
+                     "format": "",
+                     "description": "ID or commonName that can be used to uniquely identify an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "904e1405-1617-4d15-9f26-9a87666c9a58",
+                     "name": "trackConfig",
+                     "format": "",
+                     "description": "Track configuration (JBrowse JSON)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update a track in an existing organism returning a JSON object containing old and new track configurations",
+               "methodName": "updateTrackForOrganism",
+               "jsondocId": "1b015408-647f-4d29-ac49-15a028b4b15c",
+               "bodyobject": {
+                  "jsondocId": "30049161-b89a-41f0-96d3-5f4194879d70",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/updateTrackForOrganism",
+               "response": {
+                  "jsondocId": "9843d00e-ae2a-4570-ad09-36a719781fae",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "ee210d44-76a6-41f7-955a-1bf3e951a513",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "416bd7bc-a488-4dfb-9265-1f1f078b5328",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "4f97865a-cbec-4f1b-be87-18b730d01663",
+                     "name": "directory",
+                     "format": "",
+                     "description": "Filesystem path for the organisms data directory (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3bf099a6-15bf-49bd-a3ef-0b155163e84e",
+                     "name": "commonName",
+                     "format": "",
+                     "description": "A name used for the organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "fbd34057-90cd-4086-8eae-28394e557a5d",
+                     "name": "species",
+                     "format": "",
+                     "description": "(optional) Species name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8f4d3e4a-d19b-47b7-87d8-9c7980780996",
+                     "name": "genus",
+                     "format": "",
+                     "description": "(optional) Species genus",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "326f73f3-6456-4a71-9e86-7185347f12d6",
+                     "name": "blatdb",
+                     "format": "",
+                     "description": "(optional) Filesystem path for a BLAT database (e.g. a .2bit file)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "dd2db88a-2b9f-4196-8625-2decb1170194",
+                     "name": "publicMode",
+                     "format": "",
+                     "description": "(optional) A flag for whether the organism appears as in the public genomes list (default false)",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a00d10b6-d94a-4a3d-aefd-2dfe0565760b",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "(optional) Organism metadata",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3a17d846-7b30-4dcb-ac0a-dd2efd313a15",
+                     "name": "returnAllOrganisms",
+                     "format": "",
+                     "description": "(optional) Return all organisms (true / false) (default true)",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Adds an organism returning a JSON array of all organisms",
+               "methodName": "addOrganism",
+               "jsondocId": "5a57344a-ce08-4631-b5b9-ecfaa5cf74ab",
+               "bodyobject": {
+                  "jsondocId": "066284da-6347-43ed-891a-7a8783551662",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/addOrganism",
+               "response": {
+                  "jsondocId": "997d5ac2-b216-4021-b26e-f9cb21b92f4e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "0f4348cd-57cb-41cf-a812-db41aa224d5e",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2457d53f-5683-46fa-9ba9-5b5eb6c5c490",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "97e2f040-45d5-48c0-9dd6-04ef1943323d",
+                     "name": "organism",
+                     "format": "",
+                     "description": "Common name or ID for the organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Finds sequences for a given organism and returns a JSON object including the username, organism and a JSONArray of sequences",
+               "methodName": "getSequencesForOrganism",
+               "jsondocId": "86699905-4197-4cf7-a5a1-08cbf3a41154",
+               "bodyobject": {
+                  "jsondocId": "29d4d1e5-3783-453a-a322-745478acf091",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/getSequencesForOrganism",
+               "response": {
+                  "jsondocId": "36a3a8de-e623-4382-895c-9c323d35128b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "43561dbe-5e0c-4ff3-a81f-7b486ee741ac",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "711b5ec6-08d6-4efa-b21f-85bd5a56b16a",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "12805920-a729-43c6-80ae-f46f0124fde5",
+                     "name": "id",
+                     "format": "",
+                     "description": "unique id of organism to change",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d5fb7d33-358d-4acd-86b5-acee49c2f591",
+                     "name": "directory",
+                     "format": "",
+                     "description": "filesystem path for the organisms data directory (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "66c9f6cb-046b-4ac2-9fff-caa3b43aa71d",
+                     "name": "species",
+                     "format": "",
+                     "description": "species name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f735772f-9fc9-4939-a555-9745f2853058",
+                     "name": "genus",
+                     "format": "",
+                     "description": "species genus",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "242198a6-9440-450d-9d19-78cfd235715c",
+                     "name": "blatdb",
+                     "format": "",
+                     "description": "filesystem path for a BLAT database (e.g. a .2bit file)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7b441e94-f1bf-4a2b-ad04-fd8fe0e10cb9",
+                     "name": "publicMode",
+                     "format": "",
+                     "description": "a flag for whether the organism appears as in the public genomes list",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f1450b7d-6a6e-4768-b415-b9a7a7d1bea9",
+                     "name": "name",
+                     "format": "",
+                     "description": "a common name used for the organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2a041b3b-554a-404e-ab6e-9babf06c36d1",
+                     "name": "nonDefaultTranslationTable",
+                     "format": "",
+                     "description": "non-default translation table",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3471b763-2cd7-43d3-9917-d6d66802197b",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "organism metadata",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Adds an organism returning a JSON array of all organisms",
+               "methodName": "updateOrganismInfo",
+               "jsondocId": "cf601c3e-a5fe-44d2-8fb2-35e6df12fdd8",
+               "bodyobject": {
+                  "jsondocId": "c0f9e8df-84d9-46f7-800f-55ee3e291e14",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/updateOrganismInfo",
+               "response": {
+                  "jsondocId": "50bd6a34-afc9-4d5c-b5b7-f076cb601084",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "19c7b2ef-faae-4976-9252-15f05e7c107a",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f9aac6be-695b-4710-8d85-525093baf3a8",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f49a7473-25ce-4cf1-ab1b-15fee9895cd2",
+                     "name": "id",
+                     "format": "",
+                     "description": "unique id of organism to change",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b9127d95-656a-4696-9707-9e6c83226e42",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "organism metadata",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update organism metadata",
+               "methodName": "updateOrganismMetadata",
+               "jsondocId": "3607e58e-5917-4a10-acdb-589c6f84f740",
+               "bodyobject": {
+                  "jsondocId": "a8697378-1147-48df-90cb-0455e025d72a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/updateOrganismMetadata",
+               "response": {
+                  "jsondocId": "a2b6b159-6440-43eb-8d37-2c390df398ca",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "bfc667e4-bd3c-45b1-a061-6c7ed79a6e32",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "657608b2-52f5-400b-901f-4fc80a957fc8",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "f8f8973a-99a2-444c-a6d3-d98777dc09ca",
+                     "name": "organism",
+                     "format": "",
+                     "description": "(optional) ID or commonName that can be used to uniquely identify an organism",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Returns a JSON array of all organisms, or optionally, gets information about a specific organism",
+               "methodName": "findAllOrganisms",
+               "jsondocId": "46ff16d0-3034-44a4-9c66-3a8704bf7ac1",
+               "bodyobject": {
+                  "jsondocId": "9704c2ef-ad2f-4cef-a04e-3744713c8d62",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "organism"
+               },
+               "apierrors": [],
+               "path": "/organism/findAllOrganisms",
+               "response": {
+                  "jsondocId": "54611519-d14c-45f0-96fa-b803e57c8e65",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "organism"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "Organism Services",
+         "description": "Methods for managing organisms"
+      },
+      {
+         "jsondocId": "e4416744-26ff-4651-bd49-9821421ffb18",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "d7e38460-a67a-4983-89c7-b1bdf492ef68",
+                     "name": "organismString",
+                     "format": "",
+                     "description": "Organism common name or ID(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "71c59fea-301d-4d13-a4eb-44405a1bdb61",
+                     "name": "sequenceName",
+                     "format": "",
+                     "description": "Sequence name(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "5a8e3c46-066b-4f00-a9c0-5fd6c8512f38",
+                     "name": "fmin",
+                     "format": "",
+                     "description": "Minimum range(required)",
+                     "type": "integer",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "65fb9e7c-7dc7-4f57-8d93-c9e93b358d0b",
+                     "name": "fmax",
+                     "format": "",
+                     "description": "Maximum range (required)",
+                     "type": "integer",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a1d168de-d329-4404-88aa-20c80d97b3b3",
+                     "name": "ignoreCache",
+                     "format": "",
+                     "description": "(default false).  Use cache for request if available.",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Get sequence data within a range",
+               "methodName": "sequenceByLocation",
+               "jsondocId": "eecb72c1-60e1-42c7-8bc7-6095cb60ade5",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/sequence/<organism name>/<sequence name>:<fmin>..<fmax>?ignoreCache=<ignoreCache>",
+               "response": {
+                  "jsondocId": "bade745b-7eeb-44a0-8cc6-7e2462d039d0",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "sequence"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "8685d8dd-861e-4dbc-bf2a-40b0e515b74e",
+                     "name": "organismString",
+                     "format": "",
+                     "description": "Organism common name or ID (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6ddcbf21-1781-493d-8f52-5fd8cfef3e54",
+                     "name": "sequenceName",
+                     "format": "",
+                     "description": "Sequence name (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1d72d941-082e-4064-a994-f9e23d4108c2",
+                     "name": "featureName",
+                     "format": "",
+                     "description": "The uniqueName (UUID) or given name of the feature (typically transcript) of the element to retrieve sequence from",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9858e5a3-130f-4775-8b76-81d93a4ac49b",
+                     "name": "type",
+                     "format": "",
+                     "description": "(default genomic) Return type: genomic, cds, cdna, peptide",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b00ab53a-e9e3-4174-90fc-242c5767def8",
+                     "name": "ignoreCache",
+                     "format": "",
+                     "description": "(default false).  Use cache for request if available.",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Get sequence data as for a selected name",
+               "methodName": "sequenceByName",
+               "jsondocId": "42ec6f09-1bfe-489f-8c25-b73eef0c7ce6",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/sequence/<organism name>/<sequence name>/<feature name>.<type>?ignoreCache=<ignoreCache>",
+               "response": {
+                  "jsondocId": "51babfd9-ceb4-4e47-8d07-dcdee30e4464",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "sequence"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "724197ab-eccb-4760-824c-1da87bd0922b",
+                     "name": "organismName",
+                     "format": "",
+                     "description": "Organism common name (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a1c69e13-9d31-415a-a666-3fb243775de2",
+                     "name": "sequenceName",
+                     "format": "",
+                     "description": "Sequence name (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Remove sequence cache for an organism and sequence",
+               "methodName": "clearSequenceCache",
+               "jsondocId": "adaa90c3-0e0e-4f36-b594-f3f17718b7b4",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/sequence/cache/clear/<organism name>/<sequence name>",
+               "response": {
+                  "jsondocId": "8919c1e4-c56e-4b19-88dd-d4f0e5e6eb33",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "sequence"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [{
+                  "jsondocId": "f7a87cf0-fe8f-4d53-8652-58300c2b2be0",
+                  "name": "organismName",
+                  "format": "",
+                  "description": "Organism common name (required) or 'ALL' if admin",
+                  "type": "string",
+                  "required": "true",
+                  "allowedvalues": []
+               }],
+               "verb": "GET",
+               "description": "Remove sequence cache for an organism",
+               "methodName": "clearOrganismCache",
+               "jsondocId": "7882265b-68ca-4503-96b7-0a5a48fbf110",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/sequence/cache/clear/<organism name>",
+               "response": {
+                  "jsondocId": "9cd689e4-2028-45d8-9641-edbd690dd423",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "sequence"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            }
+         ],
+         "name": "Sequence Services",
+         "description": "Methods for retrieving sequence data"
+      },
+      {
+         "jsondocId": "15a995cb-f256-4b7c-9328-80c814042dc5",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [{
+                  "jsondocId": "7b676d53-c908-4edc-a409-b77bde985290",
+                  "name": "organismName",
+                  "format": "",
+                  "description": "Organism common name (required) or 'ALL' if admin",
+                  "type": "string",
+                  "required": "true",
+                  "allowedvalues": []
+               }],
+               "verb": "GET",
+               "description": "Remove track cache for an organism",
+               "methodName": "clearOrganismCache",
+               "jsondocId": "d1a9cd4f-4f51-42ff-a4ac-fd8dbda26429",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/track/cache/clear/<organism name>",
+               "response": {
+                  "jsondocId": "69627b82-40f8-4e3a-b6c5-3e75f12392bc",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "track"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "d4c2804b-7953-4356-87bd-fe95aa93ec2d",
+                     "name": "organismName",
+                     "format": "",
+                     "description": "Organism common name (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "134e4f10-7929-454a-be1a-018ebb35dc7f",
+                     "name": "trackName",
+                     "format": "",
+                     "description": "Track name (required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Remove track cache for an organism and track",
+               "methodName": "clearTrackCache",
+               "jsondocId": "ba3e94fd-9366-4da5-953a-334ccf223dbc",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/track/cache/clear/<organism name>/<track name>",
+               "response": {
+                  "jsondocId": "48c65494-67dc-4065-beb4-a01403f01546",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "track"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "70992cd0-c7e4-406d-9cf0-678a7b68d22e",
+                     "name": "organismString",
+                     "format": "",
+                     "description": "Organism common name or ID(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "541fa418-733e-4105-993c-67c665287922",
+                     "name": "trackName",
+                     "format": "",
+                     "description": "Track name(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ffa0d394-c5ad-4ab1-af7a-35ce3200d7d6",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "Sequence name(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "fbdb1ace-f5c4-4f80-b0fe-c96819766a68",
+                     "name": "featureName",
+                     "format": "",
+                     "description": "If top-level feature 'id' matches, then annotate with 'selected'=1",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "712a9af8-da4b-4c52-a49c-d3c1663b1808",
+                     "name": "ignoreCache",
+                     "format": "",
+                     "description": "(default false).  Use cache for request if available.",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "03b4c472-16e8-49e8-8329-4b465219bd91",
+                     "name": "flatten",
+                     "format": "",
+                     "description": "Brings nested top-level components to the root level.  If not provided or 'false' it will not flatten.  Default is 'gene'.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a280fc46-0a4f-4cd8-8882-69dc66b3dae2",
+                     "name": "type",
+                     "format": "",
+                     "description": ".json or .svg",
+                     "type": "json/svg",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Get track data as an JSON within but only for the selected name",
+               "methodName": "featuresByName",
+               "jsondocId": "b9acfb79-996d-44a9-af1b-a7086ca55277",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/track/<organism name>/<track name>/<sequence name>/<feature name>.<type>?ignoreCache=<ignoreCache>",
+               "response": {
+                  "jsondocId": "c6bd56b1-3db0-4d0a-9314-4224a71d23bd",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "track"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "44577443-9c7b-4513-80c4-4e56b25bc9d4",
+                     "name": "organismString",
+                     "format": "",
+                     "description": "Organism common name or ID(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0032a475-2dc0-474f-b8f2-bc67e8ac7c98",
+                     "name": "trackName",
+                     "format": "",
+                     "description": "Track name(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3750c3ed-3de2-46a0-89f1-2ca41f768264",
+                     "name": "sequence",
+                     "format": "",
+                     "description": "Sequence name(required)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3af51d2c-a3bb-444f-bde1-a40a602cf2ba",
+                     "name": "fmin",
+                     "format": "",
+                     "description": "Minimum range(required)",
+                     "type": "integer",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3528f244-0e37-4cae-b566-bbf16b10987f",
+                     "name": "fmax",
+                     "format": "",
+                     "description": "Maximum range (required)",
+                     "type": "integer",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ff32ac1d-69fb-410d-82aa-91edb7cb5d19",
+                     "name": "name",
+                     "format": "",
+                     "description": "If top-level feature 'name' matches, then annotate with 'selected'=true.  Multiple names can be passed in.",
+                     "type": "string / string[]",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "288024d6-5233-491c-a534-109df86fdce7",
+                     "name": "onlySelected",
+                     "format": "",
+                     "description": "(default false).  If 'selected'!=1 one, then exclude.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "963e7f2e-62f8-43ae-a883-7d5b7af956a3",
+                     "name": "ignoreCache",
+                     "format": "",
+                     "description": "(default false).  Use cache for request if available.",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8c8e191a-ad42-4a2e-8393-aaaa177228a4",
+                     "name": "flatten",
+                     "format": "",
+                     "description": "Brings nested top-level components to the root level.  If not provided or 'false' it will not flatten.  Default is 'gene'.",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b8de4a5e-5689-439d-998e-daf02c866c3d",
+                     "name": "type",
+                     "format": "",
+                     "description": ".json or .svg",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "GET",
+               "description": "Get track data as an JSON within an range",
+               "methodName": "featuresByLocation",
+               "jsondocId": "b9f65006-cf44-495a-976e-5e23afde2ad8",
+               "bodyobject": null,
+               "apierrors": [],
+               "path": "/track/<organism name>/<track name>/<sequence name>:<fmin>..<fmax>.<type>?name=<name>&onlySelected=<onlySelected>&ignoreCache=<ignoreCache>",
+               "response": {
+                  "jsondocId": "3816dcab-ca75-4fce-adce-6299860c0b21",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "track"
+               },
+               "produces": ["application/json"],
+               "consumes": []
+            }
+         ],
+         "name": "Track Services",
+         "description": "Methods for retrieving track data"
+      },
+      {
+         "jsondocId": "141c4632-4df9-4673-b0a4-169704d66b69",
+         "methods": [
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "de411df8-b062-4e68-b925-bc782c6bf493",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "45ef9cf9-54df-4226-bcd8-86892f6003a3",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b6ff6a51-9946-47ea-95c5-24c7379d68b2",
+                     "name": "userId",
+                     "format": "",
+                     "description": "Optionally only user a specific userId as an integer database id or a username string",
+                     "type": "long / string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0ef1b649-0bd9-4655-8b54-ec0fd77cd685",
+                     "name": "start",
+                     "format": "",
+                     "description": "(optional) Result start / offset",
+                     "type": "long / string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b1cf3045-7327-46f0-9bc4-9810304910a0",
+                     "name": "length",
+                     "format": "",
+                     "description": "(optional) Result length",
+                     "type": "long / string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "9c0c185f-cccf-4211-8b59-8256ac3bd1bd",
+                     "name": "name",
+                     "format": "",
+                     "description": "(optional) Search name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "6695b154-99b5-4e15-86dd-4ee186efdf11",
+                     "name": "sortColumn",
+                     "format": "",
+                     "description": "(optional) Sort column, default 'name'",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b9673e15-f8e6-4c89-9b5d-38a5d1d33c4f",
+                     "name": "sortAscending",
+                     "format": "",
+                     "description": "(optional) Sort column is ascending if true (default false)",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "2e56e1ba-e4c0-49e3-ab3a-78f8450c75a8",
+                     "name": "omitEmptyOrganisms",
+                     "format": "",
+                     "description": "(optional) Omits empty organism permissions from return (default false)",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Load all users and their permissions",
+               "methodName": "loadUsers",
+               "jsondocId": "fcd41968-6692-4e53-a096-7ef9b4b287ef",
+               "bodyobject": {
+                  "jsondocId": "830a5445-6bcf-4be6-a967-d9c671ef2083",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/loadUsers",
+               "response": {
+                  "jsondocId": "63415d51-b1a8-401d-a320-0298f68845fb",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "13a58492-95a4-4bbb-a8e0-7cec1cfc76dc",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "babf8740-5cb1-4fc8-83c9-fa4a7be06f30",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1f7dc184-b4ec-46f4-a154-f7377dc8f8a5",
+                     "name": "group",
+                     "format": "",
+                     "description": "Group name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "cbcf9d59-58d8-445d-87d5-ecbdd04e2aff",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User id",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a39926be-8679-4613-a0fb-40c9706fb641",
+                     "name": "user",
+                     "format": "",
+                     "description": "User email/username (supplied if user id unknown)",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Add user to group",
+               "methodName": "addUserToGroup",
+               "jsondocId": "c134d125-ca7a-4602-9a6b-51d2ec71088f",
+               "bodyobject": {
+                  "jsondocId": "2ad37ca2-d2ab-4737-b43f-6ae62c45a85b",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/addUserToGroup",
+               "response": {
+                  "jsondocId": "ba38ea51-d88c-49ac-bec3-607c394c588d",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "6e742668-5c28-47a4-83a4-ee3aa0b401bc",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ad301683-0627-4d43-a172-6a58953a1892",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c1b61668-1910-4376-ab14-bddc29761be9",
+                     "name": "group",
+                     "format": "",
+                     "description": "Group name",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "74dbd3d7-4761-4d44-a946-591f31da33a9",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User id",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "a61221cb-7484-4679-8b0a-61c3403cb7b5",
+                     "name": "user",
+                     "format": "",
+                     "description": "User email/username (supplied if user id unknown)",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Remove user from group",
+               "methodName": "removeUserFromGroup",
+               "jsondocId": "92ad1fb5-e0d8-41b8-8536-4038dece4eff",
+               "bodyobject": {
+                  "jsondocId": "faa830fe-8366-4f49-88e7-f6f411dfa77f",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/removeUserFromGroup",
+               "response": {
+                  "jsondocId": "b7534b98-5beb-459f-ba41-f62f362a9098",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "671f19b7-e249-43d4-b383-6b12158620b9",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "7fc5f089-7dd0-4325-803e-632eb9289813",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "374ee991-ed6b-44e1-9dde-bf00952213ed",
+                     "name": "email",
+                     "format": "",
+                     "description": "Email of the user to add",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "314552d9-cc90-4577-947a-c35bd4811b96",
+                     "name": "firstName",
+                     "format": "",
+                     "description": "First name of user to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "023f9f14-4979-4621-840c-6304570c71a4",
+                     "name": "lastName",
+                     "format": "",
+                     "description": "Last name of user to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "744b27f2-a34e-4faf-b6e8-10691430ea45",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "User metadata (optional)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b144d888-422d-4542-b12c-482ec0387d6f",
+                     "name": "role",
+                     "format": "",
+                     "description": "User role USER / ADMIN (optional: default USER) ",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1e3a8c2b-7b18-4286-9a12-7aac77fcd75a",
+                     "name": "newPassword",
+                     "format": "",
+                     "description": "Password of user to add",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Create user",
+               "methodName": "createUser",
+               "jsondocId": "21e3dc9f-5029-4357-89b5-8d9fc7405164",
+               "bodyobject": {
+                  "jsondocId": "7811cf82-daf6-4e5f-b185-39c620245b4c",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/createUser",
+               "response": {
+                  "jsondocId": "9c2ee224-f831-45ce-9c90-33109a3b820e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "3ef77f27-5155-4b45-a148-3029a9b005d7",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1fa77247-a6e3-4040-8179-72150a81bec5",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ca594667-fee6-4d2a-bdf9-8e427acb77f1",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User ID to delete",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0e814bdc-8f4f-4034-924f-d29d9959e6f6",
+                     "name": "userToDelete",
+                     "format": "",
+                     "description": "Username (email) to delete",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Delete user",
+               "methodName": "deleteUser",
+               "jsondocId": "9cf32fcd-7ce9-49af-b147-803993490124",
+               "bodyobject": {
+                  "jsondocId": "49c83991-108c-4268-b82a-36816f200a77",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/deleteUser",
+               "response": {
+                  "jsondocId": "5106bd00-f43f-4093-9c5a-ed9fec2a69d3",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "cf2fb07f-930d-407b-9eb4-eb609d4e3b5f",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b9c71b82-1de9-467a-b7e1-260affdf75f8",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d09afaa5-591a-48a7-ac8b-48632a575aec",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User ID to update",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ed4decb3-8f41-446f-8929-c3c45d257a87",
+                     "name": "email",
+                     "format": "",
+                     "description": "Email of the user to update",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "188419e9-5ab0-41d6-ab7d-9c51e9ea4d49",
+                     "name": "firstName",
+                     "format": "",
+                     "description": "First name of user to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d2d0687a-48c0-4394-8125-575868672161",
+                     "name": "lastName",
+                     "format": "",
+                     "description": "Last name of user to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "1d7b8c97-4fdb-4700-96e1-ce7e046d4d13",
+                     "name": "metadata",
+                     "format": "",
+                     "description": "User metadata (optional)",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "0feba106-e29e-4aaf-be84-c98d0deaf1d0",
+                     "name": "newPassword",
+                     "format": "",
+                     "description": "Password of user to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update user",
+               "methodName": "updateUser",
+               "jsondocId": "7ace71c9-4adf-4e2d-90c5-1eaec9df2090",
+               "bodyobject": {
+                  "jsondocId": "6c65fc0e-37e7-43e4-9fcf-4f8cf5d802fb",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/updateUser",
+               "response": {
+                  "jsondocId": "1886483c-982a-4104-8d98-236624c4199e",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "7e14f9e2-5c5f-4926-bf45-d91bd7278966",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "8403283a-c822-45d4-8258-ad7fdc2db857",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "d3556bc2-0e2e-436e-b38e-b3367702a8bc",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User ID to modify permissions for",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b8733c0c-9891-42a0-a3d8-cca7a77e33d3",
+                     "name": "user",
+                     "format": "",
+                     "description": "(Optional) user email of the user to modify permissions for if User ID is not provided",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "3b62f757-f36f-4d7e-bf9d-5fe202f82584",
+                     "name": "organism",
+                     "format": "",
+                     "description": "Name of organism to update",
+                     "type": "string",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "c26dc5b8-5349-4936-81f1-4b001ff7aaf5",
+                     "name": "id",
+                     "format": "",
+                     "description": "Permission ID to update (can get from userId/organism instead)",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "ac1769bd-cb2c-4ed6-b7b2-ab875bf56d23",
+                     "name": "ADMINISTRATE",
+                     "format": "",
+                     "description": "Indicate if user has administrative and all lesser (including user/group) privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "b37f789f-e8ec-4e69-a6f3-5329fda629b8",
+                     "name": "WRITE",
+                     "format": "",
+                     "description": "Indicate if user has write and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "187456ff-6754-4619-81f9-0ec16463d6e3",
+                     "name": "EXPORT",
+                     "format": "",
+                     "description": "Indicate if user has export and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "62315e5d-5a00-455d-b132-0be8d786b8a0",
+                     "name": "READ",
+                     "format": "",
+                     "description": "Indicate if user has read and all lesser privileges for the organism",
+                     "type": "boolean",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Update organism permissions",
+               "methodName": "updateOrganismPermission",
+               "jsondocId": "8ae4fe99-74a2-4517-8bef-75cba96a8e3c",
+               "bodyobject": {
+                  "jsondocId": "2ae1c2cf-b238-4b5a-8d7e-5e6e2b40d904",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/updateOrganismPermission",
+               "response": {
+                  "jsondocId": "f5972ac1-ada3-459c-a8af-eb2bbb3c264a",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            },
+            {
+               "headers": [],
+               "pathparameters": [],
+               "queryparameters": [
+                  {
+                     "jsondocId": "bf0ce9fb-2f4f-42fb-8a51-9a571935c520",
+                     "name": "username",
+                     "format": "",
+                     "description": "",
+                     "type": "email",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "33f2962e-6a3c-4574-9599-8b7370d3ab61",
+                     "name": "password",
+                     "format": "",
+                     "description": "",
+                     "type": "password",
+                     "required": "true",
+                     "allowedvalues": []
+                  },
+                  {
+                     "jsondocId": "e69a1b93-cdfa-4fac-9f1e-64bfca9de00f",
+                     "name": "userId",
+                     "format": "",
+                     "description": "User ID to fetch",
+                     "type": "long",
+                     "required": "true",
+                     "allowedvalues": []
+                  }
+               ],
+               "verb": "POST",
+               "description": "Get organism permissions for user, returns an array of permission objects",
+               "methodName": "getOrganismPermissionsForUser",
+               "jsondocId": "f48229f2-93be-4efe-88f1-a938ce7378b2",
+               "bodyobject": {
+                  "jsondocId": "9abab683-2f37-438d-883e-75470b6b0297",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "multiple": "Unknow",
+                  "map": "",
+                  "object": "user"
+               },
+               "apierrors": [],
+               "path": "/user/getOrganismPermissionsForUser",
+               "response": {
+                  "jsondocId": "03dae59d-3249-4a50-92d8-22b18b6abeba",
+                  "mapValueObject": "",
+                  "mapKeyObject": "",
+                  "object": "user"
+               },
+               "produces": ["application/json"],
+               "consumes": ["application/json"]
+            }
+         ],
+         "name": "User Services",
+         "description": "Methods for managing users"
+      }
+   ],
+   "objects": [],
+   "version": "0.1.1"
+}
\ No newline at end of file
diff --git a/web-app/translation_tables/ncbi_10_translation_table.txt b/web-app/translation_tables/ncbi_10_translation_table.txt
new file mode 100644
index 0000000..9447341
--- /dev/null
+++ b/web-app/translation_tables/ncbi_10_translation_table.txt
@@ -0,0 +1 @@
+TGA	C
diff --git a/web-app/translation_tables/ncbi_11_translation_table.txt b/web-app/translation_tables/ncbi_11_translation_table.txt
new file mode 100644
index 0000000..b7cd94c
--- /dev/null
+++ b/web-app/translation_tables/ncbi_11_translation_table.txt
@@ -0,0 +1,6 @@
+TTG	L	start
+CTG	L	start
+ATT	I	start
+ATC	I	start
+ATA	I	start
+GTG	V	start
diff --git a/web-app/translation_tables/ncbi_12_translation_table.txt b/web-app/translation_tables/ncbi_12_translation_table.txt
new file mode 100644
index 0000000..37d8e82
--- /dev/null
+++ b/web-app/translation_tables/ncbi_12_translation_table.txt
@@ -0,0 +1 @@
+CTG	S	start
diff --git a/web-app/translation_tables/ncbi_13_translation_table.txt b/web-app/translation_tables/ncbi_13_translation_table.txt
new file mode 100644
index 0000000..efb91ad
--- /dev/null
+++ b/web-app/translation_tables/ncbi_13_translation_table.txt
@@ -0,0 +1,6 @@
+AGA	G
+AGG	G
+ATA	M	start
+TGA	W
+TTG	L	start
+GTG	V	start
diff --git a/web-app/translation_tables/ncbi_14_translation_table.txt b/web-app/translation_tables/ncbi_14_translation_table.txt
new file mode 100644
index 0000000..735e4b8
--- /dev/null
+++ b/web-app/translation_tables/ncbi_14_translation_table.txt
@@ -0,0 +1,5 @@
+AAA	N
+AGA	S
+AGG	S
+TAA	Y
+TGA	W
diff --git a/web-app/translation_tables/ncbi_15_translation_table.txt b/web-app/translation_tables/ncbi_15_translation_table.txt
new file mode 100644
index 0000000..a42f89e
--- /dev/null
+++ b/web-app/translation_tables/ncbi_15_translation_table.txt
@@ -0,0 +1 @@
+TAG	Q
diff --git a/web-app/translation_tables/ncbi_16_translation_table.txt b/web-app/translation_tables/ncbi_16_translation_table.txt
new file mode 100644
index 0000000..39368d0
--- /dev/null
+++ b/web-app/translation_tables/ncbi_16_translation_table.txt
@@ -0,0 +1 @@
+TAG	L
diff --git a/web-app/translation_tables/ncbi_1_translation_table.txt b/web-app/translation_tables/ncbi_1_translation_table.txt
new file mode 100644
index 0000000..e69de29
diff --git a/web-app/translation_tables/ncbi_21_translation_table.txt b/web-app/translation_tables/ncbi_21_translation_table.txt
new file mode 100644
index 0000000..20be0cc
--- /dev/null
+++ b/web-app/translation_tables/ncbi_21_translation_table.txt
@@ -0,0 +1,6 @@
+TGA	W
+ATA	M
+AGA	S
+AGG	S
+AAA	N
+GTG	V	start
diff --git a/web-app/translation_tables/ncbi_22_translation_table.txt b/web-app/translation_tables/ncbi_22_translation_table.txt
new file mode 100644
index 0000000..0cebd24
--- /dev/null
+++ b/web-app/translation_tables/ncbi_22_translation_table.txt
@@ -0,0 +1,2 @@
+TCA	*
+TAG	L
diff --git a/web-app/translation_tables/ncbi_23_translation_table.txt b/web-app/translation_tables/ncbi_23_translation_table.txt
new file mode 100644
index 0000000..f055e5e
--- /dev/null
+++ b/web-app/translation_tables/ncbi_23_translation_table.txt
@@ -0,0 +1,3 @@
+ATT	I	start
+GTG	V	start
+TTA	*
diff --git a/web-app/translation_tables/ncbi_24_translation_table.txt b/web-app/translation_tables/ncbi_24_translation_table.txt
new file mode 100644
index 0000000..9536ed9
--- /dev/null
+++ b/web-app/translation_tables/ncbi_24_translation_table.txt
@@ -0,0 +1,6 @@
+AGA	S
+AGG	K
+TGA	W
+TTG	L	start
+CTG	L	start
+GTG	V	start
diff --git a/web-app/translation_tables/ncbi_25_translation_table.txt b/web-app/translation_tables/ncbi_25_translation_table.txt
new file mode 100644
index 0000000..5e11cab
--- /dev/null
+++ b/web-app/translation_tables/ncbi_25_translation_table.txt
@@ -0,0 +1,3 @@
+TGA	G
+TTG	L	start
+GTG	L	start
diff --git a/web-app/translation_tables/ncbi_2_translation_table.txt b/web-app/translation_tables/ncbi_2_translation_table.txt
new file mode 100644
index 0000000..a5339c0
--- /dev/null
+++ b/web-app/translation_tables/ncbi_2_translation_table.txt
@@ -0,0 +1,4 @@
+AGA	*
+AGG	*
+ATA	M start
+TGA	W
diff --git a/web-app/translation_tables/ncbi_3_translation_table.txt b/web-app/translation_tables/ncbi_3_translation_table.txt
new file mode 100644
index 0000000..78e0fb7
--- /dev/null
+++ b/web-app/translation_tables/ncbi_3_translation_table.txt
@@ -0,0 +1,8 @@
+ATA	M	start
+CTT	T
+CTC	T
+CTA	T
+CTG	T
+TGA	W
+CGA	X
+CGC	X
diff --git a/web-app/translation_tables/ncbi_4_translation_table.txt b/web-app/translation_tables/ncbi_4_translation_table.txt
new file mode 100644
index 0000000..a3f75a8
--- /dev/null
+++ b/web-app/translation_tables/ncbi_4_translation_table.txt
@@ -0,0 +1 @@
+TGA	W
diff --git a/web-app/translation_tables/ncbi_5_translation_table.txt b/web-app/translation_tables/ncbi_5_translation_table.txt
new file mode 100644
index 0000000..73578da
--- /dev/null
+++ b/web-app/translation_tables/ncbi_5_translation_table.txt
@@ -0,0 +1,5 @@
+AGA	S
+AGG	S
+ATA	M	start
+TGA	W
+ATT	I	start
diff --git a/web-app/translation_tables/ncbi_6_translation_table.txt b/web-app/translation_tables/ncbi_6_translation_table.txt
new file mode 100644
index 0000000..cbbede4
--- /dev/null
+++ b/web-app/translation_tables/ncbi_6_translation_table.txt
@@ -0,0 +1,2 @@
+TAA	Q
+TAG	Q
diff --git a/web-app/translation_tables/ncbi_9_translation_table.txt b/web-app/translation_tables/ncbi_9_translation_table.txt
new file mode 100644
index 0000000..563e4ed
--- /dev/null
+++ b/web-app/translation_tables/ncbi_9_translation_table.txt
@@ -0,0 +1,5 @@
+AAA	N
+AGA	S
+AGG	S
+TGA	W
+GTG	V	start
diff --git a/wrapper/grails-wrapper-runtime-2.5.5.jar b/wrapper/grails-wrapper-runtime-2.5.5.jar
new file mode 100644
index 0000000..64c897e
Binary files /dev/null and b/wrapper/grails-wrapper-runtime-2.5.5.jar differ
diff --git a/wrapper/grails-wrapper.properties b/wrapper/grails-wrapper.properties
new file mode 100644
index 0000000..82d01c4
--- /dev/null
+++ b/wrapper/grails-wrapper.properties
@@ -0,0 +1 @@
+wrapper.dist.url=https://github.com/grails/grails-core/releases/download/v2.5.5/

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/apollo.git



More information about the debian-med-commit mailing list